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: `` 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 `
"}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("").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
` 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 \n"
"Language-Team: Team LifterLMS \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 @@
+ 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;r1&&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({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(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 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=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;he.length)&&(t=e.length);for(var n=0,r=new Array(t);ne?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=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=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=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=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;t0?Ce(Ne,--Le):0,Te--,10===Ae&&(Te=1,Re--),Ae}function Ue(){return Ae=Le2||$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;m0?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;o6)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;r0&&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-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=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;ie.length)&&(t=e.length);for(var n=0,r=new Array(t);n0,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;n5||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-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&&o0&&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-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;i1?n-1:0),o=1;o3&&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;t0&&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;i0&&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{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{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(re>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:ee&&n<=o?{x:0,y:-l.height-s,..._s}: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;t1&&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=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;t0&&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;r0&&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;t0&&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;t1&&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:''.concat(a,"")});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 __( '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 @@
+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(
+ '
+ 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 @@
+ $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 @@
+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 @@
+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 = '
' . __( 'Welcome to the LifterLMS Helper', 'lifterlms' ) . '
';
+ $text .= '
' . __( 'This plugin allows your website to interact with your subscriptions at LifterLMS.com to ensure your add-ons stay up to date.', 'lifterlms' ) . '
' . 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'] ) ) . ':
';
+ }
+ }
+ }
+ }
+
+ 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 @@
+ $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 @@
+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 @@
+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 '';
+
+ echo '
';
+ _e( 'Your LifterLMS add-on is currently unlicensed and cannot be updated!', 'lifterlms' );
+ echo '
';
+
+ echo '
';
+ // 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' ), '', '' );
+ echo '
';
+
+ echo '
';
+ // 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 '
';
+
+ }
+
+ }
+
+ /**
+ * 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' => '' . $addon->get( 'author' )['name'] . '',
+ '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. 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 .= '
+
+
+
+
+
+
+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 @@
+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 @@
+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[\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 @@
+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 @@
+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 @@
+ '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 @@
+ 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 @@
+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 @@
+ '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 ? '' : '' . __( 'Add API Key', 'lifterlms' ) . '',
+ );
+
+ 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' => ' ' . __( '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' ),
+ '',
+ ''
+ ),
+ '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' => ' ' . 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' ),
+ '',
+ ''
+ ),
+ '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' => '
' . __( 'Make sure to copy or download the consumer key and consumer secret. After leaving this page they will not be displayed again.', 'lifterlms' ) . '