diff --git a/.distignore b/.distignore deleted file mode 100644 index 0eb76a6e10c..00000000000 --- a/.distignore +++ /dev/null @@ -1,28 +0,0 @@ -.* -.*/ -*.lock -*.md -*.zip -/bin/ -/build/ -/node_modules/ -/tests/ -babel.config.js -changelog.txt -composer.* -tsconfig.* -contributors.html -docker-compose.yaml -Dockerfile -Gruntfile.js -lerna.json -none -package-lock.json -package.json -packages/woocommerce-admin/docs -phpcs.xml -phpunit.xml -phpunit.xml.dist -README.md -renovate.json -webpack.config.js diff --git a/.editorconfig b/.editorconfig index c3dfa83750f..7b68c1c4230 100644 --- a/.editorconfig +++ b/.editorconfig @@ -22,3 +22,6 @@ trim_trailing_whitespace = false trim_trailing_whitespace = false indent_style = space indent_size = 2 + +[*.json] +indent_style = tab diff --git a/.eslintignore b/.eslintignore index f44a9c08503..b512c09d476 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,21 +1 @@ -*.min.js - -/assets/js/accounting/** -/assets/js/flexslider/** -/assets/js/jquery-blockui/** -/assets/js/jquery-cookie/** -/assets/js/jquery-flot/** -/assets/js/jquery-payment/** -/assets/js/jquery-qrcode/** -/assets/js/jquery-serializejson/** -/assets/js/jquery-tiptip/** -/assets/js/jquery-ui-touch-punch/** -/assets/js/js-cookie/** -/assets/js/photoswipe/** -/assets/js/prettyPhoto/** -/assets/js/round/** -/assets/js/select2/** -/assets/js/selectWoo/** -/assets/js/stupidtable/** -/assets/js/zeroclipboard/** -/assets/js/zoom/** \ No newline at end of file +node_modules \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 21e6da05647..1be390543e6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,33 +1,4 @@ -/** @format */ - -const { useE2EEsLintConfig } = require( './tests/e2e/env/config/use-config' ); - -module.exports = useE2EEsLintConfig( { +module.exports = { root: true, - env: { - browser: true, - es6: true, - node: true - }, - globals: { - wp: true, - wpApiSettings: true, - wcSettings: true, - es6: true - }, - rules: { - camelcase: 0, - indent: 0, - 'max-len': [ 2, { 'code': 140 } ], - 'no-console': 1 - }, - parser: 'babel-eslint', - parserOptions: { - ecmaVersion: 8, - ecmaFeatures: { - modules: true, - experimentalObjectRestSpread: true, - jsx: true - } - }, -} ); + extends: [ 'plugin:@woocommerce/eslint-plugin/recommended' ], +}; diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 418f7248ec5..3e3b424196e 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -13,7 +13,7 @@ There are many ways to contribute to the project! If you wish to contribute code, please read the information in the sections below. Then [fork](https://help.github.com/articles/fork-a-repo/) WooCommerce, commit your changes, and [submit a pull request](https://help.github.com/articles/using-pull-requests/) 🎉 -We use the `good first issue` label to mark issues that are suitable for new contributors. You can find all the issues with this label [here](https://github.com/woocommerce/woocommerce/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). +We use the `good first issue` label to mark issues that are suitable for new contributors. You can find all the issues with this label [here](https://github.com/woocommerce/woocommerce/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+good+first+issue%22). WooCommerce is licensed under the GPLv3+, and all contributions to the project will be released under the same license. You maintain copyright over any contribution you make, and by submitting a pull request, you are agreeing to release that contribution under the GPLv3+ license. @@ -26,8 +26,8 @@ If you have questions about the process to contribute code or want to discuss de - [Minification of SCSS and JS](https://github.com/woocommerce/woocommerce/wiki/Minification-of-SCSS-and-JS) - [Naming conventions](https://github.com/woocommerce/woocommerce/wiki/Naming-conventions) - [String localisation guidelines](https://github.com/woocommerce/woocommerce/wiki/String-localisation-guidelines) -- [Running unit tests](https://github.com/woocommerce/woocommerce/blob/master/tests/README.md) -- [Running e2e tests](https://github.com/woocommerce/woocommerce/wiki/End-to-end-Testing) +- [Running unit tests](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/README.md) +- [Running e2e tests](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/e2e/README.md) ## Coding Guidelines and Development 🛠 @@ -37,19 +37,18 @@ If you have questions about the process to contribute code or want to discuss de - Ensure you use LF line endings in your code editor. Use [EditorConfig](http://editorconfig.org/) if your editor supports it so that indentation, line endings and other settings are auto configured. - When committing, reference your issue number (#1234) and include a note about the fix. - Ensure that your code supports the minimum supported versions of PHP and WordPress; this is shown at the top of the `readme.txt` file. -- Push the changes to your fork and submit a pull request on the master branch of the WooCommerce repository. +- Push the changes to your fork and submit a pull request on the trunk branch of the WooCommerce repository. - Make sure to write good and detailed commit messages (see [this post](https://chris.beams.io/posts/git-commit/) for more on this) and follow all the applicable sections of the pull request template. - Please avoid modifying the changelog directly or updating the .pot files. These will be updated by the WooCommerce team. -If you are contributing code to the (Javascript-driven) WooCommerce Admin project or to Gutenberg blocks, note that these are developed in external packages. +If you are contributing code to the (Javascript-driven) Gutenberg blocks, note that it's developed in an external package. -- [WooCommerce Admin](https://github.com/woocommerce/woocommerce-admin) - [Blocks](https://github.com/woocommerce/woocommerce-gutenberg-products-block) ## Feature Requests 🚀 -Feature requests can be [submitted to our issue tracker](https://github.com/woocommerce/woocommerce/issues/new?template=6-Feature-request.md). Be sure to include a description of the expected behavior and use case, and before submitting a request, please search for similar ones in the closed issues. +Feature requests can be [submitted to our issue tracker](https://github.com/woocommerce/woocommerce/issues/new?assignees=&labels=type%3A+enhancement%2Cstatus%3A+awaiting+triage&template=2-enhancement.yml&title=%5BEnhancement%5D%3A+). Be sure to include a description of the expected behavior and use case, and before submitting a request, please search for similar ones in the closed issues. Feature request issues will remain closed until we see sufficient interest via comments and [👍 reactions](https://help.github.com/articles/about-discussions-in-issues-and-pull-requests/) from the community. -You can see a [list of current feature requests which require votes here](https://github.com/woocommerce/woocommerce/issues?q=is%3Aclosed+label%3A%22type%3A+enhancement%22+label%3A%22votes+needed%22+sort%3Areactions-%2B1-desc). +You can see a [list of current feature requests which require votes here](https://github.com/woocommerce/woocommerce/issues?q=is%3Aissue+sort%3Areactions-%2B1-desc+label%3A%22needs%3A+votes%22+). diff --git a/.github/ISSUE_TEMPLATE/1-Security-issue.md b/.github/ISSUE_TEMPLATE/1-Security-issue.md deleted file mode 100644 index 7c78be16976..00000000000 --- a/.github/ISSUE_TEMPLATE/1-Security-issue.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: "\U0001F512 Security issue" -about: Please report security issues *only* via https://www.hackerone.com -title: '' -labels: '' -assignees: '' - ---- - -For security reasons, please report all security issues via https://hackerone.com/automattic/. Also, if the issue is valid, a bug bounty will be paid out to you. - -Please disclose responsibly and not via GitHub (which allows for exploiting issues in the wild before the patch is released). diff --git a/.github/ISSUE_TEMPLATE/1-bug-report.yml b/.github/ISSUE_TEMPLATE/1-bug-report.yml new file mode 100644 index 00000000000..69cb4e1c960 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-bug-report.yml @@ -0,0 +1,95 @@ +name: 🐞 Bug Report +description: Report a bug if something isn't working as expected in WooCommerce Core. +body: + - type: markdown + attributes: + value: | + ### Thanks for contributing! + + Please provide us with the information requested in this bug report. + Without these details, we won't be able to fully evaluate this issue. + Bug reports lacking detail, or for any other reason than to report a bug, may be closed without action. + + While our goal is to address all the issues reported in this repository, GitHub should be treated as a place to report confirmed bugs only. + - If you have a support request or custom code related question please follow one of the steps below: + - Review [WooCommerce Self-Service Guide](https://woocommerce.com/document/woocommerce-self-service-guide/) to see if the solutions listed there apply to your case; + - If you are a paying customer of WooCommerce, contact WooCommerce support by [opening a ticket or starting a live chat](https://woocommerce.com/contact-us/); + - Make a post on [WooCommerce community forum](https://wordpress.org/support/plugin/woocommerce/) + - To get help on custom code questions go to the [WooCommerce Community Slack](https://woocommerce.com/community-slack/) and visit the `#developers` channel. + + Make sure to look through the [existing `type: bug` issues](https://github.com/woocommerce/woocommerce/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+bug%22) to see whether your bug has already been submitted. + Feel free to contribute to any existing issues. + Search tip: You can filter our issues using [our labels](https://github.com/woocommerce/woocommerce/labels). + Search tip: Make use of [GitHub's search syntax to refine your search](https://help.github.com/en/github/searching-for-information-on-github/searching-issues-and-pull-requests). + - type: checkboxes + id: prerequisites + attributes: + label: Prerequisites + description: Please confirm these before submitting the issue. + options: + - label: I have carried out troubleshooting steps and I believe I have found a bug. + - label: I have searched for similar bugs in both open and closed issues and cannot find a duplicate. + validations: + required: true + - type: textarea + id: summary + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + placeholder: | + A clear and concise description of what you expected to happen. + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + placeholder: | + A clear and concise description of what actually happens. Please be as descriptive as possible; + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: Attach screenshot(s) or recording(s) directly by dragging & dropping. + placeholder: | + 1. Go to + 2. Click on + 3. Scroll down to + 4. See error + validations: + required: true + - type: textarea + id: environment + attributes: + label: WordPress Environment + description: | + We use the [WooCommerce System Status Report](https://woocommerce.com/document/understanding-the-woocommerce-system-status-report/) to help us evaluate the issue. + Without this report we won't be able to fully evaluate this issue. + placeholder: | + The System Status Report is found in your WordPress admin under **WooCommerce > Status**. + Please select “Get system report”, then “Copy for support”, and then paste it here. + validations: + required: true + - type: checkboxes + id: isolating + attributes: + label: Isolating the problem + description: | + Please try testing your site for theme and plugins conflict. + To do that deactivate all plugins except for WooCommerce and switch to a default WordPress theme or [Storefront](https://en-gb.wordpress.org/themes/storefront/). Then test again. + If the issue is resolved with the default theme and all plugins deactivated, it means that one of your plugins or a theme is causing the issue. + You will then need to enable it one by one and test every time you do that in order to figure out which plugin is causing the issue. + options: + - label: I have deactivated other plugins and confirmed this bug occurs when only WooCommerce plugin is active. + - label: This bug happens with a default WordPress theme active, or [Storefront](https://woocommerce.com/storefront/). + - label: I can reproduce this bug consistently using the steps above. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/2-External-issues.md b/.github/ISSUE_TEMPLATE/2-External-issues.md deleted file mode 100644 index 1a0543b7e98..00000000000 --- a/.github/ISSUE_TEMPLATE/2-External-issues.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: "\U0001F47D External issues" -about: Please report WooCommerce Admin, WooCommerce Gutenberg Products Blocks or Action Scheduler issues directly to their respective repositories. -title: '' -labels: '' -assignees: '' - ---- - -Please report issues for the following features directly to their respective repositories. - -WooCommerce Admin: https://github.com/woocommerce/woocommerce-admin - -WooCommerce Gutenberg Products Blocks: https://github.com/woocommerce/woocommerce-gutenberg-products-block - -Action Scheduler: https://github.com/woocommerce/action-scheduler - -WooCommerce REST API Docs: https://github.com/woocommerce/woocommerce-rest-api-docs - -WooCommerce Code Reference: https://github.com/woocommerce/code-reference diff --git a/.github/ISSUE_TEMPLATE/2-enhancement.yml b/.github/ISSUE_TEMPLATE/2-enhancement.yml new file mode 100644 index 00000000000..3c7637cea8c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-enhancement.yml @@ -0,0 +1,33 @@ +name: ✨ Enhancement Request +description: If you have an idea to improve an existing feature in core or need something for development (such as a new hook) please let us know or submit a Pull Request! +title: "[Enhancement]: " +labels: ["type: enhancement", "status: awaiting triage"] +body: + - type: markdown + attributes: + value: | + ### Thanks for contributing! + + Please provide us with the information requested in this form. + + Make sure to look through [existing `type: enhancement` issues](https://github.com/woocommerce/woocommerce/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+enhancement%22) and [existing `votes needed` issues](https://github.com/woocommerce/woocommerce/issues?q=is%3Aissue+sort%3Areactions-%2B1-desc+label%3A%22needs%3A+votes%22+) to see whether your idea is already being discussed. + Feel free to contribute to any existing issues. + Search tip: You can filter our issues using [our enhancement label](https://github.com/woocommerce/woocommerce/issues?q=is%3Aissue+label%3A%22type%3A+enhancement%22+). + Search tip: Make use of [GitHub's search syntax to refine your search](https://help.github.com/en/github/searching-for-information-on-github/searching-issues-and-pull-requests). + - type: textarea + id: summary + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + id: alternative + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/3-Support.md b/.github/ISSUE_TEMPLATE/3-Support.md deleted file mode 100644 index 3ae796e2030..00000000000 --- a/.github/ISSUE_TEMPLATE/3-Support.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: "❓ Support Question" -about: "If you have a question \U0001F4AC please see our docs or use our forums, helpdesk, - or Slack Community!" -title: '' -labels: '' -assignees: '' - ---- - -We don't offer technical support on GitHub so we recommend using the following: - -**Reading our documentation** -Usage docs can be found here: https://docs.woocommerce.com/ - -If you have a problem, you may want to start with the self help guide here: https://docs.woocommerce.com/document/woocommerce-self-service-guide/ - -**Technical support for premium extensions or if you're a WooCommerce.com customer** -Contact WooCommerce support by opening a ticket. -https://woocommerce.com/contact-us/ - -**For help with custom code** -WooCommerce Slack Community: https://woocommerce.com/community-slack/ in the `#developers` channel. - -**General usage and development questions** -- WooCommerce Slack Community: https://woocommerce.com/community-slack/ -- WordPress.org Forums: https://wordpress.org/support/plugin/woocommerce -- The Official WooCommerce Facebook Group https://www.facebook.com/groups/advanced.woocommerce/ diff --git a/.github/ISSUE_TEMPLATE/4-Bug-report.md b/.github/ISSUE_TEMPLATE/4-Bug-report.md deleted file mode 100644 index c0316763647..00000000000 --- a/.github/ISSUE_TEMPLATE/4-Bug-report.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -name: "\U0001F41E Bug report" -about: Report a bug if something isn't working as expected in the core WooCommerce - plugin. -title: '' -labels: '' -assignees: '' - ---- - -Please provide us with the information requested in this bug report. Without these details, we won't be able to fully evaluate this issue. -Bug reports lacking detail, or for any other reason than to report a bug, may be closed without action. - - - - - - - -**Prerequisites (mark completed items with an [x]):** -- [ ] I have have carried out troubleshooting steps and I believe I have found a bug. -- [ ] I have searched for similar bugs in both open and closed issues and cannot find a duplicate. - -**Describe the bug** -A clear and concise description of what the bug is. - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Actual behavior** -A clear and concise description of what actually happens. Please be as descriptive as possible; - -**Steps to reproduce the bug (We need to be able to reproduce the bug in order to fix it.)** -Steps to reproduce the bug: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Screenshots** -If applicable, add screenshots to help explain your problem. - - - -**Isolating the problem (mark completed items with an [x]):** -- [ ] I have deactivated other plugins and confirmed this bug occurs when only WooCommerce plugin is active. -- [ ] This bug happens with a default WordPress theme active, or [Storefront](https://woocommerce.com/storefront/). -- [ ] I can reproduce this bug consistently using the steps above. - -**WordPress Environment** -We use the [WooCommerce System Status Report](https://docs.woocommerce.com/document/understanding-the-woocommerce-system-status-report/) to help us evaluate the issue. -Without this report we won't be able to fully evaluate this issue. -
-``` -The System Status Report is found in your WordPress admin under **WooCommerce > Status**. -Please select “Get system report”, then “Copy for support”, and then paste it here. -``` -
diff --git a/.github/ISSUE_TEMPLATE/5-Enhancement.md b/.github/ISSUE_TEMPLATE/5-Enhancement.md deleted file mode 100644 index b616509eedd..00000000000 --- a/.github/ISSUE_TEMPLATE/5-Enhancement.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: "✨ New Enhancement" -about: If you have an idea to improve an existing feature in core or need something - for development (such as a new hook) please let us know or submit a Pull Request! -title: '' -labels: '' -assignees: '' - ---- - - - - - -**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. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/6-Feature-request.md b/.github/ISSUE_TEMPLATE/6-Feature-request.md deleted file mode 100644 index e20ce518c65..00000000000 --- a/.github/ISSUE_TEMPLATE/6-Feature-request.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: "\U0001F680 Feature request" -about: "Suggest a new feature \U0001F389 We'll consider building it if it receives - sufficient interest! \U0001F44D" -title: '' -labels: '' -assignees: '' - ---- - - - - - -**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. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..dae2134fb39 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: true +contact_links: + - name: 🔒 Security issue + url: https://hackerone.com/automattic/ + about: For security reasons, please report all security issues via HackerOne. If the issue is valid, a bug bounty will be paid out to you. Please disclose responsibly and not via GitHub (which allows for exploiting issues in the wild before the patch is released). + - name: ❓ Support Question + url: https://woocommerce.com/document/woocommerce-self-service-guide/ + about: If you have a question please see our docs or use our forums, helpdesk, or Slack community! + - name: WooCommerce Blocks + url: https://github.com/woocommerce/woocommerce-gutenberg-products-block + about: Please report issues for WooCommerce Blocks directly to it's repository. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c194fb6beb0..dd08989d3c3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ ### All Submissions: -* [ ] Have you followed the [WooCommerce Contributing guideline](https://github.com/woocommerce/woocommerce/blob/master/.github/CONTRIBUTING.md)? +* [ ] Have you followed the [WooCommerce Contributing guideline](https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md)? * [ ] Does your code follow the [WordPress' coding standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/)? * [ ] Have you checked to ensure there aren't other open [Pull Requests](../../pulls) for the same update/change? @@ -25,9 +25,10 @@ Closes # . * [ ] Have you added an explanation of what your changes do and why you'd like us to include them? * [ ] Have you written new tests for your changes, as applicable? * [ ] Have you successfully run tests with your changes locally? +* [ ] Have you created a changelog file by running `pnpm nx affected --target=changelog`? -### Changelog entry +### FOR PR REVIEWER ONLY: -> Enter a summary of all changes on this Pull Request. This will appear in the changelog if accepted. +* [ ] I have reviewed that everything is sanitized/escaped appropriately for any SQL or XSS injection possibilities. I made sure Linting is not ignored or disabled. diff --git a/.github/project-pr-labeler.yml b/.github/project-pr-labeler.yml new file mode 100644 index 00000000000..6303a123f00 --- /dev/null +++ b/.github/project-pr-labeler.yml @@ -0,0 +1,76 @@ +'package: @woocommerce/api': +- packages/js/api/**/* + +'package: @woocommerce/e2e-utils': +- packages/js/e2e-utils/**/* + +'package: @woocommerce/e2e-environment': +- packages/js/e2e-environment/**/* + +'package: @woocommerce/api-core-tests': +- packages/js/api-core-tests/**/* + +'package: @woocommerce/e2e-core-tests': +- packages/js/e2e-core-tests/**/* + +'package: @woocommerce/admin-e2e-tests': +- packages/js/admin-e2e-tests/**/* + +'package: @woocommerce/components': +- packages/js/components/**/* + +'package: @woocommerce/csv-export': +- packages/js/csv-export/**/* + +'package: @woocommerce/currency': +- packages/js/currency/**/* + +'package: @woocommerce/customer-effort-score': +- packages/js/customer-effort-score/**/* + +'package: @woocommerce/data': +- packages/js/data/**/* + +'package: @woocommerce/date': +- packages/js/date/**/* + +'package: dependency-extraction-webpack-plugin': +- packages/js/dependency-extraction-webpack-plugin/**/* + +'package: @woocommerce/eslint-plugin': +- packages/js/eslint-plugin/**/* + +'package: @woocommerce/experimental': +- packages/js/experimental/**/* + +'package: @woocommerce/explat': +- packages/js/explat/**/* + +'package: @woocommerce/js-tests': +- packages/js/js-tests/**/* + +'package: @woocommerce/navigation': +- packages/js/navigation/**/* + +'package: @woocommerce/notices': +- packages/js/notices/**/* + +'package: @woocommerce/number': +- packages/js/number/**/* + +'package: @woocommerce/onboarding': +- packages/js/onboarding/**/* + +'package: @woocommerce/style-build': +- packages/js/style-build/**/* + +'package: @woocommerce/tracks': +- packages/js/tracks/**/* + +'plugin: woocommerce': +- plugins/woocommerce/**/* + +'focus: react admin': +- plugins/woocommerce/src/Admin/**/* +- plugins/woocommerce/src/Internal/Admin/**/* +- plugins/woocommerce-admin/**/* diff --git a/.github/workflows/build-release-zip-file.yml b/.github/workflows/build-release-zip-file.yml new file mode 100644 index 00000000000..4d221f45cb0 --- /dev/null +++ b/.github/workflows/build-release-zip-file.yml @@ -0,0 +1,30 @@ +name: Build release zip file +on: + workflow_dispatch: + inputs: + ref: + description: 'By default the zip file is generated from the branch the workflow runs from, but you can specify an explicit reference to use instead here (e.g. refs/tags/tag_name or refs/heads/release/x.x). The resulting file will be available as an artifact on the workflow run.' + required: false + default: '' +jobs: + build: + name: Build release zip file + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + ref: ${{ github.event.inputs.ref || github.ref }} + - name: Build the zip file + id: build + uses: woocommerce/action-build@trunk + - name: Unzip the file (prevents double zip problem) + run: unzip ${{ steps.build.outputs.zip_path }} -d zipfile + - name: Upload the zip file as an artifact + uses: actions/upload-artifact@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + name: woocommerce + path: zipfile + retention-days: 7 diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index e5f93efa41c..356ae9da4ce 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -8,10 +8,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Build id: build - uses: woocommerce/action-build@v2 + uses: woocommerce/action-build@trunk - name: Upload release asset uses: actions/upload-release-asset@v1 env: @@ -21,3 +21,30 @@ jobs: asset_path: ${{ steps.build.outputs.zip_path }} asset_name: woocommerce.zip asset_content_type: application/zip + update-code-reference: + if: github.event.release.prerelease == false && github.event.release.draft == false && github.repository_owner == 'woocommerce' + name: Update Code Reference + needs: build + runs-on: ubuntu-latest + steps: + - name: Invoke Code Reference build and deploy workflow + uses: aurelien-baudet/workflow-dispatch@v2 + with: + workflow: GitHub Pages deploy + repo: woocommerce/code-reference + token: ${{ secrets.CUSTOM_GH_TOKEN }} + ref: refs/heads/trunk + inputs: '{ "version": "${{ github.event.release.tag_name }}" }' + run-release-smoke-tests: + name: Execute Smoke test release + needs: build + runs-on: ubuntu-latest + steps: + - name: Invoke release smoke testing workflow + uses: aurelien-baudet/workflow-dispatch@v2 + with: + workflow: Smoke test release + repo: ${{ github.repository }} + token: ${{ secrets.E2E_WORKFLOW_GH_TOKEN }} + ref: refs/heads/trunk + inputs: '{ "release_id": "${{ github.event.release.id }}" }' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000000..5c5ddb4a4e2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,97 @@ +name: Run CI +on: + push: + branches: + - trunk + - 'release/**' +defaults: + run: + shell: bash +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + test: + name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} + timeout-minutes: 20 + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.wp == 'nightly' }} + strategy: + fail-fast: false + matrix: + php: [ '7.2', '7.3', '7.4', '8.0' ] + wp: [ 'latest' ] + include: + - wp: nightly + php: '7.4' + - wp: '5.8' + php: 7.2 + - wp: '5.7' + php: 7.2 + services: + database: + image: mysql:5.6 + env: + MYSQL_ROOT_PASSWORD: root + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer + extensions: mysql + coverage: none + + - name: Tool versions + run: | + php --version + composer --version + + - name: Get cached composer and pnpm directories + uses: actions/cache@v3 + id: cache-deps + with: + path: | + ~/.pnpm-store + plugins/woocommerce/packages + plugins/woocommerce/vendor + key: ${{ runner.os }}-npm-composer-${{ hashFiles('plugins/woocommerce/composer.lock', '**/pnpm-lock.yaml') }} + + - name: Install PNPM + run: npm install -g pnpm + + - name: Install dependencies + run: pnpm install + + - name: Install Composer dependencies + if: steps.cache-deps.outputs.cache-hit != 'true' + run: pnpm nx composer-install woocommerce + + - name: Build Admin feature config + run: pnpm nx build:feature-config woocommerce + + - name: Add PHP8 Compatibility. + run: | + if [ "$(php -r "echo version_compare(PHP_VERSION,'8.0','>=');")" ]; then + curl -L https://github.com/woocommerce/phpunit/archive/add-compatibility-with-php8-to-phpunit-7.zip -o /tmp/phpunit-7.5-fork.zip + unzip -d /tmp/phpunit-7.5-fork /tmp/phpunit-7.5-fork.zip + cd plugins/woocommerce + composer bin phpunit config --unset platform + composer bin phpunit config repositories.0 '{"type": "path", "url": "/tmp/phpunit-7.5-fork/phpunit-add-compatibility-with-php8-to-phpunit-7", "options": {"symlink": false}}' + composer bin phpunit require --dev -W phpunit/phpunit:@dev --ignore-platform-reqs + rm -rf ./vendor/phpunit/ + pnpm nx composer-dump-autoload woocommerce + fi + + - name: Init DB and WP + working-directory: plugins/woocommerce + run: ./tests/bin/install.sh woo_test root root 127.0.0.1 ${{ matrix.wp }} + + - name: Run tests + run: pnpm nx test-unit woocommerce diff --git a/.github/workflows/mirrors.yml b/.github/workflows/mirrors.yml new file mode 100644 index 00000000000..514b14b179f --- /dev/null +++ b/.github/workflows/mirrors.yml @@ -0,0 +1,64 @@ +name: Mirrors +on: + push: + branches: ["trunk", "release/**"] +jobs: + build: + if: github.repository == 'woocommerce/woocommerce' + name: Build WooCommerce zip + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Build + id: build + uses: woocommerce/action-build@trunk + env: + BUILD_ENV: mirrors + - name: Upload PR zip + uses: actions/upload-artifact@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + name: woocommerce + path: ${{ steps.build.outputs.zip_path }} + retention-days: 7 + mirror: + if: github.repository == 'woocommerce/woocommerce' + name: Push to Mirror + runs-on: ubuntu-latest + needs: [build] + steps: + - name: Create directories + run: | + mkdir -p tmp/woocommerce-build + mkdir -p monorepo + - name: Checkout monorepo + uses: actions/checkout@v2 + with: + path: monorepo + - name: Download WooCommerce ZIP + uses: actions/download-artifact@v2 + with: + name: woocommerce + path: tmp/woocommerce-build + - name: Extract and replace WooCommerce zip. + working-directory: tmp/woocommerce-build + run: | + mkdir -p woocommerce/woocommerce-production + unzip woocommerce.zip -d woocommerce/woocommerce-production + mv woocommerce/woocommerce-production/woocommerce/* woocommerce/woocommerce-production + rm -rf woocommerce/woocommerce-production/woocommerce + - name: Set up mirror + working-directory: tmp/woocommerce-build + run: | + touch mirrors.txt + echo "woocommerce/woocommerce-production" >> mirrors.txt + - name: Push to mirror + uses: Automattic/action-push-to-mirrors@v1 + with: + source-directory: ${{ github.workspace }}/monorepo + token: ${{ secrets.API_TOKEN_GITHUB }} + username: matticbot + working-directory: ${{ github.workspace }}/tmp/woocommerce-build + timeout-minutes: 5 # 2021-01-18: Successful runs seem to take about half a minute. diff --git a/.github/workflows/nightly-builds.yml b/.github/workflows/nightly-builds.yml index b2ff312e6fe..126d37523ac 100644 --- a/.github/workflows/nightly-builds.yml +++ b/.github/workflows/nightly-builds.yml @@ -4,20 +4,21 @@ on: - cron: '0 0 * * *' # Run at 12 AM UTC. jobs: build: + if: github.repository_owner == 'woocommerce' name: Nightly builds strategy: fail-fast: false matrix: - build: [master] + build: [trunk] runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: ref: ${{ matrix.build }} - name: Build id: build - uses: woocommerce/action-build@v2 + uses: woocommerce/action-build@trunk - name: Deploy nightly build uses: WebFreak001/deploy-nightly@v1.1.0 env: diff --git a/.github/workflows/pr-build-and-e2e-tests.yml b/.github/workflows/pr-build-and-e2e-tests.yml index 5eb717628a6..3408882d585 100644 --- a/.github/workflows/pr-build-and-e2e-tests.yml +++ b/.github/workflows/pr-build-and-e2e-tests.yml @@ -1,122 +1,222 @@ name: Build zip for PR -on: - pull_request +on: pull_request +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - build: - name: Build zip for PR - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 + build: + name: Build zip for PR + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 - - name: Build - id: build - uses: woocommerce/action-build@v2 + - name: Build + id: build + uses: woocommerce/action-build@trunk + env: + BUILD_ENV: e2e - - name: Upload PR zip - uses: actions/upload-artifact@v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - name: woocommerce - path: ${{ steps.build.outputs.zip_path }} - retention-days: 7 + - name: Upload PR zip + uses: actions/upload-artifact@v3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + name: woocommerce + path: ${{ steps.build.outputs.zip_path }} + retention-days: 7 - - name: Add comment - uses: actions/github-script@v3 - if: github.repository_owner == 'woocommerce' - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: ':package: Artifacts ready for [download](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})!' - }) - - e2e-tests-cache: - name: Set e2e caches for running tests - runs-on: ubuntu-latest - steps: - - name: Checkout code. - uses: actions/checkout@v2 - - - name: Load Node.js. - uses: actions/setup-node@v2 - with: - node-version: '12' - - # From https://docs.github.com/en/actions/guides/caching-dependencies-to-speed-up-workflows#using-the-cache-action - - name: Cache node modules - uses: actions/cache@v2 - id: cache_node_modules - env: - cache-name: cache-node-modules - with: - path: ./node_modules - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- + e2e-tests-run: + name: Runs E2E tests. + runs-on: ubuntu-18.04 + needs: [build] + steps: + - name: Create dirs. + run: | + mkdir -p code/woocommerce + mkdir -p package/woocommerce + mkdir -p tmp/woocommerce - - name: Run npm install, and cache if they aren't. - run: npm install - - e2e-tests-run: - name: Runs E2E tests. - runs-on: ubuntu-latest - needs: [ build, e2e-tests-cache ] - steps: + - name: Checkout code. + uses: actions/checkout@v3 + with: + path: package/woocommerce - - name: Create dirs. - run: | - mkdir -p code/woocommerce - mkdir -p package/woocommerce - mkdir -p tmp/woocommerce - mkdir -p node_modules + - name: Move current directory to code. We will install zip file in this dir later. + run: mv ./package/woocommerce/plugins/woocommerce/* ./code/woocommerce - - name: Checkout code. - uses: actions/checkout@v2 - with: - path: package/woocommerce - - # From https://docs.github.com/en/actions/guides/caching-dependencies-to-speed-up-workflows#using-the-cache-action - - name: Cache node modules - uses: actions/cache@v2 - id: cache_node_modules - env: - cache-name: cache-node-modules - with: - path: ./node_modules - key: ${{ runner.os }}-build-${{ hashFiles('package-lock.json') }} - restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}- + - name: Download WooCommerce ZIP. + uses: actions/download-artifact@v3 + with: + name: woocommerce + path: tmp - - name: Restore node modules from cache, if available. - run: mv ./node_modules package/woocommerce/node_modules + - name: Extract and replace WooCommerce zip. + working-directory: tmp + run: | + unzip woocommerce.zip -d woocommerce + mv woocommerce/woocommerce/* ../package/woocommerce/plugins/woocommerce/ - - name: Run npm install. - working-directory: package/woocommerce - run: npm install + - name: Cache modules + uses: actions/cache@v3 + with: + path: | + ~/.pnpm-store + key: ${{ runner.os }}-npm-${{ hashFiles('**/pnpm-lock.yaml') }} - - name: Load docker images and start containers. - working-directory: package/woocommerce - run: npx wc-e2e docker:up + - name: Install PNPM + run: npm install -g pnpm - - name: Move current directory to code. We will install zip file in this dir later. - run: mv ./package/woocommerce/* ./code/woocommerce + - name: Install dependencies + working-directory: package/woocommerce + run: pnpm install - - name: Download WooCommerce ZIP. - uses: actions/download-artifact@v2 - with: - name: woocommerce - path: tmp + - name: Load docker images and start containers. + working-directory: package/woocommerce/plugins/woocommerce + run: pnpm exec wc-e2e docker:up - - name: Extract and replace WooCommerce zip. - working-directory: tmp - run: | - unzip woocommerce.zip -d woocommerce - mv woocommerce/woocommerce/* ../package/woocommerce/ + - name: Run tests command. + working-directory: package/woocommerce/plugins/woocommerce + env: + WC_E2E_SCREENSHOTS: 1 + E2E_SLACK_TOKEN: ${{ secrets.E2E_SLACK_TOKEN }} + E2E_SLACK_CHANNEL: ${{ secrets.E2E_SLACK_CHANNEL }} + run: pnpm exec wc-e2e test:e2e - - name: Run tests command. - working-directory: code/woocommerce - run: npx wc-e2e test:e2e + - name: Archive E2E test screenshots + uses: actions/upload-artifact@v3 + if: always() + with: + name: E2E Screenshots + path: package/woocommerce/plugins/woocommerce/tests/e2e/screenshots + if-no-files-found: ignore + retention-days: 5 + + api-tests-run: + name: Runs API tests. + runs-on: ubuntu-18.04 + needs: [build] + steps: + - name: Create dirs. + run: | + mkdir -p code/woocommerce + mkdir -p package/woocommerce + mkdir -p tmp/woocommerce + + - name: Checkout code. + uses: actions/checkout@v3 + with: + path: package/woocommerce + + - name: Move current directory to code. We will install zip file in this dir later. + run: mv ./package/woocommerce/plugins/woocommerce/* ./code/woocommerce + + - name: Download WooCommerce ZIP. + uses: actions/download-artifact@v3 + with: + name: woocommerce + path: tmp + + - name: Extract and replace WooCommerce zip. + working-directory: tmp + run: | + unzip woocommerce.zip -d woocommerce + mv woocommerce/woocommerce/* ../package/woocommerce/plugins/woocommerce/ + + - name: Cache modules + uses: actions/cache@v3 + with: + path: | + ~/.pnpm-store + key: ${{ runner.os }}-npm-${{ hashFiles('**/pnpm-lock.yaml') }} + + - name: Install PNPM + run: npm install -g pnpm + + - name: Install dependencies + working-directory: package/woocommerce + run: pnpm install + + - name: Load docker images and start containers. + working-directory: package/woocommerce/plugins/woocommerce + run: pnpm exec wc-e2e docker:up + + - name: Run tests command. + working-directory: package/woocommerce/plugins/woocommerce + env: + BASE_URL: http://localhost:8084 + USER_KEY: admin + USER_SECRET: password + run: pnpm exec wc-api-tests test api + + - name: Upload API test report + uses: actions/upload-artifact@v3 + with: + name: api-test-report---pr-${{ github.event.number }} + path: | + package/woocommerce/packages/js/api-core-tests/allure-results + package/woocommerce/packages/js/api-core-tests/allure-report + retention-days: 7 + + k6-tests-run: + name: Runs k6 Performance tests + runs-on: ubuntu-18.04 + needs: [build] + steps: + - name: Create dirs. + run: | + mkdir -p code/woocommerce + mkdir -p package/woocommerce + mkdir -p tmp/woocommerce + + - name: Checkout code. + uses: actions/checkout@v3 + with: + path: package/woocommerce + + - name: Move current directory to code. We will install zip file in this dir later. + run: mv ./package/woocommerce/plugins/woocommerce/* ./code/woocommerce + + - name: Download WooCommerce ZIP. + uses: actions/download-artifact@v3 + with: + name: woocommerce + path: tmp + + - name: Extract and replace WooCommerce zip. + working-directory: tmp + run: | + unzip woocommerce.zip -d woocommerce + mv woocommerce/woocommerce/* ../package/woocommerce/plugins/woocommerce/ + + - name: Cache modules + uses: actions/cache@v3 + with: + path: | + ~/.pnpm-store + key: ${{ runner.os }}-npm-${{ hashFiles('**/pnpm-lock.yaml') }} + + - name: Install PNPM + run: npm install -g pnpm + + - name: Install dependencies + working-directory: package/woocommerce + run: pnpm install + + - name: Workaround to use initialization file with prepopulated data. + working-directory: package/woocommerce/plugins/woocommerce/tests/e2e/docker + run: | + cp init-sample-products.sh initialize.sh + + - name: Load docker images and start containers. + working-directory: package/woocommerce/plugins/woocommerce + run: pnpm exec wc-e2e docker:up + + - name: Install k6 + run: | + curl https://github.com/grafana/k6/releases/download/v0.33.0/k6-v0.33.0-linux-amd64.tar.gz -L | tar xvz --strip-components 1 + + - name: Run k6 tests + run: | + ./k6 run package/woocommerce/plugins/woocommerce/tests/performance/tests/gh-action-pr-requests.js diff --git a/.github/workflows/pr-code-coverage.yml b/.github/workflows/pr-code-coverage.yml index 0b12def78ac..a4c068e56f7 100644 --- a/.github/workflows/pr-code-coverage.yml +++ b/.github/workflows/pr-code-coverage.yml @@ -1,10 +1,16 @@ name: Run code coverage on PR on: pull_request +defaults: + run: + shell: bash +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: test: name: Code coverage (PHP 7.4, WP Latest) - timeout-minutes: 15 + timeout-minutes: 20 runs-on: ubuntu-latest services: database: @@ -16,7 +22,9 @@ jobs: options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 + with: + fetch-depth: 100 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -31,21 +39,38 @@ jobs: php --version composer --version - - name: Get cached composer directories - uses: actions/cache@v2 + - name: Get cached composer and pnpm directories + uses: actions/cache@v3 + id: cache-deps with: path: | - ./packages - ./vendor - key: ${{ runner.os }}-${{ hashFiles('./composer.lock') }} + ~/.pnpm-store + plugins/woocommerce/packages + plugins/woocommerce/vendor + key: ${{ runner.os }}-npm-composer-${{ hashFiles('plugins/woocommerce/composer.lock', '**/pnpm-lock.yaml') }} - - name: Setup and install composer - run: composer install + - name: Install PNPM + run: npm install -g pnpm + + - name: Install dependencies + run: pnpm install + + - name: Install Composer dependencies + if: steps.cache-deps.outputs.cache-hit != 'true' + run: pnpm nx composer-install woocommerce + + - name: Build Admin feature config + run: | + pnpm nx build:feature-config woocommerce - name: Init DB and WP - run: ./tests/bin/install.sh woo_test root root 127.0.0.1 latest + run: pnpm nx install-unit-test-db woocommerce - name: Run unit tests with code coverage. Allow to fail. run: | - RUN_CODE_COVERAGE=1 bash ./tests/bin/phpunit.sh + pnpm nx test-code-coverage woocommerce exit 0 + + - name: Send code coverage to Codecov. + run: | + bash <(curl -s https://codecov.io/bash) diff --git a/.github/workflows/pr-code-sniff.yml b/.github/workflows/pr-code-sniff.yml index 427760afd13..54213a6f277 100644 --- a/.github/workflows/pr-code-sniff.yml +++ b/.github/workflows/pr-code-sniff.yml @@ -1,49 +1,59 @@ name: Run code sniff on PR on: pull_request +defaults: + run: + shell: bash +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: test: name: Code sniff (PHP 7.4, WP Latest) timeout-minutes: 15 runs-on: ubuntu-latest - services: - database: - image: mysql:5.6 - env: - MYSQL_ROOT_PASSWORD: root - ports: - - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 + with: + fetch-depth: 100 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: 7.4 - tools: composer - extensions: mysql - coverage: none + tools: composer, cs2pr - name: Tool versions run: | php --version composer --version - - name: Get cached composer directories - uses: actions/cache@v2 + - name: Get cached composer and pnpm directories + uses: actions/cache@v3 + id: cache-deps with: path: | - ./packages - ./vendor - key: ${{ runner.os }}-${{ hashFiles('./composer.lock') }} + ~/.pnpm-store + plugins/woocommerce/packages + plugins/woocommerce/vendor + key: ${{ runner.os }}-npm-composer-${{ hashFiles('plugins/woocommerce/composer.lock', '**/pnpm-lock.yaml') }} - - name: Setup and install composer - run: composer install + - name: Install PNPM + run: npm install -g pnpm - - name: Init DB and WP - run: ./tests/bin/install.sh woo_test root root 127.0.0.1 latest + - name: Install dependencies + run: pnpm install + + - name: Install Composer dependencies + if: steps.cache-deps.outputs.cache-hit != 'true' + run: pnpm nx composer-install woocommerce - name: Run code sniff - run: RUN_PHPCS=1 bash ./tests/bin/phpcs.sh + continue-on-error: true + working-directory: plugins/woocommerce + run: ./tests/bin/phpcs.sh "${{ github.event.pull_request.base.sha }}" "${{ github.event.after }}" + + - name: Show PHPCS results in PR + working-directory: plugins/woocommerce + run: cs2pr ./phpcs-report.xml diff --git a/.github/workflows/pr-lint-monorepo.yml b/.github/workflows/pr-lint-monorepo.yml new file mode 100644 index 00000000000..7c952b3a641 --- /dev/null +++ b/.github/workflows/pr-lint-monorepo.yml @@ -0,0 +1,26 @@ +name: Run lint checks potentially affecting projects across the monorepo +on: pull_request +concurrency: + group: changelogger-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true +jobs: + changelogger_used: + name: Changelogger use + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + + - name: Check change files are touched for touched projects + env: + BASE: ${{ github.event.pull_request.base.sha }} + HEAD: ${{ github.event.pull_request.head.sha }} + run: php tools/monorepo/check-changelogger-use.php --debug "$BASE" "$HEAD" diff --git a/.github/workflows/pr-lint-test-js.yml b/.github/workflows/pr-lint-test-js.yml new file mode 100644 index 00000000000..c2e6c1518d7 --- /dev/null +++ b/.github/workflows/pr-lint-test-js.yml @@ -0,0 +1,47 @@ +name: Lint and tests for JS packages and woocommerce-admin/client + +on: + pull_request: + paths: + - 'packages/js/**/**' + - 'plugins/woocommerce-admin/client/**' + - '!**.md' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint-test-js: + name: Lint and Test JS + runs-on: ubuntu-18.04 + steps: + - name: Check out repository code + uses: actions/checkout@v3 + + - uses: actions/setup-node@v2 + with: + node-version: '16' + + - name: Cache modules + uses: actions/cache@v3 + with: + path: | + ~/.pnpm-store + key: ${{ runner.os }}-npm-${{ hashFiles('**/pnpm-lock.yaml') }} + + - name: Install PNPM + run: npm install -g pnpm + + - name: Install dependencies + run: pnpm install + + - name: Lint + run: | + pnpm nx lint woocommerce-admin + pnpm nx lint:js-packages woocommerce-admin + + - name: Test + run: | + pnpm nx build woocommerce-admin + pnpm nx test woocommerce-admin + pnpm nx test:packages woocommerce-admin diff --git a/.github/workflows/pr-project-label.yml b/.github/workflows/pr-project-label.yml new file mode 100644 index 00000000000..cb679c00bc1 --- /dev/null +++ b/.github/workflows/pr-project-label.yml @@ -0,0 +1,18 @@ +name: 'Label Pull Request Project' +on: + pull_request_target: + types: + - opened + - synchronize +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + label_project: + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v3 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + configuration-path: .github/project-pr-labeler.yml diff --git a/.github/workflows/pr-smoke-test.yml b/.github/workflows/pr-smoke-test.yml new file mode 100644 index 00000000000..4c6ccaa89c4 --- /dev/null +++ b/.github/workflows/pr-smoke-test.yml @@ -0,0 +1,125 @@ +name: Run smoke tests against pull request. +on: + pull_request: + branches: + - trunk + types: + - labeled +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + prcheck: + name: Smoke test a pull request. + if: "${{ contains(github.event.label.name, 'run: smoke tests') }}" + runs-on: ubuntu-18.04 + steps: + - name: Create dirs. + run: | + mkdir -p code/woocommerce + mkdir -p package/woocommerce + mkdir -p tmp/woocommerce + + - name: Checkout code. + uses: actions/checkout@v3 + with: + path: package/woocommerce + + - name: Get cached composer and pnpm directories + uses: actions/cache@v3 + id: cache-deps + with: + path: | + ~/.pnpm-store + package/woocommerce/plugins/woocommerce/packages + package/woocommerce/plugins/woocommerce/vendor + key: ${{ runner.os }}-smoke-test-npm-composer-${{ hashFiles('plugins/woocommerce/composer.lock', '**/pnpm-lock.yaml') }} + + - name: Install PNPM + run: npm install -g pnpm + + - name: Install dependencies + run: pnpm install + + - name: Install Composer dependencies + if: steps.cache-deps.outputs.cache-hit != 'true' + run: pnpm nx composer-install-no-dev woocommerce + + - name: Install prerequisites. + working-directory: package/woocommerce/plugins/woocommerce + id: installation + run: | + pnpm nx build-assets woocommerce + pnpm install jest + + - name: Run smoke test. + working-directory: package/woocommerce/plugins/woocommerce + if: always() + env: + SMOKE_TEST_URL: ${{ secrets.SMOKE_TEST_URL }} + SMOKE_TEST_ADMIN_USER: ${{ secrets.SMOKE_TEST_ADMIN_USER }} + SMOKE_TEST_ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }} + SMOKE_TEST_ADMIN_USER_EMAIL: ${{ secrets.SMOKE_TEST_ADMIN_USER_EMAIL }} + SMOKE_TEST_CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }} + SMOKE_TEST_CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }} + WC_E2E_SCREENSHOTS: 1 + E2E_RETEST: 1 + E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }} + E2E_SLACK_CHANNEL: ${{ secrets.SMOKE_TEST_SLACK_CHANNEL }} + UPDATE_WC: 1 + DEFAULT_TIMEOUT_OVERRIDE: 120000 + run: | + pnpm exec wc-e2e test:e2e tests/e2e/specs/smoke-tests/update-woocommerce.js + + - name: Post Smoke tests results comment on PR + if: always() + uses: actions/github-script@v5 + env: + TITLE: 'Smoke Test Results' + SMOKE_TEST_URL: ${{ secrets.SMOKE_TEST_URL }} + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const script = require( './package/woocommerce/packages/js/e2e-environment/bin/post-results-to-github-pr.js' ) + await script({github, context}) + + - name: Run E2E tests. + working-directory: package/woocommerce/plugins/woocommerce + if: always() + env: + SMOKE_TEST_URL: ${{ secrets.SMOKE_TEST_URL }} + SMOKE_TEST_ADMIN_USER: ${{ secrets.SMOKE_TEST_ADMIN_USER }} + SMOKE_TEST_ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }} + SMOKE_TEST_ADMIN_USER_EMAIL: ${{ secrets.SMOKE_TEST_ADMIN_USER_EMAIL }} + SMOKE_TEST_CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }} + SMOKE_TEST_CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }} + WC_E2E_SCREENSHOTS: 1 + E2E_RETEST: 1 + E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }} + E2E_SLACK_CHANNEL: ${{ secrets.SMOKE_TEST_SLACK_CHANNEL }} + UPDATE_WC: 1 + DEFAULT_TIMEOUT_OVERRIDE: 120000 + run: | + pnpm exec wc-e2e test:e2e + + - name: Post E2E tests results comment on PR + if: always() + uses: actions/github-script@v5 + env: + TITLE: 'E2E Test Results' + SMOKE_TEST_URL: ${{ secrets.SMOKE_TEST_URL }} + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const script = require( './package/woocommerce/packages/js/e2e-environment/bin/post-results-to-github-pr.js' ) + await script({github, context}) + + - name: Remove label from pull request. + if: | + always() + && contains( github.event.pull_request.labels.*.name, format('run{0} smoke tests', ':')) + uses: actions-ecosystem/action-remove-labels@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + labels: 'run: smoke tests' diff --git a/.github/workflows/pr-unit-tests.yml b/.github/workflows/pr-unit-tests.yml index 2d9d0b86862..6b3670e796e 100644 --- a/.github/workflows/pr-unit-tests.yml +++ b/.github/workflows/pr-unit-tests.yml @@ -1,22 +1,30 @@ name: Run unit tests on PR on: pull_request +defaults: + run: + shell: bash +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test: name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} - timeout-minutes: 15 + timeout-minutes: 20 runs-on: ubuntu-latest + continue-on-error: ${{ matrix.wp == 'nightly' }} strategy: fail-fast: false matrix: - php: [ '7.0', '7.1', '7.2', '7.3', '7.4', '8.0' ] + php: [ '7.2', '7.3', '7.4', '8.0' ] wp: [ "latest" ] include: - wp: nightly php: '7.4' - - wp: '5.5' + - wp: '5.8' php: 7.2 - - wp: '5.4' + - wp: '5.7' php: 7.2 services: database: @@ -43,29 +51,46 @@ jobs: php --version composer --version - - name: Get cached composer directories - uses: actions/cache@v2 + - name: Get cached composer and pnpm directories + uses: actions/cache@v3 + id: cache-deps with: path: | - ./packages - ./vendor - key: ${{ runner.os }}-${{ hashFiles('./composer.lock') }} + ~/.pnpm-store + plugins/woocommerce/packages + plugins/woocommerce/vendor + key: ${{ runner.os }}-npm-composer-${{ hashFiles('plugins/woocommerce/composer.lock', '**/pnpm-lock.yaml') }} - - name: Setup and install composer - run: composer install + - name: Install PNPM + run: npm install -g pnpm + + - name: Install dependencies + run: pnpm install + + - name: Install Composer dependencies + if: steps.cache-deps.outputs.cache-hit != 'true' + run: pnpm nx composer-install woocommerce + + - name: Build Admin feature config + run: | + pnpm nx build:feature-config woocommerce - name: Add PHP8 Compatibility. run: | if [ "$(php -r "echo version_compare(PHP_VERSION,'8.0','>=');")" ]; then curl -L https://github.com/woocommerce/phpunit/archive/add-compatibility-with-php8-to-phpunit-7.zip -o /tmp/phpunit-7.5-fork.zip unzip -d /tmp/phpunit-7.5-fork /tmp/phpunit-7.5-fork.zip + cd plugins/woocommerce composer bin phpunit config --unset platform composer bin phpunit config repositories.0 '{"type": "path", "url": "/tmp/phpunit-7.5-fork/phpunit-add-compatibility-with-php8-to-phpunit-7", "options": {"symlink": false}}' - composer bin phpunit require --dev -W phpunit/phpunit:@dev --ignore-platform-reqs - fi + composer bin phpunit require --dev -W phpunit/phpunit:@dev --ignore-platform-reqs + rm -rf ./vendor/phpunit/ + pnpm nx composer-dump-autoload woocommerce + fi - name: Init DB and WP + working-directory: plugins/woocommerce run: ./tests/bin/install.sh woo_test root root 127.0.0.1 ${{ matrix.wp }} - name: Run tests - run: ./vendor/bin/phpunit -c ./phpunit.xml + run: pnpm nx test-unit woocommerce diff --git a/.github/workflows/pull-request-post-merge-processing.yml b/.github/workflows/pull-request-post-merge-processing.yml new file mode 100644 index 00000000000..53d5611feef --- /dev/null +++ b/.github/workflows/pull-request-post-merge-processing.yml @@ -0,0 +1,46 @@ +name: "Pull request post-merge processing" +on: + pull_request_target: + types: [closed] + +jobs: + process-pull-request-after-merge: + name: "Process a pull request after it's merged" + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - name: "Get the action scripts" + run: | + scripts="assign-milestone-to-merged-pr.php add-post-merge-comment.php post-request-shared.php" + for script in $scripts + do + curl \ + --silent \ + --fail \ + --header 'Authorization: bearer ${{ secrets.GITHUB_TOKEN }}' \ + --header 'User-Agent: GitHub action to set the milestone for a pull request' \ + --header 'Accept: application/vnd.github.v3.raw' \ + --output $script \ + --location "$GITHUB_API_URL/repos/${{ github.repository }}/contents/.github/workflows/scripts/$script?ref=${{ github.event.pull_request.base.ref }}" + done + env: + GITHUB_API_URL: ${{ env.GITHUB_API_URL }} + - name: "Install PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + - name: "Run the script to assign a milestone" + if: | + contains(github.event.pull_request.labels.*.name, 'plugin: woocommerce') && + !github.event.pull_request.milestone && + github.event.pull_request.base.ref == 'trunk' + run: php assign-milestone-to-merged-pr.php + env: + PULL_REQUEST_ID: ${{ github.event.pull_request.node_id }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: "Run the script to post a comment with next steps hint" + if: "contains(github.event.pull_request.labels.*.name, 'plugin: woocommerce')" + run: php add-post-merge-comment.php + env: + PULL_REQUEST_ID: ${{ github.event.pull_request.node_id }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-code-freeze.yml b/.github/workflows/release-code-freeze.yml new file mode 100644 index 00000000000..30a0e0e3058 --- /dev/null +++ b/.github/workflows/release-code-freeze.yml @@ -0,0 +1,35 @@ +name: "Enforce release code freeze" +on: + schedule: + - cron: '0 16 * * 4' # Run at 1600 UTC on Thursdays. + +jobs: + maybe-create-next-milestone-and-release-branch: + name: "Maybe create next milestone and release branch" + runs-on: ubuntu-latest + steps: + - name: "Get the action script" + run: | + scripts="post-request-shared.php release-code-freeze.php" + for script in $scripts + do + curl \ + --silent \ + --fail \ + --header 'Authorization: bearer ${{ secrets.GITHUB_TOKEN }}' \ + --header 'User-Agent: GitHub action to enforce release code freeze' \ + --header 'Accept: application/vnd.github.v3.raw' \ + --output $script \ + --location "$GITHUB_API_URL/repos/${{ github.repository }}/contents/.github/workflows/scripts/$script?ref=$GITHUB_REF" + done + env: + GITHUB_API_URL: ${{ env.GITHUB_API_URL }} + GITHUB_REF: ${{ env.GITHUB_REF }} + - name: "Install PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + - name: "Run the script to enforce the code freeze" + run: php release-code-freeze.php + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/scripts/add-post-merge-comment.php b/.github/workflows/scripts/add-post-merge-comment.php new file mode 100644 index 00000000000..afd6ec803f4 --- /dev/null +++ b/.github/workflows/scripts/add-post-merge-comment.php @@ -0,0 +1,94 @@ + { + return new Promise( ( resolve, reject ) => { + const request = https.get( options, ( response ) => { + response.setEncoding('utf8'); + + let responseBody = ''; + + response.on( 'data', ( chunk ) => { + responseBody += chunk; + } ); + + response.on( 'end', () => { + const assets = JSON.parse( responseBody ); + // use the most recently uploaded asset + resolve( assets[ assets.length - 1 ].id ); + } ); + } ); + + request.on('error', ( error ) => { + reject( error ); + } ); + + request.end(); + + } ); +} + +module.exports = async ( { github, context, core } ) => { + const id = await fetchAssetId(); + + // set asset_id as the output + core.setOutput( 'asset_id', id ); +} diff --git a/.github/workflows/scripts/post-request-shared.php b/.github/workflows/scripts/post-request-shared.php new file mode 100644 index 00000000000..67a6794feef --- /dev/null +++ b/.github/workflows/scripts/post-request-shared.php @@ -0,0 +1,284 @@ + $title, + ) ); + + return is_array( $result ) && $result['title'] === $title; +} + +/** + * Function to create branch using the GitHub REST API. + * + * @param string $branch The branch to be created. + * @param string $sha The sha1 reference for the branch. + * @return bool True on success, False otherwise. + */ +function create_github_branch( $branch, $sha ) { + global $repo_owner, $repo_name; + + $ref = "refs/heads/{$branch}"; + $result = do_github_api_post_request( "/repos/{$repo_owner}/{$repo_name}/git/refs", array( + 'ref' => $ref, + 'sha' => $sha, + ) ); + + return is_array( $result ) && $result['ref'] === $ref; +} + +/** + * Function to create branch using the GitHub REST API from an existing branch. + * + * @param string $source The branch from which to create. + * @param string $target The branch to be created. + * @return bool True on success, False otherwise. + */ +function create_github_branch_from_branch( $source, $target ) { + $ref = get_ref_from_branch( $source ); + if ( ! $ref ) { + return false; + } + return create_github_branch( $target, $ref ); +} + +/** + * Function to do a GitHub API POST Request. + * + * @param array $request_url + * @param array $body The body of the request to be json encoded. + * @return mixed The json-decoded response if a response is received, 'false' (or whatever file_get_contents returns) otherwise. + */ +function do_github_api_post_request( $request_path, $body ) { + global $github_token, $github_api_url, $github_api_response_code; + + $context = stream_context_create( + array( + 'http' => array( + 'method' => 'POST', + 'header' => array( + 'Accept: application/vnd.github.v3+json', + 'Content-Type: application/json', + 'User-Agent: GitHub Actions for creation of milestones', + 'Authorization: bearer ' . $github_token, + ), + 'content' => json_encode( $body ), + ), + ) + ); + + $full_request_url = rtrim( $github_api_url, '/' ) . '/' . ltrim( $request_path, '/' ); + $result = @file_get_contents( $full_request_url, false, $context ); + + // Verify that the post request was sucessful. + $status_line = $http_response_header[0]; + preg_match( "/^HTTPS?\/\d\.\d\s+(\d{3})\s+/i", $status_line, $matches ); + $github_api_response_code = $matches[1]; + if ( '2' !== substr( $github_api_response_code, 0, 1 ) ) { + return false; + } + + return is_string( $result ) ? json_decode( $result, true ) : $result; +} + +/** + * Function to query the GitHub GraphQL API. + * + * @param string $body The GraphQL-formatted request body, without "query" or "mutation" wrapper. + * @param bool $is_mutation True if the request is a mutation, false if it's a query. + * @return mixed The json-decoded response if a response is received, 'false' (or whatever file_get_contents returns) otherwise. + */ +function do_graphql_api_request( $body, $is_mutation = false ) { + global $github_token, $graphql_api_url; + + $keyword = $is_mutation ? 'mutation' : 'query'; + $data = array( 'query' => "$keyword { $body }" ); + $context = stream_context_create( + array( + 'http' => array( + 'method' => 'POST', + 'header' => array( + 'Accept: application/json', + 'Content-Type: application/json', + 'User-Agent: GitHub action to set the milestone for a pull request', + 'Authorization: bearer ' . $github_token, + ), + 'content' => json_encode( $data ), + ), + ) + ); + + $result = file_get_contents( $graphql_api_url, false, $context ); + return is_string( $result ) ? json_decode( $result, true ) : $result; +} + +// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped, WordPress.WP.AlternativeFunctions diff --git a/.github/workflows/scripts/release-code-freeze.php b/.github/workflows/scripts/release-code-freeze.php new file mode 100644 index 00000000000..607480e850c --- /dev/null +++ b/.github/workflows/scripts/release-code-freeze.php @@ -0,0 +1,65 @@ + 14 ) { + echo 'Info: Today is not the Thursday of the code freeze.' . PHP_EOL; + return; +} + +$latest_version_with_release = get_latest_version_with_release(); + +if ( empty( $latest_version_with_release ) ) { + echo '*** Error: Unable to get latest version with release' . PHP_EOL; + return; +} + +// Because we go from 5.9 to 6.0, we can get the next major_minor by adding 0.1 and formatting appropriately. +$latest_float = (float) $latest_version_with_release; +$branch_major_minor = number_format( $latest_float + 0.1, 1 ); +$milestone_major_minor = number_format( $latest_float + 0.2, 1 ); + +// We use those values to get the release branch and next milestones that we need to create. +$release_branch_to_create = "release/{$branch_major_minor}"; +$milestone_to_create = "{$milestone_major_minor}.0"; + +if ( getenv( 'DRY_RUN' ) ) { + echo 'DRY RUN: Skipping actual creation of release branch and milestone...' . PHP_EOL; + echo "Release Branch: {$release_branch_to_create}" . PHP_EOL; + echo "Milestone: {$milestone_to_create}" . PHP_EOL; + return; +} + +if ( create_github_milestone( $milestone_to_create ) ) { + echo "Created milestone {$milestone_to_create}" . PHP_EOL; +} else if ( '422' === $github_api_response_code ) { + // The milestone already existed when GitHub returns a 422 status. + echo "Notice: Unable to create {$milestone_to_create} milestone. Maybe it already exists? Skipping..." . PHP_EOL; +} else { + echo "*** Error: Unable to create {$milestone_to_create} milestone" . PHP_EOL; +} + +if ( create_github_branch_from_branch( 'trunk', $release_branch_to_create ) ) { + echo "Created branch {$release_branch_to_create}" . PHP_EOL; +} else if ( '422' === $github_api_response_code ) { + // The release branch already existed when GitHub returns a 422 status. + echo "Notice: Unable to create {$release_branch_to_create} branch. Maybe it already exists? Skipping..." . PHP_EOL; +} else { + echo "*** Error: Unable to create {$release_branch_to_create}" . PHP_EOL; +} diff --git a/.github/workflows/smoke-test-daily-site-check.yml b/.github/workflows/smoke-test-daily-site-check.yml new file mode 100644 index 00000000000..5fdb80f819b --- /dev/null +++ b/.github/workflows/smoke-test-daily-site-check.yml @@ -0,0 +1,24 @@ +name: Check daily smoke test site status. +on: + schedule: + - cron: '25 7 * * *' + +jobs: + ping_site: + runs-on: ubuntu-latest + name: Check site and notify if not found + steps: + - name: Check site status + id: sitecheck + uses: srt32/uptime@958231f4d95c117f08eb0fc70907e80d0dfedf2b + with: + url-to-hit: "${{ secrets.SMOKE_TEST_URL }}ready/" + expected-statuses: "200,301" + - name: Send message to Slack API + if: failure() + uses: archive/github-actions-slack@deecc2edc496dc642d643de1d7cf3a47f51fb27a + id: notify + with: + slack-bot-user-oauth-access-token: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }} + slack-channel: ${{ secrets.SMOKE_TEST_SLACK_CHANNEL }} + slack-text: ':warning: FYI the URL ${{ secrets.SMOKE_TEST_URL }}ready/ appears to be returning `404 not found` :x:' diff --git a/.github/workflows/smoke-test-daily.yml b/.github/workflows/smoke-test-daily.yml new file mode 100644 index 00000000000..56e7ca1eba6 --- /dev/null +++ b/.github/workflows/smoke-test-daily.yml @@ -0,0 +1,153 @@ +name: Smoke test daily +on: + schedule: + - cron: '25 3 * * *' +jobs: + login-run: + name: Daily smoke test on trunk. + runs-on: ubuntu-18.04 + steps: + - name: Create dirs. + run: | + mkdir -p code/woocommerce + mkdir -p package/woocommerce + mkdir -p tmp/woocommerce + mkdir -p node_modules + + - name: Checkout code. + uses: actions/checkout@v3 + with: + path: package/woocommerce + ref: trunk + + - name: Install prerequisites. + working-directory: package/woocommerce/plugins/woocommerce + run: | + npm install -g pnpm + pnpm install + pnpm nx composer-install-no-dev woocommerce + pnpm nx build-assets woocommerce + pnpm install jest + + - name: Run smoke test. + working-directory: package/woocommerce/plugins/woocommerce + env: + SMOKE_TEST_URL: ${{ secrets.SMOKE_TEST_URL }} + SMOKE_TEST_ADMIN_USER: ${{ secrets.SMOKE_TEST_ADMIN_USER }} + SMOKE_TEST_ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }} + SMOKE_TEST_ADMIN_USER_EMAIL: ${{ secrets.SMOKE_TEST_ADMIN_USER_EMAIL }} + SMOKE_TEST_CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }} + SMOKE_TEST_CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }} + WC_E2E_SCREENSHOTS: 1 + E2E_RETEST: 1 + E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }} + E2E_SLACK_CHANNEL: 'C01U0H617MY' + UPDATE_WC: 1 + DEFAULT_TIMEOUT_OVERRIDE: 120000 + BASE_URL: ${{ secrets.SMOKE_TEST_URL }} + USER_KEY: ${{ secrets.SMOKE_TEST_ADMIN_USER }} + USER_SECRET: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }} + run: | + pnpm exec wc-e2e test:e2e tests/e2e/specs/smoke-tests/update-woocommerce.js + pnpm exec wc-e2e test:e2e + pnpm exec wc-api-tests test api + + build: + name: Build zip for PR + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Build + id: build + uses: woocommerce/action-build@trunk + env: + BUILD_ENV: e2e + + - name: Upload PR zip + uses: actions/upload-artifact@v3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + name: woocommerce + path: ${{ steps.build.outputs.zip_path }} + retention-days: 7 + + test-plugins: + name: Smoke tests with ${{ matrix.plugin }} plugin installed + runs-on: ubuntu-18.04 + needs: [build] + strategy: + fail-fast: false + matrix: + include: + - plugin: 'WooCommerce Payments' + repo: 'automattic/woocommerce-payments' + - plugin: 'WooCommerce PayPal Payments' + repo: 'woocommerce/woocommerce-paypal-payments' + - plugin: 'WooCommerce Shipping & Tax' + repo: 'automattic/woocommerce-services' + - plugin: 'WooCommerce Subscriptions' + repo: WC_SUBSCRIPTIONS_REPO + private: true + - plugin: 'WordPress SEO' # Yoast SEO in the UI, but the slug is wordpress-seo + repo: 'Yoast/wordpress-seo' + - plugin: 'Contact Form 7' + repo: 'takayukister/contact-form-7' + steps: + - name: Create dirs. + run: | + mkdir -p code/woocommerce + mkdir -p package/woocommerce + mkdir -p tmp/woocommerce + mkdir -p node_modules + + - name: Checkout code. + uses: actions/checkout@v2 + with: + path: package/woocommerce + + - name: Install PNPM and install dependencies + working-directory: package/woocommerce + run: | + npm install -g pnpm + pnpm install + + - name: Load docker images and start containers. + working-directory: package/woocommerce/plugins/woocommerce + run: pnpm exec wc-e2e docker:up + + - name: Move current directory to code. We will install zip file in this dir later. + run: mv ./package/woocommerce/plugins/woocommerce/* ./code/woocommerce + + - name: Download WooCommerce ZIP. + uses: actions/download-artifact@v3 + with: + name: woocommerce + path: tmp + + - name: Extract and replace WooCommerce zip. + working-directory: tmp + run: | + unzip woocommerce.zip -d woocommerce + mv woocommerce/woocommerce/* ../package/woocommerce/plugins/woocommerce/ + + - name: Install dependencies again + working-directory: package/woocommerce + run: | + npm install -g pnpm + pnpm install + + - name: Run tests command. + working-directory: package/woocommerce/plugins/woocommerce + env: + WC_E2E_SCREENSHOTS: 1 + E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }} + E2E_SLACK_CHANNEL: ${{ secrets.RELEASE_TEST_SLACK_CHANNEL }} + PLUGIN_REPOSITORY: ${{ matrix.private && secrets[matrix.repo] || matrix.repo }} + PLUGIN_NAME: ${{ matrix.plugin }} + GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }} + run: | + pnpm exec wc-e2e test:e2e tests/e2e/specs/smoke-tests/upload-plugin.js + pnpm exec wc-e2e test:e2e diff --git a/.github/workflows/smoke-test-release.yml b/.github/workflows/smoke-test-release.yml new file mode 100644 index 00000000000..684c4b57795 --- /dev/null +++ b/.github/workflows/smoke-test-release.yml @@ -0,0 +1,197 @@ +name: Smoke test release +on: + workflow_dispatch: + inputs: + release_id: + description: 'WooCommerce Release Id' + required: true +jobs: + login-run: + name: Daily smoke test on release. + runs-on: ubuntu-18.04 + steps: + - name: Create dirs. + run: | + mkdir -p code/woocommerce + mkdir -p package/woocommerce + mkdir -p tmp/woocommerce + mkdir -p node_modules + + - name: Checkout code. + uses: actions/checkout@v3 + with: + path: package/woocommerce + ref: trunk + + - name: Install prerequisites. + working-directory: package/woocommerce/plugins/woocommerce + run: | + npm install -g pnpm + pnpm install + pnpm nx composer-install-no-dev woocommerce + pnpm nx build-assets woocommerce + pnpm install jest + + - name: Run smoke test. + working-directory: package/woocommerce/plugins/woocommerce + env: + SMOKE_TEST_URL: ${{ secrets.RELEASE_TEST_URL }} + SMOKE_TEST_ADMIN_USER: ${{ secrets.RELEASE_TEST_ADMIN_USER }} + SMOKE_TEST_ADMIN_PASSWORD: ${{ secrets.RELEASE_TEST_ADMIN_PASSWORD }} + SMOKE_TEST_ADMIN_USER_EMAIL: ${{ secrets.RELEASE_TEST_ADMIN_USER_EMAIL }} + SMOKE_TEST_CUSTOMER_USER: ${{ secrets.RELEASE_TEST_CUSTOMER_USER }} + SMOKE_TEST_CUSTOMER_PASSWORD: ${{ secrets.RELEASE_TEST_CUSTOMER_PASSWORD }} + WC_E2E_SCREENSHOTS: 1 + E2E_RETEST: 1 + E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }} + E2E_SLACK_CHANNEL: 'C02DS4NE72S' + TEST_RELEASE: 1 + UPDATE_WC: 1 + DEFAULT_TIMEOUT_OVERRIDE: 120000 + BASE_URL: ${{ secrets.RELEASE_TEST_URL }} + USER_KEY: ${{ secrets.RELEASE_TEST_ADMIN_USER }} + USER_SECRET: ${{ secrets.RELEASE_TEST_ADMIN_PASSWORD }} + run: | + pnpm exec wc-e2e test:e2e tests/e2e/specs/smoke-tests/update-woocommerce.js + pnpm exec wc-e2e test:e2e + pnpm exec wc-api-tests test api + test-wp-version: + name: Smoke test on L-${{ matrix.wp }} WordPress version + runs-on: ubuntu-18.04 + strategy: + matrix: + wp: ['1', '2'] + steps: + - name: Create dirs. + run: | + mkdir -p code/woocommerce + mkdir -p package/woocommerce + mkdir -p tmp/woocommerce + mkdir -p node_modules + + - name: Checkout code. + uses: actions/checkout@v3 + with: + path: package/woocommerce + - name: Fetch Asset ID + id: fetch_asset_id + uses: actions/github-script@v5 + env: + RELEASE_ID: ${{ github.event.inputs.release_id }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + with: + script: | + const script = require( './package/woocommerce/.github/workflows/scripts/fetch-asset-id.js' ) + await script({github, context, core}) + + - name: Install PNPM and install dependencies + working-directory: package/woocommerce + run: | + npm install -g pnpm + pnpm install + + - name: Load docker images and start containers. + working-directory: package/woocommerce/plugins/woocommerce + env: + LATEST_WP_VERSION_MINUS: ${{ matrix.wp }} + run: pnpm nx docker-up woocommerce + + - name: Move current directory to code. We will install zip file in this dir later. + run: mv ./package/woocommerce/plugins/woocommerce/* ./code/woocommerce + + - name: Download WooCommerce release zip + working-directory: tmp + run: | + curl https://api.github.com/repos/${{ github.repository }}/releases/assets/${{ steps.fetch_asset_id.outputs.asset_id }} -LJOH 'Accept: application/octet-stream' + + unzip woocommerce.zip -d woocommerce + mv woocommerce/woocommerce/* ../package/woocommerce/plugins/woocommerce/ + + - name: Run tests command. + working-directory: package/woocommerce/plugins/woocommerce + env: + WC_E2E_SCREENSHOTS: 1 + E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }} + E2E_SLACK_CHANNEL: ${{ secrets.RELEASE_TEST_SLACK_CHANNEL }} + run: pnpm nx test-e2e woocommerce + + test-plugins: + name: Smoke tests with ${{ matrix.plugin }} plugin installed + runs-on: ubuntu-18.04 + strategy: + fail-fast: false + matrix: + include: + - plugin: 'WooCommerce Payments' + repo: 'automattic/woocommerce-payments' + - plugin: 'WooCommerce PayPal Payments' + repo: 'woocommerce/woocommerce-paypal-payments' + - plugin: 'WooCommerce Shipping & Tax' + repo: 'automattic/woocommerce-services' + - plugin: 'WooCommerce Subscriptions' + repo: WC_SUBSCRIPTIONS_REPO + private: true + - plugin: 'WordPress SEO' # Yoast SEO in the UI, but the slug is wordpress-seo + repo: 'Yoast/wordpress-seo' + - plugin: 'Contact Form 7' + repo: 'takayukister/contact-form-7' + steps: + - name: Create dirs. + run: | + mkdir -p code/woocommerce + mkdir -p package/woocommerce + mkdir -p tmp/woocommerce + mkdir -p node_modules + + - name: Checkout code. + uses: actions/checkout@v2 + with: + path: package/woocommerce + - name: Fetch Asset ID + id: fetch_asset_id + uses: actions/github-script@v5 + env: + RELEASE_ID: ${{ github.event.inputs.release_id }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + with: + script: | + const script = require( './package/woocommerce/.github/workflows/scripts/fetch-asset-id.js' ) + await script({github, context, core}) + + - name: Install PNPM and install dependencies + working-directory: package/woocommerce + run: | + npm install -g pnpm + pnpm install + + - name: Load docker images and start containers. + working-directory: package/woocommerce/plugins/woocommerce + env: + LATEST_WP_VERSION_MINUS: ${{ matrix.wp }} + run: pnpm nx docker-up woocommerce + + - name: Move current directory to code. We will install zip file in this dir later. + run: mv ./package/woocommerce/plugins/woocommerce/* ./code/woocommerce + + - name: Download WooCommerce release zip + working-directory: tmp + run: | + curl https://api.github.com/repos/${{ github.repository }}/releases/assets/${{ steps.fetch_asset_id.outputs.asset_id }} -LJOH 'Accept: application/octet-stream' + + unzip woocommerce.zip -d woocommerce + mv woocommerce/woocommerce/* ../package/woocommerce/plugins/woocommerce/ + + - name: Run tests command. + working-directory: package/woocommerce/plugins/woocommerce + env: + WC_E2E_SCREENSHOTS: 1 + E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }} + E2E_SLACK_CHANNEL: ${{ secrets.RELEASE_TEST_SLACK_CHANNEL }} + PLUGIN_REPOSITORY: ${{ matrix.private && secrets[matrix.repo] || matrix.repo }} + PLUGIN_NAME: ${{ matrix.plugin }} + GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }} + run: | + pnpm exec wc-e2e test:e2e tests/e2e/specs/smoke-tests/upload-plugin.js + pnpm exec wc-e2e test:e2e diff --git a/.github/workflows/stalebot.yml b/.github/workflows/stalebot.yml new file mode 100644 index 00000000000..f6bd4bcb592 --- /dev/null +++ b/.github/workflows/stalebot.yml @@ -0,0 +1,24 @@ +name: 'Close stale needs-feedback issues' +on: + schedule: + - cron: '21 0 * * *' + +jobs: + stale: + if: | + ! contains(github.event.issue.labels.*.name, 'type: enhancement') + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: "As a part of this repository's maintenance, this issue is being marked as stale due to inactivity. Please feel free to comment on it in case we missed something.\n\n###### After 7 days with no activity this issue will be automatically be closed." + close-issue-message: 'This issue was closed because it has been 14 days with no activity.' + days-before-issue-stale: 7 + days-before-issue-close: 7 + days-before-pr-close: -1 + stale-issue-label: 'status: stale' + stale-pr-label: 'status: stale' + only-issue-labels: 'needs: author feedback' + close-issue-label: "status: can't reproduce" + ascending: true diff --git a/.github/workflows/triage-label.yml b/.github/workflows/triage-label.yml new file mode 100644 index 00000000000..992ccea1258 --- /dev/null +++ b/.github/workflows/triage-label.yml @@ -0,0 +1,15 @@ +name: Add Triage Label + +on: + issues: + types: opened + +jobs: + add_label: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-ecosystem/action-add-labels@v1 + if: github.event.issue.labels[0] == null + with: + labels: 'status: awaiting triage' diff --git a/.github/workflows/triage-replies.yml b/.github/workflows/triage-replies.yml new file mode 100644 index 00000000000..78295cf08bb --- /dev/null +++ b/.github/workflows/triage-replies.yml @@ -0,0 +1,166 @@ +name: Add issue triage comments. +on: + issues: + types: + - labeled +jobs: + add-dev-comment: + if: "github.event.label.name == 'needs: developer feedback'" + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Add developer feedback comment + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.WC_BOT_TRIAGE_TOKEN }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'Hi @${{ github.event.issue.user.login }},\n\n\ + Thank you for opening the issue! It requires further feedback from the WooCommerce Core team.\n\n\ + We are adding the `needs developer feedback` label to this issue so that the Core team could take a look.\n\n\ + Please note it may take a few days for them to get to this issue. Thank you for your patience.' + }) + add-reproduction-comment: + if: "github.event.label.name == 'status: reproduction'" + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Add needs reproduction comment + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.WC_BOT_TRIAGE_TOKEN }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'We are adding the `status: needs reproduction` label to this issue to try reproduce it on the \ + current released version of WooCommerce.\n\n\ + Thank you for your patience.' + }) + add-support-comment: + if: "github.event.label.name == 'type: support request'" + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Add support request comment + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.WC_BOT_TRIAGE_TOKEN }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'Hi @${{ github.event.issue.user.login }},\n\n\ + While our goal is to address all the issues reported in this repository, \ + GitHub should be treated as a place to report confirmed bugs only.\n\n\ + The type of issue you submitted looks like a support request which may or may not reveal a bug once proper \ + troubleshooting is done. In order to confirm the bug, please follow one of the steps below:\n\n\ + - Review [WooCommerce Self-Service Guide](https://docs.woocommerce.com/document/woocommerce-self-service-guide/) \ + to see if the solutions listed there apply to your case;\n\ + - If you are a paying customer of WooCommerce, contact WooCommerce support by \ + [opening a ticket or starting a live chat](https://woocommerce.com/contact-us/);\n\ + - Make a post on [WooCommerce community forum](https://wordpress.org/support/plugin/woocommerce/)\n\n\ + If you confirm the bug, please provide us with clear steps to reproduce it.\n\n\ + We are closing this issue for now as it seems to be a support request and not a bug. \ + If we missed something, please leave a comment and we will take a second look.' + }) + - name: Close support request issue + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.WC_BOT_TRIAGE_TOKEN }} + script: | + github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + state: 'closed' + }) + add-votes-comment: + if: "github.event.label.name == 'needs: votes'" + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Add votes needed comment + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.WC_BOT_TRIAGE_TOKEN }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: "Thanks for the suggestion @${{ github.event.issue.user.login }},\n\n\ + While we appreciate you sharing your ideas with us, it doesn't fit in with our current priorities for the project.\n\ + At some point, we may revisit our priorities and look through the list of suggestions like this one to see if it \ + warrants a second look.\n\n\ + In the meantime, we are going to close this issue with the `votes needed` label and evaluate over time if this \ + issue collects more feedback.\n\n\ + Don't be alarmed if you don't see any activity on this issue for a while. \ + We'll keep an eye on the popularity of this request." + }) + - name: Close votes needed issue + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.WC_BOT_TRIAGE_TOKEN }} + script: | + github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + state: 'closed' + }) + fill-template-comment: + if: "github.event.label.name == 'needs: template'" + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Add reply to fill template + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.WC_BOT_TRIAGE_TOKEN }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: "Hi @${{ github.event.issue.user.login }},\n\n\ + Thank you for submitting the issue. However, you didn't fill out the details of the bug report template that we ask for. Without these details, we can't fully evaluate this issue. Please provide us with the information requested so we could take a look further.\n\n\ + **Describe the bug**\n\n\ + A clear and concise description of what the bug is. Please be as descriptive as possible; issues lacking detail, or for any other reason than to report a bug, may be closed without action.\n\n\ + **To Reproduce**\n\n\ + Steps to reproduce the behavior:\n\n\ + 1. Go to '...'\n\ + 2. Click on '....'\n\ + 3. Scroll down to '....'\n\ + 4. See error\n\n\ + **Screenshots**\n\n\ + If applicable, add screenshots to help explain your problem.\n\n\ + **Expected behavior**\n\n\ + A clear and concise description of what you expected to happen.\n\n\ + **Isolating the problem (mark completed items with an [x]):**\n\n\ + - [ ] I have deactivated other plugins and confirmed this bug occurs when only WooCommerce plugin is active.\n\ + - [ ] This bug happens with a default WordPress theme active, or [Storefront](https://woocommerce.com/storefront/).\n\ + - [ ] I can reproduce this bug consistently using the steps above.\n\n\ + **WordPress Environment**\n\n\ + Copy and paste the system status report from **WooCommerce > System Status** in WordPress admin." + }) + - name: remove-needs-template-label + uses: actions-ecosystem/action-remove-labels@v1 + with: + github-token: ${{ secrets.WC_BOT_TRIAGE_TOKEN }} + labels: 'needs: template' + - name: add-needs-author-feedback-label + uses: actions-ecosystem/action-add-labels@v1 + with: + github-token: ${{ secrets.WC_BOT_TRIAGE_TOKEN }} + labels: 'needs: author feedback' diff --git a/.github/workflows/update-feedback-labels.yml b/.github/workflows/update-feedback-labels.yml new file mode 100644 index 00000000000..9c055c4d33e --- /dev/null +++ b/.github/workflows/update-feedback-labels.yml @@ -0,0 +1,28 @@ +name: 'Update contributor feedback labels on comment' +on: 'issue_comment' + +jobs: + feedback: + if: | + github.actor != 'github-actions' && + github.actor == github.event.issue.user.login && + github.event.issue && + github.event.issue.state == 'open' && + contains(github.event.issue.labels.*.name, 'needs: author feedback') + runs-on: ubuntu-latest + steps: + - name: Add has feedback + uses: actions-ecosystem/action-add-labels@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + labels: 'needs: triage feedback' + - name: remove needs feedback + uses: actions-ecosystem/action-remove-labels@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + labels: 'needs: author feedback' + - name: remove stale + uses: actions-ecosystem/action-remove-labels@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + labels: 'status: stale' diff --git a/.gitignore b/.gitignore index c86c350cc91..a2e8f72a8cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,78 +1,85 @@ -# Editors +# Operating System files +.DS_Store +Thumbs.db + +# IDE files +.idea +.vscode/* project.xml project.properties -/nbproject/private/ -.buildpath .project .settings* -.idea -.vscode *.sublime-project *.sublime-workspace .sublimelinterrc -*.swp + +# Excluded IDE Files for developer experience tooling within workspace +!.vscode/tasks.json # Grunt -node_modules/ none # Sass .sass-cache/ -# Compiled CSS -/assets/css/*.css -/assets/css/photoswipe/**/*.min.css +# Logs +logs/ -# Minified JS -/assets/js/**/*.min.js +# Eslint Cache +.eslintcache -# OS X metadata -.DS_Store +# Environment files +wp-cli.local.yml +yarn-error.log +npm-debug.log +.pnpm-debug.log -# Windows junk -Thumbs.db +# Build files +*.sql +*.swp +*.zip -# Behat/CLI Tests -tests/cli/installer -tests/cli/composer.phar -tests/cli/composer.lock -tests/cli/composer.json -tests/cli/vendor +# Built packages +build/ +build-module/ +build-style/ +build-types/ +dist/ + +# Project files +node_modules/ +vendor/ + +# TypeScript files +tsconfig.tsbuildinfo + +# Node Package Dependencies +package-lock.json + +# wp-env config +.wp-env.override.json # Unit tests -/tmp -/tests/bin/tmp -/tests/e2e/config/local-*.json -/tests/e2e/config/local.json -/tests/e2e/config/default.json -/tests/e2e/env/config/default.json -/tests/e2e/docker -/tests/e2e/env/docker/wp-cli/initialize.sh -/tests/e2e/env/build/ -/tests/e2e/env/build-module/ -/tests/e2e/utils/build/ -/tests/e2e/utils/build-module/ - -# Logs -/logs +tmp/ # Composer -/vendor/ -/bin/composer/**/vendor/ -/lib/vendor/ +vendor/ +bin/composer/**/vendor/ +lib/vendor/ contributors.md contributors.html -# Packages -/packages/* -!/packages/README.md +# Yarn +yarn.lock -# Screenshots for e2e tests failures -/screenshots/ +# Editors +nbproject/private/ -# Language files -i18n/languages/woocommerce.pot +# Test Results +test-results.json -# Build -build/ -woocommerce.zip +# Admin Feature config +plugins/woocommerce/includes/react-admin/feature-config.php + +# PHP lint +phpcs-report.xml diff --git a/.husky/post-merge b/.husky/post-merge new file mode 100755 index 00000000000..48bc4affda4 --- /dev/null +++ b/.husky/post-merge @@ -0,0 +1,5 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +pnpm install +pnpm nx affected --target="composer-install" --base=ORIG_HEAD --head=HEAD diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000000..5e592735698 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +pnpm exec lint-staged diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 00000000000..e52fd7c5635 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +./bin/pre-push.sh diff --git a/.nvmrc b/.nvmrc index dae199aecb1..6f7f377bf51 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v12 +v16 diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 00000000000..69be168d3b1 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,3 @@ +// Import the default config file and expose it in the project root. +// Useful for editor integrations. +module.exports = require( '@wordpress/prettier-config' ); \ No newline at end of file diff --git a/.stylelintrc b/.stylelintrc index 59af9ca1e9b..b727699279b 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -1,3 +1,3 @@ { - "extends": "stylelint-config-wordpress", + "extends": "@wordpress/stylelint-config", } diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e637ca63572..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,110 +0,0 @@ -version: ~> 1.0 - -# Specifies that Travis should create builds for master and release branches and also tags. -branches: - only: - - master - - /^\d+\.\d+(\.\d+)?(-\S*)?$/ - - /^release\// - -language: php -os: - - linux -dist: xenial - -# Test main supported versions of PHP against latest WP. -php: - - "7.0" - - "7.1" - - "7.2" - - "7.3" - - "7.4" - - "8.0" - -env: - - WP_VERSION=latest WP_MULTISITE=0 - -# Additional tests against stable PHP (min version is 7.0) -# and code coverage report. -jobs: - fast_finish: true - include: - - name: "Core E2E Tests" - env: WP_VERSION=latest WP_MULTISITE=0 RUN_E2E=1 - install: - - nvm install - - npm install - - composer install --no-dev - script: - - npm run build:assets - - npm run docker:up - - npm run test:e2e - after_script: - - npm run docker:down - - name: "WP Nightly" - php: "7.4" - env: WP_VERSION=nightly WP_MULTISITE=0 - - name: "WP Latest - 1" - php: "7.2" - env: WP_VERSION=5.5 WP_MULTISITE=0 - - name: "WP Latest - 2" - php: "7.2" - env: WP_VERSION=5.4 WP_MULTISITE=0 - - name: "Code Standards" - php: "7.4" - env: WP_VERSION=latest WP_MULTISITE=0 RUN_PHPCS=1 - - name: "Code Coverage" - php: "7.4" - env: WP_VERSION=latest WP_MULTISITE=0 RUN_CODE_COVERAGE=1 - allow_failures: - - php: "7.4" - env: WP_VERSION=latest WP_MULTISITE=0 RUN_CODE_COVERAGE=1 - -# Git clone depth -# By default Travis CI clones repositories to a depth of 50 commits. Using a depth of 1 makes this step a bit faster. -git: - depth: 1 - -# Since Xenial services are not started by default, we need to instruct it below to start. -services: - - mysql - - docker - -cache: - directories: - - $HOME/.composer/cache - -# Composer 2.0.7 introduced a change that broke the jetpack autoloader in PHP 7.0 - 7.3. -before_install: - - composer self-update 2.0.6 - -install: - - export PATH="$HOME/.composer/vendor/bin:$PATH" - - | - # Remove Xdebug for a huge performance increase: - if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then - phpenv config-rm xdebug.ini - else - echo "xdebug.ini does not exist" - fi - - composer install - - | - if [ "$(php -r "echo version_compare(PHP_VERSION,'8.0','>=');")" ]; then - curl -L https://github.com/woocommerce/phpunit/archive/add-compatibility-with-php8-to-phpunit-7.zip -o /tmp/phpunit-7.5-fork.zip - unzip -d /tmp/phpunit-7.5-fork /tmp/phpunit-7.5-fork.zip - composer bin phpunit config --unset platform - composer bin phpunit config repositories.0 '{"type": "path", "url": "/tmp/phpunit-7.5-fork/phpunit-add-compatibility-with-php8-to-phpunit-7", "options": {"symlink": false}}' - composer bin phpunit require --dev -W phpunit/phpunit:@dev --ignore-platform-reqs - fi - - | - # Install WP Test suite: - if [[ ! -z "$WP_VERSION" ]]; then - bash tests/bin/install.sh woocommerce_test root '' localhost $WP_VERSION - fi - -script: - - bash tests/bin/phpunit.sh - - bash tests/bin/phpcs.sh - -after_script: - - bash tests/bin/travis.sh after diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000000..309c50d8e8d --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,29 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "command": "pnpm tsc -b tsconfig.base.json", + "type": "shell", + "problemMatcher": [ "$tsc" ], + "label": "Typescript compile", + "detail": "Run tsc against tsconfig.base.json", + "runOptions": { + "runOn": "default" + } + }, + { + "command": "pnpm tsc -b tsconfig.base.json --watch", + "type": "shell", + "problemMatcher": { + "base": "$tsc-watch", + "applyTo": "allDocuments" + }, + "isBackground": true, + "label": "Incremental Typescript compile", + "detail": "Incremental background type checks", + "runOptions": { + "runOn": "folderOpen" + } + } + ] +} diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 00000000000..8affc6b777e --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,138 @@ +# WooCommerce Development Setup with WP-ENV + +Docker development setup for WooCommerce with WP-ENV. + +## Prerequisites + +Please install WP-ENV before getting started. You can find more about WP-ENV on [here](https://github.com/WordPress/gutenberg/tree/master/packages/env). + +The following command installs WP-ENV globally. + +`npm -g i @wordpress/env` + +If you don't already have [pnpm](https://pnpm.io/installation) installed, you can quickly add it using NPM. + +`npm install -g pnpm` + +## Starting WP-ENV + +1. Navigate to the root of WooCommerce source code. +2. Start the docker container by running `wp-env start` + +You should see the following output + +``` +WordPress development site started at http://localhost:8888/ +WordPress test site started at http://localhost:8889/ +MySQL is listening on port 55003 +``` + +The port # might be different depending on your `.wp-env.override.json` configuration. + +## Getting Started with Developing + +Once you have WP-ENV container up, we need to run a few commands to start developing. + +1. Run `pnpm install` to install npm modules. +2. Run `pnpm nx build woocommerce` to build core. +3. Run `pnpm nx composer-install woocommerce` to install PHP dependencies. + +If you don't have Composer available locally, run the following command. It runs the command in WP-ENV container. + +`wp-env run composer composer install` + +You might also want to run `pnpm start` to watch your CSS and JS changes if you are working on the frontend. + +You're now ready to develop! + +### Typescript Checking + +Typescript is progressively being implemented in this repository, and you might come across some files that are `.ts` or `.tsx`. By default, a VSCode environment will run type checking on such files that are currently open. + +As of now, some parts of the codebase that were imported from the Woocommerce-Admin repository, into the `plugins/woocommerce-admin/client` directory, still fail Typescript checking. This has been scheduled on the team's backlog to be fixed. + +In order to run type checking across the entire repository, you can run this command in your shell, from the root of this repository: + +```sh +pnpm tsc -b tsconfig.base.json +``` + +For better developer experience, the folder `.vscode/tasks.json` has two VSCode tasks to run these commands automatically as well as to parse the output and highlight the errors in the `Problems` tab and in the file explorer pane. The first task runs it once, the second one runs it in the background upon saving of any modified files. This task is also automatically prompted by VSCode to be run upon opening the folder. + + +## Using Xdebug + +Please refer to [WP-ENV official README](https://github.com/WordPress/gutenberg/tree/master/packages/env#using-xdebug) section for setting up Xdebug. + +## Overriding the Default Configuration + +The default configuration comes with PHP 7.4, WooCommerce 5.0, and a few WordPress config values. + +You can create `.wp-env.override.json` file and override the default configuration values. + +You can find more about `.wp-env.override.json` configuration [here](https://github.com/WordPress/gutenberg/tree/master/packages/env#wp-envoverridejson). + +**Example: Overriding PHP version to 8.0** + +Create `.wp-env.override.json` in the root directory with the following content. + +```json +{ + "phpVersion": "8.0" +} +``` + +**Exampe: Adding a locally installed plugin** + +Method 1 - Adding to the `plugins` array + +Open the default `.wp-env.json` and copy `plugins` array and paste it into the `.wp-env.override.json` and add your locally installed plugin. Copying the default `plugins` is needed as WP-ENV does not merge the values of the `plugins`. + +```json +{ + "plugins": [ + "./plugins/woocommerce", + "https://downloads.wordpress.org/plugin/wp-crontrol.1.10.0.zip" + ] +} +``` + +Method 2 - Adding to the `mappings` + +This method is simpler, but the plugin does not get activated on startup. You need to manually activate it yourself on the first startup. + +```json +{ + "mappings": { + "wp-content/plugins/wp-crontrol": "../woocommerce" + } +} +``` + +## Accessing MySQL + +The MySQL port can change when you restart your container. + +You can get the current MySQL port from the output of `wp-env start` command. + +1. Open your choice of MySQL tool. +2. Use the following values to access the MySQL container. + +| Name | Value | +| -------- | --------------------- | +| Host | 127.0.0.1 | +| Username | root | +| Password | password | +| Port | Port from the command | + +## HOWTOs + +##### How do I ssh into the container? + +Run the following command to ssh into the container +`wp-env run wordpress /bin/bash` + +You can run a command in the container with the following syntax. You can find more about on the `run` command [here](https://github.com/WordPress/gutenberg/tree/master/packages/env#wp-env-run-container-command) + +Syntax: +`wp-env run :container-type :linux-command` diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index dc04e689442..00000000000 --- a/Gruntfile.js +++ /dev/null @@ -1,231 +0,0 @@ -module.exports = function( grunt ) { - 'use strict'; - var sass = require( 'node-sass' ); - - grunt.initConfig({ - - // Setting folder templates. - dirs: { - css: 'assets/css', - fonts: 'assets/fonts', - images: 'assets/images', - js: 'assets/js', - php: 'includes' - }, - - // JavaScript linting with ESLint. - eslint: { - src: [ - '<%= dirs.js %>/admin/*.js', - '!<%= dirs.js %>/admin/*.min.js', - '<%= dirs.js %>/frontend/*.js', - '!<%= dirs.js %>/frontend/*.min.js' - ] - }, - - // Sass linting with Stylelint. - stylelint: { - options: { - configFile: '.stylelintrc' - }, - all: [ - '<%= dirs.css %>/*.scss', - '!<%= dirs.css %>/select2.scss' - ] - }, - - // Minify .js files. - uglify: { - options: { - ie8: true, - parse: { - strict: false - }, - output: { - comments : /@license|@preserve|^!/ - } - }, - js_assets: { - files: [{ - expand: true, - cwd: '<%= dirs.js %>/', - src: [ - '**/*.js', - '!**/*.min.js' - ], - extDot: 'last', - dest: '<%= dirs.js %>', - ext: '.min.js' - }] - } - }, - - // Compile all .scss files. - sass: { - compile: { - options: { - implementation: sass, - sourceMap: 'none' - }, - files: [{ - expand: true, - cwd: '<%= dirs.css %>/', - src: ['*.scss'], - dest: '<%= dirs.css %>/', - ext: '.css' - }] - } - }, - - // Generate RTL .css files. - rtlcss: { - woocommerce: { - expand: true, - cwd: '<%= dirs.css %>', - src: [ - '*.css', - '!select2.css', - '!*-rtl.css' - ], - dest: '<%= dirs.css %>/', - ext: '-rtl.css' - } - }, - - // Minify all .css files. - cssmin: { - minify: { - files: [ - { - expand: true, - cwd: '<%= dirs.css %>/', - src: ['*.css'], - dest: '<%= dirs.css %>/', - ext: '.css' - }, - { - expand: true, - cwd: '<%= dirs.css %>/photoswipe/', - src: ['*.css', '!*.min.css'], - dest: '<%= dirs.css %>/photoswipe/', - ext: '.min.css' - }, - { - expand: true, - cwd: '<%= dirs.css %>/photoswipe/default-skin/', - src: ['*.css', '!*.min.css'], - dest: '<%= dirs.css %>/photoswipe/default-skin/', - ext: '.min.css' - } - ] - } - }, - - // Concatenate select2.css onto the admin.css files. - concat: { - admin: { - files: { - '<%= dirs.css %>/admin.css' : ['<%= dirs.css %>/select2.css', '<%= dirs.css %>/admin.css'], - '<%= dirs.css %>/admin-rtl.css' : ['<%= dirs.css %>/select2.css', '<%= dirs.css %>/admin-rtl.css'] - } - } - }, - - // Watch changes for assets. - watch: { - css: { - files: ['<%= dirs.css %>/*.scss'], - tasks: ['sass', 'rtlcss', 'postcss', 'cssmin', 'concat'] - }, - js: { - files: [ - 'GruntFile.js', - '<%= dirs.js %>/**/*.js', - '!<%= dirs.js %>/**/*.min.js' - ], - tasks: ['eslint','newer:uglify'] - } - }, - - // PHP Code Sniffer. - phpcs: { - options: { - bin: 'vendor/bin/phpcs' - }, - dist: { - src: [ - '**/*.php', // Include all php files. - '!includes/api/legacy/**', - '!includes/libraries/**', - '!node_modules/**', - '!tests/cli/**', - '!tmp/**', - '!vendor/**' - ] - } - }, - - // Autoprefixer. - postcss: { - options: { - processors: [ - require( 'autoprefixer' ) - ] - }, - dist: { - src: [ - '<%= dirs.css %>/*.css' - ] - } - } - }); - - // Load NPM tasks to be used here. - grunt.loadNpmTasks( 'grunt-sass' ); - grunt.loadNpmTasks( 'grunt-phpcs' ); - grunt.loadNpmTasks( 'grunt-rtlcss' ); - grunt.loadNpmTasks( 'grunt-postcss' ); - grunt.loadNpmTasks( 'grunt-stylelint' ); - grunt.loadNpmTasks( 'gruntify-eslint' ); - grunt.loadNpmTasks( 'grunt-contrib-uglify' ); - grunt.loadNpmTasks( 'grunt-contrib-cssmin' ); - grunt.loadNpmTasks( 'grunt-contrib-concat' ); - grunt.loadNpmTasks( 'grunt-contrib-copy' ); - grunt.loadNpmTasks( 'grunt-contrib-watch' ); - grunt.loadNpmTasks( 'grunt-contrib-clean' ); - grunt.loadNpmTasks( 'grunt-newer' ); - - // Register tasks. - grunt.registerTask( 'default', [ - 'js', - 'css' - ]); - - grunt.registerTask( 'js', [ - 'eslint', - 'uglify:js_assets' - ]); - - grunt.registerTask( 'css', [ - 'sass', - 'rtlcss', - 'postcss', - 'cssmin', - 'concat' - ]); - - grunt.registerTask( 'assets', [ - 'js', - 'css' - ]); - - grunt.registerTask( 'e2e-build', [ - 'uglify:js_assets', - 'css' - ]); - - // Only an alias to 'default' task. - grunt.registerTask( 'dev', [ - 'default' - ]); -}; diff --git a/README.md b/README.md index b239d70574e..b65934c26c6 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ Latest Stable Version WordPress.org downloads WordPress.org rating -Build Status -codecov +Build Status +codecov

Welcome to the WooCommerce repository on GitHub. Here you can browse the source, look at open issues and keep track of development. We recommend all developers to follow the [WooCommerce development blog](https://woocommerce.wordpress.com/) to stay up to date about everything happening in the project. You can also [follow @DevelopWC](https://twitter.com/DevelopWC) on Twitter for the latest development updates. @@ -18,6 +18,7 @@ If you are not a developer, please use the [WooCommerce plugin page](https://wor * [WooCommerce Developer Documentation](https://github.com/woocommerce/woocommerce/wiki) * [WooCommerce Code Reference](https://docs.woocommerce.com/wc-apidocs/) * [WooCommerce REST API Docs](https://woocommerce.github.io/woocommerce-rest-api-docs/) +* [Setting up a development environment](https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment) ## Reporting Security Issues To disclose a security issue to our team, [please submit a report via HackerOne here](https://hackerone.com/automattic/). @@ -34,4 +35,10 @@ This repository is not suitable for support. Please don't use our issue tracker Support requests in issues on this repository will be closed on sight. ## Contributing to WooCommerce -If you have a patch or have stumbled upon an issue with WooCommerce core, you can contribute this back to the code. Please read our [contributor guidelines](https://github.com/woocommerce/woocommerce/blob/master/.github/CONTRIBUTING.md) for more information how you can do this. +If you have a patch or have stumbled upon an issue with WooCommerce core, you can contribute this back to the code. Please read our [contributor guidelines](https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md) for more information how you can do this. + +

+

+ Made with 💜 by WooCommerce.
+ We're hiring! Come work with us! +

diff --git a/assets/css/_mixins.scss b/assets/css/_mixins.scss deleted file mode 100644 index 85a1eee864d..00000000000 --- a/assets/css/_mixins.scss +++ /dev/null @@ -1,288 +0,0 @@ -/** - * Deprecated - * Fallback for bourbon equivalent - */ -@mixin clearfix() { - *zoom: 1; - - &::before, - &::after { - content: " "; - display: table; - } - - &::after { - clear: both; - } -} - -/** - * Deprecated - * Vendor prefix no longer required. - */ -@mixin border_radius($radius: 4px) { - border-radius: $radius; -} - -/** - * Deprecated - * Vendor prefix no longer required. - */ -@mixin border_radius_right($radius: 4px) { - border-top-right-radius: $radius; - border-bottom-right-radius: $radius; -} - -/** - * Deprecated - * Vendor prefix no longer required. - */ -@mixin border_radius_left($radius: 4px) { - border-top-left-radius: $radius; - border-bottom-left-radius: $radius; -} - -/** - * Deprecated - * Vendor prefix no longer required. - */ -@mixin border_radius_bottom($radius: 4px) { - border-bottom-left-radius: $radius; - border-bottom-right-radius: $radius; -} - -/** - * Deprecated - * Vendor prefix no longer required. - */ -@mixin border_radius_top($radius: 4px) { - border-top-left-radius: $radius; - border-top-right-radius: $radius; -} - -/** - * Deprecated - * Vendor prefix no longer required. - */ -@mixin opacity( $opacity: 0.75 ) { - opacity: $opacity; -} - -/** - * Deprecated - * Vendor prefix no longer required. - */ -@mixin box_shadow($shadow_x: 3px, $shadow_y: 3px, $shadow_rad: 3px, $shadow_in: 3px, $shadow_color: #888) { - box-shadow: $shadow_x $shadow_y $shadow_rad $shadow_in $shadow_color; -} - -/** - * Deprecated - * Vendor prefix no longer required. - */ -@mixin inset_box_shadow($shadow_x: 3px, $shadow_y: 3px, $shadow_rad: 3px, $shadow_in: 3px, $shadow_color: #888) { - box-shadow: inset $shadow_x $shadow_y $shadow_rad $shadow_in $shadow_color; -} - -/** - * Deprecated - * Vendor prefix no longer required. - */ -@mixin text_shadow($shadow_x: 3px, $shadow_y: 3px, $shadow_rad: 3px, $shadow_color: #fff) { - text-shadow: $shadow_x $shadow_y $shadow_rad $shadow_color; -} - -/** - * Deprecated - * Vendor prefix no longer required. - */ -@mixin vertical_gradient($from: #000, $to: #fff) { - background-color: $from; - background: -webkit-linear-gradient($from, $to); -} - -/** - * Deprecated - * Vendor prefix no longer required. - */ -@mixin transition($selector: all, $animation: ease-in-out, $duration: 0.2s) { - transition: $selector $animation $duration; -} - -/** - * Deprecated - * Use bourbon mixin instead `@include transform(scale(1.5));` - */ -@mixin scale($ratio: 1.5) { - -webkit-transform: scale($ratio); - transform: scale($ratio); -} - -/** - * Deprecated - * Use bourbon mixin instead `@include box-sizing(border-box);` - */ -@mixin borderbox() { - box-sizing: border-box; -} - -@mixin darkorlighttextshadow($a, $opacity: 0.8) { - - @if lightness($a) >= 65% { - - @include text_shadow(0, -1px, 0, rgba(0, 0, 0, $opacity)); - } - - @else { - - @include text_shadow(0, 1px, 0, rgba(255, 255, 255, $opacity)); - } -} - -/** - * Objects - */ -@mixin menu() { - - @include clearfix(); - - li { - display: inline-block; - } -} - -@mixin mediaright() { - - @include clearfix(); - - img { - float: right; - height: auto; - } -} - -@mixin medialeft() { - - @include clearfix(); - - img { - float: right; - height: auto; - } -} - -@mixin ir() { - display: block; - text-indent: -9999px; - position: relative; - height: 1em; - width: 1em; -} - -@mixin icon( $glyph: "\e001" ) { - font-family: "WooCommerce"; - speak: never; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; - margin: 0; - text-indent: 0; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - text-align: center; - content: $glyph; -} - -@mixin icon_dashicons( $glyph: "\f333" ) { - font-family: "Dashicons"; - speak: never; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; - -webkit-font-smoothing: antialiased; - margin: 0; - text-indent: 0; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - text-align: center; - content: $glyph; -} - -@mixin iconbefore( $glyph: "\e001" ) { - font-family: "WooCommerce"; - speak: never; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; - -webkit-font-smoothing: antialiased; - margin-right: 0.618em; - content: $glyph; - text-decoration: none; -} - -@mixin iconbeforedashicons( $glyph: "\f333" ) { - font-family: "Dashicons"; - speak: never; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; - -webkit-font-smoothing: antialiased; - content: $glyph; - text-decoration: none; -} - -@mixin iconafter( $glyph: "\e001" ) { - font-family: "WooCommerce"; - speak: never; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; - -webkit-font-smoothing: antialiased; - margin-left: 0.618em; - content: $glyph; - text-decoration: none; -} - -@mixin loader() { - - &::before { - height: 1em; - width: 1em; - display: block; - position: absolute; - top: 50%; - left: 50%; - margin-left: -0.5em; - margin-top: -0.5em; - content: ""; - animation: spin 1s ease-in-out infinite; - background: url("../images/icons/loader.svg") center center; - background-size: cover; - line-height: 1; - text-align: center; - font-size: 2em; - color: rgba(#000, 0.75); - } -} - -@mixin inversebuttoncolors { - background-color: transparent !important; - color: var(--button--color-text-hover) !important; - - &:hover { - background-color: var(--button--color-background) !important; - color: var(--button--color-text) !important; - text-decoration: none !important; - } -} diff --git a/assets/css/admin.scss b/assets/css/admin.scss deleted file mode 100644 index 3dca45bdf8c..00000000000 --- a/assets/css/admin.scss +++ /dev/null @@ -1,7327 +0,0 @@ - -/** - * admin.scss - * General WooCommerce admin styles. Settings, product data tabs, reports, etc. - */ - -/** - * Imports - */ -@import "mixins"; -@import "variables"; -@import "animation"; -@import "fonts"; - -/** - * Styling begins - */ -.blockUI.blockOverlay { - - @include loader(); -} - -.wc_addons_wrap { - max-width: 1200px; - - h1.search-form-title { - clear: left; - padding: 0; - } - - form.search-form { - clear: both; - display: block; - position: relative; - margin-top: 1em; - margin-bottom: 1em; - - input { - border: 1px solid #ddd; - box-shadow: none; - height: 53px; - padding-left: 50px; - width: 100%; - margin: 0; - } - - button { - background: none; - border: none; - cursor: pointer; - height: 53px; - position: absolute; - width: 53px; - } - } - - .update-plugins .update-count { - background-color: #d54e21; - border-radius: 10px; - color: #fff; - display: inline-block; - font-size: 9px; - font-weight: 600; - line-height: 17px; - margin: 1px 0 0 2px; - padding: 0 6px; - vertical-align: text-top; - } - - .addons-featured { - margin: 0; - } - - ul.subsubsub.subsubsub { - margin: -2px 0 12px; - } - - .subsubsub li::after { - content: "|"; - } - - .subsubsub li:last-child::after { - content: ""; - } - - .addons-banner-block-item-icon, - .addons-column-block-item-icon { - align-items: center; - display: flex; - justify-content: center; - } - - .addons-banner-block, - .addons-wcs-banner-block { - background: #fff; - border: 1px solid #ddd; - margin: 0 0 1em 0; - padding: 2em 2em 1em; - } - - .addons-banner-block img { - height: 62px; - } - - .addons-banner-block p { - margin: 0 0 20px; - } - - .addons-banner-block-items { - display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: space-around; - margin: 0 -10px 0 -10px; - } - - .addons-banner-block-item { - border: 1px solid #e6e6e6; - border-radius: 3px; - flex: 1; - margin: 1em; - min-width: 200px; - width: 30%; - } - - .addons-banner-block-item-icon { - background: #f7f7f7; - height: 143px; - } - - .addons-banner-block-item-content { - display: flex; - flex-direction: column; - height: 184px; - justify-content: space-between; - padding: 24px; - } - - .addons-banner-block-item-content h3 { - margin-top: 0; - } - - .addons-banner-block-item-content p { - margin: 0 0 auto; - } - - .addons-wcs-banner-block { - display: flex; - align-items: center; - } - - .addons-wcs-banner-block-image { - background: #f7f7f7; - border: 1px solid #e6e6e6; - margin-right: 2em; - padding: 4em; - - .addons-img { - max-height: 86px; - max-width: 97px; - } - } - - .addons-shipping-methods .addons-wcs-banner-block { - margin-left: 0; - margin-right: 0; - margin-top: 1em; - } - - .addons-wcs-banner-block-content { - display: flex; - flex-direction: column; - justify-content: space-around; - align-self: stretch; - padding: 1em 0; - - h1 { - padding-bottom: 0; - } - - p { - margin-bottom: 0; - } - - .wcs-service-logo { - max-width: 40px; - } - } - - .addons-column-section { - display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: space-around; - } - - .addons-column { - flex: 1; - width: 50%; - padding: 0 0.5em; - } - - .addons-column:nth-child(2) { - margin-right: 0; - } - - .addons-small-light-block, - .addons-small-dark-block, - .addons-column-block { - box-sizing: border-box; - border: 1px solid #ddd; - margin: 0 0 1em; - padding: 20px; - } - - .addons-column-block img { - max-height: 50px; - max-width: 50px; - } - - .addons-small-light-block, - .addons-column-block { - background: #fff; - } - - .addons-column-block-left { - float: left; - } - - .addons-column-block-right { - float: right; - } - - .addons-column-block-item { - border-top: 2px solid #f9f9f9; - flex-direction: row; - flex-wrap: wrap; - justify-content: space-between; - margin: 0 -20px; - padding: 20px; - } - - .addons-column-block-item-icon { - background: #f7f7f7; - border: 1px solid #e6e6e6; - height: 100px; - margin: 0 10px 10px 0; - width: 100px; - } - - .addons-column-block-item-content { - display: flex; - flex: 1; - flex-wrap: wrap; - height: 20%; - justify-content: space-between; - min-width: 200px; - } - - .addons-column-block-item-content h2 { - float: left; - margin-top: 8px; - } - - .addons-column-block-item-content a { - float: right; - } - - .addons-column-block-item-content p { - float: left; - } - - .addons-banner-block-item, - .addons-column-block-item { - display: none; - } - - .addons-banner-block-item:nth-child(-n+3) { - display: block; - } - - .addons-column-block-item:nth-of-type(-n+3) { - display: flex; - } - - .addons-small-dark-block { - background-color: #54687d; - text-align: center; - } - - .addons-small-dark-items { - display: flex; - flex-wrap: wrap; - justify-content: space-around; - } - - .addons-small-dark-item { - margin: 0 0 20px; - } - - .addons-small-dark-block h1 { - color: #fff; - } - - .addons-small-dark-block p { - color: #fafafa; - } - - .addons-small-dark-item-icon img { - height: 30px; - } - - .addons-small-dark-item a { - margin: 28px auto 0; - } - - .addons-small-light-block { - display: flex; - flex-wrap: wrap; - } - - .addons-small-light-block h1 { - margin-top: -12px; - } - - .addons-small-light-block p { - margin-top: 0; - } - - .addons-small-light-block img { - height: 225px; - margin: 0 0 0 -20px; - } - - .addons-small-light-block-content { - display: flex; - flex: 1 1 100px; - flex-direction: column; - justify-content: space-around; - } - - .addons-small-light-block-buttons { - display: flex; - justify-content: space-between; - } - - .addons-small-light-block-content a { - width: 48%; - } - - .addons-button { - border-radius: 3px; - cursor: pointer; - display: block; - height: 37px; - line-height: 37px; - text-align: center; - text-decoration: none; - width: 124px; - } - - .addons-button-solid { - background-color: #674399; - color: #fff; - } - - .addons-button-solid:hover { - color: #fff; - opacity: 0.8; - } - - .addons-button-outline-green { - border: 1px solid #73ae39; - color: #73ae39; - } - - .addons-button-outline-green:hover { - color: #73ae39; - opacity: 0.8; - } - - .addons-button-outline-purple { - border: 1px solid #674399; - color: #674399; - } - - .addons-button-outline-purple:hover { - color: #674399; - opacity: 0.8; - } - - .addons-button-outline-white { - border: 1px solid #fff; - color: #fff; - } - - .addons-button-outline-white:hover { - color: #fff; - opacity: 0.8; - } - - .addons-button-installed { - background: #e6e6e6; - color: #3c3c3c; - } - - .addons-button-installed:hover { - color: #3c3c3c; - opacity: 0.8; - } - - @media only screen and (max-width: 400px) { - - .addons-featured { - margin: -1% -5%; - } - - .addons-button { - width: 100%; - } - - .addons-small-dark-item { - width: 100%; - } - - .addons-column-block-item-icon { - background: none; - border: none; - height: 75px; - margin: 0 10px 10px 0; - width: 75px; - } - } - - .products { - overflow: hidden; - display: flex; - flex-flow: row; - flex-wrap: wrap; - margin: 0 -0.5em; - - li { - float: left; - border: 1px solid #ddd; - margin: 0 0.5em 1em !important; - padding: 0; - vertical-align: top; - width: 25%; - min-width: 280px; - min-height: 220px; - flex: 1; - overflow: hidden; - background: #f5f5f5; - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.2), - inset 0 -1px 0 rgba(0, 0, 0, 0.1); - - a { - text-decoration: none; - color: inherit; - display: block; - height: 100%; - - .product-img-wrap { - background: #fff; - display: block; - } - - img { - max-width: 258px; - max-height: 24px; - padding: 17px 20px; - display: block; - margin: 0; - background: #fff; - border-right: 260px solid #fff; - } - - img.extension-thumb + h3 { - display: none; - } - - .price { - display: none; - } - - h2, - h3 { - margin: 0 !important; - padding: 20px !important; - background: #fff; - } - - p { - padding: 20px !important; - margin: 0 !important; - border-top: 1px solid #f1f1f1; - } - - &:hover, - &:focus { - background-color: #fff; - } - } - } - } - - .storefront { - background: url("../images/storefront-bg.jpg") bottom right #f6f6f6; - border: 1px solid #ddd; - margin-top: 1em; - padding: 20px; - overflow: hidden; - zoom: 1; - - img { - width: 278px; - height: auto; - float: left; - margin: 0 20px 0 0; - box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1); - } - - p { - max-width: 750px; - } - } -} - -.woocommerce-message, -.woocommerce-BlankState { - - a.button-primary, - button.button-primary { - background: #bb77ae; - border-color: #a36597; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; - color: #fff; - text-shadow: 0 -1px 1px #a36597, 1px 0 1px #a36597, 0 1px 1px #a36597, -1px 0 1px #a36597; - display: inline-block; - - &:hover, - &:focus, - &:active { - background: #a36597; - border-color: #a36597; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; - } - } -} - -.woocommerce-message { - position: relative; - overflow: hidden; - - &.updated { - border-left-color: #cc99c2 !important; - } - - a.skip, - a.docs { - text-decoration: none !important; - } - - a.woocommerce-message-close { - position: static; - float: right; - padding: 0 15px 10px 28px; - margin-top: -10px; - font-size: 13px; - line-height: 1.23076923; - text-decoration: none; - - &::before { - position: relative; - top: 18px; - left: -20px; - transition: all 0.1s ease-in-out; - } - } - - .twitter-share-button { - margin-top: -3px; - margin-left: 3px; - vertical-align: middle; - } -} - -#variable_product_options #message, -#variable_product_options .notice { - margin: 10px; -} - -#variable_product_options { - - .form-row select { - max-width: 100%; - } - - .toolbar-top { - - .button { - margin: 1px; - } - } -} - -#product_attributes { - - .toolbar-top { - - .button { - margin: 1px; - } - } -} - -.clear { - clear: both; -} - -.wrap.woocommerce div.updated, -.wrap.woocommerce div.error { - margin-top: 10px; -} - -mark.amount { - background: transparent none; - color: inherit; -} - -/** - * Help Tip - */ -.woocommerce-help-tip { - color: #666; - display: inline-block; - font-size: 1.1em; - font-style: normal; - height: 16px; - line-height: 16px; - position: relative; - vertical-align: middle; - width: 16px; - - &::after { - - @include icon_dashicons( "\f223" ); - cursor: help; - } -} - -.wc-wp-version-gte-53 { - - .woocommerce-help-tip { - font-size: 1.2em; - cursor: help; - } -} - -h2 .woocommerce-help-tip { - margin-top: -5px; - margin-left: 0.25em; -} - -table.wc_status_table { - margin-bottom: 1em; - - h2 { - font-size: 14px; - margin: 0; - } - - tr:nth-child(2n) { - - th, - td { - background: #fcfcfc; - } - } - - th { - font-weight: 700; - padding: 9px; - } - - td:first-child { - width: 33%; - } - - td.help { - width: 1em; - } - - td, - th { - font-size: 1.1em; - font-weight: normal; - - &.run-tool { - text-align: right; - } - - strong.name { - display: block; - margin-bottom: 0.5em; - } - - mark { - background: transparent none; - } - - mark.yes { - color: $green; - } - - mark.no { - color: #999; - } - - mark.error, - .red { - color: $red; - } - - ul { - margin: 0; - } - } - - .help_tip { - cursor: help; - } -} - -table.wc_status_table--tools { - - td, - th { - padding: 2em; - } -} - -.taxonomy-product_cat { - - .check-column .woocommerce-help-tip { - font-size: 1.5em; - margin: -3px 0 0 5px; - display: block; - position: absolute; - } -} - -#debug-report { - display: none; - margin: 10px 0; - padding: 0; - position: relative; - - textarea { - font-family: monospace; - width: 100%; - margin: 0; - height: 300px; - padding: 20px; - border-radius: 0; - resize: none; - font-size: 12px; - line-height: 20px; - outline: 0; - } -} - - -/** - * DB log viewer - */ -.wp-list-table.logs { - - .log-level { - display: inline; - padding: 0.2em 0.6em 0.3em; - font-size: 80%; - font-weight: bold; - line-height: 1; - color: #fff; - text-align: center; - white-space: nowrap; - vertical-align: baseline; - border-radius: 0.2em; - - &:empty { - display: none; - } - } - - /** - * Add color to levels - * - * Descending severity: - * emergency, alert -> red - * critical, error -> orange - * warning, notice -> yellow - * info -> blue - * debug -> gree - */ - - .log-level--emergency, - .log-level--alert { - background-color: #ff4136; - } - - .log-level--critical, - .log-level--error { - background-color: #ff851b; - } - - .log-level--warning, - .log-level--notice { - color: #222; - background-color: #ffdc00; - } - - .log-level--info { - background-color: #0074d9; - } - - .log-level--debug { - background-color: #3d9970; - } - - // Adjust log table columns only when table is not collapsed - @media screen and ( min-width: 783px ) { - - .column-timestamp { - width: 18%; - } - - .column-level { - width: 14%; - } - - .column-source { - width: 15%; - } - } -} - -#log-viewer-select { - padding: 10px 0 8px; - line-height: 28px; - - h2 a { - vertical-align: middle; - } -} - -#log-viewer { - background: #fff; - border: 1px solid #e5e5e5; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); - padding: 5px 20px; - - pre { - font-family: monospace; - white-space: pre-wrap; - word-wrap: break-word; - } -} - -.inline-edit-product.quick-edit-row { - - .inline-edit-col-center, - .inline-edit-col-right { - float: right !important; - } -} - -#woocommerce-fields.inline-edit-col { - clear: left; - - label.featured, - label.manage_stock { - margin-left: 10px; - } - - label.stock_status_field { - clear: both; - float: left; - } - - .dimensions div { - display: block; - margin: 0.2em 0; - - span.title { - display: block; - float: left; - width: 5em; - } - - span.input-text-wrap { - display: block; - margin-left: 5em; - } - } - - .text { - box-sizing: border-box; - width: 99%; - float: left; - margin: 1px 1% 1px 1px; - } - - .length, - .width, - .height { - width: 32.33%; - } - - .height { - margin-right: 0; - } -} - -#woocommerce-fields-bulk.inline-edit-col { - - label { - clear: left; - } - - .inline-edit-group { - - label { - clear: none; - width: 49%; - margin: 0.2em 0; - } - - &.dimensions label { - width: 75%; - max-width: 75%; - } - } - - .regular_price, - .sale_price, - .weight, - .stock, - .length { - box-sizing: border-box; - width: 100%; - margin-left: 4.4em; - } - - .length, - .width, - .height { - box-sizing: border-box; - width: 25%; - } -} - -.column-coupon_code { - line-height: 2.25em; -} - -ul.wc_coupon_list, -.column-coupon_code { - margin: 0; - overflow: hidden; - zoom: 1; - clear: both; -} - -ul.wc_coupon_list { - padding-bottom: 5px; - - li { - margin: 0; - - &.code { - display: inline-block; - position: relative; - padding: 0 0.5em; - background-color: #fff; - border: 1px solid #aaa; - -webkit-box-shadow: 0 1px 0 #dfdfdf; - box-shadow: 0 1px 0 #dfdfdf; - - border-radius: 4px; - margin-right: 5px; - margin-top: 5px; - - &.editable { - padding-right: 2em; - } - - .tips { - cursor: pointer; - - span { - color: #888; - - &:hover { - color: #000; - } - } - } - - .remove-coupon { - text-decoration: none; - color: #888; - position: absolute; - top: 7px; - right: 20px; - - /*rtl:raw: - left: 7px; - */ - - &::before { - - @include icon_dashicons( "\f158" ); - } - - &:hover::before { - color: $red; - } - } - } - } -} - -ul.wc_coupon_list_block { - margin: 0; - padding-bottom: 2px; - - li { - border-top: 1px solid #fff; - border-bottom: 1px solid #ccc; - line-height: 2.5em; - margin: 0; - padding: 0.5em 0; - } - - li:first-child { - border-top: 0; - padding-top: 0; - } - - li:last-child { - border-bottom: 0; - padding-bottom: 0; - } -} - -.button.wc-reload { - - @include ir(); - padding: 0; - height: 28px; - width: 28px !important; - display: inline-block; - - &::after { - - @include icon_dashicons( "\f345" ); - line-height: 28px; - } -} - -#woocommerce-order-data { - - .postbox-header, - .hndle, - .handlediv { - display: none; - } - - .inside { - display: block !important; - } -} - -#order_data { - padding: 23px 24px 12px; - - h2 { - margin: 0; - font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", sans-serif; - font-size: 21px; - font-weight: normal; - line-height: 1.2; - text-shadow: 1px 1px 1px white; - padding: 0; - } - - h3 { - font-size: 14px; - } - - h3, - h4 { - color: #333; - margin: 1.33em 0 0; - } - - p { - color: #777; - } - - p.order_number { - margin: 0; - font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", sans-serif; - font-weight: normal; - line-height: 1.6em; - font-size: 16px; - } - - .order_data_column_container { - clear: both; - - p._billing_email_field { - margin-top: 13px; - } - } - - .order_data_column { - width: 32%; - padding: 0 2% 0 0; - float: left; - - > h3 span { - display: block; - } - - &:last-child { - padding-right: 0; - } - - p { - padding: 0 !important; - } - - .address strong { - display: block; - } - - .form-field { - float: left; - clear: left; - width: 48%; - padding: 0; - margin: 9px 0 0; - - label { - display: block; - padding: 0 0 3px; - } - - input, - textarea { - width: 100%; - } - - select { - width: 100%; - max-width: 100%; - } - - .select2-container { - width: 100% !important; - } - - .date-picker { - width: 50%; - } - - .hour, - .minute { - width: 3.5em; - } - - small { - display: block; - margin: 5px 0 0; - color: #999; - } - } - - .form-field.last, - ._billing_last_name_field, - ._billing_address_2_field, - ._billing_postcode_field, - ._billing_state_field, - ._billing_phone_field, - ._shipping_last_name_field, - ._shipping_address_2_field, - ._shipping_postcode_field, - ._shipping_state_field { - float: right; - clear: right; - } - - .form-field-wide, - ._billing_company_field, - ._shipping_company_field, - ._transaction_id_field { - width: 100%; - clear: both; - - input, - textarea, - select, - .wc-enhanced-select, - .wc-category-search, - .wc-customer-search { - width: 100%; - } - } - - p.none_set { - color: #999; - } - - div.edit_address { - display: none; - zoom: 1; - padding-right: 1px; - - .select2-container { - - .select2-selection--single { - height: 32px; - - .select2-selection__rendered { - line-height: 32px; - } - } - } - } - - .wc-customer-user, - .wc-order-status { - - label a { - float: right; - margin-left: 8px; - } - } - - a.edit_address { - width: 14px; - height: 0; - padding: 14px 0 0; - margin: 0 0 0 6px; - overflow: hidden; - position: relative; - color: #999; - border: 0; - float: right; - - &:hover, - &:focus { - color: #000; - } - - &::after { - font-family: "WooCommerce"; - position: absolute; - top: 0; - left: 0; - text-align: center; - vertical-align: top; - line-height: 14px; - font-size: 14px; - font-weight: 400; - } - } - - a.edit_address::after { - font-family: "Dashicons"; - content: "\f464"; - } - - .billing-same-as-shipping, - .load_customer_shipping, - .load_customer_billing { - font-size: 13px; - display: inline-block; - font-weight: normal; - } - - .load_customer_shipping { - margin-right: 0.3em; - } - } -} - -.order_actions { - margin: 0; - overflow: hidden; - zoom: 1; - - li { - border-top: 1px solid #fff; - border-bottom: 1px solid #ddd; - padding: 6px 0; - margin: 0; - line-height: 1.6em; - float: left; - width: 50%; - text-align: center; - - a { - float: none; - text-align: center; - text-decoration: underline; - } - - &.wide { - width: auto; - float: none; - clear: both; - padding: 6px; - text-align: left; - overflow: hidden; - } - - #delete-action { - line-height: 25px; - vertical-align: middle; - text-align: left; - float: left; - } - - .save_order { - float: right; - } - - &#actions { - overflow: hidden; - - .button { - width: 24px; - box-sizing: border-box; - float: right; - } - - select { - width: 225px; - box-sizing: border-box; - float: left; - } - } - } -} - -#woocommerce-order-items { - - .inside { - margin: 0; - padding: 0; - background: #fefefe; - } - - .wc-order-data-row { - border-bottom: 1px solid #dfdfdf; - padding: 1.5em 2em; - background: #f8f8f8; - - @include clearfix(); - line-height: 2em; - text-align: right; - - p { - margin: 0; - line-height: 2em; - } - - .wc-used-coupons { - text-align: left; - - .tips { - display: inline-block; - } - } - } - - .wc-used-coupons { - float: left; - width: 50%; - } - - .wc-order-totals { - float: right; - width: 50%; - margin: 0; - padding: 0; - text-align: right; - - .amount { - font-weight: 700; - } - - .label { - vertical-align: top; - } - - .total { - font-size: 1em !important; - width: 10em; - margin: 0 0 0 0.5em; - box-sizing: border-box; - - input[type="text"] { - width: 96%; - float: right; - } - } - - .refunded-total { - color: $red; - } - - .label-highlight { - font-weight: bold; - } - } - - .refund-actions { - margin-top: 5px; - padding-top: 12px; - border-top: 1px solid #dfdfdf; - - .button { - float: right; - margin-left: 4px; - } - - .cancel-action { - float: left; - margin-left: 0; - } - } - - .add_meta { - margin-left: 0 !important; - } - - h3 small { - color: #999; - } - - .amount { - white-space: nowrap; - } - - .add-items { - - .description { - margin-right: 10px; - } - - .button { - float: left; - margin-right: 0.25em; - } - - .button-primary { - float: none; - margin-right: 0; - } - } -} - -#woocommerce-order-items { - - .inside { - display: block !important; - } - - .postbox-header, - .hndle, - .handlediv { - display: none; - } - - .woocommerce_order_items_wrapper { - margin: 0; - overflow-x: auto; - - table.woocommerce_order_items { - width: 100%; - background: #fff; - - thead th { - text-align: left; - padding: 1em; - font-weight: normal; - color: #999; - background: #f8f8f8; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - - &.sortable { - cursor: pointer; - } - - &:last-child { - padding-right: 2em; - } - - &:first-child { - padding-left: 2em; - } - - .wc-arrow { - float: right; - position: relative; - margin-right: -1em; - } - } - - tbody th, - td { - padding: 1.5em 1em 1em; - text-align: left; - line-height: 1.5em; - vertical-align: top; - border-bottom: 1px solid #f8f8f8; - - textarea { - width: 100%; - } - - select { - width: 50%; - } - - input, - textarea { - font-size: 14px; - padding: 4px; - color: #555; - } - - &:last-child { - padding-right: 2em; - } - - &:first-child { - padding-left: 2em; - } - } - - tbody tr:last-child td { - border-bottom: 1px solid #dfdfdf; - } - - tbody tr:first-child td { - border-top: 8px solid #f8f8f8; - } - - tbody#order_line_items tr:first-child td { - border-top: none; - } - - td.thumb { - text-align: left; - width: 38px; - padding-bottom: 1.5em; - - .wc-order-item-thumbnail { - width: 38px; - height: 38px; - border: 2px solid #e8e8e8; - background: #f8f8f8; - color: #ccc; - position: relative; - font-size: 21px; - display: block; - text-align: center; - - &::before { - - @include icon_dashicons( "\f128" ); - width: 38px; - line-height: 38px; - display: block; - } - - img { - width: 100%; - height: 100%; - margin: 0; - padding: 0; - position: relative; - } - } - } - - td.name { - - .wc-order-item-sku, - .wc-order-item-variation { - display: block; - margin-top: 0.5em; - font-size: 0.92em !important; - color: #888; - } - } - - .item { - min-width: 200px; - } - - .center, - .variation-id { - text-align: center; - } - - .cost, - .tax, - .quantity, - .line_cost, - .line_tax, - .tax_class, - .item_cost { - text-align: right; - - label { - white-space: nowrap; - color: #999; - font-size: 0.833em; - - input { - display: inline; - } - } - - input { - width: 70px; - vertical-align: middle; - text-align: right; - } - - select { - width: 85px; - height: 26px; - vertical-align: middle; - font-size: 1em; - } - - .split-input { - display: inline-block; - background: #fff; - border: 1px solid #ddd; - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.07); - margin: 1px 0; - min-width: 80px; - overflow: hidden; - line-height: 1em; - text-align: right; - - div.input { - width: 100%; - box-sizing: border-box; - - label { - font-size: 0.75em; - padding: 4px 6px 0; - color: #555; - display: block; - } - - input { - width: 100%; - box-sizing: border-box; - border: 0; - box-shadow: none; - margin: 0; - padding: 0 6px 4px; - color: #555; - background: transparent; - - &::-webkit-input-placeholder { - color: #ddd; - } - } - } - - div.input:first-child { - border-bottom: 1px dashed #ddd; - background: #fff; - - label { - color: #ccc; - } - - input { - color: #ccc; - } - } - } - - .view { - white-space: nowrap; - } - - .edit { - text-align: left; - } - - small.times, - del, - .wc-order-item-taxes, - .wc-order-item-discount, - .wc-order-item-refund-fields { - font-size: 0.92em !important; - color: #888; - } - - .wc-order-item-taxes, - .wc-order-item-refund-fields { - margin: 0; - - label { - display: block; - } - } - - .wc-order-item-discount { - display: block; - margin-top: 0.5em; - } - - small.times { - margin-right: 0.25em; - } - } - - .quantity { - text-align: center; - - input { - text-align: center; - width: 50px; - } - } - - span.subtotal { - opacity: 0.5; - } - - td.tax_class, - th.tax_class { - text-align: left; - } - - .calculated { - border-color: #ae8ca2; - border-style: dotted; - } - - table.meta { - width: 100%; - } - - table.meta, - table.display_meta { - margin: 0.5em 0 0; - font-size: 0.92em !important; - color: #888; - - tr { - - th { - border: 0; - padding: 0 4px 0.5em 0; - line-height: 1.5em; - width: 20%; - } - - td { - padding: 0 4px 0.5em 0; - border: 0; - line-height: 1.5em; - - input { - width: 100%; - margin: 0; - position: relative; - border-bottom: 0; - box-shadow: none; - } - - textarea { - width: 100%; - height: 4em; - margin: 0; - box-shadow: none; - } - - input:focus + textarea { - border-top-color: #999; - } - - p { - margin: 0 0 0.5em; - line-height: 1.5em; - } - - p:last-child { - margin: 0; - } - } - } - } - - .refund_by { - border-bottom: 1px dotted #999; - } - - tr.fee .thumb div { - - @include ir(); - font-size: 1.5em; - line-height: 1em; - vertical-align: middle; - margin: 0 auto; - - &::before { - - @include icon( "\e007" ); - color: #ccc; - } - } - - tr.refund .thumb div { - - @include ir(); - font-size: 1.5em; - line-height: 1em; - vertical-align: middle; - margin: 0 auto; - - &::before { - - @include icon( "\e014" ); - color: #ccc; - } - } - - tr.shipping { - - .thumb div { - - @include ir(); - font-size: 1.5em; - line-height: 1em; - vertical-align: middle; - margin: 0 auto; - - &::before { - - @include icon( "\e01a" ); - color: #ccc; - } - } - - .shipping_method_name, - .shipping_method { - width: 100%; - margin: 0 0 0.5em; - } - } - - th.line_tax { - white-space: nowrap; - } - - th.line_tax, - td.line_tax { - - .delete-order-tax { - - @include ir(); - float: right; - font-size: 14px; - visibility: hidden; - margin: 3px -18px 0 0; - - &::before { - - @include icon_dashicons( "\f153" ); - color: #999; - } - - &:hover::before { - color: $red; - } - } - - &:hover .delete-order-tax { - visibility: visible; - } - } - - small.refunded { - display: block; - color: $red; - white-space: nowrap; - margin-top: 0.5em; - - &::before { - - @include icon_dashicons( "\f171" ); - position: relative; - top: auto; - left: auto; - margin: -1px 4px 0 0; - vertical-align: middle; - line-height: 1em; - } - } - } - } - - .wc-order-edit-line-item { - padding-left: 0; - } - - .wc-order-edit-line-item-actions { - width: 44px; - text-align: right; - padding-left: 0; - vertical-align: middle; - - a { - color: #ccc; - display: inline-block; - cursor: pointer; - padding: 0 0 0.5em; - margin: 0 0 0 12px; - vertical-align: middle; - text-decoration: none; - line-height: 16px; - width: 16px; - overflow: hidden; - - &::before { - margin: 0; - padding: 0; - font-size: 16px; - width: 16px; - height: 16px; - } - - &:hover { - - &::before { - color: #999; - } - } - - &:first-child { - margin-left: 0; - } - } - - .edit-order-item::before { - - @include icon_dashicons( "\f464" ); - position: relative; - } - - .delete-order-item, - .delete_refund { - - &::before { - - @include icon_dashicons( "\f158" ); - position: relative; - } - - &:hover::before { - color: $red; - } - } - } - - tbody tr .wc-order-edit-line-item-actions { - visibility: hidden; - } - - tbody tr:hover .wc-order-edit-line-item-actions { - visibility: visible; - } - - .wc-order-totals .wc-order-edit-line-item-actions { - width: 1.5em; - visibility: visible !important; - - a { - padding: 0; - } - } -} - -#woocommerce-order-downloads { - - .buttons { - float: left; - padding: 0; - margin: 0; - vertical-align: top; - - .add_item_id, - .select2-container { - width: 400px !important; - margin-right: 9px; - vertical-align: top; - float: left; - } - - button { - margin: 2px 0 0; - } - } - - h3 small { - color: #999; - } -} - -#poststuff #woocommerce-order-actions .inside { - margin: 0; - padding: 0; - - ul.order_actions li { - padding: 6px 10px; - box-sizing: border-box; - - &:last-child { - border-bottom: 0; - } - } - - button { - margin: 1px; - } -} - -#poststuff #woocommerce-order-notes .inside { - margin: 0; - padding: 0; - - ul.order_notes li { - padding: 0 10px; - } - - button { - margin: 1px; - vertical-align: top; - } -} - -#woocommerce_customers { - - p.search-box { - margin: 6px 0 4px; - float: left; - } - - .tablenav { - float: right; - clear: none; - } -} - -.widefat { - - &.customers td { - vertical-align: middle; - padding: 4px 7px; - } - - .column-order_title { - width: 15%; - - time { - display: block; - color: #999; - margin: 3px 0; - } - } - - .column-orders, - .column-paying, - .column-spent { - text-align: center; - width: 8%; - } - - .column-last_order { - width: 11%; - } - - .column-wc_actions { - width: 110px; - - a.button { - - @include ir(); - display: inline-block; - margin: 2px 4px 2px 0; - padding: 0 !important; - height: 2em !important; - width: 2em; - overflow: hidden; - vertical-align: middle; - - &::after { - font-family: "Dashicons"; - speak: never; - font-weight: normal; - font-variant: normal; - text-transform: none; - margin: 0; - text-indent: 0; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - text-align: center; - line-height: 1.85; - } - - img { - display: block; - width: 12px; - height: auto; - } - } - - a.edit::after { - content: "\f464"; - } - - a.link::after { - font-family: "WooCommerce"; - content: "\e00d"; - } - - a.view::after { - content: "\f177"; - } - - a.refresh::after { - font-family: "WooCommerce"; - content: "\e031"; - } - - a.processing::after { - font-family: "WooCommerce"; - content: "\e00f"; - } - - a.complete::after { - content: "\f147"; - } - } - - small.meta { - display: block; - color: #999; - font-size: inherit; - margin: 3px 0; - } -} - -.wc-wp-version-gte-53 { - - .widefat { - - .column-wc_actions { - - a.button { - - &::after { - margin-top: 2px; - } - } - } - } -} - -.post-type-shop_order { - - .tablenav .one-page .displaying-num { - display: none; - } - - .tablenav { - - .select2-selection--single { - height: 32px; - - .select2-selection__rendered { - line-height: 29px; - } - - .select2-selection__arrow { - height: 30px; - } - } - } - - .wp-list-table { - margin-top: 1em; - - thead, - tfoot { - - th { - padding: 0.75em 1em; - } - - th.sortable a, - th.sorted a { - padding: 0; - } - - th:first-child { - padding-left: 2em; - } - - th:last-child { - padding-right: 2em; - } - } - - tbody { - - td, - th { - padding: 1em; - line-height: 26px; - } - - td:first-child { - padding-left: 2em; - } - - td:last-child { - padding-right: 2em; - } - } - - tbody tr { - border-top: 1px solid #f5f5f5; - } - - tbody tr:hover:not(.status-trash):not(.no-link) td { - cursor: pointer; - } - - .no-link { - cursor: default !important; - } - - // Columns. - td, - th { - width: 12ch; - vertical-align: middle; - - p { - margin: 0; - } - } - - .check-column { - width: 1px; - white-space: nowrap; - padding: 1em 1em 1em 1em !important; - vertical-align: middle; - - input { - vertical-align: text-top; - margin: 1px 0; - } - } - - .column-order_number { - width: 20ch; - } - - .column-order_total { - width: 8ch; - text-align: right; - - a span { - float: right; - } - } - - .column-order_date, - .column-order_status { - width: 10ch; - } - - .column-order_status { - width: 14ch; - } - - .column-shipping_address, - .column-billing_address { - width: 20ch; - line-height: 1.5em; - - .description { - display: block; - color: #999; - } - } - - .column-wc_actions { - text-align: right; - - a.button { - text-indent: 9999px; - margin: 2px 0 2px 4px; - } - } - - .order-preview { - float: right; - width: 16px; - padding: 20px 4px 4px 4px; - height: 0; - overflow: hidden; - position: relative; - border: 2px solid transparent; - border-radius: 4px; - - &::before { - - @include icon( "\e010" ); - line-height: 16px; - font-size: 14px; - vertical-align: middle; - top: 4px; - - } - - &:hover { - border: 2px solid #00a0d2; - } - } - - .order-preview.disabled { - - &::before { - content: ""; - background: url("../images/wpspin-2x.gif") no-repeat center top; - background-size: 71%; - } - } - } -} - -.order-status { - display: inline-flex; - line-height: 2.5em; - color: #777; - background: #e5e5e5; - border-radius: 4px; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - margin: -0.25em 0; - cursor: inherit !important; - white-space: nowrap; - max-width: 100%; - - &.status-completed { - background: #c8d7e1; - color: #2e4453; - } - - &.status-on-hold { - background: #f8dda7; - color: #94660c; - } - - &.status-failed { - background: #eba3a3; - color: #761919; - } - - &.status-processing { - background: #c6e1c6; - color: #5b841b; - } - - &.status-trash { - background: #eba3a3; - color: #761919; - } - - > span { - margin: 0 1em; - overflow: hidden; - text-overflow: ellipsis; - } -} - -.wc-order-preview { - - .order-status { - float: right; - margin-right: 54px; - } - - article { - padding: 0 !important; - } - - .modal-close { - border-radius: 0; - } - - .wc-order-preview-table { - width: 100%; - margin: 0; - - th, - td { - padding: 1em 1.5em; - text-align: left; - border: 0; - border-bottom: 1px solid #eee; - margin: 0; - background: transparent; - box-shadow: none; - text-align: right; - vertical-align: top; - } - - td:first-child, - th:first-child { - text-align: left; - } - - th { - border-color: #ccc; - } - - tr:last-child td { - border: 0; - } - - .wc-order-item-sku { - margin-top: 0.5em; - } - - .wc-order-item-meta { - margin-top: 0.5em; - - th, - td { - padding: 0; - border: 0; - text-align: left; - vertical-align: top; - } - - td:last-child { - padding-left: 0.5em; - } - } - } - - .wc-order-preview-addresses { - overflow: hidden; - padding-bottom: 1.5em; - - .wc-order-preview-address, - .wc-order-preview-note { - width: 50%; - float: left; - padding: 1.5em 1.5em 0; - box-sizing: border-box; - word-wrap: break-word; - - h2 { - margin-top: 0; - } - - strong { - display: block; - margin-top: 1.5em; - } - - strong:first-child { - margin-top: 0; - } - } - } - - footer { - - .wc-action-button-group { - display: inline-block; - float: left; - } - - .button.button-large { - margin-left: 10px; - padding: 0 10px !important; - line-height: 28px; - height: auto; - display: inline-block; - } - } - - .wc-action-button-group label { - display: none; - } -} - -.wc-action-button-group { - vertical-align: middle; - line-height: 26px; - text-align: left; - - label { - margin-right: 6px; - cursor: default; - font-weight: bold; - line-height: 28px; - } - - .wc-action-button-group__items { - display: inline-flex; - flex-flow: row wrap; - align-content: flex-start; - justify-content: flex-start; - } - - .wc-action-button { - margin: 0 0 0 -1px !important; - border: 1px solid #ccc; - padding: 0 10px !important; - border-radius: 0 !important; - float: none; - line-height: 28px; - height: auto; - z-index: 1; - position: relative; - overflow: hidden; - text-overflow: ellipsis; - flex: 1 0 auto; - box-sizing: border-box; - text-align: center; - white-space: nowrap; - } - - .wc-action-button:hover, - .wc-action-button:focus { - border: 1px solid #999; - z-index: 2; - } - - .wc-action-button:first-child { - margin-left: 0 !important; - border-top-left-radius: 3px !important; - border-bottom-left-radius: 3px !important; - } - - .wc-action-button:last-child { - border-top-right-radius: 3px !important; - border-bottom-right-radius: 3px !important; - } -} - -@media screen and (max-width: 782px) { - - .wc-order-preview footer { - - .wc-action-button-group .wc-action-button-group__items { - display: flex; - } - - .wc-action-button-group { - float: none; - display: block; - margin-bottom: 4px; - } - - .button.button-large { - width: 100%; - float: none; - text-align: center; - margin: 0; - display: block; - } - } - - .post-type-shop_order .wp-list-table { - - td.check-column { - width: 1em; - } - - td.column-order_number { - padding-left: 0; - padding-bottom: 0.5em; - } - - td.column-order_status, - td.column-order_date { - display: inline-block !important; - padding: 0 1em 1em 1em !important; - - &::before { - display: none !important; - } - } - - td.column-order_date { - padding-left: 0 !important; - } - - td.column-order_status { - float: right; - } - } -} - -.column-customer_message .note-on { - - @include ir(); - margin: 0 auto; - color: #999; - - &::after { - - @include icon( "\e026" ); - line-height: 16px; - } -} - -.column-order_notes .note-on { - - @include ir(); - margin: 0 auto; - color: #999; - - &::after { - - @include icon( "\e027" ); - line-height: 16px; - } -} - -.attributes-table { - - td, - th { - width: 15%; - vertical-align: top; - } - - .attribute-terms { - width: 32%; - } - - .attribute-actions { - width: 2em; - - .configure-terms { - - @include ir(); - padding: 0 !important; - height: 2em !important; - width: 2em; - - &::after { - - @include icon("\f111"); - font-family: "Dashicons"; - line-height: 1.85; - } - } - } -} - -/* Order notes */ -ul.order_notes { - padding: 2px 0 0; - - li { - - .note_content { - padding: 10px; - background: #efefef; - position: relative; - - p { - margin: 0; - padding: 0; - word-wrap: break-word; - } - } - - p.meta { - padding: 10px; - color: #999; - margin: 0; - font-size: 11px; - - .exact-date { - border-bottom: 1px dotted #999; - } - } - - a.delete_note { - color: $red; - } - - .note_content::after { - content: ""; - display: block; - position: absolute; - bottom: -10px; - left: 20px; - width: 0; - height: 0; - border-width: 10px 10px 0 0; - border-style: solid; - border-color: #efefef transparent; - } - } - - li.system-note { - - .note_content { - background: #d7cad2; - } - - .note_content::after { - border-color: #d7cad2 transparent; - } - } - - li.customer-note { - - .note_content { - background: #a7cedc; - } - - .note_content::after { - border-color: #a7cedc transparent; - } - } -} - -.add_note { - border-top: 1px solid #ddd; - padding: 10px 10px 0; - - h4 { - margin-top: 5px !important; - } - - #add_order_note { - width: 100%; - height: 50px; - } -} - -table.wp-list-table { - - .column-thumb { - width: 52px; - text-align: center; - white-space: nowrap; - } - - .column-handle { - width: 17px; - display: none; - } - - tbody { - - td.column-handle { - cursor: move; - width: 17px; - text-align: center; - vertical-align: text-top; - - &::before { - content: "\f333"; - font-family: "Dashicons"; - text-align: center; - line-height: 1; - color: #999; - display: block; - width: 17px; - height: 100%; - margin: 4px 0 0 0; - } - } - } - - .column-name { - width: 22%; - } - - .column-product_cat, - .column-product_tag { - width: 11% !important; - } - - .column-featured, - .column-product_type { - width: 48px; - text-align: left !important; - } - - .column-customer_message, - .column-order_notes { - width: 48px; - text-align: center; - - img { - margin: 0 auto; - padding-top: 0 !important; - } - } - - .manage-column.column-featured img, - .manage-column.column-product_type img { - padding-left: 2px; - } - - .column-price .woocommerce-price-suffix { - display: none; - } - - img { - margin: 1px 2px; - } - - .row-actions { - color: #999; - } - - .row-actions span.id { - padding-top: 8px; - } - - td.column-thumb img { - margin: 0; - width: auto; - height: auto; - max-width: 40px; - max-height: 40px; - vertical-align: middle; - } - - span.na { - color: #999; - } - - .column-sku { - width: 10%; - } - - .column-price { - width: 10ch; - } - - .column-is_in_stock { - text-align: left !important; - width: 12ch; - } - - span.wc-image, - span.wc-featured { - - @include ir(); - margin: 0 auto; - - &::before { - - @include icon_dashicons( "\f128" ); - } - } - - span.wc-featured { - - &::before { - content: "\f155"; - } - - &.not-featured::before { - content: "\f154"; - } - } - - td.column-featured span.wc-featured { - font-size: 1.6em; - cursor: pointer; - } - - mark { - - &.instock, - &.outofstock, - &.onbackorder { - font-weight: 700; - background: transparent none; - line-height: 1; - } - - &.instock { - color: $green; - } - - &.outofstock { - color: #a44; - } - - &.onbackorder { - color: #eaa600; - } - } - - .order-notes_head, - .notes_head, - .status_head { - - @include ir(); - margin: 0 auto; - - &::after { - - @include icon; - } - } - - .order-notes_head::after { - content: "\e028"; - } - - .notes_head::after { - content: "\e026"; - } - - .status_head::after { - content: "\e011"; - } - - .column-order_items { - width: 12%; - - table.order_items { - width: 100%; - margin: 3px 0 0; - padding: 0; - display: none; - - td { - border: 0; - margin: 0; - padding: 0 0 3px; - } - - td.qty { - color: #999; - padding-right: 6px; - text-align: left; - } - } - } -} - -mark.notice { - background: #fff; - color: $red; - margin: 0 0 0 10px; -} - -a.export_rates, -a.import_rates { - float: right; - margin-left: 9px; - margin-top: -2px; - margin-bottom: 0; -} - -#rates-search { - float: right; - - input.wc-tax-rates-search-field { - padding: 4px 8px; - font-size: 1.2em; - } -} - -#rates-pagination { - float: right; - margin-right: 0.5em; - - .tablenav { - margin: 0; - } -} - -.wc_input_table_wrapper { - overflow-x: auto; - display: block; -} - -table.wc_tax_rates, -table.wc_input_table { - width: 100%; - - th, - td { - display: table-cell !important; - } - - span.tips { - color: $blue; - } - - th { - white-space: nowrap; - padding: 10px; - } - - td { - padding: 0; - border-right: 1px solid #dfdfdf; - border-bottom: 1px solid #dfdfdf; - border-top: 0; - background: #fff; - cursor: default; - - input[type=text], - input[type=number] { - width: 100% !important; - min-width: 100px; - padding: 8px 10px; - margin: 0; - border: 0; - outline: 0; - background: transparent none; - - &:focus { - outline: 0; - box-shadow: none; - } - } - - &.compound, - &.apply_to_shipping { - padding: 5px 7px; - vertical-align: middle; - - input { - padding: 0; - } - } - } - - td:last-child { - border-right: 0; - } - - tr.current td { - background-color: #fefbcc; - } - - .item_cost, - .cost { - text-align: right; - - input { - text-align: right; - } - } - - th.sort { - width: 17px; - padding: 0 4px; - } - - td.sort { - padding: 0 4px; - } - - .ui-sortable:not(.ui-sortable-disabled) td.sort { - cursor: move; - font-size: 15px; - background: #f9f9f9; - text-align: center; - vertical-align: middle; - - &::before { - content: "\f333"; - font-family: "Dashicons"; - text-align: center; - line-height: 1; - color: #999; - display: block; - width: 17px; - float: left; - height: 100%; - } - - &:hover::before { - color: #333; - } - } - - .button { - float: left; - margin-right: 5px; - } - - .export, - .import { - float: right; - margin-right: 0; - margin-left: 5px; - } - - span.tips { - padding: 0 3px; - } - - .pagination { - float: right; - - .button { - margin-left: 5px; - margin-right: 0; - } - - .current { - background: #bbb; - text-shadow: none; - } - } - - tr:last-child td { - border-bottom: 0; - } -} - -table.wc_tax_rates { - - td.country { - position: relative; - } -} - -table.wc_gateways, -table.wc_emails, -table.wc_shipping { - position: relative; - - th, - td { - display: table-cell !important; - padding: 1em !important; - vertical-align: top; - line-height: 1.75em; - } - - &.wc_emails td { - vertical-align: middle; - } - - tr:nth-child(odd) td { - background: #f9f9f9; - } - - td.name { - font-weight: 700; - } - - .settings { - text-align: right; - } - - .radio, - .default, - .status { - text-align: center; - - .tips { - margin: 0 auto; - } - - input { - margin: 0; - } - } - - td.sort { - font-size: 15px; - text-align: center; - - .wc-item-reorder-nav { - white-space: nowrap; - width: 72px; - - &::before { - content: "\f333"; - font-family: "Dashicons"; - text-align: center; - line-height: 1; - color: #999; - display: block; - width: 24px; - float: left; - height: 100%; - line-height: 24px; - cursor: move; - } - - button { - position: relative; - overflow: hidden; - float: left; - display: block; - width: 24px; - height: 24px; - margin: 0; - background: transparent; - border: none; - box-shadow: none; - color: #82878c; - text-indent: -9999px; - cursor: pointer; - outline: none; - } - - button::before { - display: inline-block; - position: absolute; - top: 0; - right: 0; - width: 100%; - height: 100%; - font: normal 20px/23px dashicons; - text-align: center; - text-indent: 0; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } - - button:hover, - button:focus { - color: #191e23; - } - - .wc-move-down::before { - content: "\f347"; - } - - .wc-move-up::before { - content: "\f343"; - } - - .wc-move-disabled { - color: #d5d5d5 !important; - cursor: default; - pointer-events: none; - } - } - } - - .wc-payment-gateway-method-name { - font-weight: normal; - } - - .wc-email-settings-table-name { - font-weight: 700; - - span { - font-weight: normal; - color: #999; - margin: 0 0 0 4px !important; - } - } - - .wc-payment-gateway-method-toggle-enabled, - .wc-payment-gateway-method-toggle-disabled { - padding-top: 1px; - display: block; - outline: 0; - box-shadow: none; - } - - .wc-email-settings-table-status { - text-align: center; - width: 1em; - - .tips { - margin: 0 auto; - } - } -} - -.wc-shipping-zone-settings { - - th { - padding: 24px 24px 24px 0; - } - - td.forminp { - - input, - textarea { - padding: 8px; - max-width: 100% !important; - } - - .wc-shipping-zone-region-select { - width: 448px; - max-width: 100% !important; - - .select2-choices { - padding: 8px 8px 4px; - border-color: #ddd; - min-height: 0; - line-height: 1; - - input { - padding: 0; - } - - li { - margin: 0 4px 4px 0; - } - } - } - } - - .wc-shipping-zone-postcodes-toggle { - margin: 0.5em 0 0; - font-size: 0.9em; - text-decoration: underline; - display: block; - } - - .wc-shipping-zone-postcodes-toggle + .wc-shipping-zone-postcodes { - display: none; - } - - .wc-shipping-zone-postcodes { - - textarea { - margin: 10px 0; - } - - .description { - font-size: 0.9em; - color: #999; - } - } -} - -.wc-shipping-zone-settings + p.submit { - margin-top: 0; -} - -.wc-shipping-zone-settings tbody { - display: table-row-group; -} - -table { - - tr, - tr:hover { - - table.wc-shipping-zone-methods { - - tr .row-actions { - position: relative; - } - - tr:hover .row-actions { - position: static; - } - } - } -} - -.wc-shipping-zones-heading .page-title-action { - display: inline-block; -} - -table.wc-shipping-zones, -table.wc-shipping-zone-methods, -table.wc-shipping-classes { - - td, - th { - vertical-align: top; - line-height: 24px; - padding: 1em !important; - font-size: 14px; - background: #fff; - display: table-cell !important; - - li { - line-height: 24px; - font-size: 14px; - } - - .woocommerce-help-tip { - margin: 0 !important; - } - } - - thead { - - th { - vertical-align: middle; - } - - .wc-shipping-zone-sort { - text-align: center; - } - } - - td.wc-shipping-zones-blank-state, - td.wc-shipping-zone-method-blank-state { - background: #f7f1f6 !important; - overflow: hidden; - position: relative; - padding: 7.5em 7.5% !important; - border-bottom: 2px solid #eee2ec; - - &.wc-shipping-zone-method-blank-state { - padding: 2em !important; - - p { - margin-bottom: 0; - } - } - - p, - li { - color: #a46497; - font-size: 1.5em; - line-height: 1.5em; - margin: 0 0 1em; - position: relative; - z-index: 1; - text-shadow: 1px 1px 1px white; - - &.main { - font-size: 2em; - } - } - - li { - margin-left: 1em; - list-style: circle inside; - } - - &::before { - content: "\e01b"; - font-family: "WooCommerce"; - text-align: center; - line-height: 1; - color: #eee2ec; - display: block; - width: 1em; - font-size: 40em; - top: 50%; - right: -3.75%; - margin-top: -0.1875em; - position: absolute; - } - - .button-primary { - background-color: #804877; - border-color: #804877; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 0 rgba(0, 0, 0, 0.15); - margin: 0; - opacity: 1; - text-shadow: 0 -1px 1px #8a4f7f, 1px 0 1px #8a4f7f, 0 1px 1px #8a4f7f, -1px 0 1px #8a4f7f; - font-size: 1.5em; - padding: 0.75em 1em; - height: auto; - position: relative; - z-index: 1; - } - } - - .wc-shipping-zone-method-rows { - - tr:nth-child(even) td { - background: #f9f9f9; - } - } - - tr.odd, - .wc-shipping-class-rows tr:nth-child(odd) { - - td { - background: #f9f9f9; - } - } - - tbody.wc-shipping-zone-rows { - - td { - border-top: 2px solid #f9f9f9; - } - - tr:first-child { - - td { - border-top: 0; - } - } - } - - tr.wc-shipping-zone-worldwide { - - td { - background: #f9f9f9; - border-top: 2px solid #e1e1e1; - } - } - - ul, - p { - margin: 0; - } - - td.wc-shipping-zone-sort, - td.wc-shipping-zone-method-sort { - cursor: move; - font-size: 15px; - text-align: center; - - &::before { - content: "\f333"; - font-family: "Dashicons"; - text-align: center; - line-height: 1; - color: #999; - display: block; - width: 17px; - float: left; - height: 100%; - line-height: 24px; - } - - &:hover::before { - color: #333; - } - } - - td.wc-shipping-zone-worldwide { - text-align: center; - - &::before { - content: "\f319"; - font-family: "dashicons"; - text-align: center; - line-height: 1; - color: #999; - display: block; - width: 17px; - float: left; - height: 100%; - line-height: 24px; - } - } - - .wc-shipping-zone-name, - .wc-shipping-zone-methods { - width: 25%; - } - - .wc-shipping-class-description, - .wc-shipping-class-name, - .wc-shipping-class-slug, - .wc-shipping-zone-name, - .wc-shipping-zone-region { - - input, - select, - textarea { - width: 100%; - } - - a.wc-shipping-zone-delete, - a.wc-shipping-class-delete { - color: #a00; - } - - a.wc-shipping-zone-delete:hover, - a.wc-shipping-class-delete:hover { - color: red; - } - } - - .wc-shipping-class-count { - text-align: center; - } - - td.wc-shipping-zone-methods { - color: #555; - - .method_disabled { - text-decoration: line-through; - } - - ul { - position: relative; - padding-right: 32px; - - li { - color: #555; - display: inline; - margin: 0; - } - - li::before { - content: ", "; - } - - li:first-child::before { - content: ""; - } - } - - .add_shipping_method { - display: block; - width: 24px; - padding: 24px 0 0; - height: 0; - overflow: hidden; - cursor: pointer; - - &::before { - - @include icon; - font-family: "Dashicons"; - content: "\f502"; - color: #999; - vertical-align: middle; - line-height: 24px; - font-size: 16px; - margin: 0; - } - - &.disabled { - cursor: not-allowed; - - &::before { - color: #ccc; - } - } - } - } - - .wc-shipping-zone-method-title { - width: 25%; - - .wc-shipping-zone-method-delete { - color: red; - } - } - - .wc-shipping-zone-method-enabled { - text-align: center; - - a { - display: inline-block; - } - - .woocommerce-input-toggle { - margin-top: 3px; - } - } - - .wc-shipping-zone-method-type { - display: block; - } - - tfoot { - - input, - select { - vertical-align: middle !important; - } - - .button-secondary { - float: right; - } - } - - .editing { - - .wc-shipping-zone-view, - .wc-shipping-zone-edit { - display: none; - } - } -} - -.woocommerce-input-toggle { - height: 16px; - width: 32px; - border: 2px solid #935687; - background-color: #935687; - display: inline-block; - text-indent: -9999px; - border-radius: 10em; - position: relative; - margin-top: -1px; - vertical-align: text-top; - - &::before { - content: ""; - display: block; - width: 16px; - height: 16px; - background: #fff; - position: absolute; - top: 0; - right: 0; - border-radius: 100%; - } - - &.woocommerce-input-toggle--disabled { - border-color: #999; - background-color: #999; - - &::before { - right: auto; - left: 0; - } - } - - &.woocommerce-input-toggle--loading { - opacity: 0.5; - } -} - -.wc-modal-shipping-method-settings { - background: #f8f8f8; - padding: 1em !important; - - form .form-table { - width: 100%; - background: #fff; - margin: 0 0 1.5em; - - tr { - - th { - width: 30%; - position: relative; - - .woocommerce-help-tip { - float: right; - margin: -8px -0.5em 0 0; - vertical-align: middle; - right: 0; - top: 50%; - position: absolute; - } - } - - td { - - input, - select, - textarea { - width: 50%; - min-width: 250px; - } - - input[type="checkbox"] { - width: auto; - min-width: 16px; - } - } - - td, - th { - vertical-align: middle; - margin: 0; - line-height: 24px; - padding: 1em; - border-bottom: 1px solid #f8f8f8; - } - } - - &:last-of-type { - margin-bottom: 0; - } - } -} - -.wc-backbone-modal .wc-shipping-zone-method-selector { - - p { - margin-top: 0; - } - - .wc-shipping-zone-method-description { - margin: 0.75em 1px 0; - line-height: 1.5em; - color: #999; - font-style: italic; - } - - select { - width: 100%; - cursor: pointer; - } -} - -img.help_tip { - margin: 0 0 0 9px; - vertical-align: middle; -} - -.postbox img.help_tip { - margin-top: 0; -} - -.postbox .woocommerce-help-tip { - margin: 0 0 0 9px; -} - -.status-enabled, -.status-manual, -.status-disabled { - font-size: 1.4em; - - @include ir(); -} - -.status-manual::before { - - @include icon( "\e008" ); - color: #999; -} - -.status-enabled::before { - - @include icon( "\e015" ); - color: $woocommerce; -} - -.status-disabled::before { - - @include icon( "\e013" ); - color: #ccc; -} - -.woocommerce { - - h2.woo-nav-tab-wrapper { - margin-bottom: 1em; - } - - nav.woo-nav-tab-wrapper { - margin: 1.5em 0 1em; - } - - .subsubsub { - margin: -8px 0 0; - } - - .wc-admin-breadcrumb { - margin-left: 0.5em; - - a { - color: #a46497; - } - } - - #template div { - margin: 0; - - p .button { - float: right; - margin-left: 10px; - margin-top: -4px; - } - - .editor textarea { - margin-bottom: 8px; - } - } - - textarea[disabled="disabled"] { - background: #dfdfdf !important; - } - - table.form-table { - margin: 0; - position: relative; - table-layout: fixed; - - .forminp-radio ul { - margin: 0; - - li { - line-height: 1.4em; - } - } - - input[type="text"], - input[type="number"], - input[type="email"] { - height: auto; - } - - textarea.input-text { - height: 100%; - min-width: 150px; - display: block; - } - - // Give regular settings inputs a standard width and padding. - textarea, - input[type="text"], - input[type="email"], - input[type="number"], - input[type="password"], - input[type="datetime"], - input[type="datetime-local"], - input[type="date"], - input[type="time"], - input[type="week"], - input[type="url"], - input[type="tel"], - input.regular-input { - width: 400px; - margin: 0; - padding: 6px; - box-sizing: border-box; - vertical-align: top; - } - - input[type="datetime-local"], - input[type="date"], - input[type="time"], - input[type="week"], - input[type="tel"] { - width: 200px; - } - - select { - width: 400px; - margin: 0; - box-sizing: border-box; - line-height: 32px; - vertical-align: top; - } - - input[size] { - width: auto !important; - } - - // Ignore nested inputs. - table { - - select, - textarea, - input[type="text"], - input[type="email"], - input[type="number"], - input.regular-input { - width: auto; - } - } - - textarea.wide-input { - width: 100%; - } - - img.help_tip, - .woocommerce-help-tip { - padding: 0; - margin: -4px 0 0 5px; - vertical-align: middle; - cursor: help; - line-height: 1; - } - - span.help_tip { - cursor: help; - color: $blue; - } - - th { - position: relative; - padding-right: 24px; - } - - th label { - position: relative; - display: block; - - img.help_tip, - .woocommerce-help-tip { - margin: -8px -24px 0 0; - position: absolute; - right: 0; - top: 50%; - } - } - - th label + .woocommerce-help-tip { - margin: 0 0 0 0; - position: absolute; - right: 0; - top: 20px; - } - - .select2-container { - vertical-align: top; - margin-bottom: 3px; - } - - .select2-container + span.description { - display: block; - margin-top: 8px; - } - - table.widefat th { - padding-right: inherit; - } - - .wp-list-table .woocommerce-help-tip { - float: none; - } - - fieldset { - margin-top: 4px; - - img.help_tip, - .woocommerce-help-tip { - margin: -3px 0 0 5px; - } - - p.description { - margin-bottom: 8px; - } - - &:first-child { - margin-top: 0; - } - } - - .iris-picker { - z-index: 100; - display: none; - position: absolute; - border: 1px solid #ccc; - border-radius: 3px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); - - .ui-slider { - border: 0 !important; - margin: 0 !important; - width: auto !important; - height: auto !important; - background: none transparent !important; - - .ui-slider-handle { - margin-bottom: 0 !important; - } - } - } - - .iris-error { - background-color: #ffafaf; - } - - .colorpickpreview { - padding: 7px 0; - line-height: 1em; - display: inline-block; - width: 26px; - border: 1px solid #ddd; - font-size: 14px; - } - - .image_width_settings { - vertical-align: middle; - - label { - margin-left: 10px; - } - - input { - width: auto; - } - } - - .wc_payment_gateways_wrapper, - .wc_emails_wrapper { - padding: 0 15px 10px 0; - } - } - - .wc-shipping-zone-settings { - - td.forminp { - - input, - textarea { - width: 448px; - padding: 6px 11px; - } - - .select2-search input { - padding: 6px; - } - } - } -} - -.wc-wp-version-gte-53 { - - .woocommerce { - - h2.wc-table-list-header { - margin: 1em 0 0.35em 0; - } - - input + .subsubsub { - margin: 8px 0 0; - } - - table.form-table { - - // Give regular settings inputs a standard width and padding. - textarea, - input[type="text"], - input[type="email"], - input[type="number"], - input[type="password"], - input[type="datetime"], - input[type="datetime-local"], - input[type="date"], - input[type="time"], - input[type="week"], - input[type="url"], - input[type="tel"], - input.regular-input { - padding: 0 8px; - - @media only screen and (max-width: 782px) { - width: 100%; - } - } - - select { - - @media only screen and (max-width: 782px) { - width: 100%; - } - } - - th label { - - img.help_tip, - .woocommerce-help-tip { - margin: -7px -24px 0 0; - - @media only screen and (max-width: 782px) { - right: auto; - margin-left: 5px; - } - } - } - - .forminp-color { - font-size: 0; - } - - .colorpickpreview { - padding: 0; - width: 30px; - height: 30px; - box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2); - font-size: 16px; - border-radius: 4px; - margin-right: 3px; - - @media only screen and (max-width: 782px) { - float: left; - width: 40px; - height: 40px; - } - } - } - } -} - -.woocommerce #tabs-wrap table a.remove { - margin-left: 4px; -} - -.woocommerce #tabs-wrap table p { - margin: 0 0 4px !important; - overflow: hidden; - zoom: 1; -} - -.woocommerce #tabs-wrap table p a.add { - float: left; -} - -#wp-excerpt-editor-container { - background: #fff; -} - -#product_variation-parent #parent_id { - width: 100%; -} - -#postimagediv img { - border: 1px solid #d5d5d5; - max-width: 100%; -} - -#woocommerce-product-images .inside { - margin: 0; - padding: 0; - - .add_product_images { - padding: 0 12px 12px; - } - - #product_images_container { - padding: 0 0 0 9px; - - ul { - - @include clearfix(); - margin: 0; - padding: 0; - - li.image, - li.add, - li.wc-metabox-sortable-placeholder { - width: 80px; - float: left; - cursor: move; - border: 1px solid #d5d5d5; - margin: 9px 9px 0 0; - background: #f7f7f7; - - @include border-radius(2px); - position: relative; - box-sizing: border-box; - - img { - width: 100%; - height: auto; - display: block; - } - } - - li.wc-metabox-sortable-placeholder { - border: 3px dashed #ddd; - position: relative; - - &::after { - - @include icon_dashicons( "\f161" ); - font-size: 2.618em; - line-height: 72px; - color: #ddd; - } - } - - ul.actions { - position: absolute; - top: -8px; - right: -8px; - padding: 2px; - display: none; - - @media (max-width: 768px) { - display: block; - } - - li { - float: right; - margin: 0 0 0 2px; - - a { - width: 1em; - height: 1em; - margin: 0; - height: 0; - display: block; - overflow: hidden; - - &.tips { - cursor: pointer; - } - } - - a.delete { - - @include ir(); - font-size: 1.4em; - - &::before { - - @include icon_dashicons( "\f153" ); - color: #999; - background: #fff; - border-radius: 50%; - height: 1em; - width: 1em; - line-height: 1em; - } - - &:hover::before { - color: $red; - } - } - } - } - - li:hover ul.actions { - display: block; - } - } - } -} - -#woocommerce-product-data { - - .hndle { - padding: 10px; - - span { - display: block; - line-height: 24px; - } - - .type_box { - display: inline; - line-height: inherit; - vertical-align: baseline; - } - - select { - margin: 0; - } - - label { - padding-right: 1em; - font-size: 12px; - vertical-align: baseline; - } - - label:first-child { - margin-right: 1em; - border-right: 1px solid #dfdfdf; - } - - input, - select { - margin-top: -3px 0 0; - vertical-align: middle; - } - - select { - margin-left: 0.5em; - } - } - - > .handlediv { - margin-top: 4px; - } - - .wrap { - margin: 0; - } -} - -#woocommerce-coupon-description { - padding: 3px 8px; - font-size: 1.7em; - line-height: 1.42em; - height: auto; - width: 100%; - outline: 0; - margin: 10px 0; - display: block; - - &::-webkit-input-placeholder { - line-height: 1.42em; - color: #bbb; - } - - &::-moz-placeholder { - line-height: 1.42em; - color: #bbb; - } - - &:-ms-input-placeholder { - line-height: 1.42em; - color: #bbb; - } - - &:-moz-placeholder { - line-height: 1.42em; - color: #bbb; - } -} - -#woocommerce-product-data, -#woocommerce-coupon-data { - - .panel-wrap { - background: #fff; - } - - .woocommerce_options_panel, - .wc-metaboxes-wrapper { - float: left; - width: 80%; - - .wc-radios { - display: block; - float: left; - margin: 0; - - li { - display: block; - padding: 0 0 10px; - - input { - width: auto; - } - } - } - } -} - -#woocommerce-product-data, -#woocommerce-coupon-data, -.woocommerce { - - .panel-wrap { - overflow: hidden; - } - - ul.wc-tabs { - margin: 0; - width: 20%; - float: left; - line-height: 1em; - padding: 0 0 10px; - position: relative; - background-color: #fafafa; - border-right: 1px solid #eee; - box-sizing: border-box; - - &::after { - content: ""; - display: block; - width: 100%; - height: 9999em; - position: absolute; - bottom: -9999em; - left: 0; - background-color: #fafafa; - border-right: 1px solid #eee; - } - - li { - margin: 0; - padding: 0; - display: block; - position: relative; - - a { - margin: 0; - padding: 10px; - display: block; - box-shadow: none; - text-decoration: none; - line-height: 20px !important; - border-bottom: 1px solid #eee; - - span { - margin-left: 0.618em; - margin-right: 0.618em; - } - - &::before { - - @include iconbeforedashicons( "\f107" ); - } - } - - &.general_options a::before { - content: "\f107"; - } - - &.inventory_options a::before { - content: "\f481"; - } - - &.shipping_options a::before { - font-family: "WooCommerce"; - content: "\e01a"; - } - - &.linked_product_options a::before { - content: "\f103"; - } - - &.attribute_options a::before { - content: "\f175"; - } - - &.advanced_options a::before { - font-family: "Dashicons"; - content: "\f111"; - } - - &.marketplace-suggestions_options a::before { - content: none; - } - - &.variations_options a::before { - content: "\f509"; - } - - &.usage_restriction_options a::before { - font-family: "WooCommerce"; - content: "\e602"; - } - - &.usage_limit_options a::before { - font-family: "WooCommerce"; - content: "\e601"; - } - - &.general_coupon_data a::before { - font-family: "WooCommerce"; - content: "\e600"; - } - - &.active a { - color: #555; - position: relative; - background-color: #eee; - } - } - } -} - -/** - * Shipping - */ -.woocommerce_page_wc-settings { - - input[type=url], - input[type=email] { - direction: ltr; - } - - .shippingrows { - - th.check-column { - padding-top: 20px; - } - - tfoot th { - padding-left: 10px; - } - - .add.button::before { - - @include iconbefore( "\e007" ); - } - } - - h3.wc-settings-sub-title { - font-size: 1.2em; - } -} - -#woocommerce-product-data, -#woocommerce-product-type-options, -#woocommerce-order-data, -#woocommerce-order-downloads, -#woocommerce-coupon-data { - - .inside { - margin: 0; - padding: 0; - } -} - -.woocommerce_options_panel, -.panel { - padding: 9px; - color: #555; - - .form-field .woocommerce-help-tip { - font-size: 1.4em; - } -} - -.woocommerce_page_settings .woocommerce_options_panel, -.panel { - padding: 0; -} - -#woocommerce-product-type-options .panel, -#woocommerce-product-specs .inside { - margin: 0; - padding: 9px; -} - -.woocommerce_options_panel p, -#woocommerce-product-type-options .panel p, -.woocommerce_options_panel fieldset.form-field { - margin: 0 0 9px; - font-size: 12px; - padding: 5px 9px; - line-height: 24px; - - &::after { - content: "."; - display: block; - height: 0; - clear: both; - visibility: hidden; - } -} - -.woocommerce_options_panel .checkbox, -.woocommerce_variable_attributes .checkbox { - margin: 4px 0 !important; - vertical-align: middle; - float: left; -} - -.woocommerce_variations, -.woocommerce_options_panel { - - .downloadable_files table { - width: 100%; - padding: 0 !important; - - th { - padding: 7px 0 7px 7px !important; - - &.sort { - width: 17px; - padding: 7px !important; - } - - .woocommerce-help-tip { - font-size: 1.1em; - margin-left: 0; - } - } - - td { - vertical-align: middle !important; - padding: 4px 0 4px 7px !important; - position: relative; - - &:last-child { - padding-right: 7px !important; - } - - input.input_text { - width: 100%; - float: none; - min-width: 0; - margin: 1px 0; - } - - .upload_file_button { - width: auto; - float: right; - cursor: pointer; - } - - .delete { - - @include ir(); - font-size: 1.2em; - - &::before { - - @include icon_dashicons( "\f153" ); - color: #999; - } - - &:hover { - - &::before { - color: $red; - } - } - } - } - - td.sort { - width: 17px; - cursor: move; - font-size: 15px; - text-align: center; - background: #f9f9f9; - padding-right: 7px !important; - - &::before { - content: "\f333"; - font-family: "Dashicons"; - text-align: center; - line-height: 1; - color: #999; - display: block; - width: 17px; - float: left; - height: 100%; - } - - &:hover::before { - color: #333; - } - } - } -} - -.woocommerce_attribute, -.woocommerce_variation { - - h3 .sort { - width: 17px; - height: 26px; - cursor: move; - float: right; - font-size: 15px; - font-weight: 400; - margin-right: 0.5em; - visibility: hidden; - text-align: center; - vertical-align: middle; - - &::before { - content: "\f333"; - font-family: "Dashicons"; - text-align: center; - line-height: 28px; - color: #999; - display: block; - width: 17px; - float: left; - height: 100%; - } - - &:hover::before { - color: #777; - } - } - - h3:hover, - &.ui-sortable-helper { - - .sort { - visibility: visible; - } - } -} - -.woocommerce_options_panel { - min-height: 175px; - box-sizing: border-box; - - .downloadable_files { - padding: 0 9px 0 162px; - position: relative; - margin: 9px 0; - - label { - position: absolute; - left: 0; - margin: 0 0 0 12px; - line-height: 24px; - } - } - - p { - margin: 9px 0; - } - - p.form-field, - fieldset.form-field { - padding: 5px 20px 5px 162px !important; /** Padding for aligning labels left - 12px + 150 label width **/ - } - - .sale_price_dates_fields { - - .short:first-of-type { - margin-bottom: 1em; - } - - .short:nth-of-type(2) { - clear: left; - } - } - - label, - legend { - float: left; - width: 150px; - padding: 0; - margin: 0 0 0 -150px; - - .req { - font-weight: 700; - font-style: normal; - color: $red; - } - } - - .description { - padding: 0; - margin: 0 0 0 7px; - clear: none; - display: inline; - } - - .description-block { - margin-left: 0; - display: block; - } - - textarea, - input, - select { - margin: 0; - } - - textarea { - float: left; - height: 3.5em; - line-height: 1.5em; - vertical-align: top; - } - - input[type="text"], - input[type="email"], - input[type="number"], - input[type="password"] { - width: 50%; - float: left; - } - - input.button { - width: auto; - margin-left: 8px; - } - - select { - float: left; - } - - input[type="text"].short, - input[type="email"].short, - input[type="number"].short, - input[type="password"].short, - .short { - width: 50%; - } - - .sized { - width: auto !important; - margin-right: 6px; - } - - .options_group { - border-top: 1px solid white; - border-bottom: 1px solid #eee; - - &:first-child { - border-top: 0; - } - - &:last-child { - border-bottom: 0; - } - - fieldset { - margin: 9px 0; - font-size: 12px; - padding: 5px 9px; - line-height: 24px; - - label { - width: auto; - float: none; - } - - ul { - float: left; - width: 50%; - margin: 0; - padding: 0; - - li { - margin: 0; - width: auto; - - input { - width: auto; - float: none; - margin-right: 4px; - } - } - } - - ul.wc-radios label { - margin-left: 0; - } - } - } - - .dimensions_field .wrap { - display: block; - width: 50%; - - input { - width: 30.75%; - margin-right: 3.8%; - } - - .last { - margin-right: 0; - } - } - - &.padded { - padding: 1em; - } - - .select2-container { - float: left; - } -} - -#woocommerce-product-data input.dp-applied { - float: left; -} - -#grouped_product_options, -#virtual_product_options, -#simple_product_options { - padding: 12px; - font-style: italic; - color: #666; -} - -/** - * WooCommerce meta boxes - */ -.wc-metaboxes-wrapper { - - .toolbar { - margin: 0 !important; - border-top: 1px solid white; - border-bottom: 1px solid #eee; - padding: 9px 12px !important; - - &:first-child { - border-top: 0; - } - - &:last-child { - border-bottom: 0; - } - - .add_variation { - float: right; - margin-left: 5px; - } - - .save-variation-changes, - .cancel-variation-changes { - float: left; - margin-right: 5px; - } - } - - p.toolbar { - overflow: hidden; - zoom: 1; - } - - .expand-close { - margin-right: 2px; - color: #777; - font-size: 12px; - font-style: italic; - - a { - background: none; - padding: 0; - font-size: 12px; - text-decoration: none; - } - } - - &#product_attributes .expand-close { - float: right; - line-height: 28px; - } - - button.add_variable_attribute, - .fr { - float: right; - margin: 0 0 0 6px; - } - - .wc-metaboxes { - border-bottom: 1px solid #eee; - } - - .wc-metabox-sortable-placeholder { - border-color: #bbb; - background-color: #f5f5f5; - margin-bottom: 9px; - border-width: 1px; - border-style: dashed; - } - - .wc-metabox { - background: #fff; - border-bottom: 1px solid #eee; - margin: 0 !important; - - select { - font-weight: 400; - } - - &:last-of-type { - border-bottom: 0; - } - - .handlediv { - width: 27px; - float: right; - - &::before { - content: "\f142" !important; - cursor: pointer; - display: inline-block; - font: 400 20px/1 "Dashicons"; - line-height: 0.5 !important; - padding: 8px 10px; - position: relative; - right: 12px; - top: 0; - } - } - - &.closed { - - @include border-radius(3px); - - .handlediv::before { - content: "\f140" !important; - } - - h3 { - border: 0; - } - } - - h3 { - margin: 0 !important; - padding: 0.75em 0.75em 0.75em 1em !important; - font-size: 1em !important; - overflow: hidden; - zoom: 1; - cursor: move; - - button, - a.delete { - float: right; - } - - a.delete { - color: red; - font-weight: normal; - line-height: 26px; - text-decoration: none; - position: relative; - visibility: hidden; - } - - strong { - font-weight: normal; - line-height: 26px; - font-weight: 700; - } - - select { - font-family: sans-serif; - max-width: 20%; - margin: 0.25em 0.25em 0.25em 0; - } - - .handlediv { - background-position: 6px 5px !important; - visibility: hidden; - height: 26px; - } - - &.fixed { - cursor: pointer !important; - } - } - - &.woocommerce_attribute h3, - &.woocommerce_variation h3 { - cursor: pointer; - padding: 0.5em 0.75em 0.5em 1em !important; - - a.delete, - .handlediv, - .sort { - margin-top: 0.25em; - } - } - - h3:hover, - &.ui-sortable-helper { - - a.delete, - .handlediv { - visibility: visible; - } - } - - table { - width: 100%; - position: relative; - background-color: #fdfdfd; - padding: 1em; - border-top: 1px solid #eee; - - td { - text-align: left; - padding: 0 6px 1em 0; - vertical-align: top; - border: 0; - - label { - text-align: left; - display: block; - line-height: 21px; - } - - input { - float: left; - min-width: 200px; - } - - input, - textarea { - width: 100%; - margin: 0; - display: block; - font-size: 14px; - padding: 4px; - color: #555; - } - - select, - .select2-container { - width: 100% !important; - } - - input.short { - width: 200px; - } - - input.checkbox { - width: 16px; - min-width: inherit; - vertical-align: text-bottom; - display: inline-block; - float: none; - } - } - - td.attribute_name { - width: 200px; - } - - .plus, - .minus { - margin-top: 6px; - } - - .fl { - float: left; - } - - .fr { - float: right; - } - } - } -} - -.variations-pagenav { - float: right; - line-height: 24px; - - .displaying-num { - color: #777; - font-size: 12px; - font-style: italic; - } - - a { - padding: 0 10px 3px; - background: rgba(0, 0, 0, 0.05); - font-size: 16px; - font-weight: 400; - text-decoration: none; - } - - a.disabled, - a.disabled:active, - a.disabled:focus, - a.disabled:hover { - color: #a0a5aa; - background: rgba(0, 0, 0, 0.05); - } -} - -.variations-defaults { - float: left; - - select { - margin: 0.25em 0.25em 0.25em 0; - } -} - -.woocommerce_variable_attributes { - background-color: #fdfdfd; - border-top: 1px solid #eee; - - .data { - - @include clearfix; - padding: 1em 2em; - } - - .upload_image_button { - display: block; - width: 64px; - height: 64px; - float: left; - margin-right: 20px; - position: relative; - cursor: pointer; - - img { - width: 100%; - height: auto; - display: none; - } - - &::before { - content: "\f128"; - font-family: "Dashicons"; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - text-align: center; - line-height: 64px; - font-size: 64px; - font-weight: 400; - -webkit-font-smoothing: antialiased; - } - - &.remove { - - img { - display: block; - } - - &::before { - content: "\f335"; - display: none; - } - - &:hover::before { - display: block; - } - } - } - - .options { - border: 1px solid #eee; - border-width: 1px 0; - padding: 0.25em 0; - - label { - display: inline-block; - padding: 4px 1em 2px 0; - } - - input[type=checkbox] { - margin: 0 5px 0 0.5em !important; - vertical-align: middle; - } - } -} - -.form-row { - - label { - display: inline-block; - } - - .woocommerce-help-tip { - float: right; - } - - input[type="text"], - input[type="number"], - input[type="password"], - input[type="color"], - input[type="date"], - input[type="datetime"], - input[type="datetime-local"], - input[type="email"], - input[type="month"], - input[type="search"], - input[type="tel"], - input[type="time"], - input[type="url"], - input[type="week"], - select, - textarea { - width: 100%; - vertical-align: middle; - margin: 2px 0 0; - padding: 5px; - } - - select { - height: 40px; - } - - &.dimensions_field { - - .wrap { - clear: left; - display: block; - } - - input { - width: 33%; - float: left; - vertical-align: middle; - - &:last-of-type { - margin-right: 0; - width: 34%; - } - } - } - - &.form-row-first, - &.form-row-last { - width: 48%; - float: right; - } - - &.form-row-first { - clear: both; - float: left; - } - - &.form-row-full { - clear: both; - } -} - -/** - * Tooltips - */ -.tips { - cursor: help; - text-decoration: none; -} - -img.tips { - padding: 5px 0 0; -} - -#tiptip_holder { - display: none; - z-index: 8675309; - position: absolute; - top: 0; - - /*rtl:ignore*/ - left: 0; - - - &.tip_top { - padding-bottom: 5px; - - #tiptip_arrow_inner { - margin-top: -7px; - margin-left: -6px; - border-top-color: #333; - } - } - - &.tip_bottom { - padding-top: 5px; - - #tiptip_arrow_inner { - margin-top: -5px; - margin-left: -6px; - border-bottom-color: #333; - } - } - - &.tip_right { - padding-left: 5px; - - #tiptip_arrow_inner { - margin-top: -6px; - margin-left: -5px; - border-right-color: #333; - } - } - - &.tip_left { - padding-right: 5px; - - #tiptip_arrow_inner { - margin-top: -6px; - margin-left: -7px; - border-left-color: #333; - } - } -} - -#tiptip_content, -.chart-tooltip, -.wc_error_tip { - color: #fff; - font-size: 0.8em; - max-width: 150px; - background: #333; - text-align: center; - border-radius: 3px; - padding: 0.618em 1em; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); - - code { - padding: 1px; - background: #888; - } -} - -#tiptip_arrow, -#tiptip_arrow_inner { - position: absolute; - border-color: transparent; - border-style: solid; - border-width: 6px; - height: 0; - width: 0; -} - -/*rtl:raw: - #tiptip_arrow { - right: 50%; - margin-right: -6px; - } - */ - -.wc_error_tip { - max-width: 20em; - line-height: 1.8em; - position: absolute; - white-space: normal; - background: #d82223; - margin: 1.5em 1px 0 -1em; - z-index: 9999999; - - &::after { - content: ""; - display: block; - border: 8px solid #d82223; - border-right-color: transparent; - border-left-color: transparent; - border-top-color: transparent; - position: absolute; - top: -3px; - left: 50%; - margin: -1em 0 0 -3px; - } -} - -/** - * Date picker - */ -img.ui-datepicker-trigger { - vertical-align: middle; - margin-top: -1px; - cursor: pointer; -} - -.woocommerce_options_panel img.ui-datepicker-trigger, -.wc-metabox-content img.ui-datepicker-trigger { - float: left; - margin-right: 8px; - margin-top: 4px; - margin-left: 4px; -} - -#ui-datepicker-div { - display: none; -} - -/** - * Reports - */ -.woocommerce-reports-remove-filter { - color: red; - text-decoration: none; -} - -.woocommerce-reports-wrap, -.woocommerce-reports-wide { - - &.woocommerce-reports-wrap { - margin-left: 300px; - padding-top: 18px; - } - - &.halved { - margin: 0; - overflow: hidden; - zoom: 1; - } - - .widefat th { - padding: 7px; - } - - .widefat td { - vertical-align: top; - padding: 7px; - - .description { - margin: 4px 0 0; - } - } - - .postbox { - - &::after { - content: "."; - display: block; - height: 0; - clear: both; - visibility: hidden; - } - - h3 { - cursor: default !important; - } - - .inside { - padding: 10px; - margin: 0 !important; - } - - div.stats_range, - h3.stats_range { - border-bottom-color: #dfdfdf; - margin: 0; - padding: 0 !important; - - .export_csv { - float: right; - line-height: 26px; - border-left: 1px solid #dfdfdf; - padding: 10px; - display: block; - text-decoration: none; - - &::before { - - @include iconbeforedashicons( "\f346" ); - margin-right: 4px; - } - } - - ul { - list-style: none outside; - margin: 0; - padding: 0; - zoom: 1; - background: #f5f5f5; - border-bottom: 1px solid #ccc; - - &::before, - &::after { - content: " "; - display: table; - } - - &::after { - clear: both; - } - - li { - float: left; - margin: 0; - padding: 0; - line-height: 26px; - font-weight: bold; - font-size: 14px; - - a { - border-right: 1px solid #dfdfdf; - padding: 10px; - display: block; - text-decoration: none; - } - - &.active { - background: #fff; - box-shadow: 0 4px 0 0 #fff; - - a { - color: #777; - } - } - - &.custom { - padding: 9px 10px; - vertical-align: middle; - - form, - div { - display: inline; - margin: 0; - - input.range_datepicker { - padding: 0; - margin: 0 10px 0 0; - background: transparent; - border: 0; - color: #777; - text-align: center; - box-shadow: none; - - &.from { - margin-right: 0; - } - } - } - } - } - } - } - - .chart-with-sidebar { - padding: 12px 12px 12px 249px; - margin: 0 !important; - - .chart-sidebar { - width: 225px; - margin-left: -237px; - float: left; - } - } - - .chart-widgets { - margin: 0; - padding: 0; - - li.chart-widget { - margin: 0 0 1em; - background: #fafafa; - border: 1px solid #dfdfdf; - - &::after { - content: "."; - display: block; - height: 0; - clear: both; - visibility: hidden; - } - - h4 { - background: #fff; - border: 1px solid #dfdfdf; - border-left-width: 0; - border-right-width: 0; - padding: 10px; - margin: 0; - color: $blue; - border-top-width: 0; - background-image: linear-gradient(to top, #ececec, #f9f9f9); - - &.section_title:hover { - color: $red; - } - } - - .section_title { - cursor: pointer; - - span { - display: block; - - &::after { - - @include iconafter( "\e035" ); - float: right; - font-size: 0.9em; - line-height: 1.618; - } - } - - &.open { - color: #333; - - span::after { - display: none; - } - } - } - - .section { - border-bottom: 1px solid #dfdfdf; - - .select2-container { - width: 100% !important; - } - - &:last-of-type { - border-radius: 0 0 3px 3px; - } - } - - table { - width: 100%; - - td { - padding: 7px 10px; - vertical-align: top; - border-top: 1px solid #e5e5e5; - line-height: 1.4em; - } - - tr:first-child td { - border-top: 0; - } - - td.count { - background: #f5f5f5; - } - - td.name { - max-width: 175px; - - a { - word-wrap: break-word; - } - } - - td.sparkline { - vertical-align: middle; - } - - .wc_sparkline { - width: 32px; - height: 1em; - display: block; - float: right; - } - - tr.active td { - background: #f5f5f5; - } - } - - form, - p { - margin: 0; - padding: 10px; - - .submit { - margin-top: 10px; - } - } - - #product_ids { - width: 100%; - } - - .select_all, - .select_none { - float: right; - color: #999; - margin-left: 4px; - margin-top: 10px; - } - - .description { - margin-left: 0.5em; - font-weight: normal; - opacity: 0.8; - } - } - } - - .chart-legend { - list-style: none outside; - margin: 0 0 1em; - padding: 0; - border: 1px solid #dfdfdf; - border-right-width: 0; - border-bottom-width: 0; - background: #fff; - - li { - border-right: 5px solid #aaa; - color: #aaa; - padding: 1em; - display: block; - margin: 0; - transition: all ease 0.5s; - box-shadow: inset 0 -1px 0 0 #dfdfdf; - - strong { - font-size: 1.618em; - line-height: 1.2em; - color: #464646; - font-weight: normal; - display: block; - font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", sans-serif; - - del { - color: #e74c3c; - font-weight: normal; - } - } - - &:hover { - box-shadow: - inset 0 -1px 0 0 #dfdfdf, - inset 300px 0 0 rgba(156, 93, 144, 0.1); - border-right: 5px solid #9c5d90 !important; - padding-left: 1.5em; - color: #9c5d90; - } - } - } - - .pie-chart-legend { - margin: 12px 0 0; - overflow: hidden; - - li { - float: left; - margin: 0; - padding: 6px 0 0; - border-top: 4px solid #999; - text-align: center; - box-sizing: border-box; - width: 50%; - } - } - - .stat { - font-size: 1.5em !important; - font-weight: 700; - text-align: center; - } - - .chart-placeholder { - width: 100%; - height: 650px; - overflow: hidden; - position: relative; - } - - .chart-prompt { - line-height: 650px; - margin: 0; - color: #999; - font-size: 1.2em; - font-style: italic; - text-align: center; - } - - .chart-container { - background: #fff; - padding: 12px; - position: relative; - border: 1px solid #dfdfdf; - border-radius: 3px; - } - - .main .chart-legend { - margin-top: 12px; - - li { - border-right: 0; - margin: 0 8px 0 0; - float: left; - border-top: 4px solid #aaa; - } - } - } - - .woocommerce-reports-main { - float: left; - min-width: 100%; - - table td { - padding: 9px; - } - } - - .woocommerce-reports-sidebar { - display: inline; - width: 281px; - margin-left: -300px; - clear: both; - float: left; - } - - .woocommerce-reports-left { - width: 49.5%; - float: left; - } - - .woocommerce-reports-right { - width: 49.5%; - float: right; - } -} - -.woocommerce-wide-reports-wrap { - padding-bottom: 11px; - - .widefat { - - .export-data { - float: right; - } - - th, - td { - vertical-align: middle; - padding: 7px; - } - } -} - -form.report_filters { - - p { - vertical-align: middle; - } - - label, - input, - div { - vertical-align: middle; - } -} - -.chart-tooltip { - position: absolute; - display: none; - line-height: 1; -} - -table.bar_chart { - width: 100%; - - thead th { - text-align: left; - color: #ccc; - padding: 6px 0; - } - - tbody { - - th { - padding: 6px 0; - width: 25%; - text-align: left !important; - font-weight: normal !important; - border-bottom: 1px solid #fee; - } - - td { - text-align: right; - line-height: 24px; - padding: 6px 6px 6px 0; - border-bottom: 1px solid #fee; - - span { - color: #8a4b75; - display: block; - } - - span.alt { - color: #47a03e; - margin-top: 6px; - } - } - - td.bars { - position: relative; - text-align: left; - padding: 6px 6px 6px 0; - border-bottom: 1px solid #fee; - - span, - a { - text-decoration: none; - clear: both; - background: #8a4b75; - float: left; - display: block; - line-height: 24px; - height: 24px; - border-radius: 3px; - } - - span.alt { - clear: both; - background: #47a03e; - - span { - margin: 0; - color: #c5dec2 !important; - text-shadow: 0 1px 0 #47a03e; - background: transparent; - } - } - } - } -} - -.post-type-shop_order .woocommerce-BlankState-message::before { - - @include icon( "\e01d" ); -} - -.post-type-shop_coupon .woocommerce-BlankState-message::before { - - @include icon( "\e600" ); -} - -.post-type-product .woocommerce-BlankState-message::before { - - @include icon( "\e006" ); -} - -.woocommerce-BlankState--api .woocommerce-BlankState-message::before { - - @include icon( "\e01c" ); -} - -.woocommerce-BlankState--webhooks .woocommerce-BlankState-message::before { - - @include icon( "\e01b" ); -} - -.woocommerce-BlankState { - text-align: center; - padding: 5em 0 0; - - .woocommerce-BlankState-message { - color: #aaa; - margin: 0 auto 1.5em; - line-height: 1.5em; - font-size: 1.2em; - max-width: 500px; - - &::before { - color: #ddd; - text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.2), 0 1px 0 rgba(255, 255, 255, 0.8); - font-size: 8em; - display: block; - position: relative !important; - top: auto; - left: auto; - line-height: 1em; - margin: 0 0 0.1875em; - } - } - - .woocommerce-BlankState-cta { - font-size: 1.2em; - padding: 0.75em 1.5em; - margin: 0 0.25em; - height: auto; - display: inline-block !important; - } -} - -.post-type-product .woocommerce-BlankState, -.post-type-shop_order .woocommerce-BlankState { - max-width: 764px; - text-align: center; - margin: auto; - - .woocommerce-BlankState-message { - color: #444; - font-size: 1.5em; - margin: 0 auto 1em; - } - - .woocommerce-BlankState-message::before { - font-size: 120px; - } - - .woocommerce-BlankState-buttons { - margin-bottom: 4em; - } -} - -.post-type-product { - - #wp-pointer-2 .wp-pointer-arrow { - left: 240px; - } - - #wp-pointer-3 .wp-pointer-arrow, - #wp-pointer-4 .wp-pointer-arrow { - left: 46%; - } -} - -/** - * Small screen optimisation - */ -@media only screen and (max-width: 1280px) { - - #order_data { - - .order_data_column { - width: 48%; - - &:first-child { - width: 100%; - } - } - } - - .woocommerce_options_panel { - - .description { - display: block; - clear: both; - margin-left: 0; - } - - .short, - input[type="text"].short, - input[type="email"].short, - input[type="number"].short, - input[type="password"].short, - .dimensions_field .wrap { - width: 80%; - } - } - - - .woocommerce_variations, - .woocommerce_options_panel { - - .downloadable_files { - padding: 0; - clear: both; - - label { - position: static; - } - - table { - margin: 0 12px 24px; - width: 94%; - - .sort { - visibility: hidden; - } - } - } - - .woocommerce_variable_attributes .downloadable_files table { - margin: 0 0 1em; - width: 100%; - } - } -} - -/** - * Optimisation for screens 900px and smaller - */ -@media only screen and (max-width: 900px) { - - #woocommerce-coupon-data ul.coupon_data_tabs, - #woocommerce-product-data ul.product_data_tabs, - #woocommerce-product-data .wc-tabs-back { - width: 10%; - } - - #woocommerce-coupon-data .wc-metaboxes-wrapper, - #woocommerce-coupon-data .woocommerce_options_panel, - #woocommerce-product-data .wc-metaboxes-wrapper, - #woocommerce-product-data .woocommerce_options_panel { - width: 90%; - } - - #woocommerce-coupon-data ul.coupon_data_tabs li a, - #woocommerce-product-data ul.product_data_tabs li a { - position: relative; - text-indent: -999px; - padding: 10px; - - &::before { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - text-indent: 0; - text-align: center; - line-height: 40px; - width: 100%; - height: 40px; - } - } -} - -/** - * Optimisation for screens 782px and smaller - */ -@media only screen and (max-width: 782px) { - - #wp-excerpt-media-buttons a { - font-size: 16px; - line-height: 37px; - height: 39px; - padding: 0 20px 0 15px; - } - - #wp-excerpt-editor-tools { - padding-top: 20px; - padding-right: 15px; - overflow: hidden; - margin-bottom: -1px; - } - - #woocommerce-product-data .checkbox { - width: 25px; - } - - .variations-pagenav { - float: none; - text-align: center; - font-size: 18px; - - .displaying-num { - font-size: 16px; - } - - a { - padding: 8px 20px 11px; - font-size: 18px; - } - - select { - padding: 0 20px; - } - } - - .variations-defaults { - float: none; - text-align: center; - margin-top: 10px; - } - - .post-type-product { - - .wp-list-table { - - .column-thumb { - display: none; - text-align: left; - padding-bottom: 0; - - &::before { - display: none !important; - } - - img { - max-width: 32px; - } - } - - .is-expanded td:not(.hidden) { - overflow: visible; - } - - .toggle-row { - top: -28px; - } - } - } - - .post-type-shop_order { - - .wp-list-table { - - .column-customer_message, - .column-order_notes { - text-align: inherit; - } - - .column-order_notes .note-on { - font-size: 1.3em; - margin: 0; - } - - .is-expanded td:not(.hidden) { - overflow: visible; - } - - .toggle-row { - top: -15px; - } - } - } -} - -@media only screen and (max-width: 500px) { - - .woocommerce_options_panel label, - .woocommerce_options_panel legend { - float: none; - width: auto; - display: block; - margin: 0; - } - - .woocommerce_options_panel fieldset.form-field, - .woocommerce_options_panel p.form-field { - padding: 5px 20px !important; - } - - .addons-wcs-banner-block { - flex-direction: column; - } - - .wc_addons_wrap { - - .addons-wcs-banner-block { - padding: 40px; - } - - .addons-wcs-banner-block-image { - padding: 1em; - text-align: center; - width: 100%; - padding: 2em 0; - margin: 0; - - .addons-img { - margin: 0; - } - } - } -} - -/** - * Backbone modal dialog - */ -.wc-backbone-modal { - - * { - box-sizing: border-box; - } - - .wc-backbone-modal-content { - position: fixed; - background: #fff; - z-index: 100000; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - max-width: 100%; - min-width: 500px; - - article { - overflow: auto; - } - } - - &.wc-backbone-modal-shipping-method-settings .wc-backbone-modal-content { - width: 75%; - min-width: 500px; - } - - .select2-container { - width: 100% !important; - } -} - -@media screen and (max-width: 782px) { - - .wc-backbone-modal .wc-backbone-modal-content { - width: 100%; - height: 100%; - min-width: 100%; - } -} - -.wc-backbone-modal-backdrop { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - min-height: 360px; - background: #000; - opacity: 0.7; - z-index: 99900; -} - -.wc-backbone-modal-main { - padding-bottom: 55px; - - header, - article { - display: block; - position: relative; - } - - .wc-backbone-modal-header { - height: auto; - background: #fcfcfc; - padding: 1em 1.5em; - border-bottom: 1px solid #ddd; - - h1 { - margin: 0; - font-size: 18px; - font-weight: 700; - line-height: 1.5em; - } - - .modal-close-link { - cursor: pointer; - color: #777; - height: 54px; - width: 54px; - padding: 0; - position: absolute; - top: 0; - right: 0; - text-align: center; - border: 0; - border-left: 1px solid #ddd; - background-color: transparent; - transition: color 0.1s ease-in-out, background 0.1s ease-in-out; - - &::before { - font: normal 22px/50px "dashicons" !important; - color: #666; - display: block; - content: "\f335"; - font-weight: 300; - } - - &:hover, - &:focus { - background: #ddd; - border-color: #ccc; - color: #000; - } - - &:focus { - outline: none; - } - } - } - - article { - padding: 1.5em; - - p { - margin: 1.5em 0; - } - - p:first-child { - margin-top: 0; - } - - p:last-child { - margin-bottom: 0; - } - - .pagination { - padding: 10px 0 0; - text-align: center; - } - - table.widefat { - margin: 0; - width: 100%; - border: 0; - box-shadow: none; - - thead th { - padding: 0 1em 1em 1em; - text-align: left; - - &:first-child { - padding-left: 0; - } - - &:last-child { - padding-right: 0; - text-align: right; - } - } - - tbody td, - tbody th { - padding: 1em; - text-align: left; - vertical-align: middle; - - &:first-child { - padding-left: 0; - } - - &:last-child { - padding-right: 0; - text-align: right; - } - - select, - .select2-container { - width: 100%; - } - } - } - } - - footer { - position: absolute; - left: 0; - right: 0; - bottom: 0; - z-index: 100; - padding: 1em 1.5em; - background: #fcfcfc; - border-top: 1px solid #dfdfdf; - box-shadow: 0 -4px 4px -4px rgba(0, 0, 0, 0.1); - - .inner { - text-align: right; - line-height: 23px; - - .button { - margin-bottom: 0; - } - } - } -} - -/** - * Select2 elements. - */ -.select2-drop, -.select2-dropdown { - z-index: 999999 !important; -} - -.select2-results { - line-height: 1.5em; - - .select2-results__option, - .select2-results__group { - margin: 0; - padding: 8px; - } - - .description { - display: block; - color: #999; - padding-top: 4px; - } -} - -.select2-dropdown { - border-color: #ddd; -} - -.select2-dropdown--below { - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); -} - -.select2-dropdown--above { - box-shadow: 0 -1px 1px rgba(0, 0, 0, 0.1); -} - -.select2-container { - - .select2-selection__rendered.ui-sortable li { - cursor: move; - } - - .select2-selection { - border-color: #ddd; - } - - .select2-search__field { - min-width: 150px; - } - - .select2-selection--single { - height: 40px; - - .select2-selection__rendered { - line-height: 40px; - padding-right: 24px; - } - - .select2-selection__arrow { - right: 3px; - height: 36px; - } - } - - .select2-selection--multiple { - min-height: 28px; - border-radius: 0; - line-height: 1.5; - - li { - margin: 0; - } - - .select2-selection__choice { - padding: 2px 6px; - - .description { - display: none; - } - } - } - - .select2-selection__clear { - color: #999; - margin-top: -1px; - z-index: 1; - } - - .select2-search--inline .select2-search__field { - font-family: inherit; - font-size: inherit; - font-weight: inherit; - padding: 3px 0; - } -} - -.woocommerce table.form-table .select2-container { - min-width: 400px !important; -} - -.wc-wp-version-gte-53 { - - .select2-results { - - .select2-results__option, - .select2-results__group { - - &:focus { - outline: none; - } - } - } - - .select2-dropdown { - border-color: #007cba; - - &::after { - position: absolute; - left: 0; - right: 0; - height: 1px; - background: #fff; - content: ""; - } - } - - .select2-dropdown--below { - box-shadow: 0 0 0 1px #007cba, 0 2px 1px rgba(0, 0, 0, 0.1); - - &::after { - top: -1px; - } - } - - .select2-dropdown--above { - box-shadow: 0 0 0 1px #007cba, 0 -2px 1px rgba(0, 0, 0, 0.1); - - &::after { - bottom: -1px; - } - } - - .select2-container { - - @media only screen and (max-width: 782px) { - font-size: 16px; - } - - &:focus { - outline: none; - } - - .select2-selection--single { - height: 30px; - border-color: #7e8993; - - @media only screen and (max-width: 782px) { - height: 40px; - } - - &:focus { - outline: none; - } - - .select2-selection__rendered { - line-height: 28px; - - @media only screen and (max-width: 782px) { - line-height: 38px; - } - - &:hover { - color: #007cba; - } - } - - .select2-selection__arrow { - right: 1px; - height: 28px; - width: 23px; - background: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M5%206l5%205%205-5%202%201-7%207-7-7%202-1z%22%20fill%3D%22%23555%22%2F%3E%3C%2Fsvg%3E") no-repeat right 5px top 55%; - background-size: 16px 16px; - - @media only screen and (max-width: 782px) { - height: 38px; - } - - b { - display: none; - } - } - } - - &.select2-container--focus .select2-selection--single, - &.select2-container--open .select2-selection--single, - &.select2-container--open .select2-selection--multiple { - border-color: #007cba; - box-shadow: 0 0 0 1px #007cba; - } - - .select2-selection--multiple { - min-height: 30px; - border-color: #7e8993; - border-radius: 4px; - } - - .select2-search--inline .select2-search__field { - padding: 0 0 0 3px; - min-height: 28px; - } - - } - - .woocommerce table.form-table .select2-container { - - @media only screen and (max-width: 782px) { - min-width: 100% !important; - } - } -} - -.wc-wp-version-gte-55 { - - #woocommerce-product-data { - - .hndle { - display: block; - line-height: 24px; - - .type_box { - display: inline; - line-height: inherit; - vertical-align: baseline; - } - } - } -} - -/** - * Select2 colors for built-in admin color themes. - */ -.admin-color { - $wp_admin_colors: ( - blue: #096484, - coffee: #c7a589, - ectoplasm: #a3b745, - midnight: #e14d43, - ocean: #9ebaa0, - sunrise: #dd823b, - light: #04a4cc - ); - - @each $name, $color in $wp_admin_colors { - - &-#{$name}.wc-wp-version-gte-53 { - - .select2-dropdown { - border-color: $color; - } - - .select2-dropdown--below { - box-shadow: 0 0 0 1px $color, 0 2px 1px rgba(0, 0, 0, 0.1); - } - - .select2-dropdown--above { - box-shadow: 0 0 0 1px $color, 0 -2px 1px rgba(0, 0, 0, 0.1); - } - - .select2-selection--single .select2-selection__rendered:hover { - color: $color; - } - - .select2-container.select2-container--focus .select2-selection--single, - .select2-container.select2-container--open .select2-selection--single, - .select2-container.select2-container--open .select2-selection--multiple { - border-color: $color; - box-shadow: 0 0 0 1px $color; - } - - .select2-container--default .select2-results__option--highlighted[aria-selected], - .select2-container--default .select2-results__option--highlighted[data-selected] { - background-color: $color; - } - } - } -} - - -.post-type-product .tablenav, -.post-type-shop_order .tablenav { - - .actions { - overflow: visible; - } - - select, - input { - height: 32px; - } - - .select2-container { - float: left; - width: 240px !important; - font-size: 14px; - vertical-align: middle; - margin: 1px 6px 4px 1px; - } -} - -.woocommerce-progress-form-wrapper, -.woocommerce-exporter-wrapper, -.woocommerce-importer-wrapper { - text-align: center; - max-width: 700px; - margin: 40px auto; - - .error { - text-align: left; - } - - .wc-progress-steps { - padding: 0 0 24px; - margin: 0; - list-style: none outside; - overflow: hidden; - color: #ccc; - width: 100%; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; - - li { - width: 25%; - float: left; - padding: 0 0 0.8em; - margin: 0; - text-align: center; - position: relative; - border-bottom: 4px solid #ccc; - line-height: 1.4em; - } - - li::before { - content: ""; - border: 4px solid #ccc; - border-radius: 100%; - width: 4px; - height: 4px; - position: absolute; - bottom: 0; - left: 50%; - margin-left: -6px; - margin-bottom: -8px; - background: #fff; - } - - li.active { - border-color: #a16696; - color: #a16696; - - &::before { - border-color: #a16696; - } - } - - li.done { - border-color: #a16696; - color: #a16696; - - &::before { - border-color: #a16696; - background: #a16696; - } - } - } - - .button { - font-size: 1.25em; - padding: 0.5em 1em !important; - line-height: 1.5em !important; - margin-right: 0.5em; - margin-bottom: 2px; - height: auto !important; - border-radius: 4px; - background-color: #bb77ae; - border-color: #a36597; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; - text-shadow: 0 -1px 1px #a36597, 1px 0 1px #a36597, 0 1px 1px #a36597, -1px 0 1px #a36597; - margin: 0; - opacity: 1; - - &:hover, - &:focus, - &:active { - background: #a36597; - border-color: #a36597; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; - } - } - - .error .button { - font-size: 1em; - } - - .wc-actions { - overflow: hidden; - border-top: 1px solid #eee; - margin: 0; - padding: 23px 24px 24px; - line-height: 3em; - - .button { - float: right; - } - - .woocommerce-importer-toggle-advanced-options { - color: #999; - } - } - - .woocommerce-exporter, - .woocommerce-importer, - .wc-progress-form-content { - background: #fff; - overflow: hidden; - padding: 0; - margin: 0 0 16px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.13); - color: #555; - text-align: left; - - header { - border-bottom: 1px solid #eee; - margin: 0; - padding: 24px 24px 0; - } - - section { - padding: 24px 24px 0; - } - - h2 { - margin: 0 0 24px; - color: #555; - font-size: 24px; - font-weight: normal; - line-height: 1em; - } - - p { - font-size: 1em; - line-height: 1.75em; - font-size: 16px; - color: #555; - margin: 0 0 24px; - } - - .form-row { - margin-top: 24px; - } - - .spinner { - display: none; - } - - .woocommerce-importer-options th, - .woocommerce-importer-options td, - .woocommerce-exporter-options th, - .woocommerce-exporter-options td { - vertical-align: top; - line-height: 1.75em; - padding: 0 0 24px 0; - - label { - color: #555; - font-weight: normal; - } - - input[type="checkbox"] { - margin: 0 4px 0 0; - padding: 7px; - } - - input[type="text"], - input[type="number"] { - padding: 7px; - height: auto; - margin: 0; - } - - .woocommerce-importer-file-url-field-wrapper { - border: 1px solid #ddd; - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.07); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.07); - background-color: #fff; - color: #32373c; - outline: 0; - line-height: 1; - display: block; - - code { - background: none; - font-size: smaller; - padding: 0; - margin: 0; - color: #999; - padding: 7px 0 0 7px; - display: inline-block; - } - - input { - font-family: Consolas, Monaco, monospace; - border: 0; - margin: 0; - outline: 0; - box-shadow: none; - display: inline-block; - min-width: 100%; - } - } - } - - .woocommerce-exporter-options th, - .woocommerce-importer-options th { - width: 35%; - padding-right: 20px; - } - - progress { - width: 100%; - height: 42px; - margin: 0 auto 24px; - display: block; - -webkit-appearance: none; - border: none; - display: none; - background: #f5f5f5; - border: 2px solid #eee; - border-radius: 4px; - padding: 0; - box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.2); - } - - progress::-webkit-progress-bar { - background: transparent none; - border: 0; - border-radius: 4px; - padding: 0; - box-shadow: none; - } - - progress::-webkit-progress-value { - border-radius: 3px; - box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, 0.4); - background: #a46497; - background: linear-gradient(to bottom, #a46497, #66405f), #a46497; - transition: width 1s ease; - } - - progress::-moz-progress-bar { - border-radius: 3px; - box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, 0.4); - background: #a46497; - background: linear-gradient(to bottom, #a46497, #66405f), #a46497; - transition: width 1s ease; - } - - progress::-ms-fill { - border-radius: 3px; - box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, 0.4); - background: #a46497; - background: linear-gradient(to bottom, #a46497, #66405f), #a46497; - transition: width 1s ease; - } - - &.woocommerce-exporter__exporting, - &.woocommerce-importer__importing { - - .spinner { - display: block; - } - - progress { - display: block; - } - - .wc-actions, - .woocommerce-exporter-options { - display: none; - } - } - - .wc-importer-mapping-table-wrapper, - .wc-importer-error-log { - padding: 0; - } - - .wc-importer-mapping-table, - .wc-importer-error-log-table { - margin: 0; - border: 0; - box-shadow: none; - width: 100%; - table-layout: fixed; - - td, - th { - border: 0; - padding: 12px; - vertical-align: middle; - word-wrap: break-word; - - select { - width: 100%; - } - } - - tbody tr:nth-child(odd) td, - tbody tr:nth-child(odd) th { - background: #fbfbfb; - } - - th { - font-weight: bold; - } - - td:first-child, - th:first-child { - padding-left: 24px; - } - - td:last-child, - th:last-child { - padding-right: 24px; - } - - .wc-importer-mapping-table-name { - width: 50%; - - .description { - color: #999; - margin-top: 4px; - display: block; - - code { - background: none; - padding: 0; - white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */ - word-wrap: break-word; /* IE */ - word-break: break-all; - } - } - } - } - - .woocommerce-importer-done { - text-align: center; - padding: 48px 24px; - font-size: 1.5em; - line-height: 1.75em; - - &::before { - - @include icon( "\e015" ); - color: #a16696; - position: static; - font-size: 100px; - display: block; - float: none; - margin: 0 0 24px; - } - } - } -} - -.wc-pointer { - - .wc-pointer-buttons { - - .close { - float: left; - margin: 6px 0 0 15px; - } - } -} - -.wc-quick-edit-warning { - color: darkred; - font-weight: bold; -} diff --git a/assets/css/twenty-twenty-one.scss b/assets/css/twenty-twenty-one.scss deleted file mode 100644 index 1ddfb83a5dc..00000000000 --- a/assets/css/twenty-twenty-one.scss +++ /dev/null @@ -1,3060 +0,0 @@ -@import "mixins"; - -/** - * Sass variables - */ - -$headings: -apple-system, blinkmacsystemfont, "Helvetica Neue", helvetica, sans-serif; -$body: nonbreakingspaceoverride, "Hoefler Text", garamond, "Times New Roman", serif; - -$body-color: currentColor; -$highlights-color: #88a171; - -/** - * Fonts - */ -@font-face { - font-family: star; - src: url(../fonts/star.eot); - src: - url(../fonts/star.eot?#iefix) format("embedded-opentype"), - url(../fonts/star.woff) format("woff"), - url(../fonts/star.ttf) format("truetype"), - url(../fonts/star.svg#star) format("svg"); - font-weight: 400; - font-style: normal; -} - -@font-face { - font-family: WooCommerce; - src: url(../fonts/WooCommerce.eot); - src: - url(../fonts/WooCommerce.eot?#iefix) format("embedded-opentype"), - url(../fonts/WooCommerce.woff) format("woff"), - url(../fonts/WooCommerce.ttf) format("truetype"), - url(../fonts/WooCommerce.svg#WooCommerce) format("svg"); - font-weight: 400; - font-style: normal; -} - -/** - * Global elements - */ -a.button { - display: inline-block; - text-align: center; - box-sizing: border-box; - word-break: break-word; - text-decoration: none !important; - - &:hover, - &:visited { - text-decoration: underline !important; - } -} - -.woocommerce { - - form .form-row { - - .required { - color: #b22222; - text-decoration: none; - visibility: hidden; // Only show optional by default. - - &[title] { - border: 0 !important; - } - } - - .optional { - visibility: visible; - } - } - - form.woocommerce-form-login, - form.woocommerce-form-register { - - p, - label { - font-family: $headings; - } - - input { - border: 1px solid #ddd; - } - } - - .woocommerce-form-login__rememberme { - margin: 1rem 0 3rem 0; - } -} - -.woocommerce-notices-wrapper:empty { - margin: 0 auto; -} - -.woocommerce-view-order { - - .woocommerce-MyAccount-content { - - table { - - border: 0; - - tbody { - border-bottom: 1px solid $body-color; - } - - tfoot { - - tr:last-of-type { - border-top: 1px solid $body-color; - - .woocommerce-Price-amount { - font-weight: 700; - } - } - } - - td, - tr, - th { - border: 0; - } - } - } -} - -.site-main { - .woocommerce-breadcrumb { - margin-bottom: var(--global--spacing-vertical); - font-size: 0.88889em; - font-family: $headings; - } - .woocommerce-products-header { - margin-top: var(--global--spacing-vertical); - } -} - - -.woocommerce-pagination { - font-family: $headings; - font-size: 0.88889em; - - ul.page-numbers { - margin: 0; - padding: 0; - display: block; - font-weight: 700; - letter-spacing: -0.02em; - line-height: 1.2; - } - - span.page-numbers, - a.page-numbers, - .next.page-numbers, - .prev.page-numbers { - padding: 0 calc(0.5 * 1rem); - display: inline-block; - } -} - -.onsale { - position: absolute; - top: -0.7rem; - right: -0.7rem; - background: $highlights-color; - color: #fff; - font-family: $headings; - font-size: 1.2rem; - font-weight: 700; - letter-spacing: -0.02em; - z-index: 1; - border-radius: 50%; - text-align: center; - padding: 0.8rem; - margin: 0; - display: inline-flex; - align-items: center; - justify-content: center; - - &::before { - content: ""; - float: left; - padding-top: 100%; - } -} - -.onsale + .woocommerce-product-gallery .woocommerce-product-gallery__trigger { - top: 2.2em; - right: 2.2em; -} - -.single-product .type-product.sale > .onsale { - right: calc(52% - 0.7rem); -} - -.price { - font-family: $headings; - font-size: 1rem; - - del { - opacity: 0.5; - display: inline-block; - } - - ins { - display: inline-block; - text-decoration: none; - } -} - -.woocommerce-message, -.woocommerce-error, -.woocommerce-info { - margin-bottom: 2rem; - margin-left: 0; - background: var(--global--color-background); - font-size: 0.88889em; - font-family: $headings; - list-style: none; - overflow: hidden; -} - -.woocommerce-message, -.woocommerce-error li, -.woocommerce-info { - padding: 1.5rem 3rem; - justify-content: space-between; - align-items: center; - - .button { - order: 2; - } -} - -.woocommerce-error { - color: #fff; - background: #b22222; - - a { - color: #fff; - - &:hover { - color: #fff; - } - - &.button { - background: #111; - } - } - - > li { - margin: 0; - } -} - -#main { - - .woocommerce-error, - .woocommerce-info { - font-family: $headings; - } -} - -.woocommerce-message, -.woocommerce-info { - background: #eee; - color: #000; - border-top: 2px solid $highlights-color; - - a { - color: #444; - - &:hover { - color: #000; - } - - &.button { - background: $highlights-color; - color: #f5efe0; - } - } -} - -.woocommerce-store-notice { - background: #eee; - color: #000; - border-top: 2px solid $highlights-color; - padding: 2rem; - position: absolute; - top: 0; - left: 0; - width: 100%; - z-index: 999; -} - -.admin-bar .woocommerce-store-notice { - top: 32px; -} - -.woocommerce-store-notice__dismiss-link { - float: right; - color: #000; - - &:hover { - text-decoration: none; - color: #000; - } -} - -.flex-viewport { - margin-bottom: 1.5em; -} - -#main { - - .post-inner { - padding-top: 0; - } - - .wp-block-cover { - margin-top: 0; - } -} - -.cross-sells { - - .woocommerce-loop-product__title { - font-family: $headings; - } - - .star-rating { - font-size: 1.4rem; - } -} - -/* Make thumbnails in the gallery affect parent's height and wrapping */ -.flex-control-nav::after { - clear: both; - content: ""; - display: table; -} - -/** -* Tables -*/ -.woocommerce, -.woocommerce-page { - - table.shop_table { - - td, - th { - word-break: normal; - border-left: none; - border-right: none; - } - - .product-thumbnail { - max-width: 120px; - } - } -} - -/** - * Shop page - */ -.woocommerce-result-count, -.woocommerce-ordering { - margin: 0 0 1rem; - padding: 0.75rem 0; -} - -/** - * Products - */ -ul.products { - margin: 0; - padding: 0; - - li.product { - list-style: none; - - .woocommerce-loop-product__link { - display: block; - text-decoration: none; - } - - .woocommerce-loop-product__title { - margin: 0.5rem 0 0.5rem; - font-size: 1.5rem; - font-weight: 400; - - &::before { - content: none; - } - } - - .woocommerce-loop-product__title, - .price, - .star-rating { - color: $body-color; - } - - .star-rating { - margin-bottom: 0.8rem; - } - - .price { - margin-bottom: 1rem; - } - - .price, - .star-rating { - display: block; - } - - .woocommerce-placeholder { - border: 1px solid #f2f2f2; - } - - .button { - vertical-align: middle; - background-color: transparent; - color: var(--button--color-text-hover); - text-decoration: none !important; - - &.loading { - opacity: 0.5; - } - - &:hover { - background-color: var(--button--color-background); - color: var(--button--color-text); - } - } - - .added_to_cart { - margin: 0.5rem; - } - } -} - -.star-rating { - overflow: hidden; - position: relative; - height: 1em; - line-height: 1; - font-size: 1em; - width: 5.4em; - font-family: star; - margin-bottom: 0.7rem; - - &::before { - content: "\73\73\73\73\73"; - float: left; - top: 0; - left: 0; - position: absolute; - } - - span { - overflow: hidden; - float: left; - top: 0; - left: 0; - position: absolute; - padding-top: 1.5em; - } - - span::before { - content: "\53\53\53\53\53"; - top: 0; - position: absolute; - left: 0; - } -} - -a.remove { - display: inline-block; - width: 20px; - height: 20px; - line-height: 18px; - font-size: 20px; - font-weight: 700; - text-align: center; - border-radius: 100%; - text-decoration: none !important; - background: #fff; - color: #000; - - &:hover { - background: $highlights-color; - color: #fff !important; - } -} - -dl.variation, -.wc-item-meta { - list-style: none outside; - - dt, - .wc-item-meta-label { - float: left; - clear: both; - margin-right: 0.25rem; - margin-top: 0; - list-style: none outside; - font-weight: 400; - } - - dd { - margin: 0; - } - - p, - &:last-child { - margin-bottom: 0; - } -} - -/** - * Single product - */ -.single-product { - - div.product { - position: relative; - - .product_meta { - clear: both; - font-size: 0.7em; - padding-top: 0.5em; - margin-top: 3rem; - } - } - - .single_add_to_cart_button { - line-height: var(--global--line-height-body) !important; - padding-top: var(--form--spacing-unit) !important; - padding-bottom: var(--form--spacing-unit) !important; - font-size: 1.6rem; - } - - .single-featured-image-header { - display: none; - } - - - &.singular { // Needed for higher specificity to target the entry title font size - .entry-title { - font-size: var(--global--font-size-xl); - font-weight: normal; - margin: 0 0 2.5rem; - - &::before { - margin-top: 0; - } - } - } - - .summary { - margin-bottom: 8rem; - - p.price { - margin-bottom: 2rem; - } - - .woocommerce-product-details__short-description { - margin-bottom: 1rem; - } - } - - .woocommerce-variation-price { - margin: 2rem 0; - } - - .woocommerce-product-rating { - margin: -1rem 0 4rem; - line-height: 1; - font-size: 1.4rem; - - .star-rating { - float: left; - margin-right: 0.25rem; - } - } - - form.cart { - - .quantity { - float: left; - margin-right: 0.5rem; - } - - input { - width: 5em; - } - } - - .woocommerce-variation-add-to-cart { - - .button { - padding-top: 1.55rem; - padding-bottom: 1.59rem; - font-size: 1.6rem; - } - - .button.disabled { - opacity: 0.2; - } - } - - .woocommerce-message { - flex-direction: row-reverse; - } - - .woocommerce-Tabs-panel--additional_information, - .woocommerce-Tabs-panel--reviews { - - table { - border: 1px solid #ddd; - - tr, - td, - th { - border: 1px solid #ddd; - } - } - - p { - font-family: $headings; - } - - input { - border: 1px solid #ddd; - } - } - - .woocommerce-product-attributes-item__value { - - p { - margin-bottom: 0; - } - } -} - -table.variations { - margin: 1rem 0; - - label { - margin: 0; - padding: 6px 0; - } - - select { - margin-right: 0.5rem; - } -} - -a.reset_variations { - margin-left: 0.5em; -} - -.woocommerce-product-gallery { - max-width: 600px; - position: relative; - margin-bottom: 2rem; - - figure { - margin: 0; - padding: 0; - } - - .woocommerce-product-gallery__wrapper { - margin: 0; - padding: 0; - } - - .zoomImg { - background-color: #fff; - opacity: 0; - } - - .woocommerce-product-gallery__image--placeholder { - border: 1px solid #f2f2f2; - } - - .woocommerce-product-gallery__image:nth-child(n+2) { - width: 25%; - display: inline-block; - } - - .flex-control-thumbs { - - li { - list-style: none; - cursor: pointer; - float: left; - } - - img { - opacity: 0.5; - - &:hover, - &.flex-active { - opacity: 1; - } - } - } - - img { - display: block; - height: auto; - } -} - -.woocommerce-product-gallery--columns-3 { - - .flex-control-thumbs li { - width: 33.3333%; - } - - .flex-control-thumbs li:nth-child(3n+1) { - clear: left; - } -} - -.woocommerce-product-gallery--columns-4 { - - ol { - margin-left: 0; - margin-bottom: 0; - } - - .flex-control-thumbs li { - width: 14.2857142857%; - margin: 0 14.2857142857% 1.6em 0; - } - - .flex-control-thumbs li:nth-child(4n) { - margin-right: 0; - } - - .flex-control-thumbs li:nth-child(4n+1) { - clear: left; - } -} - -.woocommerce-product-gallery--columns-5 { - - .flex-control-thumbs li { - width: 20%; - } - - .flex-control-thumbs li:nth-child(5n+1) { - clear: left; - } -} - -.woocommerce-product-gallery__trigger { - position: absolute; - top: 1rem; - right: 1rem; - z-index: 99; -} - -.woocommerce-tabs { - margin: 4rem 0 2rem; - - /* reset description tab width to full width */ - #tab-description { - - h2, - p { - max-width: 100vw; - width: 100%; - } - } - - /* reset additional info tab width to full width */ - #tab-additional_information { - - .woocommerce-product-attributes { - max-width: 100vw; - width: 100%; - } - } - - #tab-reviews { - - /* reset reviews tab width to full width */ - .woocommerce-Reviews { - max-width: 100vw; - width: 100%; - } - - #submit { - float: right; - } - } - - - ul { - margin: 0 0 1.5rem; - padding: 0; - font-family: $headings; - border-bottom: var(--button--border-width) solid var(--button--color-background); - - li { - display: inline-flex !important; - - a { - color: $body-color; - text-decoration: none; - font-weight: 700; - padding: var(--button--padding-vertical) var(--button--padding-horizontal); - } - - &.active { - - a { - color: var(--button--color-text); - background-color: var(--button--color-background); - border: var(--button--border-width) solid var(--button--color-background); - } - } - } - } - - .panel { - - > * { - margin-top: 0 !important; - } - - h1, - h2 { - - &::before { - content: none; - } - } - - h2:first-of-type { - font-size: var(--global--font-size-lg); - margin: 0 0 2rem !important; - } - } - - #comments { - padding-top: 0; - } - - .comment-reply-title { - font-family: $headings; - font-size: 1em; - font-weight: 700; - display: block; - } - - #reviews { - - ol.commentlist { - padding: 0; - margin: 0; - } - - li.review, - li.comment { - list-style: none; - margin: 0.5rem 0 2.5rem 0; - - .avatar { - max-height: 36px; - width: auto; - float: right; - } - - p.meta { - margin-bottom: 0.5em; - } - } - - .comment-form-rating { - - label { - max-width: 58rem; - margin: 0 auto; - } - } - - p.stars { - margin-top: 0; - - a { - position: relative; - height: 1em; - width: 1em; - text-indent: -999em; - display: inline-block; - text-decoration: none; - box-shadow: none; - - &::before { - display: block; - position: absolute; - top: 0; - left: 0; - width: 1em; - height: 1em; - line-height: 1; - font-family: WooCommerce; - content: "\e021"; - text-indent: 0; - } - - &:hover { - - ~ a::before { - content: "\e021"; - } - } - } - - &:hover { - - a { - - &::before { - content: "\e020"; - } - } - } - - &.selected { - - a.active { - - &::before { - content: "\e020"; - } - - ~ a::before { - content: "\e021"; - } - } - - a:not(.active) { - - &::before { - content: "\e020"; - } - } - } - } - - .comment-form-author, - .comment-form-email { - float: none; - margin-left: auto; - } - } -} - -/** - * Related products - */ - -.related.products, -.up-sells { - - h2 { - margin-bottom: 2rem; - } - - clear: both; - - ul.products { - display: flex; - justify-content: space-evenly; - align-items: stretch; - - li.product { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: flex-start; - } - } -} - -/** - * Widgets - */ -.widget.woocommerce { - - ul { - padding-left: 0; - - li { - list-style: none; - } - } -} - -.widget .product_list_widget, -.site-footer .widget .product_list_widget { - margin-bottom: 1.5rem; - - a { - display: block; - box-shadow: none; - - &:hover { - box-shadow: none; - } - } - - li { - padding: 0.5rem 0; - - a.remove { - float: left; - margin-top: 7px; - line-height: 20px; - color: #fff; - margin-right: 0.5rem; - } - } - - img { - display: none; - } -} - -.widget_shopping_cart { - - .buttons { - - a { - display: inline-block; - margin: 0 0.5rem 0 0; - } - } -} - -.woocommerce-shopping-totals { - vertical-align: text-top; -} - -.widget_layered_nav { - - .chosen { - - &::before { - content: "×"; - display: inline-block; - width: 16px; - height: 16px; - line-height: 16px; - font-size: 16px; - text-align: center; - border-radius: 100%; - border: 1px solid #000; - margin-right: 0.25rem; - } - } -} - -.widget_price_filter { - - .price_slider { - margin-bottom: 1rem; - } - - .price_slider_amount { - text-align: right; - line-height: 2.4; - font-size: 0.8751em; - - .button { - float: left; - padding: 0.4rem 1rem; - } - } - - .ui-slider { - position: relative; - text-align: left; - margin-left: 0.5rem; - margin-right: 0.5rem; - } - - .ui-slider .ui-slider-handle { - position: absolute; - z-index: 2; - width: 1em; - height: 1em; - background-color: #000; - border-radius: 1em; - cursor: ew-resize; - outline: none; - top: -0.3em; - margin-left: -0.5em; - } - - .ui-slider .ui-slider-range { - position: absolute; - z-index: 1; - font-size: 0.7em; - display: block; - border: 0; - border-radius: 1em; - background-color: #000; - } - - .price_slider_wrapper .ui-widget-content { - border-radius: 1em; - background-color: #666; - border: 0; - } - - .ui-slider-horizontal { - height: 0.5em; - } - - .ui-slider-horizontal .ui-slider-range { - top: 0; - height: 100%; - } - - .ui-slider-horizontal .ui-slider-range-min { - left: -1px; - } - - .ui-slider-horizontal .ui-slider-range-max { - right: -1px; - } -} - -.widget_rating_filter { - - li { - text-align: right; - - .star-rating { - float: left; - margin-top: 0.3rem; - } - } -} - -.widget_product_search { - - form { - position: relative; - } - - .search-field { - padding-right: 100px; - } - - input[type="submit"] { - position: absolute; - top: 0.5rem; - right: 0.5rem; - padding-left: 1rem; - padding-right: 1rem; - } -} - -/** - * Account section - */ -.woocommerce-account { - - #main { - - .post-inner { - padding-top: 0; - } - - .woocommerce { - max-width: 1600px; - padding: 0 6vw; - margin: 0 auto; - } - } - - .woocommerce-MyAccount-navigation { - font-family: $headings; - margin: 0 0 2rem; - - ul { - margin: 0; - padding: 0; - } - - li { - list-style: none; - padding: 0.5rem 0; - font-family: $headings; - font-size: 2rem; - - &:first-child { - padding-top: 0; - } - - a { - box-shadow: none; - text-decoration: none; - font-weight: 600; - color: #aaa; - - &:hover { - color: #000; - text-decoration: underline; - } - } - - &.is-active { - - a { - text-decoration: underline; - color: $highlights-color; - } - } - } - } - - .woocommerce-MyAccount-content { - - p { - font-family: $headings; - font-size: 2rem; - } - - form { - - h3 { - margin-top: 0; - } - } - - .woocommerce-Addresses { - margin-top: -1rem; - - .woocommerce-Address-title { - - h3 { - display: inline-block; - margin-right: 1rem; - font-size: 1.8rem; - margin-top: 2rem; - } - } - - address { - line-height: 1.8rem; - } - } - - .woocommerce-address-fields { - - label { - font-size: 1.5rem; - margin-bottom: 0.1rem; - } - - input, - .selection { - font-size: 1.5rem; - padding-top: 0.3rem; - padding-bottom: 0.3rem; - } - - input { - border: 3px solid black; - } - - .form-row { - margin-top: 1.5rem !important; - margin-bottom: 0 !important; - } - - #billing_company_field { - padding-top: 1.5rem !important; - } - - .select2-selection { - border: 2px solid black; - height: 3rem; - padding-top: 0.5rem; - margin-top: -1rem; - } - - .select2-selection__arrow { - position: absolute; - top: -0.2rem; - } - - .select2-dropdown { - border: 2px solid black !important; - } - - .woocommerce-address-fields__field-wrapper { - margin-bottom: 2rem; - } - } - } - - table.account-orders-table { - margin-top: 0; - border: 0; - - tr, - td, - th { - border: 0; - } - - td { - padding-left: 1.5rem; - } - - thead { - border-bottom: 1px solid #ddd; - } - - .button { - margin: 0 0.35rem 0.35rem 0; - width: 80%; - } - } - - table.account-orders-table:not(.has-background) { - - tbody { - - tr:nth-child(2n+1) { - - td { - background: var(--global--color-background); - filter: brightness(88%); - - .is-dark-theme & { - filter: brightness(112%); - } - } - } - } - } - - .woocommerce-EditAccountForm { - - label { - font-size: 1.5rem; - } - - input { - border: var(--form--border-width) solid var(--form--border-color); - font-size: 1.5rem; - } - - fieldset { - border: none; - padding-left: 0; - padding-right: 0; - margin-top: 30px; - - legend { - display: contents; - font-size: 2rem; - } - - p { - margin-top: 20px; - margin-bottom: 0 !important; - } - - .show-password-input { - display: inherit; - } - } - - button { - margin-top: 0; - } - - #account_display_name + span { - font-size: 1.5rem; - } - - p { - margin-top: 20px; - - &:nth-of-type(4) { - margin-top: 30px; - } - } - } -} - -.logged-in.woocommerce-account { - - #main { - - .woocommerce { - display: flex; - flex-direction: row; - } - } -} - -.checkout-button { - display: block; - padding: 1rem 2rem; - border: 2px solid #000; - text-align: center; - font-weight: 800; - - &:hover { - border-color: #999; - } - - &::after { - content: "→"; - margin-left: 0.5rem; - } -} - -.woocommerce-cart { - - .post-inner { - padding-top: 0; - } - - #main { - - .woocommerce { - max-width: var(--responsive--alignwide-width); - margin: 0 auto; - - } - } - - .select2-container .select2-dropdown { - border: var(--form--border-width) solid var(--form--border-color); - border-radius: var(--form--border-radius); - border-top: none; - } - - .select2-container .select2-selection { - border: var(--form--border-width) solid var(--form--border-color); - border-radius: var(--form--border-radius); - } - - .select2-container--focus .select2-selection, - .select2-container--open .select2-selection { - outline-offset: 2px; - outline: 2px dotted var(--form--border-color); - } - - .select2-results__option { - margin-left: 0; - } - - .select2-container { - - .select2-search__field { - height: 3rem; - background: #eee; - } - } - - p.form-row { - - input { - border: 1px solid #ddd; - } - } - - table.cart img.woocommerce-placeholder { - height: auto !important; - } -} - -/** - * Checkout - */ -.woocommerce-form-coupon-toggle .woocommerce-info { - display: block; - margin-bottom: 2rem; - padding: 1rem; -} - -.woocommerce-form-coupon { - background: #eee; - padding: 1rem; - font-size: 0.88889em; - color: var(--form--color-text); - - #coupon_code { - border: var(--form--border-width) solid var(--form--border-color); - } - - button[name="apply_coupon"] { - padding: 0.5rem; - - .is-dark-theme & { - border-color: var(--global--color-background); - - &:hover, - &:active { - background: var(--global--color-background); - } - } - } -} - -#ship-to-different-address { - font-size: 1em; - display: inline-block; - margin: 1.42em 0; - - label { - font-weight: 400; - cursor: pointer; - - span { - position: relative; - display: block; - text-align: right; - padding-right: 45px; - - &::before { - content: ""; - display: block; - height: 16px; - width: 30px; - border: 2px solid var(--form--border-color); - background: var(--global--color-primary); - border-radius: 13rem; - box-sizing: content-box; - transition: all ease-in-out 0.3s; - position: absolute; - top: 0; - right: 0; - } - - &::after { - content: ""; - display: block; - width: 14px; - height: 14px; - background: var(--global--color-background); - position: absolute; - top: 3px; - right: 17px; - border-radius: 13rem; - transition: all ease-in-out 0.3s; - } - } - - input[type="checkbox"] { - display: none; - } - - input[type="checkbox"]:checked + span::after { - right: 3px; - background: var(--global--color-primary); - } - - input[type="checkbox"]:checked + span::before { - background: var(--global--color-background); - } - } -} - -.woocommerce-no-js { - - form.woocommerce-form-login, - form.woocommerce-form-coupon { - display: block !important; - } - - .woocommerce-form-login-toggle, - .woocommerce-form-coupon-toggle, - .showcoupon { - display: none !important; - } -} - -.woocommerce-terms-and-conditions { - border: 1px solid rgba(0, 0, 0, 0.2); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); - background: rgba(0, 0, 0, 0.05); -} - -.woocommerce-terms-and-conditions-link { - display: inline-block; - - &::after { - content: ""; - display: inline-block; - border-style: solid; - margin-bottom: 2px; - margin-left: 0.25rem; - border-width: 6px 6px 0 6px; - border-color: $body-color transparent transparent transparent; - } - - &.woocommerce-terms-and-conditions-link--open::after { - border-width: 0 6px 6px 6px; - border-color: transparent transparent $body-color transparent; - } -} - -.woocommerce-checkout { - - .woocommerce { - max-width: var(--responsive--alignwide-width); - margin: 0 auto; - } - - ul.woocommerce-error { - flex-direction: column; - align-items: flex-start; - - li { - font-family: $headings; - margin: 0.5rem 0 0.5rem; - } - } - - .post-inner { - padding-top: 0; - } - - .woocommerce-billing-fields { - - h3 { - margin: 2rem 0; - } - } - - form[name="checkout"] { - display: table; - } - - .blockUI.blockOverlay { - position: relative; - - @include loader(); - } - - form { - - .col2-set { - width: 50%; - float: left; - padding-right: 1.5vw; - - .col-1, - .col-2 { - float: none; - width: 100%; - } - - label { - font-family: $headings; - letter-spacing: normal; - } - - p { - margin-bottom: 1.15em; - } - } - - #order_review_heading { - margin-top: 2rem; - } - - #order_review_heading, - #order_review { - width: 50%; - padding-left: 1.5vw; - float: right; - clear: right; - - .woocommerce-checkout-review-order-table { - margin-top: 2rem; - border: 0; - - th, - td { - border: 0; - } - - thead { - display: none; - } - - .woocommerce-Price-amount { - font-weight: bold; - } - - .cart-subtotal, - .order-total { - border-top: 2px solid var(--form--border-color); - } - } - } - - .form-row.woocommerce-invalid { - - input.input-text { - border: 2px solid $highlights-color; - } - } - - } - - .woocommerce-input-wrapper { - - .description { - background: #4169e1; - color: #fff; - border-radius: 3px; - padding: 1rem; - margin: 0.5rem 0 0; - clear: both; - display: none; - position: relative; - - a { - color: #fff; - text-decoration: underline; - border: 0; - box-shadow: none; - } - - &::before { - left: 50%; - top: 0; - margin-top: -4px; - transform: translateX(-50%) rotate(180deg); - content: ""; - position: absolute; - border-width: 4px 6px 0 6px; - border-style: solid; - border-color: #4169e1 transparent transparent transparent; - z-index: 100; - display: block; - } - } - } - - .woocommerce-form-login { - - p.form-row.form-row-first, - p.form-row.form-row-last { - float: none; - } - } - - .select2-choice, - .select2-choice:hover { - box-shadow: none !important; - } - - .select2-choice { - padding: 0.7rem 0 0.7rem 0.7rem; - } - - .select2-container .select2-selection--single { - height: 48px; - } - - .select2-container .select2-selection--single .select2-selection__rendered { - line-height: 48px; - } - - .select2-container .select2-selection { - border: var(--form--border-width) solid var(--form--border-color); - border-radius: var(--form--border-radius); - } - - .select2-container .select2-dropdown { - border: var(--form--border-width) solid var(--form--border-color); - border-radius: var(--form--border-radius); - border-top: none; - } - - .select2-container--default .select2-selection--single .select2-selection__arrow { - height: 46px; - } - - .select2-container--focus .select2-selection, - .select2-container--open .select2-selection { - outline-offset: 2px; - outline: 2px dotted var(--form--border-color); - } - - .select2-results__option { - margin-left: 0; - } - - .select2-container { - - .select2-search__field { - height: 3rem; - background: #eee; - } - } -} - -.woocommerce-checkout-review-order-table { - - ul li { - list-style-type: none; - } - - input[type="radio"].shipping_method { - display: none; - - & + label { - - &::before { - content: ""; - display: inline-block; - width: 14px !important; - height: 14px; - border: var(--form--border-width) solid var(--form--border-color); - background: var(--global--color-white); - margin-left: 4px; - margin-right: 1.2rem; - border-radius: 100%; - transform: translateY(2px); - } - } - - &:checked + label { - - &::before { - background: var(--global--color-border); - } - - .is-dark-theme &::before { - background: var(--global--color-background); - } - } - } - - td { - padding: 1rem 0.5em; - } - - dl.variation { - margin: 0; - - p { - margin: 0; - } - - dt, - dd { - font-family: $headings; - - p { - padding-top: 1px; - font-family: $headings; - } - } - } - - tfoot { - text-align: left; - } -} - -.woocommerce-order-received { - - .woocommerce-order { - - p, - li { - font-family: $headings; - } - } - - table { - border: 0; - - td, - th, - tr { - border: 0; - } - - tr { - height: 5rem; - } - - tfoot { - border-top: 1px solid #ddd; - - /* Targeting total */ - tr:last-of-type { - border-top: 1px solid #ddd; - - .woocommerce-Price-amount { - font-weight: 700; - } - } - } - - } -} - -.woocommerce-checkout-review-order { - - ul { - margin: 2rem 0 3rem; - padding-left: 0; - } - - #place_order { - width: 100%; - } -} - -.wc_payment_method { - list-style: none; - - .payment_box { - padding: 1rem; - background: #eee; - color: var(--global--color-dark-gray); - - a, - a:hover, - a:visited { - color: var(--global--color-dark-gray); - } - - ul, - ol { - - &:last-of-type { - margin-bottom: 0; - } - } - - fieldset { - padding: 1.5rem; - padding-bottom: 0; - border: 0; - background: #f6f6f6; - } - - li { - list-style: none; - } - - p { - - &:first-child { - margin-top: 0; - } - - &:last-child { - margin-bottom: 0; - } - } - - input[type=checkbox] { - width: 25px !important; - } - - input[type=radio] { - - & + label::before { - background: #fff !important; - border: var(--form--border-width) solid #000 !important; - } - - &:checked + label::before { - background: #000 !important; - } - } - } - - > label:first-of-type { - display: block; - margin: 1rem 0; - - img { - max-height: 24px; - max-width: 200px; - float: right; - } - } - - label { - cursor: pointer; - } - - input[type="radio"] { - display: none; - - & + label { - font-family: $headings; - - &::before { - content: ""; - display: inline-block; - width: 14px; - height: 14px; - border: var(--form--border-width) solid var(--form--border-color); - background: var(--global--color-white); - margin-left: 4px; - margin-right: 1.2rem; - border-radius: 100%; - transform: translateY(2px); - } - - } - - &:checked + label { - - &::before { - background: var(--global--color-border); - } - - .is-dark-theme &::before { - background: var(--global--color-background); - } - } - } -} - -.wc_payment_methods { - - .payment_box { - - p { - font-family: $headings; - } - } -} - -.account-payment-methods-table { - padding-top: 0 !important; - margin-bottom: 1rem; - - table, - tr { - border-style: hidden; - } - - tr:nth-child(2n) { - - td { - background: transparent !important; - } - } - - tr:nth-child(2n+1) { - - td { - background: var(--global--color-background); - filter: brightness(88%); - - .is-dark-theme & { - filter: brightness(112%); - } - } - } - - td.payment-method-actions { - padding-right: 0.5rem; - padding-left: 0.5rem; - padding-top: 0.3rem; - padding-bottom: 0.3rem; - - display: grid; - border: none; - - font-size: 0; - - a { - width: 100%; - padding-top: 0.3rem !important; - padding-bottom: 0.3rem !important; - margin-top: 0.5rem !important; - margin-bottom: 0.5rem !important; - - @include inversebuttoncolors(); - } - } -} - - -.woocommerce-terms-and-conditions-wrapper { - margin-bottom: 5rem; - - .woocommerce-privacy-policy-text { - - p { - font-family: $headings; - font-size: 1.6rem; - } - } -} - -.woocommerce-order-overview { - margin-bottom: 2rem; -} - -.woocommerce-table--order-details { - margin-bottom: 2rem; -} - -/** - * Layout stuff - */ -.woocommerce { - - section { - padding-top: 2rem; - padding-bottom: 0; - } - - .content-area { - - .site-main { - margin: 0 5vw; - } - } - - /* Shop layout */ - ul.products { - display: flex; - align-items: stretch; - flex-direction: row; - flex-wrap: wrap; - box-sizing: border-box; - word-break: break-word; - min-width: 12vw; - - &.columns-2 { - - li.product { - width: calc(100% / 2 - 16px) !important; - } - } - - &.columns-3 { - - li.product { - width: calc(100% / 3 - 16px) !important; - } - } - - &.columns-4 { - - li.product { - width: calc(100% / 4 - 16px) !important; - } - } - - &.columns-5 { - - li.product { - width: calc(100% / 5 - 16px) !important; - } - } - - &.columns-6 { - - li.product { - width: calc(100% / 6 - 16px) !important; - } - } - - li.product { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: flex-start; - margin: 0 8px 16px 8px; - box-sizing: border-box; - - img.attachment-woocommerce_thumbnail, - img.woocommerce-placeholder { - height: auto !important; - } - } - - li.product-category { - - a { - text-align: left; - text-decoration: none; - - h2.woocommerce-loop-category__title { - margin-top: 0.4rem; - font-family: $headings; - font-size: 1.5rem; - - .count { - background-color: transparent; - color: $body-color; - } - } - } - - mark { - background-color: initial; - } - } - } -} - -@media only screen and (max-width: 600px) { - - .woocommerce { - - .woocommerce-ordering { - float: left; - clear: both; - margin-top: 0; - } - - .woocommerce-result-count { - margin-top: 0; - margin-bottom: 20px; - } - } -} - -@media only screen and (max-width: 667px) { - - .woocommerce, - .woocommerce-page { - - ul.products[class*=columns-] { - - li.product { - width: auto !important; - margin-left: auto; - margin-right: auto; - } - } - } -} - -@media only screen and (min-width: 668px) and (max-width: 768px) { - - .woocommerce, - .woocommerce-page { - - .related.products { - - ul.products[class*=columns-] { - - li.product { - padding: 0 2vw 3em 0 !important; - margin-bottom: 2em; - } - } - } - - ul.products[class*=columns-] { - justify-content: center; - - li.product { - width: 50%; - padding: 0 2vw 3em 0; - } - - } - - .onsale { - font-size: 1rem; - } - - .onsale + .woocommerce-product-gallery .woocommerce-product-gallery__trigger { - top: 1.8em; - right: 1.8em; - } - - } -} - -@media only screen and (max-width: 768px) { - - .woocommerce section.content-area { - padding-top: 0; - } - - #main { - - .woocommerce { - - .woocommerce-cart-form { - - .actions { - - .coupon { - margin-bottom: 2rem; - - button { - width: 100%; - } - } - } - - #coupon_code { - width: 100% !important; - } - } - } - - #shipping_method { - - li { - display: flex; - justify-content: flex-end; - } - } - } - - .woocommerce, - .woocommerce-page { - - .onsale { - right: -0.7rem !important; - } - - .woocommerce-tabs { - - ul { - - li { - font-size: 1rem; - - a { - padding: calc(0.75 * var(--button--padding-vertical)) calc(0.75 * var(--button--padding-horizontal)); - } - } - } - } - - table.shop_table_responsive { - - .button { - - @include inversebuttoncolors(); - } - - tr { - margin: 0 0 1.5rem; - - &:first-child { - border-top: 1px solid; - } - - &:last-child { - margin-bottom: 0; - } - - &:nth-child(2n) { - - td { - background: transparent; - } - } - - &:nth-child(2n+1) { - - td { - background: var(--global--color-background); - filter: brightness(88%); - - .is-dark-theme & { - filter: brightness(112%); - } - } - } - - td { - border-bottom-width: 0; - - &:last-child { - border-bottom-width: 1px; - } - } - - td.product-quantity::before { - padding-top: 0.9rem; - } - - .product-remove { - float: right; - } - - .product-thumbnail { - display: block; - - img { - width: 70px; - } - - &::before { - content: ""; - } - } - } - - } - - .woocommerce-breadcrumb { - margin-bottom: 4rem; - font-size: 0.8em; - font-family: $headings; - } - - .related.products { - - ul.products { - display: flex; - flex-direction: column; - align-items: flex-start; - - li.product { - margin-bottom: 5em; - } - } - } - - .woocommerce-products-header__title.page-title { - margin: 3rem auto 4rem; - } - - .woocommerce-result-count, - .woocommerce-ordering { - font-size: 0.8em; - } - - .woocommerce-ordering { - margin-bottom: 3rem; - } - } - - .woocommerce-cart-form { - - table { - - td.product-name { - padding-left: 0.5em; - } - - input.qty { - padding: 1rem 1.5rem; - } - } - } - - .woocommerce-checkout { - - form { - - .col2-set { - width: 100%; - float: none; - padding-right: 0; - - .col-1, - .col-2 { - float: none; - width: 100%; - } - } - - #order_review_heading { - margin-top: 2rem; - } - - #order_review_heading, - #order_review { - width: 100%; - padding-left: 0; - float: none; - } - - table { - - tbody { - - td.product-total { - text-align: end; - } - } - - tfoot { - - .cart-subtotal, - .order-total { - - td { - text-align: end; - } - } - } - } - } - } - - .logged-in.woocommerce-account { - - #main { - - .woocommerce { - flex-direction: column; - } - - .woocommerce-MyAccount-navigation, - .woocommerce-MyAccount-content { - width: 100%; - } - - table.account-orders-table { - - .button { - padding-left: 0.5em; - padding-right: 0.5em; - width: 100%; - margin: 2rem 0; - } - } - } - - table.account-orders-table { - - td { - padding-bottom: 1.5rem; - } - } - } -} - -@media only screen and (min-width: 768px) { - - /** - * Tables - */ - .woocommerce, - .woocommerce-page { - - table.shop_table { - - tbody { - - tr { - font-size: 0.88889em; - } - } - } - - .onsale { - font-size: 1rem; - } - - } - - /** - * Home page - */ - .home #main { - - [class*="woocommerce columns-"] { - word-break: break-word; - max-width: var(--responsive--aligndefault-width); - margin-left: auto; - margin-right: auto; - } - } - - /** - * Shop page - */ - - .woocommerce-pagination { - - span.page-numbers, - a.page-numbers, - .next.page-numbers, - .prev.page-numbers { - padding: 1rem; - } - } - - /** - * Account section - */ - .woocommerce-account { - - .woocommerce-MyAccount-navigation { - float: none; - width: 20%; - margin-bottom: 1.5rem; - margin-right: 3rem; - - li { - margin: 0 1rem 3rem 0; - padding: 0; - border-bottom: 0; - - &:last-child { - margin-right: 0; - } - } - } - - .woocommerce-MyAccount-content { - float: none; - width: 75%; - } - - table.account-orders-table { - margin-top: 0; - border: 0; - margin-bottom: 1rem; - - tr, - td, - th { - border: 0; - padding: 0; - } - - th, - td, - td.woocommerce-orders-table__cell-order-actions { - width: 1%; - padding-right: 0.5rem; - padding-left: 0.5rem; - - a { - padding-top: 0.3rem !important; - padding-bottom: 0.3rem !important; - margin-top: 0.5rem !important; - margin-bottom: 0.5rem !important; - } - } - - td.woocommerce-orders-table__cell-order-date { - padding-right: 0; - } - - thead { - border-bottom: 1px solid $body-color; - } - - .button { - padding-left: 0.5em; - padding-right: 0.5em; - width: 100%; - margin: 1.5rem 0; - - @include inversebuttoncolors(); - } - } - } - - /** - * Layout stuff - */ - .woocommerce { - - .content-area { - margin: 0 auto; - padding: 0 6vw; - - .site-main { - margin: 0; - } - } - } - - .single-product { - - .entry { - - .entry-content, - .entry-summary { - max-width: none; - margin: 0 0 3rem; - padding: 0; - - > * { - max-width: none; - } - } - } - } - - .woocommerce-breadcrumb { - margin-bottom: 5rem; - font-size: 0.88889em; - font-family: $headings; - } - - .woocommerce-product-gallery { - margin-bottom: 8rem; - } - - .woocommerce-checkout { - - #main { - - .woocommerce { - - max-width: 1600px; - padding: 0 6vw; - margin: 0 auto; - } - } - } - -} - -@media only screen and (min-width: 1168px) { - - .woocommerce { - - .content-area { - max-width: 1600px; - margin: 0 auto; - padding: 0 6vw; - - .site-main { - - } - } - - .onsale { - font-size: 1.2rem; - } - } - - .woocommerce-breadcrumb { - margin-bottom: 5rem; - font-size: 0.88889em; - font-family: $headings; - } - - .woocommerce-product-gallery { - margin-bottom: 8rem; - } - - .woocommerce-account { - - table.account-orders-table { - - th, - td, - td.woocommerce-orders-table__cell-order-actions { - padding-right: 1.5rem; - padding-left: 1.5rem; - } - } - } -} - -@media only screen and (max-width: 768px) { - - .woocommerce-products-header { - border-bottom: none !important; - padding-bottom: 0; - margin-bottom: 0 !important; - } -} - -@media only screen and (min-width: 600px) { - - .woocommerce-products-header { - padding-bottom: 1.5vw; - } - - .woocommerce-ordering, - .woocommerce-result-count { - margin-top: 0 !important; - } -} - -@media only screen and (min-width: 690px) { - - .woocommerce-products-header { - border-bottom: 3px solid var(--global--color-border); - } -} - -.woocommerce-account { - - .woocommerce-MyAccount-content { - - p:first-of-type { - margin-bottom: 2rem; - } - - #add_payment_method { - - ul { - list-style-type: none !important; - } - - .woocommerce-PaymentMethod { - margin-bottom: 1.5rem; - } - } - - input[type=radio] { - float: left; - margin-top: 0.5rem; - margin-right: 0.5rem; - } - - label { - font-size: 1.5rem; - display: flex; - justify-content: flex-end; - - img { - margin-left: 10px !important; - } - - img:first-child { - margin-left: auto !important; - } - - img:last-child { - margin-right: 5px !important; - } - } - - .woocommerce-PaymentBox { - - p, - label { - font-size: 1.3rem; - } - - p { - margin-bottom: 1.5rem; - } - - br { - display: none; - } - - .woocommerce_error { - margin-top: 1rem; - margin-bottom: 0; - } - } - } - - .woocommerce-MyAccount-navigation-link { - - margin-bottom: 20px !important; - - a { - color: $body-color !important; - font-weight: normal !important; - font-size: 1.8rem; - - &:hover { - color: $body-color !important; - text-decoration: underline solid $body-color 1px !important; - } - } - } -} - -.alignwide .woocommerce { - - & > * { - max-width: var(--responsive--alignwide-width); - display: block; - margin: var(--global--spacing-vertical) auto; - } -} - -.woocommerce { - - .woocommerce-notices-wrapper { - - & > * { - padding: 15px; - list-style: none; - } - } - - .return-to-shop, - .wc-proceed-to-checkout { - - a.button { - margin-top: var(--global--spacing-vertical); - float: left; - display: inline-block; - width: 100%; - } - } - - .woocommerce-cart-form { - - .shop_table_responsive { - margin-top: var(--global--spacing-vertical); - margin-bottom: var(--global--spacing-vertical); - - th { - border: none; - } - - #coupon_code { - min-width: 9rem; - } - } - - button[name="update_cart"], - button[name="apply_coupon"] { - padding: 0.5rem; - color: var(--global--color-primary); - background: var(--global--color-background); - border: var(--form--border-width) solid var(--global--color-primary); - - &:hover, - &:active { - color: var(--global--color-background); - background: var(--global--color-primary); - } - } - - .product-thumbnail { - - .attachment-woocommerce_thumbnail { - height: auto !important; - } - } - } - - .cart-collaterals { - - h2 { - margin-bottom: var(--global--spacing-vertical); - } - - #shipping_method { - list-style: none; - padding-left: 0; - } - - .shipping-calculator-form { - - p { - margin-bottom: 0.5rem; - } - - .select2-container { - - .select2-selection { - height: auto; - } - - .select2-selection__rendered { - border: var(--form--border-width) solid var(--form--border-color); - border-radius: var(--form--border-radius); - color: var(--form--color-text); - height: var(--global--line-height-body); - padding: var(--form--spacing-unit); - } - - .select2-selection__arrow { - height: 100%; - } - } - } - - .cross-sells { - - li { - list-style: none; - } - - li > em, - a { - display: inline-block; - } - } - } -} - -/** - * Downloads - */ - -.woocommerce-order-downloads { - - padding-top: 0 !important; - - table, - tr { - border-style: hidden; - - td.download-remaining { - text-align: center !important; - } - } - - tr:nth-child(2n) { - - td { - background: transparent !important; - } - } - - tr:nth-child(2n+1) { - - td { - background: var(--global--color-background); - filter: brightness(88%); - - .is-dark-theme & { - filter: brightness(112%); - } - } - } - - td.download-file { - padding-right: 0.5rem; - padding-left: 0.5rem; - padding-top: 0.3rem; - padding-bottom: 0.3rem; - - a { - width: 100%; - padding-top: 0.3rem !important; - padding-bottom: 0.3rem !important; - margin-top: 0.5rem !important; - margin-bottom: 0.5rem !important; - - @include inversebuttoncolors(); - } - } -} diff --git a/assets/css/twenty-twenty.scss b/assets/css/twenty-twenty.scss deleted file mode 100644 index ad8cdab03be..00000000000 --- a/assets/css/twenty-twenty.scss +++ /dev/null @@ -1,2578 +0,0 @@ -@import "mixins"; - -/** - * Sass variables - */ - -$headings: -apple-system, blinkmacsystemfont, "Helvetica Neue", helvetica, sans-serif; -$body: nonbreakingspaceoverride, "Hoefler Text", garamond, "Times New Roman", serif; - -$body-color: #111; -$highlights-color: #cd2653; - -/** - * Fonts - */ -@font-face { - font-family: star; - src: url(../fonts/star.eot); - src: - url(../fonts/star.eot?#iefix) format("embedded-opentype"), - url(../fonts/star.woff) format("woff"), - url(../fonts/star.ttf) format("truetype"), - url(../fonts/star.svg#star) format("svg"); - font-weight: 400; - font-style: normal; -} - -@font-face { - font-family: WooCommerce; - src: url(../fonts/WooCommerce.eot); - src: - url(../fonts/WooCommerce.eot?#iefix) format("embedded-opentype"), - url(../fonts/WooCommerce.woff) format("woff"), - url(../fonts/WooCommerce.ttf) format("truetype"), - url(../fonts/WooCommerce.svg#WooCommerce) format("svg"); - font-weight: 400; - font-style: normal; -} - -/** - * Global elements - */ -a.button { - display: inline-block; - text-align: center; - box-sizing: border-box; - word-break: break-word; - color: #fff; - text-decoration: none !important; - - &:hover, - &:visited { - color: #fff; - text-decoration: underline !important; - } -} - -.woocommerce { - - form .form-row { - - .required { - color: #b22222; - text-decoration: none; - visibility: hidden; // Only show optional by default. - - &[title] { - border: 0 !important; - } - } - - .optional { - visibility: visible; - } - } - - form.woocommerce-form-login, - form.woocommerce-form-register { - - p, - label { - font-family: $headings; - } - - input { - border: 1px solid #ddd; - } - } - - .woocommerce-form-login__rememberme { - margin: 1rem 0 3rem 0; - } -} - -.woocommerce-view-order { - - .woocommerce-MyAccount-content { - - table { - - border: 0; - - tbody { - border-bottom: 1px solid #ddd; - } - - tfoot { - - tr:last-of-type { - border-top: 1px solid #ddd; - - .woocommerce-Price-amount { - font-weight: 700; - } - } - } - - td, - tr, - th { - border: 0; - } - } - } -} - -.woocommerce-breadcrumb { - margin-bottom: 5rem; - font-size: 0.88889em; - font-family: $headings; -} - -.woocommerce-pagination { - font-family: $headings; - font-size: 0.88889em; - - ul.page-numbers { - margin: 0; - padding: 0; - display: block; - font-weight: 700; - letter-spacing: -0.02em; - line-height: 1.2; - } - - span.page-numbers, - a.page-numbers, - .next.page-numbers, - .prev.page-numbers { - padding: 0 calc(0.5 * 1rem); - display: inline-block; - } -} - -.onsale { - position: absolute; - top: 0; - left: 0; - display: inline-block; - background: $highlights-color; - color: #fff; - font-family: $headings; - font-size: 1.7rem; - font-weight: 700; - letter-spacing: -0.02em; - line-height: 1.2; - padding: 1.5rem; - text-transform: uppercase; - z-index: 1; -} - -.price { - font-family: $headings; - - del { - opacity: 0.5; - display: inline-block; - } - - ins { - display: inline-block; - text-decoration: none; - } -} - -.woocommerce-message, -.woocommerce-error, -.woocommerce-info { - margin-bottom: 5rem; - margin-left: 0; - background: #eee; - font-size: 0.88889em; - font-family: $headings; - list-style: none; - overflow: hidden; -} - -.woocommerce-message, -.woocommerce-error li, -.woocommerce-info { - padding: 1.5rem 3rem; - display: flex; - justify-content: space-between; - align-items: center; - - .button { - order: 2; - } -} - -.woocommerce-message { - background: #eee; - color: $body-color; -} - -.woocommerce-error { - color: #fff; - background: #b22222; - - a { - color: #fff; - - &:hover { - color: #fff; - } - - &.button { - background: #111; - } - } - - > li { - margin: 0; - } -} - -#site-content { - - .woocommerce-error, - .woocommerce-info { - font-family: $headings; - } -} - -.woocommerce-info { - background: #eee; - color: #000; - border-top: 2px solid $highlights-color; - - a { - color: #444; - - &:hover { - color: #000; - } - - &.button { - background: $highlights-color; - color: #f5efe0; - } - } -} - -.woocommerce-store-notice { - background: #eee; - color: #000; - border-top: 2px solid $highlights-color; - padding: 2rem; - position: absolute; - top: 0; - left: 0; - width: 100%; - z-index: 999; -} - -.admin-bar .woocommerce-store-notice { - top: 32px; -} - -.woocommerce-store-notice__dismiss-link { - float: right; - color: #000; - - &:hover { - text-decoration: none; - color: #000; - } -} - -.flex-viewport { - margin-bottom: 1.5em; -} - -#site-content { - - .post-inner { - padding-top: 0; - } - - .wp-block-cover { - margin-top: 0; - } -} - -.cross-sells { - - .woocommerce-loop-product__title { - font-family: $headings; - } - - .star-rating { - font-size: 1.4rem; - } -} - -/* Make thumbnails in the gallery affect parent's height and wrapping */ -.flex-control-nav::after { - clear: both; - content: ""; - display: table; -} - -/** -* Tables -*/ -.woocommerce, -.woocommerce-page { - - table.shop_table { - - td, - th { - word-break: normal; - } - } -} - -/** - * Shop page - */ -.woocommerce-products-header__title.page-title { - font-size: 6rem; - text-align: center; -} - -.woocommerce-result-count, -.woocommerce-ordering { - margin: 0 0 1rem; - padding: 0.75rem 0; -} - -/** - * Products - */ -ul.products { - margin: 0; - padding: 0; - - li.product { - list-style: none; - - .woocommerce-loop-product__link { - display: block; - text-decoration: none; - } - - .woocommerce-loop-product__title { - margin: 1.5rem 0 0.5rem; - font-size: 2.5rem; - - &::before { - content: none; - } - } - - .woocommerce-loop-product__title, - .price, - .star-rating { - color: $body-color; - } - - .star-rating { - margin-bottom: 0.8rem; - } - - .price { - margin-bottom: 2rem; - } - - .price, - .star-rating { - display: block; - } - - .woocommerce-placeholder { - border: 1px solid #f2f2f2; - } - - .button { - vertical-align: middle; - - &.loading { - opacity: 0.5; - } - } - - .added_to_cart { - margin: 0.5rem; - } - } -} - -.star-rating { - overflow: hidden; - position: relative; - height: 1em; - line-height: 1; - font-size: 1em; - width: 5.4em; - font-family: star; - margin-bottom: 0.7rem; - - &::before { - content: "\73\73\73\73\73"; - float: left; - top: 0; - left: 0; - position: absolute; - } - - span { - overflow: hidden; - float: left; - top: 0; - left: 0; - position: absolute; - padding-top: 1.5em; - } - - span::before { - content: "\53\53\53\53\53"; - top: 0; - position: absolute; - left: 0; - } -} - -a.remove { - display: inline-block; - width: 20px; - height: 20px; - line-height: 18px; - font-size: 20px; - font-weight: 700; - text-align: center; - border-radius: 100%; - text-decoration: none !important; - background: #fff; - color: #000; - - &:hover { - background: $highlights-color; - color: #fff !important; - } -} - -dl.variation, -.wc-item-meta { - list-style: none outside; - - dt, - .wc-item-meta-label { - float: left; - clear: both; - margin-right: 0.25rem; - margin-top: 0; - list-style: none outside; - font-weight: 400; - } - - dd { - margin: 0; - } - - p, - &:last-child { - margin-bottom: 0; - } -} - -/** - * Single product - */ -.single-product { - - div.product { - position: relative; - - .product_meta { - clear: both; - font-size: 0.7em; - padding-top: 0.5em; - margin-top: 3rem; - } - } - - .single_add_to_cart_button { - padding-top: 1.55rem; - padding-bottom: 1.59rem; - font-size: 1.6rem; - } - - .single-featured-image-header { - display: none; - } - - .entry-title { - margin: 0 0 2.5rem; - - &::before { - margin-top: 0; - } - } - - .summary { - margin-bottom: 8rem; - - p.price { - margin-bottom: 3.5rem; - } - } - - .woocommerce-product-rating { - margin: -1rem 0 4rem; - line-height: 1; - font-size: 1.4rem; - - .star-rating { - float: left; - margin-right: 0.25rem; - } - } - - form.cart { - - .quantity { - float: left; - margin-right: 0.5rem; - } - - input { - width: 5em; - } - } - - .woocommerce-variation-add-to-cart { - - .button { - padding-top: 1.55rem; - padding-bottom: 1.59rem; - font-size: 1.6rem; - } - - .button.disabled { - opacity: 0.2; - } - } - - .woocommerce-message { - flex-direction: row-reverse; - } - - .woocommerce-Tabs-panel--additional_information, - .woocommerce-Tabs-panel--reviews { - - table { - border: 1px solid #ddd; - - tr, - td, - th { - border: 1px solid #ddd; - } - } - - p { - font-family: $headings; - } - - input { - border: 1px solid #ddd; - } - } - - .woocommerce-product-attributes-item__value { - - p { - margin-bottom: 0; - } - } -} - -table.variations { - - label { - margin: 0; - padding: 6px 0; - } - - select { - margin-right: 0.5rem; - } -} - -a.reset_variations { - margin-left: 0.5em; -} - -.woocommerce-product-gallery { - max-width: 600px; - position: relative; - margin-bottom: 2rem; - - figure { - margin: 0; - padding: 0; - } - - .woocommerce-product-gallery__wrapper { - margin: 0; - padding: 0; - } - - .zoomImg { - background-color: #fff; - opacity: 0; - } - - .woocommerce-product-gallery__image--placeholder { - border: 1px solid #f2f2f2; - } - - .woocommerce-product-gallery__image:nth-child(n+2) { - width: 25%; - display: inline-block; - } - - .flex-control-thumbs { - - li { - list-style: none; - cursor: pointer; - float: left; - } - - img { - opacity: 0.5; - - &:hover, - &.flex-active { - opacity: 1; - } - } - } - - img { - display: block; - height: auto; - } -} - -.woocommerce-product-gallery--columns-3 { - - .flex-control-thumbs li { - width: 33.3333%; - } - - .flex-control-thumbs li:nth-child(3n+1) { - clear: left; - } -} - -.woocommerce-product-gallery--columns-4 { - - ol { - margin-left: 0; - margin-bottom: 0; - } - - .flex-control-thumbs li { - width: 14.2857142857%; - margin: 0 14.2857142857% 1.6em 0; - } - - .flex-control-thumbs li:nth-child(4n) { - margin-right: 0; - } - - .flex-control-thumbs li:nth-child(4n+1) { - clear: left; - } -} - -.woocommerce-product-gallery--columns-5 { - - .flex-control-thumbs li { - width: 20%; - } - - .flex-control-thumbs li:nth-child(5n+1) { - clear: left; - } -} - -.woocommerce-product-gallery__trigger { - position: absolute; - top: 1rem; - right: 1rem; - z-index: 99; -} - -.woocommerce-tabs { - margin: 4rem 0 2rem; - - /* reset description tab width to full width */ - #tab-description { - - h2, - p { - max-width: 100vw; - width: 100%; - } - } - - /* reset additional info tab width to full width */ - #tab-additional_information { - - .woocommerce-product-attributes { - max-width: 100vw; - width: 100%; - } - } - - #tab-reviews { - - /* reset reviews tab width to full width */ - .woocommerce-Reviews { - max-width: 100vw; - width: 100%; - } - - #submit { - float: right; - } - } - - - ul { - margin: 0 0 1.5rem; - padding: 0; - font-family: $headings; - - li { - margin: 0.5rem 4rem 2rem 0; - - a { - color: $body-color; - text-decoration: none; - font-weight: 700; - } - - &.active { - - a { - color: $highlights-color; - box-shadow: 0 2px 0 $highlights-color; - } - } - } - } - - .panel { - - > * { - margin-top: 0 !important; - } - - h1, - h2 { - - &::before { - content: none; - } - } - - h2:first-of-type { - font-size: 3rem; - margin: 0 0 2rem; - } - } - - #comments { - padding-top: 0; - } - - .comment-reply-title { - font-family: $headings; - font-size: 1em; - font-weight: 700; - display: block; - } - - #reviews { - - ol.commentlist { - padding: 0; - margin: 0; - } - - li.review, - li.comment { - list-style: none; - margin: 0.5rem 0 2.5rem 0; - - .avatar { - max-height: 36px; - width: auto; - float: right; - } - - p.meta { - margin-bottom: 0.5em; - } - } - - .comment-form-rating { - - label { - max-width: 58rem; - margin: 0 auto; - } - } - - p.stars { - margin-top: 0; - - a { - position: relative; - height: 1em; - width: 1em; - text-indent: -999em; - display: inline-block; - text-decoration: none; - box-shadow: none; - - &::before { - display: block; - position: absolute; - top: 0; - left: 0; - width: 1em; - height: 1em; - line-height: 1; - font-family: WooCommerce; - content: "\e021"; - text-indent: 0; - } - - &:hover { - - ~ a::before { - content: "\e021"; - } - } - } - - &:hover { - - a { - - &::before { - content: "\e020"; - } - } - } - - &.selected { - - a.active { - - &::before { - content: "\e020"; - } - - ~ a::before { - content: "\e021"; - } - } - - a:not(.active) { - - &::before { - content: "\e020"; - } - } - } - } - - .comment-form-author, - .comment-form-email { - float: none; - margin-left: auto; - } - } -} - -/** - * Related products - */ - -.related.products, -.up-sells { - - clear: both; - - ul.products { - display: flex; - justify-content: space-evenly; - align-items: stretch; - - li.product { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: flex-start; - } - } -} - -/** - * Widgets - */ -.widget.woocommerce { - - ul { - padding-left: 0; - - li { - list-style: none; - } - } -} - -.widget .product_list_widget, -.site-footer .widget .product_list_widget { - margin-bottom: 1.5rem; - - a { - display: block; - box-shadow: none; - - &:hover { - box-shadow: none; - } - } - - li { - padding: 0.5rem 0; - - a.remove { - float: left; - margin-top: 7px; - line-height: 20px; - color: #fff; - margin-right: 0.5rem; - } - } - - img { - display: none; - } -} - -.widget_shopping_cart { - - .buttons { - - a { - display: inline-block; - margin: 0 0.5rem 0 0; - } - } -} - -.woocommerce-shopping-totals { - vertical-align: text-top; -} - -.widget_layered_nav { - - .chosen { - - &::before { - content: "×"; - display: inline-block; - width: 16px; - height: 16px; - line-height: 16px; - font-size: 16px; - text-align: center; - border-radius: 100%; - border: 1px solid #000; - margin-right: 0.25rem; - } - } -} - -.widget_price_filter { - - .price_slider { - margin-bottom: 1rem; - } - - .price_slider_amount { - text-align: right; - line-height: 2.4; - font-size: 0.8751em; - - .button { - float: left; - padding: 0.4rem 1rem; - } - } - - .ui-slider { - position: relative; - text-align: left; - margin-left: 0.5rem; - margin-right: 0.5rem; - } - - .ui-slider .ui-slider-handle { - position: absolute; - z-index: 2; - width: 1em; - height: 1em; - background-color: #000; - border-radius: 1em; - cursor: ew-resize; - outline: none; - top: -0.3em; - margin-left: -0.5em; - } - - .ui-slider .ui-slider-range { - position: absolute; - z-index: 1; - font-size: 0.7em; - display: block; - border: 0; - border-radius: 1em; - background-color: #000; - } - - .price_slider_wrapper .ui-widget-content { - border-radius: 1em; - background-color: #666; - border: 0; - } - - .ui-slider-horizontal { - height: 0.5em; - } - - .ui-slider-horizontal .ui-slider-range { - top: 0; - height: 100%; - } - - .ui-slider-horizontal .ui-slider-range-min { - left: -1px; - } - - .ui-slider-horizontal .ui-slider-range-max { - right: -1px; - } -} - -.widget_rating_filter { - - li { - text-align: right; - - .star-rating { - float: left; - margin-top: 0.3rem; - } - } -} - -.widget_product_search { - - form { - position: relative; - } - - .search-field { - padding-right: 100px; - } - - input[type="submit"] { - position: absolute; - top: 0.5rem; - right: 0.5rem; - padding-left: 1rem; - padding-right: 1rem; - } -} - -/** - * Account section - */ -.woocommerce-account { - - #site-content { - - .post-inner { - padding-top: 0; - } - - .woocommerce { - max-width: 1600px; - padding: 0 6vw; - margin: 0 auto; - } - } - - .woocommerce-MyAccount-navigation { - font-family: $headings; - margin: 0 0 2rem; - - ul { - margin: 0; - padding: 0; - } - - li { - list-style: none; - padding: 0.5rem 0; - font-family: $headings; - font-size: 2rem; - - &:first-child { - padding-top: 0; - } - - a { - box-shadow: none; - text-decoration: none; - font-weight: 600; - color: #aaa; - - &:hover { - color: #000; - text-decoration: underline; - } - } - - &.is-active { - - a { - text-decoration: underline; - color: $highlights-color; - } - } - } - } - - .woocommerce-MyAccount-content { - - p { - font-family: $headings; - font-size: 2rem; - } - - form { - - h3 { - margin-top: 0; - } - } - } - - table.account-orders-table { - margin-top: 0; - border: 0; - - tr, - td, - th { - border: 0; - } - - td { - padding-left: 1.5rem; - } - - thead { - border-bottom: 1px solid #ddd; - } - - .button { - margin: 0 0.35rem 0.35rem 0; - width: 80%; - } - } - - table.account-orders-table:not(.has-background) { - - tbody { - - tr:nth-child(2n) { - - td { - background: #eee; - } - } - - tr:nth-child(2n+1) { - - td { - background: #fff; - } - } - } - } - - .woocommerce-EditAccountForm { - - input { - border: 1px solid #ddd; - } - - fieldset { - border: 0.2rem solid #ddd; - } - - button { - margin-top: 3rem; - } - } -} - -.logged-in.woocommerce-account { - - #site-content { - - .woocommerce { - display: flex; - flex-direction: row; - } - } -} - -/** - * Cart - */ -.woocommerce-cart-form { - - img { - max-width: 120px; - height: auto; - display: block; - } - - dl.variation { - margin-top: 1rem; - - dt, - dd, - p { - font-family: $headings; - font-size: 1.4rem; - } - - p, - &:last-child { - margin-bottom: 0; - } - } - - .product-remove { - text-align: center; - } - - .actions { - - .input-text { - width: 200px !important; - float: left; - margin-right: 0.25rem; - border: 1px solid #ddd; - padding-top: 1.55rem; - padding-bottom: 1.59rem; - } - - .button { - background: #f9f9f9; - border: 1px solid #555; - color: #555; - } - - button[name="update_cart"] { - background: #fff; - color: #000; - } - } - - .quantity { - - input { - width: 8rem; - border: 1px solid #eee; - } - } - - table { - border: 0; - - th, - tbody, - td { - border: 0; - } - - td.product-thumbnail { - padding: 1.4rem; - width: 10%; - } - - td.product-name { - padding-left: 1.5vw; - } - - tbody { - - tr { - border-top: 1px solid #eee; - } - } - - input.qty { - display: inline-block; - } - } - - .actions { - - button { - padding-top: 1.55rem; - padding-bottom: 1.59rem; - font-size: 1.6rem; - } - } -} - -.cart_totals { - - th, - td { - vertical-align: top; - } - - th { - padding-right: 1rem; - } - - .woocommerce-shipping-destination { - margin-bottom: 1.5rem; - font-family: $headings; - } - - table { - border: 0; - - tbody, - th, - tr, - td { - border: 0; - padding: 1rem; - } - - th { - width: 33%; - } - } - - .checkout-button { - width: 100%; - } - - input[type="radio"].shipping_method { - display: none; - - & + label { - - &::before { - content: ""; - display: inline-block; - width: 14px; - height: 14px; - border: 2px solid #fff; - box-shadow: 0 0 0 2px #6d6d6d; - background: #fff; - margin-left: 4px; - margin-right: 1.2rem; - border-radius: 100%; - transform: translateY(2px); - } - } - - &:checked + label { - - &::before { - background: #555; - } - } - } -} - -.shipping-calculator-button { - margin-top: 0.5rem; - display: inline-block; -} - -.shipping-calculator-form { - margin: 1rem 0 0 0; -} - -#shipping_method { - list-style: none; - margin: 0; - padding: 0 0 1.5rem; - font-family: $headings; - - li { - margin-bottom: 0.5rem; - margin-left: 0; - - input { - float: left; - margin-top: 0.5rem; - margin-right: 0.6rem; - } - - label { - line-height: 2.5rem; - } - } -} - -.checkout-button { - display: block; - padding: 1rem 2rem; - border: 2px solid #000; - text-align: center; - font-weight: 800; - - &:hover { - border-color: #999; - } - - &::after { - content: "→"; - margin-left: 0.5rem; - } -} - -.woocommerce-cart { - - .post-inner { - padding-top: 0; - } - - #site-content { - - .entry-header { - padding: 3vw 0 1.5vw; - } - - .woocommerce { - max-width: 1600px; - padding: 0 5vw; - margin: 0 auto; - - } - } - - .select2-container .select2-selection--single { - height: 48px; - } - - .select2-container .select2-selection--single .select2-selection__rendered { - line-height: 48px; - font-family: $headings; - font-size: 1.6rem; - color: #000; - padding-left: 1.8rem; - } - - .select2-container--default .select2-selection--single .select2-selection__arrow { - height: 46px; - } - - .select2-container--focus .select2-selection { - border-color: #000; - } - - .select2-results__option { - margin-left: 0; - } - - .select2-container { - - .select2-search__field { - height: 4rem; - background: #eee; - } - } - - p.form-row { - - input { - border: 1px solid #ddd; - } - } -} - -/** - * Checkout - */ -#ship-to-different-address { - font-size: 1em; - display: inline-block; - margin: 1.42em 0; - - label { - font-weight: 400; - cursor: pointer; - - span { - position: relative; - display: block; - text-align: right; - padding-right: 45px; - - &::before { - content: ""; - display: block; - height: 16px; - width: 30px; - border: 2px solid #bbb; - background: #bbb; - border-radius: 13rem; - box-sizing: content-box; - transition: all ease-in-out 0.3s; - position: absolute; - top: 0; - right: 0; - } - - &::after { - content: ""; - display: block; - width: 14px; - height: 14px; - background: #fff; - position: absolute; - top: 3px; - right: 17px; - border-radius: 13rem; - transition: all ease-in-out 0.3s; - } - } - - input[type="checkbox"] { - display: none; - } - - input[type="checkbox"]:checked + span::after { - right: 3px; - } - - input[type="checkbox"]:checked + span::before { - border-color: #000; - background: #000; - } - } -} - -.woocommerce-no-js { - - form.woocommerce-form-login, - form.woocommerce-form-coupon { - display: block !important; - } - - .woocommerce-form-login-toggle, - .woocommerce-form-coupon-toggle, - .showcoupon { - display: none !important; - } -} - -.woocommerce-terms-and-conditions { - border: 1px solid rgba(0, 0, 0, 0.2); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); - background: rgba(0, 0, 0, 0.05); -} - -.woocommerce-terms-and-conditions-link { - display: inline-block; - - &::after { - content: ""; - display: inline-block; - border-style: solid; - margin-bottom: 2px; - margin-left: 0.25rem; - border-width: 6px 6px 0 6px; - border-color: $body-color transparent transparent transparent; - } - - &.woocommerce-terms-and-conditions-link--open::after { - border-width: 0 6px 6px 6px; - border-color: transparent transparent $body-color transparent; - } -} - -.woocommerce-checkout { - - ul.woocommerce-error { - flex-direction: column; - align-items: flex-start; - - li { - font-family: $headings; - margin: 0.5rem 0 0.5rem; - } - } - - .post-inner { - padding-top: 0; - } - - .woocommerce-billing-fields { - - h3 { - margin-top: 4rem; - } - } - - form[name="checkout"] { - display: table; - } - - .blockUI.blockOverlay { - position: relative; - - @include loader(); - } - - form { - - .col2-set { - width: 50%; - float: left; - padding-right: 1.5vw; - - .col-1, - .col-2 { - float: none; - width: 100%; - } - - input { - border: 1px solid #ddd; - } - - label { - font-family: $headings; - letter-spacing: normal; - } - - p { - margin-bottom: 1.15em; - } - } - - #order_review_heading { - margin-top: 4rem; - } - - #order_review_heading, - #order_review { - width: 50%; - padding-left: 1.5vw; - float: right; - clear: right; - - .woocommerce-checkout-review-order-table { - margin-top: 2.85rem; - border: 0; - - th, - td { - border: 0; - } - - thead { - display: none; - } - - tbody::after { - content: ""; - display: block; - height: 2rem; - } - - .woocommerce-Price-amount { - font-weight: bold; - } - - .cart-subtotal, - .order-total { - border-top: 1px solid #ddd; - } - } - } - - .form-row.woocommerce-invalid { - - input.input-text { - border: 2px solid $highlights-color; - } - } - - } - - .woocommerce-input-wrapper { - - .description { - background: #4169e1; - color: #fff; - border-radius: 3px; - padding: 1rem; - margin: 0.5rem 0 0; - clear: both; - display: none; - position: relative; - - a { - color: #fff; - text-decoration: underline; - border: 0; - box-shadow: none; - } - - &::before { - left: 50%; - top: 0; - margin-top: -4px; - transform: translateX(-50%) rotate(180deg); - content: ""; - position: absolute; - border-width: 4px 6px 0 6px; - border-style: solid; - border-color: #4169e1 transparent transparent transparent; - z-index: 100; - display: block; - } - } - } - - .woocommerce-form-login { - - p.form-row.form-row-first, - p.form-row.form-row-last { - float: none; - } - } - - input#coupon_code { - padding-top: 1.55rem; - padding-bottom: 1.59rem; - border: 1px solid #ddd; - } - - button[name="apply_coupon"] { - padding-top: 1.55rem; - padding-bottom: 1.8rem; - font-size: 1.6rem; - } - - .select2-choice, - .select2-choice:hover { - box-shadow: none !important; - } - - .select2-choice { - padding: 0.7rem 0 0.7rem 0.7rem; - } - - .select2-container .select2-selection--single { - height: 48px; - } - - .select2-container .select2-selection--single .select2-selection__rendered { - line-height: 48px; - font-family: $headings; - font-size: 1.6rem; - color: #000; - padding-left: 1.8rem; - } - - .select2-container--default .select2-selection--single .select2-selection__arrow { - height: 46px; - } - - .select2-container--focus .select2-selection { - border-color: #000; - } - - .select2-results__option { - margin-left: 0; - } - - .select2-container { - - .select2-search__field { - height: 4rem; - background: #eee; - } - } -} - -.woocommerce-checkout-review-order-table { - - input[type="radio"].shipping_method { - display: none; - - & + label { - - &::before { - content: ""; - display: inline-block; - width: 14px; - height: 14px; - border: 2px solid #fff; - box-shadow: 0 0 0 2px #6d6d6d; - background: #fff; - margin-left: 4px; - margin-right: 1.2rem; - border-radius: 100%; - transform: translateY(2px); - } - } - - &:checked + label { - - &::before { - background: #555; - } - } - } - - td { - padding: 1rem 0.5em; - } - - dl.variation { - margin: 0; - - p { - margin: 0; - } - - dt, - dd { - font-family: $headings; - - p { - padding-top: 1px; - font-family: $headings; - } - } - } -} - -.woocommerce-order-received { - - .woocommerce-order { - - p, - li { - font-family: $headings; - } - } - - table { - border: 0; - - td, - th, - tr { - border: 0; - } - - tr { - height: 5rem; - } - - tfoot { - border-top: 1px solid #ddd; - - /* Targeting total */ - tr:last-of-type { - border-top: 1px solid #ddd; - - .woocommerce-Price-amount { - font-weight: 700; - } - } - } - - } -} - -.woocommerce-checkout-review-order { - - ul { - margin: 2rem 0 3rem; - padding-left: 0; - } - - #place_order { - width: 100%; - } -} - -.wc_payment_method { - list-style: none; - - .payment_box { - padding: 1rem; - background: #eee; - - ul, - ol { - - &:last-of-type { - margin-bottom: 0; - } - } - - fieldset { - padding: 1.5rem; - padding-bottom: 0; - border: 0; - background: #f6f6f6; - } - - li { - list-style: none; - } - - p { - - &:first-child { - margin-top: 0; - } - - &:last-child { - margin-bottom: 0; - } - } - } - - > label:first-of-type { - display: block; - margin: 1rem 0; - - img { - max-height: 24px; - max-width: 200px; - float: right; - } - } - - label { - cursor: pointer; - } - - input.input-radio[name="payment_method"] { - display: none; - - & + label { - font-family: $headings; - - &::before { - content: ""; - display: inline-block; - width: 14px; - height: 14px; - border: 2px solid #fff; - box-shadow: 0 0 0 2px #6d6d6d; - background: #fff; - margin-left: 4px; - margin-right: 1.2rem; - border-radius: 100%; - transform: translateY(2px); - } - } - - &:checked + label { - - &::before { - background: #555; - } - } - } -} - -.wc_payment_methods { - - .payment_box { - - p { - font-family: $headings; - font-size: 1.6rem; - } - } -} - - -.woocommerce-terms-and-conditions-wrapper { - margin-bottom: 5rem; - - .woocommerce-privacy-policy-text { - - p { - font-family: $headings; - font-size: 1.6rem; - } - } -} - -.woocommerce-order-overview { - margin-bottom: 2rem; -} - -.woocommerce-table--order-details { - margin-bottom: 2rem; -} - -/** - * Layout stuff - */ -.woocommerce { - - section { - padding-top: 2rem; - padding-bottom: 0; - } - - .content-area { - - .site-main { - margin: 0 5vw; - } - } - - /* Shop layout */ - ul.products { - display: flex; - align-items: stretch; - flex-direction: row; - flex-wrap: wrap; - - li.product { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 5em; - - } - - li.product-category { - - a { - text-align: center; - - h2.woocommerce-loop-category__title { - font-family: $headings; - font-size: 3rem; - } - } - } - } -} - -@media only screen and (max-width: 600px) { - - .woocommerce { - - .woocommerce-ordering { - float: left; - clear: both; - } - } -} - -@media only screen and (max-width: 667px) { - - .woocommerce, - .woocommerce-page { - - ul.products[class*=columns-] { - - li.product { - width: 100%; - } - } - } -} - -@media only screen and (min-width: 668px) and (max-width: 768px) { - - .woocommerce, - .woocommerce-page { - - ul.products[class*=columns-] { - - li.product { - width: 50%; - } - - li.product:nth-of-type(2n+1) { - padding: 0 2vw 3em 0; - } - - li.product:nth-of-type(2n) { - padding: 0 0 3em 2vw; - } - } - } -} - -@media only screen and (max-width: 768px) { - - #site-content { - - .woocommerce { - - .woocommerce-cart-form { - - .actions { - - .coupon { - margin-bottom: 2rem; - - button { - width: 100%; - } - } - } - - #coupon_code { - width: 100% !important; - } - } - } - - #shipping_method { - - li { - display: flex; - justify-content: flex-end; - } - } - } - - .woocommerce, - .woocommerce-page { - - table.shop_table_responsive { - - tr { - margin: 0 0 1.5rem; - - &:first-child { - border-top: 1px solid; - } - - &:last-child { - margin-bottom: 0; - } - - &:nth-child(2n) { - - td { - background: #fff; - } - } - - td { - border-bottom-width: 0; - - &:last-child { - border-bottom-width: 1px; - } - } - - td.product-quantity::before { - padding-top: 0.9rem; - } - - .product-remove { - float: right; - } - - .product-thumbnail { - display: block; - - img { - width: 70px; - } - - &::before { - content: ""; - } - } - } - - } - - .woocommerce-breadcrumb { - margin-bottom: 4rem; - font-size: 0.8em; - font-family: $headings; - } - - .related.products { - - ul.products { - display: flex; - flex-direction: column; - align-items: flex-start; - - li.product { - margin-bottom: 5em; - } - } - } - - .woocommerce-products-header__title.page-title { - margin: 3rem auto 4rem; - } - - .woocommerce-result-count, - .woocommerce-ordering { - font-size: 0.8em; - } - - .woocommerce-ordering { - margin-bottom: 3rem; - } - } - - .woocommerce-cart-form { - - table { - - td.product-name { - padding-left: 0.5em; - } - - input.qty { - padding: 1rem 1.5rem; - } - } - } - - .woocommerce-checkout { - - form { - - .col2-set { - width: 100%; - float: none; - padding-right: 0; - - .col-1, - .col-2 { - float: none; - width: 100%; - } - } - - #order_review_heading { - margin-top: 4rem; - } - - #order_review_heading, - #order_review { - width: 100%; - padding-left: 0; - float: none; - } - - table { - - tbody { - - td.product-total { - text-align: end; - } - } - - tfoot { - - .cart-subtotal, - .order-total { - - td { - text-align: end; - } - } - } - } - } - } - - .logged-in.woocommerce-account { - - #site-content { - - .woocommerce { - flex-direction: column; - } - - .woocommerce-MyAccount-navigation, - .woocommerce-MyAccount-content { - width: 100%; - } - - table.account-orders-table { - - .button { - padding-left: 0.5em; - padding-right: 0.5em; - width: 100%; - margin: 2rem 0; - } - } - } - - table.account-orders-table { - - td { - padding-bottom: 1.5rem; - } - } - } -} - -@media only screen and (min-width: 768px) { - - /** - * Tables - */ - .woocommerce, - .woocommerce-page { - - table.shop_table { - - tbody { - - tr { - font-size: 0.88889em; - } - } - } - - .onsale { - font-size: 1.5rem; - padding: 1rem; - } - } - - /** - * Shop page - */ - .woocommerce-products-header__title.page-title { - font-size: 8.4rem; - font-weight: 800; - } - - .woocommerce-pagination { - - span.page-numbers, - a.page-numbers, - .next.page-numbers, - .prev.page-numbers { - padding: 1rem; - } - } - - /** - * Account section - */ - .woocommerce-account { - - .woocommerce-MyAccount-navigation { - float: none; - width: 20%; - margin-bottom: 1.5rem; - margin-right: 3rem; - - li { - margin: 0 1rem 3rem 0; - padding: 0; - border-bottom: 0; - - &:last-child { - margin-right: 0; - } - } - } - - .woocommerce-MyAccount-content { - float: none; - width: 75%; - } - - table.account-orders-table { - margin-top: 0; - border: 0; - - tr, - td, - th { - border: 0; - padding: 0; - } - - th, - td, - td.woocommerce-orders-table__cell-order-actions { - padding-right: 0.5rem; - padding-left: 0.5rem; - } - - thead { - border-bottom: 1px solid #ddd; - } - - .button { - padding-left: 0.5em; - padding-right: 0.5em; - width: 100%; - margin: 1.5rem 0; - } - } - } - - /** - * Layout stuff - */ - .woocommerce { - - .content-area { - margin: 0 auto; - padding: 2vw 6vw; - - .site-main { - margin: 0; - } - } - } - - .single-product { - - .entry { - - .entry-content, - .entry-summary { - max-width: none; - margin: 0 0 3rem; - padding: 0; - - > * { - max-width: none; - } - } - } - } - - .woocommerce-breadcrumb { - margin-bottom: 5rem; - font-size: 0.88889em; - font-family: $headings; - } - - .woocommerce-product-gallery { - margin-bottom: 8rem; - } - - .woocommerce-checkout { - - #site-content { - - .woocommerce { - - max-width: 1600px; - padding: 0 6vw; - margin: 0 auto; - } - } - } - -} - -@media only screen and (min-width: 1168px) { - - .woocommerce { - - .content-area { - max-width: 1600px; - padding: 4vw 6vw; - margin: 0 auto; - - .site-main { - - } - } - - .onsale { - font-size: 1.7rem; - padding: 1.5rem; - } - } - - .woocommerce-breadcrumb { - margin-bottom: 5rem; - font-size: 0.88889em; - font-family: $headings; - } - - .woocommerce-product-gallery { - margin-bottom: 8rem; - } - - .woocommerce-account { - - table.account-orders-table { - - th, - td, - td.woocommerce-orders-table__cell-order-actions { - padding-right: 1.5rem; - padding-left: 1.5rem; - } - } - } -} diff --git a/assets/css/wc-setup.scss b/assets/css/wc-setup.scss deleted file mode 100644 index 3dd65ca614f..00000000000 --- a/assets/css/wc-setup.scss +++ /dev/null @@ -1,1628 +0,0 @@ -/* stylelint-disable no-descending-specificity */ - -/* @deprecated 4.6.0 */ -body { - margin: 65px auto 24px; - box-shadow: none; - background: #f1f1f1; - padding: 0; -} - -.wc-logo { - border: 0; - margin: 0 0 24px; - padding: 0; - text-align: center; - - img { - max-width: 30%; - } -} - -.wc-setup { - text-align: center; - - #wc_tracker_checkbox { - display: none; - } - - .select2-container { - text-align: left; - width: auto; - } - - .hidden { - display: none; - } - - #tiptip_content { - background: #5f6973; - } - - #tiptip_holder.tip_top #tiptip_arrow_inner { - border-top-color: #5f6973; - } -} - -.wc-setup-content { - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.13); - padding: 2em; - margin: 0 0 20px; - background: #fff; - overflow: hidden; - zoom: 1; - text-align: left; - - h1, - h2, - h3, - table { - margin: 0 0 20px; - border: 0; - padding: 0; - color: #666; - clear: none; - font-weight: 500; - } - - p { - margin: 20px 0; - font-size: 1em; - line-height: 1.75; - color: #666; - } - - table { - font-size: 1em; - line-height: 1.75; - color: #666; - } - - a { - color: #a16696; - - &:hover, - &:focus { - color: #111; - } - } - - .form-table { - - th { - width: 35%; - vertical-align: top; - font-weight: 400; - } - - td { - vertical-align: top; - - select, - input { - width: 100%; - box-sizing: border-box; - } - - input[size] { - width: auto; - } - - .description { - line-height: 1.5; - display: block; - margin-top: 0.25em; - color: #999; - font-style: italic; - } - - .input-checkbox, - .input-radio { - width: auto; - box-sizing: inherit; - padding: inherit; - margin: 0 0.5em 0 0; - box-shadow: none; - } - } - - .section_title { - - td { - padding: 0; - - h2, - p { - margin: 12px 0 0; - } - } - } - - th, - td { - padding: 12px 0; - margin: 0; - border: 0; - - &:first-child { - padding-right: 1em; - } - } - } - - table.tax-rates { - width: 100%; - font-size: 0.92em; - - th { - padding: 0; - text-align: center; - width: auto; - vertical-align: middle; - } - - td { - border: 1px solid #f5f5f5; - padding: 6px; - text-align: center; - vertical-align: middle; - - input { - outline: 0; - border: 0; - padding: 0; - box-shadow: none; - text-align: center; - width: 100%; - } - - &.sort { - cursor: move; - color: #ccc; - - &::before { - content: "\f333"; - font-family: dashicons; /* stylelint-disable-line font-family-no-missing-generic-family-keyword */ - } - } - - &.readonly { - background: #f5f5f5; - } - } - - .add { - padding: 1em 0 0 1em; - line-height: 1; - font-size: 1em; - width: 0; - margin: 6px 0 0; - height: 0; - overflow: hidden; - position: relative; - display: inline-block; - - &::before { - content: "\f502"; - font-family: dashicons; /* stylelint-disable-line font-family-no-missing-generic-family-keyword */ - position: absolute; - left: 0; - top: 0; - } - } - - .remove { - padding: 1em 0 0 1em; - line-height: 1; - font-size: 1em; - width: 0; - margin: 0; - height: 0; - overflow: hidden; - position: relative; - display: inline-block; - - &::before { - content: "\f182"; - font-family: dashicons; /* stylelint-disable-line font-family-no-missing-generic-family-keyword */ - position: absolute; - left: 0; - top: 0; - } - } - } - - .wc-setup-pages { - width: 100%; - border-top: 1px solid #eee; - - thead th { - display: none; - } - - .page-name { - width: 30%; - font-weight: 700; - } - - th, - td { - padding: 14px 0; - border-bottom: 1px solid #eee; - - &:first-child { - padding-right: 9px; - } - } - - th { - padding-top: 0; - } - - .page-options { - - p { - color: #777; - margin: 6px 0 0 24px; - line-height: 1.75; - - input { - vertical-align: middle; - margin: 1px 0 0; - height: 1.75em; - width: 1.75em; - line-height: 1.75; - } - - label { - line-height: 1; - } - } - } - } - - @media screen and (max-width: 782px) { - - .form-table { - - tbody { - - th { - width: auto; - } - } - } - } - - .twitter-share-button { - float: right; - } - - .wc-setup-next-steps { - overflow: hidden; - margin: 0 0 24px; - padding-bottom: 2px; - - h2 { - margin-bottom: 12px; - } - - .wc-setup-next-steps-first { - float: left; - width: 50%; - box-sizing: border-box; - } - - .wc-setup-next-steps-last { - float: right; - width: 50%; - box-sizing: border-box; - } - - ul { - padding: 0 2em 0 0; - list-style: none outside; - margin: 0; - - li a { - display: block; - padding: 0 0 0.75em; - } - - .setup-product { - - a.button { - background-color: #f7f7f7; - border-color: #ccc; - color: #23282d; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #ccc; - text-shadow: 1px 0 1px #eee, 0 1px 1px #eee; - font-size: 1em; - height: auto; - line-height: 1.75; - margin: 0 0 0.75em; - opacity: 1; - padding: 1em; - text-align: center; - - &:hover, - &:focus, - &:active { - background: #f5f5f5; - border-color: #aaa; - } - } - - a.button-primary { - color: #fff; - background-color: #bb77ae; - border-color: #a36597; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; - text-shadow: 0 -1px 1px #a36597, 1px 0 1px #a36597, 0 1px 1px #a36597, -1px 0 1px #a36597; - - &:hover, - &:focus, - &:active { - color: #fff; - background: #a36597; - border-color: #a36597; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; - } - } - } - - li a::before { - color: #82878c; - font: 400 20px/1 dashicons; /* stylelint-disable-line font-family-no-missing-generic-family-keyword */ - speak: never; - display: inline-block; - padding: 0 10px 0 0; - top: 1px; - position: relative; - text-decoration: none !important; - vertical-align: top; - } - - .learn-more a::before { - content: "\f105"; - } - - .video-walkthrough a::before { - content: "\f126"; - } - - .newsletter a::before { - content: "\f465"; - } - } - } - - .woocommerce-newsletter, - .updated { - padding: 24px 24px 0; - margin: 0 0 24px; - overflow: hidden; - background: #f5f5f5; - - p { - padding: 0; - margin: 0 0 12px; - } - - form, - p:last-child { - margin: 0 0 24px; - } - } - - .checkbox { - - input[type="checkbox"] { - opacity: 0; - position: absolute; - left: -9999px; - } - - label { - position: relative; - display: inline-block; - padding-left: 28px; - - &::before, - &::after { - position: absolute; - content: ""; - display: inline-block; - } - - &::before { - height: 16px; - width: 16px; - left: 0; - top: 3px; - border: 1px solid #aaa; - background-color: #fff; - border-radius: 3px; - } - - &::after { - height: 5px; - width: 9px; - border-left: 2px solid; - border-bottom: 2px solid; - transform: rotate(-45deg); - left: 4px; - top: 7px; - color: #fff; - } - } - - input[type="checkbox"] + label::after { - content: none; - } - - input[type="checkbox"]:checked + label::after { - content: ""; - } - - input[type="checkbox"]:focus + label::before { - outline: rgb(59, 153, 252) auto 5px; - } - - input[type="checkbox"]:checked + label::before { - background: #935687; - border-color: #935687; - } - } -} - -.woocommerce-tracker { - margin: 24px 0; - border: 1px solid #eee; - padding: 20px; - border-radius: 4px; - overflow: hidden; - text-align: left; - - h1 { - border-bottom: 0; - padding-bottom: 0; - } - - .wc-backbone-modal-header { - border-bottom: 0; - } - - .wc-backbone-modal-main article { - padding-top: 0; - } - - .wc-backbone-modal-main footer { - border-top: 0; - box-shadow: none; - } - - p { - font-size: 14px; - line-height: 1.5; - } - - .woocommerce-tracker-checkbox label { - margin-top: -4px; - display: inline-block; - } -} - -.wc-setup-steps { - padding: 0 0 24px; - margin: 0; - list-style: none outside; - overflow: hidden; - color: #ccc; - width: 100%; - display: inline-flex; - - li { - width: 100%; - float: left; - padding: 0 0 0.8em; - margin: 0; - text-align: center; - position: relative; - border-bottom: 4px solid #ccc; - line-height: 1.4; - - a { - color: #a16696; - text-decoration: none; - padding: 1.5em; - margin: -1.5em; - position: relative; - z-index: 1; - - &:hover, - &:focus { - color: #111; - text-decoration: underline; - } - } - } - - li::before { - content: ""; - border: 4px solid #ccc; - border-radius: 100%; - width: 4px; - height: 4px; - position: absolute; - bottom: 0; - left: 50%; - margin-left: -6px; - margin-bottom: -8px; - background: #fff; - } - - li.active { - border-color: #a16696; - color: #a16696; - font-weight: 700; - - &::before { - border-color: #a16696; - } - } - - li.done { - border-color: #a16696; - color: #a16696; - - &::before { - border-color: #a16696; - background: #a16696; - } - } -} - -.wc-setup .wc-setup-actions { - overflow: hidden; - margin: 20px 0 0; - position: relative; -} - -.wc-setup .wc-setup-actions .button-primary, -.woocommerce-tracker .button-primary { - background-color: #bb77ae; - border-color: #a36597; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; - text-shadow: 0 -1px 1px #a36597, 1px 0 1px #a36597, 0 1px 1px #a36597, -1px 0 1px #a36597; - margin: 0; - opacity: 1; - - &:hover, - &:focus, - &:active { - background: #a36597; - border-color: #a36597; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; - } -} - -.wc-setup-content p:last-child { - margin-bottom: 0; -} - -.wc-setup-content p.store-setup { - margin-top: 0; -} - -.wc-setup-footer-links { - font-size: 0.85em; - color: #7b7b7b; - margin: 1.18em auto; - display: inline-block; - text-align: center; -} - -.wc-wizard-storefront { - - .wc-wizard-storefront-intro { - padding: 40px 40px 0; - background: #f5f5f5; - text-align: center; - - img { - margin: 40px 0 0 0; - width: 100%; - display: block; - } - } - - .wc-wizard-storefront-features { - list-style: none outside; - margin: 0 0 20px; - padding: 0 0 0 30px; - overflow: hidden; - } - - .wc-wizard-storefront-feature { - margin: 0; - padding: 20px 30px 20px 2em; - width: 50%; - box-sizing: border-box; - - &::before { - margin-left: -2em; - position: absolute; - } - - &.first { - clear: both; - float: left; - } - - &.last { - float: right; - } - } - - .wc-wizard-storefront-feature__bulletproof::before { - content: "🔒"; - } - - .wc-wizard-storefront-feature__mobile::before { - content: "📱"; - } - - .wc-wizard-storefront-feature__accessibility::before { - content: "👓"; - } - - .wc-wizard-storefront-feature__search::before { - content: "🔍"; - } - - .wc-wizard-storefront-feature__compatibility::before { - content: "🔧"; - } - - .wc-wizard-storefront-feature__extendable::before { - content: "🎨"; - } -} - -// List of services -.wc-wizard-services { - border: 1px solid #eee; - padding: 0; - margin: 0 0 1em; - list-style: none outside; - border-radius: 4px; - overflow: hidden; // Make sure dark background doesn't cover curved border - - p { - margin: 0 0 1em 0; - padding: 0; - font-size: 1em; - line-height: 1.5; - } -} - -.wc-wizard-service-item, -.wc-wizard-services-list-toggle { - display: flex; - flex-wrap: nowrap; - justify-content: space-between; - padding: 0; - border-bottom: 1px solid #eee; - color: #666; - align-items: center; - - &:last-child { - border-bottom: 0; - } - - .payment-gateway-fee { - color: #a6a6a6; - } - - .wc-wizard-service-name { - flex-basis: 0; - min-width: 160px; - text-align: center; - font-weight: 700; - padding: 2em 0; - align-self: stretch; - display: flex; - align-items: baseline; - - .wc-wizard-payment-gateway-form & { - justify-content: center; - } - - img { - max-width: 75px; - } - } - - &.stripe-logo .wc-wizard-service-name img { - padding: 8px 0; - } - - &.paypal-logo .wc-wizard-service-name img { - max-width: 87px; - padding: 2px 0; - } - - &.klarna-logo .wc-wizard-service-name img { - max-width: 87px; - padding: 12px 0; - } - - &.square-logo .wc-wizard-service-name img { - max-width: 95px; - padding: 12px 0; - } - - &.eway-logo .wc-wizard-service-name img { - max-width: 87px; - } - - &.payfast-logo .wc-wizard-service-name img { - max-width: 140px; - } - - .wc-wizard-service-description { - flex-grow: 1; - padding: 20px; - - p { - margin-bottom: 1em; - } - - p:last-child { - margin-bottom: 0; - } - - .wc-wizard-service-settings-description { - display: block; - font-style: italic; - color: #999; - } - } - - .wc-wizard-service-enable { - flex-basis: 0; - min-width: 75px; - text-align: center; - cursor: pointer; - padding: 2em 0; - position: relative; - max-height: 1.5em; - align-self: flex-start; - order: 3; - } - - .wc-wizard-service-toggle { - height: 16px; - width: 32px; - border: 2px solid #935687; - background-color: #935687; - display: inline-block; - text-indent: -9999px; - border-radius: 10em; - position: relative; - - input[type="checkbox"] { - display: none; - } - - &::before { - content: ""; - display: block; - width: 16px; - height: 16px; - background: #fff; - position: absolute; - top: 0; - right: 0; - border-radius: 100%; - } - - &.disabled { - border-color: #999; - background-color: #999; - - &::before { - right: auto; - left: 0; - } - } - } - - .wc-wizard-service-settings { - display: none; - margin-top: 0.75em; - margin-bottom: 0; - cursor: default; - - &.hide { - display: none; - } - } - - &.checked { - - .wc-wizard-service-settings { - display: inline-block; - - &.hide { - display: none; - } - } - } - - &.closed { - border-bottom: 0; - } -} - -// Toggle display a list of services. -.wc-wizard-services-list-toggle { - cursor: pointer; - - .wc-wizard-service-enable::before { - content: "\f343"; // up chevron - font-family: dashicons; /* stylelint-disable-line font-family-no-missing-generic-family-keyword */ - color: #666; - font-size: 25px; - margin-top: -7px; - margin-left: -5px; - position: absolute; - visibility: visible; - } - - &.closed { - - .wc-wizard-service-enable::before { - content: "\f347"; // down chevron - } - } - - .wc-wizard-service-enable input { - visibility: hidden; - position: relative; - } -} - -.wc-wizard-services.manual .wc-wizard-service-item { - display: none; -} - -// Shipping list of services -.wc-wizard-services.shipping { - margin: 0; - - .wc-wizard-service-name { - font-weight: 400; - text-align: left; - align-items: center; - max-height: 5em; - padding: 0; - } - - // List header - .wc-wizard-service-item { - padding-left: 2em; - padding-top: 0.67em; - - &:first-child { - border-bottom: 0; - padding-bottom: 0; - font-weight: 700; - - .wc-wizard-service-name { - font-weight: 700; - } - } - } - - .wc-wizard-shipping-method-select, - .shipping-method-setting { - display: flex; - - &.hide { - display: none; - } - } - - .wc-wizard-shipping-method-dropdown, - .shipping-method-setting input { - margin-right: 2em; - margin-bottom: 1em; - - .select2 { - min-width: 130px; - } - } - - .wc-wizard-service-description { - display: flex; - flex-direction: column; - color: #a6a6a6; - } - - .wc-wizard-service-item:not(:first-child) .wc-wizard-service-description { - font-size: 0.92em; - padding-bottom: 10px; - } - - .shipping-method-setting { - - input { - width: 95px; // match dropdown height - border: 1px solid #aaa; - border-color: #ddd; - border-radius: 4px; - height: 28px; - padding-left: 8px; - padding-right: 24px; - font-size: 14px; - color: #444; - background-color: #fff; - display: inline-block; - } - } - - .shipping-method-description, - .shipping-method-setting .description { - color: #7e7e7e; - font-size: 0.9em; - } - - .shipping-method-setting input::placeholder { - color: #e1e1e1; - } -} - -.wc-setup-shipping-units { - - p { - line-height: 1.5; - font-size: 13px; - margin-bottom: 0.25em; - text-align: center; - } - - .wc-setup-shipping-unit { - margin-bottom: 1.75em; - - .select2 { - min-width: 125px; // widen dropdowns - top: -5px; // vertically align dropdown value with surrounding text - } - } -} - -.hide { - display: none; -} - -.wc-wizard-features { - display: flex; - flex-wrap: wrap; - list-style: none; - padding: 0; - - .wc-wizard-feature-item { - flex-basis: calc(50% - 4em - 3px); // two columns, account for padding and borders - border: 1px solid #eee; - padding: 2em; - } - - .wc-wizard-feature-item:nth-child(1) { - border-radius: 4px 0 0 0; - } - - .wc-wizard-feature-item:nth-child(2) { - border-left: 0; - border-radius: 0 4px 0 0; - } - - .wc-wizard-feature-item:nth-child(3) { - border-top: 0; - border-radius: 0 0 0 4px; - } - - .wc-wizard-feature-item:nth-child(4) { - border-top: 0; - border-left: 0; - border-radius: 0 0 4px 0; - } - - p.wc-wizard-feature-name, - p.wc-wizard-feature-description { - margin: 0; - line-height: 1.5; - } -} - -h3.jetpack-reasons { - text-align: center; - margin: 3em 0 1em 0; - font-size: 14px; -} - -.jetpack-logo, -.wcs-notice { - display: block; - margin: 1.75em auto 2em auto; - max-height: 175px; -} - -.activate-splash { - - .jetpack-logo { - width: 170px; - margin-bottom: 0; - } - - .wcs-notice { - margin-top: 1em; - padding-left: 57px; - } -} - -.wc-setup-step__new_onboarding { - - .wc-logo, - .wc-setup-steps { - display: none; - } - - .wc-setup-step__new_onboarding-wrapper { - - .wc-logo { - display: block; - } - - p { - text-align: center; - } - - .wc-setup-step__new_onboarding-welcome, - .wc-setup-step__new_onboarding-plugin-info { - color: #7c7c7c; - font-size: 12px; - } - } -} - -.step { - text-align: center; -} - -.wc-setup .wc-setup-actions .button { - font-weight: 300; - font-size: 16px; - padding: 1em 2em; - box-shadow: none; - min-width: 12em; - margin-top: 10px; - line-height: 1; - margin-right: 0.5em; - margin-bottom: 2px; - height: auto; - border-radius: 4px; - - &:focus, - &:hover, - &:active { - box-shadow: none; - } -} - -.wc-setup .wc-setup-actions .plugin-install-info { - display: block; - font-style: italic; - color: #999; - font-size: 14px; - line-height: 1.5; - margin: 5px 0; - - & > * { - display: block; - } - - .plugin-install-info-list-item::after { - content: ", "; - } - - .plugin-install-info-list-item:last-of-type::after { - content: ". "; - } - - a { - white-space: nowrap; - - &:not(:hover):not(:focus) { - color: inherit; - } - } -} - -.plugin-install-source { - $background: rgba(#bb77ae, 0.15); - background: $background; - - &:not(.wc-wizard-service-item) { - box-shadow: 0 0 0 10px $background; - } -} - -.location-prompt { - color: #666; - font-size: 13px; - font-weight: 500; - margin-bottom: 0.5em; - margin-top: 0.85em; - display: inline-block; -} - -.location-input { - border: 1px solid #aaa; - border-color: #ddd; - border-radius: 4px; - height: 30px; - width: calc(100% - 8px - 8px - 2px); - padding-left: 8px; - padding-right: 8px; - font-size: 16px; - color: #444; - background-color: #fff; - display: block; - - &.dropdown { - width: 100%; - } -} - -.branch-5-2, -.wc-wp-version-gte-53 { - - .location-input { - margin: 0; - width: 100%; - } -} - -.address-step { - - .select2 { - min-width: 100%; // widen currency, product type dropdowns - } -} - -.store-address-container { - - .city-and-postcode { - display: flex; - - div { - flex-basis: 50%; - margin-right: 1em; - - &:last-of-type { - margin-right: 0; - } - } - } - - input[type="text"], - select, - .select2-container { - margin-bottom: 10px; - } -} - -.product-type-container, -.sell-in-person-container { - margin-top: 14px; - margin-bottom: 1px; -} - -#woocommerce_sell_in_person { - margin-left: 0; - margin-top: calc(0.85em / 2); -} - -.wc-wizard-service-settings { - - .payment-email-input { - border: 1px solid #aaa; - border-color: #ddd; - border-radius: 4px; - height: 30px; - padding: 0 8px; - font-size: 14px; - color: #444; - background-color: #fff; - display: inline-block; - - &[disabled] { - color: #aaa; - } - } -} - -.newsletter-form-container { - display: flex; - - .newsletter-form-email { - border: 1px solid #aaa; - border-color: #ddd; - border-radius: 4px; - height: 42px; - padding: 0 8px; - font-size: 16px; - color: #666; - background-color: #fff; - display: inline-block; - margin-right: 6px; - flex-grow: 1; - } - - .newsletter-form-button-container { - flex-grow: 0; - } -} - -.wc-setup .wc-setup-actions .button.newsletter-form-button { - height: 42px; - padding: 0 1em; - margin: 0; -} - -.wc-wizard-next-steps { - border: 1px solid #eee; - border-radius: 4px; - list-style: none; - padding: 0; - - li { - padding: 0; - } - - .wc-wizard-next-step-item { - display: flex; - border-top: 1px solid #eee; - - &:first-child { - border-top: 0; - } - } - - .wc-wizard-next-step-description { - flex-grow: 1; - margin: 1.5em; - } - - .wc-wizard-next-step-action { - flex-grow: 0; - display: flex; - align-items: center; - - .button { - margin: 1em 1.5em; - } - } - - p { - - &.next-step-heading { - margin: 0; - font-size: 0.95em; - font-weight: 400; - font-variant: all-petite-caps; - } - - &.next-step-extra-info { - margin: 0; - } - } - - h3 { - - &.next-step-description { - margin: 0; - font-size: 16px; - font-weight: 600; - } - } - - .wc-wizard-additional-steps { - border-top: 1px solid #eee; - - .wc-wizard-next-step-description { - margin-bottom: 0; - } - - .wc-setup-actions { - margin: 0 0 1.5em 0; - - .button { - font-size: 15px; - margin: 1em 0 1em 1.5em; - } - } - } -} - -p.next-steps-help-text { - color: #9f9f9f; - padding: 0 2em; - text-align: center; - font-size: 0.9em; -} - -p.jetpack-terms { - font-size: 0.8em; - text-align: center; - max-width: 480px; - margin: 0 auto; - line-height: 1.5; -} - -.woocommerce-error { - background: #ffe6e5; - border-color: #ffc5c2; - padding: 1em; - margin-bottom: 1em; - - p { - margin-top: 0; - margin-bottom: 0.5em; - color: #444; - } - - a { - color: #ff645c; - } - - .reconnect-reminder { - font-size: 0.85em; - } - - .wc-setup-actions .button { - font-size: 14px; - } -} - -.wc-wizard-service-setting-stripe_create_account, -.wc-wizard-service-setting-ppec_paypal_reroute_requests { - display: flex; - align-items: flex-start; - - .payment-checkbox-input { - order: 1; - margin-top: 5px; - margin-left: 0; - margin-right: 0; - } - - .stripe_create_account, - .ppec_paypal_reroute_requests { - order: 2; - margin-left: 0.3em; - } -} - -.branch-5-2, -.wc-wp-version-gte-53 { - - .wc-wizard-service-setting-stripe_create_account, - .wc-wizard-service-setting-ppec_paypal_reroute_requests { - - .payment-checkbox-input { - margin-top: 3px; - } - } -} - -.wc-wizard-service-setting-stripe_email, -.wc-wizard-service-setting-ppec_paypal_email { - margin-top: 0.75em; - margin-left: 1.5em; - - label.stripe_email, - label.ppec_paypal_email { - position: absolute; - margin: -1px; - padding: 0; - height: 1px; - width: 1px; - overflow: hidden; - clip: rect(0 0 0 0); - border: 0; - } - - input.payment-email-input { - box-sizing: border-box; - margin-bottom: 0.5em; - width: 100%; - height: 32px; - } -} - -.wc-setup-content .recommended-step { - border: 1px solid #ebebeb; - border-radius: 4px; - padding: 2.5em; -} - -.wc-setup-content .recommended-item { - list-style: none; - margin-bottom: 1.5em; - - &:last-child { - margin-bottom: 0; // Avoid extra space at the end of the list. - } - - label { - display: flex; - align-items: center; - - &::before, - &::after { - top: auto; - } - - &::after { - margin-top: -1.5px; - } - } - - .recommended-item-icon { - border: 1px solid #fff; - border-radius: 7px; - height: 3.5em; - margin-right: 1em; - margin-left: 4px; - - &.recommended-item-icon-wc_admin { - background-color: #96588a; - padding: 0.5em; - height: 2em; - } - - &.recommended-item-icon-storefront_theme { - background-color: #f4a224; - max-height: 3em; - max-width: 3em; - padding: ( 3.5em - 3em ) / 2; - } - - &.recommended-item-icon-automated_taxes { - background-color: #d0011b; - max-height: 1.75em; - padding: ( 3.5em - 1.75em ) / 2; - } - - &.recommended-item-icon-mailchimp { - background-color: #ffe01b; - height: 2em; - padding: ( 3.5em - 2em ) / 2; - } - - &.recommended-item-icon-woocommerce_services { - background-color: #f0f0f0; - max-height: 1.5em; - padding: 1.3em 0.7em; - } - - &.recommended-item-icon-shipstation { - background-color: #f0f0f0; - padding: 0.3em; - } - } - - .recommended-item-description-container { - - h3 { - font-size: 15px; - font-weight: 700; - letter-spacing: 0.5px; - margin-bottom: 0; - } - - p { - margin-top: 0; - line-height: 1.5; - } - } -} - -.wc-wizard-service-info { - padding: 1em 2em; - background-color: #fafafa; -} - -.help_tip { - text-decoration: underline dotted; -} - -@media only screen and (max-width: 400px) { - - .wc-logo img { - max-width: 80%; - } - - .wc-setup-steps { - display: none; - } - - .store-address-container { - - .city-and-postcode { - display: block; - - div { - margin-right: 0; - } - } - } - - .wc-wizard-service-item, - .wc-wizard-services-list-toggle { - flex-wrap: wrap; - - .wc-wizard-service-enable { - order: 2; - padding: 20px 0 0; - } - - .wc-wizard-service-description { - order: 3; - } - - .wc-wizard-service-name { - padding: 20px 20px 0; - text-align: left; - justify-content: space-between !important; - - img { - margin: 0; - } - } - } - - .newsletter-form-container { - display: block; - - .newsletter-form-email { - display: block; - box-sizing: border-box; - width: 100%; - margin-bottom: 10px; - } - - .button.newsletter-form-button { - float: left; - } - } - - .wc-wizard-next-steps .wc-wizard-next-step-item { - flex-wrap: wrap; - - .wc-wizard-next-step-description { - margin-bottom: 0; - } - - .wc-wizard-next-step-action { - - p { - margin: 0; - } - } - } -} -/* stylelint-enable */ diff --git a/assets/css/woocommerce.scss b/assets/css/woocommerce.scss deleted file mode 100644 index c0c9409d7c6..00000000000 --- a/assets/css/woocommerce.scss +++ /dev/null @@ -1,2331 +0,0 @@ -/** - * woocommerce.scss - * Governs the general look and feel of WooCommerce sections of stores using themes that do not - * integrate with WooCommerce specifically. - */ - -/** - * Imports - */ -@import "mixins"; -@import "variables"; -@import "animation"; -@import "fonts"; - -/** - * Global styles - */ -p.demo_store, -.woocommerce-store-notice { - position: absolute; - top: 0; - left: 0; - right: 0; - margin: 0; - width: 100%; - font-size: 1em; - padding: 1em 0; - text-align: center; - background-color: $primary; - color: $primarytext; - z-index: 99998; - box-shadow: 0 1px 1em rgba(0, 0, 0, 0.2); - display: none; - - a { - color: $primarytext; - text-decoration: underline; - } -} - -.screen-reader-text { - clip: rect(1px, 1px, 1px, 1px); - height: 1px; - overflow: hidden; - position: absolute !important; - width: 1px; - word-wrap: normal !important; -} - -.admin-bar p.demo_store { - top: 32px; -} - -/** - * Utility classes - */ -.clear { - clear: both; -} - -/** - * Main WooCommerce styles - */ -.woocommerce { - - .blockUI.blockOverlay { - position: relative; - - @include loader(); - } - - .loader { - - @include loader(); - } - - a.remove { - display: block; - font-size: 1.5em; - height: 1em; - width: 1em; - text-align: center; - line-height: 1; - border-radius: 100%; - color: red !important; // Required for default theme compatibility - text-decoration: none; - font-weight: 700; - border: 0; - - &:hover { - color: #fff !important; // Required for default theme compatibility - background: red; - } - } - - small.note { - display: block; - color: $subtext; - font-size: 0.857em; - margin-top: 10px; - } - - .woocommerce-breadcrumb { - - @include clearfix(); - margin: 0 0 1em; - padding: 0; - font-size: 0.92em; - color: $subtext; - - a { - color: $subtext; - } - } - - .quantity .qty { - width: 3.631em; - text-align: center; - } - - /** - * Product Page - */ - div.product { - margin-bottom: 0; - position: relative; - - .product_title { - clear: none; - margin-top: 0; - padding: 0; - } - - span.price, - p.price { - color: $highlight; - font-size: 1.25em; - - ins { - background: inherit; - font-weight: 700; - display: inline-block; - } - - del { - opacity: 0.5; - display: inline-block; - } - } - - p.stock { - font-size: 0.92em; - } - - .stock { - color: $highlight; - } - - .out-of-stock { - color: red; - } - - .woocommerce-product-rating { - margin-bottom: 1.618em; - } - - div.images { - margin-bottom: 2em; - - img { - display: block; - width: 100%; - height: auto; - box-shadow: none; - } - - div.thumbnails { - padding-top: 1em; - } - - &.woocommerce-product-gallery { - position: relative; - } - - .woocommerce-product-gallery__wrapper { - transition: all cubic-bezier(0.795, -0.035, 0, 1) 0.5s; - margin: 0; - padding: 0; - } - - .woocommerce-product-gallery__wrapper .zoomImg { - background-color: #fff; - opacity: 0; - } - - .woocommerce-product-gallery__image--placeholder { - border: 1px solid #f2f2f2; - } - - .woocommerce-product-gallery__image:nth-child(n+2) { - width: 25%; - display: inline-block; - } - - .woocommerce-product-gallery__trigger { - position: absolute; - top: 0.5em; - right: 0.5em; - font-size: 2em; - z-index: 9; - width: 36px; - height: 36px; - background: #fff; - text-indent: -9999px; - border-radius: 100%; - box-sizing: content-box; - - &::before { - content: ""; - display: block; - width: 10px; - height: 10px; - border: 2px solid #000; - border-radius: 100%; - position: absolute; - top: 9px; - left: 9px; - box-sizing: content-box; - } - - &::after { - content: ""; - display: block; - width: 2px; - height: 8px; - background: #000; - border-radius: 6px; - position: absolute; - top: 19px; - left: 22px; - transform: rotate(-45deg); - box-sizing: content-box; - } - } - - .flex-control-thumbs { - overflow: hidden; - zoom: 1; - margin: 0; - padding: 0; - - li { - width: 25%; - float: left; - margin: 0; - list-style: none; - - img { - cursor: pointer; - opacity: 0.5; - margin: 0; - - &.flex-active, - &:hover { - opacity: 1; - } - } - } - } - } - - .woocommerce-product-gallery--columns-3 { - - .flex-control-thumbs li:nth-child(3n+1) { - clear: left; - } - } - - .woocommerce-product-gallery--columns-4 { - - .flex-control-thumbs li:nth-child(4n+1) { - clear: left; - } - } - - .woocommerce-product-gallery--columns-5 { - - .flex-control-thumbs li:nth-child(5n+1) { - clear: left; - } - } - - div.summary { - margin-bottom: 2em; - } - - div.social { - text-align: right; - margin: 0 0 1em; - - span { - margin: 0 0 0 2px; - - span { - margin: 0; - } - - .stButton .chicklets { - padding-left: 16px; - width: 0; - } - } - - iframe { - float: left; - margin-top: 3px; - } - } - - .woocommerce-tabs { - - ul.tabs { - list-style: none; - padding: 0 0 0 1em; - margin: 0 0 1.618em; - overflow: hidden; - position: relative; - - li { - border: 1px solid darken($secondary, 10%); - background-color: $secondary; - display: inline-block; - position: relative; - z-index: 0; - border-radius: 4px 4px 0 0; - margin: 0 -5px; - padding: 0 1em; - - a { - display: inline-block; - padding: 0.5em 0; - font-weight: 700; - color: $secondarytext; - text-decoration: none; - - &:hover { - text-decoration: none; - color: lighten($secondarytext, 10%); - } - } - - &.active { - background: $contentbg; - z-index: 2; - border-bottom-color: $contentbg; - - a { - color: inherit; - text-shadow: inherit; - } - - &::before { - box-shadow: 2px 2px 0 $contentbg; - } - - &::after { - box-shadow: -2px 2px 0 $contentbg; - } - } - - &::before, - &::after { - border: 1px solid darken($secondary, 10%); - position: absolute; - bottom: -1px; - width: 5px; - height: 5px; - content: " "; - box-sizing: border-box; - } - - &::before { - left: -5px; - border-bottom-right-radius: 4px; - border-width: 0 1px 1px 0; - box-shadow: 2px 2px 0 $secondary; - } - - &::after { - right: -5px; - border-bottom-left-radius: 4px; - border-width: 0 0 1px 1px; - box-shadow: -2px 2px 0 $secondary; - } - } - - &::before { - position: absolute; - content: " "; - width: 100%; - bottom: 0; - left: 0; - border-bottom: 1px solid darken($secondary, 10%); - z-index: 1; - } - } - - .panel { - margin: 0 0 2em; - padding: 0; - } - } - - p.cart { - margin-bottom: 2em; - - @include clearfix(); - } - - form.cart { - margin-bottom: 2em; - - @include clearfix(); - - div.quantity { - float: left; - margin: 0 4px 0 0; - } - - table { - border-width: 0 0 1px; - - td { - padding-left: 0; - } - - div.quantity { - float: none; - margin: 0; - } - - small.stock { - display: block; - float: none; - } - } - - .variations { - margin-bottom: 1em; - border: 0; - width: 100%; - - td, - th { - border: 0; - vertical-align: top; - line-height: 2em; - } - - label { - font-weight: 700; - } - - select { - max-width: 100%; - min-width: 75%; - display: inline-block; - margin-right: 1em; - } - - td.label { - padding-right: 1em; - } - } - - .woocommerce-variation-description p { - margin-bottom: 1em; - } - - .reset_variations { - visibility: hidden; - font-size: 0.83em; - } - - .wc-no-matching-variations { - display: none; - } - - .button { - vertical-align: middle; - float: left; - } - - .group_table { - - td.woocommerce-grouped-product-list-item__label { - padding-right: 1em; - padding-left: 1em; - } - - td { - vertical-align: top; - padding-bottom: 0.5em; - border: 0; - } - - td:first-child { - width: 4em; - text-align: center; - } - - .wc-grouped-product-add-to-cart-checkbox { - display: inline-block; - width: auto; - margin: 0 auto; - transform: scale(1.5, 1.5); - } - } - } - } - - span.onsale { - min-height: 3.236em; - min-width: 3.236em; - padding: 0.202em; - font-size: 1em; - font-weight: 700; - position: absolute; - text-align: center; - line-height: 3.236; - top: -0.5em; - left: -0.5em; - margin: 0; - border-radius: 100%; - background-color: $highlight; - color: $highlightext; - font-size: 0.857em; - z-index: 9; - } - - /** - * Product loops - */ - .products ul, - ul.products { - margin: 0 0 1em; - padding: 0; - list-style: none outside; - clear: both; - - @include clearfix(); - - li { - list-style: none outside; - } - } - - ul.products li.product { - - .onsale { - top: 0; - right: 0; - left: auto; - margin: -0.5em -0.5em 0 0; - } - - h3, - .woocommerce-loop-product__title, - .woocommerce-loop-category__title { - padding: 0.5em 0; - margin: 0; - font-size: 1em; - } - - a { - text-decoration: none; - } - - a img { - width: 100%; - height: auto; - display: block; - margin: 0 0 1em; - box-shadow: none; - } - - strong { - display: block; - } - - .woocommerce-placeholder { - border: 1px solid #f2f2f2; - } - - .star-rating { - font-size: 0.857em; - } - - .button { - margin-top: 1em; - } - - .price { - color: $highlight; - display: block; - font-weight: normal; - margin-bottom: 0.5em; - font-size: 0.857em; - - del { - color: inherit; - opacity: 0.5; - display: inline-block; - } - - ins { - background: none; - font-weight: 700; - display: inline-block; - } - - .from { - font-size: 0.67em; - margin: -2px 0 0 0; - text-transform: uppercase; - color: rgba(desaturate($highlight, 75%), 0.5); - } - } - } - - .woocommerce-result-count { - margin: 0 0 1em; - } - - .woocommerce-ordering { - margin: 0 0 1em; - - select { - vertical-align: top; - } - } - - nav.woocommerce-pagination { - text-align: center; - - ul { - display: inline-block; - white-space: nowrap; - padding: 0; - clear: both; - border: 1px solid darken($secondary, 10%); - border-right: 0; - margin: 1px; - - li { - border-right: 1px solid darken($secondary, 10%); - padding: 0; - margin: 0; - float: left; - display: inline; - overflow: hidden; - - a, - span { - margin: 0; - text-decoration: none; - padding: 0; - line-height: 1; - font-size: 1em; - font-weight: normal; - padding: 0.5em; - min-width: 1em; - display: block; - } - - span.current, - a:hover, - a:focus { - background: $secondary; - color: darken($secondary, 40%); - } - } - } - } - - /** - * Buttons - */ - a.button, - button.button, - input.button, - #respond input#submit { - font-size: 100%; - margin: 0; - line-height: 1; - cursor: pointer; - position: relative; - text-decoration: none; - overflow: visible; - padding: 0.618em 1em; - font-weight: 700; - border-radius: 3px; - left: auto; - color: $secondarytext; - background-color: $secondary; - border: 0; - display: inline-block; - background-image: none; - box-shadow: none; - text-shadow: none; - - &.loading { - opacity: 0.25; - padding-right: 2.618em; - - &::after { - font-family: "WooCommerce"; - content: "\e01c"; - vertical-align: top; - font-weight: 400; - position: absolute; - top: 0.618em; - right: 1em; - animation: spin 2s linear infinite; - } - } - - &.added::after { - font-family: "WooCommerce"; - content: "\e017"; - margin-left: 0.53em; - vertical-align: bottom; - } - - &:hover { - background-color: darken($secondary, 5%); - text-decoration: none; - background-image: none; - color: $secondarytext; - } - - &.alt { - background-color: $primary; - color: $primarytext; - -webkit-font-smoothing: antialiased; - - &:hover { - background-color: darken($primary, 5%); - color: $primarytext; - } - - &.disabled, - &:disabled, - &:disabled[disabled], - &.disabled:hover, - &:disabled:hover, - &:disabled[disabled]:hover { - background-color: $primary; - color: $primarytext; - } - } - - &:disabled, - &.disabled, - &:disabled[disabled] { - color: inherit; - cursor: not-allowed; - opacity: 0.5; - padding: 0.618em 1em; - - &:hover { - color: inherit; - background-color: $secondary; - } - } - } - - .cart .button, - .cart input.button { - float: none; - } - - a.added_to_cart { - padding-top: 0.5em; - display: inline-block; - } - - /** - * Reviews - */ - #reviews { - - h2 small { - float: right; - color: $subtext; - font-size: 15px; - margin: 10px 0 0; - - a { - text-decoration: none; - color: $subtext; - } - } - - h3 { - margin: 0; - } - - #respond { - margin: 0; - border: 0; - padding: 0; - } - - #comment { - height: 75px; - } - - #comments { - - .add_review { - - @include clearfix(); - } - - h2 { - clear: none; - } - - ol.commentlist { - - @include clearfix(); - margin: 0; - width: 100%; - background: none; - list-style: none; - - li { - padding: 0; - margin: 0 0 20px; - border: 0; - position: relative; - background: 0; - border: 0; - - .meta { - color: $subtext; - font-size: 0.75em; - } - - img.avatar { - float: left; - position: absolute; - top: 0; - left: 0; - padding: 3px; - width: 32px; - height: auto; - background: $secondary; - border: 1px solid darken($secondary, 3%); - margin: 0; - box-shadow: none; - } - - .comment-text { - margin: 0 0 0 50px; - border: 1px solid darken($secondary, 3%); - border-radius: 4px; - padding: 1em 1em 0; - - @include clearfix(); - - p { - margin: 0 0 1em; - } - - p.meta { - font-size: 0.83em; - } - } - } - - ul.children { - list-style: none outside; - margin: 20px 0 0 50px; - - .star-rating { - display: none; - } - } - - #respond { - border: 1px solid darken($secondary, 3%); - border-radius: 4px; - padding: 1em 1em 0; - margin: 20px 0 0 50px; - } - } - - .commentlist > li::before { - content: ""; - } - } - } - - /** - * Star ratings - */ - .star-rating { - float: right; - overflow: hidden; - position: relative; - height: 1em; - line-height: 1; - font-size: 1em; - width: 5.4em; - font-family: "star"; - - &::before { - content: "\73\73\73\73\73"; - color: darken($secondary, 10%); - float: left; - top: 0; - left: 0; - position: absolute; - } - - span { - overflow: hidden; - float: left; - top: 0; - left: 0; - position: absolute; - padding-top: 1.5em; - } - - span::before { - content: "\53\53\53\53\53"; - top: 0; - position: absolute; - left: 0; - } - } - - .woocommerce-product-rating { - - @include clearfix(); - line-height: 2; - display: block; - - .star-rating { - margin: 0.5em 4px 0 0; - float: left; - } - } - - .products .star-rating { - display: block; - margin: 0 0 0.5em; - float: none; - } - - .hreview-aggregate .star-rating { - margin: 10px 0 0; - } - - #review_form #respond { - - @include clearfix(); - position: static; - margin: 0; - width: auto; - padding: 0; - background: transparent none; - border: 0; - - p { - margin: 0 0 10px; - } - - .form-submit input { - left: auto; - } - - textarea { - box-sizing: border-box; - width: 100%; - } - } - - p.stars { - - a { - position: relative; - height: 1em; - width: 1em; - text-indent: -999em; - display: inline-block; - text-decoration: none; - - &::before { - display: block; - position: absolute; - top: 0; - left: 0; - width: 1em; - height: 1em; - line-height: 1; - font-family: "WooCommerce"; - content: "\e021"; - text-indent: 0; - } - - &:hover ~ a::before { - content: "\e021"; - } - } - - &:hover a::before { - content: "\e020"; - } - - &.selected { - - a.active { - - &::before { - content: "\e020"; - } - - ~ a::before { - content: "\e021"; - } - } - - a:not(.active)::before { - content: "\e020"; - } - } - } - - /** - * Tables - */ - table.shop_attributes { - border: 0; - border-top: 1px dotted rgba(0, 0, 0, 0.1); - margin-bottom: 1.618em; - width: 100%; - - th { - width: 150px; - font-weight: 700; - padding: 8px; - border-top: 0; - border-bottom: 1px dotted rgba(0, 0, 0, 0.1); - margin: 0; - line-height: 1.5; - } - - td { - font-style: italic; - padding: 0; - border-top: 0; - border-bottom: 1px dotted rgba(0, 0, 0, 0.1); - margin: 0; - line-height: 1.5; - - p { - margin: 0; - padding: 8px 0; - } - } - - tr:nth-child(even) td, - tr:nth-child(even) th { - background: rgba(0, 0, 0, 0.025); - } - } - - table.shop_table { - border: 1px solid rgba(0, 0, 0, 0.1); - margin: 0 -1px 24px 0; - text-align: left; - width: 100%; - border-collapse: separate; - border-radius: 5px; - - th { - font-weight: 700; - padding: 9px 12px; - line-height: 1.5em; - } - - td { - border-top: 1px solid rgba(0, 0, 0, 0.1); - padding: 9px 12px; - vertical-align: middle; - line-height: 1.5em; - - small { - font-weight: normal; - } - - del { - font-weight: normal; - } - } - - tbody:first-child tr:first-child { - - th, - td { - border-top: 0; - } - } - - tfoot td, - tfoot th, - tbody th { - font-weight: 700; - border-top: 1px solid rgba(0, 0, 0, 0.1); - } - } - - table.my_account_orders { - font-size: 0.85em; - - th, - td { - padding: 4px 8px; - vertical-align: middle; - } - - .button { - white-space: nowrap; - } - } - - table.woocommerce-MyAccount-downloads { - - td, - th { - vertical-align: top; - text-align: center; - - &:first-child { - text-align: left; - } - - &:last-child { - text-align: left; - } - - .woocommerce-MyAccount-downloads-file::before { - content: "\2193"; - display: inline-block; - } - } - } - - td.product-name { - - dl.variation, - .wc-item-meta { - list-style: none outside; - - dt, - .wc-item-meta-label { - float: left; - clear: both; - margin-right: 0.25em; - display: inline-block; - list-style: none outside; - } - - dd { - margin: 0; - } - - p, - &:last-child { - margin-bottom: 0; - } - } - - p.backorder_notification { - font-size: 0.83em; - } - } - - td.product-quantity { - min-width: 80px; - } - - /** - * Cart sidebar - */ - ul.cart_list, - ul.product_list_widget { - list-style: none outside; - padding: 0; - margin: 0; - - li { - padding: 4px 0; - margin: 0; - - @include clearfix(); - list-style: none; - - a { - display: block; - font-weight: 700; - } - - img { - float: right; - margin-left: 4px; - width: 32px; - height: auto; - box-shadow: none; - } - - dl { - margin: 0; - padding-left: 1em; - border-left: 2px solid rgba(0, 0, 0, 0.1); - - @include clearfix(); - - dt, - dd { - display: inline-block; - float: left; - margin-bottom: 1em; - } - - dt { - font-weight: 700; - padding: 0 0 0.25em; - margin: 0 4px 0 0; - clear: left; - } - - dd { - padding: 0 0 0.25em; - - p:last-child { - margin-bottom: 0; - } - } - } - - .star-rating { - float: none; - } - } - } - - &.widget_shopping_cart, - .widget_shopping_cart { - - .total { - border-top: 3px double $secondary; - padding: 4px 0 0; - - strong { - min-width: 40px; - display: inline-block; - } - } - - .cart_list li { - padding-left: 2em; - position: relative; - padding-top: 0; - - a.remove { - position: absolute; - top: 0; - left: 0; - } - } - - .buttons { - - @include clearfix(); - - a { - margin-right: 5px; - margin-bottom: 5px; - } - } - } - - /** - * Forms - */ - form .form-row { - padding: 3px; - margin: 0 0 6px; - - [placeholder]:focus::-webkit-input-placeholder { - transition: opacity 0.5s 0.5s ease; - opacity: 0; - } - - label { - line-height: 2; - } - - label.hidden { - visibility: hidden; - } - - label.inline { - display: inline; - } - - .woocommerce-input-wrapper { - - .description { - background: #1e85be; - color: #fff; - border-radius: 3px; - padding: 1em; - margin: 0.5em 0 0; - clear: both; - display: none; - position: relative; - - a { - color: #fff; - text-decoration: underline; - border: 0; - box-shadow: none; - } - - &::before { - left: 50%; - top: 0%; - margin-top: -4px; - transform: translateX(-50%) rotate(180deg); - content: ""; - position: absolute; - border-width: 4px 6px 0 6px; - border-style: solid; - border-color: #1e85be transparent transparent transparent; - z-index: 100; - display: block; - } - } - } - - select { - cursor: pointer; - margin: 0; - } - - .required { - color: red; - font-weight: 700; - border: 0 !important; - text-decoration: none; - visibility: hidden; // Only show optional by default. - } - - .optional { - visibility: visible; - } - - .input-checkbox { - display: inline; - margin: -2px 8px 0 0; - text-align: center; - vertical-align: middle; - } - - input.input-text, - textarea { - box-sizing: border-box; - width: 100%; - margin: 0; - outline: 0; - line-height: normal; - } - - textarea { - height: 4em; - line-height: 1.5; - display: block; - box-shadow: none; - } - - .select2-container { - width: 100%; - line-height: 2em; - } - - &.woocommerce-invalid { - - label { - color: $red; - } - - .select2-container, - input.input-text, - select { - border-color: $red; - } - } - - &.woocommerce-validated { - - .select2-container, - input.input-text, - select { - border-color: darken($green, 5%); - } - } - - ::-webkit-input-placeholder { - line-height: normal; - } - - :-moz-placeholder { - line-height: normal; - } - - :-ms-input-placeholder { - line-height: normal; - } - } - - form.login, - form.checkout_coupon, - form.register { - border: 1px solid darken($secondary, 10%); - padding: 20px; - margin: 2em 0; - text-align: left; - border-radius: 5px; - } - - ul#shipping_method { - list-style: none outside; - margin: 0; - padding: 0; - - li { - margin: 0 0 0.5em; - line-height: 1.5em; - list-style: none outside; - - input { - margin: 3px 0.4375em 0 0; - vertical-align: top; - } - - label { - display: inline; - } - } - - .amount { - font-weight: 700; - } - } - - p.woocommerce-shipping-contents { - margin: 0; - } - - /** - * Order page - */ - ul.order_details { - - @include clearfix(); - margin: 0 0 3em; - list-style: none; - - li { - float: left; - margin-right: 2em; - text-transform: uppercase; - font-size: 0.715em; - line-height: 1; - border-right: 1px dashed darken($secondary, 10%); - padding-right: 2em; - margin-left: 0; - padding-left: 0; - list-style-type: none; - - strong { - display: block; - font-size: 1.4em; - text-transform: none; - line-height: 1.5; - } - - &:last-of-type { - border: none; - } - } - } - - .wc-bacs-bank-details-account-name { - font-weight: bold; - } - - .woocommerce-order-downloads, - .woocommerce-customer-details, - .woocommerce-order-details { - margin-bottom: 2em; - - *:last-child { - margin-bottom: 0; - } - } - - .woocommerce-customer-details { - - address { - font-style: normal; - margin-bottom: 0; - border: 1px solid rgba(0, 0, 0, 0.1); - border-bottom-width: 2px; - border-right-width: 2px; - text-align: left; - width: 100%; - border-radius: 5px; - padding: 6px 12px; - } - - .woocommerce-customer-details--phone, - .woocommerce-customer-details--email { - margin-bottom: 0; - padding-left: 1.5em; - } - - .woocommerce-customer-details--phone::before { - - @include iconbefore( "\e037" ); - margin-left: -1.5em; - line-height: 1.75; - position: absolute; - } - - .woocommerce-customer-details--email::before { - - @include iconbefore( "\e02d" ); - margin-left: -1.5em; - line-height: 1.75; - position: absolute; - } - } - - /** - * Layered nav widget - */ - .woocommerce-widget-layered-nav-list { - margin: 0; - padding: 0; - border: 0; - list-style: none outside; - - .woocommerce-widget-layered-nav-list__item { - - @include clearfix(); - padding: 0 0 1px; - list-style: none; - - a, - span { - padding: 1px 0; - } - } - - .woocommerce-widget-layered-nav-list__item--chosen a::before { - - @include iconbefore( "\e013" ); - color: $red; - } - } - - .woocommerce-widget-layered-nav-dropdown__submit { - margin-top: 1em; - } - - .widget_layered_nav_filters ul { - margin: 0; - padding: 0; - border: 0; - list-style: none outside; - overflow: hidden; - zoom: 1; - - li { - float: left; - padding: 0 1em 1px 1px; - list-style: none; - - a { - text-decoration: none; - - &::before { - - @include iconbefore( "\e013" ); - color: $red; - vertical-align: inherit; - margin-right: 0.5em; - } - } - } - } - - /** - * Price filter widget - */ - .widget_price_filter { - - .price_slider { - margin-bottom: 1em; - } - - .price_slider_amount { - text-align: right; - line-height: 2.4; - font-size: 0.8751em; - - .button { - font-size: 1.15em; - float: left; - } - } - - .ui-slider { - position: relative; - text-align: left; - margin-left: 0.5em; - margin-right: 0.5em; - } - - .ui-slider .ui-slider-handle { - position: absolute; - z-index: 2; - width: 1em; - height: 1em; - background-color: $primary; - border-radius: 1em; - cursor: ew-resize; - outline: none; - top: -0.3em; - - /* rtl:ignore */ - margin-left: -0.5em; - } - - .ui-slider .ui-slider-range { - position: absolute; - z-index: 1; - font-size: 0.7em; - display: block; - border: 0; - border-radius: 1em; - background-color: $primary; - } - - .price_slider_wrapper .ui-widget-content { - border-radius: 1em; - background-color: darken($primary, 30%); - border: 0; - } - - .ui-slider-horizontal { - height: 0.5em; - } - - .ui-slider-horizontal .ui-slider-range { - top: 0; - height: 100%; - } - - .ui-slider-horizontal .ui-slider-range-min { - left: -1px; - } - - .ui-slider-horizontal .ui-slider-range-max { - right: -1px; - } - } - - /** - * Rating Filter Widget - */ - .widget_rating_filter ul { - margin: 0; - padding: 0; - border: 0; - list-style: none outside; - - li { - - @include clearfix(); - padding: 0 0 1px; - list-style: none; - - a { - padding: 1px 0; - text-decoration: none; - } - - .star-rating { - float: none; - display: inline-block; - } - } - - li.chosen a::before { - - @include iconbefore( "\e013" ); - color: $red; - } - } - - .woocommerce-form-login { - - .woocommerce-form-login__submit { - float: left; - margin-right: 1em; - } - - .woocommerce-form-login__rememberme { - display: inline-block; - } - } -} - -.woocommerce-no-js { - - form.woocommerce-form-login, - form.woocommerce-form-coupon { - display: block !important; - } - - .woocommerce-form-login-toggle, - .woocommerce-form-coupon-toggle, - .showcoupon { - display: none !important; - } -} - -.woocommerce-message, -.woocommerce-error, -.woocommerce-info { - padding: 1em 2em 1em 3.5em; - margin: 0 0 2em; - position: relative; - background-color: lighten($secondary, 5%); - color: $secondarytext; - border-top: 3px solid $primary; - list-style: none outside; - - @include clearfix(); - width: auto; - word-wrap: break-word; - - &::before { - font-family: "WooCommerce"; - content: "\e028"; - display: inline-block; - position: absolute; - top: 1em; - left: 1.5em; - } - - .button { - float: right; - } - - li { - list-style: none outside !important; // Required for default theme compatibility - padding-left: 0 !important; // Required for default theme compatibility - margin-left: 0 !important; // Required for default theme compatibility - } -} - -/** - * Right to left styles - */ -.rtl.woocommerce .price_label, -.rtl.woocommerce .price_label span { - - /* rtl:ignore */ - direction: ltr; - unicode-bidi: embed; -} - -.woocommerce-message { - border-top-color: #8fae1b; - - &::before { - content: "\e015"; - color: #8fae1b; - } -} - -.woocommerce-info { - border-top-color: #1e85be; - - &::before { - color: #1e85be; - } -} - -.woocommerce-error { - border-top-color: #b81c23; - - &::before { - content: "\e016"; - color: #b81c23; - } -} - -/** - * Account page - */ -.woocommerce-account { - - .woocommerce { - - @include clearfix(); - } - - .addresses .title { - - @include clearfix(); - - h3 { - float: left; - } - - .edit { - float: right; - } - } - - ol.commentlist.notes li.note { - - p.meta { - font-weight: 700; - margin-bottom: 0; - } - - .description p:last-child { - margin-bottom: 0; - } - } - - ul.digital-downloads { - margin-left: 0; - padding-left: 0; - - li { - list-style: none; - margin-left: 0; - padding-left: 0; - - &::before { - - @include iconbefore( "\e00a" ); - } - - .count { - float: right; - } - } - } -} - -/** - * Cart/checkout page - */ -.woocommerce-cart, -.woocommerce-checkout, -#add_payment_method { - - table.cart { - - .product-thumbnail { - min-width: 32px; - } - - img { - width: 32px; - box-shadow: none; - } - - th, - td { - vertical-align: middle; - } - - td.actions .coupon .input-text { - float: left; - box-sizing: border-box; - border: 1px solid darken($secondary, 10%); - padding: 6px 6px 5px; - margin: 0 4px 0 0; - outline: 0; - } - - input { - margin: 0; - vertical-align: middle; - } - } - - .wc-proceed-to-checkout { - - @include clearfix; - padding: 1em 0; - - a.checkout-button { - display: block; - text-align: center; - margin-bottom: 1em; - font-size: 1.25em; - padding: 1em; - } - } - - .cart-collaterals { - - .shipping-calculator-button { - float: none; - margin-top: 0.5em; - display: inline-block; - } - - .shipping-calculator-button::after { - - @include iconafter( "\e019" ); - } - - .shipping-calculator-form { - margin: 1em 0 0 0; - } - - .cart_totals { - - p small { - color: $subtext; - font-size: 0.83em; - } - - table { - border-collapse: separate; - margin: 0 0 6px; - padding: 0; - - tr:first-child { - - th, - td { - border-top: 0; - } - } - - th { - width: 35%; - } - - td, - th { - vertical-align: top; - border-left: 0; - border-right: 0; - line-height: 1.5em; - } - - small { - color: $subtext; - } - - select { - width: 100%; - } - } - - .discount td { - color: $highlight; - } - - tr td, - tr th { - border-top: 1px solid $secondary; - } - - .woocommerce-shipping-destination { - margin-bottom: 0; - } - } - - .cross-sells ul.products li.product { - margin-top: 0; - } - } - - .checkout { - - .col-2 { - - h3#ship-to-different-address { - float: left; - clear: none; - } - - .notes { - clear: left; - } - - .form-row-first { - clear: left; - } - } - - .create-account small { - font-size: 11px; - color: $subtext; - font-weight: normal; - } - - div.shipping-address { - padding: 0; - clear: left; - width: 100%; - } - - .shipping_address { - clear: both; - } - } - - #payment { - background: $secondary; - border-radius: 5px; - - ul.payment_methods { - - @include clearfix(); - text-align: left; - padding: 1em; - border-bottom: 1px solid darken($secondary, 10%); - margin: 0; - list-style: none outside; - - li { - line-height: 2; - text-align: left; - margin: 0; - font-weight: normal; - - input { - margin: 0 1em 0 0; - } - - img { - vertical-align: middle; - margin: -2px 0 0 0.5em; - padding: 0; - position: relative; - box-shadow: none; - } - - img + img { - margin-left: 2px; - } - } - - li:not(.woocommerce-notice) { - - @include clearfix; - } - } - - div.form-row { - padding: 1em; - } - - div.payment_box { - position: relative; - box-sizing: border-box; - width: 100%; - padding: 1em; - margin: 1em 0; - font-size: 0.92em; - border-radius: 2px; - line-height: 1.5; - background-color: darken($secondary, 5%); - color: $secondarytext; - - input.input-text, - textarea { - border-color: darken($secondary, 15%); - border-top-color: darken($secondary, 20%); - } - - ::-webkit-input-placeholder { - color: darken($secondary, 20%); - } - - :-moz-placeholder { - color: darken($secondary, 20%); - } - - :-ms-input-placeholder { - color: darken($secondary, 20%); - } - - .woocommerce-SavedPaymentMethods { - list-style: none outside; - margin: 0; - - .woocommerce-SavedPaymentMethods-token, - .woocommerce-SavedPaymentMethods-new { - margin: 0 0 0.5em; - - label { - cursor: pointer; - } - } - - .woocommerce-SavedPaymentMethods-tokenInput { - vertical-align: middle; - margin: -3px 1em 0 0; - position: relative; - } - } - - .wc-credit-card-form { - border: 0; - padding: 0; - margin: 1em 0 0; - } - - .wc-credit-card-form-card-number, - .wc-credit-card-form-card-expiry, - .wc-credit-card-form-card-cvc { - font-size: 1.5em; - padding: 8px; - background-repeat: no-repeat; - background-position: right 0.618em center; - background-size: 32px 20px; - - &.visa { - background-image: url("../images/icons/credit-cards/visa.svg"); - } - - &.mastercard { - background-image: url("../images/icons/credit-cards/mastercard.svg"); - } - - &.laser { - background-image: url("../images/icons/credit-cards/laser.svg"); - } - - &.dinersclub { - background-image: url("../images/icons/credit-cards/diners.svg"); - } - - &.maestro { - background-image: url("../images/icons/credit-cards/maestro.svg"); - } - - &.jcb { - background-image: url("../images/icons/credit-cards/jcb.svg"); - } - - &.amex { - background-image: url("../images/icons/credit-cards/amex.svg"); - } - - &.discover { - background-image: url("../images/icons/credit-cards/discover.svg"); - } - } - - span.help { - font-size: 0.857em; - color: $subtext; - font-weight: normal; - } - - .form-row { - margin: 0 0 1em; - } - - p:last-child { - margin-bottom: 0; - } - - &::before { - content: ""; - display: block; - border: 1em solid darken($secondary, 5%); /* arrow size / color */ - border-right-color: transparent; - border-left-color: transparent; - border-top-color: transparent; - position: absolute; - top: -0.75em; - left: 0; - margin: -1em 0 0 2em; - } - } - - .payment_method_paypal { - - .about_paypal { - float: right; - line-height: 52px; - font-size: 0.83em; - } - - img { - max-height: 52px; - vertical-align: middle; - } - } - } -} - -.woocommerce-terms-and-conditions { - border: 1px solid rgba(0, 0, 0, 0.2); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); - background: rgba(0, 0, 0, 0.05); -} - -.woocommerce-invalid { - - #terms { - outline: 2px solid red; - outline-offset: 2px; - } -} - -/** - * Password strength meter - */ -.woocommerce-password-strength { - text-align: center; - font-weight: 600; - padding: 3px 0.5em; - font-size: 1em; - - &.strong { - background-color: #c1e1b9; - border-color: #83c373; - } - - &.short { - background-color: #f1adad; - border-color: #e35b5b; - } - - &.bad { - background-color: #fbc5a9; - border-color: #f78b53; - } - - &.good { - background-color: #ffe399; - border-color: #ffc733; - } -} - -.woocommerce-password-hint { - margin: 0.5em 0 0; - display: block; -} - -/** - * Twenty Eleven specific styles - */ -#content.twentyeleven .woocommerce-pagination a { - font-size: 1em; - line-height: 1; -} - -/** - * Twenty Thirteen specific styles - */ -.single-product .twentythirteen { - - .entry-summary, - #reply-title, - #respond #commentform { - padding: 0; - } - - p.stars { - clear: both; - } -} - -.twentythirteen .woocommerce-breadcrumb { - padding-top: 40px; -} - -/** - * Twenty Fourteen specific styles - */ -.twentyfourteen ul.products li.product { - margin-top: 0 !important; -} - -/** - * Twenty Sixteen specific styles - */ -body:not(.search-results) .twentysixteen .entry-summary { - color: inherit; - font-size: inherit; - line-height: inherit; -} - -.twentysixteen .price ins { - background: inherit; - color: inherit; -} diff --git a/assets/images/jetpack_horizontal_logo.png b/assets/images/jetpack_horizontal_logo.png deleted file mode 100644 index 0209c0cc16b..00000000000 Binary files a/assets/images/jetpack_horizontal_logo.png and /dev/null differ diff --git a/assets/images/jetpack_vertical_logo.png b/assets/images/jetpack_vertical_logo.png deleted file mode 100644 index 619fabd4525..00000000000 Binary files a/assets/images/jetpack_vertical_logo.png and /dev/null differ diff --git a/assets/images/obw-facebook-icon.svg b/assets/images/obw-facebook-icon.svg deleted file mode 100644 index 3d617d09757..00000000000 --- a/assets/images/obw-facebook-icon.svg +++ /dev/null @@ -1 +0,0 @@ -flogo_RGB_HEX-512 \ No newline at end of file diff --git a/assets/images/obw-mailchimp-icon.svg b/assets/images/obw-mailchimp-icon.svg deleted file mode 100644 index f540158c66a..00000000000 --- a/assets/images/obw-mailchimp-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/images/obw-shipstation-icon.png b/assets/images/obw-shipstation-icon.png deleted file mode 100644 index b546301a945..00000000000 Binary files a/assets/images/obw-shipstation-icon.png and /dev/null differ diff --git a/assets/images/obw-storefront-icon.svg b/assets/images/obw-storefront-icon.svg deleted file mode 100644 index 4617e869942..00000000000 --- a/assets/images/obw-storefront-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/images/obw-taxes-icon.svg b/assets/images/obw-taxes-icon.svg deleted file mode 100644 index d1b0469cf1e..00000000000 --- a/assets/images/obw-taxes-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/images/obw-woocommerce-admin-icon.svg b/assets/images/obw-woocommerce-admin-icon.svg deleted file mode 100644 index c4d75f99552..00000000000 --- a/assets/images/obw-woocommerce-admin-icon.svg +++ /dev/null @@ -1 +0,0 @@ -Asset 5 \ No newline at end of file diff --git a/assets/images/obw-woocommerce-services-icon.png b/assets/images/obw-woocommerce-services-icon.png deleted file mode 100644 index 3ef4c2da453..00000000000 Binary files a/assets/images/obw-woocommerce-services-icon.png and /dev/null differ diff --git a/assets/images/storefront-bg.jpg b/assets/images/storefront-bg.jpg deleted file mode 100644 index 85f51952cf4..00000000000 Binary files a/assets/images/storefront-bg.jpg and /dev/null differ diff --git a/assets/images/storefront-intro.png b/assets/images/storefront-intro.png deleted file mode 100644 index 96c0aec579f..00000000000 Binary files a/assets/images/storefront-intro.png and /dev/null differ diff --git a/assets/images/storefront.png b/assets/images/storefront.png deleted file mode 100644 index 28c22bf4f4a..00000000000 Binary files a/assets/images/storefront.png and /dev/null differ diff --git a/assets/images/wcs-canada-post-logo.jpg b/assets/images/wcs-canada-post-logo.jpg deleted file mode 100644 index 004bbd448d4..00000000000 Binary files a/assets/images/wcs-canada-post-logo.jpg and /dev/null differ diff --git a/assets/images/wcs-extensions-banner-3x.png b/assets/images/wcs-extensions-banner-3x.png deleted file mode 100644 index 0b81938fbd1..00000000000 Binary files a/assets/images/wcs-extensions-banner-3x.png and /dev/null differ diff --git a/assets/images/wcs-notice.png b/assets/images/wcs-notice.png deleted file mode 100644 index 99b604d8e92..00000000000 Binary files a/assets/images/wcs-notice.png and /dev/null differ diff --git a/assets/images/wcs-truck-banner-3x.png b/assets/images/wcs-truck-banner-3x.png deleted file mode 100644 index 3175a5d04d0..00000000000 Binary files a/assets/images/wcs-truck-banner-3x.png and /dev/null differ diff --git a/assets/images/wcs-usps-logo.png b/assets/images/wcs-usps-logo.png deleted file mode 100644 index 04184bf2bbc..00000000000 Binary files a/assets/images/wcs-usps-logo.png and /dev/null differ diff --git a/assets/js/admin/api-keys.js b/assets/js/admin/api-keys.js deleted file mode 100644 index f51a2cf7e0d..00000000000 --- a/assets/js/admin/api-keys.js +++ /dev/null @@ -1,158 +0,0 @@ -/*global jQuery, Backbone, _, woocommerce_admin_api_keys, wcSetClipboard, wcClearClipboard */ -(function( $ ) { - - var APIView = Backbone.View.extend({ - /** - * Element - * - * @param {Object} '#key-fields' - */ - el: $( '#key-fields' ), - - /** - * Events - * - * @type {Object} - */ - events: { - 'click input#update_api_key': 'saveKey' - }, - - /** - * Initialize actions - */ - initialize: function(){ - _.bindAll( this, 'saveKey' ); - }, - - /** - * Init jQuery.BlockUI - */ - block: function() { - $( this.el ).block({ - message: null, - overlayCSS: { - background: '#fff', - opacity: 0.6 - } - }); - }, - - /** - * Remove jQuery.BlockUI - */ - unblock: function() { - $( this.el ).unblock(); - }, - - /** - * Init TipTip - */ - initTipTip: function( css_class ) { - $( document.body ) - .on( 'click', css_class, function( evt ) { - evt.preventDefault(); - if ( ! document.queryCommandSupported( 'copy' ) ) { - $( css_class ).parent().find( 'input' ).focus().select(); - $( '#copy-error' ).text( woocommerce_admin_api_keys.clipboard_failed ); - } else { - $( '#copy-error' ).text( '' ); - wcClearClipboard(); - wcSetClipboard( $( this ).prev( 'input' ).val().trim(), $( css_class ) ); - } - } ) - .on( 'aftercopy', css_class, function() { - $( '#copy-error' ).text( '' ); - $( css_class ).tipTip( { - 'attribute': 'data-tip', - 'activation': 'focus', - 'fadeIn': 50, - 'fadeOut': 50, - 'delay': 0 - } ).focus(); - } ) - .on( 'aftercopyerror', css_class, function() { - $( css_class ).parent().find( 'input' ).focus().select(); - $( '#copy-error' ).text( woocommerce_admin_api_keys.clipboard_failed ); - } ); - }, - - /** - * Create qrcode - * - * @param {string} consumer_key - * @param {string} consumer_secret - */ - createQRCode: function( consumer_key, consumer_secret ) { - $( '#keys-qrcode' ).qrcode({ - text: consumer_key + '|' + consumer_secret, - width: 120, - height: 120 - }); - }, - - /** - * Save API Key using ajax - * - * @param {Object} e - */ - saveKey: function( e ) { - e.preventDefault(); - - var self = this; - - self.block(); - - Backbone.ajax({ - method: 'POST', - dataType: 'json', - url: woocommerce_admin_api_keys.ajax_url, - data: { - action: 'woocommerce_update_api_key', - security: woocommerce_admin_api_keys.update_api_nonce, - key_id: $( '#key_id', self.el ).val(), - description: $( '#key_description', self.el ).val(), - user: $( '#key_user', self.el ).val(), - permissions: $( '#key_permissions', self.el ).val() - }, - success: function( response ) { - $( '.wc-api-message', self.el ).remove(); - - if ( response.success ) { - var data = response.data; - - $( 'h2, h3', self.el ).first().append( '

' + data.message + '

' ); - - if ( 0 < data.consumer_key.length && 0 < data.consumer_secret.length ) { - $( '#api-keys-options', self.el ).remove(); - $( 'p.submit', self.el ).empty().append( data.revoke_url ); - - var template = wp.template( 'api-keys-template' ); - - $( 'p.submit', self.el ).before( template({ - consumer_key: data.consumer_key, - consumer_secret: data.consumer_secret - }) ); - self.createQRCode( data.consumer_key, data.consumer_secret ); - self.initTipTip( '.copy-key' ); - self.initTipTip( '.copy-secret' ); - } else { - $( '#key_description', self.el ).val( data.description ); - $( '#key_user', self.el ).val( data.user_id ); - $( '#key_permissions', self.el ).val( data.permissions ); - } - } else { - $( 'h2, h3', self.el ) - .first() - .append( '

' + response.data.message + '

' ); - } - - self.unblock(); - } - }); - } - }); - - new APIView(); - -})( jQuery ); diff --git a/assets/js/admin/backbone-modal.js b/assets/js/admin/backbone-modal.js deleted file mode 100644 index 63cca56422a..00000000000 --- a/assets/js/admin/backbone-modal.js +++ /dev/null @@ -1,147 +0,0 @@ -/*global jQuery, Backbone, _ */ -( function( $, Backbone, _ ) { - 'use strict'; - - /** - * WooCommerce Backbone Modal plugin - * - * @param {object} options - */ - $.fn.WCBackboneModal = function( options ) { - return this.each( function() { - ( new $.WCBackboneModal( $( this ), options ) ); - }); - }; - - /** - * Initialize the Backbone Modal - * - * @param {object} element [description] - * @param {object} options [description] - */ - $.WCBackboneModal = function( element, options ) { - // Set settings - var settings = $.extend( {}, $.WCBackboneModal.defaultOptions, options ); - - if ( settings.template ) { - new $.WCBackboneModal.View({ - target: settings.template, - string: settings.variable - }); - } - }; - - /** - * Set default options - * - * @type {object} - */ - $.WCBackboneModal.defaultOptions = { - template: '', - variable: {} - }; - - /** - * Create the Backbone Modal - * - * @return {null} - */ - $.WCBackboneModal.View = Backbone.View.extend({ - tagName: 'div', - id: 'wc-backbone-modal-dialog', - _target: undefined, - _string: undefined, - events: { - 'click .modal-close': 'closeButton', - 'click #btn-ok' : 'addButton', - 'touchstart #btn-ok': 'addButton', - 'keydown' : 'keyboardActions' - }, - resizeContent: function() { - var $content = $( '.wc-backbone-modal-content' ).find( 'article' ); - var max_h = $( window ).height() * 0.75; - - $content.css({ - 'max-height': max_h + 'px' - }); - }, - initialize: function( data ) { - var view = this; - this._target = data.target; - this._string = data.string; - _.bindAll( this, 'render' ); - this.render(); - - $( window ).resize(function() { - view.resizeContent(); - }); - }, - render: function() { - var template = wp.template( this._target ); - - this.$el.append( - template( this._string ) - ); - - $( document.body ).css({ - 'overflow': 'hidden' - }).append( this.$el ); - - this.resizeContent(); - this.$( '.wc-backbone-modal-content' ).attr( 'tabindex' , '0' ).focus(); - - $( document.body ).trigger( 'init_tooltips' ); - - $( document.body ).trigger( 'wc_backbone_modal_loaded', this._target ); - }, - closeButton: function( e ) { - e.preventDefault(); - $( document.body ).trigger( 'wc_backbone_modal_before_remove', this._target ); - this.undelegateEvents(); - $( document ).off( 'focusin' ); - $( document.body ).css({ - 'overflow': 'auto' - }); - this.remove(); - $( document.body ).trigger( 'wc_backbone_modal_removed', this._target ); - }, - addButton: function( e ) { - $( document.body ).trigger( 'wc_backbone_modal_response', [ this._target, this.getFormData() ] ); - this.closeButton( e ); - }, - getFormData: function() { - var data = {}; - - $( document.body ).trigger( 'wc_backbone_modal_before_update', this._target ); - - $.each( $( 'form', this.$el ).serializeArray(), function( index, item ) { - if ( item.name.indexOf( '[]' ) !== -1 ) { - item.name = item.name.replace( '[]', '' ); - data[ item.name ] = $.makeArray( data[ item.name ] ); - data[ item.name ].push( item.value ); - } else { - data[ item.name ] = item.value; - } - }); - - return data; - }, - keyboardActions: function( e ) { - var button = e.keyCode || e.which; - - // Enter key - if ( - 13 === button && - ! ( e.target.tagName && ( e.target.tagName.toLowerCase() === 'input' || e.target.tagName.toLowerCase() === 'textarea' ) ) - ) { - this.addButton( e ); - } - - // ESC key - if ( 27 === button ) { - this.closeButton( e ); - } - } - }); - -}( jQuery, Backbone, _ )); diff --git a/assets/js/admin/marketplace-suggestions.js b/assets/js/admin/marketplace-suggestions.js deleted file mode 100644 index 7493d3fedf6..00000000000 --- a/assets/js/admin/marketplace-suggestions.js +++ /dev/null @@ -1,449 +0,0 @@ -/* global marketplace_suggestions, ajaxurl, Cookies */ -( function( $, marketplace_suggestions, ajaxurl ) { - $( function() { - if ( 'undefined' === typeof marketplace_suggestions ) { - return; - } - - // Stand-in wcTracks.recordEvent in case tracks is not available (for any reason). - window.wcTracks = window.wcTracks || {}; - window.wcTracks.recordEvent = window.wcTracks.recordEvent || function() { }; - - // Tracks events sent in this file: - // - marketplace_suggestion_displayed - // - marketplace_suggestion_clicked - // - marketplace_suggestion_dismissed - // All are prefixed by {WC_Tracks::PREFIX}. - // All have one property for `suggestionSlug`, to identify the specific suggestion message. - - // Dismiss the specified suggestion from the UI, and save the dismissal in settings. - function dismissSuggestion( context, product, promoted, url, suggestionSlug ) { - // hide the suggestion in the UI - var selector = '[data-suggestion-slug=' + suggestionSlug + ']'; - $( selector ).fadeOut( function() { - $( this ).remove(); - tidyProductEditMetabox(); - } ); - - // save dismissal in user settings - jQuery.post( - ajaxurl, - { - 'action': 'woocommerce_add_dismissed_marketplace_suggestion', - '_wpnonce': marketplace_suggestions.dismiss_suggestion_nonce, - 'slug': suggestionSlug - } - ); - - // if this is a high-use area, delay new suggestion that area for a short while - var highUseSuggestionContexts = [ 'products-list-inline' ]; - if ( _.contains( highUseSuggestionContexts, context ) ) { - // snooze suggestions in that area for 2 days - var contextSnoozeCookie = 'woocommerce_snooze_suggestions__' + context; - Cookies.set( contextSnoozeCookie, 'true', { expires: 2 } ); - - // keep track of how often this area gets dismissed in a cookie - var contextDismissalCountCookie = 'woocommerce_dismissed_suggestions__' + context; - var previousDismissalsInThisContext = parseInt( Cookies.get( contextDismissalCountCookie ), 10 ) || 0; - Cookies.set( contextDismissalCountCookie, previousDismissalsInThisContext + 1, { expires: 31 } ); - } - - window.wcTracks.recordEvent( 'marketplace_suggestion_dismissed', { - suggestion_slug: suggestionSlug, - context: context, - product: product || '', - promoted: promoted || '', - target: url || '' - } ); - } - - // Render DOM element for suggestion dismiss button. - function renderDismissButton( context, product, promoted, url, suggestionSlug ) { - var dismissButton = document.createElement( 'a' ); - - dismissButton.classList.add( 'suggestion-dismiss' ); - dismissButton.setAttribute( 'title', marketplace_suggestions.i18n_marketplace_suggestions_dismiss_tooltip ); - dismissButton.setAttribute( 'href', '#' ); - dismissButton.onclick = function( event ) { - event.preventDefault(); - dismissSuggestion( context, product, promoted, url, suggestionSlug ); - }; - - return dismissButton; - } - - function addURLParameters( context, url ) { - var urlParams = marketplace_suggestions.in_app_purchase_params; - urlParams.utm_source = 'unknown'; - urlParams.utm_campaign = 'marketplacesuggestions'; - urlParams.utm_medium = 'product'; - - var sourceContextMap = { - 'productstable': [ - 'products-list-inline' - ], - 'productsempty': [ - 'products-list-empty-header', - 'products-list-empty-footer', - 'products-list-empty-body' - ], - 'ordersempty': [ - 'orders-list-empty-header', - 'orders-list-empty-footer', - 'orders-list-empty-body' - ], - 'editproduct': [ - 'product-edit-meta-tab-header', - 'product-edit-meta-tab-footer', - 'product-edit-meta-tab-body' - ] - }; - var utmSource = _.findKey( sourceContextMap, function( sourceInfo ) { - return _.contains( sourceInfo, context ); - } ); - if ( utmSource ) { - urlParams.utm_source = utmSource; - } - - return url + '?' + jQuery.param( urlParams ); - } - - // Render DOM element for suggestion linkout, optionally with button style. - function renderLinkout( context, product, promoted, slug, url, text, isButton ) { - var linkoutButton = document.createElement( 'a' ); - - var utmUrl = addURLParameters( context, url ); - linkoutButton.setAttribute( 'href', utmUrl ); - - // By default, CTA links should open in same tab (and feel integrated with Woo). - // Exception: when editing products, use new tab. User may have product edits - // that need to be saved. - var newTabContexts = [ - 'product-edit-meta-tab-header', - 'product-edit-meta-tab-footer', - 'product-edit-meta-tab-body' - ]; - if ( _.includes( newTabContexts, context ) ) { - linkoutButton.setAttribute( 'target', 'blank' ); - } - - linkoutButton.textContent = text; - - linkoutButton.onclick = function() { - window.wcTracks.recordEvent( 'marketplace_suggestion_clicked', { - suggestion_slug: slug, - context: context, - product: product || '', - promoted: promoted || '', - target: url || '' - } ); - }; - - if ( isButton ) { - linkoutButton.classList.add( 'button' ); - } else { - linkoutButton.classList.add( 'linkout' ); - var linkoutIcon = document.createElement( 'span' ); - linkoutIcon.classList.add( 'dashicons', 'dashicons-external' ); - linkoutButton.appendChild( linkoutIcon ); - } - - return linkoutButton; - } - - // Render DOM element for suggestion icon image. - function renderSuggestionIcon( iconUrl ) { - if ( ! iconUrl ) { - return null; - } - - var image = document.createElement( 'img' ); - image.src = iconUrl; - image.classList.add( 'marketplace-suggestion-icon' ); - - return image; - } - - // Render DOM elements for suggestion content. - function renderSuggestionContent( slug, title, copy ) { - var container = document.createElement( 'div' ); - - container.classList.add( 'marketplace-suggestion-container-content' ); - - if ( title ) { - var titleHeading = document.createElement( 'h4' ); - titleHeading.textContent = title; - container.appendChild( titleHeading ); - } - - if ( copy ) { - var body = document.createElement( 'p' ); - body.textContent = copy; - container.appendChild( body ); - } - - // Conditionally add in a Manage suggestions link to product edit - // metabox footer (based on suggestion slug). - var slugsWithManage = [ - 'product-edit-empty-footer-browse-all', - 'product-edit-meta-tab-footer-browse-all' - ]; - if ( -1 !== slugsWithManage.indexOf( slug ) ) { - container.classList.add( 'has-manage-link' ); - - var manageSuggestionsLink = document.createElement( 'a' ); - manageSuggestionsLink.classList.add( 'marketplace-suggestion-manage-link', 'linkout' ); - manageSuggestionsLink.setAttribute( - 'href', - marketplace_suggestions.manage_suggestions_url - ); - manageSuggestionsLink.textContent = marketplace_suggestions.i18n_marketplace_suggestions_manage_suggestions; - - container.appendChild( manageSuggestionsLink ); - } - - return container; - } - - // Render DOM elements for suggestion call-to-action – button or link with dismiss 'x'. - function renderSuggestionCTA( context, product, promoted, slug, url, linkText, linkIsButton, allowDismiss ) { - var container = document.createElement( 'div' ); - - if ( ! linkText ) { - linkText = marketplace_suggestions.i18n_marketplace_suggestions_default_cta; - } - - container.classList.add( 'marketplace-suggestion-container-cta' ); - if ( url && linkText ) { - var linkoutElement = renderLinkout( context, product, promoted, slug, url, linkText, linkIsButton ); - container.appendChild( linkoutElement ); - } - - if ( allowDismiss ) { - container.appendChild( renderDismissButton( context, product, promoted, url, slug ) ); - } - - return container; - } - - // Render a "list item" style suggestion. - // These are used in onboarding style contexts, e.g. products list empty state. - function renderListItem( context, product, promoted, slug, iconUrl, title, copy, url, linkText, linkIsButton, allowDismiss ) { - var container = document.createElement( 'div' ); - container.classList.add( 'marketplace-suggestion-container' ); - container.dataset.suggestionSlug = slug; - - var icon = renderSuggestionIcon( iconUrl ); - if ( icon ) { - container.appendChild( icon ); - } - container.appendChild( - renderSuggestionContent( slug, title, copy ) - ); - container.appendChild( - renderSuggestionCTA( context, product, promoted, slug, url, linkText, linkIsButton, allowDismiss ) - ); - - return container; - } - - // Filter suggestion data to remove less-relevant suggestions. - function getRelevantPromotions( marketplaceSuggestionsApiData, displayContext ) { - // select based on display context - var promos = _.filter( marketplaceSuggestionsApiData, function( promo ) { - if ( _.isArray( promo.context ) ) { - return _.contains( promo.context, displayContext ); - } - return ( displayContext === promo.context ); - } ); - - // hide promos the user has dismissed - promos = _.filter( promos, function( promo ) { - return ! _.contains( marketplace_suggestions.dismissed_suggestions, promo.slug ); - } ); - - // hide promos for things the user already has installed - promos = _.filter( promos, function( promo ) { - return ! _.contains( marketplace_suggestions.active_plugins, promo.product ); - } ); - - // hide promos that are not applicable based on user's installed extensions - promos = _.filter( promos, function( promo ) { - if ( ! promo['show-if-active'] ) { - // this promotion is relevant to all - return true; - } - - // if the user has any of the prerequisites, show the promo - return ( _.intersection( marketplace_suggestions.active_plugins, promo['show-if-active'] ).length > 0 ); - } ); - - return promos; - } - - // Show and hide page elements dependent on suggestion state. - function hidePageElementsForSuggestionState( usedSuggestionsContexts ) { - var showingEmptyStateSuggestions = _.intersection( - usedSuggestionsContexts, - [ 'products-list-empty-body', 'orders-list-empty-body' ] - ).length > 0; - - // Streamline onboarding UI if we're in 'empty state' welcome mode. - if ( showingEmptyStateSuggestions ) { - $( '#screen-meta-links' ).hide(); - $( '#wpfooter' ).hide(); - } - - // Hide the header & footer, they don't make sense without specific promotion content - if ( ! showingEmptyStateSuggestions ) { - $( '.marketplace-suggestions-container[data-marketplace-suggestions-context="products-list-empty-header"]' ).hide(); - $( '.marketplace-suggestions-container[data-marketplace-suggestions-context="products-list-empty-footer"]' ).hide(); - $( '.marketplace-suggestions-container[data-marketplace-suggestions-context="orders-list-empty-header"]' ).hide(); - $( '.marketplace-suggestions-container[data-marketplace-suggestions-context="orders-list-empty-footer"]' ).hide(); - } - } - - // Streamline the product edit suggestions tab dependent on what's visible. - function tidyProductEditMetabox() { - var productMetaboxSuggestions = $( - '.marketplace-suggestions-container[data-marketplace-suggestions-context="product-edit-meta-tab-body"]' - ).children(); - if ( 0 >= productMetaboxSuggestions.length ) { - var metaboxSuggestionsUISelector = - '.marketplace-suggestions-container[data-marketplace-suggestions-context="product-edit-meta-tab-body"]'; - metaboxSuggestionsUISelector += - ', .marketplace-suggestions-container[data-marketplace-suggestions-context="product-edit-meta-tab-header"]'; - metaboxSuggestionsUISelector += - ', .marketplace-suggestions-container[data-marketplace-suggestions-context="product-edit-meta-tab-footer"]'; - $( metaboxSuggestionsUISelector ).fadeOut( { - complete: function() { - $( '.marketplace-suggestions-metabox-nosuggestions-placeholder' ).fadeIn(); - } - } ); - - } - } - - function addManageSuggestionsTracksHandler() { - $( 'a.marketplace-suggestion-manage-link' ).on( 'click', function() { - window.wcTracks.recordEvent( 'marketplace_suggestions_manage_clicked' ); - } ); - } - - function isContextHiddenOnPageLoad( context ) { - // Some suggestions are not visible on page load; - // e.g. the user reveals them by selecting a tab. - var revealableSuggestionsContexts = [ - 'product-edit-meta-tab-header', - 'product-edit-meta-tab-body', - 'product-edit-meta-tab-footer' - ]; - return _.includes( revealableSuggestionsContexts, context ); - } - - // track the current product data tab to avoid over-tracking suggestions - var currentTab = false; - - // Render suggestion data in appropriate places in UI. - function displaySuggestions( marketplaceSuggestionsApiData ) { - var usedSuggestionsContexts = []; - - // iterate over all suggestions containers, rendering promos - $( '.marketplace-suggestions-container' ).each( function() { - // determine the context / placement we're populating - var context = this.dataset.marketplaceSuggestionsContext; - - // find promotions that target this context - var promos = getRelevantPromotions( marketplaceSuggestionsApiData, context ); - - // shuffle/randomly select five suggestions to display - var suggestionsToDisplay = _.sample( promos, 5 ); - - // render the promo content - for ( var i in suggestionsToDisplay ) { - - var linkText = suggestionsToDisplay[ i ]['link-text']; - var linkoutIsButton = true; - if ( suggestionsToDisplay[ i ]['link-text'] ) { - linkText = suggestionsToDisplay[ i ]['link-text']; - linkoutIsButton = false; - } - - // dismiss is allowed by default - var allowDismiss = true; - if ( suggestionsToDisplay[ i ]['allow-dismiss'] === false ) { - allowDismiss = false; - } - - var content = renderListItem( - context, - suggestionsToDisplay[ i ].product, - suggestionsToDisplay[ i ].promoted, - suggestionsToDisplay[ i ].slug, - suggestionsToDisplay[ i ].icon, - suggestionsToDisplay[ i ].title, - suggestionsToDisplay[ i ].copy, - suggestionsToDisplay[ i ].url, - linkText, - linkoutIsButton, - allowDismiss - ); - $( this ).append( content ); - $( this ).addClass( 'showing-suggestion' ); - usedSuggestionsContexts.push( context ); - - if ( ! isContextHiddenOnPageLoad( context ) ) { - // Fire 'displayed' tracks events for immediately visible suggestions. - window.wcTracks.recordEvent( 'marketplace_suggestion_displayed', { - suggestion_slug: suggestionsToDisplay[ i ].slug, - context: context, - product: suggestionsToDisplay[ i ].product || '', - promoted: suggestionsToDisplay[ i ].promoted || '', - target: suggestionsToDisplay[ i ].url || '' - } ); - } - } - - // Track when suggestions are displayed (and not already visible). - $( 'ul.product_data_tabs li.marketplace-suggestions_options a' ).on( 'click', function( e ) { - e.preventDefault(); - - if ( '#marketplace_suggestions' === currentTab ) { - return; - } - - if ( ! isContextHiddenOnPageLoad( context ) ) { - // We've already fired 'displayed' event above. - return; - } - - for ( var i in suggestionsToDisplay ) { - window.wcTracks.recordEvent( 'marketplace_suggestion_displayed', { - suggestion_slug: suggestionsToDisplay[ i ].slug, - context: context, - product: suggestionsToDisplay[ i ].product || '', - promoted: suggestionsToDisplay[ i ].promoted || '', - target: suggestionsToDisplay[ i ].url || '' - } ); - } - } ); - } ); - - hidePageElementsForSuggestionState( usedSuggestionsContexts ); - tidyProductEditMetabox(); - } - - if ( marketplace_suggestions.suggestions_data ) { - displaySuggestions( marketplace_suggestions.suggestions_data ); - - // track the current product data tab to avoid over-reporting suggestion views - $( 'ul.product_data_tabs' ).on( 'click', 'li a', function( e ) { - e.preventDefault(); - currentTab = $( this ).attr( 'href' ); - } ); - } - - addManageSuggestionsTracksHandler(); - }); - -})( jQuery, marketplace_suggestions, ajaxurl ); diff --git a/assets/js/admin/meta-boxes-coupon.js b/assets/js/admin/meta-boxes-coupon.js deleted file mode 100644 index c4327f33644..00000000000 --- a/assets/js/admin/meta-boxes-coupon.js +++ /dev/null @@ -1,70 +0,0 @@ -/* global woocommerce_admin_meta_boxes_coupon */ -jQuery(function( $ ) { - - /** - * Coupon actions - */ - var wc_meta_boxes_coupon_actions = { - - /** - * Initialize variations actions - */ - init: function() { - $( 'select#discount_type' ) - .on( 'change', this.type_options ) - .trigger( 'change' ); - - this.insert_generate_coupon_code_button(); - $( '.button.generate-coupon-code' ).on( 'click', this.generate_coupon_code ); - }, - - /** - * Show/hide fields by coupon type options - */ - type_options: function() { - // Get value - var select_val = $( this ).val(); - - if ( 'percent' === select_val ) { - $( '#coupon_amount' ).removeClass( 'wc_input_price' ).addClass( 'wc_input_decimal' ); - } else { - $( '#coupon_amount' ).removeClass( 'wc_input_decimal' ).addClass( 'wc_input_price' ); - } - - if ( select_val !== 'fixed_cart' ) { - $( '.limit_usage_to_x_items_field' ).show(); - } else { - $( '.limit_usage_to_x_items_field' ).hide(); - } - }, - - /** - * Insert generate coupon code buttom HTML. - */ - insert_generate_coupon_code_button: function() { - $( '.post-type-shop_coupon' ).find( '#title' ).after( - '' + woocommerce_admin_meta_boxes_coupon.generate_button_text + '' - ); - }, - - /** - * Generate a random coupon code - */ - generate_coupon_code: function( e ) { - e.preventDefault(); - var $coupon_code_field = $( '#title' ), - $coupon_code_label = $( '#title-prompt-text' ), - $result = ''; - for ( var i = 0; i < woocommerce_admin_meta_boxes_coupon.char_length; i++ ) { - $result += woocommerce_admin_meta_boxes_coupon.characters.charAt( - Math.floor( Math.random() * woocommerce_admin_meta_boxes_coupon.characters.length ) - ); - } - $result = woocommerce_admin_meta_boxes_coupon.prefix + $result + woocommerce_admin_meta_boxes_coupon.suffix; - $coupon_code_field.focus().val( $result ); - $coupon_code_label.addClass( 'screen-reader-text' ); - } - }; - - wc_meta_boxes_coupon_actions.init(); -}); diff --git a/assets/js/admin/meta-boxes-order.js b/assets/js/admin/meta-boxes-order.js deleted file mode 100644 index 2267afe26ed..00000000000 --- a/assets/js/admin/meta-boxes-order.js +++ /dev/null @@ -1,1494 +0,0 @@ -// eslint-disable-next-line max-len -/*global woocommerce_admin_meta_boxes, woocommerce_admin, accounting, woocommerce_admin_meta_boxes_order, wcSetClipboard, wcClearClipboard */ -jQuery( function ( $ ) { - - // Stand-in wcTracks.recordEvent in case tracks is not available (for any reason). - window.wcTracks = window.wcTracks || {}; - window.wcTracks.recordEvent = window.wcTracks.recordEvent || function() { }; - - /** - * Order Data Panel - */ - var wc_meta_boxes_order = { - states: null, - init: function() { - if ( - ! ( - typeof woocommerce_admin_meta_boxes_order === 'undefined' || - typeof woocommerce_admin_meta_boxes_order.countries === 'undefined' - ) - ) { - /* State/Country select boxes */ - this.states = JSON.parse( woocommerce_admin_meta_boxes_order.countries.replace( /"/g, '"' ) ); - } - - $( '.js_field-country' ).selectWoo().change( this.change_country ); - $( '.js_field-country' ).trigger( 'change', [ true ] ); - $( document.body ).on( 'change', 'select.js_field-state', this.change_state ); - $( '#woocommerce-order-actions input, #woocommerce-order-actions a' ).on( 'click', function() { - window.onbeforeunload = ''; - }); - $( 'a.edit_address' ).on( 'click', this.edit_address ); - $( 'a.billing-same-as-shipping' ).on( 'click', this.copy_billing_to_shipping ); - $( 'a.load_customer_billing' ).on( 'click', this.load_billing ); - $( 'a.load_customer_shipping' ).on( 'click', this.load_shipping ); - $( '#customer_user' ).on( 'change', this.change_customer_user ); - }, - - change_country: function( e, stickValue ) { - // Check for stickValue before using it - if ( typeof stickValue === 'undefined' ){ - stickValue = false; - } - - // Prevent if we don't have the metabox data - if ( wc_meta_boxes_order.states === null ){ - return; - } - - var $this = $( this ), - country = $this.val(), - $state = $this.parents( 'div.edit_address' ).find( ':input.js_field-state' ), - $parent = $state.parent(), - stateValue = $state.val(), - input_name = $state.attr( 'name' ), - input_id = $state.attr( 'id' ), - value = $this.data( 'woocommerce.stickState-' + country ) ? $this.data( 'woocommerce.stickState-' + country ) : stateValue, - placeholder = $state.attr( 'placeholder' ), - $newstate; - - if ( stickValue ){ - $this.data( 'woocommerce.stickState-' + country, value ); - } - - // Remove the previous DOM element - $parent.show().find( '.select2-container' ).remove(); - - if ( ! $.isEmptyObject( wc_meta_boxes_order.states[ country ] ) ) { - var state = wc_meta_boxes_order.states[ country ], - $defaultOption = $( '' ) - .text( woocommerce_admin_meta_boxes_order.i18n_select_state_text ); - - $newstate = $( '' ) - .prop( 'id', input_id ) - .prop( 'name', input_name ) - .prop( 'placeholder', placeholder ) - .addClass( 'js_field-state select short' ) - .append( $defaultOption ); - - $.each( state, function( index ) { - var $option = $( '' ) - .prop( 'value', index ) - .text( state[ index ] ); - if ( index === stateValue ) { - $option.prop( 'selected' ); - } - $newstate.append( $option ); - } ); - - $newstate.val( value ); - - $state.replaceWith( $newstate ); - - $newstate.show().selectWoo().hide().trigger( 'change' ); - } else { - $newstate = $( '' ) - .prop( 'id', input_id ) - .prop( 'name', input_name ) - .prop( 'placeholder', placeholder ) - .addClass( 'js_field-state' ) - .val( stateValue ); - $state.replaceWith( $newstate ); - } - - // This event has a typo - deprecated in 2.5.0 - $( document.body ).trigger( 'contry-change.woocommerce', [country, $( this ).closest( 'div' )] ); - $( document.body ).trigger( 'country-change.woocommerce', [country, $( this ).closest( 'div' )] ); - }, - - change_state: function() { - // Here we will find if state value on a select has changed and stick it to the country data - var $this = $( this ), - state = $this.val(), - $country = $this.parents( 'div.edit_address' ).find( ':input.js_field-country' ), - country = $country.val(); - - $country.data( 'woocommerce.stickState-' + country, state ); - }, - - init_tiptip: function() { - $( '#tiptip_holder' ).removeAttr( 'style' ); - $( '#tiptip_arrow' ).removeAttr( 'style' ); - $( '.tips' ).tipTip({ - 'attribute': 'data-tip', - 'fadeIn': 50, - 'fadeOut': 50, - 'delay': 200, - 'keepAlive': true - }); - }, - - edit_address: function( e ) { - e.preventDefault(); - - var $this = $( this ), - $wrapper = $this.closest( '.order_data_column' ), - $edit_address = $wrapper.find( 'div.edit_address' ), - $address = $wrapper.find( 'div.address' ), - $country_input = $edit_address.find( '.js_field-country' ), - $state_input = $edit_address.find( '.js_field-state' ), - is_billing = Boolean( $edit_address.find( 'input[name^="_billing_"]' ).length ); - - $address.hide(); - $this.parent().find( 'a' ).toggle(); - - if ( ! $country_input.val() ) { - $country_input.val( woocommerce_admin_meta_boxes_order.default_country ).trigger( 'change' ); - $state_input.val( woocommerce_admin_meta_boxes_order.default_state ).trigger( 'change' ); - } - - $edit_address.show(); - - var event_name = is_billing ? 'order_edit_billing_address_click' : 'order_edit_shipping_address_click'; - window.wcTracks.recordEvent( event_name, { - order_id: woocommerce_admin_meta_boxes.post_id, - status: $( '#order_status' ).val() - } ); - }, - - change_customer_user: function() { - if ( ! $( '#_billing_country' ).val() ) { - $( 'a.edit_address' ).trigger( 'click' ); - wc_meta_boxes_order.load_billing( true ); - wc_meta_boxes_order.load_shipping( true ); - } - }, - - load_billing: function( force ) { - if ( true === force || window.confirm( woocommerce_admin_meta_boxes.load_billing ) ) { - - // Get user ID to load data for - var user_id = $( '#customer_user' ).val(); - - if ( ! user_id ) { - window.alert( woocommerce_admin_meta_boxes.no_customer_selected ); - return false; - } - - var data = { - user_id : user_id, - action : 'woocommerce_get_customer_details', - security: woocommerce_admin_meta_boxes.get_customer_details_nonce - }; - - $( this ).closest( 'div.edit_address' ).block({ - message: null, - overlayCSS: { - background: '#fff', - opacity: 0.6 - } - }); - - $.ajax({ - url: woocommerce_admin_meta_boxes.ajax_url, - data: data, - type: 'POST', - success: function( response ) { - if ( response && response.billing ) { - $.each( response.billing, function( key, data ) { - $( ':input#_billing_' + key ).val( data ).trigger( 'change' ); - }); - } - $( 'div.edit_address' ).unblock(); - } - }); - } - return false; - }, - - load_shipping: function( force ) { - if ( true === force || window.confirm( woocommerce_admin_meta_boxes.load_shipping ) ) { - - // Get user ID to load data for - var user_id = $( '#customer_user' ).val(); - - if ( ! user_id ) { - window.alert( woocommerce_admin_meta_boxes.no_customer_selected ); - return false; - } - - var data = { - user_id: user_id, - action: 'woocommerce_get_customer_details', - security: woocommerce_admin_meta_boxes.get_customer_details_nonce - }; - - $( this ).closest( 'div.edit_address' ).block({ - message: null, - overlayCSS: { - background: '#fff', - opacity: 0.6 - } - }); - - $.ajax({ - url: woocommerce_admin_meta_boxes.ajax_url, - data: data, - type: 'POST', - success: function( response ) { - if ( response && response.billing ) { - $.each( response.shipping, function( key, data ) { - $( ':input#_shipping_' + key ).val( data ).trigger( 'change' ); - }); - } - $( 'div.edit_address' ).unblock(); - } - }); - } - return false; - }, - - copy_billing_to_shipping: function() { - if ( window.confirm( woocommerce_admin_meta_boxes.copy_billing ) ) { - $('.order_data_column :input[name^="_billing_"]').each( function() { - var input_name = $(this).attr('name'); - input_name = input_name.replace( '_billing_', '_shipping_' ); - $( ':input#' + input_name ).val( $(this).val() ).trigger( 'change' ); - }); - } - return false; - } - }; - - /** - * Order Items Panel - */ - var wc_meta_boxes_order_items = { - init: function() { - this.stupidtable.init(); - - $( '#woocommerce-order-items' ) - .on( 'click', 'button.add-line-item', this.add_line_item ) - .on( 'click', 'button.add-coupon', this.add_coupon ) - .on( 'click', 'a.remove-coupon', this.remove_coupon ) - .on( 'click', 'button.refund-items', this.refund_items ) - .on( 'click', '.cancel-action', this.cancel ) - .on( 'click', '.refund-actions .cancel-action', this.track_cancel ) - .on( 'click', 'button.add-order-item', this.add_item ) - .on( 'click', 'button.add-order-fee', this.add_fee ) - .on( 'click', 'button.add-order-shipping', this.add_shipping ) - .on( 'click', 'button.add-order-tax', this.add_tax ) - .on( 'click', 'button.save-action', this.save_line_items ) - .on( 'click', 'a.delete-order-tax', this.delete_tax ) - .on( 'click', 'button.calculate-action', this.recalculate ) - .on( 'click', 'a.edit-order-item', this.edit_item ) - .on( 'click', 'a.delete-order-item', this.delete_item ) - - // Refunds - .on( 'click', '.delete_refund', this.refunds.delete_refund ) - .on( 'click', 'button.do-api-refund, button.do-manual-refund', this.refunds.do_refund ) - .on( 'change', '.refund input.refund_line_total, .refund input.refund_line_tax', this.refunds.input_changed ) - .on( 'change keyup', '.wc-order-refund-items #refund_amount', this.refunds.amount_changed ) - .on( 'change', 'input.refund_order_item_qty', this.refunds.refund_quantity_changed ) - - // Qty - .on( 'change', 'input.quantity', this.quantity_changed ) - - // Subtotal/total - .on( 'keyup change', '.split-input :input', function() { - var $subtotal = $( this ).parent().prev().find(':input'); - if ( $subtotal && ( $subtotal.val() === '' || $subtotal.is( '.match-total' ) ) ) { - $subtotal.val( $( this ).val() ).addClass( 'match-total' ); - } - }) - - .on( 'keyup', '.split-input :input', function() { - $( this ).removeClass( 'match-total' ); - }) - - // Meta - .on( 'click', 'button.add_order_item_meta', this.item_meta.add ) - .on( 'click', 'button.remove_order_item_meta', this.item_meta.remove ) - - // Reload items - .on( 'wc_order_items_reload', this.reload_items ) - .on( 'wc_order_items_reloaded', this.reloaded_items ); - - $( document.body ) - .on( 'wc_backbone_modal_loaded', this.backbone.init ) - .on( 'wc_backbone_modal_response', this.backbone.response ); - }, - - block: function() { - $( '#woocommerce-order-items' ).block({ - message: null, - overlayCSS: { - background: '#fff', - opacity: 0.6 - } - }); - }, - - unblock: function() { - $( '#woocommerce-order-items' ).unblock(); - }, - - reload_items: function() { - var data = { - order_id: woocommerce_admin_meta_boxes.post_id, - action: 'woocommerce_load_order_items', - security: woocommerce_admin_meta_boxes.order_item_nonce - }; - - wc_meta_boxes_order_items.block(); - - $.ajax({ - url: woocommerce_admin_meta_boxes.ajax_url, - data: data, - type: 'POST', - success: function( response ) { - $( '#woocommerce-order-items' ).find( '.inside' ).empty(); - $( '#woocommerce-order-items' ).find( '.inside' ).append( response ); - wc_meta_boxes_order_items.reloaded_items(); - wc_meta_boxes_order_items.unblock(); - } - }); - }, - - reloaded_items: function() { - wc_meta_boxes_order.init_tiptip(); - wc_meta_boxes_order_items.stupidtable.init(); - }, - - // When the qty is changed, increase or decrease costs - quantity_changed: function() { - var $row = $( this ).closest( 'tr.item' ); - var qty = $( this ).val(); - var o_qty = $( this ).attr( 'data-qty' ); - var line_total = $( 'input.line_total', $row ); - var line_subtotal = $( 'input.line_subtotal', $row ); - - // Totals - var unit_total = accounting.unformat( line_total.attr( 'data-total' ), woocommerce_admin.mon_decimal_point ) / o_qty; - line_total.val( - parseFloat( accounting.formatNumber( unit_total * qty, woocommerce_admin_meta_boxes.rounding_precision, '' ) ) - .toString() - .replace( '.', woocommerce_admin.mon_decimal_point ) - ); - - var unit_subtotal = accounting.unformat( line_subtotal.attr( 'data-subtotal' ), woocommerce_admin.mon_decimal_point ) / o_qty; - line_subtotal.val( - parseFloat( accounting.formatNumber( unit_subtotal * qty, woocommerce_admin_meta_boxes.rounding_precision, '' ) ) - .toString() - .replace( '.', woocommerce_admin.mon_decimal_point ) - ); - - // Taxes - $( 'input.line_tax', $row ).each( function() { - var $line_total_tax = $( this ); - var tax_id = $line_total_tax.data( 'tax_id' ); - var unit_total_tax = accounting.unformat( - $line_total_tax.attr( 'data-total_tax' ), - woocommerce_admin.mon_decimal_point - ) / o_qty; - var $line_subtotal_tax = $( 'input.line_subtotal_tax[data-tax_id="' + tax_id + '"]', $row ); - var unit_subtotal_tax = accounting.unformat( - $line_subtotal_tax.attr( 'data-subtotal_tax' ), - woocommerce_admin.mon_decimal_point - ) / o_qty; - var round_at_subtotal = 'yes' === woocommerce_admin_meta_boxes.round_at_subtotal; - var precision = woocommerce_admin_meta_boxes[ - round_at_subtotal ? 'rounding_precision' : 'currency_format_num_decimals' - ]; - - if ( 0 < unit_total_tax ) { - $line_total_tax.val( - parseFloat( accounting.formatNumber( unit_total_tax * qty, precision, '' ) ) - .toString() - .replace( '.', woocommerce_admin.mon_decimal_point ) - ); - } - - if ( 0 < unit_subtotal_tax ) { - $line_subtotal_tax.val( - parseFloat( accounting.formatNumber( unit_subtotal_tax * qty, precision, '' ) ) - .toString() - .replace( '.', woocommerce_admin.mon_decimal_point ) - ); - } - }); - - $( this ).trigger( 'quantity_changed' ); - }, - - add_line_item: function() { - $( 'div.wc-order-add-item' ).slideDown(); - $( 'div.wc-order-data-row-toggle' ).not( 'div.wc-order-add-item' ).slideUp(); - - window.wcTracks.recordEvent( 'order_edit_add_items_click', { - order_id: woocommerce_admin_meta_boxes.post_id, - status: $( '#order_status' ).val() - } ); - - return false; - }, - - add_coupon: function() { - window.wcTracks.recordEvent( 'order_edit_add_coupon_click', { - order_id: woocommerce_admin_meta_boxes.post_id, - status: $( '#order_status' ).val() - } ); - - var value = window.prompt( woocommerce_admin_meta_boxes.i18n_apply_coupon ); - - if ( null == value ) { - window.wcTracks.recordEvent( 'order_edit_add_coupon_cancel', { - order_id: woocommerce_admin_meta_boxes.post_id, - status: $( '#order_status' ).val() - } ); - } else { - wc_meta_boxes_order_items.block(); - - var user_id = $( '#customer_user' ).val(); - var user_email = $( '#_billing_email' ).val(); - - var data = $.extend( {}, wc_meta_boxes_order_items.get_taxable_address(), { - action : 'woocommerce_add_coupon_discount', - dataType : 'json', - order_id : woocommerce_admin_meta_boxes.post_id, - security : woocommerce_admin_meta_boxes.order_item_nonce, - coupon : value, - user_id : user_id, - user_email : user_email - } ); - - $.ajax( { - url: woocommerce_admin_meta_boxes.ajax_url, - data: data, - type: 'POST', - success: function( response ) { - if ( response.success ) { - $( '#woocommerce-order-items' ).find( '.inside' ).empty(); - $( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html ); - wc_meta_boxes_order_items.reloaded_items(); - wc_meta_boxes_order_items.unblock(); - } else { - window.alert( response.data.error ); - } - wc_meta_boxes_order_items.unblock(); - }, - complete: function() { - window.wcTracks.recordEvent( 'order_edit_added_coupon', { - order_id: data.order_id, - status: $( '#order_status' ).val() - } ); - } - } ); - } - return false; - }, - - remove_coupon: function() { - var $this = $( this ); - wc_meta_boxes_order_items.block(); - - var data = $.extend( {}, wc_meta_boxes_order_items.get_taxable_address(), { - action : 'woocommerce_remove_order_coupon', - dataType : 'json', - order_id : woocommerce_admin_meta_boxes.post_id, - security : woocommerce_admin_meta_boxes.order_item_nonce, - coupon : $this.data( 'code' ) - } ); - - $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { - if ( response.success ) { - $( '#woocommerce-order-items' ).find( '.inside' ).empty(); - $( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html ); - wc_meta_boxes_order_items.reloaded_items(); - wc_meta_boxes_order_items.unblock(); - } else { - window.alert( response.data.error ); - } - wc_meta_boxes_order_items.unblock(); - }); - }, - - refund_items: function() { - $( 'div.wc-order-refund-items' ).slideDown(); - $( 'div.wc-order-data-row-toggle' ).not( 'div.wc-order-refund-items' ).slideUp(); - $( 'div.wc-order-totals-items' ).slideUp(); - $( '#woocommerce-order-items' ).find( 'div.refund' ).show(); - $( '.wc-order-edit-line-item .wc-order-edit-line-item-actions' ).hide(); - - window.wcTracks.recordEvent( 'order_edit_refund_button_click', { - order_id: woocommerce_admin_meta_boxes.post_id, - status: $( '#order_status' ).val() - } ); - - return false; - }, - - cancel: function() { - $( 'div.wc-order-data-row-toggle' ).not( 'div.wc-order-bulk-actions' ).slideUp(); - $( 'div.wc-order-bulk-actions' ).slideDown(); - $( 'div.wc-order-totals-items' ).slideDown(); - $( '#woocommerce-order-items' ).find( 'div.refund' ).hide(); - $( '.wc-order-edit-line-item .wc-order-edit-line-item-actions' ).show(); - - // Reload the items - if ( 'true' === $( this ).attr( 'data-reload' ) ) { - wc_meta_boxes_order_items.reload_items(); - } - - window.wcTracks.recordEvent( 'order_edit_add_items_cancelled', { - order_id: woocommerce_admin_meta_boxes.post_id, - status: $( '#order_status' ).val() - } ); - - return false; - }, - - track_cancel: function() { - window.wcTracks.recordEvent( 'order_edit_refund_cancel', { - order_id: woocommerce_admin_meta_boxes.post_id, - status: $( '#order_status' ).val() - } ); - }, - - add_item: function() { - $( this ).WCBackboneModal({ - template: 'wc-modal-add-products' - }); - - return false; - }, - - add_fee: function() { - window.wcTracks.recordEvent( 'order_edit_add_fee_click', { - order_id: woocommerce_admin_meta_boxes.post_id, - status: $( '#order_status' ).val() - } ); - - var value = window.prompt( woocommerce_admin_meta_boxes.i18n_add_fee ); - - if ( null == value ) { - window.wcTracks.recordEvent( 'order_edit_add_fee_cancel', { - order_id: woocommerce_admin_meta_boxes.post_id, - status: $( '#order_status' ).val() - } ); - } else { - wc_meta_boxes_order_items.block(); - - var data = $.extend( {}, wc_meta_boxes_order_items.get_taxable_address(), { - action : 'woocommerce_add_order_fee', - dataType: 'json', - order_id: woocommerce_admin_meta_boxes.post_id, - security: woocommerce_admin_meta_boxes.order_item_nonce, - amount : value - } ); - - $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { - if ( response.success ) { - $( '#woocommerce-order-items' ).find( '.inside' ).empty(); - $( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html ); - wc_meta_boxes_order_items.reloaded_items(); - wc_meta_boxes_order_items.unblock(); - window.wcTracks.recordEvent( 'order_edit_added_fee', { - order_id: data.post_id, - status: $( '#order_status' ).val() - } ); - } else { - window.alert( response.data.error ); - } - wc_meta_boxes_order_items.unblock(); - }); - } - return false; - }, - - add_shipping: function() { - wc_meta_boxes_order_items.block(); - - var data = { - action : 'woocommerce_add_order_shipping', - order_id : woocommerce_admin_meta_boxes.post_id, - security : woocommerce_admin_meta_boxes.order_item_nonce, - dataType : 'json' - }; - - $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { - if ( response.success ) { - $( 'table.woocommerce_order_items tbody#order_shipping_line_items' ).append( response.data.html ); - window.wcTracks.recordEvent( 'order_edit_add_shipping', { - order_id: data.post_id, - status: $( '#order_status' ).val() - } ); - } else { - window.alert( response.data.error ); - } - wc_meta_boxes_order_items.unblock(); - }); - - return false; - }, - - add_tax: function() { - $( this ).WCBackboneModal({ - template: 'wc-modal-add-tax' - }); - return false; - }, - - edit_item: function() { - $( this ).closest( 'tr' ).find( '.view' ).hide(); - $( this ).closest( 'tr' ).find( '.edit' ).show(); - $( this ).hide(); - $( 'button.add-line-item' ).trigger( 'click' ); - $( 'button.cancel-action' ).attr( 'data-reload', true ); - window.wcTracks.recordEvent( 'order_edit_edit_item_click', { - order_id: woocommerce_admin_meta_boxes.post_id, - status: $( '#order_status' ).val() - } ); - return false; - }, - - delete_item: function() { - var answer = window.confirm( woocommerce_admin_meta_boxes.remove_item_notice ); - - if ( answer ) { - var $item = $( this ).closest( 'tr.item, tr.fee, tr.shipping' ); - var order_item_id = $item.attr( 'data-order_item_id' ); - - wc_meta_boxes_order_items.block(); - - var data = $.extend( {}, wc_meta_boxes_order_items.get_taxable_address(), { - order_id : woocommerce_admin_meta_boxes.post_id, - order_item_ids: order_item_id, - action : 'woocommerce_remove_order_item', - security : woocommerce_admin_meta_boxes.order_item_nonce - } ); - - // Check if items have changed, if so pass them through so we can save them before deleting. - if ( 'true' === $( 'button.cancel-action' ).attr( 'data-reload' ) ) { - data.items = $( 'table.woocommerce_order_items :input[name], .wc-order-totals-items :input[name]' ).serialize(); - } - - $.ajax({ - url: woocommerce_admin_meta_boxes.ajax_url, - data: data, - type: 'POST', - success: function( response ) { - if ( response.success ) { - $( '#woocommerce-order-items' ).find( '.inside' ).empty(); - $( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html ); - - // Update notes. - if ( response.data.notes_html ) { - $( 'ul.order_notes' ).empty(); - $( 'ul.order_notes' ).append( $( response.data.notes_html ).find( 'li' ) ); - } - - wc_meta_boxes_order_items.reloaded_items(); - wc_meta_boxes_order_items.unblock(); - } else { - window.alert( response.data.error ); - } - wc_meta_boxes_order_items.unblock(); - }, - complete: function() { - window.wcTracks.recordEvent( 'order_edit_remove_item', { - order_id: data.post_id, - status: $( '#order_status' ).val() - } ); - } - }); - } - return false; - }, - - delete_tax: function() { - if ( window.confirm( woocommerce_admin_meta_boxes.i18n_delete_tax ) ) { - wc_meta_boxes_order_items.block(); - - var data = { - action: 'woocommerce_remove_order_tax', - rate_id: $( this ).attr( 'data-rate_id' ), - order_id: woocommerce_admin_meta_boxes.post_id, - security: woocommerce_admin_meta_boxes.order_item_nonce - }; - - $.ajax({ - url: woocommerce_admin_meta_boxes.ajax_url, - data: data, - type: 'POST', - success: function( response ) { - if ( response.success ) { - $( '#woocommerce-order-items' ).find( '.inside' ).empty(); - $( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html ); - wc_meta_boxes_order_items.reloaded_items(); - wc_meta_boxes_order_items.unblock(); - } else { - window.alert( response.data.error ); - } - wc_meta_boxes_order_items.unblock(); - }, - complete: function() { - window.wcTracks.recordEvent( 'order_edit_delete_tax', { - order_id: data.order_id, - status: $( '#order_status' ).val() - } ); - } - }); - } else { - window.wcTracks.recordEvent( 'order_edit_delete_tax_cancel', { - order_id: woocommerce_admin_meta_boxes.post_id, - status: $( '#order_status' ).val() - } ); - } - return false; - }, - - get_taxable_address: function() { - var country = ''; - var state = ''; - var postcode = ''; - var city = ''; - - if ( 'shipping' === woocommerce_admin_meta_boxes.tax_based_on ) { - country = $( '#_shipping_country' ).val(); - state = $( '#_shipping_state' ).val(); - postcode = $( '#_shipping_postcode' ).val(); - city = $( '#_shipping_city' ).val(); - } - - if ( 'billing' === woocommerce_admin_meta_boxes.tax_based_on || ! country ) { - country = $( '#_billing_country' ).val(); - state = $( '#_billing_state' ).val(); - postcode = $( '#_billing_postcode' ).val(); - city = $( '#_billing_city' ).val(); - } - - return { - country: country, - state: state, - postcode: postcode, - city: city - }; - }, - - recalculate: function() { - if ( window.confirm( woocommerce_admin_meta_boxes.calc_totals ) ) { - wc_meta_boxes_order_items.block(); - - var data = $.extend( {}, wc_meta_boxes_order_items.get_taxable_address(), { - action: 'woocommerce_calc_line_taxes', - order_id: woocommerce_admin_meta_boxes.post_id, - items: $( 'table.woocommerce_order_items :input[name], .wc-order-totals-items :input[name]' ).serialize(), - security: woocommerce_admin_meta_boxes.calc_totals_nonce - } ); - - $( document.body ).trigger( 'order-totals-recalculate-before', data ); - - $.ajax({ - url: woocommerce_admin_meta_boxes.ajax_url, - data: data, - type: 'POST', - success: function( response ) { - $( '#woocommerce-order-items' ).find( '.inside' ).empty(); - $( '#woocommerce-order-items' ).find( '.inside' ).append( response ); - wc_meta_boxes_order_items.reloaded_items(); - wc_meta_boxes_order_items.unblock(); - - $( document.body ).trigger( 'order-totals-recalculate-success', response ); - }, - complete: function( response ) { - $( document.body ).trigger( 'order-totals-recalculate-complete', response ); - - window.wcTracks.recordEvent( 'order_edit_recalc_totals', { - order_id: data.post_id, - OK_cancel: 'OK', - status: $( '#order_status' ).val() - } ); - } - }); - } else { - window.wcTracks.recordEvent( 'order_edit_recalc_totals', { - order_id: woocommerce_admin_meta_boxes.post_id, - OK_cancel: 'cancel', - status: $( '#order_status' ).val() - } ); - } - - return false; - }, - - save_line_items: function() { - var data = { - order_id: woocommerce_admin_meta_boxes.post_id, - items: $( 'table.woocommerce_order_items :input[name], .wc-order-totals-items :input[name]' ).serialize(), - action: 'woocommerce_save_order_items', - security: woocommerce_admin_meta_boxes.order_item_nonce - }; - - wc_meta_boxes_order_items.block(); - - $.ajax({ - url: woocommerce_admin_meta_boxes.ajax_url, - data: data, - type: 'POST', - success: function( response ) { - if ( response.success ) { - $( '#woocommerce-order-items' ).find( '.inside' ).empty(); - $( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html ); - - // Update notes. - if ( response.data.notes_html ) { - $( 'ul.order_notes' ).empty(); - $( 'ul.order_notes' ).append( $( response.data.notes_html ).find( 'li' ) ); - } - - wc_meta_boxes_order_items.reloaded_items(); - wc_meta_boxes_order_items.unblock(); - } else { - wc_meta_boxes_order_items.unblock(); - window.alert( response.data.error ); - } - }, - complete: function() { - window.wcTracks.recordEvent( 'order_edit_save_line_items', { - order_id: data.post_id, - status: $( '#order_status' ).val() - } ); - } - }); - - $( this ).trigger( 'items_saved' ); - - return false; - }, - - refunds: { - - do_refund: function() { - wc_meta_boxes_order_items.block(); - - if ( window.confirm( woocommerce_admin_meta_boxes.i18n_do_refund ) ) { - var refund_amount = $( 'input#refund_amount' ).val(); - var refund_reason = $( 'input#refund_reason' ).val(); - var refunded_amount = $( 'input#refunded_amount' ).val(); - - // Get line item refunds - var line_item_qtys = {}; - var line_item_totals = {}; - var line_item_tax_totals = {}; - - $( '.refund input.refund_order_item_qty' ).each(function( index, item ) { - if ( $( item ).closest( 'tr' ).data( 'order_item_id' ) ) { - if ( item.value ) { - line_item_qtys[ $( item ).closest( 'tr' ).data( 'order_item_id' ) ] = item.value; - } - } - }); - - $( '.refund input.refund_line_total' ).each(function( index, item ) { - if ( $( item ).closest( 'tr' ).data( 'order_item_id' ) ) { - line_item_totals[ $( item ).closest( 'tr' ).data( 'order_item_id' ) ] = accounting.unformat( - item.value, - woocommerce_admin.mon_decimal_point - ); - } - }); - - $( '.refund input.refund_line_tax' ).each(function( index, item ) { - if ( $( item ).closest( 'tr' ).data( 'order_item_id' ) ) { - var tax_id = $( item ).data( 'tax_id' ); - - if ( ! line_item_tax_totals[ $( item ).closest( 'tr' ).data( 'order_item_id' ) ] ) { - line_item_tax_totals[ $( item ).closest( 'tr' ).data( 'order_item_id' ) ] = {}; - } - - line_item_tax_totals[ $( item ).closest( 'tr' ).data( 'order_item_id' ) ][ tax_id ] = accounting.unformat( - item.value, - woocommerce_admin.mon_decimal_point - ); - } - }); - - var data = { - action : 'woocommerce_refund_line_items', - order_id : woocommerce_admin_meta_boxes.post_id, - refund_amount : refund_amount, - refunded_amount : refunded_amount, - refund_reason : refund_reason, - line_item_qtys : JSON.stringify( line_item_qtys, null, '' ), - line_item_totals : JSON.stringify( line_item_totals, null, '' ), - line_item_tax_totals : JSON.stringify( line_item_tax_totals, null, '' ), - api_refund : $( this ).is( '.do-api-refund' ), - restock_refunded_items: $( '#restock_refunded_items:checked' ).length ? 'true': 'false', - security : woocommerce_admin_meta_boxes.order_item_nonce - }; - - $.ajax( { - url: woocommerce_admin_meta_boxes.ajax_url, - data: data, - type: 'POST', - success: function( response ) { - if ( true === response.success ) { - // Redirect to same page for show the refunded status - window.location.reload(); - } else { - window.alert( response.data.error ); - wc_meta_boxes_order_items.reload_items(); - wc_meta_boxes_order_items.unblock(); - } - }, - complete: function() { - window.wcTracks.recordEvent( 'order_edit_refunded', { - order_id: data.order_id, - status: $( '#order_status' ).val(), - api_refund: data.api_refund, - has_reason: Boolean( data.refund_reason.length ), - restock: 'true' === data.restock_refunded_items - } ); - } - } ); - } else { - wc_meta_boxes_order_items.unblock(); - } - }, - - delete_refund: function() { - if ( window.confirm( woocommerce_admin_meta_boxes.i18n_delete_refund ) ) { - var $refund = $( this ).closest( 'tr.refund' ); - var refund_id = $refund.attr( 'data-order_refund_id' ); - - wc_meta_boxes_order_items.block(); - - var data = { - action: 'woocommerce_delete_refund', - refund_id: refund_id, - security: woocommerce_admin_meta_boxes.order_item_nonce - }; - - $.ajax({ - url: woocommerce_admin_meta_boxes.ajax_url, - data: data, - type: 'POST', - success: function() { - wc_meta_boxes_order_items.reload_items(); - } - }); - } - return false; - }, - - input_changed: function() { - var refund_amount = 0; - var $items = $( '.woocommerce_order_items' ).find( 'tr.item, tr.fee, tr.shipping' ); - - $items.each(function() { - var $row = $( this ); - var refund_cost_fields = $row.find( '.refund input:not(.refund_order_item_qty)' ); - - refund_cost_fields.each(function( index, el ) { - refund_amount += parseFloat( accounting.unformat( $( el ).val() || 0, woocommerce_admin.mon_decimal_point ) ); - }); - }); - - $( '#refund_amount' ) - .val( accounting.formatNumber( - refund_amount, - woocommerce_admin_meta_boxes.currency_format_num_decimals, - '', - woocommerce_admin.mon_decimal_point - ) ) - .trigger( 'change' ); - }, - - amount_changed: function() { - var total = accounting.unformat( $( this ).val(), woocommerce_admin.mon_decimal_point ); - - $( 'button .wc-order-refund-amount .amount' ).text( accounting.formatMoney( total, { - symbol: woocommerce_admin_meta_boxes.currency_format_symbol, - decimal: woocommerce_admin_meta_boxes.currency_format_decimal_sep, - thousand: woocommerce_admin_meta_boxes.currency_format_thousand_sep, - precision: woocommerce_admin_meta_boxes.currency_format_num_decimals, - format: woocommerce_admin_meta_boxes.currency_format - } ) ); - }, - - // When the refund qty is changed, increase or decrease costs - refund_quantity_changed: function() { - var $row = $( this ).closest( 'tr.item' ); - var qty = $row.find( 'input.quantity' ).val(); - var refund_qty = $( this ).val(); - var line_total = $( 'input.line_total', $row ); - var refund_line_total = $( 'input.refund_line_total', $row ); - - // Totals - var unit_total = accounting.unformat( line_total.attr( 'data-total' ), woocommerce_admin.mon_decimal_point ) / qty; - - refund_line_total.val( - parseFloat( accounting.formatNumber( unit_total * refund_qty, woocommerce_admin_meta_boxes.rounding_precision, '' ) ) - .toString() - .replace( '.', woocommerce_admin.mon_decimal_point ) - ).trigger( 'change' ); - - // Taxes - $( '.refund_line_tax', $row ).each( function() { - var $refund_line_total_tax = $( this ); - var tax_id = $refund_line_total_tax.data( 'tax_id' ); - var line_total_tax = $( 'input.line_tax[data-tax_id="' + tax_id + '"]', $row ); - var unit_total_tax = accounting.unformat( - line_total_tax.data( 'total_tax' ), - woocommerce_admin.mon_decimal_point - ) / qty; - - if ( 0 < unit_total_tax ) { - var round_at_subtotal = 'yes' === woocommerce_admin_meta_boxes.round_at_subtotal; - var precision = woocommerce_admin_meta_boxes[ - round_at_subtotal ? 'rounding_precision' : 'currency_format_num_decimals' - ]; - - $refund_line_total_tax.val( - parseFloat( accounting.formatNumber( unit_total_tax * refund_qty, precision, '' ) ) - .toString() - .replace( '.', woocommerce_admin.mon_decimal_point ) - ).trigger( 'change' ); - } else { - $refund_line_total_tax.val( 0 ).trigger( 'change' ); - } - }); - - // Restock checkbox - if ( refund_qty > 0 ) { - $( '#restock_refunded_items' ).closest( 'tr' ).show(); - } else { - $( '#restock_refunded_items' ).closest( 'tr' ).hide(); - $( '.woocommerce_order_items input.refund_order_item_qty' ).each( function() { - if ( $( this ).val() > 0 ) { - $( '#restock_refunded_items' ).closest( 'tr' ).show(); - } - }); - } - - $( this ).trigger( 'refund_quantity_changed' ); - } - }, - - item_meta: { - - add: function() { - var $button = $( this ); - var $item = $button.closest( 'tr.item, tr.shipping' ); - var $items = $item.find('tbody.meta_items'); - var index = $items.find('tr').length + 1; - var $row = '' + - '' + - '' + - '' + - '' + - '' + - ''; - $items.append( $row ); - - return false; - }, - - remove: function() { - if ( window.confirm( woocommerce_admin_meta_boxes.remove_item_meta ) ) { - var $row = $( this ).closest( 'tr' ); - $row.find( ':input' ).val( '' ); - $row.hide(); - } - return false; - } - }, - - backbone: { - - init: function( e, target ) { - if ( 'wc-modal-add-products' === target ) { - $( document.body ).trigger( 'wc-enhanced-select-init' ); - - $( this ).on( 'change', '.wc-product-search', function() { - if ( ! $( this ).closest( 'tr' ).is( ':last-child' ) ) { - return; - } - var item_table = $( this ).closest( 'table.widefat' ), - item_table_body = item_table.find( 'tbody' ), - index = item_table_body.find( 'tr' ).length, - row = item_table_body.data( 'row' ).replace( /\[0\]/g, '[' + index + ']' ); - - item_table_body.append( '' + row + '' ); - $( document.body ).trigger( 'wc-enhanced-select-init' ); - } ); - } - }, - - response: function( e, target, data ) { - if ( 'wc-modal-add-tax' === target ) { - var rate_id = data.add_order_tax; - var manual_rate_id = ''; - - if ( data.manual_tax_rate_id ) { - manual_rate_id = data.manual_tax_rate_id; - } - - wc_meta_boxes_order_items.backbone.add_tax( rate_id, manual_rate_id ); - } - if ( 'wc-modal-add-products' === target ) { - // Build array of data. - var item_table = $( this ).find( 'table.widefat' ), - item_table_body = item_table.find( 'tbody' ), - rows = item_table_body.find( 'tr' ), - add_items = []; - - $( rows ).each( function() { - var item_id = $( this ).find( ':input[name="item_id"]' ).val(), - item_qty = $( this ).find( ':input[name="item_qty"]' ).val(); - - add_items.push( { - 'id' : item_id, - 'qty': item_qty ? item_qty: 1 - } ); - } ); - - return wc_meta_boxes_order_items.backbone.add_items( add_items ); - } - }, - - add_items: function( add_items ) { - wc_meta_boxes_order_items.block(); - - var data = { - action : 'woocommerce_add_order_item', - order_id : woocommerce_admin_meta_boxes.post_id, - security : woocommerce_admin_meta_boxes.order_item_nonce, - data : add_items - }; - - // Check if items have changed, if so pass them through so we can save them before adding a new item. - if ( 'true' === $( 'button.cancel-action' ).attr( 'data-reload' ) ) { - data.items = $( 'table.woocommerce_order_items :input[name], .wc-order-totals-items :input[name]' ).serialize(); - } - - $.ajax({ - type: 'POST', - url: woocommerce_admin_meta_boxes.ajax_url, - data: data, - success: function( response ) { - if ( response.success ) { - $( '#woocommerce-order-items' ).find( '.inside' ).empty(); - $( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html ); - - // Update notes. - if ( response.data.notes_html ) { - $( 'ul.order_notes' ).empty(); - $( 'ul.order_notes' ).append( $( response.data.notes_html ).find( 'li' ) ); - } - - wc_meta_boxes_order_items.reloaded_items(); - wc_meta_boxes_order_items.unblock(); - } else { - wc_meta_boxes_order_items.unblock(); - window.alert( response.data.error ); - } - }, - complete: function() { - window.wcTracks.recordEvent( 'order_edit_add_products', { - order_id: data.post_id, - status: $( '#order_status' ).val() - } ); - }, - dataType: 'json' - }); - }, - - add_tax: function( rate_id, manual_rate_id ) { - if ( manual_rate_id ) { - rate_id = manual_rate_id; - } - - if ( ! rate_id ) { - return false; - } - - var rates = $( '.order-tax-id' ).map( function() { - return $( this ).val(); - }).get(); - - // Test if already exists - if ( -1 === $.inArray( rate_id, rates ) ) { - wc_meta_boxes_order_items.block(); - - var data = { - action: 'woocommerce_add_order_tax', - rate_id: rate_id, - order_id: woocommerce_admin_meta_boxes.post_id, - security: woocommerce_admin_meta_boxes.order_item_nonce - }; - - $.ajax({ - url : woocommerce_admin_meta_boxes.ajax_url, - data : data, - dataType : 'json', - type : 'POST', - success : function( response ) { - if ( response.success ) { - $( '#woocommerce-order-items' ).find( '.inside' ).empty(); - $( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html ); - wc_meta_boxes_order_items.reloaded_items(); - } else { - window.alert( response.data.error ); - } - wc_meta_boxes_order_items.unblock(); - }, - complete: function() { - window.wcTracks.recordEvent( 'order_edit_add_tax', { - order_id: data.post_id, - status: $( '#order_status' ).val() - } ); - } - }); - } else { - window.alert( woocommerce_admin_meta_boxes.i18n_tax_rate_already_exists ); - } - } - }, - - stupidtable: { - init: function() { - $( '.woocommerce_order_items' ).stupidtable(); - $( '.woocommerce_order_items' ).on( 'aftertablesort', this.add_arrows ); - }, - - add_arrows: function( event, data ) { - var th = $( this ).find( 'th' ); - var arrow = data.direction === 'asc' ? '↑' : '↓'; - var index = data.column; - th.find( '.wc-arrow' ).remove(); - th.eq( index ).append( '' + arrow + '' ); - } - } - }; - - /** - * Order Notes Panel - */ - var wc_meta_boxes_order_notes = { - init: function() { - $( '#woocommerce-order-notes' ) - .on( 'click', 'button.add_note', this.add_order_note ) - .on( 'click', 'a.delete_note', this.delete_order_note ); - - }, - - add_order_note: function() { - if ( ! $( 'textarea#add_order_note' ).val() ) { - return; - } - - $( '#woocommerce-order-notes' ).block({ - message: null, - overlayCSS: { - background: '#fff', - opacity: 0.6 - } - }); - - var data = { - action: 'woocommerce_add_order_note', - post_id: woocommerce_admin_meta_boxes.post_id, - note: $( 'textarea#add_order_note' ).val(), - note_type: $( 'select#order_note_type' ).val(), - security: woocommerce_admin_meta_boxes.add_order_note_nonce - }; - - $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { - $( 'ul.order_notes .no-items' ).remove(); - $( 'ul.order_notes' ).prepend( response ); - $( '#woocommerce-order-notes' ).unblock(); - $( '#add_order_note' ).val( '' ); - window.wcTracks.recordEvent( 'order_edit_add_order_note', { - order_id: data.post_id, - note_type: data.note_type || 'private', - status: $( '#order_status' ).val() - } ); - }); - - return false; - }, - - delete_order_note: function() { - if ( window.confirm( woocommerce_admin_meta_boxes.i18n_delete_note ) ) { - var note = $( this ).closest( 'li.note' ); - - $( note ).block({ - message: null, - overlayCSS: { - background: '#fff', - opacity: 0.6 - } - }); - - var data = { - action: 'woocommerce_delete_order_note', - note_id: $( note ).attr( 'rel' ), - security: woocommerce_admin_meta_boxes.delete_order_note_nonce - }; - - $.post( woocommerce_admin_meta_boxes.ajax_url, data, function() { - $( note ).remove(); - }); - } - - return false; - } - }; - - /** - * Order Downloads Panel - */ - var wc_meta_boxes_order_downloads = { - init: function() { - $( '.order_download_permissions' ) - .on( 'click', 'button.grant_access', this.grant_access ) - .on( 'click', 'button.revoke_access', this.revoke_access ) - .on( 'click', '#copy-download-link', this.copy_link ) - .on( 'aftercopy', '#copy-download-link', this.copy_success ) - .on( 'aftercopyfailure', '#copy-download-link', this.copy_fail ); - }, - - grant_access: function() { - var products = $( '#grant_access_id' ).val(); - - if ( ! products ) { - return; - } - - $( '.order_download_permissions' ).block({ - message: null, - overlayCSS: { - background: '#fff', - opacity: 0.6 - } - }); - - var data = { - action: 'woocommerce_grant_access_to_download', - product_ids: products, - loop: $('.order_download_permissions .wc-metabox').length, - order_id: woocommerce_admin_meta_boxes.post_id, - security: woocommerce_admin_meta_boxes.grant_access_nonce - }; - - $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { - - if ( response ) { - $( '.order_download_permissions .wc-metaboxes' ).append( response ); - } else { - window.alert( woocommerce_admin_meta_boxes.i18n_download_permission_fail ); - } - - $( document.body ).trigger( 'wc-init-datepickers' ); - $( '#grant_access_id' ).val( '' ).trigger( 'change' ); - $( '.order_download_permissions' ).unblock(); - }); - - return false; - }, - - revoke_access: function () { - if ( window.confirm( woocommerce_admin_meta_boxes.i18n_permission_revoke ) ) { - var el = $( this ).parent().parent(); - var product = $( this ).attr( 'rel' ).split( ',' )[0]; - var file = $( this ).attr( 'rel' ).split( ',' )[1]; - var permission_id = $( this ).data( 'permission_id' ); - - if ( product > 0 ) { - $( el ).block({ - message: null, - overlayCSS: { - background: '#fff', - opacity: 0.6 - } - }); - - var data = { - action: 'woocommerce_revoke_access_to_download', - product_id: product, - download_id: file, - permission_id: permission_id, - order_id: woocommerce_admin_meta_boxes.post_id, - security: woocommerce_admin_meta_boxes.revoke_access_nonce - }; - - $.post( woocommerce_admin_meta_boxes.ajax_url, data, function() { - // Success - $( el ).fadeOut( '300', function () { - $( el ).remove(); - }); - }); - - } else { - $( el ).fadeOut( '300', function () { - $( el ).remove(); - }); - } - } - return false; - }, - - /** - * Copy download link. - * - * @param {Object} evt Copy event. - */ - copy_link: function( evt ) { - wcClearClipboard(); - wcSetClipboard( $( this ).attr( 'href' ), $( this ) ); - evt.preventDefault(); - }, - - /** - * Display a "Copied!" tip when success copying - */ - copy_success: function() { - $( this ).tipTip({ - 'attribute': 'data-tip', - 'activation': 'focus', - 'fadeIn': 50, - 'fadeOut': 50, - 'delay': 0 - }).focus(); - }, - - /** - * Displays the copy error message when failure copying. - */ - copy_fail: function() { - $( this ).tipTip({ - 'attribute': 'data-tip-failed', - 'activation': 'focus', - 'fadeIn': 50, - 'fadeOut': 50, - 'delay': 0 - }).focus(); - } - }; - - wc_meta_boxes_order.init(); - wc_meta_boxes_order_items.init(); - wc_meta_boxes_order_notes.init(); - wc_meta_boxes_order_downloads.init(); -}); diff --git a/assets/js/admin/meta-boxes-product-variation.js b/assets/js/admin/meta-boxes-product-variation.js deleted file mode 100644 index 55ac9327559..00000000000 --- a/assets/js/admin/meta-boxes-product-variation.js +++ /dev/null @@ -1,1081 +0,0 @@ -/* global wp, woocommerce_admin_meta_boxes_variations, woocommerce_admin, accounting */ -jQuery( function( $ ) { - 'use strict'; - - /** - * Variations actions - */ - var wc_meta_boxes_product_variations_actions = { - - /** - * Initialize variations actions - */ - init: function() { - $( '#variable_product_options' ) - .on( 'change', 'input.variable_is_downloadable', this.variable_is_downloadable ) - .on( 'change', 'input.variable_is_virtual', this.variable_is_virtual ) - .on( 'change', 'input.variable_manage_stock', this.variable_manage_stock ) - .on( 'click', 'button.notice-dismiss', this.notice_dismiss ) - .on( 'click', 'h3 .sort', this.set_menu_order ) - .on( 'reload', this.reload ); - - $( 'input.variable_is_downloadable, input.variable_is_virtual, input.variable_manage_stock' ).trigger( 'change' ); - $( '#woocommerce-product-data' ).on( 'woocommerce_variations_loaded', this.variations_loaded ); - $( document.body ).on( 'woocommerce_variations_added', this.variation_added ); - }, - - /** - * Reload UI - * - * @param {Object} event - * @param {Int} qty - */ - reload: function() { - wc_meta_boxes_product_variations_ajax.load_variations( 1 ); - wc_meta_boxes_product_variations_pagenav.set_paginav( 0 ); - }, - - /** - * Check if variation is downloadable and show/hide elements - */ - variable_is_downloadable: function() { - $( this ).closest( '.woocommerce_variation' ).find( '.show_if_variation_downloadable' ).hide(); - - if ( $( this ).is( ':checked' ) ) { - $( this ).closest( '.woocommerce_variation' ).find( '.show_if_variation_downloadable' ).show(); - } - }, - - /** - * Check if variation is virtual and show/hide elements - */ - variable_is_virtual: function() { - $( this ).closest( '.woocommerce_variation' ).find( '.hide_if_variation_virtual' ).show(); - - if ( $( this ).is( ':checked' ) ) { - $( this ).closest( '.woocommerce_variation' ).find( '.hide_if_variation_virtual' ).hide(); - } - }, - - /** - * Check if variation manage stock and show/hide elements - */ - variable_manage_stock: function() { - $( this ).closest( '.woocommerce_variation' ).find( '.show_if_variation_manage_stock' ).hide(); - $( this ).closest( '.woocommerce_variation' ).find( '.variable_stock_status' ).show(); - - if ( $( this ).is( ':checked' ) ) { - $( this ).closest( '.woocommerce_variation' ).find( '.show_if_variation_manage_stock' ).show(); - $( this ).closest( '.woocommerce_variation' ).find( '.variable_stock_status' ).hide(); - } - - // Parent level. - if ( $( 'input#_manage_stock:checked' ).length ) { - $( this ).closest( '.woocommerce_variation' ).find( '.variable_stock_status' ).hide(); - } - }, - - /** - * Notice dismiss - */ - notice_dismiss: function() { - $( this ).closest( 'div.notice' ).remove(); - }, - - /** - * Run actions when variations is loaded - * - * @param {Object} event - * @param {Int} needsUpdate - */ - variations_loaded: function( event, needsUpdate ) { - needsUpdate = needsUpdate || false; - - var wrapper = $( '#woocommerce-product-data' ); - - if ( ! needsUpdate ) { - // Show/hide downloadable, virtual and stock fields - $( 'input.variable_is_downloadable, input.variable_is_virtual, input.variable_manage_stock', wrapper ).trigger( 'change' ); - - // Open sale schedule fields when have some sale price date - $( '.woocommerce_variation', wrapper ).each( function( index, el ) { - var $el = $( el ), - date_from = $( '.sale_price_dates_from', $el ).val(), - date_to = $( '.sale_price_dates_to', $el ).val(); - - if ( '' !== date_from || '' !== date_to ) { - $( 'a.sale_schedule', $el ).trigger( 'click' ); - } - }); - - // Remove variation-needs-update classes - $( '.woocommerce_variations .variation-needs-update', wrapper ).removeClass( 'variation-needs-update' ); - - // Disable cancel and save buttons - $( 'button.cancel-variation-changes, button.save-variation-changes', wrapper ).attr( 'disabled', 'disabled' ); - } - - // Init TipTip - $( '#tiptip_holder' ).removeAttr( 'style' ); - $( '#tiptip_arrow' ).removeAttr( 'style' ); - $( '.woocommerce_variations .tips, .woocommerce_variations .help_tip, .woocommerce_variations .woocommerce-help-tip', wrapper ) - .tipTip({ - 'attribute': 'data-tip', - 'fadeIn': 50, - 'fadeOut': 50, - 'delay': 200 - }); - - // Datepicker fields - $( '.sale_price_dates_fields', wrapper ).find( 'input' ).datepicker({ - defaultDate: '', - dateFormat: 'yy-mm-dd', - numberOfMonths: 1, - showButtonPanel: true, - onSelect: function() { - var option = $( this ).is( '.sale_price_dates_from' ) ? 'minDate' : 'maxDate', - dates = $( this ).closest( '.sale_price_dates_fields' ).find( 'input' ), - date = $( this ).datepicker( 'getDate' ); - - dates.not( this ).datepicker( 'option', option, date ); - $( this ).trigger( 'change' ); - } - }); - - // Allow sorting - $( '.woocommerce_variations', wrapper ).sortable({ - items: '.woocommerce_variation', - cursor: 'move', - axis: 'y', - handle: '.sort', - scrollSensitivity: 40, - forcePlaceholderSize: true, - helper: 'clone', - opacity: 0.65, - stop: function() { - wc_meta_boxes_product_variations_actions.variation_row_indexes(); - } - }); - - $( document.body ).trigger( 'wc-enhanced-select-init' ); - }, - - /** - * Run actions when added a variation - * - * @param {Object} event - * @param {Int} qty - */ - variation_added: function( event, qty ) { - if ( 1 === qty ) { - wc_meta_boxes_product_variations_actions.variations_loaded( null, true ); - } - }, - - /** - * Lets the user manually input menu order to move items around pages - */ - set_menu_order: function( event ) { - event.preventDefault(); - var $menu_order = $( this ).closest( '.woocommerce_variation' ).find('.variation_menu_order'); - var value = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_enter_menu_order, $menu_order.val() ); - - if ( value != null ) { - // Set value, save changes and reload view - $menu_order.val( parseInt( value, 10 ) ).trigger( 'change' ); - wc_meta_boxes_product_variations_ajax.save_variations(); - } - }, - - /** - * Set menu order - */ - variation_row_indexes: function() { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), - current_page = parseInt( wrapper.attr( 'data-page' ), 10 ), - offset = parseInt( ( current_page - 1 ) * woocommerce_admin_meta_boxes_variations.variations_per_page, 10 ); - - $( '.woocommerce_variations .woocommerce_variation' ).each( function ( index, el ) { - $( '.variation_menu_order', el ) - .val( parseInt( $( el ) - .index( '.woocommerce_variations .woocommerce_variation' ), 10 ) + 1 + offset ) - .trigger( 'change' ); - }); - } - }; - - /** - * Variations media actions - */ - var wc_meta_boxes_product_variations_media = { - - /** - * wp.media frame object - * - * @type {Object} - */ - variable_image_frame: null, - - /** - * Variation image ID - * - * @type {Int} - */ - setting_variation_image_id: null, - - /** - * Variation image object - * - * @type {Object} - */ - setting_variation_image: null, - - /** - * wp.media post ID - * - * @type {Int} - */ - wp_media_post_id: wp.media.model.settings.post.id, - - /** - * Initialize media actions - */ - init: function() { - $( '#variable_product_options' ).on( 'click', '.upload_image_button', this.add_image ); - $( 'a.add_media' ).on( 'click', this.restore_wp_media_post_id ); - }, - - /** - * Added new image - * - * @param {Object} event - */ - add_image: function( event ) { - var $button = $( this ), - post_id = $button.attr( 'rel' ), - $parent = $button.closest( '.upload_image' ); - - wc_meta_boxes_product_variations_media.setting_variation_image = $parent; - wc_meta_boxes_product_variations_media.setting_variation_image_id = post_id; - - event.preventDefault(); - - if ( $button.is( '.remove' ) ) { - - $( '.upload_image_id', wc_meta_boxes_product_variations_media.setting_variation_image ).val( '' ).trigger( 'change' ); - wc_meta_boxes_product_variations_media.setting_variation_image.find( 'img' ).eq( 0 ) - .attr( 'src', woocommerce_admin_meta_boxes_variations.woocommerce_placeholder_img_src ); - wc_meta_boxes_product_variations_media.setting_variation_image.find( '.upload_image_button' ).removeClass( 'remove' ); - - } else { - - // If the media frame already exists, reopen it. - if ( wc_meta_boxes_product_variations_media.variable_image_frame ) { - wc_meta_boxes_product_variations_media.variable_image_frame.uploader.uploader - .param( 'post_id', wc_meta_boxes_product_variations_media.setting_variation_image_id ); - wc_meta_boxes_product_variations_media.variable_image_frame.open(); - return; - } else { - wp.media.model.settings.post.id = wc_meta_boxes_product_variations_media.setting_variation_image_id; - } - - // Create the media frame. - wc_meta_boxes_product_variations_media.variable_image_frame = wp.media.frames.variable_image = wp.media({ - // Set the title of the modal. - title: woocommerce_admin_meta_boxes_variations.i18n_choose_image, - button: { - text: woocommerce_admin_meta_boxes_variations.i18n_set_image - }, - states: [ - new wp.media.controller.Library({ - title: woocommerce_admin_meta_boxes_variations.i18n_choose_image, - filterable: 'all' - }) - ] - }); - - // When an image is selected, run a callback. - wc_meta_boxes_product_variations_media.variable_image_frame.on( 'select', function () { - - var attachment = wc_meta_boxes_product_variations_media.variable_image_frame.state() - .get( 'selection' ).first().toJSON(), - url = attachment.sizes && attachment.sizes.thumbnail ? attachment.sizes.thumbnail.url : attachment.url; - - $( '.upload_image_id', wc_meta_boxes_product_variations_media.setting_variation_image ).val( attachment.id ) - .trigger( 'change' ); - wc_meta_boxes_product_variations_media.setting_variation_image.find( '.upload_image_button' ).addClass( 'remove' ); - wc_meta_boxes_product_variations_media.setting_variation_image.find( 'img' ).eq( 0 ).attr( 'src', url ); - - wp.media.model.settings.post.id = wc_meta_boxes_product_variations_media.wp_media_post_id; - }); - - // Finally, open the modal. - wc_meta_boxes_product_variations_media.variable_image_frame.open(); - } - }, - - /** - * Restore wp.media post ID. - */ - restore_wp_media_post_id: function() { - wp.media.model.settings.post.id = wc_meta_boxes_product_variations_media.wp_media_post_id; - } - }; - - /** - * Product variations metabox ajax methods - */ - var wc_meta_boxes_product_variations_ajax = { - - /** - * Initialize variations ajax methods - */ - init: function() { - $( 'li.variations_tab a' ).on( 'click', this.initial_load ); - - $( '#variable_product_options' ) - .on( 'click', 'button.save-variation-changes', this.save_variations ) - .on( 'click', 'button.cancel-variation-changes', this.cancel_variations ) - .on( 'click', '.remove_variation', this.remove_variation ) - .on( 'click','.downloadable_files a.delete', this.input_changed ); - - $( document.body ) - .on( 'change', '#variable_product_options .woocommerce_variations :input', this.input_changed ) - .on( 'change', '.variations-defaults select', this.defaults_changed ); - - var postForm = $( 'form#post' ); - - postForm.on( 'submit', this.save_on_submit ); - - $( 'input:submit', postForm ).on( 'click keypress', function() { - postForm.data( 'callerid', this.id ); - }); - - $( '.wc-metaboxes-wrapper' ).on( 'click', 'a.do_variation_action', this.do_variation_action ); - }, - - /** - * Check if have some changes before leave the page - * - * @return {Bool} - */ - check_for_changes: function() { - var need_update = $( '#variable_product_options' ).find( '.woocommerce_variations .variation-needs-update' ); - - if ( 0 < need_update.length ) { - if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_edited_variations ) ) { - wc_meta_boxes_product_variations_ajax.save_changes(); - } else { - need_update.removeClass( 'variation-needs-update' ); - return false; - } - } - - return true; - }, - - /** - * Block edit screen - */ - block: function() { - $( '#woocommerce-product-data' ).block({ - message: null, - overlayCSS: { - background: '#fff', - opacity: 0.6 - } - }); - }, - - /** - * Unblock edit screen - */ - unblock: function() { - $( '#woocommerce-product-data' ).unblock(); - }, - - /** - * Initial load variations - * - * @return {Bool} - */ - initial_load: function() { - if ( 0 === $( '#variable_product_options' ).find( '.woocommerce_variations .woocommerce_variation' ).length ) { - wc_meta_boxes_product_variations_pagenav.go_to_page(); - } - }, - - /** - * Load variations via Ajax - * - * @param {Int} page (default: 1) - * @param {Int} per_page (default: 10) - */ - load_variations: function( page, per_page ) { - page = page || 1; - per_page = per_page || woocommerce_admin_meta_boxes_variations.variations_per_page; - - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ); - - wc_meta_boxes_product_variations_ajax.block(); - - $.ajax({ - url: woocommerce_admin_meta_boxes_variations.ajax_url, - data: { - action: 'woocommerce_load_variations', - security: woocommerce_admin_meta_boxes_variations.load_variations_nonce, - product_id: woocommerce_admin_meta_boxes_variations.post_id, - attributes: wrapper.data( 'attributes' ), - page: page, - per_page: per_page - }, - type: 'POST', - success: function( response ) { - wrapper.empty().append( response ).attr( 'data-page', page ); - - $( '#woocommerce-product-data' ).trigger( 'woocommerce_variations_loaded' ); - - wc_meta_boxes_product_variations_ajax.unblock(); - } - }); - }, - - /** - * Ger variations fields and convert to object - * - * @param {Object} fields - * - * @return {Object} - */ - get_variations_fields: function( fields ) { - var data = $( ':input', fields ).serializeJSON(); - - $( '.variations-defaults select' ).each( function( index, element ) { - var select = $( element ); - data[ select.attr( 'name' ) ] = select.val(); - }); - - return data; - }, - - /** - * Save variations changes - * - * @param {Function} callback Called once saving is complete - */ - save_changes: function( callback ) { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), - need_update = $( '.variation-needs-update', wrapper ), - data = {}; - - // Save only with products need update. - if ( 0 < need_update.length ) { - wc_meta_boxes_product_variations_ajax.block(); - - data = wc_meta_boxes_product_variations_ajax.get_variations_fields( need_update ); - data.action = 'woocommerce_save_variations'; - data.security = woocommerce_admin_meta_boxes_variations.save_variations_nonce; - data.product_id = woocommerce_admin_meta_boxes_variations.post_id; - data['product-type'] = $( '#product-type' ).val(); - - $.ajax({ - url: woocommerce_admin_meta_boxes_variations.ajax_url, - data: data, - type: 'POST', - success: function( response ) { - // Allow change page, delete and add new variations - need_update.removeClass( 'variation-needs-update' ); - $( 'button.cancel-variation-changes, button.save-variation-changes' ).attr( 'disabled', 'disabled' ); - - $( '#woocommerce-product-data' ).trigger( 'woocommerce_variations_saved' ); - - if ( typeof callback === 'function' ) { - callback( response ); - } - - wc_meta_boxes_product_variations_ajax.unblock(); - } - }); - } - }, - - /** - * Save variations - * - * @return {Bool} - */ - save_variations: function() { - $( '#variable_product_options' ).trigger( 'woocommerce_variations_save_variations_button' ); - - wc_meta_boxes_product_variations_ajax.save_changes( function( error ) { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), - current = wrapper.attr( 'data-page' ); - - $( '#variable_product_options' ).find( '#woocommerce_errors' ).remove(); - - if ( error ) { - wrapper.before( error ); - } - - $( '.variations-defaults select' ).each( function() { - $( this ).attr( 'data-current', $( this ).val() ); - }); - - wc_meta_boxes_product_variations_pagenav.go_to_page( current ); - }); - - return false; - }, - - /** - * Save on post form submit - */ - save_on_submit: function( e ) { - var need_update = $( '#variable_product_options' ).find( '.woocommerce_variations .variation-needs-update' ); - - if ( 0 < need_update.length ) { - e.preventDefault(); - $( '#variable_product_options' ).trigger( 'woocommerce_variations_save_variations_on_submit' ); - wc_meta_boxes_product_variations_ajax.save_changes( wc_meta_boxes_product_variations_ajax.save_on_submit_done ); - } - }, - - /** - * After saved, continue with form submission - */ - save_on_submit_done: function() { - var postForm = $( 'form#post' ), - callerid = postForm.data( 'callerid' ); - - if ( 'publish' === callerid ) { - postForm.append('').trigger( 'submit' ); - } else { - postForm.append('').trigger( 'submit' ); - } - }, - - /** - * Discart changes. - * - * @return {Bool} - */ - cancel_variations: function() { - var current = parseInt( $( '#variable_product_options' ).find( '.woocommerce_variations' ).attr( 'data-page' ), 10 ); - - $( '#variable_product_options' ).find( '.woocommerce_variations .variation-needs-update' ) - .removeClass( 'variation-needs-update' ); - $( '.variations-defaults select' ).each( function() { - $( this ).val( $( this ).attr( 'data-current' ) ); - }); - - wc_meta_boxes_product_variations_pagenav.go_to_page( current ); - - return false; - }, - - /** - * Add variation - * - * @return {Bool} - */ - add_variation: function() { - wc_meta_boxes_product_variations_ajax.block(); - - var data = { - action: 'woocommerce_add_variation', - post_id: woocommerce_admin_meta_boxes_variations.post_id, - loop: $( '.woocommerce_variation' ).length, - security: woocommerce_admin_meta_boxes_variations.add_variation_nonce - }; - - $.post( woocommerce_admin_meta_boxes_variations.ajax_url, data, function( response ) { - var variation = $( response ); - variation.addClass( 'variation-needs-update' ); - - $( '.woocommerce-notice-invalid-variation' ).remove(); - $( '#variable_product_options' ).find( '.woocommerce_variations' ).prepend( variation ); - $( 'button.cancel-variation-changes, button.save-variation-changes' ).removeAttr( 'disabled' ); - $( '#variable_product_options' ).trigger( 'woocommerce_variations_added', 1 ); - wc_meta_boxes_product_variations_ajax.unblock(); - }); - - return false; - }, - - /** - * Remove variation - * - * @return {Bool} - */ - remove_variation: function() { - wc_meta_boxes_product_variations_ajax.check_for_changes(); - - if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_remove_variation ) ) { - var variation = $( this ).attr( 'rel' ), - variation_ids = [], - data = { - action: 'woocommerce_remove_variations' - }; - - wc_meta_boxes_product_variations_ajax.block(); - - if ( 0 < variation ) { - variation_ids.push( variation ); - - data.variation_ids = variation_ids; - data.security = woocommerce_admin_meta_boxes_variations.delete_variations_nonce; - - $.post( woocommerce_admin_meta_boxes_variations.ajax_url, data, function() { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), - current_page = parseInt( wrapper.attr( 'data-page' ), 10 ), - total_pages = Math.ceil( ( - parseInt( wrapper.attr( 'data-total' ), 10 ) - 1 - ) / woocommerce_admin_meta_boxes_variations.variations_per_page ), - page = 1; - - $( '#woocommerce-product-data' ).trigger( 'woocommerce_variations_removed' ); - - if ( current_page === total_pages || current_page <= total_pages ) { - page = current_page; - } else if ( current_page > total_pages && 0 !== total_pages ) { - page = total_pages; - } - - wc_meta_boxes_product_variations_pagenav.go_to_page( page, -1 ); - }); - - } else { - wc_meta_boxes_product_variations_ajax.unblock(); - } - } - - return false; - }, - - /** - * Link all variations (or at least try :p) - * - * @return {Bool} - */ - link_all_variations: function() { - wc_meta_boxes_product_variations_ajax.check_for_changes(); - - if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_link_all_variations ) ) { - wc_meta_boxes_product_variations_ajax.block(); - - var data = { - action: 'woocommerce_link_all_variations', - post_id: woocommerce_admin_meta_boxes_variations.post_id, - security: woocommerce_admin_meta_boxes_variations.link_variation_nonce - }; - - $.post( woocommerce_admin_meta_boxes_variations.ajax_url, data, function( response ) { - var count = parseInt( response, 10 ); - - if ( 1 === count ) { - window.alert( count + ' ' + woocommerce_admin_meta_boxes_variations.i18n_variation_added ); - } else if ( 0 === count || count > 1 ) { - window.alert( count + ' ' + woocommerce_admin_meta_boxes_variations.i18n_variations_added ); - } else { - window.alert( woocommerce_admin_meta_boxes_variations.i18n_no_variations_added ); - } - - if ( count > 0 ) { - wc_meta_boxes_product_variations_pagenav.go_to_page( 1, count ); - $( '#variable_product_options' ).trigger( 'woocommerce_variations_added', count ); - } else { - wc_meta_boxes_product_variations_ajax.unblock(); - } - }); - } - - return false; - }, - - /** - * Add new class when have changes in some input - */ - input_changed: function() { - $( this ) - .closest( '.woocommerce_variation' ) - .addClass( 'variation-needs-update' ); - - $( 'button.cancel-variation-changes, button.save-variation-changes' ).removeAttr( 'disabled' ); - - $( '#variable_product_options' ).trigger( 'woocommerce_variations_input_changed' ); - }, - - /** - * Added new .variation-needs-update class when defaults is changed - */ - defaults_changed: function() { - $( this ) - .closest( '#variable_product_options' ) - .find( '.woocommerce_variation:first' ) - .addClass( 'variation-needs-update' ); - - $( 'button.cancel-variation-changes, button.save-variation-changes' ).removeAttr( 'disabled' ); - - $( '#variable_product_options' ).trigger( 'woocommerce_variations_defaults_changed' ); - }, - - /** - * Actions - */ - do_variation_action: function() { - var do_variation_action = $( 'select.variation_actions' ).val(), - data = {}, - changes = 0, - value; - - switch ( do_variation_action ) { - case 'add_variation' : - wc_meta_boxes_product_variations_ajax.add_variation(); - return; - case 'link_all_variations' : - wc_meta_boxes_product_variations_ajax.link_all_variations(); - return; - case 'delete_all' : - if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_delete_all_variations ) ) { - if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_last_warning ) ) { - data.allowed = true; - changes = parseInt( $( '#variable_product_options' ).find( '.woocommerce_variations' ) - .attr( 'data-total' ), 10 ) * -1; - } - } - break; - case 'variable_regular_price_increase' : - case 'variable_regular_price_decrease' : - case 'variable_sale_price_increase' : - case 'variable_sale_price_decrease' : - value = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_enter_a_value_fixed_or_percent ); - - if ( value != null ) { - if ( value.indexOf( '%' ) >= 0 ) { - data.value = accounting.unformat( value.replace( /\%/, '' ), woocommerce_admin.mon_decimal_point ) + '%'; - } else { - data.value = accounting.unformat( value, woocommerce_admin.mon_decimal_point ); - } - } else { - return; - } - break; - case 'variable_regular_price' : - case 'variable_sale_price' : - case 'variable_stock' : - case 'variable_weight' : - case 'variable_length' : - case 'variable_width' : - case 'variable_height' : - case 'variable_download_limit' : - case 'variable_download_expiry' : - value = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_enter_a_value ); - - if ( value != null ) { - data.value = value; - } else { - return; - } - break; - case 'variable_sale_schedule' : - data.date_from = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_scheduled_sale_start ); - data.date_to = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_scheduled_sale_end ); - - if ( null === data.date_from ) { - data.date_from = false; - } - - if ( null === data.date_to ) { - data.date_to = false; - } - - if ( false === data.date_to && false === data.date_from ) { - return; - } - break; - default : - $( 'select.variation_actions' ).trigger( do_variation_action ); - data = $( 'select.variation_actions' ).triggerHandler( do_variation_action + '_ajax_data', data ); - break; - } - - if ( 'delete_all' === do_variation_action && data.allowed ) { - $( '#variable_product_options' ).find( '.variation-needs-update' ).removeClass( 'variation-needs-update' ); - } else { - wc_meta_boxes_product_variations_ajax.check_for_changes(); - } - - wc_meta_boxes_product_variations_ajax.block(); - - $.ajax({ - url: woocommerce_admin_meta_boxes_variations.ajax_url, - data: { - action: 'woocommerce_bulk_edit_variations', - security: woocommerce_admin_meta_boxes_variations.bulk_edit_variations_nonce, - product_id: woocommerce_admin_meta_boxes_variations.post_id, - product_type: $( '#product-type' ).val(), - bulk_action: do_variation_action, - data: data - }, - type: 'POST', - success: function() { - wc_meta_boxes_product_variations_pagenav.go_to_page( 1, changes ); - } - }); - } - }; - - /** - * Product variations pagenav - */ - var wc_meta_boxes_product_variations_pagenav = { - - /** - * Initialize products variations meta box - */ - init: function() { - $( document.body ) - .on( 'woocommerce_variations_added', this.update_single_quantity ) - .on( 'change', '.variations-pagenav .page-selector', this.page_selector ) - .on( 'click', '.variations-pagenav .first-page', this.first_page ) - .on( 'click', '.variations-pagenav .prev-page', this.prev_page ) - .on( 'click', '.variations-pagenav .next-page', this.next_page ) - .on( 'click', '.variations-pagenav .last-page', this.last_page ); - }, - - /** - * Set variations count - * - * @param {Int} qty - * - * @return {Int} - */ - update_variations_count: function( qty ) { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), - total = parseInt( wrapper.attr( 'data-total' ), 10 ) + qty, - displaying_num = $( '.variations-pagenav .displaying-num' ); - - // Set the new total of variations - wrapper.attr( 'data-total', total ); - - if ( 1 === total ) { - displaying_num.text( woocommerce_admin_meta_boxes_variations.i18n_variation_count_single.replace( '%qty%', total ) ); - } else { - displaying_num.text( woocommerce_admin_meta_boxes_variations.i18n_variation_count_plural.replace( '%qty%', total ) ); - } - - return total; - }, - - /** - * Update variations quantity when add a new variation - * - * @param {Object} event - * @param {Int} qty - */ - update_single_quantity: function( event, qty ) { - if ( 1 === qty ) { - var page_nav = $( '.variations-pagenav' ); - - wc_meta_boxes_product_variations_pagenav.update_variations_count( qty ); - - if ( page_nav.is( ':hidden' ) ) { - $( 'option, optgroup', '.variation_actions' ).show(); - $( '.variation_actions' ).val( 'add_variation' ); - $( '#variable_product_options' ).find( '.toolbar' ).show(); - page_nav.show(); - $( '.pagination-links', page_nav ).hide(); - } - } - }, - - /** - * Set the pagenav fields - * - * @param {Int} qty - */ - set_paginav: function( qty ) { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), - new_qty = wc_meta_boxes_product_variations_pagenav.update_variations_count( qty ), - toolbar = $( '#variable_product_options' ).find( '.toolbar' ), - variation_action = $( '.variation_actions' ), - page_nav = $( '.variations-pagenav' ), - displaying_links = $( '.pagination-links', page_nav ), - total_pages = Math.ceil( new_qty / woocommerce_admin_meta_boxes_variations.variations_per_page ), - options = ''; - - // Set the new total of pages - wrapper.attr( 'data-total_pages', total_pages ); - - $( '.total-pages', page_nav ).text( total_pages ); - - // Set the new pagenav options - for ( var i = 1; i <= total_pages; i++ ) { - options += ''; - } - - $( '.page-selector', page_nav ).empty().html( options ); - - // Show/hide pagenav - if ( 0 === new_qty ) { - toolbar.not( '.toolbar-top, .toolbar-buttons' ).hide(); - page_nav.hide(); - $( 'option, optgroup', variation_action ).hide(); - $( '.variation_actions' ).val( 'add_variation' ); - $( 'option[data-global="true"]', variation_action ).show(); - - } else { - toolbar.show(); - page_nav.show(); - $( 'option, optgroup', variation_action ).show(); - $( '.variation_actions' ).val( 'add_variation' ); - - // Show/hide links - if ( 1 === total_pages ) { - displaying_links.hide(); - } else { - displaying_links.show(); - } - } - }, - - /** - * Check button if enabled and if don't have changes - * - * @return {Bool} - */ - check_is_enabled: function( current ) { - return ! $( current ).hasClass( 'disabled' ); - }, - - /** - * Change "disabled" class on pagenav - */ - change_classes: function( selected, total ) { - var first_page = $( '.variations-pagenav .first-page' ), - prev_page = $( '.variations-pagenav .prev-page' ), - next_page = $( '.variations-pagenav .next-page' ), - last_page = $( '.variations-pagenav .last-page' ); - - if ( 1 === selected ) { - first_page.addClass( 'disabled' ); - prev_page.addClass( 'disabled' ); - } else { - first_page.removeClass( 'disabled' ); - prev_page.removeClass( 'disabled' ); - } - - if ( total === selected ) { - next_page.addClass( 'disabled' ); - last_page.addClass( 'disabled' ); - } else { - next_page.removeClass( 'disabled' ); - last_page.removeClass( 'disabled' ); - } - }, - - /** - * Set page - */ - set_page: function( page ) { - $( '.variations-pagenav .page-selector' ).val( page ).first().trigger( 'change' ); - }, - - /** - * Navigate on variations pages - * - * @param {Int} page - * @param {Int} qty - */ - go_to_page: function( page, qty ) { - page = page || 1; - qty = qty || 0; - - wc_meta_boxes_product_variations_pagenav.set_paginav( qty ); - wc_meta_boxes_product_variations_pagenav.set_page( page ); - }, - - /** - * Paginav pagination selector - */ - page_selector: function() { - var selected = parseInt( $( this ).val(), 10 ), - wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ); - - $( '.variations-pagenav .page-selector' ).val( selected ); - - wc_meta_boxes_product_variations_ajax.check_for_changes(); - wc_meta_boxes_product_variations_pagenav.change_classes( selected, parseInt( wrapper.attr( 'data-total_pages' ), 10 ) ); - wc_meta_boxes_product_variations_ajax.load_variations( selected ); - }, - - /** - * Go to first page - * - * @return {Bool} - */ - first_page: function() { - if ( wc_meta_boxes_product_variations_pagenav.check_is_enabled( this ) ) { - wc_meta_boxes_product_variations_pagenav.set_page( 1 ); - } - - return false; - }, - - /** - * Go to previous page - * - * @return {Bool} - */ - prev_page: function() { - if ( wc_meta_boxes_product_variations_pagenav.check_is_enabled( this ) ) { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), - prev_page = parseInt( wrapper.attr( 'data-page' ), 10 ) - 1, - new_page = ( 0 < prev_page ) ? prev_page : 1; - - wc_meta_boxes_product_variations_pagenav.set_page( new_page ); - } - - return false; - }, - - /** - * Go to next page - * - * @return {Bool} - */ - next_page: function() { - if ( wc_meta_boxes_product_variations_pagenav.check_is_enabled( this ) ) { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), - total_pages = parseInt( wrapper.attr( 'data-total_pages' ), 10 ), - next_page = parseInt( wrapper.attr( 'data-page' ), 10 ) + 1, - new_page = ( total_pages >= next_page ) ? next_page : total_pages; - - wc_meta_boxes_product_variations_pagenav.set_page( new_page ); - } - - return false; - }, - - /** - * Go to last page - * - * @return {Bool} - */ - last_page: function() { - if ( wc_meta_boxes_product_variations_pagenav.check_is_enabled( this ) ) { - var last_page = $( '#variable_product_options' ).find( '.woocommerce_variations' ).attr( 'data-total_pages' ); - - wc_meta_boxes_product_variations_pagenav.set_page( last_page ); - } - - return false; - } - }; - - wc_meta_boxes_product_variations_actions.init(); - wc_meta_boxes_product_variations_media.init(); - wc_meta_boxes_product_variations_ajax.init(); - wc_meta_boxes_product_variations_pagenav.init(); - -}); diff --git a/assets/js/admin/meta-boxes-product.js b/assets/js/admin/meta-boxes-product.js deleted file mode 100644 index 6d00cbc7a7b..00000000000 --- a/assets/js/admin/meta-boxes-product.js +++ /dev/null @@ -1,691 +0,0 @@ -/*global woocommerce_admin_meta_boxes */ -jQuery( function( $ ) { - - // Scroll to first checked category - // https://github.com/scribu/wp-category-checklist-tree/blob/d1c3c1f449e1144542efa17dde84a9f52ade1739/category-checklist-tree.php - $( function() { - $( '[id$="-all"] > ul.categorychecklist' ).each( function() { - var $list = $( this ); - var $firstChecked = $list.find( ':checked' ).first(); - - if ( ! $firstChecked.length ) { - return; - } - - var pos_first = $list.find( 'input' ).position().top; - var pos_checked = $firstChecked.position().top; - - $list.closest( '.tabs-panel' ).scrollTop( pos_checked - pos_first + 5 ); - }); - }); - - // Prevent enter submitting post form. - $( '#upsell_product_data' ).on( 'keypress', function( e ) { - if ( e.keyCode === 13 ) { - return false; - } - }); - - // Type box. - if ( $( 'body' ).hasClass( 'wc-wp-version-gte-55' ) ) { - $( '.type_box' ).appendTo( '#woocommerce-product-data .hndle' ); - } else { - $( '.type_box' ).appendTo( '#woocommerce-product-data .hndle span' ); - } - - $( function() { - // Prevent inputs in meta box headings opening/closing contents. - $( '#woocommerce-product-data' ).find( '.hndle' ).unbind( 'click.postboxes' ); - - $( '#woocommerce-product-data' ).on( 'click', '.hndle', function( event ) { - - // If the user clicks on some form input inside the h3 the box should not be toggled. - if ( $( event.target ).filter( 'input, option, label, select' ).length ) { - return; - } - - $( '#woocommerce-product-data' ).toggleClass( 'closed' ); - }); - }); - - // Catalog Visibility. - $( '#catalog-visibility' ).find( '.edit-catalog-visibility' ).on( 'click', function() { - if ( $( '#catalog-visibility-select' ).is( ':hidden' ) ) { - $( '#catalog-visibility-select' ).slideDown( 'fast' ); - $( this ).hide(); - } - return false; - }); - $( '#catalog-visibility' ).find( '.save-post-visibility' ).on( 'click', function() { - $( '#catalog-visibility-select' ).slideUp( 'fast' ); - $( '#catalog-visibility' ).find( '.edit-catalog-visibility' ).show(); - - var label = $( 'input[name=_visibility]:checked' ).attr( 'data-label' ); - - if ( $( 'input[name=_featured]' ).is( ':checked' ) ) { - label = label + ', ' + woocommerce_admin_meta_boxes.featured_label; - $( 'input[name=_featured]' ).attr( 'checked', 'checked' ); - } - - $( '#catalog-visibility-display' ).text( label ); - return false; - }); - $( '#catalog-visibility' ).find( '.cancel-post-visibility' ).on( 'click', function() { - $( '#catalog-visibility-select' ).slideUp( 'fast' ); - $( '#catalog-visibility' ).find( '.edit-catalog-visibility' ).show(); - - var current_visibility = $( '#current_visibility' ).val(); - var current_featured = $( '#current_featured' ).val(); - - $( 'input[name=_visibility]' ).removeAttr( 'checked' ); - $( 'input[name=_visibility][value=' + current_visibility + ']' ).attr( 'checked', 'checked' ); - - var label = $( 'input[name=_visibility]:checked' ).attr( 'data-label' ); - - if ( 'yes' === current_featured ) { - label = label + ', ' + woocommerce_admin_meta_boxes.featured_label; - $( 'input[name=_featured]' ).attr( 'checked', 'checked' ); - } else { - $( 'input[name=_featured]' ).removeAttr( 'checked' ); - } - - $( '#catalog-visibility-display' ).text( label ); - return false; - }); - - // Product type specific options. - $( 'select#product-type' ).change( function() { - - // Get value. - var select_val = $( this ).val(); - - if ( 'variable' === select_val ) { - $( 'input#_manage_stock' ).trigger( 'change' ); - $( 'input#_downloadable' ).prop( 'checked', false ); - $( 'input#_virtual' ).removeAttr( 'checked' ); - } else if ( 'grouped' === select_val ) { - $( 'input#_downloadable' ).prop( 'checked', false ); - $( 'input#_virtual' ).removeAttr( 'checked' ); - } else if ( 'external' === select_val ) { - $( 'input#_downloadable' ).prop( 'checked', false ); - $( 'input#_virtual' ).removeAttr( 'checked' ); - } - - show_and_hide_panels(); - - $( 'ul.wc-tabs li:visible' ).eq( 0 ).find( 'a' ).trigger( 'click' ); - - $( document.body ).trigger( 'woocommerce-product-type-change', select_val, $( this ) ); - - }).trigger( 'change' ); - - $( 'input#_downloadable, input#_virtual' ).change( function() { - show_and_hide_panels(); - }); - - function show_and_hide_panels() { - var product_type = $( 'select#product-type' ).val(); - var is_virtual = $( 'input#_virtual:checked' ).length; - var is_downloadable = $( 'input#_downloadable:checked' ).length; - - // Hide/Show all with rules. - var hide_classes = '.hide_if_downloadable, .hide_if_virtual'; - var show_classes = '.show_if_downloadable, .show_if_virtual'; - - $.each( woocommerce_admin_meta_boxes.product_types, function( index, value ) { - hide_classes = hide_classes + ', .hide_if_' + value; - show_classes = show_classes + ', .show_if_' + value; - }); - - $( hide_classes ).show(); - $( show_classes ).hide(); - - // Shows rules. - if ( is_downloadable ) { - $( '.show_if_downloadable' ).show(); - } - if ( is_virtual ) { - $( '.show_if_virtual' ).show(); - - // If user enables virtual while on shipping tab, switch to general tab. - if ( $( '.shipping_options.shipping_tab' ).hasClass( 'active' ) ) { - $( '.general_options.general_tab > a' ).trigger( 'click' ); - } - } - - $( '.show_if_' + product_type ).show(); - - // Hide rules. - if ( is_downloadable ) { - $( '.hide_if_downloadable' ).hide(); - } - if ( is_virtual ) { - $( '.hide_if_virtual' ).hide(); - } - - $( '.hide_if_' + product_type ).hide(); - - $( 'input#_manage_stock' ).trigger( 'change' ); - - // Hide empty panels/tabs after display. - $( '.woocommerce_options_panel' ).each( function() { - var $children = $( this ).children( '.options_group' ); - - if ( 0 === $children.length ) { - return; - } - - var $invisble = $children.filter( function() { - return 'none' === $( this ).css( 'display' ); - }); - - // Hide panel. - if ( $invisble.length === $children.length ) { - var $id = $( this ).prop( 'id' ); - $( '.product_data_tabs' ).find( 'li a[href="#' + $id + '"]' ).parent().hide(); - } - }); - } - - // Sale price schedule. - $( '.sale_price_dates_fields' ).each( function() { - var $these_sale_dates = $( this ); - var sale_schedule_set = false; - var $wrap = $these_sale_dates.closest( 'div, table' ); - - $these_sale_dates.find( 'input' ).each( function() { - if ( '' !== $( this ).val() ) { - sale_schedule_set = true; - } - }); - - if ( sale_schedule_set ) { - $wrap.find( '.sale_schedule' ).hide(); - $wrap.find( '.sale_price_dates_fields' ).show(); - } else { - $wrap.find( '.sale_schedule' ).show(); - $wrap.find( '.sale_price_dates_fields' ).hide(); - } - }); - - $( '#woocommerce-product-data' ).on( 'click', '.sale_schedule', function() { - var $wrap = $( this ).closest( 'div, table' ); - - $( this ).hide(); - $wrap.find( '.cancel_sale_schedule' ).show(); - $wrap.find( '.sale_price_dates_fields' ).show(); - - return false; - }); - $( '#woocommerce-product-data' ).on( 'click', '.cancel_sale_schedule', function() { - var $wrap = $( this ).closest( 'div, table' ); - - $( this ).hide(); - $wrap.find( '.sale_schedule' ).show(); - $wrap.find( '.sale_price_dates_fields' ).hide(); - $wrap.find( '.sale_price_dates_fields' ).find( 'input' ).val(''); - - return false; - }); - - // File inputs. - $( '#woocommerce-product-data' ).on( 'click','.downloadable_files a.insert', function() { - $( this ).closest( '.downloadable_files' ).find( 'tbody' ).append( $( this ).data( 'row' ) ); - return false; - }); - $( '#woocommerce-product-data' ).on( 'click','.downloadable_files a.delete',function() { - $( this ).closest( 'tr' ).remove(); - return false; - }); - - // Stock options. - $( 'input#_manage_stock' ).change( function() { - if ( $( this ).is( ':checked' ) ) { - $( 'div.stock_fields' ).show(); - $( 'p.stock_status_field' ).hide(); - } else { - var product_type = $( 'select#product-type' ).val(); - - $( 'div.stock_fields' ).hide(); - $( 'p.stock_status_field:not( .hide_if_' + product_type + ' )' ).show(); - } - - $( 'input.variable_manage_stock' ).trigger( 'change' ); - }).trigger( 'change' ); - - // Date picker fields. - function date_picker_select( datepicker ) { - var option = $( datepicker ).next().is( '.hasDatepicker' ) ? 'minDate' : 'maxDate', - otherDateField = 'minDate' === option ? $( datepicker ).next() : $( datepicker ).prev(), - date = $( datepicker ).datepicker( 'getDate' ); - - $( otherDateField ).datepicker( 'option', option, date ); - $( datepicker ).trigger( 'change' ); - } - - $( '.sale_price_dates_fields' ).each( function() { - $( this ).find( 'input' ).datepicker({ - defaultDate: '', - dateFormat: 'yy-mm-dd', - numberOfMonths: 1, - showButtonPanel: true, - onSelect: function() { - date_picker_select( $( this ) ); - } - }); - $( this ).find( 'input' ).each( function() { date_picker_select( $( this ) ); } ); - }); - - // Attribute Tables. - - // Initial order. - var woocommerce_attribute_items = $( '.product_attributes' ).find( '.woocommerce_attribute' ).get(); - - woocommerce_attribute_items.sort( function( a, b ) { - var compA = parseInt( $( a ).attr( 'rel' ), 10 ); - var compB = parseInt( $( b ).attr( 'rel' ), 10 ); - return ( compA < compB ) ? -1 : ( compA > compB ) ? 1 : 0; - }); - $( woocommerce_attribute_items ).each( function( index, el ) { - $( '.product_attributes' ).append( el ); - }); - - function attribute_row_indexes() { - $( '.product_attributes .woocommerce_attribute' ).each( function( index, el ) { - $( '.attribute_position', el ).val( parseInt( $( el ).index( '.product_attributes .woocommerce_attribute' ), 10 ) ); - }); - } - - $( '.product_attributes .woocommerce_attribute' ).each( function( index, el ) { - if ( $( el ).css( 'display' ) !== 'none' && $( el ).is( '.taxonomy' ) ) { - $( 'select.attribute_taxonomy' ).find( 'option[value="' + $( el ).data( 'taxonomy' ) + '"]' ).attr( 'disabled', 'disabled' ); - } - }); - - // Add rows. - $( 'button.add_attribute' ).on( 'click', function() { - var size = $( '.product_attributes .woocommerce_attribute' ).length; - var attribute = $( 'select.attribute_taxonomy' ).val(); - var $wrapper = $( this ).closest( '#product_attributes' ); - var $attributes = $wrapper.find( '.product_attributes' ); - var product_type = $( 'select#product-type' ).val(); - var data = { - action: 'woocommerce_add_attribute', - taxonomy: attribute, - i: size, - security: woocommerce_admin_meta_boxes.add_attribute_nonce - }; - - $wrapper.block({ - message: null, - overlayCSS: { - background: '#fff', - opacity: 0.6 - } - }); - - $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { - $attributes.append( response ); - - if ( 'variable' !== product_type ) { - $attributes.find( '.enable_variation' ).hide(); - } - - $( document.body ).trigger( 'wc-enhanced-select-init' ); - - attribute_row_indexes(); - - $attributes.find( '.woocommerce_attribute' ).last().find( 'h3' ).trigger( 'click' ); - - $wrapper.unblock(); - - $( document.body ).trigger( 'woocommerce_added_attribute' ); - }); - - if ( attribute ) { - $( 'select.attribute_taxonomy' ).find( 'option[value="' + attribute + '"]' ).attr( 'disabled','disabled' ); - $( 'select.attribute_taxonomy' ).val( '' ); - } - - return false; - }); - - $( '.product_attributes' ).on( 'blur', 'input.attribute_name', function() { - $( this ).closest( '.woocommerce_attribute' ).find( 'strong.attribute_name' ).text( $( this ).val() ); - }); - - $( '.product_attributes' ).on( 'click', 'button.select_all_attributes', function() { - $( this ).closest( 'td' ).find( 'select option' ).prop( 'selected', 'selected' ); - $( this ).closest( 'td' ).find( 'select' ).trigger( 'change' ); - return false; - }); - - $( '.product_attributes' ).on( 'click', 'button.select_no_attributes', function() { - $( this ).closest( 'td' ).find( 'select option' ).removeAttr( 'selected' ); - $( this ).closest( 'td' ).find( 'select' ).trigger( 'change' ); - return false; - }); - - $( '.product_attributes' ).on( 'click', '.remove_row', function() { - if ( window.confirm( woocommerce_admin_meta_boxes.remove_attribute ) ) { - var $parent = $( this ).parent().parent(); - - if ( $parent.is( '.taxonomy' ) ) { - $parent.find( 'select, input[type=text]' ).val( '' ); - $parent.hide(); - $( 'select.attribute_taxonomy' ).find( 'option[value="' + $parent.data( 'taxonomy' ) + '"]' ).removeAttr( 'disabled' ); - } else { - $parent.find( 'select, input[type=text]' ).val( '' ); - $parent.hide(); - attribute_row_indexes(); - } - } - return false; - }); - - // Attribute ordering. - $( '.product_attributes' ).sortable({ - items: '.woocommerce_attribute', - cursor: 'move', - axis: 'y', - handle: 'h3', - scrollSensitivity: 40, - forcePlaceholderSize: true, - helper: 'clone', - opacity: 0.65, - placeholder: 'wc-metabox-sortable-placeholder', - start: function( event, ui ) { - ui.item.css( 'background-color', '#f6f6f6' ); - }, - stop: function( event, ui ) { - ui.item.removeAttr( 'style' ); - attribute_row_indexes(); - } - }); - - // Add a new attribute (via ajax). - $( '.product_attributes' ).on( 'click', 'button.add_new_attribute', function() { - - $( '.product_attributes' ).block({ - message: null, - overlayCSS: { - background: '#fff', - opacity: 0.6 - } - }); - - var $wrapper = $( this ).closest( '.woocommerce_attribute' ); - var attribute = $wrapper.data( 'taxonomy' ); - var new_attribute_name = window.prompt( woocommerce_admin_meta_boxes.new_attribute_prompt ); - - if ( new_attribute_name ) { - - var data = { - action: 'woocommerce_add_new_attribute', - taxonomy: attribute, - term: new_attribute_name, - security: woocommerce_admin_meta_boxes.add_attribute_nonce - }; - - $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { - - if ( response.error ) { - // Error. - window.alert( response.error ); - } else if ( response.slug ) { - // Success. - $wrapper.find( 'select.attribute_values' ) - .append( '' ); - $wrapper.find( 'select.attribute_values' ).trigger( 'change' ); - } - - $( '.product_attributes' ).unblock(); - }); - - } else { - $( '.product_attributes' ).unblock(); - } - - return false; - }); - - // Save attributes and update variations. - $( '.save_attributes' ).on( 'click', function() { - - $( '.product_attributes' ).block({ - message: null, - overlayCSS: { - background: '#fff', - opacity: 0.6 - } - }); - var original_data = $( '.product_attributes' ).find( 'input, select, textarea' ); - var data = { - post_id : woocommerce_admin_meta_boxes.post_id, - product_type: $( '#product-type' ).val(), - data : original_data.serialize(), - action : 'woocommerce_save_attributes', - security : woocommerce_admin_meta_boxes.save_attributes_nonce - }; - - $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { - if ( response.error ) { - // Error. - window.alert( response.error ); - } else if ( response.data ) { - // Success. - $( '.product_attributes' ).html( response.data.html ); - $( '.product_attributes' ).unblock(); - - // Hide the 'Used for variations' checkbox if not viewing a variable product - show_and_hide_panels(); - - // Make sure the dropdown is not disabled for empty value attributes. - $( 'select.attribute_taxonomy' ).find( 'option' ).prop( 'disabled', false ); - - $( '.product_attributes .woocommerce_attribute' ).each( function( index, el ) { - if ( $( el ).css( 'display' ) !== 'none' && $( el ).is( '.taxonomy' ) ) { - $( 'select.attribute_taxonomy' ) - .find( 'option[value="' + $( el ).data( 'taxonomy' ) + '"]' ) - .prop( 'disabled', true ); - } - }); - - // Reload variations panel. - var this_page = window.location.toString(); - this_page = this_page.replace( 'post-new.php?', 'post.php?post=' + woocommerce_admin_meta_boxes.post_id + '&action=edit&' ); - - $( '#variable_product_options' ).load( this_page + ' #variable_product_options_inner', function() { - $( '#variable_product_options' ).trigger( 'reload' ); - } ); - } - }); - }); - - // Uploading files. - var downloadable_file_frame; - var file_path_field; - - $( document.body ).on( 'click', '.upload_file_button', function( event ) { - var $el = $( this ); - - file_path_field = $el.closest( 'tr' ).find( 'td.file_url input' ); - - event.preventDefault(); - - // If the media frame already exists, reopen it. - if ( downloadable_file_frame ) { - downloadable_file_frame.open(); - return; - } - - var downloadable_file_states = [ - // Main states. - new wp.media.controller.Library({ - library: wp.media.query(), - multiple: true, - title: $el.data('choose'), - priority: 20, - filterable: 'uploaded' - }) - ]; - - // Create the media frame. - downloadable_file_frame = wp.media.frames.downloadable_file = wp.media({ - // Set the title of the modal. - title: $el.data('choose'), - library: { - type: '' - }, - button: { - text: $el.data('update') - }, - multiple: true, - states: downloadable_file_states - }); - - // When an image is selected, run a callback. - downloadable_file_frame.on( 'select', function() { - var file_path = ''; - var selection = downloadable_file_frame.state().get( 'selection' ); - - selection.map( function( attachment ) { - attachment = attachment.toJSON(); - if ( attachment.url ) { - file_path = attachment.url; - } - }); - - file_path_field.val( file_path ).trigger( 'change' ); - }); - - // Set post to 0 and set our custom type. - downloadable_file_frame.on( 'ready', function() { - downloadable_file_frame.uploader.options.uploader.params = { - type: 'downloadable_product' - }; - }); - - // Finally, open the modal. - downloadable_file_frame.open(); - }); - - // Download ordering. - $( '.downloadable_files tbody' ).sortable({ - items: 'tr', - cursor: 'move', - axis: 'y', - handle: 'td.sort', - scrollSensitivity: 40, - forcePlaceholderSize: true, - helper: 'clone', - opacity: 0.65 - }); - - // Product gallery file uploads. - var product_gallery_frame; - var $image_gallery_ids = $( '#product_image_gallery' ); - var $product_images = $( '#product_images_container' ).find( 'ul.product_images' ); - - $( '.add_product_images' ).on( 'click', 'a', function( event ) { - var $el = $( this ); - - event.preventDefault(); - - // If the media frame already exists, reopen it. - if ( product_gallery_frame ) { - product_gallery_frame.open(); - return; - } - - // Create the media frame. - product_gallery_frame = wp.media.frames.product_gallery = wp.media({ - // Set the title of the modal. - title: $el.data( 'choose' ), - button: { - text: $el.data( 'update' ) - }, - states: [ - new wp.media.controller.Library({ - title: $el.data( 'choose' ), - filterable: 'all', - multiple: true - }) - ] - }); - - // When an image is selected, run a callback. - product_gallery_frame.on( 'select', function() { - var selection = product_gallery_frame.state().get( 'selection' ); - var attachment_ids = $image_gallery_ids.val(); - - selection.map( function( attachment ) { - attachment = attachment.toJSON(); - - if ( attachment.id ) { - attachment_ids = attachment_ids ? attachment_ids + ',' + attachment.id : attachment.id; - var attachment_image = attachment.sizes && attachment.sizes.thumbnail ? attachment.sizes.thumbnail.url : attachment.url; - - $product_images.append( - '
  • ' - ); - } - }); - - $image_gallery_ids.val( attachment_ids ); - }); - - // Finally, open the modal. - product_gallery_frame.open(); - }); - - // Image ordering. - $product_images.sortable({ - items: 'li.image', - cursor: 'move', - scrollSensitivity: 40, - forcePlaceholderSize: true, - forceHelperSize: false, - helper: 'clone', - opacity: 0.65, - placeholder: 'wc-metabox-sortable-placeholder', - start: function( event, ui ) { - ui.item.css( 'background-color', '#f6f6f6' ); - }, - stop: function( event, ui ) { - ui.item.removeAttr( 'style' ); - }, - update: function() { - var attachment_ids = ''; - - $( '#product_images_container' ).find( 'ul li.image' ).css( 'cursor', 'default' ).each( function() { - var attachment_id = $( this ).attr( 'data-attachment_id' ); - attachment_ids = attachment_ids + attachment_id + ','; - }); - - $image_gallery_ids.val( attachment_ids ); - } - }); - - // Remove images. - $( '#product_images_container' ).on( 'click', 'a.delete', function() { - $( this ).closest( 'li.image' ).remove(); - - var attachment_ids = ''; - - $( '#product_images_container' ).find( 'ul li.image' ).css( 'cursor', 'default' ).each( function() { - var attachment_id = $( this ).attr( 'data-attachment_id' ); - attachment_ids = attachment_ids + attachment_id + ','; - }); - - $image_gallery_ids.val( attachment_ids ); - - // Remove any lingering tooltips. - $( '#tiptip_holder' ).removeAttr( 'style' ); - $( '#tiptip_arrow' ).removeAttr( 'style' ); - - return false; - }); -}); diff --git a/assets/js/admin/meta-boxes.js b/assets/js/admin/meta-boxes.js deleted file mode 100644 index 1f82968994f..00000000000 --- a/assets/js/admin/meta-boxes.js +++ /dev/null @@ -1,68 +0,0 @@ -jQuery( function ( $ ) { - - // Run tipTip - function runTipTip() { - // Remove any lingering tooltips - $( '#tiptip_holder' ).removeAttr( 'style' ); - $( '#tiptip_arrow' ).removeAttr( 'style' ); - $( '.tips' ).tipTip({ - 'attribute': 'data-tip', - 'fadeIn': 50, - 'fadeOut': 50, - 'delay': 200, - 'keepAlive': true - }); - } - - runTipTip(); - - $( '.wc-metaboxes-wrapper' ).on( 'click', '.wc-metabox > h3', function() { - $( this ).parent( '.wc-metabox' ).toggleClass( 'closed' ).toggleClass( 'open' ); - }); - - // Tabbed Panels - $( document.body ).on( 'wc-init-tabbed-panels', function() { - $( 'ul.wc-tabs' ).show(); - $( 'ul.wc-tabs a' ).on( 'click', function( e ) { - e.preventDefault(); - var panel_wrap = $( this ).closest( 'div.panel-wrap' ); - $( 'ul.wc-tabs li', panel_wrap ).removeClass( 'active' ); - $( this ).parent().addClass( 'active' ); - $( 'div.panel', panel_wrap ).hide(); - $( $( this ).attr( 'href' ) ).show(); - }); - $( 'div.panel-wrap' ).each( function() { - $( this ).find( 'ul.wc-tabs li' ).eq( 0 ).find( 'a' ).trigger( 'click' ); - }); - }).trigger( 'wc-init-tabbed-panels' ); - - // Date Picker - $( document.body ).on( 'wc-init-datepickers', function() { - $( '.date-picker-field, .date-picker' ).datepicker({ - dateFormat: 'yy-mm-dd', - numberOfMonths: 1, - showButtonPanel: true - }); - }).trigger( 'wc-init-datepickers' ); - - // Meta-Boxes - Open/close - $( '.wc-metaboxes-wrapper' ).on( 'click', '.wc-metabox h3', function( event ) { - // If the user clicks on some form input inside the h3, like a select list (for variations), the box should not be toggled - if ( $( event.target ).filter( ':input, option, .sort' ).length ) { - return; - } - - $( this ).next( '.wc-metabox-content' ).stop().slideToggle(); - }) - .on( 'click', '.expand_all', function() { - $( this ).closest( '.wc-metaboxes-wrapper' ).find( '.wc-metabox > .wc-metabox-content' ).show(); - return false; - }) - .on( 'click', '.close_all', function() { - $( this ).closest( '.wc-metaboxes-wrapper' ).find( '.wc-metabox > .wc-metabox-content' ).hide(); - return false; - }); - $( '.wc-metabox.closed' ).each( function() { - $( this ).find( '.wc-metabox-content' ).hide(); - }); -}); diff --git a/assets/js/admin/quick-edit.js b/assets/js/admin/quick-edit.js deleted file mode 100644 index 8c64a049f33..00000000000 --- a/assets/js/admin/quick-edit.js +++ /dev/null @@ -1,167 +0,0 @@ -/*global inlineEditPost, woocommerce_admin, woocommerce_quick_edit */ -jQuery( - function( $ ) { - $( '#the-list' ).on( - 'click', - '.editinline', - function() { - - inlineEditPost.revert(); - - var post_id = $( this ).closest( 'tr' ).attr( 'id' ); - - post_id = post_id.replace( 'post-', '' ); - - var $wc_inline_data = $( '#woocommerce_inline_' + post_id ); - - var sku = $wc_inline_data.find( '.sku' ).text(), - regular_price = $wc_inline_data.find( '.regular_price' ).text(), - sale_price = $wc_inline_data.find( '.sale_price ' ).text(), - weight = $wc_inline_data.find( '.weight' ).text(), - length = $wc_inline_data.find( '.length' ).text(), - width = $wc_inline_data.find( '.width' ).text(), - height = $wc_inline_data.find( '.height' ).text(), - shipping_class = $wc_inline_data.find( '.shipping_class' ).text(), - visibility = $wc_inline_data.find( '.visibility' ).text(), - stock_status = $wc_inline_data.find( '.stock_status' ).text(), - stock = $wc_inline_data.find( '.stock' ).text(), - featured = $wc_inline_data.find( '.featured' ).text(), - manage_stock = $wc_inline_data.find( '.manage_stock' ).text(), - menu_order = $wc_inline_data.find( '.menu_order' ).text(), - tax_status = $wc_inline_data.find( '.tax_status' ).text(), - tax_class = $wc_inline_data.find( '.tax_class' ).text(), - backorders = $wc_inline_data.find( '.backorders' ).text(), - product_type = $wc_inline_data.find( '.product_type' ).text(); - - var formatted_regular_price = regular_price.replace( '.', woocommerce_admin.mon_decimal_point ), - formatted_sale_price = sale_price.replace( '.', woocommerce_admin.mon_decimal_point ); - - $( 'input[name="_sku"]', '.inline-edit-row' ).val( sku ); - $( 'input[name="_regular_price"]', '.inline-edit-row' ).val( formatted_regular_price ); - $( 'input[name="_sale_price"]', '.inline-edit-row' ).val( formatted_sale_price ); - $( 'input[name="_weight"]', '.inline-edit-row' ).val( weight ); - $( 'input[name="_length"]', '.inline-edit-row' ).val( length ); - $( 'input[name="_width"]', '.inline-edit-row' ).val( width ); - $( 'input[name="_height"]', '.inline-edit-row' ).val( height ); - - $( 'select[name="_shipping_class"] option:selected', '.inline-edit-row' ).attr( 'selected', false ).trigger( 'change' ); - $( 'select[name="_shipping_class"] option[value="' + shipping_class + '"]' ).attr( 'selected', 'selected' ) - .trigger( 'change' ); - - $( 'input[name="_stock"]', '.inline-edit-row' ).val( stock ); - $( 'input[name="menu_order"]', '.inline-edit-row' ).val( menu_order ); - - $( - 'select[name="_tax_status"] option, ' + - 'select[name="_tax_class"] option, ' + - 'select[name="_visibility"] option, ' + - 'select[name="_stock_status"] option, ' + - 'select[name="_backorders"] option' - ).removeAttr( 'selected' ); - - var is_variable_product = 'variable' === product_type; - $( 'select[name="_stock_status"] ~ .wc-quick-edit-warning', '.inline-edit-row' ).toggle( is_variable_product ); - $( 'select[name="_stock_status"] option[value="' + (is_variable_product ? '' : stock_status) + '"]', '.inline-edit-row' ) - .attr( 'selected', 'selected' ); - - $( 'select[name="_tax_status"] option[value="' + tax_status + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' ); - $( 'select[name="_tax_class"] option[value="' + tax_class + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' ); - $( 'select[name="_visibility"] option[value="' + visibility + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' ); - $( 'select[name="_backorders"] option[value="' + backorders + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' ); - - if ( 'yes' === featured ) { - $( 'input[name="_featured"]', '.inline-edit-row' ).attr( 'checked', 'checked' ); - } else { - $( 'input[name="_featured"]', '.inline-edit-row' ).removeAttr( 'checked' ); - } - - // Conditional display. - var product_is_virtual = $wc_inline_data.find( '.product_is_virtual' ).text(); - - var product_supports_stock_status = 'external' !== product_type; - var product_supports_stock_fields = 'external' !== product_type && 'grouped' !== product_type; - - $( '.stock_fields, .manage_stock_field, .stock_status_field, .backorder_field' ).show(); - - if ( product_supports_stock_fields ) { - if ( 'yes' === manage_stock ) { - $( '.stock_qty_field, .backorder_field', '.inline-edit-row' ).show().removeAttr( 'style' ); - $( '.stock_status_field' ).hide(); - $( '.manage_stock_field input' ).prop( 'checked', true ); - } else { - $( '.stock_qty_field, .backorder_field', '.inline-edit-row' ).hide(); - $( '.stock_status_field' ).show().removeAttr( 'style' ); - $( '.manage_stock_field input' ).prop( 'checked', false ); - } - } else if ( product_supports_stock_status ) { - $( '.stock_fields, .manage_stock_field, .backorder_field' ).hide(); - } else { - $( '.stock_fields, .manage_stock_field, .stock_status_field, .backorder_field' ).hide(); - } - - if ( 'simple' === product_type || 'external' === product_type ) { - $( '.price_fields', '.inline-edit-row' ).show().removeAttr( 'style' ); - } else { - $( '.price_fields', '.inline-edit-row' ).hide(); - } - - if ( 'yes' === product_is_virtual ) { - $( '.dimension_fields', '.inline-edit-row' ).hide(); - } else { - $( '.dimension_fields', '.inline-edit-row' ).show().removeAttr( 'style' ); - } - - // Rename core strings. - $( 'input[name="comment_status"]' ).parent().find( '.checkbox-title' ).text( woocommerce_quick_edit.strings.allow_reviews ); - } - ); - - $( '#the-list' ).on( - 'change', - '.inline-edit-row input[name="_manage_stock"]', - function() { - - if ( $( this ).is( ':checked' ) ) { - $( '.stock_qty_field, .backorder_field', '.inline-edit-row' ).show().removeAttr( 'style' ); - $( '.stock_status_field' ).hide(); - } else { - $( '.stock_qty_field, .backorder_field', '.inline-edit-row' ).hide(); - $( '.stock_status_field' ).show().removeAttr( 'style' ); - } - - } - ); - - $( '#wpbody' ).on( - 'click', - '#doaction, #doaction2', - function() { - $( 'input.text', '.inline-edit-row' ).val( '' ); - $( '#woocommerce-fields' ).find( 'select' ).prop( 'selectedIndex', 0 ); - $( '#woocommerce-fields-bulk' ).find( '.inline-edit-group .change-input' ).hide(); - } - ); - - $( '#wpbody' ).on( - 'change', - '#woocommerce-fields-bulk .inline-edit-group .change_to', - function() { - - if ( 0 < $( this ).val() ) { - $( this ).closest( 'div' ).find( '.change-input' ).show(); - } else { - $( this ).closest( 'div' ).find( '.change-input' ).hide(); - } - - } - ); - - $( '#wpbody' ).on( - 'click', - '.trash-product', - function() { - return window.confirm( woocommerce_admin.i18n_delete_product_notice ); - } - ); - } -); diff --git a/assets/js/admin/settings-views-html-settings-tax.js b/assets/js/admin/settings-views-html-settings-tax.js deleted file mode 100644 index 061c2bd1af7..00000000000 --- a/assets/js/admin/settings-views-html-settings-tax.js +++ /dev/null @@ -1,383 +0,0 @@ -/* global htmlSettingsTaxLocalizeScript, ajaxurl */ - -/** - * Used by woocommerce/includes/admin/settings/views/html-settings-tax.php - */ -( function( $, data, wp, ajaxurl ) { - $( function() { - - if ( ! String.prototype.trim ) { - String.prototype.trim = function () { - return this.replace( /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '' ); - }; - } - - var rowTemplate = wp.template( 'wc-tax-table-row' ), - rowTemplateEmpty = wp.template( 'wc-tax-table-row-empty' ), - paginationTemplate = wp.template( 'wc-tax-table-pagination' ), - $table = $( '.wc_tax_rates' ), - $tbody = $( '#rates' ), - $save_button = $( ':input[name="save"]' ), - $pagination = $( '#rates-pagination' ), - $search_field = $( '#rates-search .wc-tax-rates-search-field' ), - $submit = $( '.submit .button-primary[type=submit]' ), - WCTaxTableModelConstructor = Backbone.Model.extend({ - changes: {}, - setRateAttribute: function( rateID, attribute, value ) { - var rates = _.indexBy( this.get( 'rates' ), 'tax_rate_id' ), - changes = {}; - - if ( rates[ rateID ][ attribute ] !== value ) { - changes[ rateID ] = {}; - changes[ rateID ][ attribute ] = value; - rates[ rateID ][ attribute ] = value; - } - - this.logChanges( changes ); - }, - logChanges: function( changedRows ) { - var changes = this.changes || {}; - - _.each( changedRows, function( row, id ) { - changes[ id ] = _.extend( changes[ id ] || { - tax_rate_id : id - }, row ); - } ); - - this.changes = changes; - this.trigger( 'change:rates' ); - }, - getFilteredRates: function() { - var rates = this.get( 'rates' ), - search = $search_field.val().toLowerCase(); - - if ( search.length ) { - rates = _.filter( rates, function( rate ) { - var search_text = _.toArray( rate ).join( ' ' ).toLowerCase(); - return ( -1 !== search_text.indexOf( search ) ); - } ); - } - - rates = _.sortBy( rates, function( rate ) { - return parseInt( rate.tax_rate_order, 10 ); - } ); - - return rates; - }, - block: function() { - $( '.wc_tax_rates' ).block({ - message: null, - overlayCSS: { - background: '#fff', - opacity: 0.6 - } - }); - }, - unblock: function() { - $( '.wc_tax_rates' ).unblock(); - }, - save: function() { - var self = this; - - self.block(); - - Backbone.ajax({ - method: 'POST', - dataType: 'json', - url: ajaxurl + ( ajaxurl.indexOf( '?' ) > 0 ? '&' : '?' ) + 'action=woocommerce_tax_rates_save_changes', - data: { - current_class: data.current_class, - wc_tax_nonce: data.wc_tax_nonce, - changes: self.changes - }, - success: function( response, textStatus ) { - if ( 'success' === textStatus && response.success ) { - WCTaxTableModelInstance.set( 'rates', response.data.rates ); - WCTaxTableModelInstance.trigger( 'change:rates' ); - - WCTaxTableModelInstance.changes = {}; - WCTaxTableModelInstance.trigger( 'saved:rates' ); - - // Reload view. - WCTaxTableInstance.render(); - } - - self.unblock(); - } - }); - } - } ), - WCTaxTableViewConstructor = Backbone.View.extend({ - rowTemplate: rowTemplate, - per_page: data.limit, - page: data.page, - initialize: function() { - var qty_pages = Math.ceil( _.toArray( this.model.get( 'rates' ) ).length / this.per_page ); - - this.qty_pages = 0 === qty_pages ? 1 : qty_pages; - this.page = this.sanitizePage( data.page ); - - this.listenTo( this.model, 'change:rates', this.setUnloadConfirmation ); - this.listenTo( this.model, 'saved:rates', this.clearUnloadConfirmation ); - $tbody.on( 'change autocompletechange', ':input', { view: this }, this.updateModelOnChange ); - $search_field.on( 'keyup search', { view: this }, this.onSearchField ); - $pagination.on( 'click', 'a', { view: this }, this.onPageChange ); - $pagination.on( 'change', 'input', { view: this }, this.onPageChange ); - $( window ).on( 'beforeunload', { view: this }, this.unloadConfirmation ); - $submit.on( 'click', { view: this }, this.onSubmit ); - $save_button.prop( 'disabled', true ); - - // Can bind these directly to the buttons, as they won't get overwritten. - $table.find( '.insert' ).on( 'click', { view: this }, this.onAddNewRow ); - $table.find( '.remove_tax_rates' ).on( 'click', { view: this }, this.onDeleteRow ); - $table.find( '.export' ).on( 'click', { view: this }, this.onExport ); - }, - render: function() { - var rates = this.model.getFilteredRates(), - qty_rates = _.size( rates ), - qty_pages = Math.ceil( qty_rates / this.per_page ), - first_index = 0 === qty_rates ? 0 : this.per_page * ( this.page - 1 ), - last_index = this.per_page * this.page, - paged_rates = _.toArray( rates ).slice( first_index, last_index ), - view = this; - - // Blank out the contents. - this.$el.empty(); - - if ( paged_rates.length ) { - // Populate $tbody with the current page of results. - $.each( paged_rates, function( id, rowData ) { - view.$el.append( view.rowTemplate( rowData ) ); - } ); - } else { - view.$el.append( rowTemplateEmpty() ); - } - - // Initialize autocomplete for countries. - this.$el.find( 'td.country input' ).autocomplete({ - source: data.countries, - minLength: 2 - }); - - // Initialize autocomplete for states. - this.$el.find( 'td.state input' ).autocomplete({ - source: data.states, - minLength: 3 - }); - - // Postcode and city don't have `name` values by default. - // They're only created if the contents changes, to save on database queries (I think) - this.$el.find( 'td.postcode input, td.city input' ).change( function() { - $( this ).attr( 'name', $( this ).data( 'name' ) ); - }); - - if ( qty_pages > 1 ) { - // We've now displayed our initial page, time to render the pagination box. - $pagination.html( paginationTemplate( { - qty_rates: qty_rates, - current_page: this.page, - qty_pages: qty_pages - } ) ); - } else { - $pagination.empty(); - view.page = 1; - } - }, - updateUrl: function() { - if ( ! window.history.replaceState ) { - return; - } - - var url = data.base_url, - search = $search_field.val(); - - if ( 1 < this.page ) { - url += '&p=' + encodeURIComponent( this.page ); - } - - if ( search.length ) { - url += '&s=' + encodeURIComponent( search ); - } - - window.history.replaceState( {}, '', url ); - }, - onSubmit: function( event ) { - event.data.view.model.save(); - event.preventDefault(); - }, - onAddNewRow: function( event ) { - var view = event.data.view, - model = view.model, - rates = _.indexBy( model.get( 'rates' ), 'tax_rate_id' ), - changes = {}, - size = _.size( rates ), - newRow = _.extend( {}, data.default_rate, { - tax_rate_id: 'new-' + size + '-' + Date.now(), - newRow: true - } ), - $current, current_id, current_order, rates_to_reorder, reordered_rates; - - $current = $tbody.children( '.current' ); - - if ( $current.length ) { - current_id = $current.last().data( 'id' ); - current_order = parseInt( rates[ current_id ].tax_rate_order, 10 ); - newRow.tax_rate_order = 1 + current_order; - - rates_to_reorder = _.filter( rates, function( rate ) { - if ( parseInt( rate.tax_rate_order, 10 ) > current_order ) { - return true; - } - return false; - } ); - - reordered_rates = _.map( rates_to_reorder, function( rate ) { - rate.tax_rate_order++; - changes[ rate.tax_rate_id ] = _.extend( - changes[ rate.tax_rate_id ] || {}, { tax_rate_order : rate.tax_rate_order } - ); - return rate; - } ); - } else { - newRow.tax_rate_order = 1 + _.max( - _.pluck( rates, 'tax_rate_order' ), - function ( val ) { - // Cast them all to integers, because strings compare funky. Sighhh. - return parseInt( val, 10 ); - } - ); - // Move the last page - view.page = view.qty_pages; - } - - rates[ newRow.tax_rate_id ] = newRow; - changes[ newRow.tax_rate_id ] = newRow; - - model.set( 'rates', rates ); - model.logChanges( changes ); - - view.render(); - }, - onDeleteRow: function( event ) { - var view = event.data.view, - model = view.model, - rates = _.indexBy( model.get( 'rates' ), 'tax_rate_id' ), - changes = {}, - $current, current_id; - - event.preventDefault(); - - if ( $current = $tbody.children( '.current' ) ) { - $current.each(function(){ - current_id = $( this ).data('id'); - - delete rates[ current_id ]; - - changes[ current_id ] = _.extend( changes[ current_id ] || {}, { deleted : 'deleted' } ); - }); - - model.set( 'rates', rates ); - model.logChanges( changes ); - - view.render(); - } else { - window.alert( data.strings.no_rows_selected ); - } - }, - onSearchField: function( event ){ - event.data.view.updateUrl(); - event.data.view.render(); - }, - onPageChange: function( event ) { - var $target = $( event.currentTarget ); - - event.preventDefault(); - event.data.view.page = $target.data( 'goto' ) ? $target.data( 'goto' ) : $target.val(); - event.data.view.render(); - event.data.view.updateUrl(); - }, - onExport: function( event ) { - var csv_data = 'data:application/csv;charset=utf-8,' + data.strings.csv_data_cols.join(',') + '\n'; - - $.each( event.data.view.model.getFilteredRates(), function( id, rowData ) { - var row = ''; - - row += rowData.tax_rate_country + ','; - row += rowData.tax_rate_state + ','; - row += ( rowData.postcode ? rowData.postcode.join( '; ' ) : '' ) + ','; - row += ( rowData.city ? rowData.city.join( '; ' ) : '' ) + ','; - row += rowData.tax_rate + ','; - row += rowData.tax_rate_name + ','; - row += rowData.tax_rate_priority + ','; - row += rowData.tax_rate_compound + ','; - row += rowData.tax_rate_shipping + ','; - row += data.current_class; - - csv_data += row + '\n'; - }); - - $( this ).attr( 'href', encodeURI( csv_data ) ); - - return true; - }, - setUnloadConfirmation: function() { - this.needsUnloadConfirm = true; - $save_button.prop( 'disabled', false ); - }, - clearUnloadConfirmation: function() { - this.needsUnloadConfirm = false; - $save_button.prop( 'disabled', true ); - }, - unloadConfirmation: function( event ) { - if ( event.data.view.needsUnloadConfirm ) { - event.returnValue = data.strings.unload_confirmation_msg; - window.event.returnValue = data.strings.unload_confirmation_msg; - return data.strings.unload_confirmation_msg; - } - }, - updateModelOnChange: function( event ) { - var model = event.data.view.model, - $target = $( event.target ), - id = $target.closest( 'tr' ).data( 'id' ), - attribute = $target.data( 'attribute' ), - val = $target.val(); - - if ( 'city' === attribute || 'postcode' === attribute ) { - val = val.split( ';' ); - val = $.map( val, function( thing ) { - return thing.trim(); - }); - } - - if ( 'tax_rate_compound' === attribute || 'tax_rate_shipping' === attribute ) { - if ( $target.is( ':checked' ) ) { - val = 1; - } else { - val = 0; - } - } - - model.setRateAttribute( id, attribute, val ); - }, - sanitizePage: function( page_num ) { - page_num = parseInt( page_num, 10 ); - if ( page_num < 1 ) { - page_num = 1; - } else if ( page_num > this.qty_pages ) { - page_num = this.qty_pages; - } - return page_num; - } - } ), - WCTaxTableModelInstance = new WCTaxTableModelConstructor({ - rates: data.rates - } ), - WCTaxTableInstance = new WCTaxTableViewConstructor({ - model: WCTaxTableModelInstance, - el: '#rates' - } ); - - WCTaxTableInstance.render(); - - }); -})( jQuery, htmlSettingsTaxLocalizeScript, wp, ajaxurl ); diff --git a/assets/js/admin/settings.js b/assets/js/admin/settings.js deleted file mode 100644 index de5931fd25d..00000000000 --- a/assets/js/admin/settings.js +++ /dev/null @@ -1,183 +0,0 @@ -/* global woocommerce_settings_params, wp */ -( function( $, params, wp ) { - $( function() { - // Sell Countries - $( 'select#woocommerce_allowed_countries' ).change( function() { - if ( 'specific' === $( this ).val() ) { - $( this ).closest('tr').next( 'tr' ).hide(); - $( this ).closest('tr').next().next( 'tr' ).show(); - } else if ( 'all_except' === $( this ).val() ) { - $( this ).closest('tr').next( 'tr' ).show(); - $( this ).closest('tr').next().next( 'tr' ).hide(); - } else { - $( this ).closest('tr').next( 'tr' ).hide(); - $( this ).closest('tr').next().next( 'tr' ).hide(); - } - }).trigger( 'change' ); - - // Ship Countries - $( 'select#woocommerce_ship_to_countries' ).change( function() { - if ( 'specific' === $( this ).val() ) { - $( this ).closest('tr').next( 'tr' ).show(); - } else { - $( this ).closest('tr').next( 'tr' ).hide(); - } - }).trigger( 'change' ); - - // Stock management - $( 'input#woocommerce_manage_stock' ).change( function() { - if ( $( this ).is(':checked') ) { - $( this ).closest('tbody').find( '.manage_stock_field' ).closest( 'tr' ).show(); - } else { - $( this ).closest('tbody').find( '.manage_stock_field' ).closest( 'tr' ).hide(); - } - }).trigger( 'change' ); - - // Color picker - $( '.colorpick' ) - - .iris({ - change: function( event, ui ) { - $( this ).parent().find( '.colorpickpreview' ).css({ backgroundColor: ui.color.toString() }); - }, - hide: true, - border: true - }) - - .on( 'click focus', function( event ) { - event.stopPropagation(); - $( '.iris-picker' ).hide(); - $( this ).closest( 'td' ).find( '.iris-picker' ).show(); - $( this ).data( 'original-value', $( this ).val() ); - }) - - .on( 'change', function() { - if ( $( this ).is( '.iris-error' ) ) { - var original_value = $( this ).data( 'original-value' ); - - if ( original_value.match( /^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/ ) ) { - $( this ).val( $( this ).data( 'original-value' ) ).trigger( 'change' ); - } else { - $( this ).val( '' ).trigger( 'change' ); - } - } - }); - - $( 'body' ).on( 'click', function() { - $( '.iris-picker' ).hide(); - }); - - // Edit prompt - $( function() { - var changed = false; - - $( 'input, textarea, select, checkbox' ).change( function() { - if ( ! changed ) { - window.onbeforeunload = function() { - return params.i18n_nav_warning; - }; - changed = true; - } - }); - - $( '.submit :input' ).on( 'click', function() { - window.onbeforeunload = ''; - }); - }); - - // Sorting - $( 'table.wc_gateways tbody, table.wc_shipping tbody' ).sortable({ - items: 'tr', - cursor: 'move', - axis: 'y', - handle: 'td.sort', - scrollSensitivity: 40, - helper: function( event, ui ) { - ui.children().each( function() { - $( this ).width( $( this ).width() ); - }); - ui.css( 'left', '0' ); - return ui; - }, - start: function( event, ui ) { - ui.item.css( 'background-color', '#f6f6f6' ); - }, - stop: function( event, ui ) { - ui.item.removeAttr( 'style' ); - ui.item.trigger( 'updateMoveButtons' ); - } - }); - - // Select all/none - $( '.woocommerce' ).on( 'click', '.select_all', function() { - $( this ).closest( 'td' ).find( 'select option' ).attr( 'selected', 'selected' ); - $( this ).closest( 'td' ).find( 'select' ).trigger( 'change' ); - return false; - }); - - $( '.woocommerce' ).on( 'click', '.select_none', function() { - $( this ).closest( 'td' ).find( 'select option' ).removeAttr( 'selected' ); - $( this ).closest( 'td' ).find( 'select' ).trigger( 'change' ); - return false; - }); - - // Re-order buttons. - $( '.wc-item-reorder-nav').find( '.wc-move-up, .wc-move-down' ).on( 'click', function() { - var moveBtn = $( this ), - $row = moveBtn.closest( 'tr' ); - - moveBtn.focus(); - - var isMoveUp = moveBtn.is( '.wc-move-up' ), - isMoveDown = moveBtn.is( '.wc-move-down' ); - - if ( isMoveUp ) { - var $previewRow = $row.prev( 'tr' ); - - if ( $previewRow && $previewRow.length ) { - $previewRow.before( $row ); - wp.a11y.speak( params.i18n_moved_up ); - } - } else if ( isMoveDown ) { - var $nextRow = $row.next( 'tr' ); - - if ( $nextRow && $nextRow.length ) { - $nextRow.after( $row ); - wp.a11y.speak( params.i18n_moved_down ); - } - } - - moveBtn.focus(); // Re-focus after the container was moved. - moveBtn.closest( 'table' ).trigger( 'updateMoveButtons' ); - } ); - - $( '.wc-item-reorder-nav').closest( 'table' ).on( 'updateMoveButtons', function() { - var table = $( this ), - lastRow = $( this ).find( 'tbody tr:last' ), - firstRow = $( this ).find( 'tbody tr:first' ); - - table.find( '.wc-item-reorder-nav .wc-move-disabled' ).removeClass( 'wc-move-disabled' ) - .attr( { 'tabindex': '0', 'aria-hidden': 'false' } ); - firstRow.find( '.wc-item-reorder-nav .wc-move-up' ).addClass( 'wc-move-disabled' ) - .attr( { 'tabindex': '-1', 'aria-hidden': 'true' } ); - lastRow.find( '.wc-item-reorder-nav .wc-move-down' ).addClass( 'wc-move-disabled' ) - .attr( { 'tabindex': '-1', 'aria-hidden': 'true' } ); - } ); - - $( '.wc-item-reorder-nav').closest( 'table' ).trigger( 'updateMoveButtons' ); - - - $( '.submit button' ).on( 'click', function() { - if ( - $( 'select#woocommerce_allowed_countries' ).val() === 'specific' && - ! $( '[name="woocommerce_specific_allowed_countries[]"]' ).val() - ) { - if ( window.confirm( woocommerce_settings_params.i18n_no_specific_countries_selected ) ) { - return true; - } - return false; - } - } ); - - }); -})( jQuery, woocommerce_settings_params, wp ); diff --git a/assets/js/admin/system-status.js b/assets/js/admin/system-status.js deleted file mode 100644 index cb84c875444..00000000000 --- a/assets/js/admin/system-status.js +++ /dev/null @@ -1,126 +0,0 @@ -/* global jQuery, woocommerce_admin_system_status, wcSetClipboard, wcClearClipboard */ -jQuery( function ( $ ) { - - /** - * Users country and state fields - */ - var wcSystemStatus = { - init: function() { - $( document.body ) - .on( 'click', 'a.help_tip, a.woocommerce-help-tip', this.preventTipTipClick ) - .on( 'click', 'a.debug-report', this.generateReport ) - .on( 'click', '#copy-for-support', this.copyReport ) - .on( 'aftercopy', '#copy-for-support', this.copySuccess ) - .on( 'aftercopyfailure', '#copy-for-support', this.copyFail ); - }, - - /** - * Prevent anchor behavior when click on TipTip. - * - * @return {Bool} - */ - preventTipTipClick: function() { - return false; - }, - - /** - * Generate system status report. - * - * @return {Bool} - */ - generateReport: function() { - var report = ''; - - $( '.wc_status_table thead, .wc_status_table tbody' ).each( function() { - if ( $( this ).is( 'thead' ) ) { - var label = $( this ).find( 'th:eq(0)' ).data( 'export-label' ) || $( this ).text(); - report = report + '\n### ' + label.trim() + ' ###\n\n'; - } else { - $( 'tr', $( this ) ).each( function() { - var label = $( this ).find( 'td:eq(0)' ).data( 'export-label' ) || $( this ).find( 'td:eq(0)' ).text(); - var the_name = label.trim().replace( /(<([^>]+)>)/ig, '' ); // Remove HTML. - - // Find value - var $value_html = $( this ).find( 'td:eq(2)' ).clone(); - $value_html.find( '.private' ).remove(); - $value_html.find( '.dashicons-yes' ).replaceWith( '✔' ); - $value_html.find( '.dashicons-no-alt, .dashicons-warning' ).replaceWith( '❌' ); - - // Format value - var the_value = $value_html.text().trim(); - var value_array = the_value.split( ', ' ); - - if ( value_array.length > 1 ) { - // If value have a list of plugins ','. - // Split to add new line. - var temp_line =''; - $.each( value_array, function( key, line ) { - temp_line = temp_line + line + '\n'; - }); - - the_value = temp_line; - } - - report = report + '' + the_name + ': ' + the_value + '\n'; - }); - } - }); - - try { - $( '#debug-report' ).slideDown(); - $( '#debug-report' ).find( 'textarea' ).val( '`' + report + '`' ).focus().select(); - $( this ).fadeOut(); - return false; - } catch ( e ) { - /* jshint devel: true */ - console.log( e ); - } - - return false; - }, - - /** - * Copy for report. - * - * @param {Object} evt Copy event. - */ - copyReport: function( evt ) { - wcClearClipboard(); - wcSetClipboard( $( '#debug-report' ).find( 'textarea' ).val(), $( this ) ); - evt.preventDefault(); - }, - - /** - * Display a "Copied!" tip when success copying - */ - copySuccess: function() { - $( '#copy-for-support' ).tipTip({ - 'attribute': 'data-tip', - 'activation': 'focus', - 'fadeIn': 50, - 'fadeOut': 50, - 'delay': 0 - }).focus(); - }, - - /** - * Displays the copy error message when failure copying. - */ - copyFail: function() { - $( '.copy-error' ).removeClass( 'hidden' ); - $( '#debug-report' ).find( 'textarea' ).focus().select(); - } - }; - - wcSystemStatus.init(); - - $( '.wc_status_table' ).on( 'click', '.run-tool .button', function( evt ) { - evt.stopImmediatePropagation(); - return window.confirm( woocommerce_admin_system_status.run_tool_confirmation ); - }); - - $( '#log-viewer-select' ).on( 'click', 'h2 a.page-title-action', function( evt ) { - evt.stopImmediatePropagation(); - return window.confirm( woocommerce_admin_system_status.delete_log_confirmation ); - }); -}); diff --git a/assets/js/admin/users.js b/assets/js/admin/users.js deleted file mode 100644 index cb2b816604e..00000000000 --- a/assets/js/admin/users.js +++ /dev/null @@ -1,120 +0,0 @@ -/*global wc_users_params */ -jQuery( function ( $ ) { - - /** - * Users country and state fields - */ - var wc_users_fields = { - states: null, - init: function() { - if ( typeof wc_users_params.countries !== 'undefined' ) { - /* State/Country select boxes */ - this.states = JSON.parse( wc_users_params.countries.replace( /"/g, '"' ) ); - } - - $( '.js_field-country' ).selectWoo().change( this.change_country ); - $( '.js_field-country' ).trigger( 'change', [ true ] ); - $( document.body ).on( 'change', 'select.js_field-state', this.change_state ); - - $( document.body ).on( 'click', 'button.js_copy-billing', this.copy_billing ); - }, - - change_country: function( e, stickValue ) { - // Check for stickValue before using it - if ( typeof stickValue === 'undefined' ) { - stickValue = false; - } - - // Prevent if we don't have the metabox data - if ( wc_users_fields.states === null ) { - return; - } - - var $this = $( this ), - country = $this.val(), - $state = $this.parents( '.form-table' ).find( ':input.js_field-state' ), - $parent = $state.parent(), - input_name = $state.attr( 'name' ), - input_id = $state.attr( 'id' ), - stickstatefield = 'woocommerce.stickState-' + country, - value = $this.data( stickstatefield ) ? $this.data( stickstatefield ) : $state.val(), - placeholder = $state.attr( 'placeholder' ), - $newstate; - - if ( stickValue ){ - $this.data( 'woocommerce.stickState-' + country, value ); - } - - // Remove the previous DOM element - $parent.show().find( '.select2-container' ).remove(); - - if ( ! $.isEmptyObject( wc_users_fields.states[ country ] ) ) { - var state = wc_users_fields.states[ country ], - $defaultOption = $( '' ) - .text( wc_users_fields.i18n_select_state_text ); - - $newstate = $( '' ) - .prop( 'id', input_id ) - .prop( 'name', input_name ) - .prop( 'placeholder', placeholder ) - .addClass( 'js_field-state' ) - .append( $defaultOption ); - - $.each( state, function( index ) { - var $option = $( '' ) - .prop( 'value', index ) - .text( state[ index ] ); - $newstate.append( $option ); - } ); - - $newstate.val( value ); - - $state.replaceWith( $newstate ); - - $newstate.show().selectWoo().hide().trigger( 'change' ); - } else { - $newstate = $( '' ) - .prop( 'id', input_id ) - .prop( 'name', input_name ) - .prop( 'placeholder', placeholder ) - .addClass( 'js_field-state regular-text' ) - .val( value ); - $state.replaceWith( $newstate ); - } - - // This event has a typo - deprecated in 2.5.0 - $( document.body ).trigger( 'contry-change.woocommerce', [country, $( this ).closest( 'div' )] ); - $( document.body ).trigger( 'country-change.woocommerce', [country, $( this ).closest( 'div' )] ); - }, - - change_state: function() { - // Here we will find if state value on a select has changed and stick it to the country data - var $this = $( this ), - state = $this.val(), - $country = $this.parents( '.form-table' ).find( ':input.js_field-country' ), - country = $country.val(); - - $country.data( 'woocommerce.stickState-' + country, state ); - }, - - copy_billing: function( event ) { - event.preventDefault(); - - $( '#fieldset-billing' ).find( 'input, select' ).each( function( i, el ) { - // The address keys match up, except for the prefix - var shipName = el.name.replace( /^billing_/, 'shipping_' ); - // Swap prefix, then check if there are any elements - var shipEl = $( '[name="' + shipName + '"]' ); - // No corresponding shipping field, skip this item - if ( ! shipEl.length ) { - return; - } - // Found a matching shipping element, update the value - shipEl.val( el.value ).trigger( 'change' ); - } ); - } - }; - - wc_users_fields.init(); - -}); diff --git a/assets/js/admin/wc-clipboard.js b/assets/js/admin/wc-clipboard.js deleted file mode 100644 index fc009d02041..00000000000 --- a/assets/js/admin/wc-clipboard.js +++ /dev/null @@ -1,38 +0,0 @@ -/* exported wcSetClipboard, wcClearClipboard */ - -/** - * Simple text copy functions using native browser clipboard capabilities. - * @since 3.2.0 - */ - -/** - * Set the user's clipboard contents. - * - * @param string data: Text to copy to clipboard. - * @param object $el: jQuery element to trigger copy events on. (Default: document) - */ -function wcSetClipboard( data, $el ) { - if ( 'undefined' === typeof $el ) { - $el = jQuery( document ); - } - var $temp_input = jQuery( ' - get_description_html( $data ); // WPCS: XSS ok. ?> - - - - get_field_key( $key ); - $defaults = array( - 'title' => '', - 'label' => '', - 'disabled' => false, - 'class' => '', - 'css' => '', - 'type' => 'text', - 'desc_tip' => false, - 'description' => '', - 'custom_attributes' => array(), - ); - - $data = wp_parse_args( $data, $defaults ); - - if ( ! $data['label'] ) { - $data['label'] = $data['title']; - } - - ob_start(); - ?> - - - - - -
    - -
    - get_description_html( $data ); // WPCS: XSS ok. ?> -
    - - - get_field_key( $key ); - $defaults = array( - 'title' => '', - 'disabled' => false, - 'class' => '', - 'css' => '', - 'placeholder' => '', - 'type' => 'text', - 'desc_tip' => false, - 'description' => '', - 'custom_attributes' => array(), - 'options' => array(), - ); - - $data = wp_parse_args( $data, $defaults ); - - ob_start(); - ?> - - - - - -
    - - - get_description_html( $data ); // WPCS: XSS ok. ?> -
    - - - get_field_key( $key ); - $defaults = array( - 'title' => '', - 'disabled' => false, - 'class' => '', - 'css' => '', - 'placeholder' => '', - 'type' => 'text', - 'desc_tip' => false, - 'description' => '', - 'custom_attributes' => array(), - 'select_buttons' => false, - 'options' => array(), - ); - - $data = wp_parse_args( $data, $defaults ); - $value = (array) $this->get_option( $key, array() ); - - ob_start(); - ?> - - - - - -
    - - - get_description_html( $data ); // WPCS: XSS ok. ?> - -
    - -
    - - - get_field_key( $key ); - $defaults = array( - 'title' => '', - 'class' => '', - ); - - $data = wp_parse_args( $data, $defaults ); - - ob_start(); - ?> - -

    - -

    - - - array( - 'src' => true, - 'style' => true, - 'id' => true, - 'class' => true, - ), - ), - wp_kses_allowed_html( 'post' ) - ) - ); - } - - /** - * Validate Checkbox Field. - * - * If not set, return "no", otherwise return "yes". - * - * @param string $key Field key. - * @param string $value Posted Value. - * @return string - */ - public function validate_checkbox_field( $key, $value ) { - return ! is_null( $value ) ? 'yes' : 'no'; - } - - /** - * Validate Select Field. - * - * @param string $key Field key. - * @param string $value Posted Value. - * @return string - */ - public function validate_select_field( $key, $value ) { - $value = is_null( $value ) ? '' : $value; - return wc_clean( stripslashes( $value ) ); - } - - /** - * Validate Multiselect Field. - * - * @param string $key Field key. - * @param string $value Posted Value. - * @return string|array - */ - public function validate_multiselect_field( $key, $value ) { - return is_array( $value ) ? array_map( 'wc_clean', array_map( 'stripslashes', $value ) ) : ''; - } - - /** - * Validate the data on the "Settings" form. - * - * @deprecated 2.6.0 No longer used. - * @param array $form_fields Array of fields. - */ - public function validate_settings_fields( $form_fields = array() ) { - wc_deprecated_function( 'validate_settings_fields', '2.6' ); - } - - /** - * Format settings if needed. - * - * @deprecated 2.6.0 Unused. - * @param array $value Value to format. - * @return array - */ - public function format_settings( $value ) { - wc_deprecated_function( 'format_settings', '2.6' ); - return $value; - } -} diff --git a/includes/abstracts/abstract-wc-widget.php b/includes/abstracts/abstract-wc-widget.php deleted file mode 100644 index 10ac9e4cec6..00000000000 --- a/includes/abstracts/abstract-wc-widget.php +++ /dev/null @@ -1,407 +0,0 @@ - $this->widget_cssclass, - 'description' => $this->widget_description, - 'customize_selective_refresh' => true, - ); - - parent::__construct( $this->widget_id, $this->widget_name, $widget_ops ); - - add_action( 'save_post', array( $this, 'flush_widget_cache' ) ); - add_action( 'deleted_post', array( $this, 'flush_widget_cache' ) ); - add_action( 'switch_theme', array( $this, 'flush_widget_cache' ) ); - } - - /** - * Get cached widget. - * - * @param array $args Arguments. - * @return bool true if the widget is cached otherwise false - */ - public function get_cached_widget( $args ) { - // Don't get cache if widget_id doesn't exists. - if ( empty( $args['widget_id'] ) ) { - return false; - } - - $cache = wp_cache_get( $this->get_widget_id_for_cache( $this->widget_id ), 'widget' ); - - if ( ! is_array( $cache ) ) { - $cache = array(); - } - - if ( isset( $cache[ $this->get_widget_id_for_cache( $args['widget_id'] ) ] ) ) { - echo $cache[ $this->get_widget_id_for_cache( $args['widget_id'] ) ]; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped - return true; - } - - return false; - } - - /** - * Cache the widget. - * - * @param array $args Arguments. - * @param string $content Content. - * @return string the content that was cached - */ - public function cache_widget( $args, $content ) { - // Don't set any cache if widget_id doesn't exist. - if ( empty( $args['widget_id'] ) ) { - return $content; - } - - $cache = wp_cache_get( $this->get_widget_id_for_cache( $this->widget_id ), 'widget' ); - - if ( ! is_array( $cache ) ) { - $cache = array(); - } - - $cache[ $this->get_widget_id_for_cache( $args['widget_id'] ) ] = $content; - - wp_cache_set( $this->get_widget_id_for_cache( $this->widget_id ), $cache, 'widget' ); - - return $content; - } - - /** - * Flush the cache. - */ - public function flush_widget_cache() { - foreach ( array( 'https', 'http' ) as $scheme ) { - wp_cache_delete( $this->get_widget_id_for_cache( $this->widget_id, $scheme ), 'widget' ); - } - } - - /** - * Get this widgets title. - * - * @param array $instance Array of instance options. - * @return string - */ - protected function get_instance_title( $instance ) { - if ( isset( $instance['title'] ) ) { - return $instance['title']; - } - - if ( isset( $this->settings, $this->settings['title'], $this->settings['title']['std'] ) ) { - return $this->settings['title']['std']; - } - - return ''; - } - - /** - * Output the html at the start of a widget. - * - * @param array $args Arguments. - * @param array $instance Instance. - */ - public function widget_start( $args, $instance ) { - echo $args['before_widget']; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped - - $title = apply_filters( 'widget_title', $this->get_instance_title( $instance ), $instance, $this->id_base ); - - if ( $title ) { - echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped - } - } - - /** - * Output the html at the end of a widget. - * - * @param array $args Arguments. - */ - public function widget_end( $args ) { - echo $args['after_widget']; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped - } - - /** - * Updates a particular instance of a widget. - * - * @see WP_Widget->update - * @param array $new_instance New instance. - * @param array $old_instance Old instance. - * @return array - */ - public function update( $new_instance, $old_instance ) { - - $instance = $old_instance; - - if ( empty( $this->settings ) ) { - return $instance; - } - - // Loop settings and get values to save. - foreach ( $this->settings as $key => $setting ) { - if ( ! isset( $setting['type'] ) ) { - continue; - } - - // Format the value based on settings type. - switch ( $setting['type'] ) { - case 'number': - $instance[ $key ] = absint( $new_instance[ $key ] ); - - if ( isset( $setting['min'] ) && '' !== $setting['min'] ) { - $instance[ $key ] = max( $instance[ $key ], $setting['min'] ); - } - - if ( isset( $setting['max'] ) && '' !== $setting['max'] ) { - $instance[ $key ] = min( $instance[ $key ], $setting['max'] ); - } - break; - case 'textarea': - $instance[ $key ] = wp_kses( trim( wp_unslash( $new_instance[ $key ] ) ), wp_kses_allowed_html( 'post' ) ); - break; - case 'checkbox': - $instance[ $key ] = empty( $new_instance[ $key ] ) ? 0 : 1; - break; - default: - $instance[ $key ] = isset( $new_instance[ $key ] ) ? sanitize_text_field( $new_instance[ $key ] ) : $setting['std']; - break; - } - - /** - * Sanitize the value of a setting. - */ - $instance[ $key ] = apply_filters( 'woocommerce_widget_settings_sanitize_option', $instance[ $key ], $new_instance, $key, $setting ); - } - - $this->flush_widget_cache(); - - return $instance; - } - - /** - * Outputs the settings update form. - * - * @see WP_Widget->form - * - * @param array $instance Instance. - */ - public function form( $instance ) { - - if ( empty( $this->settings ) ) { - return; - } - - foreach ( $this->settings as $key => $setting ) { - - $class = isset( $setting['class'] ) ? $setting['class'] : ''; - $value = isset( $instance[ $key ] ) ? $instance[ $key ] : $setting['std']; - - switch ( $setting['type'] ) { - - case 'text': - ?> -

    - - -

    - -

    - - -

    - -

    - - -

    - -

    - - - - - -

    - -

    - /> - -

    - slug, $queried_object->taxonomy ); - } - - // Min/Max. - if ( isset( $_GET['min_price'] ) ) { - $link = add_query_arg( 'min_price', wc_clean( wp_unslash( $_GET['min_price'] ) ), $link ); - } - - if ( isset( $_GET['max_price'] ) ) { - $link = add_query_arg( 'max_price', wc_clean( wp_unslash( $_GET['max_price'] ) ), $link ); - } - - // Order by. - if ( isset( $_GET['orderby'] ) ) { - $link = add_query_arg( 'orderby', wc_clean( wp_unslash( $_GET['orderby'] ) ), $link ); - } - - /** - * Search Arg. - * To support quote characters, first they are decoded from " entities, then URL encoded. - */ - if ( get_search_query() ) { - $link = add_query_arg( 's', rawurlencode( htmlspecialchars_decode( get_search_query() ) ), $link ); - } - - // Post Type Arg. - if ( isset( $_GET['post_type'] ) ) { - $link = add_query_arg( 'post_type', wc_clean( wp_unslash( $_GET['post_type'] ) ), $link ); - - // Prevent post type and page id when pretty permalinks are disabled. - if ( is_shop() ) { - $link = remove_query_arg( 'page_id', $link ); - } - } - - // Min Rating Arg. - if ( isset( $_GET['rating_filter'] ) ) { - $link = add_query_arg( 'rating_filter', wc_clean( wp_unslash( $_GET['rating_filter'] ) ), $link ); - } - - // All current filters. - if ( $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes() ) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found, WordPress.CodeAnalysis.AssignmentInCondition.Found - foreach ( $_chosen_attributes as $name => $data ) { - $filter_name = wc_attribute_taxonomy_slug( $name ); - if ( ! empty( $data['terms'] ) ) { - $link = add_query_arg( 'filter_' . $filter_name, implode( ',', $data['terms'] ), $link ); - } - if ( 'or' === $data['query_type'] ) { - $link = add_query_arg( 'query_type_' . $filter_name, 'or', $link ); - } - } - } - - return apply_filters( 'woocommerce_widget_get_current_page_url', $link, $this ); - } - - /** - * Get widget id plus scheme/protocol to prevent serving mixed content from (persistently) cached widgets. - * - * @since 3.4.0 - * @param string $widget_id Id of the cached widget. - * @param string $scheme Scheme for the widget id. - * @return string Widget id including scheme/protocol. - */ - protected function get_widget_id_for_cache( $widget_id, $scheme = '' ) { - if ( $scheme ) { - $widget_id_for_cache = $widget_id . '-' . $scheme; - } else { - $widget_id_for_cache = $widget_id . '-' . ( is_ssl() ? 'https' : 'http' ); - } - - return apply_filters( 'woocommerce_cached_widget_id', $widget_id_for_cache ); - } -} diff --git a/includes/admin/class-wc-admin-addons.php b/includes/admin/class-wc-admin-addons.php deleted file mode 100644 index ed3a0461368..00000000000 --- a/includes/admin/class-wc-admin-addons.php +++ /dev/null @@ -1,739 +0,0 @@ - $headers, - 'user-agent' => 'WooCommerce Addons Page', - ) - ); - - if ( ! is_wp_error( $raw_featured ) ) { - $featured = json_decode( wp_remote_retrieve_body( $raw_featured ) ); - if ( $featured ) { - set_transient( 'wc_addons_featured', $featured, DAY_IN_SECONDS ); - } - } - } - - if ( is_object( $featured ) ) { - self::output_featured_sections( $featured->sections ); - return $featured; - } - } - - /** - * Build url parameter string - * - * @param string $category Addon (sub) category. - * @param string $term Search terms. - * @param string $country Store country. - * - * @return string url parameter string - */ - public static function build_parameter_string( $category, $term, $country ) { - - $parameters = array( - 'category' => $category, - 'term' => $term, - 'country' => $country, - ); - - return '?' . http_build_query( $parameters ); - } - - /** - * Call API to get extensions - * - * @param string $category Addon (sub) category. - * @param string $term Search terms. - * @param string $country Store country. - * - * @return array of extensions - */ - public static function get_extension_data( $category, $term, $country ) { - $parameters = self::build_parameter_string( $category, $term, $country ); - - $headers = array(); - $auth = WC_Helper_Options::get( 'auth' ); - - if ( ! empty( $auth['access_token'] ) ) { - $headers['Authorization'] = 'Bearer ' . $auth['access_token']; - } - - $raw_extensions = wp_safe_remote_get( - 'https://woocommerce.com/wp-json/wccom-extensions/1.0/search' . $parameters, - array( 'headers' => $headers ) - ); - - if ( ! is_wp_error( $raw_extensions ) ) { - $addons = json_decode( wp_remote_retrieve_body( $raw_extensions ) )->products; - } - return $addons; - } - - /** - * Get sections for the addons screen - * - * @return array of objects - */ - public static function get_sections() { - $addon_sections = get_transient( 'wc_addons_sections' ); - if ( false === ( $addon_sections ) ) { - $raw_sections = wp_safe_remote_get( - 'https://woocommerce.com/wp-json/wccom-extensions/1.0/categories' - ); - if ( ! is_wp_error( $raw_sections ) ) { - $addon_sections = json_decode( wp_remote_retrieve_body( $raw_sections ) ); - if ( $addon_sections ) { - set_transient( 'wc_addons_sections', $addon_sections, WEEK_IN_SECONDS ); - } - } - } - return apply_filters( 'woocommerce_addons_sections', $addon_sections ); - } - - /** - * Get section for the addons screen. - * - * @param string $section_id Required section ID. - * - * @return object|bool - */ - public static function get_section( $section_id ) { - $sections = self::get_sections(); - if ( isset( $sections[ $section_id ] ) ) { - return $sections[ $section_id ]; - } - return false; - } - - /** - * Get section content for the addons screen. - * - * @param string $section_id Required section ID. - * - * @return array - */ - public static function get_section_data( $section_id ) { - $section = self::get_section( $section_id ); - $section_data = ''; - - if ( ! empty( $section->endpoint ) ) { - $section_data = get_transient( 'wc_addons_section_' . $section_id ); - if ( false === $section_data ) { - $raw_section = wp_safe_remote_get( esc_url_raw( $section->endpoint ), array( 'user-agent' => 'WooCommerce Addons Page' ) ); - - if ( ! is_wp_error( $raw_section ) ) { - $section_data = json_decode( wp_remote_retrieve_body( $raw_section ) ); - - if ( ! empty( $section_data->products ) ) { - set_transient( 'wc_addons_section_' . $section_id, $section_data, WEEK_IN_SECONDS ); - } - } - } - } - - return apply_filters( 'woocommerce_addons_section_data', $section_data->products, $section_id ); - } - - /** - * Handles the outputting of a contextually aware Storefront link (points to child themes if Storefront is already active). - */ - public static function output_storefront_button() { - $template = get_option( 'template' ); - $stylesheet = get_option( 'stylesheet' ); - - if ( 'storefront' === $template ) { - if ( 'storefront' === $stylesheet ) { - $url = 'https://woocommerce.com/product-category/themes/storefront-child-theme-themes/'; - $text = __( 'Need a fresh look? Try Storefront child themes', 'woocommerce' ); - $utm_content = 'nostorefrontchildtheme'; - } else { - $url = 'https://woocommerce.com/product-category/themes/storefront-child-theme-themes/'; - $text = __( 'View more Storefront child themes', 'woocommerce' ); - $utm_content = 'hasstorefrontchildtheme'; - } - } else { - $url = 'https://woocommerce.com/storefront/'; - $text = __( 'Need a theme? Try Storefront', 'woocommerce' ); - $utm_content = 'nostorefront'; - } - - $url = add_query_arg( - array( - 'utm_source' => 'addons', - 'utm_medium' => 'product', - 'utm_campaign' => 'woocommerceplugin', - 'utm_content' => $utm_content, - ), - $url - ); - - echo '' . esc_html( $text ) . '' . "\n"; - } - - /** - * Handles the outputting of a banner block. - * - * @param object $block Banner data. - */ - public static function output_banner_block( $block ) { - ?> -
    -

    title ); ?>

    -

    description ); ?>

    -
    - items as $item ) : ?> - -
    -
    - -
    -
    -

    title ); ?>

    -

    description ); ?>

    - href, - $item->button, - 'addons-button-solid', - $item->plugin - ); - ?> -
    -
    - - -
    -
    - container ) && 'column_container_start' === $block->container ) { - ?> -
    - module ) { - ?> -
    - -
    - container ) && 'column_container_end' === $block->container ) { - ?> -
    - -
    -

    title ); ?>

    -

    description ); ?>

    - items as $item ) : ?> - -
    -
    - -
    -
    -

    title ); ?>

    - href, - $item->button, - 'addons-button-solid', - $item->plugin - ); - ?> -

    description ); ?>

    -
    -
    - - -
    - - -
    - -
    -

    title ); ?>

    -

    description ); ?>

    -
    - buttons as $button ) : ?> - href, - $button->text, - 'addons-button-solid' - ); - ?> - -
    -
    -
    - -
    -

    title ); ?>

    -

    description ); ?>

    -
    - items as $item ) : ?> -
    - image ) ) : ?> -
    - -
    - - href, - $item->button, - 'addons-button-outline-white' - ); - ?> -
    - -
    -
    - 'woocommerce-services', - ) - ), - 'install-addon_woocommerce-services' - ); - - $defaults = array( - 'image' => WC()->plugin_url() . '/assets/images/wcs-extensions-banner-3x.png', - 'image_alt' => __( 'WooCommerce Shipping', 'woocommerce' ), - 'title' => __( 'Buy discounted shipping labels — then print them from your dashboard.', 'woocommerce' ), - 'description' => __( 'Integrate your store with USPS to buy discounted shipping labels, and print them directly from your WooCommerce dashboard. Powered by WooCommerce Shipping.', 'woocommerce' ), - 'button' => __( 'Free - Install now', 'woocommerce' ), - 'href' => $button_url, - 'logos' => array(), - ); - - switch ( $location['country'] ) { - case 'CA': - $local_defaults = array( - 'image' => WC()->plugin_url() . '/assets/images/wcs-truck-banner-3x.png', - 'title' => __( 'Show Canada Post shipping rates', 'woocommerce' ), - 'description' => __( 'Display live rates from Canada Post at checkout to make shipping a breeze. Powered by WooCommerce Shipping.', 'woocommerce' ), - 'logos' => array_merge( - $defaults['logos'], - array( - array( - 'link' => WC()->plugin_url() . '/assets/images/wcs-canada-post-logo.jpg', - 'alt' => 'Canada Post logo', - ), - ) - ), - ); - break; - case 'US': - $local_defaults = array( - 'logos' => array_merge( - $defaults['logos'], - array( - array( - 'link' => WC()->plugin_url() . '/assets/images/wcs-usps-logo.png', - 'alt' => 'USPS logo', - ), - ) - ), - ); - break; - default: - $local_defaults = array(); - } - - $block_data = array_merge( $defaults, $local_defaults, $block ); - ?> -
    -
    - <?php echo esc_attr( $block_data['image_alt'] ); ?> -
    -
    -

    -

    - - -
    -
    - 'woocommerce-payments', - ) - ), - 'install-addon_woocommerce-payments' - ); - - $defaults = array( - 'image' => WC()->plugin_url() . '/assets/images/wcpayments-icon-secure.png', - 'image_alt' => __( 'WooCommerce Payments', 'woocommerce' ), - 'title' => __( 'Payments made simple, with no monthly fees — exclusively for WooCommerce stores.', 'woocommerce' ), - 'description' => __( 'Securely accept cards in your store. See payments, track cash flow into your bank account, and stay on top of disputes – right from your dashboard.', 'woocommerce' ), - 'button' => __( 'Free - Install now', 'woocommerce' ), - 'href' => $button_url, - 'logos' => array(), - ); - - $block_data = array_merge( $defaults, $block ); - ?> -
    -
    - <?php echo esc_attr( $block_data['image_alt'] ); ?> -
    -
    -

    -

    - -
    -
    - module ) { - case 'banner_block': - self::output_banner_block( $section ); - break; - case 'column_start': - self::output_column( $section ); - break; - case 'column_end': - self::output_column( $section ); - break; - case 'column_block': - self::output_column_block( $section ); - break; - case 'small_light_block': - self::output_small_light_block( $section ); - break; - case 'small_dark_block': - self::output_small_dark_block( $section ); - break; - case 'wcs_banner_block': - self::output_wcs_banner_block( (array) $section ); - break; - case 'wcpay_banner_block': - self::output_wcpay_banner_block( (array) $section ); - break; - } - } - } - - /** - * Returns in-app-purchase URL params. - */ - public static function get_in_app_purchase_url_params() { - // Get url (from path onward) for the current page, - // so WCCOM "back" link returns user to where they were. - $back_admin_path = add_query_arg( array() ); - return array( - 'wccom-site' => site_url(), - 'wccom-back' => rawurlencode( $back_admin_path ), - 'wccom-woo-version' => Constants::get_constant( 'WC_VERSION' ), - 'wccom-connect-nonce' => wp_create_nonce( 'connect' ), - ); - } - - /** - * Add in-app-purchase URL params to link. - * - * Adds various url parameters to a url to support a streamlined - * flow for obtaining and setting up WooCommerce extensons. - * - * @param string $url Destination URL. - */ - public static function add_in_app_purchase_url_params( $url ) { - return add_query_arg( - self::get_in_app_purchase_url_params(), - $url - ); - } - - /** - * Outputs a button. - * - * @param string $url Destination URL. - * @param string $text Button label text. - * @param string $style Button style class. - * @param string $plugin The plugin the button is promoting. - */ - public static function output_button( $url, $text, $style, $plugin = '' ) { - $style = __( 'Free', 'woocommerce' ) === $text ? 'addons-button-outline-purple' : $style; - $style = is_plugin_active( $plugin ) ? 'addons-button-installed' : $style; - $text = is_plugin_active( $plugin ) ? __( 'Installed', 'woocommerce' ) : $text; - $url = self::add_in_app_purchase_url_params( $url ); - ?> - - - - countries->get_base_country(); - $addons = self::get_extension_data( $category, $term, $country ); - } - - /** - * Addon page view. - * - * @uses $addons - * @uses $sections - * @uses $theme - * @uses $current_section - */ - include_once dirname( __FILE__ ) . '/views/html-admin-page-addons.php'; - } - - /** - * Install WooCommerce Services from Extensions screens. - */ - public static function install_woocommerce_services_addon() { - check_admin_referer( 'install-addon_woocommerce-services' ); - - $services_plugin_id = 'woocommerce-services'; - $services_plugin = array( - 'name' => __( 'WooCommerce Services', 'woocommerce' ), - 'repo-slug' => 'woocommerce-services', - ); - - WC_Install::background_installer( $services_plugin_id, $services_plugin ); - - wp_safe_redirect( remove_query_arg( array( 'install-addon', '_wpnonce' ) ) ); - exit; - } - - /** - * Install WooCommerce Payments from the Extensions screens. - * - * @param string $section Optional. Extenstions tab. - * - * @return void - */ - public static function install_woocommerce_payments_addon( $section = '_featured' ) { - check_admin_referer( 'install-addon_woocommerce-payments' ); - - $wcpay_plugin_id = 'woocommerce-payments'; - $wcpay_plugin = array( - 'name' => __( 'WooCommerce Payments', 'woocommerce' ), - 'repo-slug' => 'woocommerce-payments', - ); - - WC_Install::background_installer( $wcpay_plugin_id, $wcpay_plugin ); - - do_action( 'woocommerce_addon_installed', $wcpay_plugin_id, $section ); - - wp_safe_redirect( remove_query_arg( array( 'install-addon', '_wpnonce' ) ) ); - exit; - } - - /** - * Should an extension be shown on the featured page. - * - * @param object $item Item data. - * @return boolean - */ - public static function show_extension( $item ) { - $location = WC()->countries->get_base_country(); - if ( isset( $item->geowhitelist ) && ! in_array( $location, $item->geowhitelist, true ) ) { - return false; - } - - if ( isset( $item->geoblacklist ) && in_array( $location, $item->geoblacklist, true ) ) { - return false; - } - - if ( is_plugin_active( $item->plugin ) ) { - return false; - } - - return true; - } -} diff --git a/includes/admin/class-wc-admin-assets.php b/includes/admin/class-wc-admin-assets.php deleted file mode 100644 index 567e2efe7ca..00000000000 --- a/includes/admin/class-wc-admin-assets.php +++ /dev/null @@ -1,487 +0,0 @@ -id : ''; - - // Register admin styles. - wp_register_style( 'woocommerce_admin_menu_styles', WC()->plugin_url() . '/assets/css/menu.css', array(), $version ); - wp_register_style( 'woocommerce_admin_styles', WC()->plugin_url() . '/assets/css/admin.css', array(), $version ); - wp_register_style( 'jquery-ui-style', WC()->plugin_url() . '/assets/css/jquery-ui/jquery-ui.min.css', array(), $version ); - wp_register_style( 'woocommerce_admin_dashboard_styles', WC()->plugin_url() . '/assets/css/dashboard.css', array(), $version ); - wp_register_style( 'woocommerce_admin_print_reports_styles', WC()->plugin_url() . '/assets/css/reports-print.css', array(), $version, 'print' ); - wp_register_style( 'woocommerce_admin_marketplace_styles', WC()->plugin_url() . '/assets/css/marketplace-suggestions.css', array(), $version ); - wp_register_style( 'woocommerce_admin_privacy_styles', WC()->plugin_url() . '/assets/css/privacy.css', array(), $version ); - - // Add RTL support for admin styles. - wp_style_add_data( 'woocommerce_admin_menu_styles', 'rtl', 'replace' ); - wp_style_add_data( 'woocommerce_admin_styles', 'rtl', 'replace' ); - wp_style_add_data( 'woocommerce_admin_dashboard_styles', 'rtl', 'replace' ); - wp_style_add_data( 'woocommerce_admin_print_reports_styles', 'rtl', 'replace' ); - wp_style_add_data( 'woocommerce_admin_marketplace_styles', 'rtl', 'replace' ); - wp_style_add_data( 'woocommerce_admin_privacy_styles', 'rtl', 'replace' ); - - // Sitewide menu CSS. - wp_enqueue_style( 'woocommerce_admin_menu_styles' ); - - // Admin styles for WC pages only. - if ( in_array( $screen_id, wc_get_screen_ids() ) ) { - wp_enqueue_style( 'woocommerce_admin_styles' ); - wp_enqueue_style( 'jquery-ui-style' ); - wp_enqueue_style( 'wp-color-picker' ); - } - - if ( in_array( $screen_id, array( 'dashboard' ) ) ) { - wp_enqueue_style( 'woocommerce_admin_dashboard_styles' ); - } - - if ( in_array( $screen_id, array( 'woocommerce_page_wc-reports', 'toplevel_page_wc-reports' ) ) ) { - wp_enqueue_style( 'woocommerce_admin_print_reports_styles' ); - } - - // Privacy Policy Guide css for back-compat. - if ( isset( $_GET['wp-privacy-policy-guide'] ) || in_array( $screen_id, array( 'privacy-policy-guide' ) ) ) { - wp_enqueue_style( 'woocommerce_admin_privacy_styles' ); - } - - // @deprecated 2.3. - if ( has_action( 'woocommerce_admin_css' ) ) { - do_action( 'woocommerce_admin_css' ); - wc_deprecated_function( 'The woocommerce_admin_css action', '2.3', 'admin_enqueue_scripts' ); - } - - if ( WC_Marketplace_Suggestions::show_suggestions_for_screen( $screen_id ) ) { - wp_enqueue_style( 'woocommerce_admin_marketplace_styles' ); - } - } - - - /** - * Enqueue scripts. - */ - public function admin_scripts() { - global $wp_query, $post; - - $screen = get_current_screen(); - $screen_id = $screen ? $screen->id : ''; - $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) ); - $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; - $version = Constants::get_constant( 'WC_VERSION' ); - - // Register scripts. - wp_register_script( 'woocommerce_admin', WC()->plugin_url() . '/assets/js/admin/woocommerce_admin' . $suffix . '.js', array( 'jquery', 'jquery-blockui', 'jquery-ui-sortable', 'jquery-ui-widget', 'jquery-ui-core', 'jquery-tiptip' ), $version ); - wp_register_script( 'jquery-blockui', WC()->plugin_url() . '/assets/js/jquery-blockui/jquery.blockUI' . $suffix . '.js', array( 'jquery' ), '2.70', true ); - wp_register_script( 'jquery-tiptip', WC()->plugin_url() . '/assets/js/jquery-tiptip/jquery.tipTip' . $suffix . '.js', array( 'jquery' ), $version, true ); - wp_register_script( 'round', WC()->plugin_url() . '/assets/js/round/round' . $suffix . '.js', array( 'jquery' ), $version ); - wp_register_script( 'wc-admin-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes' . $suffix . '.js', array( 'jquery', 'jquery-ui-datepicker', 'jquery-ui-sortable', 'accounting', 'round', 'wc-enhanced-select', 'plupload-all', 'stupidtable', 'jquery-tiptip' ), $version ); - wp_register_script( 'zeroclipboard', WC()->plugin_url() . '/assets/js/zeroclipboard/jquery.zeroclipboard' . $suffix . '.js', array( 'jquery' ), $version ); - wp_register_script( 'qrcode', WC()->plugin_url() . '/assets/js/jquery-qrcode/jquery.qrcode' . $suffix . '.js', array( 'jquery' ), $version ); - wp_register_script( 'stupidtable', WC()->plugin_url() . '/assets/js/stupidtable/stupidtable' . $suffix . '.js', array( 'jquery' ), $version ); - wp_register_script( 'serializejson', WC()->plugin_url() . '/assets/js/jquery-serializejson/jquery.serializejson' . $suffix . '.js', array( 'jquery' ), '2.8.1' ); - wp_register_script( 'flot', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot' . $suffix . '.js', array( 'jquery' ), $version ); - wp_register_script( 'flot-resize', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot.resize' . $suffix . '.js', array( 'jquery', 'flot' ), $version ); - wp_register_script( 'flot-time', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot.time' . $suffix . '.js', array( 'jquery', 'flot' ), $version ); - wp_register_script( 'flot-pie', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot.pie' . $suffix . '.js', array( 'jquery', 'flot' ), $version ); - wp_register_script( 'flot-stack', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot.stack' . $suffix . '.js', array( 'jquery', 'flot' ), $version ); - wp_register_script( 'wc-settings-tax', WC()->plugin_url() . '/assets/js/admin/settings-views-html-settings-tax' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone', 'jquery-blockui' ), $version ); - wp_register_script( 'wc-backbone-modal', WC()->plugin_url() . '/assets/js/admin/backbone-modal' . $suffix . '.js', array( 'underscore', 'backbone', 'wp-util' ), $version ); - wp_register_script( 'wc-shipping-zones', WC()->plugin_url() . '/assets/js/admin/wc-shipping-zones' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone', 'jquery-ui-sortable', 'wc-enhanced-select', 'wc-backbone-modal' ), $version ); - wp_register_script( 'wc-shipping-zone-methods', WC()->plugin_url() . '/assets/js/admin/wc-shipping-zone-methods' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone', 'jquery-ui-sortable', 'wc-backbone-modal' ), $version ); - wp_register_script( 'wc-shipping-classes', WC()->plugin_url() . '/assets/js/admin/wc-shipping-classes' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone' ), $version ); - wp_register_script( 'wc-clipboard', WC()->plugin_url() . '/assets/js/admin/wc-clipboard' . $suffix . '.js', array( 'jquery' ), $version ); - wp_register_script( 'select2', WC()->plugin_url() . '/assets/js/select2/select2.full' . $suffix . '.js', array( 'jquery' ), '4.0.3' ); - wp_register_script( 'selectWoo', WC()->plugin_url() . '/assets/js/selectWoo/selectWoo.full' . $suffix . '.js', array( 'jquery' ), '1.0.6' ); - wp_register_script( 'wc-enhanced-select', WC()->plugin_url() . '/assets/js/admin/wc-enhanced-select' . $suffix . '.js', array( 'jquery', 'selectWoo' ), $version ); - wp_register_script( 'js-cookie', WC()->plugin_url() . '/assets/js/js-cookie/js.cookie' . $suffix . '.js', array(), '2.1.4', true ); - - wp_localize_script( - 'wc-enhanced-select', - 'wc_enhanced_select_params', - array( - 'i18n_no_matches' => _x( 'No matches found', 'enhanced select', 'woocommerce' ), - 'i18n_ajax_error' => _x( 'Loading failed', 'enhanced select', 'woocommerce' ), - 'i18n_input_too_short_1' => _x( 'Please enter 1 or more characters', 'enhanced select', 'woocommerce' ), - 'i18n_input_too_short_n' => _x( 'Please enter %qty% or more characters', 'enhanced select', 'woocommerce' ), - 'i18n_input_too_long_1' => _x( 'Please delete 1 character', 'enhanced select', 'woocommerce' ), - 'i18n_input_too_long_n' => _x( 'Please delete %qty% characters', 'enhanced select', 'woocommerce' ), - 'i18n_selection_too_long_1' => _x( 'You can only select 1 item', 'enhanced select', 'woocommerce' ), - 'i18n_selection_too_long_n' => _x( 'You can only select %qty% items', 'enhanced select', 'woocommerce' ), - 'i18n_load_more' => _x( 'Loading more results…', 'enhanced select', 'woocommerce' ), - 'i18n_searching' => _x( 'Searching…', 'enhanced select', 'woocommerce' ), - 'ajax_url' => admin_url( 'admin-ajax.php' ), - 'search_products_nonce' => wp_create_nonce( 'search-products' ), - 'search_customers_nonce' => wp_create_nonce( 'search-customers' ), - 'search_categories_nonce' => wp_create_nonce( 'search-categories' ), - ) - ); - - wp_register_script( 'accounting', WC()->plugin_url() . '/assets/js/accounting/accounting' . $suffix . '.js', array( 'jquery' ), '0.4.2' ); - wp_localize_script( - 'accounting', - 'accounting_params', - array( - 'mon_decimal_point' => wc_get_price_decimal_separator(), - ) - ); - - wp_register_script( 'wc-orders', WC()->plugin_url() . '/assets/js/admin/wc-orders' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone', 'jquery-blockui' ), $version ); - wp_localize_script( - 'wc-orders', - 'wc_orders_params', - array( - 'ajax_url' => admin_url( 'admin-ajax.php' ), - 'preview_nonce' => wp_create_nonce( 'woocommerce-preview-order' ), - ) - ); - - // WooCommerce admin pages. - if ( in_array( $screen_id, wc_get_screen_ids() ) ) { - wp_enqueue_script( 'iris' ); - wp_enqueue_script( 'woocommerce_admin' ); - wp_enqueue_script( 'wc-enhanced-select' ); - wp_enqueue_script( 'jquery-ui-sortable' ); - wp_enqueue_script( 'jquery-ui-autocomplete' ); - - $locale = localeconv(); - $decimal = isset( $locale['decimal_point'] ) ? $locale['decimal_point'] : '.'; - - $params = array( - /* translators: %s: decimal */ - 'i18n_decimal_error' => sprintf( __( 'Please enter with one decimal point (%s) without thousand separators.', 'woocommerce' ), $decimal ), - /* translators: %s: price decimal separator */ - 'i18n_mon_decimal_error' => sprintf( __( 'Please enter with one monetary decimal point (%s) without thousand separators and currency symbols.', 'woocommerce' ), wc_get_price_decimal_separator() ), - 'i18n_country_iso_error' => __( 'Please enter in country code with two capital letters.', 'woocommerce' ), - 'i18n_sale_less_than_regular_error' => __( 'Please enter in a value less than the regular price.', 'woocommerce' ), - 'i18n_delete_product_notice' => __( 'This product has produced sales and may be linked to existing orders. Are you sure you want to delete it?', 'woocommerce' ), - 'i18n_remove_personal_data_notice' => __( 'This action cannot be reversed. Are you sure you wish to erase personal data from the selected orders?', 'woocommerce' ), - 'decimal_point' => $decimal, - 'mon_decimal_point' => wc_get_price_decimal_separator(), - 'ajax_url' => admin_url( 'admin-ajax.php' ), - 'strings' => array( - 'import_products' => __( 'Import', 'woocommerce' ), - 'export_products' => __( 'Export', 'woocommerce' ), - ), - 'nonces' => array( - 'gateway_toggle' => wp_create_nonce( 'woocommerce-toggle-payment-gateway-enabled' ), - ), - 'urls' => array( - 'import_products' => current_user_can( 'import' ) ? esc_url_raw( admin_url( 'edit.php?post_type=product&page=product_importer' ) ) : null, - 'export_products' => current_user_can( 'export' ) ? esc_url_raw( admin_url( 'edit.php?post_type=product&page=product_exporter' ) ) : null, - ), - ); - - wp_localize_script( 'woocommerce_admin', 'woocommerce_admin', $params ); - } - - // Edit product category pages. - if ( in_array( $screen_id, array( 'edit-product_cat' ) ) ) { - wp_enqueue_media(); - } - - // Products. - if ( in_array( $screen_id, array( 'edit-product' ) ) ) { - wp_enqueue_script( 'woocommerce_quick-edit', WC()->plugin_url() . '/assets/js/admin/quick-edit' . $suffix . '.js', array( 'jquery', 'woocommerce_admin' ), $version ); - - $params = array( - 'strings' => array( - 'allow_reviews' => esc_js( __( 'Enable reviews', 'woocommerce' ) ), - ), - ); - - wp_localize_script( 'woocommerce_quick-edit', 'woocommerce_quick_edit', $params ); - } - - // Meta boxes. - if ( in_array( $screen_id, array( 'product', 'edit-product' ) ) ) { - wp_enqueue_media(); - wp_register_script( 'wc-admin-product-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'media-models' ), $version ); - wp_register_script( 'wc-admin-variation-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product-variation' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'serializejson', 'media-models' ), $version ); - - wp_enqueue_script( 'wc-admin-product-meta-boxes' ); - wp_enqueue_script( 'wc-admin-variation-meta-boxes' ); - - $params = array( - 'post_id' => isset( $post->ID ) ? $post->ID : '', - 'plugin_url' => WC()->plugin_url(), - 'ajax_url' => admin_url( 'admin-ajax.php' ), - 'woocommerce_placeholder_img_src' => wc_placeholder_img_src(), - 'add_variation_nonce' => wp_create_nonce( 'add-variation' ), - 'link_variation_nonce' => wp_create_nonce( 'link-variations' ), - 'delete_variations_nonce' => wp_create_nonce( 'delete-variations' ), - 'load_variations_nonce' => wp_create_nonce( 'load-variations' ), - 'save_variations_nonce' => wp_create_nonce( 'save-variations' ), - 'bulk_edit_variations_nonce' => wp_create_nonce( 'bulk-edit-variations' ), - /* translators: %d: Number of variations */ - 'i18n_link_all_variations' => esc_js( sprintf( __( 'Are you sure you want to link all variations? This will create a new variation for each and every possible combination of variation attributes (max %d per run).', 'woocommerce' ), Constants::is_defined( 'WC_MAX_LINKED_VARIATIONS' ) ? Constants::get_constant( 'WC_MAX_LINKED_VARIATIONS' ) : 50 ) ), - 'i18n_enter_a_value' => esc_js( __( 'Enter a value', 'woocommerce' ) ), - 'i18n_enter_menu_order' => esc_js( __( 'Variation menu order (determines position in the list of variations)', 'woocommerce' ) ), - 'i18n_enter_a_value_fixed_or_percent' => esc_js( __( 'Enter a value (fixed or %)', 'woocommerce' ) ), - 'i18n_delete_all_variations' => esc_js( __( 'Are you sure you want to delete all variations? This cannot be undone.', 'woocommerce' ) ), - 'i18n_last_warning' => esc_js( __( 'Last warning, are you sure?', 'woocommerce' ) ), - 'i18n_choose_image' => esc_js( __( 'Choose an image', 'woocommerce' ) ), - 'i18n_set_image' => esc_js( __( 'Set variation image', 'woocommerce' ) ), - 'i18n_variation_added' => esc_js( __( 'variation added', 'woocommerce' ) ), - 'i18n_variations_added' => esc_js( __( 'variations added', 'woocommerce' ) ), - 'i18n_no_variations_added' => esc_js( __( 'No variations added', 'woocommerce' ) ), - 'i18n_remove_variation' => esc_js( __( 'Are you sure you want to remove this variation?', 'woocommerce' ) ), - 'i18n_scheduled_sale_start' => esc_js( __( 'Sale start date (YYYY-MM-DD format or leave blank)', 'woocommerce' ) ), - 'i18n_scheduled_sale_end' => esc_js( __( 'Sale end date (YYYY-MM-DD format or leave blank)', 'woocommerce' ) ), - 'i18n_edited_variations' => esc_js( __( 'Save changes before changing page?', 'woocommerce' ) ), - 'i18n_variation_count_single' => esc_js( __( '%qty% variation', 'woocommerce' ) ), - 'i18n_variation_count_plural' => esc_js( __( '%qty% variations', 'woocommerce' ) ), - 'variations_per_page' => absint( apply_filters( 'woocommerce_admin_meta_boxes_variations_per_page', 15 ) ), - ); - - wp_localize_script( 'wc-admin-variation-meta-boxes', 'woocommerce_admin_meta_boxes_variations', $params ); - } - if ( in_array( str_replace( 'edit-', '', $screen_id ), wc_get_order_types( 'order-meta-boxes' ) ) ) { - $default_location = wc_get_customer_default_location(); - - wp_enqueue_script( 'wc-admin-order-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-order' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'wc-backbone-modal', 'selectWoo', 'wc-clipboard' ), $version ); - wp_localize_script( - 'wc-admin-order-meta-boxes', - 'woocommerce_admin_meta_boxes_order', - array( - 'countries' => wp_json_encode( array_merge( WC()->countries->get_allowed_country_states(), WC()->countries->get_shipping_country_states() ) ), - 'i18n_select_state_text' => esc_attr__( 'Select an option…', 'woocommerce' ), - 'default_country' => isset( $default_location['country'] ) ? $default_location['country'] : '', - 'default_state' => isset( $default_location['state'] ) ? $default_location['state'] : '', - 'placeholder_name' => esc_attr__( 'Name (required)', 'woocommerce' ), - 'placeholder_value' => esc_attr__( 'Value (required)', 'woocommerce' ), - ) - ); - } - if ( in_array( $screen_id, array( 'shop_coupon', 'edit-shop_coupon' ) ) ) { - wp_enqueue_script( 'wc-admin-coupon-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-coupon' . $suffix . '.js', array( 'wc-admin-meta-boxes' ), $version ); - wp_localize_script( - 'wc-admin-coupon-meta-boxes', - 'woocommerce_admin_meta_boxes_coupon', - array( - 'generate_button_text' => esc_html__( 'Generate coupon code', 'woocommerce' ), - 'characters' => apply_filters( 'woocommerce_coupon_code_generator_characters', 'ABCDEFGHJKMNPQRSTUVWXYZ23456789' ), - 'char_length' => apply_filters( 'woocommerce_coupon_code_generator_character_length', 8 ), - 'prefix' => apply_filters( 'woocommerce_coupon_code_generator_prefix', '' ), - 'suffix' => apply_filters( 'woocommerce_coupon_code_generator_suffix', '' ), - ) - ); - } - if ( in_array( str_replace( 'edit-', '', $screen_id ), array_merge( array( 'shop_coupon', 'product' ), wc_get_order_types( 'order-meta-boxes' ) ) ) ) { - $post_id = isset( $post->ID ) ? $post->ID : ''; - $currency = ''; - $remove_item_notice = __( 'Are you sure you want to remove the selected items?', 'woocommerce' ); - - if ( $post_id && in_array( get_post_type( $post_id ), wc_get_order_types( 'order-meta-boxes' ) ) ) { - $order = wc_get_order( $post_id ); - if ( $order ) { - $currency = $order->get_currency(); - - if ( ! $order->has_status( array( 'pending', 'failed', 'cancelled' ) ) ) { - $remove_item_notice = $remove_item_notice . ' ' . __( "You may need to manually restore the item's stock.", 'woocommerce' ); - } - } - } - - $params = array( - 'remove_item_notice' => $remove_item_notice, - 'i18n_select_items' => __( 'Please select some items.', 'woocommerce' ), - 'i18n_do_refund' => __( 'Are you sure you wish to process this refund? This action cannot be undone.', 'woocommerce' ), - 'i18n_delete_refund' => __( 'Are you sure you wish to delete this refund? This action cannot be undone.', 'woocommerce' ), - 'i18n_delete_tax' => __( 'Are you sure you wish to delete this tax column? This action cannot be undone.', 'woocommerce' ), - 'remove_item_meta' => __( 'Remove this item meta?', 'woocommerce' ), - 'remove_attribute' => __( 'Remove this attribute?', 'woocommerce' ), - 'name_label' => __( 'Name', 'woocommerce' ), - 'remove_label' => __( 'Remove', 'woocommerce' ), - 'click_to_toggle' => __( 'Click to toggle', 'woocommerce' ), - 'values_label' => __( 'Value(s)', 'woocommerce' ), - 'text_attribute_tip' => __( 'Enter some text, or some attributes by pipe (|) separating values.', 'woocommerce' ), - 'visible_label' => __( 'Visible on the product page', 'woocommerce' ), - 'used_for_variations_label' => __( 'Used for variations', 'woocommerce' ), - 'new_attribute_prompt' => __( 'Enter a name for the new attribute term:', 'woocommerce' ), - 'calc_totals' => __( 'Recalculate totals? This will calculate taxes based on the customers country (or the store base country) and update totals.', 'woocommerce' ), - 'copy_billing' => __( 'Copy billing information to shipping information? This will remove any currently entered shipping information.', 'woocommerce' ), - 'load_billing' => __( "Load the customer's billing information? This will remove any currently entered billing information.", 'woocommerce' ), - 'load_shipping' => __( "Load the customer's shipping information? This will remove any currently entered shipping information.", 'woocommerce' ), - 'featured_label' => __( 'Featured', 'woocommerce' ), - 'prices_include_tax' => esc_attr( get_option( 'woocommerce_prices_include_tax' ) ), - 'tax_based_on' => esc_attr( get_option( 'woocommerce_tax_based_on' ) ), - 'round_at_subtotal' => esc_attr( get_option( 'woocommerce_tax_round_at_subtotal' ) ), - 'no_customer_selected' => __( 'No customer selected', 'woocommerce' ), - 'plugin_url' => WC()->plugin_url(), - 'ajax_url' => admin_url( 'admin-ajax.php' ), - 'order_item_nonce' => wp_create_nonce( 'order-item' ), - 'add_attribute_nonce' => wp_create_nonce( 'add-attribute' ), - 'save_attributes_nonce' => wp_create_nonce( 'save-attributes' ), - 'calc_totals_nonce' => wp_create_nonce( 'calc-totals' ), - 'get_customer_details_nonce' => wp_create_nonce( 'get-customer-details' ), - 'search_products_nonce' => wp_create_nonce( 'search-products' ), - 'grant_access_nonce' => wp_create_nonce( 'grant-access' ), - 'revoke_access_nonce' => wp_create_nonce( 'revoke-access' ), - 'add_order_note_nonce' => wp_create_nonce( 'add-order-note' ), - 'delete_order_note_nonce' => wp_create_nonce( 'delete-order-note' ), - 'calendar_image' => WC()->plugin_url() . '/assets/images/calendar.png', - 'post_id' => isset( $post->ID ) ? $post->ID : '', - 'base_country' => WC()->countries->get_base_country(), - 'currency_format_num_decimals' => wc_get_price_decimals(), - 'currency_format_symbol' => get_woocommerce_currency_symbol( $currency ), - 'currency_format_decimal_sep' => esc_attr( wc_get_price_decimal_separator() ), - 'currency_format_thousand_sep' => esc_attr( wc_get_price_thousand_separator() ), - 'currency_format' => esc_attr( str_replace( array( '%1$s', '%2$s' ), array( '%s', '%v' ), get_woocommerce_price_format() ) ), // For accounting JS. - 'rounding_precision' => wc_get_rounding_precision(), - 'tax_rounding_mode' => wc_get_tax_rounding_mode(), - 'product_types' => array_unique( array_merge( array( 'simple', 'grouped', 'variable', 'external' ), array_keys( wc_get_product_types() ) ) ), - 'i18n_download_permission_fail' => __( 'Could not grant access - the user may already have permission for this file or billing email is not set. Ensure the billing email is set, and the order has been saved.', 'woocommerce' ), - 'i18n_permission_revoke' => __( 'Are you sure you want to revoke access to this download?', 'woocommerce' ), - 'i18n_tax_rate_already_exists' => __( 'You cannot add the same tax rate twice!', 'woocommerce' ), - 'i18n_delete_note' => __( 'Are you sure you wish to delete this note? This action cannot be undone.', 'woocommerce' ), - 'i18n_apply_coupon' => __( 'Enter a coupon code to apply. Discounts are applied to line totals, before taxes.', 'woocommerce' ), - 'i18n_add_fee' => __( 'Enter a fixed amount or percentage to apply as a fee.', 'woocommerce' ), - ); - - wp_localize_script( 'wc-admin-meta-boxes', 'woocommerce_admin_meta_boxes', $params ); - } - - // Term ordering - only when sorting by term_order. - if ( ( strstr( $screen_id, 'edit-pa_' ) || ( ! empty( $_GET['taxonomy'] ) && in_array( wp_unslash( $_GET['taxonomy'] ), apply_filters( 'woocommerce_sortable_taxonomies', array( 'product_cat' ) ) ) ) ) && ! isset( $_GET['orderby'] ) ) { - - wp_register_script( 'woocommerce_term_ordering', WC()->plugin_url() . '/assets/js/admin/term-ordering' . $suffix . '.js', array( 'jquery-ui-sortable' ), $version ); - wp_enqueue_script( 'woocommerce_term_ordering' ); - - $taxonomy = isset( $_GET['taxonomy'] ) ? wc_clean( wp_unslash( $_GET['taxonomy'] ) ) : ''; - - $woocommerce_term_order_params = array( - 'taxonomy' => $taxonomy, - ); - - wp_localize_script( 'woocommerce_term_ordering', 'woocommerce_term_ordering_params', $woocommerce_term_order_params ); - } - - // Product sorting - only when sorting by menu order on the products page. - if ( current_user_can( 'edit_others_pages' ) && 'edit-product' === $screen_id && isset( $wp_query->query['orderby'] ) && 'menu_order title' === $wp_query->query['orderby'] ) { - wp_register_script( 'woocommerce_product_ordering', WC()->plugin_url() . '/assets/js/admin/product-ordering' . $suffix . '.js', array( 'jquery-ui-sortable' ), $version, true ); - wp_enqueue_script( 'woocommerce_product_ordering' ); - } - - // Reports Pages. - if ( in_array( $screen_id, apply_filters( 'woocommerce_reports_screen_ids', array( $wc_screen_id . '_page_wc-reports', 'toplevel_page_wc-reports', 'dashboard' ) ) ) ) { - wp_register_script( 'wc-reports', WC()->plugin_url() . '/assets/js/admin/reports' . $suffix . '.js', array( 'jquery', 'jquery-ui-datepicker' ), $version ); - - wp_enqueue_script( 'wc-reports' ); - wp_enqueue_script( 'flot' ); - wp_enqueue_script( 'flot-resize' ); - wp_enqueue_script( 'flot-time' ); - wp_enqueue_script( 'flot-pie' ); - wp_enqueue_script( 'flot-stack' ); - } - - // API settings. - if ( $wc_screen_id . '_page_wc-settings' === $screen_id && isset( $_GET['section'] ) && 'keys' == $_GET['section'] ) { - wp_register_script( 'wc-api-keys', WC()->plugin_url() . '/assets/js/admin/api-keys' . $suffix . '.js', array( 'jquery', 'woocommerce_admin', 'underscore', 'backbone', 'wp-util', 'qrcode', 'wc-clipboard' ), $version, true ); - wp_enqueue_script( 'wc-api-keys' ); - wp_localize_script( - 'wc-api-keys', - 'woocommerce_admin_api_keys', - array( - 'ajax_url' => admin_url( 'admin-ajax.php' ), - 'update_api_nonce' => wp_create_nonce( 'update-api-key' ), - 'clipboard_failed' => esc_html__( 'Copying to clipboard failed. Please press Ctrl/Cmd+C to copy.', 'woocommerce' ), - ) - ); - } - - // System status. - if ( $wc_screen_id . '_page_wc-status' === $screen_id ) { - wp_register_script( 'wc-admin-system-status', WC()->plugin_url() . '/assets/js/admin/system-status' . $suffix . '.js', array( 'wc-clipboard' ), $version ); - wp_enqueue_script( 'wc-admin-system-status' ); - wp_localize_script( - 'wc-admin-system-status', - 'woocommerce_admin_system_status', - array( - 'delete_log_confirmation' => esc_js( __( 'Are you sure you want to delete this log?', 'woocommerce' ) ), - 'run_tool_confirmation' => esc_js( __( 'Are you sure you want to run this tool?', 'woocommerce' ) ), - ) - ); - } - - if ( in_array( $screen_id, array( 'user-edit', 'profile' ) ) ) { - wp_register_script( 'wc-users', WC()->plugin_url() . '/assets/js/admin/users' . $suffix . '.js', array( 'jquery', 'wc-enhanced-select', 'selectWoo' ), $version, true ); - wp_enqueue_script( 'wc-users' ); - wp_localize_script( - 'wc-users', - 'wc_users_params', - array( - 'countries' => wp_json_encode( array_merge( WC()->countries->get_allowed_country_states(), WC()->countries->get_shipping_country_states() ) ), - 'i18n_select_state_text' => esc_attr__( 'Select an option…', 'woocommerce' ), - ) - ); - } - - if ( WC_Marketplace_Suggestions::show_suggestions_for_screen( $screen_id ) ) { - $active_plugin_slugs = array_map( 'dirname', get_option( 'active_plugins' ) ); - wp_register_script( - 'marketplace-suggestions', - WC()->plugin_url() . '/assets/js/admin/marketplace-suggestions' . $suffix . '.js', - array( 'jquery', 'underscore', 'js-cookie' ), - $version, - true - ); - wp_localize_script( - 'marketplace-suggestions', - 'marketplace_suggestions', - array( - 'dismiss_suggestion_nonce' => wp_create_nonce( 'add_dismissed_marketplace_suggestion' ), - 'active_plugins' => $active_plugin_slugs, - 'dismissed_suggestions' => WC_Marketplace_Suggestions::get_dismissed_suggestions(), - 'suggestions_data' => WC_Marketplace_Suggestions::get_suggestions_api_data(), - 'manage_suggestions_url' => admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=woocommerce_com' ), - 'in_app_purchase_params' => WC_Admin_Addons::get_in_app_purchase_url_params(), - 'i18n_marketplace_suggestions_default_cta' - => esc_html__( 'Learn More', 'woocommerce' ), - 'i18n_marketplace_suggestions_dismiss_tooltip' - => esc_attr__( 'Dismiss this suggestion', 'woocommerce' ), - 'i18n_marketplace_suggestions_manage_suggestions' - => esc_html__( 'Manage suggestions', 'woocommerce' ), - ) - ); - wp_enqueue_script( 'marketplace-suggestions' ); - } - - } - - } - -endif; - -return new WC_Admin_Assets(); diff --git a/includes/admin/class-wc-admin-dashboard.php b/includes/admin/class-wc-admin-dashboard.php deleted file mode 100644 index 2bc03a07edb..00000000000 --- a/includes/admin/class-wc-admin-dashboard.php +++ /dev/null @@ -1,547 +0,0 @@ -register_network_order_widget(); - } - } - - /** - * Register the network order dashboard widget. - */ - public function register_network_order_widget() { - wp_add_dashboard_widget( 'woocommerce_network_orders', __( 'WooCommerce Network Orders', 'woocommerce' ), array( $this, 'network_orders' ) ); - } - - /** - * Get top seller from DB. - * - * @return object - */ - private function get_top_seller() { - global $wpdb; - - $query = array(); - $query['fields'] = "SELECT SUM( order_item_meta.meta_value ) as qty, order_item_meta_2.meta_value as product_id - FROM {$wpdb->posts} as posts"; - $query['join'] = "INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_id "; - $query['join'] .= "INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta ON order_items.order_item_id = order_item_meta.order_item_id "; - $query['join'] .= "INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_2 ON order_items.order_item_id = order_item_meta_2.order_item_id "; - $query['where'] = "WHERE posts.post_type IN ( '" . implode( "','", wc_get_order_types( 'order-count' ) ) . "' ) "; - $query['where'] .= "AND posts.post_status IN ( 'wc-" . implode( "','wc-", apply_filters( 'woocommerce_reports_order_statuses', array( 'completed', 'processing', 'on-hold' ) ) ) . "' ) "; - $query['where'] .= "AND order_item_meta.meta_key = '_qty' "; - $query['where'] .= "AND order_item_meta_2.meta_key = '_product_id' "; - $query['where'] .= "AND posts.post_date >= '" . gmdate( 'Y-m-01', current_time( 'timestamp' ) ) . "' "; - $query['where'] .= "AND posts.post_date <= '" . gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) ) . "' "; - $query['groupby'] = 'GROUP BY product_id'; - $query['orderby'] = 'ORDER BY qty DESC'; - $query['limits'] = 'LIMIT 1'; - - return $wpdb->get_row( implode( ' ', apply_filters( 'woocommerce_dashboard_status_widget_top_seller_query', $query ) ) ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - } - - /** - * Get sales report data. - * - * @return object - */ - private function get_sales_report_data() { - include_once dirname( __FILE__ ) . '/reports/class-wc-report-sales-by-date.php'; - - $sales_by_date = new WC_Report_Sales_By_Date(); - $sales_by_date->start_date = strtotime( gmdate( 'Y-m-01', current_time( 'timestamp' ) ) ); - $sales_by_date->end_date = strtotime( gmdate( 'Y-m-d', current_time( 'timestamp' ) ) ); - $sales_by_date->chart_groupby = 'day'; - $sales_by_date->group_by_query = 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)'; - - return $sales_by_date->get_report_data(); - } - - /** - * Show status widget. - */ - public function status_widget() { - $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; - $version = Constants::get_constant( 'WC_VERSION' ); - - wp_enqueue_script( 'wc-status-widget', WC()->plugin_url() . '/assets/js/admin/wc-status-widget' . $suffix . '.js', array( 'jquery' ), $version, true ); - - include_once dirname( __FILE__ ) . '/reports/class-wc-admin-report.php'; - - $is_wc_admin_disabled = apply_filters( 'woocommerce_admin_disabled', false ); - - $reports = new WC_Admin_Report(); - - $net_sales_link = 'admin.php?page=wc-reports&tab=orders&range=month'; - $top_seller_link = 'admin.php?page=wc-reports&tab=orders&report=sales_by_product&range=month&product_ids='; - $report_data = $is_wc_admin_disabled ? $this->get_sales_report_data() : $this->get_wc_admin_performance_data(); - if ( ! $is_wc_admin_disabled ) { - $net_sales_link = 'admin.php?page=wc-admin&path=%2Fanalytics%2Frevenue&chart=net_revenue&orderby=net_revenue&period=month&compare=previous_period'; - $top_seller_link = 'admin.php?page=wc-admin&filter=single_product&path=%2Fanalytics%2Fproducts&products='; - } - - echo ''; - } - - /** - * Show order data is status widget. - */ - private function status_widget_order_rows() { - if ( ! current_user_can( 'edit_shop_orders' ) ) { - return; - } - $on_hold_count = 0; - $processing_count = 0; - - foreach ( wc_get_order_types( 'order-count' ) as $type ) { - $counts = (array) wp_count_posts( $type ); - $on_hold_count += isset( $counts['wc-on-hold'] ) ? $counts['wc-on-hold'] : 0; - $processing_count += isset( $counts['wc-processing'] ) ? $counts['wc-processing'] : 0; - } - ?> -
  • - - %s order awaiting processing', '%s orders awaiting processing', $processing_count, 'woocommerce' ), - $processing_count - ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped - ?> - -
  • -
  • - - %s order on-hold', '%s orders on-hold', $on_hold_count, 'woocommerce' ), - $on_hold_count - ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped - ?> - -
  • - get_var( - $wpdb->prepare( - "SELECT COUNT( product_id ) - FROM {$wpdb->wc_product_meta_lookup} AS lookup - INNER JOIN {$wpdb->posts} as posts ON lookup.product_id = posts.ID - WHERE stock_quantity <= %d - AND stock_quantity > %d - AND posts.post_status = 'publish'", - $stock, - $nostock - ) - ); - } - - set_transient( $transient_name, (int) $lowinstock_count, DAY_IN_SECONDS * 30 ); - } - - $transient_name = 'wc_outofstock_count'; - $outofstock_count = get_transient( $transient_name ); - $lowstock_link = 'admin.php?page=wc-reports&tab=stock&report=low_in_stock'; - $outofstock_link = 'admin.php?page=wc-reports&tab=stock&report=out_of_stock'; - - if ( false === $is_wc_admin_disabled ) { - $lowstock_link = 'admin.php?page=wc-admin&type=lowstock&path=%2Fanalytics%2Fstock'; - $outofstock_link = 'admin.php?page=wc-admin&type=outofstock&path=%2Fanalytics%2Fstock'; - } - - if ( false === $outofstock_count ) { - /** - * Status widget out of stock count pre query. - * - * @since 4.3.0 - * @param null|string $outofstock_count Out of stock count, by default null. - * @param int $nostock No stock amount - */ - $outofstock_count = apply_filters( 'woocommerce_status_widget_out_of_stock_count_pre_query', null, $nostock ); - - if ( is_null( $outofstock_count ) ) { - $outofstock_count = (int) $wpdb->get_var( - $wpdb->prepare( - "SELECT COUNT( product_id ) - FROM {$wpdb->wc_product_meta_lookup} AS lookup - INNER JOIN {$wpdb->posts} as posts ON lookup.product_id = posts.ID - WHERE stock_quantity <= %d - AND posts.post_status = 'publish'", - $nostock - ) - ); - } - - set_transient( $transient_name, (int) $outofstock_count, DAY_IN_SECONDS * 30 ); - } - ?> -
  • - - %s product low in stock', '%s products low in stock', $lowinstock_count, 'woocommerce' ), - $lowinstock_count - ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped - ?> - -
  • -
  • - - %s product out of stock', '%s products out of stock', $outofstock_count, 'woocommerce' ), - $outofstock_count - ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped - ?> - -
  • - comments} comments - LEFT JOIN {$wpdb->posts} posts ON (comments.comment_post_ID = posts.ID) - WHERE comments.comment_approved = '1' - AND comments.comment_type = 'review' - AND posts.post_password = '' - AND posts.post_type = 'product' - AND comments.comment_parent = 0 - ORDER BY comments.comment_date_gmt DESC - LIMIT 5" - ); - - $comments = $wpdb->get_results( - "SELECT posts.ID, posts.post_title, comments.comment_author, comments.comment_author_email, comments.comment_ID, comments.comment_content {$query_from};" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - ); - - if ( $comments ) { - echo ''; - } else { - echo '

    ' . esc_html__( 'There are no product reviews yet.', 'woocommerce' ) . '

    '; - } - } - - /** - * Network orders widget. - */ - public function network_orders() { - $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; - $version = Constants::get_constant( 'WC_VERSION' ); - - wp_enqueue_style( 'wc-network-orders', WC()->plugin_url() . '/assets/css/network-order-widget.css', array(), $version ); - - wp_enqueue_script( 'wc-network-orders', WC()->plugin_url() . '/assets/js/admin/network-orders' . $suffix . '.js', array( 'jquery', 'underscore' ), $version, true ); - - $user = wp_get_current_user(); - $blogs = get_blogs_of_user( $user->ID ); - $blog_ids = wp_list_pluck( $blogs, 'userblog_id' ); - - wp_localize_script( - 'wc-network-orders', - 'woocommerce_network_orders', - array( - 'nonce' => wp_create_nonce( 'wp_rest' ), - 'sites' => array_values( $blog_ids ), - 'order_endpoint' => get_rest_url( null, 'wc/v3/orders/network' ), - ) - ); - ?> -
    -
    -

    - -

    - -
    -
    - - - - - - - - - - -
    -
    -

    - -

    -
    - - - - - set_query_params( - array( - 'before' => $end_date, - 'after' => $start_date, - 'stats' => 'revenue/total_sales,revenue/net_revenue,orders/orders_count,products/items_sold,variations/items_sold', - ) - ); - $response = rest_do_request( $request ); - - if ( is_wp_error( $response ) ) { - return $response; - } - - if ( 200 !== $response->get_status() ) { - return new \WP_Error( 'woocommerce_analytics_performance_indicators_result_failed', __( 'Sorry, fetching performance indicators failed.', 'woocommerce' ) ); - } - $report_keys = array( - 'net_revenue' => 'net_sales', - ); - $performance_data = new stdClass(); - foreach ( $response->get_data() as $indicator ) { - if ( isset( $indicator['chart'] ) && isset( $indicator['value'] ) ) { - $key = isset( $report_keys[ $indicator['chart'] ] ) ? $report_keys[ $indicator['chart'] ] : $indicator['chart']; - $performance_data->$key = $indicator['value']; - } - } - return $performance_data; - } - - /** - * Overwrites the original sparkline to use the new reports data if WooAdmin is enabled. - * Prepares a sparkline to show sales in the last X days. - * - * @param WC_Admin_Report $reports old class for getting reports. - * @param bool $is_wc_admin_disabled If WC Admin is disabled or not. - * @param int $id ID of the product to show. Blank to get all orders. - * @param string $type Type of sparkline to get. Ignored if ID is not set. - * @return string - */ - private function sales_sparkline( $reports, $is_wc_admin_disabled = false, $id = '', $type = 'sales' ) { - $days = max( 7, gmdate( 'd', current_time( 'timestamp' ) ) ); - if ( $is_wc_admin_disabled ) { - return $reports->sales_sparkline( $id, $days, $type ); - } - $sales_endpoint = '/wc-analytics/reports/revenue/stats'; - $start_date = gmdate( 'Y-m-d 00:00:00', current_time( 'timestamp' ) - ( ( $days - 1 ) * DAY_IN_SECONDS ) ); - $end_date = gmdate( 'Y-m-d 23:59:59', current_time( 'timestamp' ) ); - $meta_key = 'net_revenue'; - $params = array( - 'order' => 'asc', - 'interval' => 'day', - 'per_page' => 100, - 'before' => $end_date, - 'after' => $start_date, - ); - if ( $id ) { - $sales_endpoint = '/wc-analytics/reports/products/stats'; - $meta_key = ( 'sales' === $type ) ? 'net_revenue' : 'items_sold'; - $params['products'] = $id; - } - $request = new \WP_REST_Request( 'GET', $sales_endpoint ); - $params['fields'] = array( $meta_key ); - $request->set_query_params( $params ); - - $response = rest_do_request( $request ); - - if ( is_wp_error( $response ) ) { - return $response; - } - - $resp_data = $response->get_data(); - $data = $resp_data['intervals']; - - $sparkline_data = array(); - $total = 0; - foreach ( $data as $d ) { - $total += $d['subtotals']->$meta_key; - array_push( $sparkline_data, array( strval( strtotime( $d['interval'] ) * 1000 ), $d['subtotals']->$meta_key ) ); - } - - if ( 'sales' === $type ) { - /* translators: 1: total income 2: days */ - $tooltip = sprintf( __( 'Sold %1$s worth in the last %2$d days', 'woocommerce' ), strip_tags( wc_price( $total ) ), $days ); - } else { - /* translators: 1: total items sold 2: days */ - $tooltip = sprintf( _n( 'Sold %1$d item in the last %2$d days', 'Sold %1$d items in the last %2$d days', $total, 'woocommerce' ), $total, $days ); - } - - return ''; - } - } - -endif; - -return new WC_Admin_Dashboard(); diff --git a/includes/admin/class-wc-admin-help.php b/includes/admin/class-wc-admin-help.php deleted file mode 100644 index e2654e8888f..00000000000 --- a/includes/admin/class-wc-admin-help.php +++ /dev/null @@ -1,85 +0,0 @@ -id, wc_get_screen_ids() ) ) { - return; - } - - $screen->add_help_tab( - array( - 'id' => 'woocommerce_support_tab', - 'title' => __( 'Help & Support', 'woocommerce' ), - 'content' => - '

    ' . __( 'Help & Support', 'woocommerce' ) . '

    ' . - '

    ' . sprintf( - /* translators: %s: Documentation URL */ - __( 'Should you need help understanding, using, or extending WooCommerce, please read our documentation. You will find all kinds of resources including snippets, tutorials and much more.', 'woocommerce' ), - 'https://docs.woocommerce.com/documentation/plugins/woocommerce/?utm_source=helptab&utm_medium=product&utm_content=docs&utm_campaign=woocommerceplugin' - ) . '

    ' . - '

    ' . sprintf( - /* translators: %s: Forum URL */ - __( 'For further assistance with WooCommerce core, use the community forum. For help with premium extensions sold on WooCommerce.com, open a support request at WooCommerce.com.', 'woocommerce' ), - 'https://wordpress.org/support/plugin/woocommerce', - 'https://woocommerce.com/my-account/create-a-ticket/?utm_source=helptab&utm_medium=product&utm_content=tickets&utm_campaign=woocommerceplugin' - ) . '

    ' . - '

    ' . __( 'Before asking for help, we recommend checking the system status page to identify any problems with your configuration.', 'woocommerce' ) . '

    ' . - '

    ' . __( 'System status', 'woocommerce' ) . ' ' . __( 'Community forum', 'woocommerce' ) . ' ' . __( 'WooCommerce.com support', 'woocommerce' ) . '

    ', - ) - ); - - $screen->add_help_tab( - array( - 'id' => 'woocommerce_bugs_tab', - 'title' => __( 'Found a bug?', 'woocommerce' ), - 'content' => - '

    ' . __( 'Found a bug?', 'woocommerce' ) . '

    ' . - /* translators: 1: GitHub issues URL 2: GitHub contribution guide URL 3: System status report URL */ - '

    ' . sprintf( __( 'If you find a bug within WooCommerce core you can create a ticket via Github issues. Ensure you read the contribution guide prior to submitting your report. To help us solve your issue, please be as descriptive as possible and include your system status report.', 'woocommerce' ), 'https://github.com/woocommerce/woocommerce/issues?state=open', 'https://github.com/woocommerce/woocommerce/blob/master/.github/CONTRIBUTING.md', admin_url( 'admin.php?page=wc-status' ) ) . '

    ' . - '

    ' . __( 'Report a bug', 'woocommerce' ) . ' ' . __( 'System status', 'woocommerce' ) . '

    ', - - ) - ); - - $screen->set_help_sidebar( - '

    ' . __( 'For more information:', 'woocommerce' ) . '

    ' . - '

    ' . __( 'About WooCommerce', 'woocommerce' ) . '

    ' . - '

    ' . __( 'WordPress.org project', 'woocommerce' ) . '

    ' . - '

    ' . __( 'Github project', 'woocommerce' ) . '

    ' . - '

    ' . __( 'Official theme', 'woocommerce' ) . '

    ' . - '

    ' . __( 'Official extensions', 'woocommerce' ) . '

    ' - ); - } -} - -return new WC_Admin_Help(); diff --git a/includes/admin/class-wc-admin-menus.php b/includes/admin/class-wc-admin-menus.php deleted file mode 100644 index d926fcf548e..00000000000 --- a/includes/admin/class-wc-admin-menus.php +++ /dev/null @@ -1,392 +0,0 @@ - Menus > Pages. - add_action( 'admin_head-nav-menus.php', array( $this, 'add_nav_menu_meta_boxes' ) ); - - // Admin bar menus. - if ( apply_filters( 'woocommerce_show_admin_bar_visit_store', true ) ) { - add_action( 'admin_bar_menu', array( $this, 'admin_bar_menus' ), 31 ); - } - - // Handle saving settings earlier than load-{page} hook to avoid race conditions in conditional menus. - add_action( 'wp_loaded', array( $this, 'save_settings' ) ); - } - - /** - * Add menu items. - */ - public function admin_menu() { - global $menu; - - $woocommerce_icon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiPjxwYXRoIGZpbGw9IiNhMmFhYjIiIGQ9Ik02MTIuMTkyIDQyNi4zMzZjMC02Ljg5Ni0zLjEzNi01MS42LTI4LTUxLjYtMzcuMzYgMC00Ni43MDQgNzIuMjU2LTQ2LjcwNCA4Mi42MjQgMCAzLjQwOCAzLjE1MiA1OC40OTYgMjguMDMyIDU4LjQ5NiAzNC4xOTItLjAzMiA0Ni42NzItNzIuMjg4IDQ2LjY3Mi04OS41MnptMjAyLjE5MiAwYzAtNi44OTYtMy4xNTItNTEuNi0yOC4wMzItNTEuNi0zNy4yOCAwLTQ2LjYwOCA3Mi4yNTYtNDYuNjA4IDgyLjYyNCAwIDMuNDA4IDMuMDcyIDU4LjQ5NiAyNy45NTIgNTguNDk2IDM0LjE5Mi0uMDMyIDQ2LjY4OC03Mi4yODggNDYuNjg4LTg5LjUyek0xNDEuMjk2Ljc2OGMtNjguMjI0IDAtMTIzLjUwNCA1NS40ODgtMTIzLjUwNCAxMjMuOTJ2NjUwLjcyYzAgNjguNDMyIDU1LjI5NiAxMjMuOTIgMTIzLjUwNCAxMjMuOTJoMzM5LjgwOGwxMjMuNTA0IDEyMy45MzZWODk5LjMyOGgyNzguMDQ4YzY4LjIyNCAwIDEyMy41Mi01NS40NzIgMTIzLjUyLTEyMy45MnYtNjUwLjcyYzAtNjguNDMyLTU1LjI5Ni0xMjMuOTItMTIzLjUyLTEyMy45MmgtNzQxLjM2em01MjYuODY0IDQyMi4xNmMwIDU1LjA4OC0zMS4wODggMTU0Ljg4LTEwMi42NCAxNTQuODgtNi4yMDggMC0xOC40OTYtMy42MTYtMjUuNDI0LTYuMDE2LTMyLjUxMi0xMS4xNjgtNTAuMTkyLTQ5LjY5Ni01Mi4zNTItNjYuMjU2IDAgMC0zLjA3Mi0xNy43OTItMy4wNzItNDAuNzUyIDAtMjIuOTkyIDMuMDcyLTQ1LjMyOCAzLjA3Mi00NS4zMjggMTUuNTUyLTc1LjcyOCA0My41NTItMTA2LjczNiA5Ni40NDgtMTA2LjczNiA1OS4wNzItLjAzMiA4My45NjggNTguNTI4IDgzLjk2OCAxMTAuMjA4ek00ODYuNDk2IDMwMi40YzAgMy4zOTItNDMuNTUyIDE0MS4xNjgtNDMuNTUyIDIxMy40MjR2NzUuNzEyYy0yLjU5MiAxMi4wOC00LjE2IDI0LjE0NC0yMS44MjQgMjQuMTQ0LTQ2LjYwOCAwLTg4Ljg4LTE1MS40NzItOTIuMDE2LTE2MS44NC02LjIwOCA2Ljg5Ni02Mi4yNCAxNjEuODQtOTYuNDQ4IDE2MS44NC0yNC44NjQgMC00My41NTItMTEzLjY0OC00Ni42MDgtMTIzLjkzNkMxNzYuNzA0IDQzNi42NzIgMTYwIDMzNC4yMjQgMTYwIDMyNy4zMjhjMC0yMC42NzIgMS4xNTItMzguNzM2IDI2LjA0OC0zOC43MzYgNi4yMDggMCAyMS42IDYuMDY0IDIzLjcxMiAxNy4xNjggMTEuNjQ4IDYyLjAzMiAxNi42ODggMTIwLjUxMiAyOS4xNjggMTg1Ljk2OCAxLjg1NiAyLjkyOCAxLjUwNCA3LjAwOCA0LjU2IDEwLjQzMiAzLjE1Mi0xMC4yODggNjYuOTI4LTE2OC43ODQgOTQuOTYtMTY4Ljc4NCAyMi41NDQgMCAzMC40IDQ0LjU5MiAzMy41MzYgNjEuODI0IDYuMjA4IDIwLjY1NiAxMy4wODggNTUuMjE2IDIyLjQxNiA4Mi43NTIgMC0xMy43NzYgMTIuNDgtMjAzLjEyIDY1LjM5Mi0yMDMuMTIgMTguNTkyLjAzMiAyNi43MDQgNi45MjggMjYuNzA0IDI3LjU2OHpNODcwLjMyIDQyMi45MjhjMCA1NS4wODgtMzEuMDg4IDE1NC44OC0xMDIuNjQgMTU0Ljg4LTYuMTkyIDAtMTguNDQ4LTMuNjE2LTI1LjQyNC02LjAxNi0zMi40MzItMTEuMTY4LTUwLjE3Ni00OS42OTYtNTIuMjg4LTY2LjI1NiAwIDAtMy44ODgtMTcuOTItMy44ODgtNDAuODk2czMuODg4LTQ1LjE4NCAzLjg4OC00NS4xODRjMTUuNTUyLTc1LjcyOCA0My40ODgtMTA2LjczNiA5Ni4zODQtMTA2LjczNiA1OS4xMDQtLjAzMiA4My45NjggNTguNTI4IDgzLjk2OCAxMTAuMjA4eiIvPjwvc3ZnPg=='; - - if ( current_user_can( 'edit_others_shop_orders' ) ) { - $menu[] = array( '', 'read', 'separator-woocommerce', '', 'wp-menu-separator woocommerce' ); // WPCS: override ok. - } - - add_menu_page( __( 'WooCommerce', 'woocommerce' ), __( 'WooCommerce', 'woocommerce' ), 'edit_others_shop_orders', 'woocommerce', null, $woocommerce_icon, '55.5' ); - - add_submenu_page( 'edit.php?post_type=product', __( 'Attributes', 'woocommerce' ), __( 'Attributes', 'woocommerce' ), 'manage_product_terms', 'product_attributes', array( $this, 'attributes_page' ) ); - } - - /** - * Add menu item. - */ - public function reports_menu() { - if ( current_user_can( 'edit_others_shop_orders' ) ) { - add_submenu_page( 'woocommerce', __( 'Reports', 'woocommerce' ), __( 'Reports', 'woocommerce' ), 'view_woocommerce_reports', 'wc-reports', array( $this, 'reports_page' ) ); - } else { - add_menu_page( __( 'Sales reports', 'woocommerce' ), __( 'Sales reports', 'woocommerce' ), 'view_woocommerce_reports', 'wc-reports', array( $this, 'reports_page' ), 'dashicons-chart-bar', '55.6' ); - } - } - - /** - * Add menu item. - */ - public function settings_menu() { - $settings_page = add_submenu_page( 'woocommerce', __( 'WooCommerce settings', 'woocommerce' ), __( 'Settings', 'woocommerce' ), 'manage_woocommerce', 'wc-settings', array( $this, 'settings_page' ) ); - - add_action( 'load-' . $settings_page, array( $this, 'settings_page_init' ) ); - } - - /** - * Loads gateways and shipping methods into memory for use within settings. - */ - public function settings_page_init() { - WC()->payment_gateways(); - WC()->shipping(); - - // Include settings pages. - WC_Admin_Settings::get_settings_pages(); - - // Add any posted messages. - if ( ! empty( $_GET['wc_error'] ) ) { // WPCS: input var okay, CSRF ok. - WC_Admin_Settings::add_error( wp_kses_post( wp_unslash( $_GET['wc_error'] ) ) ); // WPCS: input var okay, CSRF ok. - } - - if ( ! empty( $_GET['wc_message'] ) ) { // WPCS: input var okay, CSRF ok. - WC_Admin_Settings::add_message( wp_kses_post( wp_unslash( $_GET['wc_message'] ) ) ); // WPCS: input var okay, CSRF ok. - } - - do_action( 'woocommerce_settings_page_init' ); - } - - /** - * Handle saving of settings. - * - * @return void - */ - public function save_settings() { - global $current_tab, $current_section; - - // We should only save on the settings page. - if ( ! is_admin() || ! isset( $_GET['page'] ) || 'wc-settings' !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - return; - } - - // Include settings pages. - WC_Admin_Settings::get_settings_pages(); - - // Get current tab/section. - $current_tab = empty( $_GET['tab'] ) ? 'general' : sanitize_title( wp_unslash( $_GET['tab'] ) ); // WPCS: input var okay, CSRF ok. - $current_section = empty( $_REQUEST['section'] ) ? '' : sanitize_title( wp_unslash( $_REQUEST['section'] ) ); // WPCS: input var okay, CSRF ok. - - // Save settings if data has been posted. - if ( '' !== $current_section && apply_filters( "woocommerce_save_settings_{$current_tab}_{$current_section}", ! empty( $_POST['save'] ) ) ) { // WPCS: input var okay, CSRF ok. - WC_Admin_Settings::save(); - } elseif ( '' === $current_section && apply_filters( "woocommerce_save_settings_{$current_tab}", ! empty( $_POST['save'] ) ) ) { // WPCS: input var okay, CSRF ok. - WC_Admin_Settings::save(); - } - } - - /** - * Add menu item. - */ - public function status_menu() { - add_submenu_page( 'woocommerce', __( 'WooCommerce status', 'woocommerce' ), __( 'Status', 'woocommerce' ), 'manage_woocommerce', 'wc-status', array( $this, 'status_page' ) ); - } - - /** - * Addons menu item. - */ - public function addons_menu() { - $count_html = WC_Helper_Updater::get_updates_count_html(); - /* translators: %s: extensions count */ - $menu_title = sprintf( __( 'Extensions %s', 'woocommerce' ), $count_html ); - add_submenu_page( 'woocommerce', __( 'WooCommerce extensions', 'woocommerce' ), $menu_title, 'manage_woocommerce', 'wc-addons', array( $this, 'addons_page' ) ); - } - - /** - * Highlights the correct top level admin menu item for post type add screens. - */ - public function menu_highlight() { - global $parent_file, $submenu_file, $post_type; - - switch ( $post_type ) { - case 'shop_order': - case 'shop_coupon': - $parent_file = 'woocommerce'; // WPCS: override ok. - break; - case 'product': - $screen = get_current_screen(); - if ( $screen && taxonomy_is_product_attribute( $screen->taxonomy ) ) { - $submenu_file = 'product_attributes'; // WPCS: override ok. - $parent_file = 'edit.php?post_type=product'; // WPCS: override ok. - } - break; - } - } - - /** - * Adds the order processing count to the menu. - */ - public function menu_order_count() { - global $submenu; - - if ( isset( $submenu['woocommerce'] ) ) { - // Remove 'WooCommerce' sub menu item. - unset( $submenu['woocommerce'][0] ); - - // Add count if user has access. - if ( apply_filters( 'woocommerce_include_processing_order_count_in_menu', true ) && current_user_can( 'edit_others_shop_orders' ) ) { - $order_count = apply_filters( 'woocommerce_menu_order_count', wc_processing_order_count() ); - - if ( $order_count ) { - foreach ( $submenu['woocommerce'] as $key => $menu_item ) { - if ( 0 === strpos( $menu_item[0], _x( 'Orders', 'Admin menu name', 'woocommerce' ) ) ) { - $submenu['woocommerce'][ $key ][0] .= ' ' . number_format_i18n( $order_count ) . ''; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - break; - } - } - } - } - } - } - - /** - * Reorder the WC menu items in admin. - * - * @param int $menu_order Menu order. - * @return array - */ - public function menu_order( $menu_order ) { - // Initialize our custom order array. - $woocommerce_menu_order = array(); - - // Get the index of our custom separator. - $woocommerce_separator = array_search( 'separator-woocommerce', $menu_order, true ); - - // Get index of product menu. - $woocommerce_product = array_search( 'edit.php?post_type=product', $menu_order, true ); - - // Loop through menu order and do some rearranging. - foreach ( $menu_order as $index => $item ) { - - if ( 'woocommerce' === $item ) { - $woocommerce_menu_order[] = 'separator-woocommerce'; - $woocommerce_menu_order[] = $item; - $woocommerce_menu_order[] = 'edit.php?post_type=product'; - unset( $menu_order[ $woocommerce_separator ] ); - unset( $menu_order[ $woocommerce_product ] ); - } elseif ( ! in_array( $item, array( 'separator-woocommerce' ), true ) ) { - $woocommerce_menu_order[] = $item; - } - } - - // Return order. - return $woocommerce_menu_order; - } - - /** - * Custom menu order. - * - * @param bool $enabled Whether custom menu ordering is already enabled. - * @return bool - */ - public function custom_menu_order( $enabled ) { - return $enabled || current_user_can( 'edit_others_shop_orders' ); - } - - /** - * Validate screen options on update. - * - * @param bool|int $status Screen option value. Default false to skip. - * @param string $option The option name. - * @param int $value The number of rows to use. - */ - public function set_screen_option( $status, $option, $value ) { - if ( in_array( $option, array( 'woocommerce_keys_per_page', 'woocommerce_webhooks_per_page' ), true ) ) { - return $value; - } - - return $status; - } - - /** - * Init the reports page. - */ - public function reports_page() { - WC_Admin_Reports::output(); - } - - /** - * Init the settings page. - */ - public function settings_page() { - WC_Admin_Settings::output(); - } - - /** - * Init the attributes page. - */ - public function attributes_page() { - WC_Admin_Attributes::output(); - } - - /** - * Init the status page. - */ - public function status_page() { - WC_Admin_Status::output(); - } - - /** - * Init the addons page. - */ - public function addons_page() { - WC_Admin_Addons::output(); - } - - /** - * Add custom nav meta box. - * - * Adapted from http://www.johnmorrisonline.com/how-to-add-a-fully-functional-custom-meta-box-to-wordpress-navigation-menus/. - */ - public function add_nav_menu_meta_boxes() { - add_meta_box( 'woocommerce_endpoints_nav_link', __( 'WooCommerce endpoints', 'woocommerce' ), array( $this, 'nav_menu_links' ), 'nav-menus', 'side', 'low' ); - } - - /** - * Output menu links. - */ - public function nav_menu_links() { - // Get items from account menu. - $endpoints = wc_get_account_menu_items(); - - // Remove dashboard item. - if ( isset( $endpoints['dashboard'] ) ) { - unset( $endpoints['dashboard'] ); - } - - // Include missing lost password. - $endpoints['lost-password'] = __( 'Lost password', 'woocommerce' ); - - $endpoints = apply_filters( 'woocommerce_custom_nav_menu_items', $endpoints ); - - ?> -
    -
    - -
    -

    - - - - - - - -

    -
    - add_node( - array( - 'parent' => 'site-name', - 'id' => 'view-store', - 'title' => __( 'Visit Store', 'woocommerce' ), - 'href' => wc_get_page_permalink( 'shop' ), - ) - ); - } -} - -return new WC_Admin_Menus(); diff --git a/includes/admin/class-wc-admin-meta-boxes.php b/includes/admin/class-wc-admin-meta-boxes.php deleted file mode 100644 index df1db141968..00000000000 --- a/includes/admin/class-wc-admin-meta-boxes.php +++ /dev/null @@ -1,227 +0,0 @@ -'; - - foreach ( $errors as $error ) { - echo '

    ' . wp_kses_post( $error ) . '

    '; - } - - echo ''; - - // Clear. - delete_option( 'woocommerce_meta_box_errors' ); - } - } - - /** - * Add WC Meta boxes. - */ - public function add_meta_boxes() { - $screen = get_current_screen(); - $screen_id = $screen ? $screen->id : ''; - - // Products. - add_meta_box( 'postexcerpt', __( 'Product short description', 'woocommerce' ), 'WC_Meta_Box_Product_Short_Description::output', 'product', 'normal' ); - add_meta_box( 'woocommerce-product-data', __( 'Product data', 'woocommerce' ), 'WC_Meta_Box_Product_Data::output', 'product', 'normal', 'high' ); - add_meta_box( 'woocommerce-product-images', __( 'Product gallery', 'woocommerce' ), 'WC_Meta_Box_Product_Images::output', 'product', 'side', 'low' ); - - // Orders. - foreach ( wc_get_order_types( 'order-meta-boxes' ) as $type ) { - $order_type_object = get_post_type_object( $type ); - /* Translators: %s order type name. */ - add_meta_box( 'woocommerce-order-data', sprintf( __( '%s data', 'woocommerce' ), $order_type_object->labels->singular_name ), 'WC_Meta_Box_Order_Data::output', $type, 'normal', 'high' ); - add_meta_box( 'woocommerce-order-items', __( 'Items', 'woocommerce' ), 'WC_Meta_Box_Order_Items::output', $type, 'normal', 'high' ); - /* Translators: %s order type name. */ - add_meta_box( 'woocommerce-order-notes', sprintf( __( '%s notes', 'woocommerce' ), $order_type_object->labels->singular_name ), 'WC_Meta_Box_Order_Notes::output', $type, 'side', 'default' ); - add_meta_box( 'woocommerce-order-downloads', __( 'Downloadable product permissions', 'woocommerce' ) . wc_help_tip( __( 'Note: Permissions for order items will automatically be granted when the order status changes to processing/completed.', 'woocommerce' ) ), 'WC_Meta_Box_Order_Downloads::output', $type, 'normal', 'default' ); - /* Translators: %s order type name. */ - add_meta_box( 'woocommerce-order-actions', sprintf( __( '%s actions', 'woocommerce' ), $order_type_object->labels->singular_name ), 'WC_Meta_Box_Order_Actions::output', $type, 'side', 'high' ); - } - - // Coupons. - add_meta_box( 'woocommerce-coupon-data', __( 'Coupon data', 'woocommerce' ), 'WC_Meta_Box_Coupon_Data::output', 'shop_coupon', 'normal', 'high' ); - - // Comment rating. - if ( 'comment' === $screen_id && isset( $_GET['c'] ) && metadata_exists( 'comment', wc_clean( wp_unslash( $_GET['c'] ) ), 'rating' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - add_meta_box( 'woocommerce-rating', __( 'Rating', 'woocommerce' ), 'WC_Meta_Box_Product_Reviews::output', 'comment', 'normal', 'high' ); - } - } - - /** - * Remove bloat. - */ - public function remove_meta_boxes() { - remove_meta_box( 'postexcerpt', 'product', 'normal' ); - remove_meta_box( 'product_shipping_classdiv', 'product', 'side' ); - remove_meta_box( 'commentsdiv', 'product', 'normal' ); - remove_meta_box( 'commentstatusdiv', 'product', 'side' ); - remove_meta_box( 'commentstatusdiv', 'product', 'normal' ); - remove_meta_box( 'woothemes-settings', 'shop_coupon', 'normal' ); - remove_meta_box( 'commentstatusdiv', 'shop_coupon', 'normal' ); - remove_meta_box( 'slugdiv', 'shop_coupon', 'normal' ); - - foreach ( wc_get_order_types( 'order-meta-boxes' ) as $type ) { - remove_meta_box( 'commentsdiv', $type, 'normal' ); - remove_meta_box( 'woothemes-settings', $type, 'normal' ); - remove_meta_box( 'commentstatusdiv', $type, 'normal' ); - remove_meta_box( 'slugdiv', $type, 'normal' ); - remove_meta_box( 'submitdiv', $type, 'side' ); - } - } - - /** - * Rename core meta boxes. - */ - public function rename_meta_boxes() { - global $post; - - // Comments/Reviews. - if ( isset( $post ) && ( 'publish' === $post->post_status || 'private' === $post->post_status ) && post_type_supports( 'product', 'comments' ) ) { - remove_meta_box( 'commentsdiv', 'product', 'normal' ); - add_meta_box( 'commentsdiv', __( 'Reviews', 'woocommerce' ), 'post_comment_meta_box', 'product', 'normal' ); - } - } - - /** - * Check if we're saving, the trigger an action based on the post type. - * - * @param int $post_id Post ID. - * @param object $post Post object. - */ - public function save_meta_boxes( $post_id, $post ) { - $post_id = absint( $post_id ); - - // $post_id and $post are required - if ( empty( $post_id ) || empty( $post ) || self::$saved_meta_boxes ) { - return; - } - - // Dont' save meta boxes for revisions or autosaves. - if ( Constants::is_true( 'DOING_AUTOSAVE' ) || is_int( wp_is_post_revision( $post ) ) || is_int( wp_is_post_autosave( $post ) ) ) { - return; - } - - // Check the nonce. - if ( empty( $_POST['woocommerce_meta_nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['woocommerce_meta_nonce'] ), 'woocommerce_save_data' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - return; - } - - // Check the post being saved == the $post_id to prevent triggering this call for other save_post events. - if ( empty( $_POST['post_ID'] ) || absint( $_POST['post_ID'] ) !== $post_id ) { - return; - } - - // Check user has permission to edit. - if ( ! current_user_can( 'edit_post', $post_id ) ) { - return; - } - - // We need this save event to run once to avoid potential endless loops. This would have been perfect: - // remove_action( current_filter(), __METHOD__ ); - // But cannot be used due to https://github.com/woocommerce/woocommerce/issues/6485 - // When that is patched in core we can use the above. - self::$saved_meta_boxes = true; - - // Check the post type. - if ( in_array( $post->post_type, wc_get_order_types( 'order-meta-boxes' ), true ) ) { - do_action( 'woocommerce_process_shop_order_meta', $post_id, $post ); - } elseif ( in_array( $post->post_type, array( 'product', 'shop_coupon' ), true ) ) { - do_action( 'woocommerce_process_' . $post->post_type . '_meta', $post_id, $post ); - } - } -} - -new WC_Admin_Meta_Boxes(); diff --git a/includes/admin/class-wc-admin-notices.php b/includes/admin/class-wc-admin-notices.php deleted file mode 100644 index fa9716cc498..00000000000 --- a/includes/admin/class-wc-admin-notices.php +++ /dev/null @@ -1,650 +0,0 @@ - callback. - * - * @var array - */ - private static $core_notices = array( - 'update' => 'update_notice', - 'template_files' => 'template_file_check_notice', - 'legacy_shipping' => 'legacy_shipping_notice', - 'no_shipping_methods' => 'no_shipping_methods_notice', - 'regenerating_thumbnails' => 'regenerating_thumbnails_notice', - 'regenerating_lookup_table' => 'regenerating_lookup_table_notice', - 'no_secure_connection' => 'secure_connection_notice', - WC_PHP_MIN_REQUIREMENTS_NOTICE => 'wp_php_min_requirements_notice', - 'maxmind_license_key' => 'maxmind_missing_license_key_notice', - 'redirect_download_method' => 'redirect_download_method_notice', - 'uploads_directory_is_unprotected' => 'uploads_directory_is_unprotected_notice', - 'base_tables_missing' => 'base_tables_missing_notice', - ); - - /** - * Constructor. - */ - public static function init() { - self::$notices = get_option( 'woocommerce_admin_notices', array() ); - - add_action( 'switch_theme', array( __CLASS__, 'reset_admin_notices' ) ); - add_action( 'woocommerce_installed', array( __CLASS__, 'reset_admin_notices' ) ); - add_action( 'wp_loaded', array( __CLASS__, 'add_redirect_download_method_notice' ) ); - add_action( 'wp_loaded', array( __CLASS__, 'hide_notices' ) ); - // @TODO: This prevents Action Scheduler async jobs from storing empty list of notices during WC installation. - // That could lead to OBW not starting and 'Run setup wizard' notice not appearing in WP admin, which we want - // to avoid. - if ( ! WC_Install::is_new_install() || ! wc_is_running_from_async_action_scheduler() ) { - add_action( 'shutdown', array( __CLASS__, 'store_notices' ) ); - } - - if ( current_user_can( 'manage_woocommerce' ) ) { - add_action( 'admin_print_styles', array( __CLASS__, 'add_notices' ) ); - } - } - - /** - * Parses query to create nonces when available. - * - * @param object $response The WP_REST_Response we're working with. - * @return object $response The prepared WP_REST_Response object. - */ - public static function prepare_note_with_nonce( $response ) { - if ( 'wc-update-db-reminder' !== $response->data['name'] || ! isset( $response->data['actions'] ) ) { - return $response; - } - - foreach ( $response->data['actions'] as $action_key => $action ) { - $url_parts = ! empty( $action->query ) ? wp_parse_url( $action->query ) : ''; - - if ( ! isset( $url_parts['query'] ) ) { - continue; - } - - wp_parse_str( $url_parts['query'], $params ); - - if ( array_key_exists( '_nonce_action', $params ) && array_key_exists( '_nonce_name', $params ) ) { - $org_params = $params; - - // Check to make sure we're acting on the whitelisted nonce actions. - if ( 'wc_db_update' !== $params['_nonce_action'] && 'woocommerce_hide_notices_nonce' !== $params['_nonce_action'] ) { - continue; - } - - unset( $org_params['_nonce_action'] ); - unset( $org_params['_nonce_name'] ); - - $url = $url_parts['scheme'] . '://' . $url_parts['host'] . $url_parts['path']; - - $nonce = array( $params['_nonce_name'] => wp_create_nonce( $params['_nonce_action'] ) ); - $merged_params = array_merge( $nonce, $org_params ); - $parsed_query = add_query_arg( $merged_params, $url ); - - $response->data['actions'][ $action_key ]->query = $parsed_query; - $response->data['actions'][ $action_key ]->url = $parsed_query; - } - } - - return $response; - } - - /** - * Store notices to DB - */ - public static function store_notices() { - update_option( 'woocommerce_admin_notices', self::get_notices() ); - } - - /** - * Get notices - * - * @return array - */ - public static function get_notices() { - return self::$notices; - } - - /** - * Remove all notices. - */ - public static function remove_all_notices() { - self::$notices = array(); - } - - /** - * Reset notices for themes when switched or a new version of WC is installed. - */ - public static function reset_admin_notices() { - if ( ! self::is_ssl() ) { - self::add_notice( 'no_secure_connection' ); - } - if ( ! self::is_uploads_directory_protected() ) { - self::add_notice( 'uploads_directory_is_unprotected' ); - } - self::add_notice( 'template_files' ); - self::add_min_version_notice(); - self::add_maxmind_missing_license_key_notice(); - } - - /** - * Show a notice. - * - * @param string $name Notice name. - * @param bool $force_save Force saving inside this method instead of at the 'shutdown'. - */ - public static function add_notice( $name, $force_save = false ) { - self::$notices = array_unique( array_merge( self::get_notices(), array( $name ) ) ); - - if ( $force_save ) { - // Adding early save to prevent more race conditions with notices. - self::store_notices(); - } - } - - /** - * Remove a notice from being displayed. - * - * @param string $name Notice name. - * @param bool $force_save Force saving inside this method instead of at the 'shutdown'. - */ - public static function remove_notice( $name, $force_save = false ) { - self::$notices = array_diff( self::get_notices(), array( $name ) ); - delete_option( 'woocommerce_admin_notice_' . $name ); - - if ( $force_save ) { - // Adding early save to prevent more race conditions with notices. - self::store_notices(); - } - } - - /** - * See if a notice is being shown. - * - * @param string $name Notice name. - * - * @return boolean - */ - public static function has_notice( $name ) { - return in_array( $name, self::get_notices(), true ); - } - - /** - * Hide a notice if the GET variable is set. - */ - public static function hide_notices() { - if ( isset( $_GET['wc-hide-notice'] ) && isset( $_GET['_wc_notice_nonce'] ) ) { // WPCS: input var ok, CSRF ok. - if ( ! wp_verify_nonce( sanitize_key( wp_unslash( $_GET['_wc_notice_nonce'] ) ), 'woocommerce_hide_notices_nonce' ) ) { // WPCS: input var ok, CSRF ok. - wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); - } - - if ( ! current_user_can( 'manage_woocommerce' ) ) { - wp_die( esc_html__( 'You don’t have permission to do this.', 'woocommerce' ) ); - } - - $hide_notice = sanitize_text_field( wp_unslash( $_GET['wc-hide-notice'] ) ); // WPCS: input var ok, CSRF ok. - - self::remove_notice( $hide_notice ); - - update_user_meta( get_current_user_id(), 'dismissed_' . $hide_notice . '_notice', true ); - - do_action( 'woocommerce_hide_' . $hide_notice . '_notice' ); - } - } - - /** - * Add notices + styles if needed. - */ - public static function add_notices() { - $notices = self::get_notices(); - - if ( empty( $notices ) ) { - return; - } - - $screen = get_current_screen(); - $screen_id = $screen ? $screen->id : ''; - $show_on_screens = array( - 'dashboard', - 'plugins', - ); - - // Notices should only show on WooCommerce screens, the main dashboard, and on the plugins screen. - if ( ! in_array( $screen_id, wc_get_screen_ids(), true ) && ! in_array( $screen_id, $show_on_screens, true ) ) { - return; - } - - wp_enqueue_style( 'woocommerce-activation', plugins_url( '/assets/css/activation.css', WC_PLUGIN_FILE ), array(), Constants::get_constant( 'WC_VERSION' ) ); - - // Add RTL support. - wp_style_add_data( 'woocommerce-activation', 'rtl', 'replace' ); - - foreach ( $notices as $notice ) { - if ( ! empty( self::$core_notices[ $notice ] ) && apply_filters( 'woocommerce_show_admin_notice', true, $notice ) ) { - add_action( 'admin_notices', array( __CLASS__, self::$core_notices[ $notice ] ) ); - } else { - add_action( 'admin_notices', array( __CLASS__, 'output_custom_notices' ) ); - } - } - } - - /** - * Add a custom notice. - * - * @param string $name Notice name. - * @param string $notice_html Notice HTML. - */ - public static function add_custom_notice( $name, $notice_html ) { - self::add_notice( $name ); - update_option( 'woocommerce_admin_notice_' . $name, wp_kses_post( $notice_html ) ); - } - - /** - * Output any stored custom notices. - */ - public static function output_custom_notices() { - $notices = self::get_notices(); - - if ( ! empty( $notices ) ) { - foreach ( $notices as $notice ) { - if ( empty( self::$core_notices[ $notice ] ) ) { - $notice_html = get_option( 'woocommerce_admin_notice_' . $notice ); - - if ( $notice_html ) { - include dirname( __FILE__ ) . '/views/html-notice-custom.php'; - } - } - } - } - } - - /** - * If we need to update the database, include a message with the DB update button. - */ - public static function update_notice() { - $screen = get_current_screen(); - $screen_id = $screen ? $screen->id : ''; - if ( WC()->is_wc_admin_active() && in_array( $screen_id, wc_get_screen_ids(), true ) ) { - return; - } - - if ( WC_Install::needs_db_update() ) { - $next_scheduled_date = WC()->queue()->get_next( 'woocommerce_run_update_callback', null, 'woocommerce-db-updates' ); - - if ( $next_scheduled_date || ! empty( $_GET['do_update_woocommerce'] ) ) { // WPCS: input var ok, CSRF ok. - include dirname( __FILE__ ) . '/views/html-notice-updating.php'; - } else { - include dirname( __FILE__ ) . '/views/html-notice-update.php'; - } - } else { - include dirname( __FILE__ ) . '/views/html-notice-updated.php'; - } - } - - /** - * If we have just installed, show a message with the install pages button. - * - * @deprecated 4.6.0 - */ - public static function install_notice() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); - } - - /** - * Show a notice highlighting bad template files. - */ - public static function template_file_check_notice() { - $core_templates = WC_Admin_Status::scan_template_files( WC()->plugin_path() . '/templates' ); - $outdated = false; - - foreach ( $core_templates as $file ) { - - $theme_file = false; - if ( file_exists( get_stylesheet_directory() . '/' . $file ) ) { - $theme_file = get_stylesheet_directory() . '/' . $file; - } elseif ( file_exists( get_stylesheet_directory() . '/' . WC()->template_path() . $file ) ) { - $theme_file = get_stylesheet_directory() . '/' . WC()->template_path() . $file; - } elseif ( file_exists( get_template_directory() . '/' . $file ) ) { - $theme_file = get_template_directory() . '/' . $file; - } elseif ( file_exists( get_template_directory() . '/' . WC()->template_path() . $file ) ) { - $theme_file = get_template_directory() . '/' . WC()->template_path() . $file; - } - - if ( false !== $theme_file ) { - $core_version = WC_Admin_Status::get_file_version( WC()->plugin_path() . '/templates/' . $file ); - $theme_version = WC_Admin_Status::get_file_version( $theme_file ); - - if ( $core_version && $theme_version && version_compare( $theme_version, $core_version, '<' ) ) { - $outdated = true; - break; - } - } - } - - if ( $outdated ) { - include dirname( __FILE__ ) . '/views/html-notice-template-check.php'; - } else { - self::remove_notice( 'template_files' ); - } - } - - /** - * Show a notice asking users to convert to shipping zones. - * - * @todo remove in 4.0.0 - */ - public static function legacy_shipping_notice() { - $maybe_load_legacy_methods = array( 'flat_rate', 'free_shipping', 'international_delivery', 'local_delivery', 'local_pickup' ); - $enabled = false; - - foreach ( $maybe_load_legacy_methods as $method ) { - $options = get_option( 'woocommerce_' . $method . '_settings' ); - if ( $options && isset( $options['enabled'] ) && 'yes' === $options['enabled'] ) { - $enabled = true; - } - } - - if ( $enabled ) { - include dirname( __FILE__ ) . '/views/html-notice-legacy-shipping.php'; - } else { - self::remove_notice( 'template_files' ); - } - } - - /** - * No shipping methods. - */ - public static function no_shipping_methods_notice() { - if ( wc_shipping_enabled() && ( empty( $_GET['page'] ) || empty( $_GET['tab'] ) || 'wc-settings' !== $_GET['page'] || 'shipping' !== $_GET['tab'] ) ) { // WPCS: input var ok, CSRF ok. - $product_count = wp_count_posts( 'product' ); - $method_count = wc_get_shipping_method_count(); - - if ( $product_count->publish > 0 && 0 === $method_count ) { - include dirname( __FILE__ ) . '/views/html-notice-no-shipping-methods.php'; - } - - if ( $method_count > 0 ) { - self::remove_notice( 'no_shipping_methods' ); - } - } - } - - /** - * Notice shown when regenerating thumbnails background process is running. - */ - public static function regenerating_thumbnails_notice() { - include dirname( __FILE__ ) . '/views/html-notice-regenerating-thumbnails.php'; - } - - /** - * Notice about secure connection. - */ - public static function secure_connection_notice() { - if ( self::is_ssl() || get_user_meta( get_current_user_id(), 'dismissed_no_secure_connection_notice', true ) ) { - return; - } - - include dirname( __FILE__ ) . '/views/html-notice-secure-connection.php'; - } - - /** - * Notice shown when regenerating thumbnails background process is running. - * - * @since 3.6.0 - */ - public static function regenerating_lookup_table_notice() { - // See if this is still relevent. - if ( ! wc_update_product_lookup_tables_is_running() ) { - self::remove_notice( 'regenerating_lookup_table' ); - return; - } - - include dirname( __FILE__ ) . '/views/html-notice-regenerating-lookup-table.php'; - } - - /** - * Add notice about minimum PHP and WordPress requirement. - * - * @since 3.6.5 - */ - public static function add_min_version_notice() { - if ( version_compare( phpversion(), WC_NOTICE_MIN_PHP_VERSION, '<' ) || version_compare( get_bloginfo( 'version' ), WC_NOTICE_MIN_WP_VERSION, '<' ) ) { - self::add_notice( WC_PHP_MIN_REQUIREMENTS_NOTICE ); - } - } - - /** - * Notice about WordPress and PHP minimum requirements. - * - * @since 3.6.5 - * @return void - */ - public static function wp_php_min_requirements_notice() { - if ( apply_filters( 'woocommerce_hide_php_wp_nag', get_user_meta( get_current_user_id(), 'dismissed_' . WC_PHP_MIN_REQUIREMENTS_NOTICE . '_notice', true ) ) ) { - self::remove_notice( WC_PHP_MIN_REQUIREMENTS_NOTICE ); - return; - } - - $old_php = version_compare( phpversion(), WC_NOTICE_MIN_PHP_VERSION, '<' ); - $old_wp = version_compare( get_bloginfo( 'version' ), WC_NOTICE_MIN_WP_VERSION, '<' ); - - // Both PHP and WordPress up to date version => no notice. - if ( ! $old_php && ! $old_wp ) { - return; - } - - if ( $old_php && $old_wp ) { - $msg = sprintf( - /* translators: 1: Minimum PHP version 2: Minimum WordPress version */ - __( 'Update required: WooCommerce will soon require PHP version %1$s and WordPress version %2$s or newer.', 'woocommerce' ), - WC_NOTICE_MIN_PHP_VERSION, - WC_NOTICE_MIN_WP_VERSION - ); - } elseif ( $old_php ) { - $msg = sprintf( - /* translators: %s: Minimum PHP version */ - __( 'Update required: WooCommerce will soon require PHP version %s or newer.', 'woocommerce' ), - WC_NOTICE_MIN_PHP_VERSION - ); - } elseif ( $old_wp ) { - $msg = sprintf( - /* translators: %s: Minimum WordPress version */ - __( 'Update required: WooCommerce will soon require WordPress version %s or newer.', 'woocommerce' ), - WC_NOTICE_MIN_WP_VERSION - ); - } - - include dirname( __FILE__ ) . '/views/html-notice-wp-php-minimum-requirements.php'; - } - - /** - * Add MaxMind missing license key notice. - * - * @since 3.9.0 - */ - public static function add_maxmind_missing_license_key_notice() { - $default_address = get_option( 'woocommerce_default_customer_address' ); - - if ( ! in_array( $default_address, array( 'geolocation', 'geolocation_ajax' ), true ) ) { - return; - } - - $integration_options = get_option( 'woocommerce_maxmind_geolocation_settings' ); - if ( empty( $integration_options['license_key'] ) ) { - self::add_notice( 'maxmind_license_key' ); - - } - } - - /** - * Add notice about Redirect-only download method, nudging user to switch to a different method instead. - */ - public static function add_redirect_download_method_notice() { - if ( 'redirect' === get_option( 'woocommerce_file_download_method' ) ) { - self::add_notice( 'redirect_download_method' ); - } else { - self::remove_notice( 'redirect_download_method' ); - } - } - - /** - * Display MaxMind missing license key notice. - * - * @since 3.9.0 - */ - public static function maxmind_missing_license_key_notice() { - $user_dismissed_notice = get_user_meta( get_current_user_id(), 'dismissed_maxmind_license_key_notice', true ); - $filter_dismissed_notice = ! apply_filters( 'woocommerce_maxmind_geolocation_display_notices', true ); - - if ( $user_dismissed_notice || $filter_dismissed_notice ) { - self::remove_notice( 'maxmind_license_key' ); - return; - } - - include dirname( __FILE__ ) . '/views/html-notice-maxmind-license-key.php'; - } - - /** - * Notice about Redirect-Only download method. - * - * @since 4.0 - */ - public static function redirect_download_method_notice() { - if ( apply_filters( 'woocommerce_hide_redirect_method_nag', get_user_meta( get_current_user_id(), 'dismissed_redirect_download_method_notice', true ) ) ) { - self::remove_notice( 'redirect_download_method' ); - return; - } - - include dirname( __FILE__ ) . '/views/html-notice-redirect-only-download.php'; - } - - /** - * Notice about uploads directory begin unprotected. - * - * @since 4.2.0 - */ - public static function uploads_directory_is_unprotected_notice() { - if ( get_user_meta( get_current_user_id(), 'dismissed_uploads_directory_is_unprotected_notice', true ) || self::is_uploads_directory_protected() ) { - self::remove_notice( 'uploads_directory_is_unprotected' ); - return; - } - - include dirname( __FILE__ ) . '/views/html-notice-uploads-directory-is-unprotected.php'; - } - - /** - * Notice about base tables missing. - */ - public static function base_tables_missing_notice() { - $notice_dismissed = apply_filters( - 'woocommerce_hide_base_tables_missing_nag', - get_user_meta( get_current_user_id(), 'dismissed_base_tables_missing_notice', true ) - ); - if ( $notice_dismissed ) { - self::remove_notice( 'base_tables_missing' ); - } - - include dirname( __FILE__ ) . '/views/html-notice-base-table-missing.php'; - } - - /** - * Determine if the store is running SSL. - * - * @return bool Flag SSL enabled. - * @since 3.5.1 - */ - protected static function is_ssl() { - $shop_page = wc_get_page_permalink( 'shop' ); - - return ( is_ssl() && 'https' === substr( $shop_page, 0, 5 ) ); - } - - /** - * Wrapper for is_plugin_active. - * - * @param string $plugin Plugin to check. - * @return boolean - */ - protected static function is_plugin_active( $plugin ) { - if ( ! function_exists( 'is_plugin_active' ) ) { - include_once ABSPATH . 'wp-admin/includes/plugin.php'; - } - return is_plugin_active( $plugin ); - } - - /** - * Simplify Commerce is no longer in core. - * - * @deprecated 3.6.0 No longer shown. - */ - public static function simplify_commerce_notice() { - wc_deprecated_function( 'WC_Admin_Notices::simplify_commerce_notice', '3.6.0' ); - } - - /** - * Show the Theme Check notice. - * - * @deprecated 3.3.0 No longer shown. - */ - public static function theme_check_notice() { - wc_deprecated_function( 'WC_Admin_Notices::theme_check_notice', '3.3.0' ); - } - - /** - * Check if uploads directory is protected. - * - * @since 4.2.0 - * @return bool - */ - protected static function is_uploads_directory_protected() { - $cache_key = '_woocommerce_upload_directory_status'; - $status = get_transient( $cache_key ); - - // Check for cache. - if ( false !== $status ) { - return 'protected' === $status; - } - - // Get only data from the uploads directory. - $uploads = wp_get_upload_dir(); - - // Check for the "uploads/woocommerce_uploads" directory. - $response = wp_safe_remote_get( - esc_url_raw( $uploads['baseurl'] . '/woocommerce_uploads/' ), - array( - 'redirection' => 0, - ) - ); - $response_code = intval( wp_remote_retrieve_response_code( $response ) ); - $response_content = wp_remote_retrieve_body( $response ); - - // Check if returns 200 with empty content in case can open an index.html file, - // and check for non-200 codes in case the directory is protected. - $is_protected = ( 200 === $response_code && empty( $response_content ) ) || ( 200 !== $response_code ); - set_transient( $cache_key, $is_protected ? 'protected' : 'unprotected', 1 * DAY_IN_SECONDS ); - - return $is_protected; - } -} - -WC_Admin_Notices::init(); diff --git a/includes/admin/class-wc-admin-permalink-settings.php b/includes/admin/class-wc-admin-permalink-settings.php deleted file mode 100644 index abcd784d721..00000000000 --- a/includes/admin/class-wc-admin-permalink-settings.php +++ /dev/null @@ -1,217 +0,0 @@ -settings_init(); - $this->settings_save(); - } - - /** - * Init our settings. - */ - public function settings_init() { - add_settings_section( 'woocommerce-permalink', __( 'Product permalinks', 'woocommerce' ), array( $this, 'settings' ), 'permalink' ); - - add_settings_field( - 'woocommerce_product_category_slug', - __( 'Product category base', 'woocommerce' ), - array( $this, 'product_category_slug_input' ), - 'permalink', - 'optional' - ); - add_settings_field( - 'woocommerce_product_tag_slug', - __( 'Product tag base', 'woocommerce' ), - array( $this, 'product_tag_slug_input' ), - 'permalink', - 'optional' - ); - add_settings_field( - 'woocommerce_product_attribute_slug', - __( 'Product attribute base', 'woocommerce' ), - array( $this, 'product_attribute_slug_input' ), - 'permalink', - 'optional' - ); - - $this->permalinks = wc_get_permalink_structure(); - } - - /** - * Show a slug input box. - */ - public function product_category_slug_input() { - ?> - - - - - /attribute-name/attribute/ - shop would make your product links like %sshop/sample-product/. This setting affects product URLs only, not things such as product categories.', 'woocommerce' ), esc_url( home_url( '/' ) ) ) ) ); - - $shop_page_id = wc_get_page_id( 'shop' ); - $base_slug = urldecode( ( $shop_page_id > 0 && get_post( $shop_page_id ) ) ? get_page_uri( $shop_page_id ) : _x( 'shop', 'default-slug', 'woocommerce' ) ); - $product_base = _x( 'product', 'default-slug', 'woocommerce' ); - - $structures = array( - 0 => '', - 1 => '/' . trailingslashit( $base_slug ), - 2 => '/' . trailingslashit( $base_slug ) . trailingslashit( '%product_cat%' ), - ); - ?> - - - - - - - - - - - - - - - - - - - - - - - - - 0 && get_post( $shop_page_id ) ) ? get_page_uri( $shop_page_id ) : _x( 'shop', 'default-slug', 'woocommerce' ); - - if ( $shop_page_id && stristr( trim( $permalinks['product_base'], '/' ), $shop_permalink ) ) { - $permalinks['use_verbose_page_rules'] = true; - } - - update_option( 'woocommerce_permalinks', $permalinks ); - wc_restore_locale(); - } - } -} - -return new WC_Admin_Permalink_Settings(); diff --git a/includes/admin/class-wc-admin-pointers.php b/includes/admin/class-wc-admin-pointers.php deleted file mode 100644 index 1ca73026713..00000000000 --- a/includes/admin/class-wc-admin-pointers.php +++ /dev/null @@ -1,287 +0,0 @@ -id ) { - case 'product': - $this->create_product_tutorial(); - break; - } - } - - /** - * Pointers for creating a product. - */ - public function create_product_tutorial() { - if ( ! isset( $_GET['tutorial'] ) || ! current_user_can( 'manage_options' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - return; - } - - // These pointers will chain - they will not be shown at once. - $pointers = array( - 'pointers' => array( - 'title' => array( - 'target' => '#title', - 'next' => 'content', - 'next_trigger' => array( - 'target' => '#title', - 'event' => 'input', - ), - 'options' => array( - 'content' => '

    ' . esc_html__( 'Product name', 'woocommerce' ) . '

    ' . - '

    ' . esc_html__( 'Give your new product a name here. This is a required field and will be what your customers will see in your store.', 'woocommerce' ) . '

    ', - 'position' => array( - 'edge' => 'top', - 'align' => 'left', - ), - ), - ), - 'content' => array( - 'target' => '#wp-content-editor-container', - 'next' => 'product-type', - 'next_trigger' => array(), - 'options' => array( - 'content' => '

    ' . esc_html__( 'Product description', 'woocommerce' ) . '

    ' . - '

    ' . esc_html__( 'This is your products main body of content. Here you should describe your product in detail.', 'woocommerce' ) . '

    ', - 'position' => array( - 'edge' => 'bottom', - 'align' => 'middle', - ), - ), - ), - 'product-type' => array( - 'target' => '#product-type', - 'next' => 'virtual', - 'next_trigger' => array( - 'target' => '#product-type', - 'event' => 'change blur click', - ), - 'options' => array( - 'content' => '

    ' . esc_html__( 'Choose product type', 'woocommerce' ) . '

    ' . - '

    ' . esc_html__( 'Choose a type for this product. Simple is suitable for most physical goods and services (we recommend setting up a simple product for now).', 'woocommerce' ) . '

    ' . - '

    ' . esc_html__( 'Variable is for more complex products such as t-shirts with multiple sizes.', 'woocommerce' ) . '

    ' . - '

    ' . esc_html__( 'Grouped products are for grouping several simple products into one.', 'woocommerce' ) . '

    ' . - '

    ' . esc_html__( 'Finally, external products are for linking off-site.', 'woocommerce' ) . '

    ', - 'position' => array( - 'edge' => 'bottom', - 'align' => 'middle', - ), - ), - ), - 'virtual' => array( - 'target' => '#_virtual', - 'next' => 'downloadable', - 'next_trigger' => array( - 'target' => '#_virtual', - 'event' => 'change', - ), - 'options' => array( - 'content' => '

    ' . esc_html__( 'Virtual products', 'woocommerce' ) . '

    ' . - '

    ' . esc_html__( 'Check the "Virtual" box if this is a non-physical item, for example a service, which does not need shipping.', 'woocommerce' ) . '

    ', - 'position' => array( - 'edge' => 'bottom', - 'align' => 'middle', - ), - ), - ), - 'downloadable' => array( - 'target' => '#_downloadable', - 'next' => 'regular_price', - 'next_trigger' => array( - 'target' => '#_downloadable', - 'event' => 'change', - ), - 'options' => array( - 'content' => '

    ' . esc_html__( 'Downloadable products', 'woocommerce' ) . '

    ' . - '

    ' . esc_html__( 'If purchasing this product gives a customer access to a downloadable file, e.g. software, check this box.', 'woocommerce' ) . '

    ', - 'position' => array( - 'edge' => 'bottom', - 'align' => 'middle', - ), - ), - ), - 'regular_price' => array( - 'target' => '#_regular_price', - 'next' => 'postexcerpt', - 'next_trigger' => array( - 'target' => '#_regular_price', - 'event' => 'input', - ), - 'options' => array( - 'content' => '

    ' . esc_html__( 'Prices', 'woocommerce' ) . '

    ' . - '

    ' . esc_html__( 'Next you need to give your product a price.', 'woocommerce' ) . '

    ', - 'position' => array( - 'edge' => 'bottom', - 'align' => 'middle', - ), - ), - ), - 'postexcerpt' => array( - 'target' => '#postexcerpt', - 'next' => 'postimagediv', - 'next_trigger' => array( - 'target' => '#postexcerpt', - 'event' => 'input', - ), - 'options' => array( - 'content' => '

    ' . esc_html__( 'Product short description', 'woocommerce' ) . '

    ' . - '

    ' . esc_html__( 'Add a quick summary for your product here. This will appear on the product page under the product name.', 'woocommerce' ) . '

    ', - 'position' => array( - 'edge' => 'bottom', - 'align' => 'middle', - ), - ), - ), - 'postimagediv' => array( - 'target' => '#postimagediv', - 'next' => 'product_tag', - 'options' => array( - 'content' => '

    ' . esc_html__( 'Product images', 'woocommerce' ) . '

    ' . - '

    ' . esc_html__( "Upload or assign an image to your product here. This image will be shown in your store's catalog.", 'woocommerce' ) . '

    ', - 'position' => array( - 'edge' => 'right', - 'align' => 'middle', - ), - ), - ), - 'product_tag' => array( - 'target' => '#tagsdiv-product_tag', - 'next' => 'product_catdiv', - 'options' => array( - 'content' => '

    ' . esc_html__( 'Product tags', 'woocommerce' ) . '

    ' . - '

    ' . esc_html__( 'You can optionally "tag" your products here. Tags are a method of labeling your products to make them easier for customers to find.', 'woocommerce' ) . '

    ', - 'position' => array( - 'edge' => 'right', - 'align' => 'middle', - ), - ), - ), - 'product_catdiv' => array( - 'target' => '#product_catdiv', - 'next' => 'submitdiv', - 'options' => array( - 'content' => '

    ' . esc_html__( 'Product categories', 'woocommerce' ) . '

    ' . - '

    ' . esc_html__( 'Optionally assign categories to your products to make them easier to browse through and find in your store.', 'woocommerce' ) . '

    ', - 'position' => array( - 'edge' => 'right', - 'align' => 'middle', - ), - ), - ), - 'submitdiv' => array( - 'target' => '#submitdiv', - 'next' => '', - 'options' => array( - 'content' => '

    ' . esc_html__( 'Publish your product!', 'woocommerce' ) . '

    ' . - '

    ' . esc_html__( 'When you are finished editing your product, hit the "Publish" button to publish your product to your store.', 'woocommerce' ) . '

    ', - 'position' => array( - 'edge' => 'right', - 'align' => 'middle', - ), - ), - ), - ), - ); - - $this->enqueue_pointers( $pointers ); - } - - /** - * Enqueue pointers and add script to page. - * - * @param array $pointers Pointers data. - */ - public function enqueue_pointers( $pointers ) { - $pointers = rawurlencode( wp_json_encode( $pointers ) ); - wp_enqueue_style( 'wp-pointer' ); - wp_enqueue_script( 'wp-pointer' ); - wc_enqueue_js( - "jQuery( function( $ ) { - var wc_pointers = JSON.parse( decodeURIComponent( '{$pointers}' ) ); - - setTimeout( init_wc_pointers, 800 ); - - function init_wc_pointers() { - $.each( wc_pointers.pointers, function( i ) { - show_wc_pointer( i ); - return false; - }); - } - - function show_wc_pointer( id ) { - var pointer = wc_pointers.pointers[ id ]; - var options = $.extend( pointer.options, { - pointerClass: 'wp-pointer wc-pointer', - close: function() { - if ( pointer.next ) { - show_wc_pointer( pointer.next ); - } - }, - buttons: function( event, t ) { - var close = '" . esc_js( __( 'Dismiss', 'woocommerce' ) ) . "', - next = '" . esc_js( __( 'Next', 'woocommerce' ) ) . "', - button = $( '' + close + '' ), - button2 = $( '' + next + '' ), - wrapper = $( '
    ' ); - - button.bind( 'click.pointer', function(e) { - e.preventDefault(); - t.element.pointer('destroy'); - }); - - button2.bind( 'click.pointer', function(e) { - e.preventDefault(); - t.element.pointer('close'); - }); - - wrapper.append( button ); - wrapper.append( button2 ); - - return wrapper; - }, - } ); - var this_pointer = $( pointer.target ).pointer( options ); - this_pointer.pointer( 'open' ); - - if ( pointer.next_trigger ) { - $( pointer.next_trigger.target ).on( pointer.next_trigger.event, function() { - setTimeout( function() { this_pointer.pointer( 'close' ); }, 400 ); - }); - } - } - });" - ); - } -} - -new WC_Admin_Pointers(); diff --git a/includes/admin/class-wc-admin-post-types.php b/includes/admin/class-wc-admin-post-types.php deleted file mode 100644 index e2d145206a1..00000000000 --- a/includes/admin/class-wc-admin-post-types.php +++ /dev/null @@ -1,992 +0,0 @@ -request_data(); - - $screen_id = false; - - if ( function_exists( 'get_current_screen' ) ) { - $screen = get_current_screen(); - $screen_id = isset( $screen, $screen->id ) ? $screen->id : ''; - } - - if ( ! empty( $request_data['screen'] ) ) { - $screen_id = wc_clean( wp_unslash( $request_data['screen'] ) ); - } - - switch ( $screen_id ) { - case 'edit-shop_order': - include_once __DIR__ . '/list-tables/class-wc-admin-list-table-orders.php'; - $wc_list_table = new WC_Admin_List_Table_Orders(); - break; - case 'edit-shop_coupon': - include_once __DIR__ . '/list-tables/class-wc-admin-list-table-coupons.php'; - $wc_list_table = new WC_Admin_List_Table_Coupons(); - break; - case 'edit-product': - include_once __DIR__ . '/list-tables/class-wc-admin-list-table-products.php'; - $wc_list_table = new WC_Admin_List_Table_Products(); - break; - } - - // Ensure the table handler is only loaded once. Prevents multiple loads if a plugin calls check_ajax_referer many times. - remove_action( 'current_screen', array( $this, 'setup_screen' ) ); - remove_action( 'check_ajax_referer', array( $this, 'setup_screen' ) ); - } - - /** - * Change messages when a post type is updated. - * - * @param array $messages Array of messages. - * @return array - */ - public function post_updated_messages( $messages ) { - global $post; - - $messages['product'] = array( - 0 => '', // Unused. Messages start at index 1. - /* translators: %s: Product view URL. */ - 1 => sprintf( __( 'Product updated. View Product', 'woocommerce' ), esc_url( get_permalink( $post->ID ) ) ), - 2 => __( 'Custom field updated.', 'woocommerce' ), - 3 => __( 'Custom field deleted.', 'woocommerce' ), - 4 => __( 'Product updated.', 'woocommerce' ), - 5 => __( 'Revision restored.', 'woocommerce' ), - /* translators: %s: product url */ - 6 => sprintf( __( 'Product published. View Product', 'woocommerce' ), esc_url( get_permalink( $post->ID ) ) ), - 7 => __( 'Product saved.', 'woocommerce' ), - /* translators: %s: product url */ - 8 => sprintf( __( 'Product submitted. Preview product', 'woocommerce' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ) ) ), - 9 => sprintf( - /* translators: 1: date 2: product url */ - __( 'Product scheduled for: %1$s. Preview product', 'woocommerce' ), - '' . date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $post->post_date ) ) . '', - esc_url( get_permalink( $post->ID ) ) - ), - /* translators: %s: product url */ - 10 => sprintf( __( 'Product draft updated. Preview product', 'woocommerce' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ) ) ), - ); - - $messages['shop_order'] = array( - 0 => '', // Unused. Messages start at index 1. - 1 => __( 'Order updated.', 'woocommerce' ), - 2 => __( 'Custom field updated.', 'woocommerce' ), - 3 => __( 'Custom field deleted.', 'woocommerce' ), - 4 => __( 'Order updated.', 'woocommerce' ), - 5 => __( 'Revision restored.', 'woocommerce' ), - 6 => __( 'Order updated.', 'woocommerce' ), - 7 => __( 'Order saved.', 'woocommerce' ), - 8 => __( 'Order submitted.', 'woocommerce' ), - 9 => sprintf( - /* translators: %s: date */ - __( 'Order scheduled for: %s.', 'woocommerce' ), - '' . date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $post->post_date ) ) . '' - ), - 10 => __( 'Order draft updated.', 'woocommerce' ), - 11 => __( 'Order updated and sent.', 'woocommerce' ), - ); - - $messages['shop_coupon'] = array( - 0 => '', // Unused. Messages start at index 1. - 1 => __( 'Coupon updated.', 'woocommerce' ), - 2 => __( 'Custom field updated.', 'woocommerce' ), - 3 => __( 'Custom field deleted.', 'woocommerce' ), - 4 => __( 'Coupon updated.', 'woocommerce' ), - 5 => __( 'Revision restored.', 'woocommerce' ), - 6 => __( 'Coupon updated.', 'woocommerce' ), - 7 => __( 'Coupon saved.', 'woocommerce' ), - 8 => __( 'Coupon submitted.', 'woocommerce' ), - 9 => sprintf( - /* translators: %s: date */ - __( 'Coupon scheduled for: %s.', 'woocommerce' ), - '' . date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $post->post_date ) ) . '' - ), - 10 => __( 'Coupon draft updated.', 'woocommerce' ), - ); - - return $messages; - } - - /** - * Specify custom bulk actions messages for different post types. - * - * @param array $bulk_messages Array of messages. - * @param array $bulk_counts Array of how many objects were updated. - * @return array - */ - public function bulk_post_updated_messages( $bulk_messages, $bulk_counts ) { - $bulk_messages['product'] = array( - /* translators: %s: product count */ - 'updated' => _n( '%s product updated.', '%s products updated.', $bulk_counts['updated'], 'woocommerce' ), - /* translators: %s: product count */ - 'locked' => _n( '%s product not updated, somebody is editing it.', '%s products not updated, somebody is editing them.', $bulk_counts['locked'], 'woocommerce' ), - /* translators: %s: product count */ - 'deleted' => _n( '%s product permanently deleted.', '%s products permanently deleted.', $bulk_counts['deleted'], 'woocommerce' ), - /* translators: %s: product count */ - 'trashed' => _n( '%s product moved to the Trash.', '%s products moved to the Trash.', $bulk_counts['trashed'], 'woocommerce' ), - /* translators: %s: product count */ - 'untrashed' => _n( '%s product restored from the Trash.', '%s products restored from the Trash.', $bulk_counts['untrashed'], 'woocommerce' ), - ); - - $bulk_messages['shop_order'] = array( - /* translators: %s: order count */ - 'updated' => _n( '%s order updated.', '%s orders updated.', $bulk_counts['updated'], 'woocommerce' ), - /* translators: %s: order count */ - 'locked' => _n( '%s order not updated, somebody is editing it.', '%s orders not updated, somebody is editing them.', $bulk_counts['locked'], 'woocommerce' ), - /* translators: %s: order count */ - 'deleted' => _n( '%s order permanently deleted.', '%s orders permanently deleted.', $bulk_counts['deleted'], 'woocommerce' ), - /* translators: %s: order count */ - 'trashed' => _n( '%s order moved to the Trash.', '%s orders moved to the Trash.', $bulk_counts['trashed'], 'woocommerce' ), - /* translators: %s: order count */ - 'untrashed' => _n( '%s order restored from the Trash.', '%s orders restored from the Trash.', $bulk_counts['untrashed'], 'woocommerce' ), - ); - - $bulk_messages['shop_coupon'] = array( - /* translators: %s: coupon count */ - 'updated' => _n( '%s coupon updated.', '%s coupons updated.', $bulk_counts['updated'], 'woocommerce' ), - /* translators: %s: coupon count */ - 'locked' => _n( '%s coupon not updated, somebody is editing it.', '%s coupons not updated, somebody is editing them.', $bulk_counts['locked'], 'woocommerce' ), - /* translators: %s: coupon count */ - 'deleted' => _n( '%s coupon permanently deleted.', '%s coupons permanently deleted.', $bulk_counts['deleted'], 'woocommerce' ), - /* translators: %s: coupon count */ - 'trashed' => _n( '%s coupon moved to the Trash.', '%s coupons moved to the Trash.', $bulk_counts['trashed'], 'woocommerce' ), - /* translators: %s: coupon count */ - 'untrashed' => _n( '%s coupon restored from the Trash.', '%s coupons restored from the Trash.', $bulk_counts['untrashed'], 'woocommerce' ), - ); - - return $bulk_messages; - } - - /** - * Custom bulk edit - form. - * - * @param string $column_name Column being shown. - * @param string $post_type Post type being shown. - */ - public function bulk_edit( $column_name, $post_type ) { - if ( 'price' !== $column_name || 'product' !== $post_type ) { - return; - } - - $shipping_class = get_terms( - 'product_shipping_class', - array( - 'hide_empty' => false, - ) - ); - - include WC()->plugin_path() . '/includes/admin/views/html-bulk-edit-product.php'; - } - - /** - * Custom quick edit - form. - * - * @param string $column_name Column being shown. - * @param string $post_type Post type being shown. - */ - public function quick_edit( $column_name, $post_type ) { - if ( 'price' !== $column_name || 'product' !== $post_type ) { - return; - } - - $shipping_class = get_terms( - 'product_shipping_class', - array( - 'hide_empty' => false, - ) - ); - - include WC()->plugin_path() . '/includes/admin/views/html-quick-edit-product.php'; - } - - /** - * Offers a way to hook into save post without causing an infinite loop - * when quick/bulk saving product info. - * - * @since 3.0.0 - * @param int $post_id Post ID being saved. - * @param object $post Post object being saved. - */ - public function bulk_and_quick_edit_hook( $post_id, $post ) { - remove_action( 'save_post', array( $this, 'bulk_and_quick_edit_hook' ) ); - do_action( 'woocommerce_product_bulk_and_quick_edit', $post_id, $post ); - add_action( 'save_post', array( $this, 'bulk_and_quick_edit_hook' ), 10, 2 ); - } - - /** - * Quick and bulk edit saving. - * - * @param int $post_id Post ID being saved. - * @param object $post Post object being saved. - * @return int - */ - public function bulk_and_quick_edit_save_post( $post_id, $post ) { - $request_data = $this->request_data(); - - // If this is an autosave, our form has not been submitted, so we don't want to do anything. - if ( Constants::is_true( 'DOING_AUTOSAVE' ) ) { - return $post_id; - } - - // Don't save revisions and autosaves. - if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) || 'product' !== $post->post_type || ! current_user_can( 'edit_post', $post_id ) ) { - return $post_id; - } - - // Check nonce. - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash - if ( ! isset( $request_data['woocommerce_quick_edit_nonce'] ) || ! wp_verify_nonce( $request_data['woocommerce_quick_edit_nonce'], 'woocommerce_quick_edit_nonce' ) ) { - return $post_id; - } - - // Get the product and save. - $product = wc_get_product( $post ); - - if ( ! empty( $request_data['woocommerce_quick_edit'] ) ) { // WPCS: input var ok. - $this->quick_edit_save( $post_id, $product ); - } else { - $this->bulk_edit_save( $post_id, $product ); - } - - return $post_id; - } - - /** - * Quick edit. - * - * @param int $post_id Post ID being saved. - * @param WC_Product $product Product object. - */ - private function quick_edit_save( $post_id, $product ) { - $request_data = $this->request_data(); - - $data_store = $product->get_data_store(); - $old_regular_price = $product->get_regular_price(); - $old_sale_price = $product->get_sale_price(); - $input_to_props = array( - '_weight' => 'weight', - '_length' => 'length', - '_width' => 'width', - '_height' => 'height', - '_visibility' => 'catalog_visibility', - '_tax_class' => 'tax_class', - '_tax_status' => 'tax_status', - ); - - foreach ( $input_to_props as $input_var => $prop ) { - if ( isset( $request_data[ $input_var ] ) ) { - $product->{"set_{$prop}"}( wc_clean( wp_unslash( $request_data[ $input_var ] ) ) ); - } - } - - if ( isset( $request_data['_sku'] ) ) { - $sku = $product->get_sku(); - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash - $new_sku = (string) wc_clean( $request_data['_sku'] ); - - if ( $new_sku !== $sku ) { - if ( ! empty( $new_sku ) ) { - $unique_sku = wc_product_has_unique_sku( $post_id, $new_sku ); - if ( $unique_sku ) { - $product->set_sku( wc_clean( wp_unslash( $new_sku ) ) ); - } - } else { - $product->set_sku( '' ); - } - } - } - - if ( ! empty( $request_data['_shipping_class'] ) ) { - if ( '_no_shipping_class' === $request_data['_shipping_class'] ) { - $product->set_shipping_class_id( 0 ); - } else { - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash - $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $request_data['_shipping_class'] ) ); - $product->set_shipping_class_id( $shipping_class_id ); - } - } - - $product->set_featured( isset( $request_data['_featured'] ) ); - - if ( $product->is_type( 'simple' ) || $product->is_type( 'external' ) ) { - - if ( isset( $request_data['_regular_price'] ) ) { - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash - $new_regular_price = ( '' === $request_data['_regular_price'] ) ? '' : wc_format_decimal( $request_data['_regular_price'] ); - $product->set_regular_price( $new_regular_price ); - } else { - $new_regular_price = null; - } - if ( isset( $request_data['_sale_price'] ) ) { - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash - $new_sale_price = ( '' === $request_data['_sale_price'] ) ? '' : wc_format_decimal( $request_data['_sale_price'] ); - $product->set_sale_price( $new_sale_price ); - } else { - $new_sale_price = null; - } - - // Handle price - remove dates and set to lowest. - $price_changed = false; - - if ( ! is_null( $new_regular_price ) && $new_regular_price !== $old_regular_price ) { - $price_changed = true; - } elseif ( ! is_null( $new_sale_price ) && $new_sale_price !== $old_sale_price ) { - $price_changed = true; - } - - if ( $price_changed ) { - $product->set_date_on_sale_to( '' ); - $product->set_date_on_sale_from( '' ); - } - } - - // Handle Stock Data. - // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - $manage_stock = ! empty( $request_data['_manage_stock'] ) && 'grouped' !== $product->get_type() ? 'yes' : 'no'; - $backorders = ! empty( $request_data['_backorders'] ) ? wc_clean( $request_data['_backorders'] ) : 'no'; - if ( ! empty( $request_data['_stock_status'] ) ) { - $stock_status = wc_clean( $request_data['_stock_status'] ); - } else { - $stock_status = $product->is_type( 'variable' ) ? null : 'instock'; - } - // phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - - $product->set_manage_stock( $manage_stock ); - $product->set_backorders( $backorders ); - - if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { - $stock_amount = 'yes' === $manage_stock && isset( $request_data['_stock'] ) && is_numeric( wp_unslash( $request_data['_stock'] ) ) ? wc_stock_amount( wp_unslash( $request_data['_stock'] ) ) : ''; - $product->set_stock_quantity( $stock_amount ); - } - - $product = $this->maybe_update_stock_status( $product, $stock_status ); - - $product->save(); - - do_action( 'woocommerce_product_quick_edit_save', $product ); - } - - /** - * Bulk edit. - * - * @param int $post_id Post ID being saved. - * @param WC_Product $product Product object. - */ - public function bulk_edit_save( $post_id, $product ) { - // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash - - $request_data = $this->request_data(); - - $data_store = $product->get_data_store(); - - if ( ! empty( $request_data['change_weight'] ) && isset( $request_data['_weight'] ) ) { - $product->set_weight( wc_clean( wp_unslash( $request_data['_weight'] ) ) ); - } - - if ( ! empty( $request_data['change_dimensions'] ) ) { - if ( isset( $request_data['_length'] ) ) { - $product->set_length( wc_clean( wp_unslash( $request_data['_length'] ) ) ); - } - if ( isset( $request_data['_width'] ) ) { - $product->set_width( wc_clean( wp_unslash( $request_data['_width'] ) ) ); - } - if ( isset( $request_data['_height'] ) ) { - $product->set_height( wc_clean( wp_unslash( $request_data['_height'] ) ) ); - } - } - - if ( ! empty( $request_data['_tax_status'] ) ) { - $product->set_tax_status( wc_clean( $request_data['_tax_status'] ) ); - } - - if ( ! empty( $request_data['_tax_class'] ) ) { - $tax_class = wc_clean( wp_unslash( $request_data['_tax_class'] ) ); - if ( 'standard' === $tax_class ) { - $tax_class = ''; - } - $product->set_tax_class( $tax_class ); - } - - if ( ! empty( $request_data['_shipping_class'] ) ) { - if ( '_no_shipping_class' === $request_data['_shipping_class'] ) { - $product->set_shipping_class_id( 0 ); - } else { - $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $request_data['_shipping_class'] ) ); - $product->set_shipping_class_id( $shipping_class_id ); - } - } - - if ( ! empty( $request_data['_visibility'] ) ) { - $product->set_catalog_visibility( wc_clean( $request_data['_visibility'] ) ); - } - - if ( ! empty( $request_data['_featured'] ) ) { - // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - $product->set_featured( wp_unslash( $request_data['_featured'] ) ); - // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - } - - if ( ! empty( $request_data['_sold_individually'] ) ) { - if ( 'yes' === $request_data['_sold_individually'] ) { - $product->set_sold_individually( 'yes' ); - } else { - $product->set_sold_individually( '' ); - } - } - - // Handle price - remove dates and set to lowest. - $change_price_product_types = apply_filters( 'woocommerce_bulk_edit_save_price_product_types', array( 'simple', 'external' ) ); - $can_product_type_change_price = false; - foreach ( $change_price_product_types as $product_type ) { - if ( $product->is_type( $product_type ) ) { - $can_product_type_change_price = true; - break; - } - } - - if ( $can_product_type_change_price ) { - $regular_price_changed = $this->set_new_price( $product, 'regular' ); - $sale_price_changed = $this->set_new_price( $product, 'sale' ); - - if ( $regular_price_changed || $sale_price_changed ) { - $product->set_date_on_sale_to( '' ); - $product->set_date_on_sale_from( '' ); - - if ( $product->get_regular_price() < $product->get_sale_price() ) { - $product->set_sale_price( '' ); - } - } - } - - // Handle Stock Data. - $was_managing_stock = $product->get_manage_stock() ? 'yes' : 'no'; - $backorders = $product->get_backorders(); - $backorders = ! empty( $request_data['_backorders'] ) ? wc_clean( $request_data['_backorders'] ) : $backorders; - - if ( ! empty( $request_data['_manage_stock'] ) ) { - $manage_stock = 'yes' === wc_clean( $request_data['_manage_stock'] ) && 'grouped' !== $product->get_type() ? 'yes' : 'no'; - } else { - $manage_stock = $was_managing_stock; - } - - $stock_amount = 'yes' === $manage_stock && ! empty( $request_data['change_stock'] ) && isset( $request_data['_stock'] ) ? wc_stock_amount( $request_data['_stock'] ) : $product->get_stock_quantity(); - - $product->set_manage_stock( $manage_stock ); - $product->set_backorders( $backorders ); - - if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { - $change_stock = absint( $request_data['change_stock'] ); - switch ( $change_stock ) { - case 2: - wc_update_product_stock( $product, $stock_amount, 'increase', true ); - break; - case 3: - wc_update_product_stock( $product, $stock_amount, 'decrease', true ); - break; - default: - wc_update_product_stock( $product, $stock_amount, 'set', true ); - break; - } - } else { - // Reset values if WooCommerce Setting - Manage Stock status is disabled. - $product->set_stock_quantity( '' ); - $product->set_manage_stock( 'no' ); - } - - $stock_status = empty( $request_data['_stock_status'] ) ? null : wc_clean( $request_data['_stock_status'] ); - $product = $this->maybe_update_stock_status( $product, $stock_status ); - - $product->save(); - - do_action( 'woocommerce_product_bulk_edit_save', $product ); - - // phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash - } - - /** - * Disable the auto-save functionality for Orders. - */ - public function disable_autosave() { - global $post; - - if ( $post && in_array( get_post_type( $post->ID ), wc_get_order_types( 'order-meta-boxes' ), true ) ) { - wp_dequeue_script( 'autosave' ); - } - } - - /** - * Output extra data on post forms. - * - * @param WP_Post $post Current post object. - */ - public function edit_form_top( $post ) { - echo ''; - } - - /** - * Change title boxes in admin. - * - * @param string $text Text to shown. - * @param WP_Post $post Current post object. - * @return string - */ - public function enter_title_here( $text, $post ) { - switch ( $post->post_type ) { - case 'product': - $text = esc_html__( 'Product name', 'woocommerce' ); - break; - case 'shop_coupon': - $text = esc_html__( 'Coupon code', 'woocommerce' ); - break; - } - return $text; - } - - /** - * Print coupon description textarea field. - * - * @param WP_Post $post Current post object. - */ - public function edit_form_after_title( $post ) { - // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped - if ( 'shop_coupon' === $post->post_type ) { - ?> - - post_type && 'post' === $screen->base ) { - $hidden = array_merge( $hidden, array( 'postcustom' ) ); - } - - return $hidden; - } - - /** - * Output product visibility options. - */ - public function product_data_visibility() { - global $post, $thepostid, $product_object; - - if ( 'product' !== $post->post_type ) { - return; - } - - $thepostid = $post->ID; - $product_object = $thepostid ? wc_get_product( $thepostid ) : new WC_Product(); - $current_visibility = $product_object->get_catalog_visibility(); - $current_featured = wc_bool_to_string( $product_object->get_featured() ); - $visibility_options = wc_get_product_visibility_options(); - ?> -
    - - - - - - - -
    - - - - - ' . esc_html__( 'This setting determines which shop pages products will be listed on.', 'woocommerce' ) . '

    '; - - foreach ( $visibility_options as $name => $label ) { - echo '
    '; - } - - echo '

    '; - ?> -

    - - -

    -
    -
    - unique_filename( $full_filename, $ext ); - // phpcs:enable WordPress.Security.NonceVerification.Missing - } - - /** - * Change filename to append random text. - * - * @param string $full_filename Original filename with extension. - * @param string $ext Extension. - * - * @return string Modified filename. - */ - public function unique_filename( $full_filename, $ext ) { - $ideal_random_char_length = 6; // Not going with a larger length because then downloaded filename will not be pretty. - $max_filename_length = 255; // Max file name length for most file systems. - $length_to_prepend = min( $ideal_random_char_length, $max_filename_length - strlen( $full_filename ) - 1 ); - - if ( 1 > $length_to_prepend ) { - return $full_filename; - } - - $suffix = strtolower( wp_generate_password( $length_to_prepend, false, false ) ); - $filename = $full_filename; - - if ( strlen( $ext ) > 0 ) { - $filename = substr( $filename, 0, strlen( $filename ) - strlen( $ext ) ); - } - - $full_filename = str_replace( - $filename, - "$filename-$suffix", - $full_filename - ); - - return $full_filename; - } - - /** - * Run a filter when uploading a downloadable product. - */ - public function woocommerce_media_upload_downloadable_product() { - do_action( 'media_upload_file' ); - } - - /** - * Grant downloadable file access to any newly added files on any existing. - * orders for this product that have previously been granted downloadable file access. - * - * @param int $product_id product identifier. - * @param int $variation_id optional product variation identifier. - * @param array $downloadable_files newly set files. - * @deprecated 3.3.0 and moved to post-data class. - */ - public function process_product_file_download_paths( $product_id, $variation_id, $downloadable_files ) { - wc_deprecated_function( 'WC_Admin_Post_Types::process_product_file_download_paths', '3.3', '' ); - WC_Post_Data::process_product_file_download_paths( $product_id, $variation_id, $downloadable_files ); - } - - /** - * When editing the shop page, we should hide templates. - * - * @param array $page_templates Templates array. - * @param string $theme Classname. - * @param WP_Post $post The current post object. - * @return array - */ - public function hide_cpt_archive_templates( $page_templates, $theme, $post ) { - $shop_page_id = wc_get_page_id( 'shop' ); - - if ( $post && absint( $post->ID ) === $shop_page_id ) { - $page_templates = array(); - } - - return $page_templates; - } - - /** - * Show a notice above the CPT archive. - * - * @param WP_Post $post The current post object. - */ - public function show_cpt_archive_notice( $post ) { - $shop_page_id = wc_get_page_id( 'shop' ); - - if ( $post && absint( $post->ID ) === $shop_page_id ) { - echo '
    '; - /* translators: %s: URL to read more about the shop page. */ - echo '

    ' . sprintf( wp_kses_post( __( 'This is the WooCommerce shop page. The shop page is a special archive that lists your products. You can read more about this here.', 'woocommerce' ) ), 'https://docs.woocommerce.com/document/woocommerce-pages/#section-4' ) . '

    '; - echo '
    '; - } - } - - /** - * Add a post display state for special WC pages in the page list table. - * - * @param array $post_states An array of post display states. - * @param WP_Post $post The current post object. - */ - public function add_display_post_states( $post_states, $post ) { - if ( wc_get_page_id( 'shop' ) === $post->ID ) { - $post_states['wc_page_for_shop'] = __( 'Shop Page', 'woocommerce' ); - } - - if ( wc_get_page_id( 'cart' ) === $post->ID ) { - $post_states['wc_page_for_cart'] = __( 'Cart Page', 'woocommerce' ); - } - - if ( wc_get_page_id( 'checkout' ) === $post->ID ) { - $post_states['wc_page_for_checkout'] = __( 'Checkout Page', 'woocommerce' ); - } - - if ( wc_get_page_id( 'myaccount' ) === $post->ID ) { - $post_states['wc_page_for_myaccount'] = __( 'My Account Page', 'woocommerce' ); - } - - if ( wc_get_page_id( 'terms' ) === $post->ID ) { - $post_states['wc_page_for_terms'] = __( 'Terms and Conditions Page', 'woocommerce' ); - } - - return $post_states; - } - - /** - * Apply product type constraints to stock status. - * - * @param WC_Product $product The product whose stock status will be adjusted. - * @param string|null $stock_status The stock status to use for adjustment, or null if no new stock status has been supplied in the request. - * @return WC_Product The supplied product, or the synced product if it was a variable product. - */ - private function maybe_update_stock_status( $product, $stock_status ) { - if ( $product->is_type( 'external' ) ) { - // External products are always in stock. - $product->set_stock_status( 'instock' ); - } elseif ( isset( $stock_status ) ) { - if ( $product->is_type( 'variable' ) && ! $product->get_manage_stock() ) { - // Stock status is determined by children. - foreach ( $product->get_children() as $child_id ) { - $child = wc_get_product( $child_id ); - if ( ! $product->get_manage_stock() ) { - $child->set_stock_status( $stock_status ); - $child->save(); - } - } - $product = WC_Product_Variable::sync( $product, false ); - } else { - $product->set_stock_status( $stock_status ); - } - } - - return $product; - } - - /** - * Set the new regular or sale price if requested. - * - * @param WC_Product $product The product to set the new price for. - * @param string $price_type 'regular' or 'sale'. - * @return bool true if a new price has been set, false otherwise. - */ - private function set_new_price( $product, $price_type ) { - // phpcs:disable WordPress.Security.NonceVerification.Recommended - - $request_data = $this->request_data(); - - if ( empty( $request_data[ "change_{$price_type}_price" ] ) || ! isset( $request_data[ "_{$price_type}_price" ] ) ) { - return false; - } - - $old_price = $product->{"get_{$price_type}_price"}(); - $price_changed = false; - - $change_price = absint( $request_data[ "change_{$price_type}_price" ] ); - $raw_price = wc_clean( wp_unslash( $request_data[ "_{$price_type}_price" ] ) ); - $is_percentage = (bool) strstr( $raw_price, '%' ); - $price = wc_format_decimal( $raw_price ); - - switch ( $change_price ) { - case 1: - $new_price = $price; - break; - case 2: - if ( $is_percentage ) { - $percent = $price / 100; - $new_price = $old_price + ( $old_price * $percent ); - } else { - $new_price = $old_price + $price; - } - break; - case 3: - if ( $is_percentage ) { - $percent = $price / 100; - $new_price = max( 0, $old_price - ( $old_price * $percent ) ); - } else { - $new_price = max( 0, $old_price - $price ); - } - break; - case 4: - if ( 'sale' !== $price_type ) { - break; - } - $regular_price = $product->get_regular_price(); - if ( $is_percentage ) { - $percent = $price / 100; - $new_price = max( 0, $regular_price - ( NumberUtil::round( $regular_price * $percent, wc_get_price_decimals() ) ) ); - } else { - $new_price = max( 0, $regular_price - $price ); - } - break; - - default: - break; - } - - if ( isset( $new_price ) && $new_price !== $old_price ) { - $price_changed = true; - $new_price = NumberUtil::round( $new_price, wc_get_price_decimals() ); - $product->{"set_{$price_type}_price"}( $new_price ); - } - - return $price_changed; - - // phpcs:disable WordPress.Security.NonceVerification.Recommended - } - - /** - * Get the current request data ($_REQUEST superglobal). - * This method is added to ease unit testing. - * - * @return array The $_REQUEST superglobal. - */ - protected function request_data() { - return $_REQUEST; - } -} - -new WC_Admin_Post_Types(); diff --git a/includes/admin/class-wc-admin-profile.php b/includes/admin/class-wc-admin-profile.php deleted file mode 100644 index 94180722001..00000000000 --- a/includes/admin/class-wc-admin-profile.php +++ /dev/null @@ -1,248 +0,0 @@ - array( - 'title' => __( 'Customer billing address', 'woocommerce' ), - 'fields' => array( - 'billing_first_name' => array( - 'label' => __( 'First name', 'woocommerce' ), - 'description' => '', - ), - 'billing_last_name' => array( - 'label' => __( 'Last name', 'woocommerce' ), - 'description' => '', - ), - 'billing_company' => array( - 'label' => __( 'Company', 'woocommerce' ), - 'description' => '', - ), - 'billing_address_1' => array( - 'label' => __( 'Address line 1', 'woocommerce' ), - 'description' => '', - ), - 'billing_address_2' => array( - 'label' => __( 'Address line 2', 'woocommerce' ), - 'description' => '', - ), - 'billing_city' => array( - 'label' => __( 'City', 'woocommerce' ), - 'description' => '', - ), - 'billing_postcode' => array( - 'label' => __( 'Postcode / ZIP', 'woocommerce' ), - 'description' => '', - ), - 'billing_country' => array( - 'label' => __( 'Country / Region', 'woocommerce' ), - 'description' => '', - 'class' => 'js_field-country', - 'type' => 'select', - 'options' => array( '' => __( 'Select a country / region…', 'woocommerce' ) ) + WC()->countries->get_allowed_countries(), - ), - 'billing_state' => array( - 'label' => __( 'State / County', 'woocommerce' ), - 'description' => __( 'State / County or state code', 'woocommerce' ), - 'class' => 'js_field-state', - ), - 'billing_phone' => array( - 'label' => __( 'Phone', 'woocommerce' ), - 'description' => '', - ), - 'billing_email' => array( - 'label' => __( 'Email address', 'woocommerce' ), - 'description' => '', - ), - ), - ), - 'shipping' => array( - 'title' => __( 'Customer shipping address', 'woocommerce' ), - 'fields' => array( - 'copy_billing' => array( - 'label' => __( 'Copy from billing address', 'woocommerce' ), - 'description' => '', - 'class' => 'js_copy-billing', - 'type' => 'button', - 'text' => __( 'Copy', 'woocommerce' ), - ), - 'shipping_first_name' => array( - 'label' => __( 'First name', 'woocommerce' ), - 'description' => '', - ), - 'shipping_last_name' => array( - 'label' => __( 'Last name', 'woocommerce' ), - 'description' => '', - ), - 'shipping_company' => array( - 'label' => __( 'Company', 'woocommerce' ), - 'description' => '', - ), - 'shipping_address_1' => array( - 'label' => __( 'Address line 1', 'woocommerce' ), - 'description' => '', - ), - 'shipping_address_2' => array( - 'label' => __( 'Address line 2', 'woocommerce' ), - 'description' => '', - ), - 'shipping_city' => array( - 'label' => __( 'City', 'woocommerce' ), - 'description' => '', - ), - 'shipping_postcode' => array( - 'label' => __( 'Postcode / ZIP', 'woocommerce' ), - 'description' => '', - ), - 'shipping_country' => array( - 'label' => __( 'Country / Region', 'woocommerce' ), - 'description' => '', - 'class' => 'js_field-country', - 'type' => 'select', - 'options' => array( '' => __( 'Select a country / region…', 'woocommerce' ) ) + WC()->countries->get_allowed_countries(), - ), - 'shipping_state' => array( - 'label' => __( 'State / County', 'woocommerce' ), - 'description' => __( 'State / County or state code', 'woocommerce' ), - 'class' => 'js_field-state', - ), - ), - ), - ) - ); - return $show_fields; - } - - /** - * Show Address Fields on edit user pages. - * - * @param WP_User $user - */ - public function add_customer_meta_fields( $user ) { - if ( ! apply_filters( 'woocommerce_current_user_can_edit_customer_meta_fields', current_user_can( 'manage_woocommerce' ), $user->ID ) ) { - return; - } - - $show_fields = $this->get_customer_meta_fields(); - - foreach ( $show_fields as $fieldset_key => $fieldset ) : - ?> -

    - - $field ) : ?> - - - - - -
    - - - - - - ID, $key, true ), 1, true ); ?> /> - - - - - -

    -
    - get_customer_meta_fields(); - - foreach ( $save_fields as $fieldset ) { - - foreach ( $fieldset['fields'] as $key => $field ) { - - if ( isset( $field['type'] ) && 'checkbox' === $field['type'] ) { - update_user_meta( $user_id, $key, isset( $_POST[ $key ] ) ); - } elseif ( isset( $_POST[ $key ] ) ) { - update_user_meta( $user_id, $key, wc_clean( $_POST[ $key ] ) ); - } - } - } - } - - /** - * Get user meta for a given key, with fallbacks to core user info for pre-existing fields. - * - * @since 3.1.0 - * @param int $user_id User ID of the user being edited - * @param string $key Key for user meta field - * @return string - */ - protected function get_user_meta( $user_id, $key ) { - $value = get_user_meta( $user_id, $key, true ); - $existing_fields = array( 'billing_first_name', 'billing_last_name' ); - if ( ! $value && in_array( $key, $existing_fields ) ) { - $value = get_user_meta( $user_id, str_replace( 'billing_', '', $key ), true ); - } elseif ( ! $value && ( 'billing_email' === $key ) ) { - $user = get_userdata( $user_id ); - $value = $user->user_email; - } - - return $value; - } - } - -endif; - -return new WC_Admin_Profile(); diff --git a/includes/admin/class-wc-admin-settings.php b/includes/admin/class-wc-admin-settings.php deleted file mode 100644 index b36d9a889d6..00000000000 --- a/includes/admin/class-wc-admin-settings.php +++ /dev/null @@ -1,900 +0,0 @@ -query->init_query_vars(); - WC()->query->add_endpoints(); - - do_action( 'woocommerce_settings_saved' ); - } - - /** - * Add a message. - * - * @param string $text Message. - */ - public static function add_message( $text ) { - self::$messages[] = $text; - } - - /** - * Add an error. - * - * @param string $text Message. - */ - public static function add_error( $text ) { - self::$errors[] = $text; - } - - /** - * Output messages + errors. - */ - public static function show_messages() { - if ( count( self::$errors ) > 0 ) { - foreach ( self::$errors as $error ) { - echo '

    ' . esc_html( $error ) . '

    '; - } - } elseif ( count( self::$messages ) > 0 ) { - foreach ( self::$messages as $message ) { - echo '

    ' . esc_html( $message ) . '

    '; - } - } - } - - /** - * Settings page. - * - * Handles the display of the main woocommerce settings page in admin. - */ - public static function output() { - global $current_section, $current_tab; - - $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; - - do_action( 'woocommerce_settings_start' ); - - wp_enqueue_script( 'woocommerce_settings', WC()->plugin_url() . '/assets/js/admin/settings' . $suffix . '.js', array( 'jquery', 'wp-util', 'jquery-ui-datepicker', 'jquery-ui-sortable', 'iris', 'selectWoo' ), WC()->version, true ); - - wp_localize_script( - 'woocommerce_settings', - 'woocommerce_settings_params', - array( - 'i18n_nav_warning' => __( 'The changes you made will be lost if you navigate away from this page.', 'woocommerce' ), - 'i18n_moved_up' => __( 'Item moved up', 'woocommerce' ), - 'i18n_moved_down' => __( 'Item moved down', 'woocommerce' ), - 'i18n_no_specific_countries_selected' => __( 'Selecting no country / region to sell to prevents from completing the checkout. Continue anyway?', 'woocommerce' ), - ) - ); - - // Get tabs for the settings page. - $tabs = apply_filters( 'woocommerce_settings_tabs_array', array() ); - - include dirname( __FILE__ ) . '/views/html-admin-settings.php'; - } - - /** - * Get a setting from the settings API. - * - * @param string $option_name Option name. - * @param mixed $default Default value. - * @return mixed - */ - public static function get_option( $option_name, $default = '' ) { - if ( ! $option_name ) { - return $default; - } - - // Array value. - if ( strstr( $option_name, '[' ) ) { - - parse_str( $option_name, $option_array ); - - // Option name is first key. - $option_name = current( array_keys( $option_array ) ); - - // Get value. - $option_values = get_option( $option_name, '' ); - - $key = key( $option_array[ $option_name ] ); - - if ( isset( $option_values[ $key ] ) ) { - $option_value = $option_values[ $key ]; - } else { - $option_value = null; - } - } else { - // Single value. - $option_value = get_option( $option_name, null ); - } - - if ( is_array( $option_value ) ) { - $option_value = wp_unslash( $option_value ); - } elseif ( ! is_null( $option_value ) ) { - $option_value = stripslashes( $option_value ); - } - - return ( null === $option_value ) ? $default : $option_value; - } - - /** - * Output admin fields. - * - * Loops through the woocommerce options array and outputs each field. - * - * @param array[] $options Opens array to output. - */ - public static function output_fields( $options ) { - foreach ( $options as $value ) { - if ( ! isset( $value['type'] ) ) { - continue; - } - if ( ! isset( $value['id'] ) ) { - $value['id'] = ''; - } - if ( ! isset( $value['title'] ) ) { - $value['title'] = isset( $value['name'] ) ? $value['name'] : ''; - } - if ( ! isset( $value['class'] ) ) { - $value['class'] = ''; - } - if ( ! isset( $value['css'] ) ) { - $value['css'] = ''; - } - if ( ! isset( $value['default'] ) ) { - $value['default'] = ''; - } - if ( ! isset( $value['desc'] ) ) { - $value['desc'] = ''; - } - if ( ! isset( $value['desc_tip'] ) ) { - $value['desc_tip'] = false; - } - if ( ! isset( $value['placeholder'] ) ) { - $value['placeholder'] = ''; - } - if ( ! isset( $value['suffix'] ) ) { - $value['suffix'] = ''; - } - if ( ! isset( $value['value'] ) ) { - $value['value'] = self::get_option( $value['id'], $value['default'] ); - } - - // Custom attribute handling. - $custom_attributes = array(); - - if ( ! empty( $value['custom_attributes'] ) && is_array( $value['custom_attributes'] ) ) { - foreach ( $value['custom_attributes'] as $attribute => $attribute_value ) { - $custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $attribute_value ) . '"'; - } - } - - // Description handling. - $field_description = self::get_field_description( $value ); - $description = $field_description['description']; - $tooltip_html = $field_description['tooltip_html']; - - // Switch based on type. - switch ( $value['type'] ) { - - // Section Titles. - case 'title': - if ( ! empty( $value['title'] ) ) { - echo '

    ' . esc_html( $value['title'] ) . '

    '; - } - if ( ! empty( $value['desc'] ) ) { - echo '
    '; - echo wp_kses_post( wpautop( wptexturize( $value['desc'] ) ) ); - echo '
    '; - } - echo '' . "\n\n"; - if ( ! empty( $value['id'] ) ) { - do_action( 'woocommerce_settings_' . sanitize_title( $value['id'] ) ); - } - break; - - // Section Ends. - case 'sectionend': - if ( ! empty( $value['id'] ) ) { - do_action( 'woocommerce_settings_' . sanitize_title( $value['id'] ) . '_end' ); - } - echo '
    '; - if ( ! empty( $value['id'] ) ) { - do_action( 'woocommerce_settings_' . sanitize_title( $value['id'] ) . '_after' ); - } - break; - - // Standard text inputs and subtypes like 'number'. - case 'text': - case 'password': - case 'datetime': - case 'datetime-local': - case 'date': - case 'month': - case 'time': - case 'week': - case 'number': - case 'email': - case 'url': - case 'tel': - $option_value = $value['value']; - - ?> - - - - - - /> - - - - - - - - ‎ -   - - />‎ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - -
    - - - - - - -
    - -
    - - - - - -
    - - - -
    - ' . esc_html__( 'The settings of this image size have been disabled because its values are being overwritten by a filter.', 'woocommerce' ) . '

    '; - } - - ?> - - - - - - - id="-width" type="text" size="3" value="" /> × id="-height" type="text" size="3" value="" />px - - - - - - $value['id'], - 'id' => $value['id'], - 'sort_column' => 'menu_order', - 'sort_order' => 'ASC', - 'show_option_none' => ' ', - 'class' => $value['class'], - 'echo' => false, - 'selected' => absint( $value['value'] ), - 'post_status' => 'publish,private,draft', - ); - - if ( isset( $value['args'] ) ) { - $args = wp_parse_args( $value['args'], $args ); - } - - ?> - - - - - - - - - - - - - - - - - countries->countries; - } - - asort( $countries ); - ?> - - - - - -
    - - - __( 'Day(s)', 'woocommerce' ), - 'weeks' => __( 'Week(s)', 'woocommerce' ), - 'months' => __( 'Month(s)', 'woocommerce' ), - 'years' => __( 'Year(s)', 'woocommerce' ), - ); - $option_value = wc_parse_relative_date_option( $value['value'] ); - ?> - - - - - - - />  - - - - ' . wp_kses_post( $description ) . '

    '; - } elseif ( $description && in_array( $value['type'], array( 'checkbox' ), true ) ) { - $description = wp_kses_post( $description ); - } elseif ( $description ) { - $description = '

    ' . wp_kses_post( $description ) . '

    '; - } - - if ( $tooltip_html && in_array( $value['type'], array( 'checkbox' ), true ) ) { - $tooltip_html = '

    ' . $tooltip_html . '

    '; - } elseif ( $tooltip_html ) { - $tooltip_html = wc_help_tip( $tooltip_html ); - } - - return array( - 'description' => $description, - 'tooltip_html' => $tooltip_html, - ); - } - - /** - * Save admin fields. - * - * Loops through the woocommerce options array and outputs each field. - * - * @param array $options Options array to output. - * @param array $data Optional. Data to use for saving. Defaults to $_POST. - * @return bool - */ - public static function save_fields( $options, $data = null ) { - if ( is_null( $data ) ) { - $data = $_POST; // WPCS: input var okay, CSRF ok. - } - if ( empty( $data ) ) { - return false; - } - - // Options to update will be stored here and saved later. - $update_options = array(); - $autoload_options = array(); - - // Loop options and get values to save. - foreach ( $options as $option ) { - if ( ! isset( $option['id'] ) || ! isset( $option['type'] ) || ( isset( $option['is_option'] ) && false === $option['is_option'] ) ) { - continue; - } - - // Get posted value. - if ( strstr( $option['id'], '[' ) ) { - parse_str( $option['id'], $option_name_array ); - $option_name = current( array_keys( $option_name_array ) ); - $setting_name = key( $option_name_array[ $option_name ] ); - $raw_value = isset( $data[ $option_name ][ $setting_name ] ) ? wp_unslash( $data[ $option_name ][ $setting_name ] ) : null; - } else { - $option_name = $option['id']; - $setting_name = ''; - $raw_value = isset( $data[ $option['id'] ] ) ? wp_unslash( $data[ $option['id'] ] ) : null; - } - - // Format the value based on option type. - switch ( $option['type'] ) { - case 'checkbox': - $value = '1' === $raw_value || 'yes' === $raw_value ? 'yes' : 'no'; - break; - case 'textarea': - $value = wp_kses_post( trim( $raw_value ) ); - break; - case 'multiselect': - case 'multi_select_countries': - $value = array_filter( array_map( 'wc_clean', (array) $raw_value ) ); - break; - case 'image_width': - $value = array(); - if ( isset( $raw_value['width'] ) ) { - $value['width'] = wc_clean( $raw_value['width'] ); - $value['height'] = wc_clean( $raw_value['height'] ); - $value['crop'] = isset( $raw_value['crop'] ) ? 1 : 0; - } else { - $value['width'] = $option['default']['width']; - $value['height'] = $option['default']['height']; - $value['crop'] = $option['default']['crop']; - } - break; - case 'select': - $allowed_values = empty( $option['options'] ) ? array() : array_map( 'strval', array_keys( $option['options'] ) ); - if ( empty( $option['default'] ) && empty( $allowed_values ) ) { - $value = null; - break; - } - $default = ( empty( $option['default'] ) ? $allowed_values[0] : $option['default'] ); - $value = in_array( $raw_value, $allowed_values, true ) ? $raw_value : $default; - break; - case 'relative_date_selector': - $value = wc_parse_relative_date_option( $raw_value ); - break; - default: - $value = wc_clean( $raw_value ); - break; - } - - /** - * Fire an action when a certain 'type' of field is being saved. - * - * @deprecated 2.4.0 - doesn't allow manipulation of values! - */ - if ( has_action( 'woocommerce_update_option_' . sanitize_title( $option['type'] ) ) ) { - wc_deprecated_function( 'The woocommerce_update_option_X action', '2.4.0', 'woocommerce_admin_settings_sanitize_option filter' ); - do_action( 'woocommerce_update_option_' . sanitize_title( $option['type'] ), $option ); - continue; - } - - /** - * Sanitize the value of an option. - * - * @since 2.4.0 - */ - $value = apply_filters( 'woocommerce_admin_settings_sanitize_option', $value, $option, $raw_value ); - - /** - * Sanitize the value of an option by option name. - * - * @since 2.4.0 - */ - $value = apply_filters( "woocommerce_admin_settings_sanitize_option_$option_name", $value, $option, $raw_value ); - - if ( is_null( $value ) ) { - continue; - } - - // Check if option is an array and handle that differently to single values. - if ( $option_name && $setting_name ) { - if ( ! isset( $update_options[ $option_name ] ) ) { - $update_options[ $option_name ] = get_option( $option_name, array() ); - } - if ( ! is_array( $update_options[ $option_name ] ) ) { - $update_options[ $option_name ] = array(); - } - $update_options[ $option_name ][ $setting_name ] = $value; - } else { - $update_options[ $option_name ] = $value; - } - - $autoload_options[ $option_name ] = isset( $option['autoload'] ) ? (bool) $option['autoload'] : true; - - /** - * Fire an action before saved. - * - * @deprecated 2.4.0 - doesn't allow manipulation of values! - */ - do_action( 'woocommerce_update_option', $option ); - } - - // Save all options in our array. - foreach ( $update_options as $name => $value ) { - update_option( $name, $value, $autoload_options[ $name ] ? 'yes' : 'no' ); - } - - return true; - } - - /** - * Checks which method we're using to serve downloads. - * - * If using force or x-sendfile, this ensures the .htaccess is in place. - */ - public static function check_download_folder_protection() { - $upload_dir = wp_get_upload_dir(); - $downloads_path = $upload_dir['basedir'] . '/woocommerce_uploads'; - $download_method = get_option( 'woocommerce_file_download_method' ); - $file_path = $downloads_path . '/.htaccess'; - $file_content = 'redirect' === $download_method ? 'Options -Indexes' : 'deny from all'; - $create = false; - - if ( wp_mkdir_p( $downloads_path ) && ! file_exists( $file_path ) ) { - $create = true; - } else { - $current_content = @file_get_contents( $file_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents - - if ( $current_content !== $file_content ) { - unlink( $file_path ); - $create = true; - } - } - - if ( $create ) { - $file_handle = @fopen( $file_path, 'wb' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen - if ( $file_handle ) { - fwrite( $file_handle, $file_content ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite - fclose( $file_handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose - } - } - } - } - -endif; diff --git a/includes/admin/class-wc-admin-setup-wizard.php b/includes/admin/class-wc-admin-setup-wizard.php deleted file mode 100644 index 14c11081d8c..00000000000 --- a/includes/admin/class-wc-admin-setup-wizard.php +++ /dev/null @@ -1,2306 +0,0 @@ -countries->get_base_country(); - // https://developers.taxjar.com/api/reference/#countries . - $tax_supported_countries = array_merge( - array( 'US', 'CA', 'AU' ), - WC()->countries->get_european_union_countries() - ); - - return in_array( $country_code, $tax_supported_countries, true ); - } - - /** - * Should we show the MailChimp install option? - * True only if the user can install plugins. - * - * @deprecated 4.6.0 - * @return boolean - */ - protected function should_show_mailchimp() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - return current_user_can( 'install_plugins' ); - } - - /** - * Should we show the Facebook install option? - * True only if the user can install plugins, - * and up until the end date of the recommendation. - * - * @deprecated 4.6.0 - * @return boolean - */ - protected function should_show_facebook() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - return current_user_can( 'install_plugins' ); - } - - /** - * Is the WooCommerce Admin actively included in the WooCommerce core? - * Based on presence of a basic WC Admin function. - * - * @deprecated 4.6.0 - * @return boolean - */ - protected function is_wc_admin_active() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - return function_exists( 'wc_admin_url' ); - } - - /** - * Should we show the WooCommerce Admin install option? - * True only if the user can install plugins, - * and is running the correct version of WordPress. - * - * @see WC_Admin_Setup_Wizard::$wc_admin_plugin_minimum_wordpress_version - * - * @deprecated 4.6.0 - * @return boolean - */ - protected function should_show_wc_admin() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - $wordpress_minimum_met = version_compare( get_bloginfo( 'version' ), $this->wc_admin_plugin_minimum_wordpress_version, '>=' ); - return current_user_can( 'install_plugins' ) && $wordpress_minimum_met && ! $this->is_wc_admin_active(); - } - - /** - * Should we show the new WooCommerce Admin onboarding experience? - * - * @deprecated 4.6.0 - * @return boolean - */ - protected function should_show_wc_admin_onboarding() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - // As of WooCommerce 4.1, all new sites should use the latest OBW from wc-admin package. - // This filter will allow for forcing the old wizard while we migrate e2e tests. - return ! apply_filters( 'woocommerce_setup_wizard_force_legacy', false ); - } - - /** - * Should we display the 'Recommended' step? - * True if at least one of the recommendations will be displayed. - * - * @deprecated 4.6.0 - * @return boolean - */ - protected function should_show_recommended_step() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - return $this->should_show_theme() - || $this->should_show_automated_tax() - || $this->should_show_mailchimp() - || $this->should_show_facebook() - || $this->should_show_wc_admin(); - } - - /** - * Register/enqueue scripts and styles for the Setup Wizard. - * - * Hooked onto 'admin_enqueue_scripts'. - * - * @deprecated 4.6.0 - */ - public function enqueue_scripts() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - } - - /** - * Show the setup wizard. - * - * @deprecated 4.6.0 - */ - public function setup_wizard() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - if ( empty( $_GET['page'] ) || 'wc-setup' !== $_GET['page'] ) { // WPCS: CSRF ok, input var ok. - return; - } - $default_steps = array( - 'new_onboarding' => array( - 'name' => '', - 'view' => array( $this, 'wc_setup_new_onboarding' ), - 'handler' => array( $this, 'wc_setup_new_onboarding_save' ), - ), - 'store_setup' => array( - 'name' => __( 'Store setup', 'woocommerce' ), - 'view' => array( $this, 'wc_setup_store_setup' ), - 'handler' => array( $this, 'wc_setup_store_setup_save' ), - ), - 'payment' => array( - 'name' => __( 'Payment', 'woocommerce' ), - 'view' => array( $this, 'wc_setup_payment' ), - 'handler' => array( $this, 'wc_setup_payment_save' ), - ), - 'shipping' => array( - 'name' => __( 'Shipping', 'woocommerce' ), - 'view' => array( $this, 'wc_setup_shipping' ), - 'handler' => array( $this, 'wc_setup_shipping_save' ), - ), - 'recommended' => array( - 'name' => __( 'Recommended', 'woocommerce' ), - 'view' => array( $this, 'wc_setup_recommended' ), - 'handler' => array( $this, 'wc_setup_recommended_save' ), - ), - 'activate' => array( - 'name' => __( 'Activate', 'woocommerce' ), - 'view' => array( $this, 'wc_setup_activate' ), - 'handler' => array( $this, 'wc_setup_activate_save' ), - ), - 'next_steps' => array( - 'name' => __( 'Ready!', 'woocommerce' ), - 'view' => array( $this, 'wc_setup_ready' ), - 'handler' => '', - ), - ); - - // Hide the new/improved onboarding experience screen if the user is not part of the a/b test. - if ( ! $this->should_show_wc_admin_onboarding() ) { - unset( $default_steps['new_onboarding'] ); - } - - // Hide recommended step if nothing is going to be shown there. - if ( ! $this->should_show_recommended_step() ) { - unset( $default_steps['recommended'] ); - } - - // Hide shipping step if the store is selling digital products only. - if ( 'virtual' === get_option( 'woocommerce_product_type' ) ) { - unset( $default_steps['shipping'] ); - } - - // Hide activate section when the user does not have capabilities to install plugins, think multiside admins not being a super admin. - if ( ! current_user_can( 'install_plugins' ) ) { - unset( $default_steps['activate'] ); - } - - $this->steps = apply_filters( 'woocommerce_setup_wizard_steps', $default_steps ); - $this->step = isset( $_GET['step'] ) ? sanitize_key( $_GET['step'] ) : current( array_keys( $this->steps ) ); // WPCS: CSRF ok, input var ok. - - // @codingStandardsIgnoreStart - if ( ! empty( $_POST['save_step'] ) && isset( $this->steps[ $this->step ]['handler'] ) ) { - call_user_func( $this->steps[ $this->step ]['handler'], $this ); - } - // @codingStandardsIgnoreEnd - - ob_start(); - $this->setup_wizard_header(); - $this->setup_wizard_steps(); - $this->setup_wizard_content(); - $this->setup_wizard_footer(); - exit; - } - - /** - * Get the URL for the next step's screen. - * - * @param string $step slug (default: current step). - * @return string URL for next step if a next step exists. - * Admin URL if it's the last step. - * Empty string on failure. - * - * @deprecated 4.6.0 - * @since 3.0.0 - */ - public function get_next_step_link( $step = '' ) { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - if ( ! $step ) { - $step = $this->step; - } - - $keys = array_keys( $this->steps ); - if ( end( $keys ) === $step ) { - return admin_url(); - } - - $step_index = array_search( $step, $keys, true ); - if ( false === $step_index ) { - return ''; - } - - return add_query_arg( 'step', $keys[ $step_index + 1 ], remove_query_arg( 'activate_error' ) ); - } - - /** - * Setup Wizard Header. - * - * @deprecated 4.6.0 - */ - public function setup_wizard_header() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - // same as default WP from wp-admin/admin-header.php. - $wp_version_class = 'branch-' . str_replace( array( '.', ',' ), '-', floatval( get_bloginfo( 'version' ) ) ); - - set_current_screen(); - ?> - - > - - - - <?php esc_html_e( 'WooCommerce › Setup Wizard', 'woocommerce' ); ?> - - - - - - -

    <?php esc_attr_e( 'WooCommerce', 'woocommerce' ); ?>

    - step; - ?> - - - - - - - - - steps; - $selected_features = array_filter( $this->wc_setup_activate_get_feature_list() ); - - // Hide the activate step if Jetpack is already active, unless WooCommerce Services - // features are selected, or unless the Activate step was already taken. - if ( class_exists( 'Jetpack' ) && Jetpack::is_active() && empty( $selected_features ) && 'yes' !== get_transient( 'wc_setup_activated' ) ) { - unset( $output_steps['activate'] ); - } - - unset( $output_steps['new_onboarding'] ); - - ?> -
      - $step ) { - $is_completed = array_search( $this->step, array_keys( $this->steps ), true ) > array_search( $step_key, array_keys( $this->steps ), true ); - - if ( $step_key === $this->step ) { - ?> -
    1. - -
    2. - -
    3. - -
    4. - -
    - '; - if ( ! empty( $this->steps[ $this->step ]['view'] ) ) { - call_user_func( $this->steps[ $this->step ]['view'], $this ); - } - echo '
    '; - } - - /** - * Display's a prompt for users to try out the new improved WooCommerce onboarding experience in WooCommerce Admin. - * - * @deprecated 4.6.0 - */ - public function wc_setup_new_onboarding() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - ?> -
    -

    -

    <?php esc_attr_e( 'WooCommerce', 'woocommerce' ); ?>

    -

    - -
    - - -

    - -

    -
    - is_wc_admin_active() ) : ?> -

    - -
    - countries->get_base_address(); - $address_2 = WC()->countries->get_base_address_2(); - $city = WC()->countries->get_base_city(); - $state = WC()->countries->get_base_state(); - $country = WC()->countries->get_base_country(); - $postcode = WC()->countries->get_base_postcode(); - $currency = get_option( 'woocommerce_currency', 'GBP' ); - $product_type = get_option( 'woocommerce_product_type', 'both' ); - $sell_in_person = get_option( 'woocommerce_sell_in_person', 'none_selected' ); - - if ( empty( $country ) ) { - $user_location = WC_Geolocation::geolocate_ip(); - $country = $user_location['country']; - $state = $user_location['state']; - } - - $locale_info = include WC()->plugin_path() . '/i18n/locale-info.php'; - $currency_by_country = wp_list_pluck( $locale_info, 'currency_code' ); - ?> -
    - - -

    - -
    - - - - - - - - - - -
    -
    - - -
    - -
    - - -
    -
    -
    - -
    - - - -
    - -
    - - -
    - -
    - - /> - -
    - - /> - - tracking_modal(); ?> - -

    - -

    -
    - - - close_http_connection(); - foreach ( $this->deferred_actions as $action ) { - $action['func']( ...$action['args'] ); - - // Clear the background installation flag if this is a plugin. - if ( - isset( $action['func'][1] ) && - 'background_installer' === $action['func'][1] && - isset( $action['args'][0] ) - ) { - delete_option( 'woocommerce_setup_background_installing_' . $action['args'][0] ); - } - } - } - - /** - * Helper method to queue the background install of a plugin. - * - * @param string $plugin_id Plugin id used for background install. - * @param array $plugin_info Plugin info array containing name and repo-slug, and optionally file if different from [repo-slug].php. - * - * @deprecated 4.6.0 - */ - protected function install_plugin( $plugin_id, $plugin_info ) { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - // Make sure we don't trigger multiple simultaneous installs. - if ( get_option( 'woocommerce_setup_background_installing_' . $plugin_id ) ) { - return; - } - - $plugin_file = isset( $plugin_info['file'] ) ? $plugin_info['file'] : $plugin_info['repo-slug'] . '.php'; - if ( is_plugin_active( $plugin_info['repo-slug'] . '/' . $plugin_file ) ) { - return; - } - - if ( empty( $this->deferred_actions ) ) { - add_action( 'shutdown', array( $this, 'run_deferred_actions' ) ); - } - - array_push( - $this->deferred_actions, - array( - 'func' => array( 'WC_Install', 'background_installer' ), - 'args' => array( $plugin_id, $plugin_info ), - ) - ); - - // Set the background installation flag for this plugin. - update_option( 'woocommerce_setup_background_installing_' . $plugin_id, true ); - } - - - /** - * Helper method to queue the background install of a theme. - * - * @param string $theme_id Theme id used for background install. - * - * @deprecated 4.6.0 - */ - protected function install_theme( $theme_id ) { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - if ( empty( $this->deferred_actions ) ) { - add_action( 'shutdown', array( $this, 'run_deferred_actions' ) ); - } - array_push( - $this->deferred_actions, - array( - 'func' => array( 'WC_Install', 'theme_background_installer' ), - 'args' => array( $theme_id ), - ) - ); - } - - /** - * Helper method to install Jetpack. - * - * @deprecated 4.6.0 - */ - protected function install_jetpack() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - $this->install_plugin( - 'jetpack', - array( - 'name' => __( 'Jetpack', 'woocommerce' ), - 'repo-slug' => 'jetpack', - ) - ); - } - - /** - * Helper method to install WooCommerce Services and its Jetpack dependency. - * - * @deprecated 4.6.0 - */ - protected function install_woocommerce_services() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - $this->install_jetpack(); - $this->install_plugin( - 'woocommerce-services', - array( - 'name' => __( 'WooCommerce Services', 'woocommerce' ), - 'repo-slug' => 'woocommerce-services', - ) - ); - } - - /** - * Retrieve info for missing WooCommerce Services and/or Jetpack plugin. - * - * @deprecated 4.6.0 - * @return array - */ - protected function get_wcs_requisite_plugins() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - $plugins = array(); - if ( ! is_plugin_active( 'woocommerce-services/woocommerce-services.php' ) && ! get_option( 'woocommerce_setup_background_installing_woocommerce-services' ) ) { - $plugins[] = array( - 'name' => __( 'WooCommerce Services', 'woocommerce' ), - 'slug' => 'woocommerce-services', - ); - } - if ( ! is_plugin_active( 'jetpack/jetpack.php' ) && ! get_option( 'woocommerce_setup_background_installing_jetpack' ) ) { - $plugins[] = array( - 'name' => __( 'Jetpack', 'woocommerce' ), - 'slug' => 'jetpack', - ); - } - return $plugins; - } - - /** - * Plugin install info message markup with heading. - * - * @deprecated 4.6.0 - */ - public function plugin_install_info() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - ?> - - - - - array( - 'name' => __( 'Flat Rate', 'woocommerce' ), - 'description' => __( 'Set a fixed price to cover shipping costs.', 'woocommerce' ), - 'settings' => array( - 'cost' => array( - 'type' => 'text', - 'default_value' => __( 'Cost', 'woocommerce' ), - 'description' => __( 'What would you like to charge for flat rate shipping?', 'woocommerce' ), - 'required' => true, - ), - ), - ), - 'free_shipping' => array( - 'name' => __( 'Free Shipping', 'woocommerce' ), - 'description' => __( "Don't charge for shipping.", 'woocommerce' ), - ), - ); - - return $shipping_methods; - } - - /** - * Render the available shipping methods for a given country code. - * - * @param string $country_code Country code. - * @param string $currency_code Currency code. - * @param string $input_prefix Input prefix. - * - * @deprecated 4.6.0 - */ - protected function shipping_method_selection_form( $country_code, $currency_code, $input_prefix ) { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - $selected = 'flat_rate'; - $shipping_methods = $this->get_wizard_shipping_methods( $country_code, $currency_code ); - ?> -
    -
    - -
    -
    - $method ) : ?> -

    - -

    - -
    -
    - -
    - $method ) : ?> - -
    - $setting ) : ?> - - - /> -

    - -

    - -
    - -
    - - - - - - - - - countries->get_base_country(); - $country_name = WC()->countries->countries[ $country_code ]; - $prefixed_country_name = WC()->countries->estimated_for_prefix( $country_code ) . $country_name; - $currency_code = get_woocommerce_currency(); - $existing_zones = WC_Shipping_Zones::get_zones(); - $intro_text = ''; - - if ( empty( $existing_zones ) ) { - $intro_text = sprintf( - /* translators: %s: country name including the 'the' prefix if needed */ - __( "We've created two Shipping Zones - for %s and for the rest of the world. Below you can set Flat Rate shipping costs for these Zones or offer Free Shipping.", 'woocommerce' ), - $prefixed_country_name - ); - } - - $is_wcs_labels_supported = $this->is_wcs_shipping_labels_supported_country( $country_code ); - $is_shipstation_supported = $this->is_shipstation_supported_country( $country_code ); - - ?> -

    - -

    - -
    - - - - - - - - -
    -

    - get_product_weight_selection(), - $this->get_product_dimension_selection() - ), - array( - 'span' => array( - 'class' => array(), - ), - 'select' => array( - 'id' => array(), - 'name' => array(), - 'class' => array(), - ), - 'option' => array( - 'value' => array(), - 'selected' => array(), - ), - ) - ); - ?> -

    -
    - -

    - plugin_install_info(); ?> - - -

    -
    - user_email; - - return $user_email; - } - - /** - * Array of all possible "in cart" gateways that can be offered. - * - * @deprecated 4.6.0 - * @return array - */ - protected function get_wizard_available_in_cart_payment_gateways() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - $user_email = $this->get_current_user_email(); - - $stripe_description = '

    ' . sprintf( - /* translators: %s: URL */ - __( 'Accept debit and credit cards in 135+ currencies, methods such as Alipay, and one-touch checkout with Apple Pay. Learn more.', 'woocommerce' ), - 'https://woocommerce.com/products/stripe/' - ) . '

    '; - $paypal_checkout_description = '

    ' . sprintf( - /* translators: %s: URL */ - __( 'Safe and secure payments using credit cards or your customer\'s PayPal account. Learn more.', 'woocommerce' ), - 'https://woocommerce.com/products/woocommerce-gateway-paypal-checkout/' - ) . '

    '; - $klarna_checkout_description = '

    ' . sprintf( - /* translators: %s: URL */ - __( 'Full checkout experience with pay now, pay later and slice it. No credit card numbers, no passwords, no worries. Learn more about Klarna.', 'woocommerce' ), - 'https://woocommerce.com/products/klarna-checkout/' - ) . '

    '; - $klarna_payments_description = '

    ' . sprintf( - /* translators: %s: URL */ - __( 'Choose the payment that you want, pay now, pay later or slice it. No credit card numbers, no passwords, no worries. Learn more about Klarna.', 'woocommerce' ), - 'https://woocommerce.com/products/klarna-payments/ ' - ) . '

    '; - $square_description = '

    ' . sprintf( - /* translators: %s: URL */ - __( 'Securely accept credit and debit cards with one low rate, no surprise fees (custom rates available). Sell online and in store and track sales and inventory in one place. Learn more about Square.', 'woocommerce' ), - 'https://woocommerce.com/products/square/' - ) . '

    '; - - return array( - 'stripe' => array( - 'name' => __( 'WooCommerce Stripe Gateway', 'woocommerce' ), - 'image' => WC()->plugin_url() . '/assets/images/stripe.png', - 'description' => $stripe_description, - 'class' => 'checked stripe-logo', - 'repo-slug' => 'woocommerce-gateway-stripe', - 'settings' => array( - 'create_account' => array( - 'label' => __( 'Set up Stripe for me using this email:', 'woocommerce' ), - 'type' => 'checkbox', - 'value' => 'yes', - 'default' => 'yes', - 'placeholder' => '', - 'required' => false, - 'plugins' => $this->get_wcs_requisite_plugins(), - ), - 'email' => array( - 'label' => __( 'Stripe email address:', 'woocommerce' ), - 'type' => 'email', - 'value' => $user_email, - 'placeholder' => __( 'Stripe email address', 'woocommerce' ), - 'required' => true, - ), - ), - ), - 'ppec_paypal' => array( - 'name' => __( 'WooCommerce PayPal Checkout Gateway', 'woocommerce' ), - 'image' => WC()->plugin_url() . '/assets/images/paypal.png', - 'description' => $paypal_checkout_description, - 'enabled' => false, - 'class' => 'checked paypal-logo', - 'repo-slug' => 'woocommerce-gateway-paypal-express-checkout', - 'settings' => array( - 'reroute_requests' => array( - 'label' => __( 'Set up PayPal for me using this email:', 'woocommerce' ), - 'type' => 'checkbox', - 'value' => 'yes', - 'default' => 'yes', - 'placeholder' => '', - 'required' => false, - 'plugins' => $this->get_wcs_requisite_plugins(), - ), - 'email' => array( - 'label' => __( 'Direct payments to email address:', 'woocommerce' ), - 'type' => 'email', - 'value' => $user_email, - 'placeholder' => __( 'Email address to receive payments', 'woocommerce' ), - 'required' => true, - ), - ), - ), - 'paypal' => array( - 'name' => __( 'PayPal Standard', 'woocommerce' ), - 'description' => __( 'Accept payments via PayPal using account balance or credit card.', 'woocommerce' ), - 'image' => '', - 'settings' => array( - 'email' => array( - 'label' => __( 'PayPal email address:', 'woocommerce' ), - 'type' => 'email', - 'value' => $user_email, - 'placeholder' => __( 'PayPal email address', 'woocommerce' ), - 'required' => true, - ), - ), - ), - 'klarna_checkout' => array( - 'name' => __( 'Klarna Checkout for WooCommerce', 'woocommerce' ), - 'description' => $klarna_checkout_description, - 'image' => WC()->plugin_url() . '/assets/images/klarna-black.png', - 'enabled' => true, - 'class' => 'klarna-logo', - 'repo-slug' => 'klarna-checkout-for-woocommerce', - ), - 'klarna_payments' => array( - 'name' => __( 'Klarna Payments for WooCommerce', 'woocommerce' ), - 'description' => $klarna_payments_description, - 'image' => WC()->plugin_url() . '/assets/images/klarna-black.png', - 'enabled' => true, - 'class' => 'klarna-logo', - 'repo-slug' => 'klarna-payments-for-woocommerce', - ), - 'square' => array( - 'name' => __( 'WooCommerce Square', 'woocommerce' ), - 'description' => $square_description, - 'image' => WC()->plugin_url() . '/assets/images/square-black.png', - 'class' => 'square-logo', - 'enabled' => false, - 'repo-slug' => 'woocommerce-square', - ), - 'eway' => array( - 'name' => __( 'WooCommerce eWAY Gateway', 'woocommerce' ), - 'description' => __( 'The eWAY extension for WooCommerce allows you to take credit card payments directly on your store without redirecting your customers to a third party site to make payment.', 'woocommerce' ), - 'image' => WC()->plugin_url() . '/assets/images/eway-logo.jpg', - 'enabled' => false, - 'class' => 'eway-logo', - 'repo-slug' => 'woocommerce-gateway-eway', - ), - 'payfast' => array( - 'name' => __( 'WooCommerce PayFast Gateway', 'woocommerce' ), - 'description' => __( 'The PayFast extension for WooCommerce enables you to accept payments by Credit Card and EFT via one of South Africa’s most popular payment gateways. No setup fees or monthly subscription costs.', 'woocommerce' ), - 'image' => WC()->plugin_url() . '/assets/images/payfast.png', - 'class' => 'payfast-logo', - 'enabled' => false, - 'repo-slug' => 'woocommerce-payfast-gateway', - 'file' => 'gateway-payfast.php', - ), - ); - } - - /** - * Simple array of "in cart" gateways to show in wizard. - * - * @deprecated 4.6.0 - * @return array - */ - public function get_wizard_in_cart_payment_gateways() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - $gateways = $this->get_wizard_available_in_cart_payment_gateways(); - $country = WC()->countries->get_base_country(); - $currency = get_woocommerce_currency(); - - $can_stripe = $this->is_stripe_supported_country( $country ); - $can_eway = $this->is_eway_payments_supported_country( $country ); - $can_payfast = ( 'ZA' === $country ); // South Africa. - $can_paypal = $this->is_paypal_supported_currency( $currency ); - - if ( ! current_user_can( 'install_plugins' ) ) { - return $can_paypal ? array( 'paypal' => $gateways['paypal'] ) : array(); - } - - $klarna_or_square = false; - - if ( $this->is_klarna_checkout_supported_country( $country ) ) { - $klarna_or_square = 'klarna_checkout'; - } elseif ( $this->is_klarna_payments_supported_country( $country ) ) { - $klarna_or_square = 'klarna_payments'; - } elseif ( $this->is_square_supported_country( $country ) && get_option( 'woocommerce_sell_in_person' ) ) { - $klarna_or_square = 'square'; - } - - $offered_gateways = array(); - - if ( $can_stripe ) { - $gateways['stripe']['enabled'] = true; - $gateways['stripe']['featured'] = true; - $offered_gateways += array( 'stripe' => $gateways['stripe'] ); - } elseif ( $can_paypal ) { - $gateways['ppec_paypal']['enabled'] = true; - } - - if ( $klarna_or_square ) { - if ( in_array( $klarna_or_square, array( 'klarna_checkout', 'klarna_payments' ), true ) ) { - $gateways[ $klarna_or_square ]['enabled'] = true; - $gateways[ $klarna_or_square ]['featured'] = false; - $offered_gateways += array( - $klarna_or_square => $gateways[ $klarna_or_square ], - ); - } else { - $offered_gateways += array( - $klarna_or_square => $gateways[ $klarna_or_square ], - ); - } - } - - if ( $can_paypal ) { - $offered_gateways += array( 'ppec_paypal' => $gateways['ppec_paypal'] ); - } - - if ( $can_eway ) { - $offered_gateways += array( 'eway' => $gateways['eway'] ); - } - - if ( $can_payfast ) { - $offered_gateways += array( 'payfast' => $gateways['payfast'] ); - } - - return $offered_gateways; - } - - /** - * Simple array of "manual" gateways to show in wizard. - * - * @deprecated 4.6.0 - * @return array - */ - public function get_wizard_manual_payment_gateways() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - $gateways = array( - 'cheque' => array( - 'name' => _x( 'Check payments', 'Check payment method', 'woocommerce' ), - 'description' => __( 'A simple offline gateway that lets you accept a check as method of payment.', 'woocommerce' ), - 'image' => '', - 'class' => '', - ), - 'bacs' => array( - 'name' => __( 'Bank transfer (BACS) payments', 'woocommerce' ), - 'description' => __( 'A simple offline gateway that lets you accept BACS payment.', 'woocommerce' ), - 'image' => '', - 'class' => '', - ), - 'cod' => array( - 'name' => __( 'Cash on delivery', 'woocommerce' ), - 'description' => __( 'A simple offline gateway that lets you accept cash on delivery.', 'woocommerce' ), - 'image' => '', - 'class' => '', - ), - ); - - return $gateways; - } - - /** - * Display service item in list. - * - * @param int $item_id Item ID. - * @param array $item_info Item info array. - * - * @deprecated 4.6.0 - */ - public function display_service_item( $item_id, $item_info ) { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - $item_class = 'wc-wizard-service-item'; - if ( isset( $item_info['class'] ) ) { - $item_class .= ' ' . $item_info['class']; - } - - $previously_saved_settings = get_option( 'woocommerce_' . $item_id . '_settings' ); - - // Show the user-saved state if it was previously saved. - // Otherwise, rely on the item info. - if ( is_array( $previously_saved_settings ) ) { - $should_enable_toggle = ( isset( $previously_saved_settings['enabled'] ) && 'yes' === $previously_saved_settings['enabled'] ) ? true : ( isset( $item_info['enabled'] ) && $item_info['enabled'] ); - } else { - $should_enable_toggle = isset( $item_info['enabled'] ) && $item_info['enabled']; - } - - $plugins = null; - if ( isset( $item_info['repo-slug'] ) ) { - $plugin = array( - 'slug' => $item_info['repo-slug'], - 'name' => $item_info['name'], - ); - $plugins = array( $plugin ); - } - - ?> -
  • -
    - - <?php echo esc_attr( $item_info['name'] ); ?> - -

    - -
    -
    - - - data-plugins="" - /> - -
    -
    - - -
    - $setting ) : ?> - - -
    - - - - data-plugins="" - /> - - - -
    - -
    - -
    -
  • - is_featured_service( $service ); - } - - /** - * Payment Step. - * - * @deprecated 4.6.0 - */ - public function wc_setup_payment() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - $featured_gateways = array_filter( $this->get_wizard_in_cart_payment_gateways(), array( $this, 'is_featured_service' ) ); - $in_cart_gateways = array_filter( $this->get_wizard_in_cart_payment_gateways(), array( $this, 'is_not_featured_service' ) ); - $manual_gateways = $this->get_wizard_manual_payment_gateways(); - ?> -

    -
    -

    - Additional payment methods can be installed later.', 'woocommerce' ), - array( - 'a' => array( - 'href' => array(), - 'target' => array(), - ), - ) - ), - esc_url( admin_url( 'admin.php?page=wc-addons§ion=payment-gateways' ) ) - ); - ?> -

    - - - - - - - -

    - plugin_install_info(); ?> - - -

    -
    - - - -

    -

    - -

    -
    - -

    - plugin_install_info(); ?> - - -

    -
    - get_next_step_link() ) ) ); - exit; - } - } - - /** - * - * @deprecated 4.6.0 - */ - protected function wc_setup_activate_get_feature_list() { - $features = array(); - - $stripe_settings = get_option( 'woocommerce_stripe_settings', false ); - $stripe_enabled = is_array( $stripe_settings ) - && isset( $stripe_settings['create_account'] ) && 'yes' === $stripe_settings['create_account'] - && isset( $stripe_settings['enabled'] ) && 'yes' === $stripe_settings['enabled']; - $ppec_settings = get_option( 'woocommerce_ppec_paypal_settings', false ); - $ppec_enabled = is_array( $ppec_settings ) - && isset( $ppec_settings['reroute_requests'] ) && 'yes' === $ppec_settings['reroute_requests'] - && isset( $ppec_settings['enabled'] ) && 'yes' === $ppec_settings['enabled']; - - $features['payment'] = $stripe_enabled || $ppec_enabled; - $features['taxes'] = (bool) get_option( 'woocommerce_setup_automated_taxes', false ); - $features['labels'] = (bool) get_option( 'woocommerce_setup_shipping_labels', false ); - - return $features; - } - - /** - * - * @deprecated 4.6.0 - */ - protected function wc_setup_activate_get_feature_list_str() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - $features = $this->wc_setup_activate_get_feature_list(); - if ( $features['payment'] && $features['taxes'] && $features['labels'] ) { - return __( 'payment setup, automated taxes and discounted shipping labels', 'woocommerce' ); - } else if ( $features['payment'] && $features['taxes'] ) { - return __( 'payment setup and automated taxes', 'woocommerce' ); - } else if ( $features['payment'] && $features['labels'] ) { - return __( 'payment setup and discounted shipping labels', 'woocommerce' ); - } else if ( $features['payment'] ) { - return __( 'payment setup', 'woocommerce' ); - } else if ( $features['taxes'] && $features['labels'] ) { - return __( 'automated taxes and discounted shipping labels', 'woocommerce' ); - } else if ( $features['taxes'] ) { - return __( 'automated taxes', 'woocommerce' ); - } else if ( $features['labels'] ) { - return __( 'discounted shipping labels', 'woocommerce' ); - } - return false; - } - - /** - * Activate step. - * - * @deprecated 4.6.0 - */ - public function wc_setup_activate() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - $this->wc_setup_activate_actions(); - - $jetpack_connected = class_exists( 'Jetpack' ) && Jetpack::is_active(); - - $has_jetpack_error = false; - if ( isset( $_GET['activate_error'] ) ) { - $has_jetpack_error = true; - - $title = __( "Sorry, we couldn't connect your store to Jetpack", 'woocommerce' ); - - $error_message = $this->get_activate_error_message( sanitize_text_field( wp_unslash( $_GET['activate_error'] ) ) ); - $description = $error_message; - } else { - $feature_list = $this->wc_setup_activate_get_feature_list_str(); - - $description = false; - - if ( $feature_list ) { - if ( ! $jetpack_connected ) { - /* translators: %s: list of features, potentially comma separated */ - $description_base = __( 'Your store is almost ready! To activate services like %s, just connect with Jetpack.', 'woocommerce' ); - } else { - $description_base = __( 'Thanks for using Jetpack! Your store is almost ready: to activate services like %s, just connect your store.', 'woocommerce' ); - } - $description = sprintf( $description_base, $feature_list ); - } - - if ( ! $jetpack_connected ) { - $title = $feature_list ? - __( 'Connect your store to Jetpack', 'woocommerce' ) : - __( 'Connect your store to Jetpack to enable extra features', 'woocommerce' ); - $button_text = __( 'Continue with Jetpack', 'woocommerce' ); - } elseif ( $feature_list ) { - $title = __( 'Connect your store to activate WooCommerce Services', 'woocommerce' ); - $button_text = __( 'Continue with WooCommerce Services', 'woocommerce' ); - } else { - wp_redirect( esc_url_raw( $this->get_next_step_link() ) ); - exit; - } - } - ?> -

    -

    - - -
    - - -
    - - - - - -

    - - - -

    - -

    - Terms of Service and to share details with WordPress.com', 'woocommerce' ) ), - 'https://wordpress.com/tos', - 'https://jetpack.com/support/what-data-does-jetpack-sync' - ); - ?> -

    -
    -

    - -

    - - -
    - -

    - -

    - - - - __( "Sorry! We tried, but we couldn't connect Jetpack just now 😭. Please go to the Plugins tab to connect Jetpack, so that you can finish setting up your store.", 'woocommerce' ), - 'jetpack_cant_be_installed' => __( "Sorry! We tried, but we couldn't install Jetpack for you 😭. Please go to the Plugins tab to install it, and finish setting up your store.", 'woocommerce' ), - 'register_http_request_failed' => __( "Sorry! We couldn't contact Jetpack just now 😭. Please make sure that your site is visible over the internet, and that it accepts incoming and outgoing requests via curl. You can also try to connect to Jetpack again, and if you run into any more issues, please contact support.", 'woocommerce' ), - 'siteurl_private_ip_dev' => __( "Your site might be on a private network. Jetpack can only connect to public sites. Please make sure your site is visible over the internet, and then try connecting again 🙏." , 'woocommerce' ), - ); - } - - /** - * - * @deprecated 4.6.0 - */ - protected function get_activate_error_message( $code = '' ) { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - $errors = $this->get_all_activate_errors(); - return array_key_exists( $code, $errors ) ? $errors[ $code ] : $errors['default']; - } - - /** - * Activate step save. - * - * Install, activate, and launch connection flow for Jetpack. - * - * @deprecated 4.6.0 - */ - public function wc_setup_activate_save() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - } - - /** - * Final step. - * - * @deprecated 4.6.0 - */ - public function wc_setup_ready() { - _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); - // We've made it! Don't prompt the user to run the wizard again. - WC_Admin_Notices::remove_notice( 'install', true ); - - $user_email = $this->get_current_user_email(); - $docs_url = 'https://docs.woocommerce.com/documentation/plugins/woocommerce/getting-started/?utm_source=setupwizard&utm_medium=product&utm_content=docs&utm_campaign=woocommerceplugin'; - $help_text = sprintf( - /* translators: %1$s: link to docs */ - __( 'Visit WooCommerce.com to learn more about getting started.', 'woocommerce' ), - $docs_url - ); - ?> -

    - -
    -

    -
    - -
    -
    - - -

    - execute_tool( $action ); - - $tool = $tools[ $action ]; - $tool = array( - 'id' => $action, - 'name' => $tool['name'], - 'action' => $tool['button'], - 'description' => $tool['desc'], - ); - $tool = array_merge( $tool, $response ); - - /** - * Fires after a WooCommerce system status tool has been executed. - * - * @param array $tool Details about the tool that has been executed. - */ - do_action( 'woocommerce_system_status_tool_executed', $tool ); - } else { - $response = array( - 'success' => false, - 'message' => __( 'Tool does not exist.', 'woocommerce' ), - ); - } - - if ( $response['success'] ) { - echo '

    ' . esc_html( $response['message'] ) . '

    '; - } else { - echo '

    ' . esc_html( $response['message'] ) . '

    '; - } - } - - // Display message if settings settings have been saved. - if ( isset( $_REQUEST['settings-updated'] ) ) { // WPCS: input var ok. - echo '

    ' . esc_html__( 'Your changes have been saved.', 'woocommerce' ) . '

    '; - } - - include_once __DIR__ . '/views/html-admin-page-status-tools.php'; - } - - /** - * Get tools. - * - * @return array of tools - */ - public static function get_tools() { - $tools_controller = new WC_REST_System_Status_Tools_Controller(); - return $tools_controller->get_tools(); - } - - /** - * Show the logs page. - */ - public static function status_logs() { - $log_handler = Constants::get_constant( 'WC_LOG_HANDLER' ); - - if ( 'WC_Log_Handler_DB' === $log_handler ) { - self::status_logs_db(); - } else { - self::status_logs_file(); - } - } - - /** - * Show the log page contents for file log handler. - */ - public static function status_logs_file() { - $logs = self::scan_log_files(); - - if ( ! empty( $_REQUEST['log_file'] ) && isset( $logs[ sanitize_title( wp_unslash( $_REQUEST['log_file'] ) ) ] ) ) { // WPCS: input var ok, CSRF ok. - $viewed_log = $logs[ sanitize_title( wp_unslash( $_REQUEST['log_file'] ) ) ]; // WPCS: input var ok, CSRF ok. - } elseif ( ! empty( $logs ) ) { - $viewed_log = current( $logs ); - } - - $handle = ! empty( $viewed_log ) ? self::get_log_file_handle( $viewed_log ) : ''; - - if ( ! empty( $_REQUEST['handle'] ) ) { // WPCS: input var ok, CSRF ok. - self::remove_log(); - } - - include_once __DIR__ . '/views/html-admin-page-status-logs.php'; - } - - /** - * Show the log page contents for db log handler. - */ - public static function status_logs_db() { - if ( ! empty( $_REQUEST['flush-logs'] ) ) { // WPCS: input var ok, CSRF ok. - self::flush_db_logs(); - } - - if ( isset( $_REQUEST['action'] ) && isset( $_REQUEST['log'] ) ) { // WPCS: input var ok, CSRF ok. - self::log_table_bulk_actions(); - } - - $log_table_list = new WC_Admin_Log_Table_List(); - $log_table_list->prepare_items(); - - include_once __DIR__ . '/views/html-admin-page-status-logs-db.php'; - } - - /** - * Retrieve metadata from a file. Based on WP Core's get_file_data function. - * - * @since 2.1.1 - * @param string $file Path to the file. - * @return string - */ - public static function get_file_version( $file ) { - - // Avoid notices if file does not exist. - if ( ! file_exists( $file ) ) { - return ''; - } - - // We don't need to write to the file, so just open for reading. - $fp = fopen( $file, 'r' ); // @codingStandardsIgnoreLine. - - // Pull only the first 8kiB of the file in. - $file_data = fread( $fp, 8192 ); // @codingStandardsIgnoreLine. - - // PHP will close file handle, but we are good citizens. - fclose( $fp ); // @codingStandardsIgnoreLine. - - // Make sure we catch CR-only line endings. - $file_data = str_replace( "\r", "\n", $file_data ); - $version = ''; - - if ( preg_match( '/^[ \t\/*#@]*' . preg_quote( '@version', '/' ) . '(.*)$/mi', $file_data, $match ) && $match[1] ) { - $version = _cleanup_header_comment( $match[1] ); - } - - return $version; - } - - /** - * Return the log file handle. - * - * @param string $filename Filename to get the handle for. - * @return string - */ - public static function get_log_file_handle( $filename ) { - return substr( $filename, 0, strlen( $filename ) > 48 ? strlen( $filename ) - 48 : strlen( $filename ) - 4 ); - } - - /** - * Scan the template files. - * - * @param string $template_path Path to the template directory. - * @return array - */ - public static function scan_template_files( $template_path ) { - $files = @scandir( $template_path ); // @codingStandardsIgnoreLine. - $result = array(); - - if ( ! empty( $files ) ) { - - foreach ( $files as $key => $value ) { - - if ( ! in_array( $value, array( '.', '..' ), true ) ) { - - if ( is_dir( $template_path . DIRECTORY_SEPARATOR . $value ) ) { - $sub_files = self::scan_template_files( $template_path . DIRECTORY_SEPARATOR . $value ); - foreach ( $sub_files as $sub_file ) { - $result[] = $value . DIRECTORY_SEPARATOR . $sub_file; - } - } else { - $result[] = $value; - } - } - } - } - return $result; - } - - /** - * Scan the log files. - * - * @return array - */ - public static function scan_log_files() { - return WC_Log_Handler_File::get_log_files(); - } - - /** - * Get latest version of a theme by slug. - * - * @param object $theme WP_Theme object. - * @return string Version number if found. - */ - public static function get_latest_theme_version( $theme ) { - include_once ABSPATH . 'wp-admin/includes/theme.php'; - - $api = themes_api( - 'theme_information', - array( - 'slug' => $theme->get_stylesheet(), - 'fields' => array( - 'sections' => false, - 'tags' => false, - ), - ) - ); - - $update_theme_version = 0; - - // Check .org for updates. - if ( is_object( $api ) && ! is_wp_error( $api ) ) { - $update_theme_version = $api->version; - } elseif ( strstr( $theme->{'Author URI'}, 'woothemes' ) ) { // Check WooThemes Theme Version. - $theme_dir = substr( strtolower( str_replace( ' ', '', $theme->Name ) ), 0, 45 ); // @codingStandardsIgnoreLine. - $theme_version_data = get_transient( $theme_dir . '_version_data' ); - - if ( false === $theme_version_data ) { - $theme_changelog = wp_safe_remote_get( 'http://dzv365zjfbd8v.cloudfront.net/changelogs/' . $theme_dir . '/changelog.txt' ); - $cl_lines = explode( "\n", wp_remote_retrieve_body( $theme_changelog ) ); - if ( ! empty( $cl_lines ) ) { - foreach ( $cl_lines as $line_num => $cl_line ) { - if ( preg_match( '/^[0-9]/', $cl_line ) ) { - $theme_date = str_replace( '.', '-', trim( substr( $cl_line, 0, strpos( $cl_line, '-' ) ) ) ); - $theme_version = preg_replace( '~[^0-9,.]~', '', stristr( $cl_line, 'version' ) ); - $theme_update = trim( str_replace( '*', '', $cl_lines[ $line_num + 1 ] ) ); - $theme_version_data = array( - 'date' => $theme_date, - 'version' => $theme_version, - 'update' => $theme_update, - 'changelog' => $theme_changelog, - ); - set_transient( $theme_dir . '_version_data', $theme_version_data, DAY_IN_SECONDS ); - break; - } - } - } - } - - if ( ! empty( $theme_version_data['version'] ) ) { - $update_theme_version = $theme_version_data['version']; - } - } - - return $update_theme_version; - } - - /** - * Remove/delete the chosen file. - */ - public static function remove_log() { - if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'remove_log' ) ) { // WPCS: input var ok, sanitization ok. - wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); - } - - if ( ! empty( $_REQUEST['handle'] ) ) { // WPCS: input var ok. - $log_handler = new WC_Log_Handler_File(); - $log_handler->remove( wp_unslash( $_REQUEST['handle'] ) ); // WPCS: input var ok, sanitization ok. - } - - wp_safe_redirect( esc_url_raw( admin_url( 'admin.php?page=wc-status&tab=logs' ) ) ); - exit(); - } - - /** - * Clear DB log table. - * - * @since 3.0.0 - */ - private static function flush_db_logs() { - if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'woocommerce-status-logs' ) ) { // WPCS: input var ok, sanitization ok. - wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); - } - - WC_Log_Handler_DB::flush(); - - wp_safe_redirect( esc_url_raw( admin_url( 'admin.php?page=wc-status&tab=logs' ) ) ); - exit(); - } - - /** - * Bulk DB log table actions. - * - * @since 3.0.0 - */ - private static function log_table_bulk_actions() { - if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'woocommerce-status-logs' ) ) { // WPCS: input var ok, sanitization ok. - wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); - } - - $log_ids = array_map( 'absint', (array) isset( $_REQUEST['log'] ) ? wp_unslash( $_REQUEST['log'] ) : array() ); // WPCS: input var ok, sanitization ok. - - if ( ( isset( $_REQUEST['action'] ) && 'delete' === $_REQUEST['action'] ) || ( isset( $_REQUEST['action2'] ) && 'delete' === $_REQUEST['action2'] ) ) { // WPCS: input var ok, sanitization ok. - WC_Log_Handler_DB::delete( $log_ids ); - wp_safe_redirect( esc_url_raw( admin_url( 'admin.php?page=wc-status&tab=logs' ) ) ); - exit(); - } - } - - /** - * Prints table info if a base table is not present. - */ - private static function output_tables_info() { - $missing_tables = WC_Install::verify_base_tables( false ); - if ( 0 === count( $missing_tables ) ) { - return; - } - ?> - -
    - - - - - - ' . $plugin_name . ''; - } - - $has_newer_version = false; - $version_string = $plugin['version']; - $network_string = ''; - if ( strstr( $plugin['url'], 'woothemes.com' ) || strstr( $plugin['url'], 'woocommerce.com' ) ) { - if ( ! empty( $plugin['version_latest'] ) && version_compare( $plugin['version_latest'], $plugin['version'], '>' ) ) { - /* translators: 1: current version. 2: latest version */ - $version_string = sprintf( __( '%1$s (update to version %2$s is available)', 'woocommerce' ), $plugin['version'], $plugin['version_latest'] ); - } - - if ( false !== $plugin['network_activated'] ) { - $network_string = ' – ' . esc_html__( 'Network enabled', 'woocommerce' ) . ''; - } - } - $untested_string = ''; - if ( array_key_exists( $plugin['plugin'], $untested_plugins ) ) { - $untested_string = ' – '; - - /* translators: %s: version */ - $untested_string .= esc_html( sprintf( __( 'Installed version not tested with active version of WooCommerce %s', 'woocommerce' ), $wc_version ) ); - - $untested_string .= ''; - } - ?> - - -   - - - - - default_cat_id = get_option( 'default_product_cat', 0 ); - - // Category/term ordering. - add_action( 'create_term', array( $this, 'create_term' ), 5, 3 ); - - // Add form. - add_action( 'product_cat_add_form_fields', array( $this, 'add_category_fields' ) ); - add_action( 'product_cat_edit_form_fields', array( $this, 'edit_category_fields' ), 10 ); - add_action( 'created_term', array( $this, 'save_category_fields' ), 10, 3 ); - add_action( 'edit_term', array( $this, 'save_category_fields' ), 10, 3 ); - - // Add columns. - add_filter( 'manage_edit-product_cat_columns', array( $this, 'product_cat_columns' ) ); - add_filter( 'manage_product_cat_custom_column', array( $this, 'product_cat_column' ), 10, 3 ); - - // Add row actions. - add_filter( 'product_cat_row_actions', array( $this, 'product_cat_row_actions' ), 10, 2 ); - add_filter( 'admin_init', array( $this, 'handle_product_cat_row_actions' ) ); - - // Taxonomy page descriptions. - add_action( 'product_cat_pre_add_form', array( $this, 'product_cat_description' ) ); - add_action( 'after-product_cat-table', array( $this, 'product_cat_notes' ) ); - - $attribute_taxonomies = wc_get_attribute_taxonomies(); - - if ( ! empty( $attribute_taxonomies ) ) { - foreach ( $attribute_taxonomies as $attribute ) { - add_action( 'pa_' . $attribute->attribute_name . '_pre_add_form', array( $this, 'product_attribute_description' ) ); - } - } - - // Maintain hierarchy of terms. - add_filter( 'wp_terms_checklist_args', array( $this, 'disable_checked_ontop' ) ); - - // Admin footer scripts for this product categories admin screen. - add_action( 'admin_footer', array( $this, 'scripts_at_product_cat_screen_footer' ) ); - } - - /** - * Order term when created (put in position 0). - * - * @param mixed $term_id Term ID. - * @param mixed $tt_id Term taxonomy ID. - * @param string $taxonomy Taxonomy slug. - */ - public function create_term( $term_id, $tt_id = '', $taxonomy = '' ) { - if ( 'product_cat' != $taxonomy && ! taxonomy_is_product_attribute( $taxonomy ) ) { - return; - } - - $meta_name = taxonomy_is_product_attribute( $taxonomy ) ? 'order_' . esc_attr( $taxonomy ) : 'order'; - - update_term_meta( $term_id, $meta_name, 0 ); - } - - /** - * When a term is deleted, delete its meta. - * - * @deprecated 3.6.0 No longer needed. - * @param mixed $term_id Term ID. - */ - public function delete_term( $term_id ) { - wc_deprecated_function( 'delete_term', '3.6' ); - } - - /** - * Category thumbnail fields. - */ - public function add_category_fields() { - ?> -
    - - -
    -
    - -
    -
    - - - -
    - -
    -
    - term_id, 'display_type', true ); - $thumbnail_id = absint( get_term_meta( $term->term_id, 'thumbnail_id', true ) ); - - if ( $thumbnail_id ) { - $image = wp_get_attachment_thumb_url( $thumbnail_id ); - } else { - $image = wc_placeholder_img_src(); - } - ?> - - - - - - - - - -
    -
    - - - -
    - -
    - - - array() ) - ); - } - - /** - * Add some notes to describe the behavior of the default category. - */ - public function product_cat_notes() { - $category_id = get_option( 'default_product_cat', 0 ); - $category = get_term( $category_id, 'product_cat' ); - $category_name = ( ! $category || is_wp_error( $category ) ) ? _x( 'Uncategorized', 'Default category slug', 'woocommerce' ) : $category->name; - ?> -
    -

    -
    - ' . esc_html( $category_name ) . '' - ); - ?> -

    -
    -
    Note: Deleting a term will remove it from all products and variations to which it has been assigned. Recreating a term will not automatically assign it back to products.', 'woocommerce' ) ), - array( 'p' => array() ) - ); - } - - /** - * Thumbnail column added to category admin. - * - * @param mixed $columns Columns array. - * @return array - */ - public function product_cat_columns( $columns ) { - $new_columns = array(); - - if ( isset( $columns['cb'] ) ) { - $new_columns['cb'] = $columns['cb']; - unset( $columns['cb'] ); - } - - $new_columns['thumb'] = __( 'Image', 'woocommerce' ); - - $columns = array_merge( $new_columns, $columns ); - $columns['handle'] = ''; - - return $columns; - } - - /** - * Adjust row actions. - * - * @param array $actions Array of actions. - * @param object $term Term object. - * @return array - */ - public function product_cat_row_actions( $actions, $term ) { - $default_category_id = absint( get_option( 'default_product_cat', 0 ) ); - - if ( $default_category_id !== $term->term_id && current_user_can( 'edit_term', $term->term_id ) ) { - $actions['make_default'] = sprintf( - '%s', - wp_nonce_url( 'edit-tags.php?action=make_default&taxonomy=product_cat&post_type=product&tag_ID=' . absint( $term->term_id ), 'make_default_' . absint( $term->term_id ) ), - /* translators: %s: taxonomy term name */ - esc_attr( sprintf( __( 'Make “%s” the default category', 'woocommerce' ), $term->name ) ), - __( 'Make default', 'woocommerce' ) - ); - } - - return $actions; - } - - /** - * Handle custom row actions. - */ - public function handle_product_cat_row_actions() { - if ( isset( $_GET['action'], $_GET['tag_ID'], $_GET['_wpnonce'] ) && 'make_default' === $_GET['action'] ) { // WPCS: CSRF ok, input var ok. - $make_default_id = absint( $_GET['tag_ID'] ); // WPCS: Input var ok. - - if ( wp_verify_nonce( $_GET['_wpnonce'], 'make_default_' . $make_default_id ) && current_user_can( 'edit_term', $make_default_id ) ) { // WPCS: Sanitization ok, input var ok, CSRF ok. - update_option( 'default_product_cat', $make_default_id ); - } - } - } - - /** - * Thumbnail column value added to category admin. - * - * @param string $columns Column HTML output. - * @param string $column Column name. - * @param int $id Product ID. - * - * @return string - */ - public function product_cat_column( $columns, $column, $id ) { - if ( 'thumb' === $column ) { - // Prepend tooltip for default category. - $default_category_id = absint( get_option( 'default_product_cat', 0 ) ); - - if ( $default_category_id === $id ) { - $columns .= wc_help_tip( __( 'This is the default category and it cannot be deleted. It will be automatically assigned to products with no category.', 'woocommerce' ) ); - } - - $thumbnail_id = get_term_meta( $id, 'thumbnail_id', true ); - - if ( $thumbnail_id ) { - $image = wp_get_attachment_thumb_url( $thumbnail_id ); - } else { - $image = wc_placeholder_img_src(); - } - - // Prevent esc_url from breaking spaces in urls for image embeds. Ref: https://core.trac.wordpress.org/ticket/23605 . - $image = str_replace( ' ', '%20', $image ); - $columns .= '' . esc_attr__( 'Thumbnail', 'woocommerce' ) . ''; - } - if ( 'handle' === $column ) { - $columns .= ''; - } - return $columns; - } - - /** - * Maintain term hierarchy when editing a product. - * - * @param array $args Term checklist args. - * @return array - */ - public function disable_checked_ontop( $args ) { - if ( ! empty( $args['taxonomy'] ) && 'product_cat' === $args['taxonomy'] ) { - $args['checked_ontop'] = false; - } - return $args; - } - - /** - * Admin footer scripts for the product categories admin screen - * - * @return void - */ - public function scripts_at_product_cat_screen_footer() { - if ( ! isset( $_GET['taxonomy'] ) || 'product_cat' !== $_GET['taxonomy'] ) { // WPCS: CSRF ok, input var ok. - return; - } - // Ensure the tooltip is displayed when the image column is disabled on product categories. - wc_enqueue_js( - "(function( $ ) { - 'use strict'; - var product_cat = $( 'tr#tag-" . absint( $this->default_cat_id ) . "' ); - product_cat.find( 'th' ).empty(); - product_cat.find( 'td.thumb span' ).detach( 'span' ).appendTo( product_cat.find( 'th' ) ); - })( jQuery );" - ); - } -} - -$wc_admin_taxonomies = WC_Admin_Taxonomies::get_instance(); diff --git a/includes/admin/class-wc-admin-webhooks-table-list.php b/includes/admin/class-wc-admin-webhooks-table-list.php deleted file mode 100644 index d2c6ea5bbc1..00000000000 --- a/includes/admin/class-wc-admin-webhooks-table-list.php +++ /dev/null @@ -1,316 +0,0 @@ - 'webhook', - 'plural' => 'webhooks', - 'ajax' => false, - ) - ); - } - - /** - * No items found text. - */ - public function no_items() { - esc_html_e( 'No webhooks found.', 'woocommerce' ); - } - - /** - * Get list columns. - * - * @return array - */ - public function get_columns() { - return array( - 'cb' => '', - 'title' => __( 'Name', 'woocommerce' ), - 'status' => __( 'Status', 'woocommerce' ), - 'topic' => __( 'Topic', 'woocommerce' ), - 'delivery_url' => __( 'Delivery URL', 'woocommerce' ), - ); - } - - /** - * Column cb. - * - * @param WC_Webhook $webhook Webhook instance. - * @return string - */ - public function column_cb( $webhook ) { - return sprintf( '', $this->_args['singular'], $webhook->get_id() ); - } - - /** - * Return title column. - * - * @param WC_Webhook $webhook Webhook instance. - * @return string - */ - public function column_title( $webhook ) { - $edit_link = admin_url( 'admin.php?page=wc-settings&tab=advanced&section=webhooks&edit-webhook=' . $webhook->get_id() ); - $output = ''; - - // Title. - $output .= '' . esc_html( $webhook->get_name() ) . ''; - - // Get actions. - $actions = array( - /* translators: %s: webhook ID. */ - 'id' => sprintf( __( 'ID: %d', 'woocommerce' ), $webhook->get_id() ), - 'edit' => '' . esc_html__( 'Edit', 'woocommerce' ) . '', - /* translators: %s: webhook name */ - 'delete' => 'get_name() ) ) . '" href="' . esc_url( - wp_nonce_url( - add_query_arg( - array( - 'delete' => $webhook->get_id(), - ), - admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks' ) - ), - 'delete-webhook' - ) - ) . '">' . esc_html__( 'Delete permanently', 'woocommerce' ) . '', - ); - - $actions = apply_filters( 'webhook_row_actions', $actions, $webhook ); - $row_actions = array(); - - foreach ( $actions as $action => $link ) { - $row_actions[] = '' . $link . ''; - } - - $output .= '
    ' . implode( ' | ', $row_actions ) . '
    '; - - return $output; - } - - /** - * Return status column. - * - * @param WC_Webhook $webhook Webhook instance. - * @return string - */ - public function column_status( $webhook ) { - return $webhook->get_i18n_status(); - } - - /** - * Return topic column. - * - * @param WC_Webhook $webhook Webhook instance. - * @return string - */ - public function column_topic( $webhook ) { - return $webhook->get_topic(); - } - - /** - * Return delivery URL column. - * - * @param WC_Webhook $webhook Webhook instance. - * @return string - */ - public function column_delivery_url( $webhook ) { - return $webhook->get_delivery_url(); - } - - /** - * Get the status label for webhooks. - * - * @param string $status_name Status name. - * @param int $amount Amount of webhooks. - * @return array - */ - private function get_status_label( $status_name, $amount ) { - $statuses = wc_get_webhook_statuses(); - - if ( isset( $statuses[ $status_name ] ) ) { - return array( - 'singular' => sprintf( '%s (%s)', esc_html( $statuses[ $status_name ] ), $amount ), - 'plural' => sprintf( '%s (%s)', esc_html( $statuses[ $status_name ] ), $amount ), - 'context' => '', - 'domain' => 'woocommerce', - ); - } - - return array( - 'singular' => sprintf( '%s (%s)', esc_html( $status_name ), $amount ), - 'plural' => sprintf( '%s (%s)', esc_html( $status_name ), $amount ), - 'context' => '', - 'domain' => 'woocommerce', - ); - } - - /** - * Table list views. - * - * @return array - */ - protected function get_views() { - $status_links = array(); - $data_store = WC_Data_Store::load( 'webhook' ); - $num_webhooks = $data_store->get_count_webhooks_by_status(); - $total_webhooks = array_sum( (array) $num_webhooks ); - $statuses = array_keys( wc_get_webhook_statuses() ); - $class = empty( $_REQUEST['status'] ) ? ' class="current"' : ''; // WPCS: input var okay. CSRF ok. - - /* translators: %s: count */ - $status_links['all'] = "" . sprintf( _nx( 'All (%s)', 'All (%s)', $total_webhooks, 'posts', 'woocommerce' ), number_format_i18n( $total_webhooks ) ) . ''; - - foreach ( $statuses as $status_name ) { - $class = ''; - - if ( empty( $num_webhooks[ $status_name ] ) ) { - continue; - } - - if ( isset( $_REQUEST['status'] ) && sanitize_key( wp_unslash( $_REQUEST['status'] ) ) === $status_name ) { // WPCS: input var okay, CSRF ok. - $class = ' class="current"'; - } - - $label = $this->get_status_label( $status_name, $num_webhooks[ $status_name ] ); - - $status_links[ $status_name ] = "" . sprintf( translate_nooped_plural( $label, $num_webhooks[ $status_name ] ), number_format_i18n( $num_webhooks[ $status_name ] ) ) . ''; - } - - return $status_links; - } - - /** - * Get bulk actions. - * - * @return array - */ - protected function get_bulk_actions() { - return array( - 'delete' => __( 'Delete permanently', 'woocommerce' ), - ); - } - - /** - * Process bulk actions. - */ - public function process_bulk_action() { - $action = $this->current_action(); - $webhooks = isset( $_REQUEST['webhook'] ) ? array_map( 'absint', (array) $_REQUEST['webhook'] ) : array(); // WPCS: input var okay, CSRF ok. - - if ( ! current_user_can( 'manage_woocommerce' ) ) { - wp_die( esc_html__( 'You do not have permission to edit Webhooks', 'woocommerce' ) ); - } - - if ( 'delete' === $action ) { - WC_Admin_Webhooks::bulk_delete( $webhooks ); - } - } - - /** - * Generate the table navigation above or below the table. - * Included to remove extra nonce input. - * - * @param string $which The location of the extra table nav markup: 'top' or 'bottom'. - */ - protected function display_tablenav( $which ) { - echo '
    '; - - if ( $this->has_items() ) { - echo '
    '; - $this->bulk_actions( $which ); - echo '
    '; - } - - $this->extra_tablenav( $which ); - $this->pagination( $which ); - echo '
    '; - echo '
    '; - } - - /** - * Search box. - * - * @param string $text Button text. - * @param string $input_id Input ID. - */ - public function search_box( $text, $input_id ) { - if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) { // WPCS: input var okay, CSRF ok. - return; - } - - $input_id = $input_id . '-search-input'; - $search_query = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : ''; // WPCS: input var okay, CSRF ok. - - echo ''; - } - - /** - * Prepare table list items. - */ - public function prepare_items() { - $per_page = $this->get_items_per_page( 'woocommerce_webhooks_per_page' ); - $current_page = $this->get_pagenum(); - - // Query args. - $args = array( - 'limit' => $per_page, - 'offset' => $per_page * ( $current_page - 1 ), - ); - - // Handle the status query. - if ( ! empty( $_REQUEST['status'] ) ) { // WPCS: input var okay, CSRF ok. - $args['status'] = sanitize_key( wp_unslash( $_REQUEST['status'] ) ); // WPCS: input var okay, CSRF ok. - } - - if ( ! empty( $_REQUEST['s'] ) ) { // WPCS: input var okay, CSRF ok. - $args['search'] = sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ); // WPCS: input var okay, CSRF ok. - } - - $args['paginate'] = true; - - // Get the webhooks. - $data_store = WC_Data_Store::load( 'webhook' ); - $webhooks = $data_store->search_webhooks( $args ); - $this->items = array_map( 'wc_get_webhook', $webhooks->webhooks ); - - // Set the pagination. - $this->set_pagination_args( - array( - 'total_items' => $webhooks->total, - 'per_page' => $per_page, - 'total_pages' => $webhooks->max_num_pages, - ) - ); - } -} diff --git a/includes/admin/class-wc-admin.php b/includes/admin/class-wc-admin.php deleted file mode 100644 index e136974d539..00000000000 --- a/includes/admin/class-wc-admin.php +++ /dev/null @@ -1,311 +0,0 @@ -id ) { - case 'dashboard': - case 'dashboard-network': - include __DIR__ . '/class-wc-admin-dashboard.php'; - break; - case 'options-permalink': - include __DIR__ . '/class-wc-admin-permalink-settings.php'; - break; - case 'plugins': - include __DIR__ . '/plugin-updates/class-wc-plugins-screen-updates.php'; - break; - case 'update-core': - include __DIR__ . '/plugin-updates/class-wc-updates-screen-updates.php'; - break; - case 'users': - case 'user': - case 'profile': - case 'user-edit': - include __DIR__ . '/class-wc-admin-profile.php'; - break; - } - } - - /** - * Handle redirects to setup/welcome page after install and updates. - * - * The user must have access rights, and we must ignore the network/bulk plugin updaters. - */ - public function admin_redirects() { - // Don't run this fn from Action Scheduler requests, as it would clear _wc_activation_redirect transient. - // That means OBW would never be shown. - if ( wc_is_running_from_async_action_scheduler() ) { - return; - } - - // phpcs:disable WordPress.Security.NonceVerification.Recommended - // Nonced plugin install redirects. - if ( ! empty( $_GET['wc-install-plugin-redirect'] ) ) { - $plugin_slug = wc_clean( wp_unslash( $_GET['wc-install-plugin-redirect'] ) ); - - if ( current_user_can( 'install_plugins' ) && in_array( $plugin_slug, array( 'woocommerce-gateway-stripe' ), true ) ) { - $nonce = wp_create_nonce( 'install-plugin_' . $plugin_slug ); - $url = self_admin_url( 'update.php?action=install-plugin&plugin=' . $plugin_slug . '&_wpnonce=' . $nonce ); - } else { - $url = admin_url( 'plugin-install.php?tab=search&type=term&s=' . $plugin_slug ); - } - - wp_safe_redirect( $url ); - exit; - } - - // phpcs:enable WordPress.Security.NonceVerification.Recommended - } - - /** - * Prevent any user who cannot 'edit_posts' (subscribers, customers etc) from accessing admin. - */ - public function prevent_admin_access() { - $prevent_access = false; - - if ( apply_filters( 'woocommerce_disable_admin_bar', true ) && ! is_ajax() && isset( $_SERVER['SCRIPT_FILENAME'] ) && basename( sanitize_text_field( wp_unslash( $_SERVER['SCRIPT_FILENAME'] ) ) ) !== 'admin-post.php' ) { - $has_cap = false; - $access_caps = array( 'edit_posts', 'manage_woocommerce', 'view_admin_dashboard' ); - - foreach ( $access_caps as $access_cap ) { - if ( current_user_can( $access_cap ) ) { - $has_cap = true; - break; - } - } - - if ( ! $has_cap ) { - $prevent_access = true; - } - } - - if ( apply_filters( 'woocommerce_prevent_admin_access', $prevent_access ) ) { - wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) ); - exit; - } - } - - /** - * Preview email template. - */ - public function preview_emails() { - - if ( isset( $_GET['preview_woocommerce_mail'] ) ) { - if ( ! ( isset( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'preview-mail' ) ) ) { - die( 'Security check' ); - } - - // load the mailer class. - $mailer = WC()->mailer(); - - // get the preview email subject. - $email_heading = __( 'HTML email template', 'woocommerce' ); - - // get the preview email content. - ob_start(); - include __DIR__ . '/views/html-email-template-preview.php'; - $message = ob_get_clean(); - - // create a new email. - $email = new WC_Email(); - - // wrap the content with the email template and then add styles. - $message = apply_filters( 'woocommerce_mail_content', $email->style_inline( $mailer->wrap_message( $email_heading, $message ) ) ); - - // print the preview email. - // phpcs:ignore WordPress.Security.EscapeOutput - echo $message; - // phpcs:enable - exit; - } - } - - /** - * Change the admin footer text on WooCommerce admin pages. - * - * @since 2.3 - * @param string $footer_text text to be rendered in the footer. - * @return string - */ - public function admin_footer_text( $footer_text ) { - if ( ! current_user_can( 'manage_woocommerce' ) || ! function_exists( 'wc_get_screen_ids' ) ) { - return $footer_text; - } - $current_screen = get_current_screen(); - $wc_pages = wc_get_screen_ids(); - - // Set only WC pages. - $wc_pages = array_diff( $wc_pages, array( 'profile', 'user-edit' ) ); - - // Check to make sure we're on a WooCommerce admin page. - if ( isset( $current_screen->id ) && apply_filters( 'woocommerce_display_admin_footer_text', in_array( $current_screen->id, $wc_pages, true ) ) ) { - // Change the footer text. - if ( ! get_option( 'woocommerce_admin_footer_text_rated' ) ) { - $footer_text = sprintf( - /* translators: 1: WooCommerce 2:: five stars */ - __( 'If you like %1$s please leave us a %2$s rating. A huge thanks in advance!', 'woocommerce' ), - sprintf( '%s', esc_html__( 'WooCommerce', 'woocommerce' ) ), - '★★★★★' - ); - wc_enqueue_js( - "jQuery( 'a.wc-rating-link' ).on( 'click', function() { - jQuery.post( '" . WC()->ajax_url() . "', { action: 'woocommerce_rated' } ); - jQuery( this ).parent().text( jQuery( this ).data( 'rated' ) ); - });" - ); - } else { - $footer_text = __( 'Thank you for selling with WooCommerce.', 'woocommerce' ); - } - } - - return $footer_text; - } - - /** - * Check on a Jetpack install queued by the Setup Wizard. - * - * See: WC_Admin_Setup_Wizard::install_jetpack() - */ - public function setup_wizard_check_jetpack() { - $jetpack_active = class_exists( 'Jetpack' ); - - wp_send_json_success( - array( - 'is_active' => $jetpack_active ? 'yes' : 'no', - ) - ); - } - - /** - * Disable WXR export of scheduled action posts. - * - * @since 3.6.2 - * - * @param array $args Scehduled action post type registration args. - * - * @return array - */ - public function disable_webhook_post_export( $args ) { - $args['can_export'] = false; - return $args; - } - - /** - * Include admin classes. - * - * @since 4.2.0 - * @param string $classes Body classes string. - * @return string - */ - public function include_admin_body_class( $classes ) { - if ( in_array( array( 'wc-wp-version-gte-53', 'wc-wp-version-gte-55' ), explode( ' ', $classes ), true ) ) { - return $classes; - } - - $raw_version = get_bloginfo( 'version' ); - $version_parts = explode( '-', $raw_version ); - $version = count( $version_parts ) > 1 ? $version_parts[0] : $raw_version; - - // Add WP 5.3+ compatibility class. - if ( $raw_version && version_compare( $version, '5.3', '>=' ) ) { - $classes .= ' wc-wp-version-gte-53'; - } - - // Add WP 5.5+ compatibility class. - if ( $raw_version && version_compare( $version, '5.5', '>=' ) ) { - $classes .= ' wc-wp-version-gte-55'; - } - - return $classes; - } -} - -return new WC_Admin(); diff --git a/includes/admin/helper/class-wc-helper.php b/includes/admin/helper/class-wc-helper.php deleted file mode 100644 index 6fb43c27fa2..00000000000 --- a/includes/admin/helper/class-wc-helper.php +++ /dev/null @@ -1,1641 +0,0 @@ - 'wc-addons', - 'section' => 'helper', - 'wc-helper-connect' => 1, - 'wc-helper-nonce' => wp_create_nonce( 'connect' ), - ), - admin_url( 'admin.php' ) - ); - - include self::get_view_filename( 'html-oauth-start.php' ); - return; - } - $disconnect_url = add_query_arg( - array( - 'page' => 'wc-addons', - 'section' => 'helper', - 'wc-helper-disconnect' => 1, - 'wc-helper-nonce' => wp_create_nonce( 'disconnect' ), - ), - admin_url( 'admin.php' ) - ); - - $current_filter = self::get_current_filter(); - $refresh_url = add_query_arg( - array( - 'page' => 'wc-addons', - 'section' => 'helper', - 'filter' => $current_filter, - 'wc-helper-refresh' => 1, - 'wc-helper-nonce' => wp_create_nonce( 'refresh' ), - ), - admin_url( 'admin.php' ) - ); - - // Installed plugins and themes, with or without an active subscription. - $woo_plugins = self::get_local_woo_plugins(); - $woo_themes = self::get_local_woo_themes(); - - $site_id = absint( $auth['site_id'] ); - $subscriptions = self::get_subscriptions(); - $updates = WC_Helper_Updater::get_update_data(); - $subscriptions_product_ids = wp_list_pluck( $subscriptions, 'product_id' ); - - foreach ( $subscriptions as &$subscription ) { - $subscription['active'] = in_array( $site_id, $subscription['connections'] ); - - $subscription['activate_url'] = add_query_arg( - array( - 'page' => 'wc-addons', - 'section' => 'helper', - 'filter' => $current_filter, - 'wc-helper-activate' => 1, - 'wc-helper-product-key' => $subscription['product_key'], - 'wc-helper-product-id' => $subscription['product_id'], - 'wc-helper-nonce' => wp_create_nonce( 'activate:' . $subscription['product_key'] ), - ), - admin_url( 'admin.php' ) - ); - - $subscription['deactivate_url'] = add_query_arg( - array( - 'page' => 'wc-addons', - 'section' => 'helper', - 'filter' => $current_filter, - 'wc-helper-deactivate' => 1, - 'wc-helper-product-key' => $subscription['product_key'], - 'wc-helper-product-id' => $subscription['product_id'], - 'wc-helper-nonce' => wp_create_nonce( 'deactivate:' . $subscription['product_key'] ), - ), - admin_url( 'admin.php' ) - ); - - $subscription['local'] = array( - 'installed' => false, - 'active' => false, - 'version' => null, - ); - - $subscription['update_url'] = admin_url( 'update-core.php' ); - - $local = wp_list_filter( array_merge( $woo_plugins, $woo_themes ), array( '_product_id' => $subscription['product_id'] ) ); - - if ( ! empty( $local ) ) { - $local = array_shift( $local ); - $subscription['local']['installed'] = true; - $subscription['local']['version'] = $local['Version']; - - if ( 'plugin' == $local['_type'] ) { - if ( is_plugin_active( $local['_filename'] ) ) { - $subscription['local']['active'] = true; - } elseif ( is_multisite() && is_plugin_active_for_network( $local['_filename'] ) ) { - $subscription['local']['active'] = true; - } - - // A magic update_url. - $subscription['update_url'] = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' ) . $local['_filename'], 'upgrade-plugin_' . $local['_filename'] ); - - } elseif ( 'theme' == $local['_type'] ) { - if ( in_array( $local['_stylesheet'], array( get_stylesheet(), get_template() ) ) ) { - $subscription['local']['active'] = true; - } - - // Another magic update_url. - $subscription['update_url'] = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-theme&theme=' . $local['_stylesheet'] ), 'upgrade-theme_' . $local['_stylesheet'] ); - } - } - - $subscription['has_update'] = false; - if ( $subscription['local']['installed'] && ! empty( $updates[ $subscription['product_id'] ] ) ) { - $subscription['has_update'] = version_compare( $updates[ $subscription['product_id'] ]['version'], $subscription['local']['version'], '>' ); - } - - $subscription['download_primary'] = true; - $subscription['download_url'] = 'https://woocommerce.com/my-account/downloads/'; - if ( ! $subscription['local']['installed'] && ! empty( $updates[ $subscription['product_id'] ] ) ) { - $subscription['download_url'] = $updates[ $subscription['product_id'] ]['package']; - } - - $subscription['actions'] = array(); - - if ( $subscription['has_update'] && ! $subscription['expired'] ) { - $action = array( - /* translators: %s: version number */ - 'message' => sprintf( __( 'Version %s is available.', 'woocommerce' ), esc_html( $updates[ $subscription['product_id'] ]['version'] ) ), - 'button_label' => __( 'Update', 'woocommerce' ), - 'button_url' => $subscription['update_url'], - 'status' => 'update-available', - 'icon' => 'dashicons-update', - ); - - // Subscription is not active on this site. - if ( ! $subscription['active'] ) { - $action['message'] .= ' ' . __( 'To enable this update you need to activate this subscription.', 'woocommerce' ); - $action['button_label'] = null; - $action['button_url'] = null; - } - - $subscription['actions'][] = $action; - } - - if ( $subscription['has_update'] && $subscription['expired'] ) { - $action = array( - /* translators: %s: version number */ - 'message' => sprintf( __( 'Version %s is available.', 'woocommerce' ), esc_html( $updates[ $subscription['product_id'] ]['version'] ) ), - 'status' => 'expired', - 'icon' => 'dashicons-info', - ); - - $action['message'] .= ' ' . __( 'To enable this update you need to purchase a new subscription.', 'woocommerce' ); - $action['button_label'] = __( 'Purchase', 'woocommerce' ); - $action['button_url'] = $subscription['product_url']; - - $subscription['actions'][] = $action; - } elseif ( $subscription['expired'] && ! empty( $subscription['master_user_email'] ) ) { - $action = array( - 'message' => sprintf( __( 'This subscription has expired. Contact the owner to renew the subscription to receive updates and support.', 'woocommerce' ) ), - 'status' => 'expired', - 'icon' => 'dashicons-info', - ); - - $subscription['actions'][] = $action; - } elseif ( $subscription['expired'] ) { - $action = array( - 'message' => sprintf( __( 'This subscription has expired. Please renew to receive updates and support.', 'woocommerce' ) ), - 'button_label' => __( 'Renew', 'woocommerce' ), - 'button_url' => 'https://woocommerce.com/my-account/my-subscriptions/', - 'status' => 'expired', - 'icon' => 'dashicons-info', - ); - - $subscription['actions'][] = $action; - } - - if ( $subscription['expiring'] && ! $subscription['autorenew'] ) { - $action = array( - 'message' => __( 'Subscription is expiring soon.', 'woocommerce' ), - 'button_label' => __( 'Enable auto-renew', 'woocommerce' ), - 'button_url' => 'https://woocommerce.com/my-account/my-subscriptions/', - 'status' => 'expired', - 'icon' => 'dashicons-info', - ); - - $subscription['download_primary'] = false; - $subscription['actions'][] = $action; - } elseif ( $subscription['expiring'] ) { - $action = array( - 'message' => sprintf( __( 'This subscription is expiring soon. Please renew to continue receiving updates and support.', 'woocommerce' ) ), - 'button_label' => __( 'Renew', 'woocommerce' ), - 'button_url' => 'https://woocommerce.com/my-account/my-subscriptions/', - 'status' => 'expired', - 'icon' => 'dashicons-info', - ); - - $subscription['download_primary'] = false; - $subscription['actions'][] = $action; - } - - // Mark the first action primary. - foreach ( $subscription['actions'] as $key => $action ) { - if ( ! empty( $action['button_label'] ) ) { - $subscription['actions'][ $key ]['primary'] = true; - break; - } - } - } - - // Break the by-ref. - unset( $subscription ); - - // Installed products without a subscription. - $no_subscriptions = array(); - foreach ( array_merge( $woo_plugins, $woo_themes ) as $filename => $data ) { - if ( in_array( $data['_product_id'], $subscriptions_product_ids ) ) { - continue; - } - - $data['_product_url'] = '#'; - $data['_has_update'] = false; - - if ( ! empty( $updates[ $data['_product_id'] ] ) ) { - $data['_has_update'] = version_compare( $updates[ $data['_product_id'] ]['version'], $data['Version'], '>' ); - - if ( ! empty( $updates[ $data['_product_id'] ]['url'] ) ) { - $data['_product_url'] = $updates[ $data['_product_id'] ]['url']; - } elseif ( ! empty( $data['PluginURI'] ) ) { - $data['_product_url'] = $data['PluginURI']; - } - } - - $data['_actions'] = array(); - - if ( $data['_has_update'] ) { - $action = array( - /* translators: %s: version number */ - 'message' => sprintf( __( 'Version %s is available. To enable this update you need to purchase a new subscription.', 'woocommerce' ), esc_html( $updates[ $data['_product_id'] ]['version'] ) ), - 'button_label' => __( 'Purchase', 'woocommerce' ), - 'button_url' => $data['_product_url'], - 'status' => 'expired', - 'icon' => 'dashicons-info', - ); - - $data['_actions'][] = $action; - } else { - $action = array( - /* translators: 1: subscriptions docs 2: subscriptions docs */ - 'message' => sprintf( __( 'To receive updates and support for this extension, you need to purchase a new subscription or consolidate your extensions to one connected account by sharing or transferring this extension to this connected account.', 'woocommerce' ), 'https://docs.woocommerce.com/document/managing-woocommerce-com-subscriptions/#section-10', 'https://docs.woocommerce.com/document/managing-woocommerce-com-subscriptions/#section-5' ), - 'button_label' => __( 'Purchase', 'woocommerce' ), - 'button_url' => $data['_product_url'], - 'status' => 'expired', - 'icon' => 'dashicons-info', - ); - - $data['_actions'][] = $action; - } - - $no_subscriptions[ $filename ] = $data; - } - - // Update the user id if it came from a migrated connection. - if ( empty( $auth['user_id'] ) ) { - $auth['user_id'] = get_current_user_id(); - WC_Helper_Options::update( 'auth', $auth ); - } - - // Sort alphabetically. - uasort( $subscriptions, array( __CLASS__, '_sort_by_product_name' ) ); - uasort( $no_subscriptions, array( __CLASS__, '_sort_by_name' ) ); - - // Filters. - self::get_filters_counts( $subscriptions ); // Warm it up. - self::_filter( $subscriptions, self::get_current_filter() ); - - // We have an active connection. - include self::get_view_filename( 'html-main.php' ); - return; - } - - /** - * Get available subscriptions filters. - * - * @return array An array of filter keys and labels. - */ - public static function get_filters() { - $filters = array( - 'all' => __( 'All', 'woocommerce' ), - 'active' => __( 'Active', 'woocommerce' ), - 'inactive' => __( 'Inactive', 'woocommerce' ), - 'installed' => __( 'Installed', 'woocommerce' ), - 'update-available' => __( 'Update Available', 'woocommerce' ), - 'expiring' => __( 'Expiring Soon', 'woocommerce' ), - 'expired' => __( 'Expired', 'woocommerce' ), - 'download' => __( 'Download', 'woocommerce' ), - ); - - return $filters; - } - - /** - * Get counts data for the filters array. - * - * @param array $subscriptions The array of all available subscriptions. - * - * @return array Filter counts (filter => count). - */ - public static function get_filters_counts( $subscriptions = null ) { - static $filters; - - if ( isset( $filters ) ) { - return $filters; - } - - $filters = array_fill_keys( array_keys( self::get_filters() ), 0 ); - if ( empty( $subscriptions ) ) { - return array(); - } - - foreach ( $filters as $key => $count ) { - $_subs = $subscriptions; - self::_filter( $_subs, $key ); - $filters[ $key ] = count( $_subs ); - } - - return $filters; - } - - /** - * Get current filter. - * - * @return string The current filter. - */ - public static function get_current_filter() { - $current_filter = 'all'; - $valid_filters = array_keys( self::get_filters() ); - - if ( ! empty( $_GET['filter'] ) && in_array( wp_unslash( $_GET['filter'] ), $valid_filters ) ) { - $current_filter = wc_clean( wp_unslash( $_GET['filter'] ) ); - } - - return $current_filter; - } - - /** - * Filter an array of subscriptions by $filter. - * - * @param array $subscriptions The subscriptions array, passed by ref. - * @param string $filter The filter. - */ - private static function _filter( &$subscriptions, $filter ) { - switch ( $filter ) { - case 'active': - $subscriptions = wp_list_filter( $subscriptions, array( 'active' => true ) ); - break; - - case 'inactive': - $subscriptions = wp_list_filter( $subscriptions, array( 'active' => false ) ); - break; - - case 'installed': - foreach ( $subscriptions as $key => $subscription ) { - if ( empty( $subscription['local']['installed'] ) ) { - unset( $subscriptions[ $key ] ); - } - } - break; - - case 'update-available': - $subscriptions = wp_list_filter( $subscriptions, array( 'has_update' => true ) ); - break; - - case 'expiring': - $subscriptions = wp_list_filter( $subscriptions, array( 'expiring' => true ) ); - break; - - case 'expired': - $subscriptions = wp_list_filter( $subscriptions, array( 'expired' => true ) ); - break; - - case 'download': - foreach ( $subscriptions as $key => $subscription ) { - if ( $subscription['local']['installed'] || $subscription['expired'] ) { - unset( $subscriptions[ $key ] ); - } - } - break; - } - } - - /** - * Enqueue admin scripts and styles. - */ - public static function admin_enqueue_scripts() { - $screen = get_current_screen(); - $screen_id = $screen ? $screen->id : ''; - $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) ); - - if ( $wc_screen_id . '_page_wc-addons' === $screen_id && isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) { - wp_enqueue_style( 'woocommerce-helper', WC()->plugin_url() . '/assets/css/helper.css', array(), Constants::get_constant( 'WC_VERSION' ) ); - wp_style_add_data( 'woocommerce-helper', 'rtl', 'replace' ); - } - } - - /** - * Various success/error notices. - * - * Runs during admin page render, so no headers/redirects here. - * - * @return array Array pairs of message/type strings with notices. - */ - private static function _get_return_notices() { - $return_status = isset( $_GET['wc-helper-status'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-status'] ) ) : null; - $notices = array(); - - switch ( $return_status ) { - case 'activate-success': - $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; - $subscription = self::_get_subscriptions_from_product_id( $product_id ); - $notices[] = array( - 'type' => 'updated', - 'message' => sprintf( - /* translators: %s: product name */ - __( '%s activated successfully. You will now receive updates for this product.', 'woocommerce' ), - '' . esc_html( $subscription['product_name'] ) . '' - ), - ); - break; - - case 'activate-error': - $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; - $subscription = self::_get_subscriptions_from_product_id( $product_id ); - $notices[] = array( - 'type' => 'error', - 'message' => sprintf( - /* translators: %s: product name */ - __( 'An error has occurred when activating %s. Please try again later.', 'woocommerce' ), - '' . esc_html( $subscription['product_name'] ) . '' - ), - ); - break; - - case 'deactivate-success': - $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; - $subscription = self::_get_subscriptions_from_product_id( $product_id ); - $local = self::_get_local_from_product_id( $product_id ); - - $message = sprintf( - /* translators: %s: product name */ - __( 'Subscription for %s deactivated successfully. You will no longer receive updates for this product.', 'woocommerce' ), - '' . esc_html( $subscription['product_name'] ) . '' - ); - - if ( $local && is_plugin_active( $local['_filename'] ) && current_user_can( 'activate_plugins' ) ) { - $deactivate_plugin_url = add_query_arg( - array( - 'page' => 'wc-addons', - 'section' => 'helper', - 'filter' => self::get_current_filter(), - 'wc-helper-deactivate-plugin' => 1, - 'wc-helper-product-id' => $subscription['product_id'], - 'wc-helper-nonce' => wp_create_nonce( 'deactivate-plugin:' . $subscription['product_id'] ), - ), - admin_url( 'admin.php' ) - ); - - $message = sprintf( - /* translators: %1$s: product name, %2$s: deactivate url */ - __( 'Subscription for %1$s deactivated successfully. You will no longer receive updates for this product. Click here if you wish to deactivate the plugin as well.', 'woocommerce' ), - '' . esc_html( $subscription['product_name'] ) . '', - esc_url( $deactivate_plugin_url ) - ); - } - - $notices[] = array( - 'message' => $message, - 'type' => 'updated', - ); - break; - - case 'deactivate-error': - $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; - $subscription = self::_get_subscriptions_from_product_id( $product_id ); - $notices[] = array( - 'type' => 'error', - 'message' => sprintf( - /* translators: %s: product name */ - __( 'An error has occurred when deactivating the subscription for %s. Please try again later.', 'woocommerce' ), - '' . esc_html( $subscription['product_name'] ) . '' - ), - ); - break; - - case 'deactivate-plugin-success': - $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; - $subscription = self::_get_subscriptions_from_product_id( $product_id ); - $notices[] = array( - 'type' => 'updated', - 'message' => sprintf( - /* translators: %s: product name */ - __( 'The extension %s has been deactivated successfully.', 'woocommerce' ), - '' . esc_html( $subscription['product_name'] ) . '' - ), - ); - break; - - case 'deactivate-plugin-error': - $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; - $subscription = self::_get_subscriptions_from_product_id( $product_id ); - $notices[] = array( - 'type' => 'error', - 'message' => sprintf( - /* translators: %1$s: product name, %2$s: plugins screen url */ - __( 'An error has occurred when deactivating the extension %1$s. Please proceed to the Plugins screen to deactivate it manually.', 'woocommerce' ), - '' . esc_html( $subscription['product_name'] ) . '', - admin_url( 'plugins.php' ) - ), - ); - break; - - case 'helper-connected': - $notices[] = array( - 'message' => __( 'You have successfully connected your store to WooCommerce.com', 'woocommerce' ), - 'type' => 'updated', - ); - break; - - case 'helper-disconnected': - $notices[] = array( - 'message' => __( 'You have successfully disconnected your store from WooCommerce.com', 'woocommerce' ), - 'type' => 'updated', - ); - break; - - case 'helper-refreshed': - $notices[] = array( - 'message' => __( 'Authentication and subscription caches refreshed successfully.', 'woocommerce' ), - 'type' => 'updated', - ); - break; - } - - return $notices; - } - - /** - * Various early-phase actions with possible redirects. - * - * @param object $screen WP screen object. - */ - public static function current_screen( $screen ) { - $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) ); - - if ( $wc_screen_id . '_page_wc-addons' !== $screen->id ) { - return; - } - - if ( empty( $_GET['section'] ) || 'helper' !== $_GET['section'] ) { - return; - } - - if ( ! empty( $_GET['wc-helper-connect'] ) ) { - return self::_helper_auth_connect(); - } - - if ( ! empty( $_GET['wc-helper-return'] ) ) { - return self::_helper_auth_return(); - } - - if ( ! empty( $_GET['wc-helper-disconnect'] ) ) { - return self::_helper_auth_disconnect(); - } - - if ( ! empty( $_GET['wc-helper-refresh'] ) ) { - return self::_helper_auth_refresh(); - } - - if ( ! empty( $_GET['wc-helper-activate'] ) ) { - return self::_helper_subscription_activate(); - } - - if ( ! empty( $_GET['wc-helper-deactivate'] ) ) { - return self::_helper_subscription_deactivate(); - } - - if ( ! empty( $_GET['wc-helper-deactivate-plugin'] ) ) { - return self::_helper_plugin_deactivate(); - } - } - - /** - * Initiate a new OAuth connection. - */ - private static function _helper_auth_connect() { - if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'connect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - self::log( 'Could not verify nonce in _helper_auth_connect' ); - wp_die( 'Could not verify nonce' ); - } - - $redirect_uri = add_query_arg( - array( - 'page' => 'wc-addons', - 'section' => 'helper', - 'wc-helper-return' => 1, - 'wc-helper-nonce' => wp_create_nonce( 'connect' ), - ), - admin_url( 'admin.php' ) - ); - - $request = WC_Helper_API::post( - 'oauth/request_token', - array( - 'body' => array( - 'home_url' => home_url(), - 'redirect_uri' => $redirect_uri, - ), - ) - ); - - $code = wp_remote_retrieve_response_code( $request ); - - if ( 200 !== $code ) { - self::log( sprintf( 'Call to oauth/request_token returned a non-200 response code (%d)', $code ) ); - wp_die( 'Something went wrong' ); - } - - $secret = json_decode( wp_remote_retrieve_body( $request ) ); - if ( empty( $secret ) ) { - self::log( sprintf( 'Call to oauth/request_token returned an invalid body: %s', wp_remote_retrieve_body( $request ) ) ); - wp_die( 'Something went wrong' ); - } - - /** - * Fires when the Helper connection process is initiated. - */ - do_action( 'woocommerce_helper_connect_start' ); - - $connect_url = add_query_arg( - array( - 'home_url' => rawurlencode( home_url() ), - 'redirect_uri' => rawurlencode( $redirect_uri ), - 'secret' => rawurlencode( $secret ), - ), - WC_Helper_API::url( 'oauth/authorize' ) - ); - - wp_redirect( esc_url_raw( $connect_url ) ); - die(); - } - - /** - * Return from WooCommerce.com OAuth flow. - */ - private static function _helper_auth_return() { - if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'connect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - self::log( 'Could not verify nonce in _helper_auth_return' ); - wp_die( 'Something went wrong' ); - } - - // Bail if the user clicked deny. - if ( ! empty( $_GET['deny'] ) ) { - /** - * Fires when the Helper connection process is denied/cancelled. - */ - do_action( 'woocommerce_helper_denied' ); - wp_safe_redirect( admin_url( 'admin.php?page=wc-addons§ion=helper' ) ); - die(); - } - - // We do need a request token... - if ( empty( $_GET['request_token'] ) ) { - self::log( 'Request token not found in _helper_auth_return' ); - wp_die( 'Something went wrong' ); - } - - // Obtain an access token. - $request = WC_Helper_API::post( - 'oauth/access_token', - array( - 'body' => array( - 'request_token' => wp_unslash( $_GET['request_token'] ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - 'home_url' => home_url(), - ), - ) - ); - - $code = wp_remote_retrieve_response_code( $request ); - - if ( 200 !== $code ) { - self::log( sprintf( 'Call to oauth/access_token returned a non-200 response code (%d)', $code ) ); - wp_die( 'Something went wrong' ); - } - - $access_token = json_decode( wp_remote_retrieve_body( $request ), true ); - if ( ! $access_token ) { - self::log( sprintf( 'Call to oauth/access_token returned an invalid body: %s', wp_remote_retrieve_body( $request ) ) ); - wp_die( 'Something went wrong' ); - } - - WC_Helper_Options::update( - 'auth', - array( - 'access_token' => $access_token['access_token'], - 'access_token_secret' => $access_token['access_token_secret'], - 'site_id' => $access_token['site_id'], - 'user_id' => get_current_user_id(), - 'updated' => time(), - ) - ); - - // Obtain the connected user info. - if ( ! self::_flush_authentication_cache() ) { - self::log( 'Could not obtain connected user info in _helper_auth_return' ); - WC_Helper_Options::update( 'auth', array() ); - wp_die( 'Something went wrong.' ); - } - - self::_flush_subscriptions_cache(); - self::_flush_updates_cache(); - - /** - * Fires when the Helper connection process has completed successfully. - */ - do_action( 'woocommerce_helper_connected' ); - - // Enable tracking when connected. - if ( class_exists( 'WC_Tracker' ) ) { - update_option( 'woocommerce_allow_tracking', 'yes' ); - WC_Tracker::send_tracking_data( true ); - } - - // If connecting through in-app purchase, redirects back to WooCommerce.com - // for product installation. - if ( ! empty( $_GET['wccom-install-url'] ) ) { - wp_redirect( wp_unslash( $_GET['wccom-install-url'] ) ); - exit; - } - - wp_safe_redirect( - add_query_arg( - array( - 'page' => 'wc-addons', - 'section' => 'helper', - 'wc-helper-status' => 'helper-connected', - ), - admin_url( 'admin.php' ) - ) - ); - die(); - } - - /** - * Disconnect from WooCommerce.com, clear OAuth tokens. - */ - private static function _helper_auth_disconnect() { - if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'disconnect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - self::log( 'Could not verify nonce in _helper_auth_disconnect' ); - wp_die( 'Could not verify nonce' ); - } - - /** - * Fires when the Helper has been disconnected. - */ - do_action( 'woocommerce_helper_disconnected' ); - - $redirect_uri = add_query_arg( - array( - 'page' => 'wc-addons', - 'section' => 'helper', - 'wc-helper-status' => 'helper-disconnected', - ), - admin_url( 'admin.php' ) - ); - - WC_Helper_API::post( - 'oauth/invalidate_token', - array( - 'authenticated' => true, - ) - ); - - WC_Helper_Options::update( 'auth', array() ); - WC_Helper_Options::update( 'auth_user_data', array() ); - - self::_flush_subscriptions_cache(); - self::_flush_updates_cache(); - - wp_safe_redirect( $redirect_uri ); - die(); - } - - /** - * User hit the Refresh button, clear all caches. - */ - private static function _helper_auth_refresh() { - if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'refresh' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - self::log( 'Could not verify nonce in _helper_auth_refresh' ); - wp_die( 'Could not verify nonce' ); - } - - /** - * Fires when Helper subscriptions are refreshed. - */ - do_action( 'woocommerce_helper_subscriptions_refresh' ); - - $redirect_uri = add_query_arg( - array( - 'page' => 'wc-addons', - 'section' => 'helper', - 'filter' => self::get_current_filter(), - 'wc-helper-status' => 'helper-refreshed', - ), - admin_url( 'admin.php' ) - ); - - self::_flush_authentication_cache(); - self::_flush_subscriptions_cache(); - self::_flush_updates_cache(); - - wp_safe_redirect( $redirect_uri ); - die(); - } - - /** - * Active a product subscription. - */ - private static function _helper_subscription_activate() { - $product_key = isset( $_GET['wc-helper-product-key'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-product-key'] ) ) : ''; - $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; - - if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'activate:' . $product_key ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - self::log( 'Could not verify nonce in _helper_subscription_activate' ); - wp_die( 'Could not verify nonce' ); - } - - // Activate subscription. - $activation_response = WC_Helper_API::post( - 'activate', - array( - 'authenticated' => true, - 'body' => wp_json_encode( - array( - 'product_key' => $product_key, - ) - ), - ) - ); - - $activated = wp_remote_retrieve_response_code( $activation_response ) === 200; - $body = json_decode( wp_remote_retrieve_body( $activation_response ), true ); - - if ( ! $activated && ! empty( $body['code'] ) && 'already_connected' === $body['code'] ) { - $activated = true; - } - - if ( $activated ) { - /** - * Fires when the Helper activates a product successfully. - * - * @param int $product_id Product ID being activated. - * @param string $product_key Subscription product key. - * @param array $activation_response The response object from wp_safe_remote_request(). - */ - do_action( 'woocommerce_helper_subscription_activate_success', $product_id, $product_key, $activation_response ); - } else { - /** - * Fires when the Helper fails to activate a product. - * - * @param int $product_id Product ID being activated. - * @param string $product_key Subscription product key. - * @param array $activation_response The response object from wp_safe_remote_request(). - */ - do_action( 'woocommerce_helper_subscription_activate_error', $product_id, $product_key, $activation_response ); - } - - // Attempt to activate this plugin. - $local = self::_get_local_from_product_id( $product_id ); - if ( $local && 'plugin' == $local['_type'] && current_user_can( 'activate_plugins' ) && ! is_plugin_active( $local['_filename'] ) ) { - activate_plugin( $local['_filename'] ); - } - - self::_flush_subscriptions_cache(); - self::_flush_updates_cache(); - - $redirect_uri = add_query_arg( - array( - 'page' => 'wc-addons', - 'section' => 'helper', - 'filter' => self::get_current_filter(), - 'wc-helper-status' => $activated ? 'activate-success' : 'activate-error', - 'wc-helper-product-id' => $product_id, - ), - admin_url( 'admin.php' ) - ); - - wp_safe_redirect( $redirect_uri ); - die(); - } - - /** - * Deactivate a product subscription. - */ - private static function _helper_subscription_deactivate() { - $product_key = isset( $_GET['wc-helper-product-key'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-product-key'] ) ) : ''; - $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; - - if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'deactivate:' . $product_key ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - self::log( 'Could not verify nonce in _helper_subscription_deactivate' ); - wp_die( 'Could not verify nonce' ); - } - - $deactivation_response = WC_Helper_API::post( - 'deactivate', - array( - 'authenticated' => true, - 'body' => wp_json_encode( - array( - 'product_key' => $product_key, - ) - ), - ) - ); - - $code = wp_remote_retrieve_response_code( $deactivation_response ); - $deactivated = 200 === $code; - - if ( $deactivated ) { - /** - * Fires when the Helper activates a product successfully. - * - * @param int $product_id Product ID being deactivated. - * @param string $product_key Subscription product key. - * @param array $deactivation_response The response object from wp_safe_remote_request(). - */ - do_action( 'woocommerce_helper_subscription_deactivate_success', $product_id, $product_key, $deactivation_response ); - } else { - self::log( sprintf( 'Deactivate API call returned a non-200 response code (%d)', $code ) ); - - /** - * Fires when the Helper fails to activate a product. - * - * @param int $product_id Product ID being deactivated. - * @param string $product_key Subscription product key. - * @param array $deactivation_response The response object from wp_safe_remote_request(). - */ - do_action( 'woocommerce_helper_subscription_deactivate_error', $product_id, $product_key, $deactivation_response ); - } - - self::_flush_subscriptions_cache(); - - $redirect_uri = add_query_arg( - array( - 'page' => 'wc-addons', - 'section' => 'helper', - 'filter' => self::get_current_filter(), - 'wc-helper-status' => $deactivated ? 'deactivate-success' : 'deactivate-error', - 'wc-helper-product-id' => $product_id, - ), - admin_url( 'admin.php' ) - ); - - wp_safe_redirect( $redirect_uri ); - die(); - } - - /** - * Deactivate a plugin. - */ - private static function _helper_plugin_deactivate() { - $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; - $deactivated = false; - - if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'deactivate-plugin:' . $product_id ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - self::log( 'Could not verify nonce in _helper_plugin_deactivate' ); - wp_die( 'Could not verify nonce' ); - } - - if ( ! current_user_can( 'activate_plugins' ) ) { - wp_die( 'You are not allowed to manage plugins on this site.' ); - } - - $local = wp_list_filter( - array_merge( - self::get_local_woo_plugins(), - self::get_local_woo_themes() - ), - array( '_product_id' => $product_id ) - ); - - // Attempt to deactivate this plugin or theme. - if ( ! empty( $local ) ) { - $local = array_shift( $local ); - if ( is_plugin_active( $local['_filename'] ) ) { - deactivate_plugins( $local['_filename'] ); - } - - $deactivated = ! is_plugin_active( $local['_filename'] ); - } - - $redirect_uri = add_query_arg( - array( - 'page' => 'wc-addons', - 'section' => 'helper', - 'filter' => self::get_current_filter(), - 'wc-helper-status' => $deactivated ? 'deactivate-plugin-success' : 'deactivate-plugin-error', - 'wc-helper-product-id' => $product_id, - ), - admin_url( 'admin.php' ) - ); - - wp_safe_redirect( $redirect_uri ); - die(); - } - - /** - * Get a local plugin/theme entry from product_id. - * - * @param int $product_id The product id. - * - * @return array|bool The array containing the local plugin/theme data or false. - */ - private static function _get_local_from_product_id( $product_id ) { - $local = wp_list_filter( - array_merge( - self::get_local_woo_plugins(), - self::get_local_woo_themes() - ), - array( '_product_id' => $product_id ) - ); - - if ( ! empty( $local ) ) { - return array_shift( $local ); - } - - return false; - } - - /** - * Checks whether current site has product subscription of a given ID. - * - * @since 3.7.0 - * - * @param int $product_id The product id. - * - * @return bool Returns true if product subscription exists, false otherwise. - */ - public static function has_product_subscription( $product_id ) { - $subscription = self::_get_subscriptions_from_product_id( $product_id, true ); - return ! empty( $subscription ); - } - - /** - * Get a subscription entry from product_id. If multiple subscriptions are - * found with the same product id and $single is set to true, will return the - * first one in the list, so you can use this method to get things like extension - * name, version, etc. - * - * @param int $product_id The product id. - * @param bool $single Whether to return a single subscription or all matching a product id. - * - * @return array|bool The array containing sub data or false. - */ - private static function _get_subscriptions_from_product_id( $product_id, $single = true ) { - $subscriptions = wp_list_filter( self::get_subscriptions(), array( 'product_id' => $product_id ) ); - if ( ! empty( $subscriptions ) ) { - return $single ? array_shift( $subscriptions ) : $subscriptions; - } - - return false; - } - - /** - * Obtain a list of data about locally installed Woo extensions. - */ - public static function get_local_woo_plugins() { - if ( ! function_exists( 'get_plugins' ) ) { - require_once ABSPATH . 'wp-admin/includes/plugin.php'; - } - - $plugins = get_plugins(); - - /** - * Check if plugins have WC headers, if not then clear cache and fetch again. - * WC Headers will not be present if `wc_enable_wc_plugin_headers` hook was added after a `get_plugins` call -- for example when WC is activated/updated. - * Also, get_plugins call is expensive so we should clear this cache very conservatively. - */ - if ( ! empty( $plugins ) && ! array_key_exists( 'Woo', current( $plugins ) ) ) { - wp_clean_plugins_cache( false ); - $plugins = get_plugins(); - } - - $woo_plugins = array(); - - // Backwards compatibility for woothemes_queue_update(). - $_compat = array(); - if ( ! empty( $GLOBALS['woothemes_queued_updates'] ) ) { - foreach ( $GLOBALS['woothemes_queued_updates'] as $_compat_plugin ) { - $_compat[ $_compat_plugin->file ] = array( - 'product_id' => $_compat_plugin->product_id, - 'file_id' => $_compat_plugin->file_id, - ); - } - } - - foreach ( $plugins as $filename => $data ) { - if ( empty( $data['Woo'] ) && ! empty( $_compat[ $filename ] ) ) { - $data['Woo'] = sprintf( '%d:%s', $_compat[ $filename ]['product_id'], $_compat[ $filename ]['file_id'] ); - } - - if ( empty( $data['Woo'] ) ) { - continue; - } - - list( $product_id, $file_id ) = explode( ':', $data['Woo'] ); - if ( empty( $product_id ) || empty( $file_id ) ) { - continue; - } - - $data['_filename'] = $filename; - $data['_product_id'] = absint( $product_id ); - $data['_file_id'] = $file_id; - $data['_type'] = 'plugin'; - $data['slug'] = dirname( $filename ); - $woo_plugins[ $filename ] = $data; - } - - return $woo_plugins; - } - - /** - * Get locally installed Woo themes. - */ - public static function get_local_woo_themes() { - $themes = wp_get_themes(); - $woo_themes = array(); - - foreach ( $themes as $theme ) { - $header = $theme->get( 'Woo' ); - - // Backwards compatibility for theme_info.txt. - if ( ! $header ) { - $txt = $theme->get_stylesheet_directory() . '/theme_info.txt'; - if ( is_readable( $txt ) ) { - $txt = file_get_contents( $txt ); - $txt = preg_split( '#\s#', $txt ); - if ( count( $txt ) >= 2 ) { - $header = sprintf( '%d:%s', $txt[0], $txt[1] ); - } - } - } - - if ( empty( $header ) ) { - continue; - } - - list( $product_id, $file_id ) = explode( ':', $header ); - if ( empty( $product_id ) || empty( $file_id ) ) { - continue; - } - - $data = array( - 'Name' => $theme->get( 'Name' ), - 'Version' => $theme->get( 'Version' ), - 'Woo' => $header, - - '_filename' => $theme->get_stylesheet() . '/style.css', - '_stylesheet' => $theme->get_stylesheet(), - '_product_id' => absint( $product_id ), - '_file_id' => $file_id, - '_type' => 'theme', - ); - - $woo_themes[ $data['_filename'] ] = $data; - } - - return $woo_themes; - } - - /** - * Get the connected user's subscriptions. - * - * @return array - */ - public static function get_subscriptions() { - $cache_key = '_woocommerce_helper_subscriptions'; - $data = get_transient( $cache_key ); - if ( false !== $data ) { - return $data; - } - - // Obtain the connected user info. - $request = WC_Helper_API::get( - 'subscriptions', - array( - 'authenticated' => true, - ) - ); - - if ( wp_remote_retrieve_response_code( $request ) !== 200 ) { - set_transient( $cache_key, array(), 15 * MINUTE_IN_SECONDS ); - return array(); - } - - $data = json_decode( wp_remote_retrieve_body( $request ), true ); - if ( empty( $data ) || ! is_array( $data ) ) { - $data = array(); - } - - set_transient( $cache_key, $data, 1 * HOUR_IN_SECONDS ); - return $data; - } - - /** - * Runs when any plugin is activated. - * - * Depending on the activated plugin attempts to look through available - * subscriptions and auto-activate one if possible, so the user does not - * need to visit the Helper UI at all after installing a new extension. - * - * @param string $filename The filename of the activated plugin. - */ - public static function activated_plugin( $filename ) { - $plugins = self::get_local_woo_plugins(); - - // Not a local woo plugin. - if ( empty( $plugins[ $filename ] ) ) { - return; - } - - // Make sure we have a connection. - $auth = WC_Helper_Options::get( 'auth' ); - if ( empty( $auth ) ) { - return; - } - - $plugin = $plugins[ $filename ]; - $product_id = $plugin['_product_id']; - $subscriptions = self::_get_subscriptions_from_product_id( $product_id, false ); - - // No valid subscriptions for this product. - if ( empty( $subscriptions ) ) { - return; - } - - $subscription = null; - foreach ( $subscriptions as $_sub ) { - - // Don't attempt to activate expired subscriptions. - if ( $_sub['expired'] ) { - continue; - } - - // No more sites available in this subscription. - if ( $_sub['sites_max'] && $_sub['sites_active'] >= $_sub['sites_max'] ) { - continue; - } - - // Looks good. - $subscription = $_sub; - break; - } - - // No valid subscription found. - if ( ! $subscription ) { - return; - } - - $product_key = $subscription['product_key']; - $activation_response = WC_Helper_API::post( - 'activate', - array( - 'authenticated' => true, - 'body' => wp_json_encode( - array( - 'product_key' => $product_key, - ) - ), - ) - ); - - $activated = wp_remote_retrieve_response_code( $activation_response ) === 200; - $body = json_decode( wp_remote_retrieve_body( $activation_response ), true ); - - if ( ! $activated && ! empty( $body['code'] ) && 'already_connected' === $body['code'] ) { - $activated = true; - } - - if ( $activated ) { - self::log( 'Auto-activated a subscription for ' . $filename ); - /** - * Fires when the Helper activates a product successfully. - * - * @param int $product_id Product ID being activated. - * @param string $product_key Subscription product key. - * @param array $activation_response The response object from wp_safe_remote_request(). - */ - do_action( 'woocommerce_helper_subscription_activate_success', $product_id, $product_key, $activation_response ); - } else { - self::log( 'Could not activate a subscription upon plugin activation: ' . $filename ); - - /** - * Fires when the Helper fails to activate a product. - * - * @param int $product_id Product ID being activated. - * @param string $product_key Subscription product key. - * @param array $activation_response The response object from wp_safe_remote_request(). - */ - do_action( 'woocommerce_helper_subscription_activate_error', $product_id, $product_key, $activation_response ); - } - - self::_flush_subscriptions_cache(); - self::_flush_updates_cache(); - } - - /** - * Runs when any plugin is deactivated. - * - * When a user deactivates a plugin, attempt to deactivate any subscriptions - * associated with the extension. - * - * @param string $filename The filename of the deactivated plugin. - */ - public static function deactivated_plugin( $filename ) { - $plugins = self::get_local_woo_plugins(); - - // Not a local woo plugin. - if ( empty( $plugins[ $filename ] ) ) { - return; - } - - // Make sure we have a connection. - $auth = WC_Helper_Options::get( 'auth' ); - if ( empty( $auth ) ) { - return; - } - - $plugin = $plugins[ $filename ]; - $product_id = $plugin['_product_id']; - $subscriptions = self::_get_subscriptions_from_product_id( $product_id, false ); - $site_id = absint( $auth['site_id'] ); - - // No valid subscriptions for this product. - if ( empty( $subscriptions ) ) { - return; - } - - $deactivated = 0; - - foreach ( $subscriptions as $subscription ) { - // Don't touch subscriptions that aren't activated on this site. - if ( ! in_array( $site_id, $subscription['connections'], true ) ) { - continue; - } - - $product_key = $subscription['product_key']; - $deactivation_response = WC_Helper_API::post( - 'deactivate', - array( - 'authenticated' => true, - 'body' => wp_json_encode( - array( - 'product_key' => $product_key, - ) - ), - ) - ); - - if ( wp_remote_retrieve_response_code( $deactivation_response ) === 200 ) { - $deactivated++; - - /** - * Fires when the Helper activates a product successfully. - * - * @param int $product_id Product ID being deactivated. - * @param string $product_key Subscription product key. - * @param array $deactivation_response The response object from wp_safe_remote_request(). - */ - do_action( 'woocommerce_helper_subscription_deactivate_success', $product_id, $product_key, $deactivation_response ); - } else { - /** - * Fires when the Helper fails to activate a product. - * - * @param int $product_id Product ID being deactivated. - * @param string $product_key Subscription product key. - * @param array $deactivation_response The response object from wp_safe_remote_request(). - */ - do_action( 'woocommerce_helper_subscription_deactivate_error', $product_id, $product_key, $deactivation_response ); - } - } - - if ( $deactivated ) { - self::log( sprintf( 'Auto-deactivated %d subscription(s) for %s', $deactivated, $filename ) ); - self::_flush_subscriptions_cache(); - self::_flush_updates_cache(); - } - } - - /** - * Various Helper-related admin notices. - */ - public static function admin_notices() { - if ( apply_filters( 'woocommerce_helper_suppress_admin_notices', false ) ) { - return; - } - - $screen = get_current_screen(); - $screen_id = $screen ? $screen->id : ''; - - if ( 'update-core' !== $screen_id ) { - return; - } - - // Don't nag if Woo doesn't have an update available. - if ( ! self::_woo_core_update_available() ) { - return; - } - - // Add a note about available extension updates if Woo core has an update available. - $notice = self::_get_extensions_update_notice(); - if ( ! empty( $notice ) ) { - echo '

    ' . $notice . '

    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } - } - - /** - * Get an update notice if one or more Woo extensions has an update available. - * - * @return string|null The update notice or null if everything is up to date. - */ - private static function _get_extensions_update_notice() { - $plugins = self::get_local_woo_plugins(); - $updates = WC_Helper_Updater::get_update_data(); - $available = 0; - - foreach ( $plugins as $data ) { - if ( empty( $updates[ $data['_product_id'] ] ) ) { - continue; - } - - $product_id = $data['_product_id']; - if ( version_compare( $updates[ $product_id ]['version'], $data['Version'], '>' ) ) { - $available++; - } - } - - if ( ! $available ) { - return; - } - - return sprintf( - /* translators: %1$s: helper url, %2$d: number of extensions */ - _n( 'Note: You currently have %2$d paid extension which should be updated first before updating WooCommerce.', 'Note: You currently have %2$d paid extensions which should be updated first before updating WooCommerce.', $available, 'woocommerce' ), - admin_url( 'admin.php?page=wc-addons§ion=helper' ), - $available - ); - } - - /** - * Whether WooCommerce has an update available. - * - * @return bool True if a Woo core update is available. - */ - private static function _woo_core_update_available() { - $updates = get_site_transient( 'update_plugins' ); - if ( empty( $updates->response ) ) { - return false; - } - - if ( empty( $updates->response['woocommerce/woocommerce.php'] ) ) { - return false; - } - - $data = $updates->response['woocommerce/woocommerce.php']; - if ( version_compare( Constants::get_constant( 'WC_VERSION' ), $data->new_version, '>=' ) ) { - return false; - } - - return true; - } - - /** - * Flush subscriptions cache. - */ - public static function _flush_subscriptions_cache() { - delete_transient( '_woocommerce_helper_subscriptions' ); - } - - /** - * Flush auth cache. - */ - public static function _flush_authentication_cache() { - $request = WC_Helper_API::get( - 'oauth/me', - array( - 'authenticated' => true, - ) - ); - - if ( wp_remote_retrieve_response_code( $request ) !== 200 ) { - return false; - } - - $user_data = json_decode( wp_remote_retrieve_body( $request ), true ); - if ( ! $user_data ) { - return false; - } - - WC_Helper_Options::update( - 'auth_user_data', - array( - 'name' => $user_data['name'], - 'email' => $user_data['email'], - ) - ); - - return true; - } - - /** - * Flush updates cache. - */ - private static function _flush_updates_cache() { - WC_Helper_Updater::flush_updates_cache(); - } - - /** - * Sort subscriptions by the product_name. - * - * @param array $a Subscription array. - * @param array $b Subscription array. - * - * @return int - */ - public static function _sort_by_product_name( $a, $b ) { - return strcmp( $a['product_name'], $b['product_name'] ); - } - - /** - * Sort subscriptions by the Name. - * - * @param array $a Product array. - * @param array $b Product array. - * - * @return int - */ - public static function _sort_by_name( $a, $b ) { - return strcmp( $a['Name'], $b['Name'] ); - } - - /** - * Log a helper event. - * - * @param string $message Log message. - * @param string $level Optional, defaults to info, valid levels: emergency|alert|critical|error|warning|notice|info|debug. - */ - public static function log( $message, $level = 'info' ) { - if ( ! Constants::is_true( 'WP_DEBUG' ) ) { - return; - } - - if ( ! isset( self::$log ) ) { - self::$log = wc_get_logger(); - } - - self::$log->log( $level, $message, array( 'source' => 'helper' ) ); - } -} - -WC_Helper::load(); diff --git a/includes/admin/helper/views/html-main.php b/includes/admin/helper/views/html-main.php deleted file mode 100644 index 3023a192b14..00000000000 --- a/includes/admin/helper/views/html-main.php +++ /dev/null @@ -1,214 +0,0 @@ - - -
    - -

    - - - -
    -

    - -

    Plugins screen.', 'woocommerce' ), admin_url( 'plugins.php' ) ); ?>

    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - 0 ) { - /* translators: %1$d: sites active, %2$d max sites active */ - printf( __( 'Subscription: Using %1$d of %2$d sites available', 'woocommerce' ), absint( $subscription['sites_active'] ), absint( $subscription['sites_max'] ) ); - } else { - _e( 'Subscription: Unlimited', 'woocommerce' ); - } - - // Check shared. - if ( ! empty( $subscription['is_shared'] ) && ! empty( $subscription['owner_email'] ) ) { - printf( '
    ' . __( 'Shared by %s', 'woocommerce' ), esc_html( $subscription['owner_email'] ) ); - } elseif ( isset( $subscription['master_user_email'] ) ) { - printf( '
    ' . __( 'Shared by %s', 'woocommerce' ), esc_html( $subscription['master_user_email'] ) ); - } - ?> -
    -
    -
    - - - - - - - - - - - - - - - - - - - - -
    -

    - -

    -
    - - - -
    - - -

    -

    Below is a list of WooCommerce.com products available on your site - but are either out-dated or do not have a valid subscription.

    - - - - $data ) : ?> - - - - - - - - - - - - - - - - -
    -
    - -
    -
    -
    -
    - - - - -
    -

    - -

    -
    - -
    - -
    diff --git a/includes/admin/helper/views/html-oauth-start.php b/includes/admin/helper/views/html-oauth-start.php deleted file mode 100644 index 4b2e8cb0f13..00000000000 --- a/includes/admin/helper/views/html-oauth-start.php +++ /dev/null @@ -1,30 +0,0 @@ - WooCommerce -> Extensions -> WooCommerce.com Subscriptions main page. - * - * @package WooCommerce\Views - */ - -defined( 'ABSPATH' ) || exit(); - -?> - -
    - -

    - - -
    -
    - <?php esc_attr_e( 'WooCommerce', 'woocommerce' ); ?> - - -

    - - -

    -

    -

    -
    -
    -
    diff --git a/includes/admin/helper/views/html-section-nav.php b/includes/admin/helper/views/html-section-nav.php deleted file mode 100644 index 9af76fe32a0..00000000000 --- a/includes/admin/helper/views/html-section-nav.php +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/includes/admin/importers/class-wc-product-csv-importer-controller.php b/includes/admin/importers/class-wc-product-csv-importer-controller.php deleted file mode 100644 index 0de82507fdc..00000000000 --- a/includes/admin/importers/class-wc-product-csv-importer-controller.php +++ /dev/null @@ -1,748 +0,0 @@ - 'text/csv', - 'txt' => 'text/plain', - ) - ); - } - - /** - * Constructor. - */ - public function __construct() { - $default_steps = array( - 'upload' => array( - 'name' => __( 'Upload CSV file', 'woocommerce' ), - 'view' => array( $this, 'upload_form' ), - 'handler' => array( $this, 'upload_form_handler' ), - ), - 'mapping' => array( - 'name' => __( 'Column mapping', 'woocommerce' ), - 'view' => array( $this, 'mapping_form' ), - 'handler' => '', - ), - 'import' => array( - 'name' => __( 'Import', 'woocommerce' ), - 'view' => array( $this, 'import' ), - 'handler' => '', - ), - 'done' => array( - 'name' => __( 'Done!', 'woocommerce' ), - 'view' => array( $this, 'done' ), - 'handler' => '', - ), - ); - - $this->steps = apply_filters( 'woocommerce_product_csv_importer_steps', $default_steps ); - - // phpcs:disable WordPress.Security.NonceVerification.Recommended - $this->step = isset( $_REQUEST['step'] ) ? sanitize_key( $_REQUEST['step'] ) : current( array_keys( $this->steps ) ); - $this->file = isset( $_REQUEST['file'] ) ? wc_clean( wp_unslash( $_REQUEST['file'] ) ) : ''; - $this->update_existing = isset( $_REQUEST['update_existing'] ) ? (bool) $_REQUEST['update_existing'] : false; - $this->delimiter = ! empty( $_REQUEST['delimiter'] ) ? wc_clean( wp_unslash( $_REQUEST['delimiter'] ) ) : ','; - $this->map_preferences = isset( $_REQUEST['map_preferences'] ) ? (bool) $_REQUEST['map_preferences'] : false; - // phpcs:enable - - // Import mappings for CSV data. - include_once dirname( __FILE__ ) . '/mappings/mappings.php'; - - if ( $this->map_preferences ) { - add_filter( 'woocommerce_csv_product_import_mapped_columns', array( $this, 'auto_map_user_preferences' ), 9999 ); - } - } - - /** - * Get the URL for the next step's screen. - * - * @param string $step slug (default: current step). - * @return string URL for next step if a next step exists. - * Admin URL if it's the last step. - * Empty string on failure. - */ - public function get_next_step_link( $step = '' ) { - if ( ! $step ) { - $step = $this->step; - } - - $keys = array_keys( $this->steps ); - - if ( end( $keys ) === $step ) { - return admin_url(); - } - - $step_index = array_search( $step, $keys, true ); - - if ( false === $step_index ) { - return ''; - } - - $params = array( - 'step' => $keys[ $step_index + 1 ], - 'file' => str_replace( DIRECTORY_SEPARATOR, '/', $this->file ), - 'delimiter' => $this->delimiter, - 'update_existing' => $this->update_existing, - 'map_preferences' => $this->map_preferences, - '_wpnonce' => wp_create_nonce( 'woocommerce-csv-importer' ), // wp_nonce_url() escapes & to & breaking redirects. - ); - - return add_query_arg( $params ); - } - - /** - * Output header view. - */ - protected function output_header() { - include dirname( __FILE__ ) . '/views/html-csv-import-header.php'; - } - - /** - * Output steps view. - */ - protected function output_steps() { - include dirname( __FILE__ ) . '/views/html-csv-import-steps.php'; - } - - /** - * Output footer view. - */ - protected function output_footer() { - include dirname( __FILE__ ) . '/views/html-csv-import-footer.php'; - } - - /** - * Add error message. - * - * @param string $message Error message. - * @param array $actions List of actions with 'url' and 'label'. - */ - protected function add_error( $message, $actions = array() ) { - $this->errors[] = array( - 'message' => $message, - 'actions' => $actions, - ); - } - - /** - * Add error message. - */ - protected function output_errors() { - if ( ! $this->errors ) { - return; - } - - foreach ( $this->errors as $error ) { - echo '
    '; - echo '

    ' . esc_html( $error['message'] ) . '

    '; - - if ( ! empty( $error['actions'] ) ) { - echo '

    '; - foreach ( $error['actions'] as $action ) { - echo '' . esc_html( $action['label'] ) . ' '; - } - echo '

    '; - } - echo '
    '; - } - } - - /** - * Dispatch current step and show correct view. - */ - public function dispatch() { - // phpcs:ignore WordPress.Security.NonceVerification.Missing - if ( ! empty( $_POST['save_step'] ) && ! empty( $this->steps[ $this->step ]['handler'] ) ) { - call_user_func( $this->steps[ $this->step ]['handler'], $this ); - } - $this->output_header(); - $this->output_steps(); - $this->output_errors(); - call_user_func( $this->steps[ $this->step ]['view'], $this ); - $this->output_footer(); - } - - /** - * Output information about the uploading process. - */ - protected function upload_form() { - $bytes = apply_filters( 'import_upload_size_limit', wp_max_upload_size() ); - $size = size_format( $bytes ); - $upload_dir = wp_upload_dir(); - - include dirname( __FILE__ ) . '/views/html-product-csv-import-form.php'; - } - - /** - * Handle the upload form and store options. - */ - public function upload_form_handler() { - check_admin_referer( 'woocommerce-csv-importer' ); - - $file = $this->handle_upload(); - - if ( is_wp_error( $file ) ) { - $this->add_error( $file->get_error_message() ); - return; - } else { - $this->file = $file; - } - - wp_redirect( esc_url_raw( $this->get_next_step_link() ) ); - exit; - } - - /** - * Handles the CSV upload and initial parsing of the file to prepare for - * displaying author import options. - * - * @return string|WP_Error - */ - public function handle_upload() { - // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce already verified in WC_Product_CSV_Importer_Controller::upload_form_handler() - $file_url = isset( $_POST['file_url'] ) ? wc_clean( wp_unslash( $_POST['file_url'] ) ) : ''; - - if ( empty( $file_url ) ) { - if ( ! isset( $_FILES['import'] ) ) { - return new WP_Error( 'woocommerce_product_csv_importer_upload_file_empty', __( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your php.ini or by post_max_size being defined as smaller than upload_max_filesize in php.ini.', 'woocommerce' ) ); - } - - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated - if ( ! self::is_file_valid_csv( wc_clean( wp_unslash( $_FILES['import']['name'] ) ), false ) ) { - return new WP_Error( 'woocommerce_product_csv_importer_upload_file_invalid', __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); - } - - $overrides = array( - 'test_form' => false, - 'mimes' => self::get_valid_csv_filetypes(), - ); - $import = $_FILES['import']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash - $upload = wp_handle_upload( $import, $overrides ); - - if ( isset( $upload['error'] ) ) { - return new WP_Error( 'woocommerce_product_csv_importer_upload_error', $upload['error'] ); - } - - // Construct the object array. - $object = array( - 'post_title' => basename( $upload['file'] ), - 'post_content' => $upload['url'], - 'post_mime_type' => $upload['type'], - 'guid' => $upload['url'], - 'context' => 'import', - 'post_status' => 'private', - ); - - // Save the data. - $id = wp_insert_attachment( $object, $upload['file'] ); - - /* - * Schedule a cleanup for one day from now in case of failed - * import or missing wp_import_cleanup() call. - */ - wp_schedule_single_event( time() + DAY_IN_SECONDS, 'importer_scheduled_cleanup', array( $id ) ); - - return $upload['file']; - } elseif ( file_exists( ABSPATH . $file_url ) ) { - if ( ! self::is_file_valid_csv( ABSPATH . $file_url ) ) { - return new WP_Error( 'woocommerce_product_csv_importer_upload_file_invalid', __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); - } - - return ABSPATH . $file_url; - } - // phpcs:enable - - return new WP_Error( 'woocommerce_product_csv_importer_upload_invalid_file', __( 'Please upload or provide the link to a valid CSV file.', 'woocommerce' ) ); - } - - /** - * Mapping step. - */ - protected function mapping_form() { - check_admin_referer( 'woocommerce-csv-importer' ); - $args = array( - 'lines' => 1, - 'delimiter' => $this->delimiter, - ); - - $importer = self::get_importer( $this->file, $args ); - $headers = $importer->get_raw_keys(); - $mapped_items = $this->auto_map_columns( $headers ); - $sample = current( $importer->get_raw_data() ); - - if ( empty( $sample ) ) { - $this->add_error( - __( 'The file is empty or using a different encoding than UTF-8, please try again with a new file.', 'woocommerce' ), - array( - array( - 'url' => admin_url( 'edit.php?post_type=product&page=product_importer' ), - 'label' => __( 'Upload a new file', 'woocommerce' ), - ), - ) - ); - - // Force output the errors in the same page. - $this->output_errors(); - return; - } - - include_once dirname( __FILE__ ) . '/views/html-csv-import-mapping.php'; - } - - /** - * Import the file if it exists and is valid. - */ - public function import() { - // Displaying this page triggers Ajax action to run the import with a valid nonce, - // therefore this page needs to be nonce protected as well. - check_admin_referer( 'woocommerce-csv-importer' ); - - if ( ! self::is_file_valid_csv( $this->file ) ) { - $this->add_error( __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); - $this->output_errors(); - return; - } - - if ( ! is_file( $this->file ) ) { - $this->add_error( __( 'The file does not exist, please try again.', 'woocommerce' ) ); - $this->output_errors(); - return; - } - - if ( ! empty( $_POST['map_from'] ) && ! empty( $_POST['map_to'] ) ) { - $mapping_from = wc_clean( wp_unslash( $_POST['map_from'] ) ); - $mapping_to = wc_clean( wp_unslash( $_POST['map_to'] ) ); - - // Save mapping preferences for future imports. - update_user_option( get_current_user_id(), 'woocommerce_product_import_mapping', $mapping_to ); - } else { - wp_redirect( esc_url_raw( $this->get_next_step_link( 'upload' ) ) ); - exit; - } - - wp_localize_script( - 'wc-product-import', - 'wc_product_import_params', - array( - 'import_nonce' => wp_create_nonce( 'wc-product-import' ), - 'mapping' => array( - 'from' => $mapping_from, - 'to' => $mapping_to, - ), - 'file' => $this->file, - 'update_existing' => $this->update_existing, - 'delimiter' => $this->delimiter, - ) - ); - wp_enqueue_script( 'wc-product-import' ); - - include_once dirname( __FILE__ ) . '/views/html-csv-import-progress.php'; - } - - /** - * Done step. - */ - protected function done() { - check_admin_referer( 'woocommerce-csv-importer' ); - $imported = isset( $_GET['products-imported'] ) ? absint( $_GET['products-imported'] ) : 0; - $updated = isset( $_GET['products-updated'] ) ? absint( $_GET['products-updated'] ) : 0; - $failed = isset( $_GET['products-failed'] ) ? absint( $_GET['products-failed'] ) : 0; - $skipped = isset( $_GET['products-skipped'] ) ? absint( $_GET['products-skipped'] ) : 0; - $file_name = isset( $_GET['file-name'] ) ? sanitize_text_field( wp_unslash( $_GET['file-name'] ) ) : ''; - $errors = array_filter( (array) get_user_option( 'product_import_error_log' ) ); - - include_once dirname( __FILE__ ) . '/views/html-csv-import-done.php'; - } - - /** - * Columns to normalize. - * - * @param array $columns List of columns names and keys. - * @return array - */ - protected function normalize_columns_names( $columns ) { - $normalized = array(); - - foreach ( $columns as $key => $value ) { - $normalized[ strtolower( $key ) ] = $value; - } - - return $normalized; - } - - /** - * Auto map column names. - * - * @param array $raw_headers Raw header columns. - * @param bool $num_indexes If should use numbers or raw header columns as indexes. - * @return array - */ - protected function auto_map_columns( $raw_headers, $num_indexes = true ) { - $weight_unit = get_option( 'woocommerce_weight_unit' ); - $dimension_unit = get_option( 'woocommerce_dimension_unit' ); - - /* - * @hooked wc_importer_generic_mappings - 10 - * @hooked wc_importer_wordpress_mappings - 10 - * @hooked wc_importer_default_english_mappings - 100 - */ - $default_columns = $this->normalize_columns_names( - apply_filters( - 'woocommerce_csv_product_import_mapping_default_columns', - array( - __( 'ID', 'woocommerce' ) => 'id', - __( 'Type', 'woocommerce' ) => 'type', - __( 'SKU', 'woocommerce' ) => 'sku', - __( 'Name', 'woocommerce' ) => 'name', - __( 'Published', 'woocommerce' ) => 'published', - __( 'Is featured?', 'woocommerce' ) => 'featured', - __( 'Visibility in catalog', 'woocommerce' ) => 'catalog_visibility', - __( 'Short description', 'woocommerce' ) => 'short_description', - __( 'Description', 'woocommerce' ) => 'description', - __( 'Date sale price starts', 'woocommerce' ) => 'date_on_sale_from', - __( 'Date sale price ends', 'woocommerce' ) => 'date_on_sale_to', - __( 'Tax status', 'woocommerce' ) => 'tax_status', - __( 'Tax class', 'woocommerce' ) => 'tax_class', - __( 'In stock?', 'woocommerce' ) => 'stock_status', - __( 'Stock', 'woocommerce' ) => 'stock_quantity', - __( 'Backorders allowed?', 'woocommerce' ) => 'backorders', - __( 'Low stock amount', 'woocommerce' ) => 'low_stock_amount', - __( 'Sold individually?', 'woocommerce' ) => 'sold_individually', - /* translators: %s: Weight unit */ - sprintf( __( 'Weight (%s)', 'woocommerce' ), $weight_unit ) => 'weight', - /* translators: %s: Length unit */ - sprintf( __( 'Length (%s)', 'woocommerce' ), $dimension_unit ) => 'length', - /* translators: %s: Width unit */ - sprintf( __( 'Width (%s)', 'woocommerce' ), $dimension_unit ) => 'width', - /* translators: %s: Height unit */ - sprintf( __( 'Height (%s)', 'woocommerce' ), $dimension_unit ) => 'height', - __( 'Allow customer reviews?', 'woocommerce' ) => 'reviews_allowed', - __( 'Purchase note', 'woocommerce' ) => 'purchase_note', - __( 'Sale price', 'woocommerce' ) => 'sale_price', - __( 'Regular price', 'woocommerce' ) => 'regular_price', - __( 'Categories', 'woocommerce' ) => 'category_ids', - __( 'Tags', 'woocommerce' ) => 'tag_ids', - __( 'Shipping class', 'woocommerce' ) => 'shipping_class_id', - __( 'Images', 'woocommerce' ) => 'images', - __( 'Download limit', 'woocommerce' ) => 'download_limit', - __( 'Download expiry days', 'woocommerce' ) => 'download_expiry', - __( 'Parent', 'woocommerce' ) => 'parent_id', - __( 'Upsells', 'woocommerce' ) => 'upsell_ids', - __( 'Cross-sells', 'woocommerce' ) => 'cross_sell_ids', - __( 'Grouped products', 'woocommerce' ) => 'grouped_products', - __( 'External URL', 'woocommerce' ) => 'product_url', - __( 'Button text', 'woocommerce' ) => 'button_text', - __( 'Position', 'woocommerce' ) => 'menu_order', - ), - $raw_headers - ) - ); - - $special_columns = $this->get_special_columns( - $this->normalize_columns_names( - apply_filters( - 'woocommerce_csv_product_import_mapping_special_columns', - array( - /* translators: %d: Attribute number */ - __( 'Attribute %d name', 'woocommerce' ) => 'attributes:name', - /* translators: %d: Attribute number */ - __( 'Attribute %d value(s)', 'woocommerce' ) => 'attributes:value', - /* translators: %d: Attribute number */ - __( 'Attribute %d visible', 'woocommerce' ) => 'attributes:visible', - /* translators: %d: Attribute number */ - __( 'Attribute %d global', 'woocommerce' ) => 'attributes:taxonomy', - /* translators: %d: Attribute number */ - __( 'Attribute %d default', 'woocommerce' ) => 'attributes:default', - /* translators: %d: Download number */ - __( 'Download %d name', 'woocommerce' ) => 'downloads:name', - /* translators: %d: Download number */ - __( 'Download %d URL', 'woocommerce' ) => 'downloads:url', - /* translators: %d: Meta number */ - __( 'Meta: %s', 'woocommerce' ) => 'meta:', - ), - $raw_headers - ) - ) - ); - - $headers = array(); - foreach ( $raw_headers as $key => $field ) { - $normalized_field = strtolower( $field ); - $index = $num_indexes ? $key : $field; - $headers[ $index ] = $normalized_field; - - if ( isset( $default_columns[ $normalized_field ] ) ) { - $headers[ $index ] = $default_columns[ $normalized_field ]; - } else { - foreach ( $special_columns as $regex => $special_key ) { - // Don't use the normalized field in the regex since meta might be case-sensitive. - if ( preg_match( $regex, $field, $matches ) ) { - $headers[ $index ] = $special_key . $matches[1]; - break; - } - } - } - } - - return apply_filters( 'woocommerce_csv_product_import_mapped_columns', $headers, $raw_headers ); - } - - /** - * Map columns using the user's lastest import mappings. - * - * @param array $headers Header columns. - * @return array - */ - public function auto_map_user_preferences( $headers ) { - $mapping_preferences = get_user_option( 'woocommerce_product_import_mapping' ); - - if ( ! empty( $mapping_preferences ) && is_array( $mapping_preferences ) ) { - return $mapping_preferences; - } - - return $headers; - } - - /** - * Sanitize special column name regex. - * - * @param string $value Raw special column name. - * @return string - */ - protected function sanitize_special_column_name_regex( $value ) { - return '/' . str_replace( array( '%d', '%s' ), '(.*)', trim( quotemeta( $value ) ) ) . '/i'; - } - - /** - * Get special columns. - * - * @param array $columns Raw special columns. - * @return array - */ - protected function get_special_columns( $columns ) { - $formatted = array(); - - foreach ( $columns as $key => $value ) { - $regex = $this->sanitize_special_column_name_regex( $key ); - - $formatted[ $regex ] = $value; - } - - return $formatted; - } - - /** - * Get mapping options. - * - * @param string $item Item name. - * @return array - */ - protected function get_mapping_options( $item = '' ) { - // Get index for special column names. - $index = $item; - - if ( preg_match( '/\d+/', $item, $matches ) ) { - $index = $matches[0]; - } - - // Properly format for meta field. - $meta = str_replace( 'meta:', '', $item ); - - // Available options. - $weight_unit = get_option( 'woocommerce_weight_unit' ); - $dimension_unit = get_option( 'woocommerce_dimension_unit' ); - $options = array( - 'id' => __( 'ID', 'woocommerce' ), - 'type' => __( 'Type', 'woocommerce' ), - 'sku' => __( 'SKU', 'woocommerce' ), - 'name' => __( 'Name', 'woocommerce' ), - 'published' => __( 'Published', 'woocommerce' ), - 'featured' => __( 'Is featured?', 'woocommerce' ), - 'catalog_visibility' => __( 'Visibility in catalog', 'woocommerce' ), - 'short_description' => __( 'Short description', 'woocommerce' ), - 'description' => __( 'Description', 'woocommerce' ), - 'price' => array( - 'name' => __( 'Price', 'woocommerce' ), - 'options' => array( - 'regular_price' => __( 'Regular price', 'woocommerce' ), - 'sale_price' => __( 'Sale price', 'woocommerce' ), - 'date_on_sale_from' => __( 'Date sale price starts', 'woocommerce' ), - 'date_on_sale_to' => __( 'Date sale price ends', 'woocommerce' ), - ), - ), - 'tax_status' => __( 'Tax status', 'woocommerce' ), - 'tax_class' => __( 'Tax class', 'woocommerce' ), - 'stock_status' => __( 'In stock?', 'woocommerce' ), - 'stock_quantity' => _x( 'Stock', 'Quantity in stock', 'woocommerce' ), - 'backorders' => __( 'Backorders allowed?', 'woocommerce' ), - 'low_stock_amount' => __( 'Low stock amount', 'woocommerce' ), - 'sold_individually' => __( 'Sold individually?', 'woocommerce' ), - /* translators: %s: weight unit */ - 'weight' => sprintf( __( 'Weight (%s)', 'woocommerce' ), $weight_unit ), - 'dimensions' => array( - 'name' => __( 'Dimensions', 'woocommerce' ), - 'options' => array( - /* translators: %s: dimension unit */ - 'length' => sprintf( __( 'Length (%s)', 'woocommerce' ), $dimension_unit ), - /* translators: %s: dimension unit */ - 'width' => sprintf( __( 'Width (%s)', 'woocommerce' ), $dimension_unit ), - /* translators: %s: dimension unit */ - 'height' => sprintf( __( 'Height (%s)', 'woocommerce' ), $dimension_unit ), - ), - ), - 'category_ids' => __( 'Categories', 'woocommerce' ), - 'tag_ids' => __( 'Tags (comma separated)', 'woocommerce' ), - 'tag_ids_spaces' => __( 'Tags (space separated)', 'woocommerce' ), - 'shipping_class_id' => __( 'Shipping class', 'woocommerce' ), - 'images' => __( 'Images', 'woocommerce' ), - 'parent_id' => __( 'Parent', 'woocommerce' ), - 'upsell_ids' => __( 'Upsells', 'woocommerce' ), - 'cross_sell_ids' => __( 'Cross-sells', 'woocommerce' ), - 'grouped_products' => __( 'Grouped products', 'woocommerce' ), - 'external' => array( - 'name' => __( 'External product', 'woocommerce' ), - 'options' => array( - 'product_url' => __( 'External URL', 'woocommerce' ), - 'button_text' => __( 'Button text', 'woocommerce' ), - ), - ), - 'downloads' => array( - 'name' => __( 'Downloads', 'woocommerce' ), - 'options' => array( - 'downloads:name' . $index => __( 'Download name', 'woocommerce' ), - 'downloads:url' . $index => __( 'Download URL', 'woocommerce' ), - 'download_limit' => __( 'Download limit', 'woocommerce' ), - 'download_expiry' => __( 'Download expiry days', 'woocommerce' ), - ), - ), - 'attributes' => array( - 'name' => __( 'Attributes', 'woocommerce' ), - 'options' => array( - 'attributes:name' . $index => __( 'Attribute name', 'woocommerce' ), - 'attributes:value' . $index => __( 'Attribute value(s)', 'woocommerce' ), - 'attributes:taxonomy' . $index => __( 'Is a global attribute?', 'woocommerce' ), - 'attributes:visible' . $index => __( 'Attribute visibility', 'woocommerce' ), - 'attributes:default' . $index => __( 'Default attribute', 'woocommerce' ), - ), - ), - 'reviews_allowed' => __( 'Allow customer reviews?', 'woocommerce' ), - 'purchase_note' => __( 'Purchase note', 'woocommerce' ), - 'meta:' . $meta => __( 'Import as meta data', 'woocommerce' ), - 'menu_order' => __( 'Position', 'woocommerce' ), - ); - - return apply_filters( 'woocommerce_csv_product_import_mapping_options', $options, $item ); - } -} diff --git a/includes/admin/importers/class-wc-tax-rate-importer.php b/includes/admin/importers/class-wc-tax-rate-importer.php deleted file mode 100644 index 79f9cd10c09..00000000000 --- a/includes/admin/importers/class-wc-tax-rate-importer.php +++ /dev/null @@ -1,341 +0,0 @@ -import_page = 'woocommerce_tax_rate_csv'; - $this->delimiter = empty( $_POST['delimiter'] ) ? ',' : (string) wc_clean( wp_unslash( $_POST['delimiter'] ) ); // WPCS: CSRF ok. - } - - /** - * Registered callback function for the WordPress Importer. - * - * Manages the three separate stages of the CSV import process. - */ - public function dispatch() { - - $this->header(); - - $step = empty( $_GET['step'] ) ? 0 : (int) $_GET['step']; - - switch ( $step ) { - - case 0: - $this->greet(); - break; - - case 1: - check_admin_referer( 'import-upload' ); - - if ( $this->handle_upload() ) { - - if ( $this->id ) { - $file = get_attached_file( $this->id ); - } else { - $file = ABSPATH . $this->file_url; - } - - add_filter( 'http_request_timeout', array( $this, 'bump_request_timeout' ) ); - - $this->import( $file ); - } - break; - } - - $this->footer(); - } - - /** - * Import is starting. - */ - private function import_start() { - if ( function_exists( 'gc_enable' ) ) { - gc_enable(); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.gc_enableFound - } - wc_set_time_limit( 0 ); - @ob_flush(); - @flush(); - @ini_set( 'auto_detect_line_endings', '1' ); - } - - /** - * UTF-8 encode the data if `$enc` value isn't UTF-8. - * - * @param mixed $data Data. - * @param string $enc Encoding. - * @return string - */ - public function format_data_from_csv( $data, $enc ) { - return ( 'UTF-8' === $enc ) ? $data : utf8_encode( $data ); - } - - /** - * Import the file if it exists and is valid. - * - * @param mixed $file File. - */ - public function import( $file ) { - if ( ! is_file( $file ) ) { - $this->import_error( __( 'The file does not exist, please try again.', 'woocommerce' ) ); - } - - $this->import_start(); - - $loop = 0; - $handle = fopen( $file, 'r' ); - - if ( false !== $handle ) { - - $header = fgetcsv( $handle, 0, $this->delimiter ); - - if ( 10 === count( $header ) ) { - - $row = fgetcsv( $handle, 0, $this->delimiter ); - - while ( false !== $row ) { - - list( $country, $state, $postcode, $city, $rate, $name, $priority, $compound, $shipping, $class ) = $row; - - $tax_rate = array( - 'tax_rate_country' => $country, - 'tax_rate_state' => $state, - 'tax_rate' => $rate, - 'tax_rate_name' => $name, - 'tax_rate_priority' => $priority, - 'tax_rate_compound' => $compound ? 1 : 0, - 'tax_rate_shipping' => $shipping ? 1 : 0, - 'tax_rate_order' => $loop ++, - 'tax_rate_class' => $class, - ); - - $tax_rate_id = WC_Tax::_insert_tax_rate( $tax_rate ); - WC_Tax::_update_tax_rate_postcodes( $tax_rate_id, wc_clean( $postcode ) ); - WC_Tax::_update_tax_rate_cities( $tax_rate_id, wc_clean( $city ) ); - - $row = fgetcsv( $handle, 0, $this->delimiter ); - } - } else { - $this->import_error( __( 'The CSV is invalid.', 'woocommerce' ) ); - } - - fclose( $handle ); - } - - // Show Result. - echo '

    '; - printf( - /* translators: %s: tax rates count */ - esc_html__( 'Import complete - imported %s tax rates.', 'woocommerce' ), - '' . absint( $loop ) . '' - ); - echo '

    '; - - $this->import_end(); - } - - /** - * Performs post-import cleanup of files and the cache. - */ - public function import_end() { - echo '

    ' . esc_html__( 'All done!', 'woocommerce' ) . ' ' . esc_html__( 'View tax rates', 'woocommerce' ) . '

    '; - - do_action( 'import_end' ); - } - - /** - * Handles the CSV upload and initial parsing of the file to prepare for. - * displaying author import options. - * - * @return bool False if error uploading or invalid file, true otherwise - */ - public function handle_upload() { - $file_url = isset( $_POST['file_url'] ) ? wc_clean( wp_unslash( $_POST['file_url'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce already verified in WC_Tax_Rate_Importer::dispatch() - - if ( empty( $file_url ) ) { - $file = wp_import_handle_upload(); - - if ( isset( $file['error'] ) ) { - $this->import_error( $file['error'] ); - } - - if ( ! wc_is_file_valid_csv( $file['file'], false ) ) { - // Remove file if not valid. - wp_delete_attachment( $file['id'], true ); - - $this->import_error( __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); - } - - $this->id = absint( $file['id'] ); - } elseif ( file_exists( ABSPATH . $file_url ) ) { - if ( ! wc_is_file_valid_csv( ABSPATH . $file_url ) ) { - $this->import_error( __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); - } - - $this->file_url = esc_attr( $file_url ); - } else { - $this->import_error(); - } - - return true; - } - - /** - * Output header html. - */ - public function header() { - echo '
    '; - echo '

    ' . esc_html__( 'Import tax rates', 'woocommerce' ) . '

    '; - } - - /** - * Output footer html. - */ - public function footer() { - echo '
    '; - } - - /** - * Output information about the uploading process. - */ - public function greet() { - - echo '
    '; - echo '

    ' . esc_html__( 'Hi there! Upload a CSV file containing tax rates to import the contents into your shop. Choose a .csv file to upload, then click "Upload file and import".', 'woocommerce' ) . '

    '; - - /* translators: 1: Link to tax rates sample file 2: Closing link. */ - echo '

    ' . sprintf( esc_html__( 'Your CSV needs to include columns in a specific order. %1$sClick here to download a sample%2$s.', 'woocommerce' ), '', '' ) . '

    '; - - $action = 'admin.php?import=woocommerce_tax_rate_csv&step=1'; - - $bytes = apply_filters( 'import_upload_size_limit', wp_max_upload_size() ); - $size = size_format( $bytes ); - $upload_dir = wp_upload_dir(); - if ( ! empty( $upload_dir['error'] ) ) : - ?> -
    -

    -

    -
    - -
    - - - - - - - - - - - - - - - -
    - - - - - - - - -
    - - - -

    -

    - -

    -
    - '; - } - - /** - * Show import error and quit. - * - * @param string $message Error message. - */ - private function import_error( $message = '' ) { - echo '

    ' . esc_html__( 'Sorry, there has been an error.', 'woocommerce' ) . '
    '; - if ( $message ) { - echo esc_html( $message ); - } - echo '

    '; - $this->footer(); - die(); - } - - /** - * Added to http_request_timeout filter to force timeout at 60 seconds during import. - * - * @param int $val Value. - * @return int 60 - */ - public function bump_request_timeout( $val ) { - return 60; - } -} diff --git a/includes/admin/importers/mappings/default.php b/includes/admin/importers/mappings/default.php deleted file mode 100644 index 4de9337d89c..00000000000 --- a/includes/admin/importers/mappings/default.php +++ /dev/null @@ -1,112 +0,0 @@ - 'id', - 'Type' => 'type', - 'SKU' => 'sku', - 'Name' => 'name', - 'Published' => 'published', - 'Is featured?' => 'featured', - 'Visibility in catalog' => 'catalog_visibility', - 'Short description' => 'short_description', - 'Description' => 'description', - 'Date sale price starts' => 'date_on_sale_from', - 'Date sale price ends' => 'date_on_sale_to', - 'Tax status' => 'tax_status', - 'Tax class' => 'tax_class', - 'In stock?' => 'stock_status', - 'Stock' => 'stock_quantity', - 'Backorders allowed?' => 'backorders', - 'Low stock amount' => 'low_stock_amount', - 'Sold individually?' => 'sold_individually', - sprintf( 'Weight (%s)', $weight_unit ) => 'weight', - sprintf( 'Length (%s)', $dimension_unit ) => 'length', - sprintf( 'Width (%s)', $dimension_unit ) => 'width', - sprintf( 'Height (%s)', $dimension_unit ) => 'height', - 'Allow customer reviews?' => 'reviews_allowed', - 'Purchase note' => 'purchase_note', - 'Sale price' => 'sale_price', - 'Regular price' => 'regular_price', - 'Categories' => 'category_ids', - 'Tags' => 'tag_ids', - 'Shipping class' => 'shipping_class_id', - 'Images' => 'images', - 'Download limit' => 'download_limit', - 'Download expiry days' => 'download_expiry', - 'Parent' => 'parent_id', - 'Upsells' => 'upsell_ids', - 'Cross-sells' => 'cross_sell_ids', - 'Grouped products' => 'grouped_products', - 'External URL' => 'product_url', - 'Button text' => 'button_text', - 'Position' => 'menu_order', - ); - - return array_merge( $mappings, $new_mappings ); -} -add_filter( 'woocommerce_csv_product_import_mapping_default_columns', 'wc_importer_default_english_mappings', 100 ); - -/** - * Add English special mapping placeholders when not using English as current language. - * - * @since 3.1.0 - * @param array $mappings Importer columns mappings. - * @return array - */ -function wc_importer_default_special_english_mappings( $mappings ) { - if ( 'en_US' === wc_importer_current_locale() ) { - return $mappings; - } - - $new_mappings = array( - 'Attribute %d name' => 'attributes:name', - 'Attribute %d value(s)' => 'attributes:value', - 'Attribute %d visible' => 'attributes:visible', - 'Attribute %d global' => 'attributes:taxonomy', - 'Attribute %d default' => 'attributes:default', - 'Download %d name' => 'downloads:name', - 'Download %d URL' => 'downloads:url', - 'Meta: %s' => 'meta:', - ); - - return array_merge( $mappings, $new_mappings ); -} -add_filter( 'woocommerce_csv_product_import_mapping_special_columns', 'wc_importer_default_special_english_mappings', 100 ); diff --git a/includes/admin/importers/views/html-csv-import-done.php b/includes/admin/importers/views/html-csv-import-done.php deleted file mode 100644 index 1774001d014..00000000000 --- a/includes/admin/importers/views/html-csv-import-done.php +++ /dev/null @@ -1,104 +0,0 @@ - -
    -
    - ' . number_format_i18n( $imported ) . '' - ); - } - - if ( 0 < $updated ) { - $results[] = sprintf( - /* translators: %d: products count */ - _n( '%s product updated', '%s products updated', $updated, 'woocommerce' ), - '' . number_format_i18n( $updated ) . '' - ); - } - - if ( 0 < $skipped ) { - $results[] = sprintf( - /* translators: %d: products count */ - _n( '%s product was skipped', '%s products were skipped', $skipped, 'woocommerce' ), - '' . number_format_i18n( $skipped ) . '' - ); - } - - if ( 0 < $failed ) { - $results [] = sprintf( - /* translators: %d: products count */ - _n( 'Failed to import %s product', 'Failed to import %s products', $failed, 'woocommerce' ), - '' . number_format_i18n( $failed ) . '' - ); - } - - if ( 0 < $failed || 0 < $skipped ) { - $results[] = '' . __( 'View import log', 'woocommerce' ) . ''; - } - - if ( ! empty( $file_name ) ) { - $results[] = sprintf( - /* translators: %s: File name */ - __( 'File uploaded: %s', 'woocommerce' ), - '' . $file_name . '' - ); - } - - /* translators: %d: import results */ - echo wp_kses_post( __( 'Import complete!', 'woocommerce' ) . ' ' . implode( '. ', $results ) ); - ?> -
    - - -
    - -
    -
    diff --git a/includes/admin/list-tables/class-wc-admin-list-table-coupons.php b/includes/admin/list-tables/class-wc-admin-list-table-coupons.php deleted file mode 100644 index 91f499abef3..00000000000 --- a/includes/admin/list-tables/class-wc-admin-list-table-coupons.php +++ /dev/null @@ -1,232 +0,0 @@ -'; - echo '

    ' . esc_html__( 'Coupons are a great way to offer discounts and rewards to your customers. They will appear here once created.', 'woocommerce' ) . '

    '; - echo '' . esc_html__( 'Create your first coupon', 'woocommerce' ) . ''; - echo '' . esc_html__( 'Learn more about coupons', 'woocommerce' ) . ''; - echo '
    '; - } - - /** - * Define primary column. - * - * @return string - */ - protected function get_primary_column() { - return 'coupon_code'; - } - - /** - * Get row actions to show in the list table. - * - * @param array $actions Array of actions. - * @param WP_Post $post Current post object. - * @return array - */ - protected function get_row_actions( $actions, $post ) { - unset( $actions['inline hide-if-no-js'] ); - return $actions; - } - - /** - * Define which columns to show on this screen. - * - * @param array $columns Existing columns. - * @return array - */ - public function define_columns( $columns ) { - $show_columns = array(); - $show_columns['cb'] = $columns['cb']; - $show_columns['coupon_code'] = __( 'Code', 'woocommerce' ); - $show_columns['type'] = __( 'Coupon type', 'woocommerce' ); - $show_columns['amount'] = __( 'Coupon amount', 'woocommerce' ); - $show_columns['description'] = __( 'Description', 'woocommerce' ); - $show_columns['products'] = __( 'Product IDs', 'woocommerce' ); - $show_columns['usage'] = __( 'Usage / Limit', 'woocommerce' ); - $show_columns['expiry_date'] = __( 'Expiry date', 'woocommerce' ); - - return $show_columns; - } - - /** - * Pre-fetch any data for the row each column has access to it. the_coupon global is there for bw compat. - * - * @param int $post_id Post ID being shown. - */ - protected function prepare_row_data( $post_id ) { - global $the_coupon; - - if ( empty( $this->object ) || $this->object->get_id() !== $post_id ) { - $this->object = new WC_Coupon( $post_id ); - $the_coupon = $this->object; - } - } - - /** - * Render columm: coupon_code. - */ - protected function render_coupon_code_column() { - global $post; - - $edit_link = get_edit_post_link( $this->object->get_id() ); - $title = $this->object->get_code(); - - echo '' . esc_html( $title ) . ''; - _post_states( $post ); - echo ''; - } - - /** - * Render columm: type. - */ - protected function render_type_column() { - echo esc_html( wc_get_coupon_type( $this->object->get_discount_type() ) ); - } - - /** - * Render columm: amount. - */ - protected function render_amount_column() { - echo esc_html( wc_format_localized_price( $this->object->get_amount() ) ); - } - /** - * Render columm: products. - */ - protected function render_products_column() { - $product_ids = $this->object->get_product_ids(); - - if ( count( $product_ids ) > 0 ) { - echo esc_html( implode( ', ', $product_ids ) ); - } else { - echo '–'; - } - } - - /** - * Render columm: usage_limit. - */ - protected function render_usage_limit_column() { - $usage_limit = $this->object->get_usage_limit(); - - if ( $usage_limit ) { - echo esc_html( $usage_limit ); - } else { - echo '–'; - } - } - - /** - * Render columm: usage. - */ - protected function render_usage_column() { - $usage_count = $this->object->get_usage_count(); - $usage_limit = $this->object->get_usage_limit(); - - printf( - /* translators: 1: count 2: limit */ - __( '%1$s / %2$s', 'woocommerce' ), - esc_html( $usage_count ), - $usage_limit ? esc_html( $usage_limit ) : '∞' - ); - } - - /** - * Render columm: expiry_date. - */ - protected function render_expiry_date_column() { - $expiry_date = $this->object->get_date_expires(); - - if ( $expiry_date ) { - echo esc_html( $expiry_date->date_i18n( 'F j, Y' ) ); - } else { - echo '–'; - } - } - - /** - * Render columm: description. - */ - protected function render_description_column() { - echo wp_kses_post( $this->object->get_description() ? $this->object->get_description() : '–' ); - } - - /** - * Render any custom filters and search inputs for the list table. - */ - protected function render_filters() { - ?> - - '; - - echo '

    ' . esc_html__( 'When you receive a new order, it will appear here.', 'woocommerce' ) . '

    '; - - echo '
    '; - echo '' . esc_html__( 'Learn more about orders', 'woocommerce' ) . ''; - echo '
    '; - - do_action( 'wc_marketplace_suggestions_orders_empty_state' ); - - echo ''; - } - - /** - * Define primary column. - * - * @return string - */ - protected function get_primary_column() { - return 'order_number'; - } - - /** - * Get row actions to show in the list table. - * - * @param array $actions Array of actions. - * @param WP_Post $post Current post object. - * @return array - */ - protected function get_row_actions( $actions, $post ) { - return array(); - } - - /** - * Define hidden columns. - * - * @return array - */ - protected function define_hidden_columns() { - return array( - 'shipping_address', - 'billing_address', - 'wc_actions', - ); - } - - /** - * Define which columns are sortable. - * - * @param array $columns Existing columns. - * @return array - */ - public function define_sortable_columns( $columns ) { - $custom = array( - 'order_number' => 'ID', - 'order_total' => 'order_total', - 'order_date' => 'date', - ); - unset( $columns['comments'] ); - - return wp_parse_args( $custom, $columns ); - } - - /** - * Define which columns to show on this screen. - * - * @param array $columns Existing columns. - * @return array - */ - public function define_columns( $columns ) { - $show_columns = array(); - $show_columns['cb'] = $columns['cb']; - $show_columns['order_number'] = __( 'Order', 'woocommerce' ); - $show_columns['order_date'] = __( 'Date', 'woocommerce' ); - $show_columns['order_status'] = __( 'Status', 'woocommerce' ); - $show_columns['billing_address'] = __( 'Billing', 'woocommerce' ); - $show_columns['shipping_address'] = __( 'Ship to', 'woocommerce' ); - $show_columns['order_total'] = __( 'Total', 'woocommerce' ); - $show_columns['wc_actions'] = __( 'Actions', 'woocommerce' ); - - wp_enqueue_script( 'wc-orders' ); - - return $show_columns; - } - - /** - * Define bulk actions. - * - * @param array $actions Existing actions. - * @return array - */ - public function define_bulk_actions( $actions ) { - if ( isset( $actions['edit'] ) ) { - unset( $actions['edit'] ); - } - - $actions['mark_processing'] = __( 'Change status to processing', 'woocommerce' ); - $actions['mark_on-hold'] = __( 'Change status to on-hold', 'woocommerce' ); - $actions['mark_completed'] = __( 'Change status to completed', 'woocommerce' ); - - if ( wc_string_to_bool( get_option( 'woocommerce_allow_bulk_remove_personal_data', 'no' ) ) ) { - $actions['remove_personal_data'] = __( 'Remove personal data', 'woocommerce' ); - } - - return $actions; - } - - /** - * Pre-fetch any data for the row each column has access to it. the_order global is there for bw compat. - * - * @param int $post_id Post ID being shown. - */ - protected function prepare_row_data( $post_id ) { - global $the_order; - - if ( empty( $this->object ) || $this->object->get_id() !== $post_id ) { - $this->object = wc_get_order( $post_id ); - $the_order = $this->object; - } - } - - /** - * Render columm: order_number. - */ - protected function render_order_number_column() { - $buyer = ''; - - if ( $this->object->get_billing_first_name() || $this->object->get_billing_last_name() ) { - /* translators: 1: first name 2: last name */ - $buyer = trim( sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $this->object->get_billing_first_name(), $this->object->get_billing_last_name() ) ); - } elseif ( $this->object->get_billing_company() ) { - $buyer = trim( $this->object->get_billing_company() ); - } elseif ( $this->object->get_customer_id() ) { - $user = get_user_by( 'id', $this->object->get_customer_id() ); - $buyer = ucwords( $user->display_name ); - } - - /** - * Filter buyer name in list table orders. - * - * @since 3.7.0 - * @param string $buyer Buyer name. - * @param WC_Order $order Order data. - */ - $buyer = apply_filters( 'woocommerce_admin_order_buyer_name', $buyer, $this->object ); - - if ( $this->object->get_status() === 'trash' ) { - echo '#' . esc_attr( $this->object->get_order_number() ) . ' ' . esc_html( $buyer ) . ''; - } else { - echo '' . esc_html( __( 'Preview', 'woocommerce' ) ) . ''; - echo '#' . esc_attr( $this->object->get_order_number() ) . ' ' . esc_html( $buyer ) . ''; - } - } - - /** - * Render columm: order_status. - */ - protected function render_order_status_column() { - $tooltip = ''; - $comment_count = get_comment_count( $this->object->get_id() ); - $approved_comments_count = absint( $comment_count['approved'] ); - - if ( $approved_comments_count ) { - $latest_notes = wc_get_order_notes( - array( - 'order_id' => $this->object->get_id(), - 'limit' => 1, - 'orderby' => 'date_created_gmt', - ) - ); - - $latest_note = current( $latest_notes ); - - if ( isset( $latest_note->content ) && 1 === $approved_comments_count ) { - $tooltip = wc_sanitize_tooltip( $latest_note->content ); - } elseif ( isset( $latest_note->content ) ) { - /* translators: %d: notes count */ - $tooltip = wc_sanitize_tooltip( $latest_note->content . '
    ' . sprintf( _n( 'Plus %d other note', 'Plus %d other notes', ( $approved_comments_count - 1 ), 'woocommerce' ), $approved_comments_count - 1 ) . '' ); - } else { - /* translators: %d: notes count */ - $tooltip = wc_sanitize_tooltip( sprintf( _n( '%d note', '%d notes', $approved_comments_count, 'woocommerce' ), $approved_comments_count ) ); - } - } - - if ( $tooltip ) { - printf( '%s', esc_attr( sanitize_html_class( 'status-' . $this->object->get_status() ) ), wp_kses_post( $tooltip ), esc_html( wc_get_order_status_name( $this->object->get_status() ) ) ); - } else { - printf( '%s', esc_attr( sanitize_html_class( 'status-' . $this->object->get_status() ) ), esc_html( wc_get_order_status_name( $this->object->get_status() ) ) ); - } - } - - /** - * Render columm: order_date. - */ - protected function render_order_date_column() { - $order_timestamp = $this->object->get_date_created() ? $this->object->get_date_created()->getTimestamp() : ''; - - if ( ! $order_timestamp ) { - echo '–'; - return; - } - - // Check if the order was created within the last 24 hours, and not in the future. - if ( $order_timestamp > strtotime( '-1 day', time() ) && $order_timestamp <= time() ) { - $show_date = sprintf( - /* translators: %s: human-readable time difference */ - _x( '%s ago', '%s = human-readable time difference', 'woocommerce' ), - human_time_diff( $this->object->get_date_created()->getTimestamp(), time() ) - ); - } else { - $show_date = $this->object->get_date_created()->date_i18n( apply_filters( 'woocommerce_admin_order_date_format', __( 'M j, Y', 'woocommerce' ) ) ); - } - printf( - '', - esc_attr( $this->object->get_date_created()->date( 'c' ) ), - esc_html( $this->object->get_date_created()->date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ) ), - esc_html( $show_date ) - ); - } - - /** - * Render columm: order_total. - */ - protected function render_order_total_column() { - if ( $this->object->get_payment_method_title() ) { - /* translators: %s: method */ - echo '' . wp_kses_post( $this->object->get_formatted_order_total() ) . ''; - } else { - echo wp_kses_post( $this->object->get_formatted_order_total() ); - } - } - - /** - * Render columm: wc_actions. - */ - protected function render_wc_actions_column() { - echo '

    '; - - do_action( 'woocommerce_admin_order_actions_start', $this->object ); - - $actions = array(); - - if ( $this->object->has_status( array( 'pending', 'on-hold' ) ) ) { - $actions['processing'] = array( - 'url' => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=processing&order_id=' . $this->object->get_id() ), 'woocommerce-mark-order-status' ), - 'name' => __( 'Processing', 'woocommerce' ), - 'action' => 'processing', - ); - } - - if ( $this->object->has_status( array( 'pending', 'on-hold', 'processing' ) ) ) { - $actions['complete'] = array( - 'url' => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=completed&order_id=' . $this->object->get_id() ), 'woocommerce-mark-order-status' ), - 'name' => __( 'Complete', 'woocommerce' ), - 'action' => 'complete', - ); - } - - $actions = apply_filters( 'woocommerce_admin_order_actions', $actions, $this->object ); - - echo wc_render_action_buttons( $actions ); // WPCS: XSS ok. - - do_action( 'woocommerce_admin_order_actions_end', $this->object ); - - echo '

    '; - } - - /** - * Render columm: billing_address. - */ - protected function render_billing_address_column() { - $address = $this->object->get_formatted_billing_address(); - - if ( $address ) { - echo esc_html( preg_replace( '##i', ', ', $address ) ); - - if ( $this->object->get_payment_method() ) { - /* translators: %s: payment method */ - echo '' . sprintf( __( 'via %s', 'woocommerce' ), esc_html( $this->object->get_payment_method_title() ) ) . ''; // WPCS: XSS ok. - } - } else { - echo '–'; - } - } - - /** - * Render columm: shipping_address. - */ - protected function render_shipping_address_column() { - $address = $this->object->get_formatted_shipping_address(); - - if ( $address ) { - echo '' . esc_html( preg_replace( '##i', ', ', $address ) ) . ''; - if ( $this->object->get_shipping_method() ) { - /* translators: %s: shipping method */ - echo '' . sprintf( __( 'via %s', 'woocommerce' ), esc_html( $this->object->get_shipping_method() ) ) . ''; // WPCS: XSS ok. - } - } else { - echo '–'; - } - } - - /** - * Template for order preview. - * - * @since 3.3.0 - */ - public function order_preview_template() { - ?> - - get_items(), $order ); - $columns = apply_filters( - 'woocommerce_admin_order_preview_line_item_columns', - array( - 'product' => __( 'Product', 'woocommerce' ), - 'quantity' => __( 'Quantity', 'woocommerce' ), - 'tax' => __( 'Tax', 'woocommerce' ), - 'total' => __( 'Total', 'woocommerce' ), - ), - $order - ); - - if ( ! wc_tax_enabled() ) { - unset( $columns['tax'] ); - } - - $html = ' -
    - - - '; - - foreach ( $columns as $column => $label ) { - $html .= ''; - } - - $html .= ' - - - '; - - foreach ( $line_items as $item_id => $item ) { - - $product_object = is_callable( array( $item, 'get_product' ) ) ? $item->get_product() : null; - $row_class = apply_filters( 'woocommerce_admin_html_order_preview_item_class', '', $item, $order ); - - $html .= ''; - - foreach ( $columns as $column => $label ) { - $html .= ''; - } - - $html .= ''; - } - - $html .= ' - -
    ' . esc_html( $label ) . '
    '; - switch ( $column ) { - case 'product': - $html .= wp_kses_post( $item->get_name() ); - - if ( $product_object ) { - $html .= '
    ' . esc_html( $product_object->get_sku() ) . '
    '; - } - - $meta_data = $item->get_formatted_meta_data( '' ); - - if ( $meta_data ) { - $html .= ''; - - foreach ( $meta_data as $meta_id => $meta ) { - if ( in_array( $meta->key, $hidden_order_itemmeta, true ) ) { - continue; - } - $html .= ''; - } - $html .= '
    ' . wp_kses_post( $meta->display_key ) . ':' . wp_kses_post( force_balance_tags( $meta->display_value ) ) . '
    '; - } - break; - case 'quantity': - $html .= esc_html( $item->get_quantity() ); - break; - case 'tax': - $html .= wc_price( $item->get_total_tax(), array( 'currency' => $order->get_currency() ) ); - break; - case 'total': - $html .= wc_price( $item->get_total(), array( 'currency' => $order->get_currency() ) ); - break; - default: - $html .= apply_filters( 'woocommerce_admin_order_preview_line_item_column_' . sanitize_key( $column ), '', $item, $item_id, $order ); - break; - } - $html .= '
    -
    '; - - return $html; - } - - /** - * Get actions to display in the preview as HTML. - * - * @param WC_Order $order Order object. - * @return string - */ - public static function get_order_preview_actions_html( $order ) { - $actions = array(); - $status_actions = array(); - - if ( $order->has_status( array( 'pending' ) ) ) { - $status_actions['on-hold'] = array( - 'url' => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=on-hold&order_id=' . $order->get_id() ), 'woocommerce-mark-order-status' ), - 'name' => __( 'On-hold', 'woocommerce' ), - 'title' => __( 'Change order status to on-hold', 'woocommerce' ), - 'action' => 'on-hold', - ); - } - - if ( $order->has_status( array( 'pending', 'on-hold' ) ) ) { - $status_actions['processing'] = array( - 'url' => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=processing&order_id=' . $order->get_id() ), 'woocommerce-mark-order-status' ), - 'name' => __( 'Processing', 'woocommerce' ), - 'title' => __( 'Change order status to processing', 'woocommerce' ), - 'action' => 'processing', - ); - } - - if ( $order->has_status( array( 'pending', 'on-hold', 'processing' ) ) ) { - $status_actions['complete'] = array( - 'url' => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=completed&order_id=' . $order->get_id() ), 'woocommerce-mark-order-status' ), - 'name' => __( 'Completed', 'woocommerce' ), - 'title' => __( 'Change order status to completed', 'woocommerce' ), - 'action' => 'complete', - ); - } - - if ( $status_actions ) { - $actions['status'] = array( - 'group' => __( 'Change status: ', 'woocommerce' ), - 'actions' => $status_actions, - ); - } - - return wc_render_action_buttons( apply_filters( 'woocommerce_admin_order_preview_actions', $actions, $order ) ); - } - - /** - * Get order details to send to the ajax endpoint for previews. - * - * @param WC_Order $order Order object. - * @return array - */ - public static function order_preview_get_order_details( $order ) { - if ( ! $order ) { - return array(); - } - - $payment_via = $order->get_payment_method_title(); - $payment_method = $order->get_payment_method(); - $payment_gateways = WC()->payment_gateways() ? WC()->payment_gateways->payment_gateways() : array(); - $transaction_id = $order->get_transaction_id(); - - if ( $transaction_id ) { - - $url = isset( $payment_gateways[ $payment_method ] ) ? $payment_gateways[ $payment_method ]->get_transaction_url( $order ) : false; - - if ( $url ) { - $payment_via .= ' (' . esc_html( $transaction_id ) . ')'; - } else { - $payment_via .= ' (' . esc_html( $transaction_id ) . ')'; - } - } - - $billing_address = $order->get_formatted_billing_address(); - $shipping_address = $order->get_formatted_shipping_address(); - - return apply_filters( - 'woocommerce_admin_order_preview_get_order_details', - array( - 'data' => $order->get_data(), - 'order_number' => $order->get_order_number(), - 'item_html' => self::get_order_preview_item_html( $order ), - 'actions_html' => self::get_order_preview_actions_html( $order ), - 'ship_to_billing' => wc_ship_to_billing_address_only(), - 'needs_shipping' => $order->needs_shipping_address(), - 'formatted_billing_address' => $billing_address ? $billing_address : __( 'N/A', 'woocommerce' ), - 'formatted_shipping_address' => $shipping_address ? $shipping_address : __( 'N/A', 'woocommerce' ), - 'shipping_address_map_url' => $order->get_shipping_address_map_url(), - 'payment_via' => $payment_via, - 'shipping_via' => $order->get_shipping_method(), - 'status' => $order->get_status(), - 'status_name' => wc_get_order_status_name( $order->get_status() ), - ), - $order - ); - } - - /** - * Handle bulk actions. - * - * @param string $redirect_to URL to redirect to. - * @param string $action Action name. - * @param array $ids List of ids. - * @return string - */ - public function handle_bulk_actions( $redirect_to, $action, $ids ) { - $ids = apply_filters( 'woocommerce_bulk_action_ids', array_reverse( array_map( 'absint', $ids ) ), $action, 'order' ); - $changed = 0; - - if ( 'remove_personal_data' === $action ) { - $report_action = 'removed_personal_data'; - - foreach ( $ids as $id ) { - $order = wc_get_order( $id ); - - if ( $order ) { - do_action( 'woocommerce_remove_order_personal_data', $order ); - $changed++; - } - } - } elseif ( false !== strpos( $action, 'mark_' ) ) { - $order_statuses = wc_get_order_statuses(); - $new_status = substr( $action, 5 ); // Get the status name from action. - $report_action = 'marked_' . $new_status; - - // Sanity check: bail out if this is actually not a status, or is not a registered status. - if ( isset( $order_statuses[ 'wc-' . $new_status ] ) ) { - // Initialize payment gateways in case order has hooked status transition actions. - WC()->payment_gateways(); - - foreach ( $ids as $id ) { - $order = wc_get_order( $id ); - $order->update_status( $new_status, __( 'Order status changed by bulk edit:', 'woocommerce' ), true ); - do_action( 'woocommerce_order_edit_status', $id, $new_status ); - $changed++; - } - } - } - - if ( $changed ) { - $redirect_to = add_query_arg( - array( - 'post_type' => $this->list_table_type, - 'bulk_action' => $report_action, - 'changed' => $changed, - 'ids' => join( ',', $ids ), - ), - $redirect_to - ); - } - - return esc_url_raw( $redirect_to ); - } - - /** - * Show confirmation message that order status changed for number of orders. - */ - public function bulk_admin_notices() { - global $post_type, $pagenow; - - // Bail out if not on shop order list page. - if ( 'edit.php' !== $pagenow || 'shop_order' !== $post_type || ! isset( $_REQUEST['bulk_action'] ) ) { // WPCS: input var ok, CSRF ok. - return; - } - - $order_statuses = wc_get_order_statuses(); - $number = isset( $_REQUEST['changed'] ) ? absint( $_REQUEST['changed'] ) : 0; // WPCS: input var ok, CSRF ok. - $bulk_action = wc_clean( wp_unslash( $_REQUEST['bulk_action'] ) ); // WPCS: input var ok, CSRF ok. - - // Check if any status changes happened. - foreach ( $order_statuses as $slug => $name ) { - if ( 'marked_' . str_replace( 'wc-', '', $slug ) === $bulk_action ) { // WPCS: input var ok, CSRF ok. - /* translators: %d: orders count */ - $message = sprintf( _n( '%d order status changed.', '%d order statuses changed.', $number, 'woocommerce' ), number_format_i18n( $number ) ); - echo '

    ' . esc_html( $message ) . '

    '; - break; - } - } - - if ( 'removed_personal_data' === $bulk_action ) { // WPCS: input var ok, CSRF ok. - /* translators: %d: orders count */ - $message = sprintf( _n( 'Removed personal data from %d order.', 'Removed personal data from %d orders.', $number, 'woocommerce' ), number_format_i18n( $number ) ); - echo '

    ' . esc_html( $message ) . '

    '; - } - } - - /** - * See if we should render search filters or not. - */ - public function restrict_manage_posts() { - global $typenow; - - if ( in_array( $typenow, wc_get_order_types( 'order-meta-boxes' ), true ) ) { - $this->render_filters(); - } - } - - /** - * Render any custom filters and search inputs for the list table. - */ - protected function render_filters() { - $user_string = ''; - $user_id = ''; - - if ( ! empty( $_GET['_customer_user'] ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended - $user_id = absint( $_GET['_customer_user'] ); // WPCS: input var ok, sanitization ok. - $user = get_user_by( 'id', $user_id ); - - $user_string = sprintf( - /* translators: 1: user display name 2: user ID 3: user email */ - esc_html__( '%1$s (#%2$s – %3$s)', 'woocommerce' ), - $user->display_name, - absint( $user->ID ), - $user->user_email - ); - } - ?> - - query_filters( $query_vars ); - } - - return $query_vars; - } - - /** - * Handle any custom filters. - * - * @param array $query_vars Query vars. - * @return array - */ - protected function query_filters( $query_vars ) { - global $wp_post_statuses; - - // Filter the orders by the posted customer. - if ( ! empty( $_GET['_customer_user'] ) ) { // WPCS: input var ok. - // @codingStandardsIgnoreStart. - $query_vars['meta_query'] = array( - array( - 'key' => '_customer_user', - 'value' => (int) $_GET['_customer_user'], // WPCS: input var ok, sanitization ok. - 'compare' => '=', - ), - ); - // @codingStandardsIgnoreEnd - } - - // Sorting. - if ( isset( $query_vars['orderby'] ) ) { - if ( 'order_total' === $query_vars['orderby'] ) { - // @codingStandardsIgnoreStart - $query_vars = array_merge( $query_vars, array( - 'meta_key' => '_order_total', - 'orderby' => 'meta_value_num', - ) ); - // @codingStandardsIgnoreEnd - } - } - - // Status. - if ( empty( $query_vars['post_status'] ) ) { - $post_statuses = wc_get_order_statuses(); - - foreach ( $post_statuses as $status => $value ) { - if ( isset( $wp_post_statuses[ $status ] ) && false === $wp_post_statuses[ $status ]->show_in_admin_all_list ) { - unset( $post_statuses[ $status ] ); - } - } - - $query_vars['post_status'] = array_keys( $post_statuses ); - } - return $query_vars; - } - - /** - * Change the label when searching orders. - * - * @param mixed $query Current search query. - * @return string - */ - public function search_label( $query ) { - global $pagenow, $typenow; - - if ( 'edit.php' !== $pagenow || 'shop_order' !== $typenow || ! get_query_var( 'shop_order_search' ) || ! isset( $_GET['s'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - return $query; - } - - return wc_clean( wp_unslash( $_GET['s'] ) ); // WPCS: input var ok, sanitization ok. - } - - /** - * Query vars for custom searches. - * - * @param mixed $public_query_vars Array of query vars. - * @return array - */ - public function add_custom_query_var( $public_query_vars ) { - $public_query_vars[] = 'shop_order_search'; - return $public_query_vars; - } - - /** - * Search custom fields as well as content. - * - * @param WP_Query $wp Query object. - */ - public function search_custom_fields( $wp ) { - global $pagenow; - - if ( 'edit.php' !== $pagenow || empty( $wp->query_vars['s'] ) || 'shop_order' !== $wp->query_vars['post_type'] || ! isset( $_GET['s'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - return; - } - - $post_ids = wc_order_search( wc_clean( wp_unslash( $_GET['s'] ) ) ); // WPCS: input var ok, sanitization ok. - - if ( ! empty( $post_ids ) ) { - // Remove "s" - we don't want to search order name. - unset( $wp->query_vars['s'] ); - - // so we know we're doing this. - $wp->query_vars['shop_order_search'] = true; - - // Search by found posts. - $wp->query_vars['post__in'] = array_merge( $post_ids, array( 0 ) ); - } - } -} diff --git a/includes/admin/list-tables/class-wc-admin-list-table-products.php b/includes/admin/list-tables/class-wc-admin-list-table-products.php deleted file mode 100644 index a7990fa053f..00000000000 --- a/includes/admin/list-tables/class-wc-admin-list-table-products.php +++ /dev/null @@ -1,661 +0,0 @@ -'; - - echo '

    ' . esc_html__( 'Ready to start selling something awesome?', 'woocommerce' ) . '

    '; - - echo '
    '; - - echo '' . esc_html__( 'Create Product', 'woocommerce' ) . ''; - echo '' . esc_html__( 'Start Import', 'woocommerce' ) . ''; - - echo '
    '; - - do_action( 'wc_marketplace_suggestions_products_empty_state' ); - - echo ''; - } - - /** - * Define primary column. - * - * @return string - */ - protected function get_primary_column() { - return 'name'; - } - - /** - * Get row actions to show in the list table. - * - * @param array $actions Array of actions. - * @param WP_Post $post Current post object. - * @return array - */ - protected function get_row_actions( $actions, $post ) { - /* translators: %d: product ID. */ - return array_merge( array( 'id' => sprintf( __( 'ID: %d', 'woocommerce' ), $post->ID ) ), $actions ); - } - - /** - * Define which columns are sortable. - * - * @param array $columns Existing columns. - * @return array - */ - public function define_sortable_columns( $columns ) { - $custom = array( - 'price' => 'price', - 'sku' => 'sku', - 'name' => 'title', - ); - return wp_parse_args( $custom, $columns ); - } - - /** - * Define which columns to show on this screen. - * - * @param array $columns Existing columns. - * @return array - */ - public function define_columns( $columns ) { - if ( empty( $columns ) && ! is_array( $columns ) ) { - $columns = array(); - } - - unset( $columns['title'], $columns['comments'], $columns['date'] ); - - $show_columns = array(); - $show_columns['cb'] = ''; - $show_columns['thumb'] = '' . __( 'Image', 'woocommerce' ) . ''; - $show_columns['name'] = __( 'Name', 'woocommerce' ); - - if ( wc_product_sku_enabled() ) { - $show_columns['sku'] = __( 'SKU', 'woocommerce' ); - } - - if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { - $show_columns['is_in_stock'] = __( 'Stock', 'woocommerce' ); - } - - $show_columns['price'] = __( 'Price', 'woocommerce' ); - $show_columns['product_cat'] = __( 'Categories', 'woocommerce' ); - $show_columns['product_tag'] = __( 'Tags', 'woocommerce' ); - $show_columns['featured'] = '' . __( 'Featured', 'woocommerce' ) . ''; - $show_columns['date'] = __( 'Date', 'woocommerce' ); - - return array_merge( $show_columns, $columns ); - } - - /** - * Pre-fetch any data for the row each column has access to it. the_product global is there for bw compat. - * - * @param int $post_id Post ID being shown. - */ - protected function prepare_row_data( $post_id ) { - global $the_product; - - if ( empty( $this->object ) || $this->object->get_id() !== $post_id ) { - $the_product = wc_get_product( $post_id ); - $this->object = $the_product; - } - } - - /** - * Render columm: thumb. - */ - protected function render_thumb_column() { - echo '' . $this->object->get_image( 'thumbnail' ) . ''; // WPCS: XSS ok. - } - - /** - * Render column: name. - */ - protected function render_name_column() { - global $post; - - $edit_link = get_edit_post_link( $this->object->get_id() ); - $title = _draft_or_post_title(); - - echo '' . esc_html( $title ) . ''; - - _post_states( $post ); - - echo ''; - - if ( $this->object->get_parent_id() > 0 ) { - echo '  ← ' . get_the_title( $this->object->get_parent_id() ) . ''; // @codingStandardsIgnoreLine. - } - - get_inline_data( $post ); - - /* Custom inline data for woocommerce. */ - echo ' - - '; - } - - /** - * Render columm: sku. - */ - protected function render_sku_column() { - echo $this->object->get_sku() ? esc_html( $this->object->get_sku() ) : ''; - } - - /** - * Render columm: price. - */ - protected function render_price_column() { - echo $this->object->get_price_html() ? wp_kses_post( $this->object->get_price_html() ) : ''; - } - - /** - * Render columm: product_cat. - */ - protected function render_product_cat_column() { - $terms = get_the_terms( $this->object->get_id(), 'product_cat' ); - if ( ! $terms ) { - echo ''; - } else { - $termlist = array(); - foreach ( $terms as $term ) { - $termlist[] = '' . esc_html( $term->name ) . ''; - } - - echo apply_filters( 'woocommerce_admin_product_term_list', implode( ', ', $termlist ), 'product_cat', $this->object->get_id(), $termlist, $terms ); // WPCS: XSS ok. - } - } - - /** - * Render columm: product_tag. - */ - protected function render_product_tag_column() { - $terms = get_the_terms( $this->object->get_id(), 'product_tag' ); - if ( ! $terms ) { - echo ''; - } else { - $termlist = array(); - foreach ( $terms as $term ) { - $termlist[] = '' . esc_html( $term->name ) . ''; - } - - echo apply_filters( 'woocommerce_admin_product_term_list', implode( ', ', $termlist ), 'product_tag', $this->object->get_id(), $termlist, $terms ); // WPCS: XSS ok. - } - } - - /** - * Render columm: featured. - */ - protected function render_featured_column() { - $url = wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_feature_product&product_id=' . $this->object->get_id() ), 'woocommerce-feature-product' ); - echo ''; - if ( $this->object->is_featured() ) { - echo '' . esc_html__( 'Yes', 'woocommerce' ) . ''; - } else { - echo '' . esc_html__( 'No', 'woocommerce' ) . ''; - } - echo ''; - } - - /** - * Render columm: is_in_stock. - */ - protected function render_is_in_stock_column() { - if ( $this->object->is_on_backorder() ) { - $stock_html = '' . __( 'On backorder', 'woocommerce' ) . ''; - } elseif ( $this->object->is_in_stock() ) { - $stock_html = '' . __( 'In stock', 'woocommerce' ) . ''; - } else { - $stock_html = '' . __( 'Out of stock', 'woocommerce' ) . ''; - } - - if ( $this->object->managing_stock() ) { - $stock_html .= ' (' . wc_stock_amount( $this->object->get_stock_quantity() ) . ')'; - } - - echo wp_kses_post( apply_filters( 'woocommerce_admin_stock_html', $stock_html, $this->object ) ); - } - - /** - * Query vars for custom searches. - * - * @param mixed $public_query_vars Array of query vars. - * @return array - */ - public function add_custom_query_var( $public_query_vars ) { - $public_query_vars[] = 'sku'; - return $public_query_vars; - } - - /** - * Render any custom filters and search inputs for the list table. - */ - protected function render_filters() { - $filters = apply_filters( - 'woocommerce_products_admin_list_table_filters', - array( - 'product_category' => array( $this, 'render_products_category_filter' ), - 'product_type' => array( $this, 'render_products_type_filter' ), - 'stock_status' => array( $this, 'render_products_stock_status_filter' ), - ) - ); - - ob_start(); - foreach ( $filters as $filter_callback ) { - call_user_func( $filter_callback ); - } - $output = ob_get_clean(); - - echo apply_filters( 'woocommerce_product_filters', $output ); // WPCS: XSS ok. - } - - /** - * Render the product category filter for the list table. - * - * @since 3.5.0 - */ - protected function render_products_category_filter() { - $categories_count = (int) wp_count_terms( 'product_cat' ); - - if ( $categories_count <= apply_filters( 'woocommerce_product_category_filter_threshold', 100 ) ) { - wc_product_dropdown_categories( - array( - 'option_select_text' => __( 'Filter by category', 'woocommerce' ), - 'hide_empty' => 0, - ) - ); - } else { - $current_category_slug = isset( $_GET['product_cat'] ) ? wc_clean( wp_unslash( $_GET['product_cat'] ) ) : false; // WPCS: input var ok, CSRF ok. - $current_category = $current_category_slug ? get_term_by( 'slug', $current_category_slug, 'product_cat' ) : false; - ?> - - '; - - foreach ( wc_get_product_types() as $value => $label ) { - $output .= ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } - } - } - } - - /** - * Get country address formats. - * - * These define how addresses are formatted for display in various countries. - * - * @return array - */ - public function get_address_formats() { - if ( empty( $this->address_formats ) ) { - $this->address_formats = apply_filters( - 'woocommerce_localisation_address_formats', - array( - 'default' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode}\n{country}", - 'AU' => "{name}\n{company}\n{address_1}\n{address_2}\n{city} {state} {postcode}\n{country}", - 'AT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'BE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'CA' => "{company}\n{name}\n{address_1}\n{address_2}\n{city} {state_code} {postcode}\n{country}", - 'CH' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'CL' => "{company}\n{name}\n{address_1}\n{address_2}\n{state}\n{postcode} {city}\n{country}", - 'CN' => "{country} {postcode}\n{state}, {city}, {address_2}, {address_1}\n{company}\n{name}", - 'CZ' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'DE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'EE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'FI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'DK' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'FR' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city_upper}\n{country}", - 'HK' => "{company}\n{first_name} {last_name_upper}\n{address_1}\n{address_2}\n{city_upper}\n{state_upper}\n{country}", - 'HU' => "{last_name} {first_name}\n{company}\n{city}\n{address_1}\n{address_2}\n{postcode}\n{country}", - 'IN' => "{company}\n{name}\n{address_1}\n{address_2}\n{city} {postcode}\n{state}, {country}", - 'IS' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'IT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode}\n{city}\n{state_upper}\n{country}", - 'JM' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode_upper}\n{country}", - 'JP' => "{postcode}\n{state} {city} {address_1}\n{address_2}\n{company}\n{last_name} {first_name}\n{country}", - 'TW' => "{company}\n{last_name} {first_name}\n{address_1}\n{address_2}\n{state}, {city} {postcode}\n{country}", - 'LI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'NL' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'NZ' => "{name}\n{company}\n{address_1}\n{address_2}\n{city} {postcode}\n{country}", - 'NO' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'PL' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'PR' => "{company}\n{name}\n{address_1} {address_2}\n{city} \n{country} {postcode}", - 'PT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'SK' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'RS' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'SI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'ES' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city}\n{state}\n{country}", - 'SE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'TR' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city} {state}\n{country}", - 'UG' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}, {country}", - 'US' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}, {state_code} {postcode}\n{country}", - 'VN' => "{name}\n{company}\n{address_1}\n{city}\n{country}", - ) - ); - } - return $this->address_formats; - } - - /** - * Get country address format. - * - * @param array $args Arguments. - * @param string $separator How to separate address lines. @since 3.5.0. - * @return string - */ - public function get_formatted_address( $args = array(), $separator = '
    ' ) { - $default_args = array( - 'first_name' => '', - 'last_name' => '', - 'company' => '', - 'address_1' => '', - 'address_2' => '', - 'city' => '', - 'state' => '', - 'postcode' => '', - 'country' => '', - ); - - $args = array_map( 'trim', wp_parse_args( $args, $default_args ) ); - $state = $args['state']; - $country = $args['country']; - - // Get all formats. - $formats = $this->get_address_formats(); - - // Get format for the address' country. - $format = ( $country && isset( $formats[ $country ] ) ) ? $formats[ $country ] : $formats['default']; - - // Handle full country name. - $full_country = ( isset( $this->countries[ $country ] ) ) ? $this->countries[ $country ] : $country; - - // Country is not needed if the same as base. - if ( $country === $this->get_base_country() && ! apply_filters( 'woocommerce_formatted_address_force_country_display', false ) ) { - $format = str_replace( '{country}', '', $format ); - } - - // Handle full state name. - $full_state = ( $country && $state && isset( $this->states[ $country ][ $state ] ) ) ? $this->states[ $country ][ $state ] : $state; - - // Substitute address parts into the string. - $replace = array_map( - 'esc_html', - apply_filters( - 'woocommerce_formatted_address_replacements', - array( - '{first_name}' => $args['first_name'], - '{last_name}' => $args['last_name'], - '{name}' => $args['first_name'] . ' ' . $args['last_name'], - '{company}' => $args['company'], - '{address_1}' => $args['address_1'], - '{address_2}' => $args['address_2'], - '{city}' => $args['city'], - '{state}' => $full_state, - '{postcode}' => $args['postcode'], - '{country}' => $full_country, - '{first_name_upper}' => wc_strtoupper( $args['first_name'] ), - '{last_name_upper}' => wc_strtoupper( $args['last_name'] ), - '{name_upper}' => wc_strtoupper( $args['first_name'] . ' ' . $args['last_name'] ), - '{company_upper}' => wc_strtoupper( $args['company'] ), - '{address_1_upper}' => wc_strtoupper( $args['address_1'] ), - '{address_2_upper}' => wc_strtoupper( $args['address_2'] ), - '{city_upper}' => wc_strtoupper( $args['city'] ), - '{state_upper}' => wc_strtoupper( $full_state ), - '{state_code}' => wc_strtoupper( $state ), - '{postcode_upper}' => wc_strtoupper( $args['postcode'] ), - '{country_upper}' => wc_strtoupper( $full_country ), - ), - $args - ) - ); - - $formatted_address = str_replace( array_keys( $replace ), $replace, $format ); - - // Clean up white space. - $formatted_address = preg_replace( '/ +/', ' ', trim( $formatted_address ) ); - $formatted_address = preg_replace( '/\n\n+/', "\n", $formatted_address ); - - // Break newlines apart and remove empty lines/trim commas and white space. - $formatted_address = array_filter( array_map( array( $this, 'trim_formatted_address_line' ), explode( "\n", $formatted_address ) ) ); - - // Add html breaks. - $formatted_address = implode( $separator, $formatted_address ); - - // We're done! - return $formatted_address; - } - - /** - * Trim white space and commas off a line. - * - * @param string $line Line. - * @return string - */ - private function trim_formatted_address_line( $line ) { - return trim( $line, ', ' ); - } - - /** - * Returns the fields we show by default. This can be filtered later on. - * - * @return array - */ - public function get_default_address_fields() { - $address_2_label = __( 'Apartment, suite, unit, etc.', 'woocommerce' ); - - // If necessary, append '(optional)' to the placeholder: we don't need to worry about the - // label, though, as woocommerce_form_field() takes care of that. - if ( 'optional' === get_option( 'woocommerce_checkout_address_2_field', 'optional' ) ) { - $address_2_placeholder = __( 'Apartment, suite, unit, etc. (optional)', 'woocommerce' ); - } else { - $address_2_placeholder = $address_2_label; - } - - $fields = array( - 'first_name' => array( - 'label' => __( 'First name', 'woocommerce' ), - 'required' => true, - 'class' => array( 'form-row-first' ), - 'autocomplete' => 'given-name', - 'priority' => 10, - ), - 'last_name' => array( - 'label' => __( 'Last name', 'woocommerce' ), - 'required' => true, - 'class' => array( 'form-row-last' ), - 'autocomplete' => 'family-name', - 'priority' => 20, - ), - 'company' => array( - 'label' => __( 'Company name', 'woocommerce' ), - 'class' => array( 'form-row-wide' ), - 'autocomplete' => 'organization', - 'priority' => 30, - 'required' => 'required' === get_option( 'woocommerce_checkout_company_field', 'optional' ), - ), - 'country' => array( - 'type' => 'country', - 'label' => __( 'Country / Region', 'woocommerce' ), - 'required' => true, - 'class' => array( 'form-row-wide', 'address-field', 'update_totals_on_change' ), - 'autocomplete' => 'country', - 'priority' => 40, - ), - 'address_1' => array( - 'label' => __( 'Street address', 'woocommerce' ), - /* translators: use local order of street name and house number. */ - 'placeholder' => esc_attr__( 'House number and street name', 'woocommerce' ), - 'required' => true, - 'class' => array( 'form-row-wide', 'address-field' ), - 'autocomplete' => 'address-line1', - 'priority' => 50, - ), - 'address_2' => array( - 'label' => $address_2_label, - 'label_class' => array( 'screen-reader-text' ), - 'placeholder' => esc_attr( $address_2_placeholder ), - 'class' => array( 'form-row-wide', 'address-field' ), - 'autocomplete' => 'address-line2', - 'priority' => 60, - 'required' => 'required' === get_option( 'woocommerce_checkout_address_2_field', 'optional' ), - ), - 'city' => array( - 'label' => __( 'Town / City', 'woocommerce' ), - 'required' => true, - 'class' => array( 'form-row-wide', 'address-field' ), - 'autocomplete' => 'address-level2', - 'priority' => 70, - ), - 'state' => array( - 'type' => 'state', - 'label' => __( 'State / County', 'woocommerce' ), - 'required' => true, - 'class' => array( 'form-row-wide', 'address-field' ), - 'validate' => array( 'state' ), - 'autocomplete' => 'address-level1', - 'priority' => 80, - ), - 'postcode' => array( - 'label' => __( 'Postcode / ZIP', 'woocommerce' ), - 'required' => true, - 'class' => array( 'form-row-wide', 'address-field' ), - 'validate' => array( 'postcode' ), - 'autocomplete' => 'postal-code', - 'priority' => 90, - ), - ); - - if ( 'hidden' === get_option( 'woocommerce_checkout_company_field', 'optional' ) ) { - unset( $fields['company'] ); - } - - if ( 'hidden' === get_option( 'woocommerce_checkout_address_2_field', 'optional' ) ) { - unset( $fields['address_2'] ); - } - - $default_address_fields = apply_filters( 'woocommerce_default_address_fields', $fields ); - // Sort each of the fields based on priority. - uasort( $default_address_fields, 'wc_checkout_fields_uasort_comparison' ); - - return $default_address_fields; - } - - /** - * Get JS selectors for fields which are shown/hidden depending on the locale. - * - * @return array - */ - public function get_country_locale_field_selectors() { - $locale_fields = array( - 'address_1' => '#billing_address_1_field, #shipping_address_1_field', - 'address_2' => '#billing_address_2_field, #shipping_address_2_field', - 'state' => '#billing_state_field, #shipping_state_field, #calc_shipping_state_field', - 'postcode' => '#billing_postcode_field, #shipping_postcode_field, #calc_shipping_postcode_field', - 'city' => '#billing_city_field, #shipping_city_field, #calc_shipping_city_field', - ); - return apply_filters( 'woocommerce_country_locale_field_selectors', $locale_fields ); - } - - /** - * Get country locale settings. - * - * These locales override the default country selections after a country is chosen. - * - * @return array - */ - public function get_country_locale() { - if ( empty( $this->locale ) ) { - $this->locale = apply_filters( - 'woocommerce_get_country_locale', - array( - 'AE' => array( - 'postcode' => array( - 'required' => false, - 'hidden' => true, - ), - 'state' => array( - 'required' => false, - ), - ), - 'AF' => array( - 'state' => array( - 'required' => false, - ), - ), - 'AO' => array( - 'postcode' => array( - 'required' => false, - 'hidden' => true, - ), - 'state' => array( - 'label' => __( 'Province', 'woocommerce' ), - ), - ), - 'AT' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'required' => false, - ), - ), - 'AU' => array( - 'city' => array( - 'label' => __( 'Suburb', 'woocommerce' ), - ), - 'postcode' => array( - 'label' => __( 'Postcode', 'woocommerce' ), - ), - 'state' => array( - 'label' => __( 'State', 'woocommerce' ), - ), - ), - 'AX' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'required' => false, - ), - ), - 'BA' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'label' => __( 'Canton', 'woocommerce' ), - 'required' => false, - 'hidden' => true, - ), - ), - 'BD' => array( - 'postcode' => array( - 'required' => false, - ), - 'state' => array( - 'label' => __( 'District', 'woocommerce' ), - ), - ), - 'BE' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'required' => false, - 'label' => __( 'Province', 'woocommerce' ), - ), - ), - 'BH' => array( - 'postcode' => array( - 'required' => false, - ), - 'state' => array( - 'required' => false, - ), - ), - 'BI' => array( - 'state' => array( - 'required' => false, - ), - ), - 'BO' => array( - 'postcode' => array( - 'required' => false, - 'hidden' => true, - ), - ), - 'BS' => array( - 'postcode' => array( - 'required' => false, - 'hidden' => true, - ), - ), - 'CA' => array( - 'postcode' => array( - 'label' => __( 'Postal code', 'woocommerce' ), - ), - 'state' => array( - 'label' => __( 'Province', 'woocommerce' ), - ), - ), - 'CH' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'label' => __( 'Canton', 'woocommerce' ), - 'required' => false, - ), - ), - 'CL' => array( - 'city' => array( - 'required' => true, - ), - 'postcode' => array( - 'required' => false, - ), - 'state' => array( - 'label' => __( 'Region', 'woocommerce' ), - ), - ), - 'CN' => array( - 'state' => array( - 'label' => __( 'Province', 'woocommerce' ), - ), - ), - 'CO' => array( - 'postcode' => array( - 'required' => false, - ), - ), - 'CZ' => array( - 'state' => array( - 'required' => false, - ), - ), - 'DE' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'required' => false, - 'hidden' => true, - ), - ), - 'DK' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'required' => false, - 'hidden' => true, - ), - ), - 'EE' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'required' => false, - ), - ), - 'FI' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'required' => false, - ), - ), - 'FR' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'required' => false, - ), - ), - 'GH' => array( - 'postcode' => array( - 'required' => false, - ), - 'state' => array( - 'label' => __( 'Region', 'woocommerce' ), - ), - ), - 'GP' => array( - 'state' => array( - 'required' => false, - ), - ), - 'GF' => array( - 'state' => array( - 'required' => false, - ), - ), - 'GR' => array( - 'state' => array( - 'required' => false, - ), - ), - 'GT' => array( - 'postcode' => array( - 'required' => false, - 'hidden' => true, - ), - 'state' => array( - 'label' => __( 'Department', 'woocommerce' ), - ), - ), - 'HK' => array( - 'postcode' => array( - 'required' => false, - ), - 'city' => array( - 'label' => __( 'Town / District', 'woocommerce' ), - ), - 'state' => array( - 'label' => __( 'Region', 'woocommerce' ), - ), - ), - 'HU' => array( - 'last_name' => array( - 'class' => array( 'form-row-first' ), - 'priority' => 10, - ), - 'first_name' => array( - 'class' => array( 'form-row-last' ), - 'priority' => 20, - ), - 'postcode' => array( - 'class' => array( 'form-row-first', 'address-field' ), - 'priority' => 65, - ), - 'city' => array( - 'class' => array( 'form-row-last', 'address-field' ), - ), - 'address_1' => array( - 'priority' => 71, - ), - 'address_2' => array( - 'priority' => 72, - ), - 'state' => array( - 'label' => __( 'County', 'woocommerce' ), - ), - ), - 'ID' => array( - 'state' => array( - 'label' => __( 'Province', 'woocommerce' ), - ), - ), - 'IE' => array( - 'postcode' => array( - 'required' => false, - 'label' => __( 'Eircode', 'woocommerce' ), - ), - 'state' => array( - 'label' => __( 'County', 'woocommerce' ), - ), - ), - 'IS' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'required' => false, - ), - ), - 'IL' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'required' => false, - ), - ), - 'IM' => array( - 'state' => array( - 'required' => false, - ), - ), - 'IN' => array( - 'postcode' => array( - 'label' => __( 'Pin code', 'woocommerce' ), - ), - 'state' => array( - 'label' => __( 'State', 'woocommerce' ), - ), - ), - 'IT' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'required' => true, - 'label' => __( 'Province', 'woocommerce' ), - ), - ), - 'JM' => array( - 'city' => array( - 'label' => __( 'Town / City / Post Office', 'woocommerce' ), - ), - 'postcode' => array( - 'required' => false, - 'label' => __( 'Postal Code', 'woocommerce' ), - ), - 'state' => array( - 'required' => true, - 'label' => __( 'Parish', 'woocommerce' ), - ), - ), - 'JP' => array( - 'last_name' => array( - 'class' => array( 'form-row-first' ), - 'priority' => 10, - ), - 'first_name' => array( - 'class' => array( 'form-row-last' ), - 'priority' => 20, - ), - 'postcode' => array( - 'class' => array( 'form-row-first', 'address-field' ), - 'priority' => 65, - ), - 'state' => array( - 'label' => __( 'Prefecture', 'woocommerce' ), - 'class' => array( 'form-row-last', 'address-field' ), - 'priority' => 66, - ), - 'city' => array( - 'priority' => 67, - ), - 'address_1' => array( - 'priority' => 68, - ), - 'address_2' => array( - 'priority' => 69, - ), - ), - 'KR' => array( - 'state' => array( - 'required' => false, - ), - ), - 'KW' => array( - 'state' => array( - 'required' => false, - ), - ), - 'LV' => array( - 'state' => array( - 'label' => __( 'Municipality', 'woocommerce' ), - 'required' => false, - ), - ), - 'LB' => array( - 'state' => array( - 'required' => false, - ), - ), - 'MQ' => array( - 'state' => array( - 'required' => false, - ), - ), - 'MT' => array( - 'state' => array( - 'required' => false, - ), - ), - 'MZ' => array( - 'postcode' => array( - 'required' => false, - 'hidden' => true, - ), - 'state' => array( - 'label' => __( 'Province', 'woocommerce' ), - ), - ), - 'NL' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'required' => false, - 'label' => __( 'Province', 'woocommerce' ), - ), - ), - 'NG' => array( - 'postcode' => array( - 'label' => __( 'Postcode', 'woocommerce' ), - 'required' => false, - 'hidden' => true, - ), - 'state' => array( - 'label' => __( 'State', 'woocommerce' ), - ), - ), - 'NZ' => array( - 'postcode' => array( - 'label' => __( 'Postcode', 'woocommerce' ), - ), - 'state' => array( - 'required' => false, - 'label' => __( 'Region', 'woocommerce' ), - ), - ), - 'NO' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'required' => false, - ), - ), - 'NP' => array( - 'state' => array( - 'label' => __( 'State / Zone', 'woocommerce' ), - ), - 'postcode' => array( - 'required' => false, - ), - ), - 'PL' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'required' => false, - ), - ), - 'PR' => array( - 'city' => array( - 'label' => __( 'Municipality', 'woocommerce' ), - ), - 'state' => array( - 'required' => false, - 'hidden' => true, - ), - ), - 'PT' => array( - 'state' => array( - 'required' => false, - ), - ), - 'RE' => array( - 'state' => array( - 'required' => false, - ), - ), - 'RO' => array( - 'state' => array( - 'label' => __( 'County', 'woocommerce' ), - 'required' => true, - ), - ), - 'RS' => array( - 'city' => array( - 'required' => true, - ), - 'postcode' => array( - 'required' => true, - ), - 'state' => array( - 'label' => __( 'District', 'woocommerce' ), - 'required' => false, - ), - ), - 'SG' => array( - 'state' => array( - 'required' => false, - ), - 'city' => array( - 'required' => false, - ), - ), - 'SK' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'required' => false, - ), - ), - 'SI' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'required' => false, - ), - ), - 'SR' => array( - 'postcode' => array( - 'required' => false, - 'hidden' => true, - ), - ), - 'ES' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'label' => __( 'Province', 'woocommerce' ), - ), - ), - 'LI' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'label' => __( 'Municipality', 'woocommerce' ), - 'required' => false, - ), - ), - 'LK' => array( - 'state' => array( - 'required' => false, - ), - ), - 'LU' => array( - 'state' => array( - 'required' => false, - ), - ), - 'MD' => array( - 'state' => array( - 'label' => __( 'Municipality / District', 'woocommerce' ), - ), - ), - 'SE' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'required' => false, - 'hidden' => true, - ), - ), - 'TR' => array( - 'postcode' => array( - 'priority' => 65, - ), - 'state' => array( - 'label' => __( 'Province', 'woocommerce' ), - ), - ), - 'UG' => array( - 'postcode' => array( - 'required' => false, - 'hidden' => true, - ), - 'city' => array( - 'label' => __( 'Town / Village', 'woocommerce' ), - 'required' => true, - ), - 'state' => array( - 'label' => __( 'District', 'woocommerce' ), - 'required' => true, - ), - ), - 'US' => array( - 'postcode' => array( - 'label' => __( 'ZIP', 'woocommerce' ), - ), - 'state' => array( - 'label' => __( 'State', 'woocommerce' ), - ), - ), - 'GB' => array( - 'postcode' => array( - 'label' => __( 'Postcode', 'woocommerce' ), - ), - 'state' => array( - 'label' => __( 'County', 'woocommerce' ), - 'required' => false, - ), - ), - 'ST' => array( - 'postcode' => array( - 'required' => false, - 'hidden' => true, - ), - 'state' => array( - 'label' => __( 'District', 'woocommerce' ), - ), - ), - 'VN' => array( - 'state' => array( - 'required' => false, - 'hidden' => true, - ), - 'postcode' => array( - 'priority' => 65, - 'required' => false, - 'hidden' => false, - ), - 'address_2' => array( - 'required' => false, - 'hidden' => true, - ), - ), - 'WS' => array( - 'postcode' => array( - 'required' => false, - 'hidden' => true, - ), - ), - 'YT' => array( - 'state' => array( - 'required' => false, - ), - ), - 'ZA' => array( - 'state' => array( - 'label' => __( 'Province', 'woocommerce' ), - ), - ), - 'ZW' => array( - 'postcode' => array( - 'required' => false, - 'hidden' => true, - ), - ), - ) - ); - - $this->locale = array_intersect_key( $this->locale, array_merge( $this->get_allowed_countries(), $this->get_shipping_countries() ) ); - - // Default Locale Can be filtered to override fields in get_address_fields(). Countries with no specific locale will use default. - $this->locale['default'] = apply_filters( 'woocommerce_get_country_locale_default', $this->get_default_address_fields() ); - - // Filter default AND shop base locales to allow overides via a single function. These will be used when changing countries on the checkout. - if ( ! isset( $this->locale[ $this->get_base_country() ] ) ) { - $this->locale[ $this->get_base_country() ] = $this->locale['default']; - } - - $this->locale['default'] = apply_filters( 'woocommerce_get_country_locale_base', $this->locale['default'] ); - $this->locale[ $this->get_base_country() ] = apply_filters( 'woocommerce_get_country_locale_base', $this->locale[ $this->get_base_country() ] ); - } - - return $this->locale; - } - - /** - * Apply locale and get address fields. - * - * @param mixed $country Country. - * @param string $type Address type, defaults to 'billing_'. - * @return array - */ - public function get_address_fields( $country = '', $type = 'billing_' ) { - if ( ! $country ) { - $country = $this->get_base_country(); - } - - $fields = $this->get_default_address_fields(); - $locale = $this->get_country_locale(); - - if ( isset( $locale[ $country ] ) ) { - $fields = wc_array_overlay( $fields, $locale[ $country ] ); - } - - // Prepend field keys. - $address_fields = array(); - - foreach ( $fields as $key => $value ) { - if ( 'state' === $key ) { - $value['country_field'] = $type . 'country'; - $value['country'] = $country; - } - $address_fields[ $type . $key ] = $value; - } - - // Add email and phone fields. - if ( 'billing_' === $type ) { - if ( 'hidden' !== get_option( 'woocommerce_checkout_phone_field', 'required' ) ) { - $address_fields['billing_phone'] = array( - 'label' => __( 'Phone', 'woocommerce' ), - 'required' => 'required' === get_option( 'woocommerce_checkout_phone_field', 'required' ), - 'type' => 'tel', - 'class' => array( 'form-row-wide' ), - 'validate' => array( 'phone' ), - 'autocomplete' => 'tel', - 'priority' => 100, - ); - } - $address_fields['billing_email'] = array( - 'label' => __( 'Email address', 'woocommerce' ), - 'required' => true, - 'type' => 'email', - 'class' => array( 'form-row-wide' ), - 'validate' => array( 'email' ), - 'autocomplete' => 'no' === get_option( 'woocommerce_registration_generate_username' ) ? 'email' : 'email username', - 'priority' => 110, - ); - } - - /** - * Important note on this filter: Changes to address fields can and will be overridden by - * the woocommerce_default_address_fields. The locales/default locales apply on top based - * on country selection. If you want to change things like the required status of an - * address field, filter woocommerce_default_address_fields instead. - */ - $address_fields = apply_filters( 'woocommerce_' . $type . 'fields', $address_fields, $country ); - // Sort each of the fields based on priority. - uasort( $address_fields, 'wc_checkout_fields_uasort_comparison' ); - - return $address_fields; - } -} diff --git a/includes/class-wc-coupon.php b/includes/class-wc-coupon.php deleted file mode 100644 index d8cf7a02410..00000000000 --- a/includes/class-wc-coupon.php +++ /dev/null @@ -1,1077 +0,0 @@ - '', - 'amount' => 0, - 'date_created' => null, - 'date_modified' => null, - 'date_expires' => null, - 'discount_type' => 'fixed_cart', - 'description' => '', - 'usage_count' => 0, - 'individual_use' => false, - 'product_ids' => array(), - 'excluded_product_ids' => array(), - 'usage_limit' => 0, - 'usage_limit_per_user' => 0, - 'limit_usage_to_x_items' => null, - 'free_shipping' => false, - 'product_categories' => array(), - 'excluded_product_categories' => array(), - 'exclude_sale_items' => false, - 'minimum_amount' => '', - 'maximum_amount' => '', - 'email_restrictions' => array(), - 'used_by' => array(), - 'virtual' => false, - ); - - // Coupon message codes. - const E_WC_COUPON_INVALID_FILTERED = 100; - const E_WC_COUPON_INVALID_REMOVED = 101; - const E_WC_COUPON_NOT_YOURS_REMOVED = 102; - const E_WC_COUPON_ALREADY_APPLIED = 103; - const E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY = 104; - const E_WC_COUPON_NOT_EXIST = 105; - const E_WC_COUPON_USAGE_LIMIT_REACHED = 106; - const E_WC_COUPON_EXPIRED = 107; - const E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET = 108; - const E_WC_COUPON_NOT_APPLICABLE = 109; - const E_WC_COUPON_NOT_VALID_SALE_ITEMS = 110; - const E_WC_COUPON_PLEASE_ENTER = 111; - const E_WC_COUPON_MAX_SPEND_LIMIT_MET = 112; - const E_WC_COUPON_EXCLUDED_PRODUCTS = 113; - const E_WC_COUPON_EXCLUDED_CATEGORIES = 114; - const E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK = 115; - const E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST = 116; - const WC_COUPON_SUCCESS = 200; - const WC_COUPON_REMOVED = 201; - - /** - * Cache group. - * - * @var string - */ - protected $cache_group = 'coupons'; - - /** - * Coupon constructor. Loads coupon data. - * - * @param mixed $data Coupon data, object, ID or code. - */ - public function __construct( $data = '' ) { - parent::__construct( $data ); - - // If we already have a coupon object, read it again. - if ( $data instanceof WC_Coupon ) { - $this->set_id( absint( $data->get_id() ) ); - $this->read_object_from_database(); - return; - } - - // This filter allows custom coupon objects to be created on the fly. - $coupon = apply_filters( 'woocommerce_get_shop_coupon_data', false, $data, $this ); - - if ( $coupon ) { - $this->read_manual_coupon( $data, $coupon ); - return; - } - - // Try to load coupon using ID or code. - if ( is_int( $data ) && 'shop_coupon' === get_post_type( $data ) ) { - $this->set_id( $data ); - } elseif ( ! empty( $data ) ) { - $id = wc_get_coupon_id_by_code( $data ); - // Need to support numeric strings for backwards compatibility. - if ( ! $id && 'shop_coupon' === get_post_type( $data ) ) { - $this->set_id( $data ); - } else { - $this->set_id( $id ); - $this->set_code( $data ); - } - } else { - $this->set_object_read( true ); - } - - $this->read_object_from_database(); - } - - /** - * If the object has an ID, read using the data store. - * - * @since 3.4.1 - */ - protected function read_object_from_database() { - $this->data_store = WC_Data_Store::load( 'coupon' ); - - if ( $this->get_id() > 0 ) { - $this->data_store->read( $this ); - } - } - /** - * Checks the coupon type. - * - * @param string $type Array or string of types. - * @return bool - */ - public function is_type( $type ) { - return ( $this->get_discount_type() === $type || ( is_array( $type ) && in_array( $this->get_discount_type(), $type, true ) ) ); - } - - /** - * Prefix for action and filter hooks on data. - * - * @since 3.0.0 - * @return string - */ - protected function get_hook_prefix() { - return 'woocommerce_coupon_get_'; - } - - /* - |-------------------------------------------------------------------------- - | Getters - |-------------------------------------------------------------------------- - | - | Methods for getting data from the coupon object. - | - */ - - /** - * Get coupon code. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_code( $context = 'view' ) { - return $this->get_prop( 'code', $context ); - } - - /** - * Get coupon description. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_description( $context = 'view' ) { - return $this->get_prop( 'description', $context ); - } - - /** - * Get discount type. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_discount_type( $context = 'view' ) { - return $this->get_prop( 'discount_type', $context ); - } - - /** - * Get coupon amount. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return float - */ - public function get_amount( $context = 'view' ) { - return wc_format_decimal( $this->get_prop( 'amount', $context ) ); - } - - /** - * Get coupon expiration date. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return WC_DateTime|NULL object if the date is set or null if there is no date. - */ - public function get_date_expires( $context = 'view' ) { - return $this->get_prop( 'date_expires', $context ); - } - - /** - * Get date_created - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return WC_DateTime|NULL object if the date is set or null if there is no date. - */ - public function get_date_created( $context = 'view' ) { - return $this->get_prop( 'date_created', $context ); - } - - /** - * Get date_modified - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return WC_DateTime|NULL object if the date is set or null if there is no date. - */ - public function get_date_modified( $context = 'view' ) { - return $this->get_prop( 'date_modified', $context ); - } - - /** - * Get coupon usage count. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return integer - */ - public function get_usage_count( $context = 'view' ) { - return $this->get_prop( 'usage_count', $context ); - } - - /** - * Get the "indvidual use" checkbox status. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return bool - */ - public function get_individual_use( $context = 'view' ) { - return $this->get_prop( 'individual_use', $context ); - } - - /** - * Get product IDs this coupon can apply to. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return array - */ - public function get_product_ids( $context = 'view' ) { - return $this->get_prop( 'product_ids', $context ); - } - - /** - * Get product IDs that this coupon should not apply to. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return array - */ - public function get_excluded_product_ids( $context = 'view' ) { - return $this->get_prop( 'excluded_product_ids', $context ); - } - - /** - * Get coupon usage limit. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return integer - */ - public function get_usage_limit( $context = 'view' ) { - return $this->get_prop( 'usage_limit', $context ); - } - - /** - * Get coupon usage limit per customer (for a single customer) - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return integer - */ - public function get_usage_limit_per_user( $context = 'view' ) { - return $this->get_prop( 'usage_limit_per_user', $context ); - } - - /** - * Usage limited to certain amount of items - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return integer|null - */ - public function get_limit_usage_to_x_items( $context = 'view' ) { - return $this->get_prop( 'limit_usage_to_x_items', $context ); - } - - /** - * If this coupon grants free shipping or not. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return bool - */ - public function get_free_shipping( $context = 'view' ) { - return $this->get_prop( 'free_shipping', $context ); - } - - /** - * Get product categories this coupon can apply to. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return array - */ - public function get_product_categories( $context = 'view' ) { - return $this->get_prop( 'product_categories', $context ); - } - - /** - * Get product categories this coupon cannot not apply to. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return array - */ - public function get_excluded_product_categories( $context = 'view' ) { - return $this->get_prop( 'excluded_product_categories', $context ); - } - - /** - * If this coupon should exclude items on sale. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return bool - */ - public function get_exclude_sale_items( $context = 'view' ) { - return $this->get_prop( 'exclude_sale_items', $context ); - } - - /** - * Get minimum spend amount. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return float - */ - public function get_minimum_amount( $context = 'view' ) { - return $this->get_prop( 'minimum_amount', $context ); - } - /** - * Get maximum spend amount. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return float - */ - public function get_maximum_amount( $context = 'view' ) { - return $this->get_prop( 'maximum_amount', $context ); - } - - /** - * Get emails to check customer usage restrictions. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return array - */ - public function get_email_restrictions( $context = 'view' ) { - return $this->get_prop( 'email_restrictions', $context ); - } - - /** - * Get records of all users who have used the current coupon. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return array - */ - public function get_used_by( $context = 'view' ) { - return $this->get_prop( 'used_by', $context ); - } - - /** - * If the filter is added through the woocommerce_get_shop_coupon_data filter, it's virtual and not in the DB. - * - * @since 3.2.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return boolean - */ - public function get_virtual( $context = 'view' ) { - return (bool) $this->get_prop( 'virtual', $context ); - } - - /** - * Get discount amount for a cart item. - * - * @param float $discounting_amount Amount the coupon is being applied to. - * @param array|null $cart_item Cart item being discounted if applicable. - * @param boolean $single True if discounting a single qty item, false if its the line. - * @return float Amount this coupon has discounted. - */ - public function get_discount_amount( $discounting_amount, $cart_item = null, $single = false ) { - $discount = 0; - $cart_item_qty = is_null( $cart_item ) ? 1 : $cart_item['quantity']; - - if ( $this->is_type( array( 'percent' ) ) ) { - $discount = (float) $this->get_amount() * ( $discounting_amount / 100 ); - } elseif ( $this->is_type( 'fixed_cart' ) && ! is_null( $cart_item ) && WC()->cart->subtotal_ex_tax ) { - /** - * This is the most complex discount - we need to divide the discount between rows based on their price in. - * proportion to the subtotal. This is so rows with different tax rates get a fair discount, and so rows. - * with no price (free) don't get discounted. - * - * Get item discount by dividing item cost by subtotal to get a %. - * - * Uses price inc tax if prices include tax to work around https://github.com/woocommerce/woocommerce/issues/7669 and https://github.com/woocommerce/woocommerce/issues/8074. - */ - if ( wc_prices_include_tax() ) { - $discount_percent = ( wc_get_price_including_tax( $cart_item['data'] ) * $cart_item_qty ) / WC()->cart->subtotal; - } else { - $discount_percent = ( wc_get_price_excluding_tax( $cart_item['data'] ) * $cart_item_qty ) / WC()->cart->subtotal_ex_tax; - } - $discount = ( (float) $this->get_amount() * $discount_percent ) / $cart_item_qty; - - } elseif ( $this->is_type( 'fixed_product' ) ) { - $discount = min( $this->get_amount(), $discounting_amount ); - $discount = $single ? $discount : $discount * $cart_item_qty; - } - - return apply_filters( - 'woocommerce_coupon_get_discount_amount', - NumberUtil::round( min( $discount, $discounting_amount ), wc_get_rounding_precision() ), - $discounting_amount, - $cart_item, - $single, - $this - ); - } - - /* - |-------------------------------------------------------------------------- - | Setters - |-------------------------------------------------------------------------- - | - | Functions for setting coupon data. These should not update anything in the - | database itself and should only change what is stored in the class - | object. - | - */ - - /** - * Set coupon code. - * - * @since 3.0.0 - * @param string $code Coupon code. - */ - public function set_code( $code ) { - $this->set_prop( 'code', wc_format_coupon_code( $code ) ); - } - - /** - * Set coupon description. - * - * @since 3.0.0 - * @param string $description Description. - */ - public function set_description( $description ) { - $this->set_prop( 'description', $description ); - } - - /** - * Set discount type. - * - * @since 3.0.0 - * @param string $discount_type Discount type. - */ - public function set_discount_type( $discount_type ) { - if ( 'percent_product' === $discount_type ) { - $discount_type = 'percent'; // Backwards compatibility. - } - if ( ! in_array( $discount_type, array_keys( wc_get_coupon_types() ), true ) ) { - $this->error( 'coupon_invalid_discount_type', __( 'Invalid discount type', 'woocommerce' ) ); - } - $this->set_prop( 'discount_type', $discount_type ); - } - - /** - * Set amount. - * - * @since 3.0.0 - * @param float $amount Amount. - */ - public function set_amount( $amount ) { - $amount = wc_format_decimal( $amount ); - - if ( ! is_numeric( $amount ) ) { - $amount = 0; - } - - if ( $amount < 0 ) { - $this->error( 'coupon_invalid_amount', __( 'Invalid discount amount', 'woocommerce' ) ); - } - - if ( 'percent' === $this->get_discount_type() && $amount > 100 ) { - $this->error( 'coupon_invalid_amount', __( 'Invalid discount amount', 'woocommerce' ) ); - } - - $this->set_prop( 'amount', $amount ); - } - - /** - * Set expiration date. - * - * @since 3.0.0 - * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. - */ - public function set_date_expires( $date ) { - $this->set_date_prop( 'date_expires', $date ); - } - - /** - * Set date_created - * - * @since 3.0.0 - * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. - */ - public function set_date_created( $date ) { - $this->set_date_prop( 'date_created', $date ); - } - - /** - * Set date_modified - * - * @since 3.0.0 - * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. - */ - public function set_date_modified( $date ) { - $this->set_date_prop( 'date_modified', $date ); - } - - /** - * Set how many times this coupon has been used. - * - * @since 3.0.0 - * @param int $usage_count Usage count. - */ - public function set_usage_count( $usage_count ) { - $this->set_prop( 'usage_count', absint( $usage_count ) ); - } - - /** - * Set if this coupon can only be used once. - * - * @since 3.0.0 - * @param bool $is_individual_use If is for individual use. - */ - public function set_individual_use( $is_individual_use ) { - $this->set_prop( 'individual_use', (bool) $is_individual_use ); - } - - /** - * Set the product IDs this coupon can be used with. - * - * @since 3.0.0 - * @param array $product_ids Products IDs. - */ - public function set_product_ids( $product_ids ) { - $this->set_prop( 'product_ids', array_filter( wp_parse_id_list( (array) $product_ids ) ) ); - } - - /** - * Set the product IDs this coupon cannot be used with. - * - * @since 3.0.0 - * @param array $excluded_product_ids Exclude product IDs. - */ - public function set_excluded_product_ids( $excluded_product_ids ) { - $this->set_prop( 'excluded_product_ids', array_filter( wp_parse_id_list( (array) $excluded_product_ids ) ) ); - } - - /** - * Set the amount of times this coupon can be used. - * - * @since 3.0.0 - * @param int $usage_limit Usage limit. - */ - public function set_usage_limit( $usage_limit ) { - $this->set_prop( 'usage_limit', absint( $usage_limit ) ); - } - - /** - * Set the amount of times this coupon can be used per user. - * - * @since 3.0.0 - * @param int $usage_limit Usage limit. - */ - public function set_usage_limit_per_user( $usage_limit ) { - $this->set_prop( 'usage_limit_per_user', absint( $usage_limit ) ); - } - - /** - * Set usage limit to x number of items. - * - * @since 3.0.0 - * @param int|null $limit_usage_to_x_items Limit usage to X items. - */ - public function set_limit_usage_to_x_items( $limit_usage_to_x_items ) { - $this->set_prop( 'limit_usage_to_x_items', is_null( $limit_usage_to_x_items ) ? null : absint( $limit_usage_to_x_items ) ); - } - - /** - * Set if this coupon enables free shipping or not. - * - * @since 3.0.0 - * @param bool $free_shipping If grant free shipping. - */ - public function set_free_shipping( $free_shipping ) { - $this->set_prop( 'free_shipping', (bool) $free_shipping ); - } - - /** - * Set the product category IDs this coupon can be used with. - * - * @since 3.0.0 - * @param array $product_categories List of product categories. - */ - public function set_product_categories( $product_categories ) { - $this->set_prop( 'product_categories', array_filter( wp_parse_id_list( (array) $product_categories ) ) ); - } - - /** - * Set the product category IDs this coupon cannot be used with. - * - * @since 3.0.0 - * @param array $excluded_product_categories List of excluded product categories. - */ - public function set_excluded_product_categories( $excluded_product_categories ) { - $this->set_prop( 'excluded_product_categories', array_filter( wp_parse_id_list( (array) $excluded_product_categories ) ) ); - } - - /** - * Set if this coupon should excluded sale items or not. - * - * @since 3.0.0 - * @param bool $exclude_sale_items If should exclude sale items. - */ - public function set_exclude_sale_items( $exclude_sale_items ) { - $this->set_prop( 'exclude_sale_items', (bool) $exclude_sale_items ); - } - - /** - * Set the minimum spend amount. - * - * @since 3.0.0 - * @param float $amount Minium amount. - */ - public function set_minimum_amount( $amount ) { - $this->set_prop( 'minimum_amount', wc_format_decimal( $amount ) ); - } - - /** - * Set the maximum spend amount. - * - * @since 3.0.0 - * @param float $amount Maximum amount. - */ - public function set_maximum_amount( $amount ) { - $this->set_prop( 'maximum_amount', wc_format_decimal( $amount ) ); - } - - /** - * Set email restrictions. - * - * @since 3.0.0 - * @param array $emails List of emails. - */ - public function set_email_restrictions( $emails = array() ) { - $emails = array_filter( array_map( 'sanitize_email', array_map( 'strtolower', (array) $emails ) ) ); - foreach ( $emails as $email ) { - if ( ! is_email( $email ) ) { - $this->error( 'coupon_invalid_email_address', __( 'Invalid email address restriction', 'woocommerce' ) ); - } - } - $this->set_prop( 'email_restrictions', $emails ); - } - - /** - * Set which users have used this coupon. - * - * @since 3.0.0 - * @param array $used_by List of user IDs. - */ - public function set_used_by( $used_by ) { - $this->set_prop( 'used_by', array_filter( $used_by ) ); - } - - /** - * Set coupon virtual state. - * - * @param boolean $virtual Whether it is virtual or not. - * @since 3.2.0 - */ - public function set_virtual( $virtual ) { - $this->set_prop( 'virtual', (bool) $virtual ); - } - - /* - |-------------------------------------------------------------------------- - | Other Actions - |-------------------------------------------------------------------------- - */ - - /** - * Developers can programmatically return coupons. This function will read those values into our WC_Coupon class. - * - * @since 3.0.0 - * @param string $code Coupon code. - * @param array $coupon Array of coupon properties. - */ - public function read_manual_coupon( $code, $coupon ) { - foreach ( $coupon as $key => $value ) { - switch ( $key ) { - case 'excluded_product_ids': - case 'exclude_product_ids': - if ( ! is_array( $coupon[ $key ] ) ) { - wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' ); - $coupon['excluded_product_ids'] = wc_string_to_array( $value ); - } - break; - case 'exclude_product_categories': - case 'excluded_product_categories': - if ( ! is_array( $coupon[ $key ] ) ) { - wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' ); - $coupon['excluded_product_categories'] = wc_string_to_array( $value ); - } - break; - case 'product_ids': - if ( ! is_array( $coupon[ $key ] ) ) { - wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' ); - $coupon[ $key ] = wc_string_to_array( $value ); - } - break; - case 'individual_use': - case 'free_shipping': - case 'exclude_sale_items': - if ( ! is_bool( $coupon[ $key ] ) ) { - wc_doing_it_wrong( $key, $key . ' should be true or false instead of yes or no.', '3.0' ); - $coupon[ $key ] = wc_string_to_bool( $value ); - } - break; - case 'expiry_date': - $coupon['date_expires'] = $value; - break; - } - } - $this->set_props( $coupon ); - $this->set_code( $code ); - $this->set_id( 0 ); - $this->set_virtual( true ); - } - - /** - * Increase usage count for current coupon. - * - * @param string $used_by Either user ID or billing email. - * @param WC_Order $order If provided, will clear the coupons held by this order. - */ - public function increase_usage_count( $used_by = '', $order = null ) { - if ( $this->get_id() && $this->data_store ) { - $new_count = $this->data_store->increase_usage_count( $this, $used_by, $order ); - - // Bypass set_prop and remove pending changes since the data store saves the count already. - $this->data['usage_count'] = $new_count; - if ( isset( $this->changes['usage_count'] ) ) { - unset( $this->changes['usage_count'] ); - } - } - } - - /** - * Decrease usage count for current coupon. - * - * @param string $used_by Either user ID or billing email. - */ - public function decrease_usage_count( $used_by = '' ) { - if ( $this->get_id() && $this->get_usage_count() > 0 && $this->data_store ) { - $new_count = $this->data_store->decrease_usage_count( $this, $used_by ); - - // Bypass set_prop and remove pending changes since the data store saves the count already. - $this->data['usage_count'] = $new_count; - if ( isset( $this->changes['usage_count'] ) ) { - unset( $this->changes['usage_count'] ); - } - } - } - - /* - |-------------------------------------------------------------------------- - | Validation & Error Handling - |-------------------------------------------------------------------------- - */ - - /** - * Returns the error_message string. - - * @return string - */ - public function get_error_message() { - return $this->error_message; - } - - /** - * Check if a coupon is valid for the cart. - * - * @deprecated 3.2.0 In favor of WC_Discounts->is_coupon_valid. - * @return bool - */ - public function is_valid() { - $discounts = new WC_Discounts( WC()->cart ); - $valid = $discounts->is_coupon_valid( $this ); - - if ( is_wp_error( $valid ) ) { - $this->error_message = $valid->get_error_message(); - return false; - } - - return $valid; - } - - /** - * Check if a coupon is valid. - * - * @return bool - */ - public function is_valid_for_cart() { - return apply_filters( 'woocommerce_coupon_is_valid_for_cart', $this->is_type( wc_get_cart_coupon_types() ), $this ); - } - - /** - * Check if a coupon is valid for a product. - * - * @param WC_Product $product Product instance. - * @param array $values Values. - * @return bool - */ - public function is_valid_for_product( $product, $values = array() ) { - if ( ! $this->is_type( wc_get_product_coupon_types() ) ) { - return apply_filters( 'woocommerce_coupon_is_valid_for_product', false, $product, $this, $values ); - } - - $valid = false; - $product_cats = wc_get_product_cat_ids( $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id() ); - $product_ids = array( $product->get_id(), $product->get_parent_id() ); - - // Specific products get the discount. - if ( count( $this->get_product_ids() ) && count( array_intersect( $product_ids, $this->get_product_ids() ) ) ) { - $valid = true; - } - - // Category discounts. - if ( count( $this->get_product_categories() ) && count( array_intersect( $product_cats, $this->get_product_categories() ) ) ) { - $valid = true; - } - - // No product ids - all items discounted. - if ( ! count( $this->get_product_ids() ) && ! count( $this->get_product_categories() ) ) { - $valid = true; - } - - // Specific product IDs excluded from the discount. - if ( count( $this->get_excluded_product_ids() ) && count( array_intersect( $product_ids, $this->get_excluded_product_ids() ) ) ) { - $valid = false; - } - - // Specific categories excluded from the discount. - if ( count( $this->get_excluded_product_categories() ) && count( array_intersect( $product_cats, $this->get_excluded_product_categories() ) ) ) { - $valid = false; - } - - // Sale Items excluded from discount. - if ( $this->get_exclude_sale_items() && $product->is_on_sale() ) { - $valid = false; - } - - return apply_filters( 'woocommerce_coupon_is_valid_for_product', $valid, $product, $this, $values ); - } - - /** - * Converts one of the WC_Coupon message/error codes to a message string and. - * displays the message/error. - * - * @param int $msg_code Message/error code. - */ - public function add_coupon_message( $msg_code ) { - $msg = $msg_code < 200 ? $this->get_coupon_error( $msg_code ) : $this->get_coupon_message( $msg_code ); - - if ( ! $msg ) { - return; - } - - if ( $msg_code < 200 ) { - wc_add_notice( $msg, 'error' ); - } else { - wc_add_notice( $msg ); - } - } - - /** - * Map one of the WC_Coupon message codes to a message string. - * - * @param integer $msg_code Message code. - * @return string Message/error string. - */ - public function get_coupon_message( $msg_code ) { - switch ( $msg_code ) { - case self::WC_COUPON_SUCCESS: - $msg = __( 'Coupon code applied successfully.', 'woocommerce' ); - break; - case self::WC_COUPON_REMOVED: - $msg = __( 'Coupon code removed successfully.', 'woocommerce' ); - break; - default: - $msg = ''; - break; - } - return apply_filters( 'woocommerce_coupon_message', $msg, $msg_code, $this ); - } - - /** - * Map one of the WC_Coupon error codes to a message string. - * - * @param int $err_code Message/error code. - * @return string Message/error string - */ - public function get_coupon_error( $err_code ) { - switch ( $err_code ) { - case self::E_WC_COUPON_INVALID_FILTERED: - $err = __( 'Coupon is not valid.', 'woocommerce' ); - break; - case self::E_WC_COUPON_NOT_EXIST: - /* translators: %s: coupon code */ - $err = sprintf( __( 'Coupon "%s" does not exist!', 'woocommerce' ), esc_html( $this->get_code() ) ); - break; - case self::E_WC_COUPON_INVALID_REMOVED: - /* translators: %s: coupon code */ - $err = sprintf( __( 'Sorry, it seems the coupon "%s" is invalid - it has now been removed from your order.', 'woocommerce' ), esc_html( $this->get_code() ) ); - break; - case self::E_WC_COUPON_NOT_YOURS_REMOVED: - /* translators: %s: coupon code */ - $err = sprintf( __( 'Sorry, it seems the coupon "%s" is not yours - it has now been removed from your order.', 'woocommerce' ), esc_html( $this->get_code() ) ); - break; - case self::E_WC_COUPON_ALREADY_APPLIED: - $err = __( 'Coupon code already applied!', 'woocommerce' ); - break; - case self::E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY: - /* translators: %s: coupon code */ - $err = sprintf( __( 'Sorry, coupon "%s" has already been applied and cannot be used in conjunction with other coupons.', 'woocommerce' ), esc_html( $this->get_code() ) ); - break; - case self::E_WC_COUPON_USAGE_LIMIT_REACHED: - $err = __( 'Coupon usage limit has been reached.', 'woocommerce' ); - break; - case self::E_WC_COUPON_EXPIRED: - $err = __( 'This coupon has expired.', 'woocommerce' ); - break; - case self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET: - /* translators: %s: coupon minimum amount */ - $err = sprintf( __( 'The minimum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->get_minimum_amount() ) ); - break; - case self::E_WC_COUPON_MAX_SPEND_LIMIT_MET: - /* translators: %s: coupon maximum amount */ - $err = sprintf( __( 'The maximum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->get_maximum_amount() ) ); - break; - case self::E_WC_COUPON_NOT_APPLICABLE: - $err = __( 'Sorry, this coupon is not applicable to your cart contents.', 'woocommerce' ); - break; - case self::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK: - if ( is_user_logged_in() && wc_get_page_id( 'myaccount' ) > 0 ) { - /* translators: %s: myaccount page link. */ - $err = sprintf( __( 'Coupon usage limit has been reached. If you were using this coupon just now but order was not complete, you can retry or cancel the order by going to the my account page.', 'woocommerce' ), wc_get_endpoint_url( 'orders', '', wc_get_page_permalink( 'myaccount' ) ) ); - } else { - $err = $this->get_coupon_error( self::E_WC_COUPON_USAGE_LIMIT_REACHED ); - } - break; - case self::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST: - $err = __( 'Coupon usage limit has been reached. Please try again after some time, or contact us for help.', 'woocommerce' ); - break; - case self::E_WC_COUPON_EXCLUDED_PRODUCTS: - // Store excluded products that are in cart in $products. - $products = array(); - if ( ! WC()->cart->is_empty() ) { - foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - if ( in_array( intval( $cart_item['product_id'] ), $this->get_excluded_product_ids(), true ) || in_array( intval( $cart_item['variation_id'] ), $this->get_excluded_product_ids(), true ) || in_array( intval( $cart_item['data']->get_parent_id() ), $this->get_excluded_product_ids(), true ) ) { - $products[] = $cart_item['data']->get_name(); - } - } - } - - /* translators: %s: products list */ - $err = sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) ); - break; - case self::E_WC_COUPON_EXCLUDED_CATEGORIES: - // Store excluded categories that are in cart in $categories. - $categories = array(); - if ( ! WC()->cart->is_empty() ) { - foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - $product_cats = wc_get_product_cat_ids( $cart_item['product_id'] ); - $intersect = array_intersect( $product_cats, $this->get_excluded_product_categories() ); - - if ( count( $intersect ) > 0 ) { - foreach ( $intersect as $cat_id ) { - $cat = get_term( $cat_id, 'product_cat' ); - $categories[] = $cat->name; - } - } - } - } - - /* translators: %s: categories list */ - $err = sprintf( __( 'Sorry, this coupon is not applicable to the categories: %s.', 'woocommerce' ), implode( ', ', array_unique( $categories ) ) ); - break; - case self::E_WC_COUPON_NOT_VALID_SALE_ITEMS: - $err = __( 'Sorry, this coupon is not valid for sale items.', 'woocommerce' ); - break; - default: - $err = ''; - break; - } - return apply_filters( 'woocommerce_coupon_error', $err, $err_code, $this ); - } - - /** - * Map one of the WC_Coupon error codes to an error string. - * No coupon instance will be available where a coupon does not exist, - * so this static method exists. - * - * @param int $err_code Error code. - * @return string Error string. - */ - public static function get_generic_coupon_error( $err_code ) { - switch ( $err_code ) { - case self::E_WC_COUPON_NOT_EXIST: - $err = __( 'Coupon does not exist!', 'woocommerce' ); - break; - case self::E_WC_COUPON_PLEASE_ENTER: - $err = __( 'Please enter a coupon code.', 'woocommerce' ); - break; - default: - $err = ''; - break; - } - // When using this static method, there is no $this to pass to filter. - return apply_filters( 'woocommerce_coupon_error', $err, $err_code, null ); - } -} diff --git a/includes/class-wc-customer-download.php b/includes/class-wc-customer-download.php deleted file mode 100644 index 1e1a6d35323..00000000000 --- a/includes/class-wc-customer-download.php +++ /dev/null @@ -1,407 +0,0 @@ - '', - 'product_id' => 0, - 'user_id' => 0, - 'user_email' => '', - 'order_id' => 0, - 'order_key' => '', - 'downloads_remaining' => '', - 'access_granted' => null, - 'access_expires' => null, - 'download_count' => 0, - ); - - /** - * Constructor. - * - * @param int|object|array $download Download ID, instance or data. - */ - public function __construct( $download = 0 ) { - parent::__construct( $download ); - - if ( is_numeric( $download ) && $download > 0 ) { - $this->set_id( $download ); - } elseif ( $download instanceof self ) { - $this->set_id( $download->get_id() ); - } elseif ( is_object( $download ) && ! empty( $download->permission_id ) ) { - $this->set_id( $download->permission_id ); - $this->set_props( (array) $download ); - $this->set_object_read( true ); - } else { - $this->set_object_read( true ); - } - - $this->data_store = WC_Data_Store::load( 'customer-download' ); - - if ( $this->get_id() > 0 ) { - $this->data_store->read( $this ); - } - } - - /* - |-------------------------------------------------------------------------- - | Getters - |-------------------------------------------------------------------------- - */ - - /** - * Get download id. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_download_id( $context = 'view' ) { - return $this->get_prop( 'download_id', $context ); - } - - /** - * Get product id. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return integer - */ - public function get_product_id( $context = 'view' ) { - return $this->get_prop( 'product_id', $context ); - } - - /** - * Get user id. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return integer - */ - public function get_user_id( $context = 'view' ) { - return $this->get_prop( 'user_id', $context ); - } - - /** - * Get user_email. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_user_email( $context = 'view' ) { - return $this->get_prop( 'user_email', $context ); - } - - /** - * Get order_id. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return integer - */ - public function get_order_id( $context = 'view' ) { - return $this->get_prop( 'order_id', $context ); - } - - /** - * Get order_key. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_order_key( $context = 'view' ) { - return $this->get_prop( 'order_key', $context ); - } - - /** - * Get downloads_remaining. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return integer|string - */ - public function get_downloads_remaining( $context = 'view' ) { - return $this->get_prop( 'downloads_remaining', $context ); - } - - /** - * Get access_granted. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return WC_DateTime|null Object if the date is set or null if there is no date. - */ - public function get_access_granted( $context = 'view' ) { - return $this->get_prop( 'access_granted', $context ); - } - - /** - * Get access_expires. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return WC_DateTime|null Object if the date is set or null if there is no date. - */ - public function get_access_expires( $context = 'view' ) { - return $this->get_prop( 'access_expires', $context ); - } - - /** - * Get download_count. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return integer - */ - public function get_download_count( $context = 'view' ) { - // Check for count of download logs. - $data_store = WC_Data_Store::load( 'customer-download-log' ); - $download_log_ids = $data_store->get_download_logs_for_permission( $this->get_id() ); - - $download_log_count = 0; - if ( ! empty( $download_log_ids ) ) { - $download_log_count = count( $download_log_ids ); - } - - // Check download count in prop. - $download_count_prop = $this->get_prop( 'download_count', $context ); - - // Return the larger of the two in case they differ. - // If logs are removed for some reason, we should still respect the - // count stored in the prop. - return max( $download_log_count, $download_count_prop ); - } - - /* - |-------------------------------------------------------------------------- - | Setters - |-------------------------------------------------------------------------- - */ - - /** - * Set download id. - * - * @param string $value Download ID. - */ - public function set_download_id( $value ) { - $this->set_prop( 'download_id', $value ); - } - /** - * Set product id. - * - * @param int $value Product ID. - */ - public function set_product_id( $value ) { - $this->set_prop( 'product_id', absint( $value ) ); - } - - /** - * Set user id. - * - * @param int $value User ID. - */ - public function set_user_id( $value ) { - $this->set_prop( 'user_id', absint( $value ) ); - } - - /** - * Set user_email. - * - * @param int $value User email. - */ - public function set_user_email( $value ) { - $this->set_prop( 'user_email', sanitize_email( $value ) ); - } - - /** - * Set order_id. - * - * @param int $value Order ID. - */ - public function set_order_id( $value ) { - $this->set_prop( 'order_id', absint( $value ) ); - } - - /** - * Set order_key. - * - * @param string $value Order key. - */ - public function set_order_key( $value ) { - $this->set_prop( 'order_key', $value ); - } - - /** - * Set downloads_remaining. - * - * @param integer|string $value Amount of downloads remaining. - */ - public function set_downloads_remaining( $value ) { - $this->set_prop( 'downloads_remaining', '' === $value ? '' : absint( $value ) ); - } - - /** - * Set access_granted. - * - * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. - */ - public function set_access_granted( $date = null ) { - $this->set_date_prop( 'access_granted', $date ); - } - - /** - * Set access_expires. - * - * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. - */ - public function set_access_expires( $date = null ) { - $this->set_date_prop( 'access_expires', $date ); - } - - /** - * Set download_count. - * - * @param int $value Download count. - */ - public function set_download_count( $value ) { - $this->set_prop( 'download_count', absint( $value ) ); - } - - /** - * Track a download on this permission. - * - * @since 3.3.0 - * @throws Exception When permission ID is invalid. - * @param int $user_id Id of the user performing the download. - * @param string $user_ip_address IP Address of the user performing the download. - */ - public function track_download( $user_id = null, $user_ip_address = null ) { - global $wpdb; - - // Must have a permission_id to track download log. - if ( ! ( $this->get_id() > 0 ) ) { - throw new Exception( __( 'Invalid permission ID.', 'woocommerce' ) ); - } - - // Increment download count, and decrement downloads remaining. - // Use SQL to avoid possible issues with downloads in quick succession. - // If downloads_remaining is blank, leave it blank (unlimited). - // Also, ensure downloads_remaining doesn't drop below zero. - $query = $wpdb->prepare( - " -UPDATE {$wpdb->prefix}woocommerce_downloadable_product_permissions -SET download_count = download_count + 1, -downloads_remaining = IF( downloads_remaining = '', '', GREATEST( 0, downloads_remaining - 1 ) ) -WHERE permission_id = %d", - $this->get_id() - ); - $wpdb->query( $query ); // WPCS: unprepared SQL ok. - - // Re-read this download from the data store to pull updated counts. - $this->data_store->read( $this ); - - // Track download in download log. - $download_log = new WC_Customer_Download_Log(); - $download_log->set_timestamp( current_time( 'timestamp', true ) ); - $download_log->set_permission_id( $this->get_id() ); - - if ( ! is_null( $user_id ) ) { - $download_log->set_user_id( $user_id ); - } - - if ( ! is_null( $user_ip_address ) ) { - $download_log->set_user_ip_address( $user_ip_address ); - } - - $download_log->save(); - } - - /* - |-------------------------------------------------------------------------- - | ArrayAccess/Backwards compatibility. - |-------------------------------------------------------------------------- - */ - - /** - * OffsetGet. - * - * @param string $offset Offset. - * @return mixed - */ - public function offsetGet( $offset ) { - if ( is_callable( array( $this, "get_$offset" ) ) ) { - return $this->{"get_$offset"}(); - } - } - - /** - * OffsetSet. - * - * @param string $offset Offset. - * @param mixed $value Value. - */ - public function offsetSet( $offset, $value ) { - if ( is_callable( array( $this, "set_$offset" ) ) ) { - $this->{"set_$offset"}( $value ); - } - } - - /** - * OffsetUnset - * - * @param string $offset Offset. - */ - public function offsetUnset( $offset ) { - if ( is_callable( array( $this, "set_$offset" ) ) ) { - $this->{"set_$offset"}( '' ); - } - } - - /** - * OffsetExists. - * - * @param string $offset Offset. - * @return bool - */ - public function offsetExists( $offset ) { - return in_array( $offset, array_keys( $this->data ), true ); - } - - /** - * Magic __isset method for backwards compatibility. Legacy properties which could be accessed directly in the past. - * - * @param string $key Key name. - * @return bool - */ - public function __isset( $key ) { - return in_array( $key, array_keys( $this->data ), true ); - } - - /** - * Magic __get method for backwards compatibility. Maps legacy vars to new getters. - * - * @param string $key Key name. - * @return mixed - */ - public function __get( $key ) { - if ( is_callable( array( $this, "get_$key" ) ) ) { - return $this->{"get_$key"}( '' ); - } - } -} diff --git a/includes/class-wc-customer.php b/includes/class-wc-customer.php deleted file mode 100644 index 3c5c53079f3..00000000000 --- a/includes/class-wc-customer.php +++ /dev/null @@ -1,1084 +0,0 @@ - null, - 'date_modified' => null, - 'email' => '', - 'first_name' => '', - 'last_name' => '', - 'display_name' => '', - 'role' => 'customer', - 'username' => '', - 'billing' => array( - 'first_name' => '', - 'last_name' => '', - 'company' => '', - 'address_1' => '', - 'address_2' => '', - 'city' => '', - 'postcode' => '', - 'country' => '', - 'state' => '', - 'email' => '', - 'phone' => '', - ), - 'shipping' => array( - 'first_name' => '', - 'last_name' => '', - 'company' => '', - 'address_1' => '', - 'address_2' => '', - 'city' => '', - 'postcode' => '', - 'country' => '', - 'state' => '', - ), - 'is_paying_customer' => false, - ); - - /** - * Stores a password if this needs to be changed. Write-only and hidden from _data. - * - * @var string - */ - protected $password = ''; - - /** - * Stores if user is VAT exempt for this session. - * - * @var string - */ - protected $is_vat_exempt = false; - - /** - * Stores if user has calculated shipping in this session. - * - * @var string - */ - protected $calculated_shipping = false; - - /** - * Load customer data based on how WC_Customer is called. - * - * If $customer is 'new', you can build a new WC_Customer object. If it's empty, some - * data will be pulled from the session for the current user/customer. - * - * @param WC_Customer|int $data Customer ID or data. - * @param bool $is_session True if this is the customer session. - * @throws Exception If customer cannot be read/found and $data is set. - */ - public function __construct( $data = 0, $is_session = false ) { - parent::__construct( $data ); - - if ( $data instanceof WC_Customer ) { - $this->set_id( absint( $data->get_id() ) ); - } elseif ( is_numeric( $data ) ) { - $this->set_id( $data ); - } - - $this->data_store = WC_Data_Store::load( 'customer' ); - - // If we have an ID, load the user from the DB. - if ( $this->get_id() ) { - try { - $this->data_store->read( $this ); - } catch ( Exception $e ) { - $this->set_id( 0 ); - $this->set_object_read( true ); - } - } else { - $this->set_object_read( true ); - } - - // If this is a session, set or change the data store to sessions. Changes do not persist in the database. - if ( $is_session && isset( WC()->session ) ) { - $this->data_store = WC_Data_Store::load( 'customer-session' ); - $this->data_store->read( $this ); - } - } - - /** - * Prefix for action and filter hooks on data. - * - * @since 3.0.0 - * @return string - */ - protected function get_hook_prefix() { - return 'woocommerce_customer_get_'; - } - - /** - * Delete a customer and reassign posts.. - * - * @param int $reassign Reassign posts and links to new User ID. - * @since 3.0.0 - * @return bool - */ - public function delete_and_reassign( $reassign = null ) { - if ( $this->data_store ) { - $this->data_store->delete( - $this, - array( - 'force_delete' => true, - 'reassign' => $reassign, - ) - ); - $this->set_id( 0 ); - return true; - } - return false; - } - - /** - * Is customer outside base country (for tax purposes)? - * - * @return bool - */ - public function is_customer_outside_base() { - list( $country, $state ) = $this->get_taxable_address(); - if ( $country ) { - $default = wc_get_base_location(); - if ( $default['country'] !== $country ) { - return true; - } - if ( $default['state'] && $default['state'] !== $state ) { - return true; - } - } - return false; - } - - /** - * Return this customer's avatar. - * - * @since 3.0.0 - * @return string - */ - public function get_avatar_url() { - return get_avatar_url( $this->get_email() ); - } - - /** - * Get taxable address. - * - * @return array - */ - public function get_taxable_address() { - $tax_based_on = get_option( 'woocommerce_tax_based_on' ); - - // Check shipping method at this point to see if we need special handling. - if ( true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ) && count( array_intersect( wc_get_chosen_shipping_method_ids(), apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) ) ) ) > 0 ) { - $tax_based_on = 'base'; - } - - if ( 'base' === $tax_based_on ) { - $country = WC()->countries->get_base_country(); - $state = WC()->countries->get_base_state(); - $postcode = WC()->countries->get_base_postcode(); - $city = WC()->countries->get_base_city(); - } elseif ( 'billing' === $tax_based_on ) { - $country = $this->get_billing_country(); - $state = $this->get_billing_state(); - $postcode = $this->get_billing_postcode(); - $city = $this->get_billing_city(); - } else { - $country = $this->get_shipping_country(); - $state = $this->get_shipping_state(); - $postcode = $this->get_shipping_postcode(); - $city = $this->get_shipping_city(); - } - - return apply_filters( 'woocommerce_customer_taxable_address', array( $country, $state, $postcode, $city ) ); - } - - /** - * Gets a customer's downloadable products. - * - * @return array Array of downloadable products - */ - public function get_downloadable_products() { - $downloads = array(); - if ( $this->get_id() ) { - $downloads = wc_get_customer_available_downloads( $this->get_id() ); - } - return apply_filters( 'woocommerce_customer_get_downloadable_products', $downloads ); - } - - /** - * Is customer VAT exempt? - * - * @return bool - */ - public function is_vat_exempt() { - return $this->get_is_vat_exempt(); - } - - /** - * Has calculated shipping? - * - * @return bool - */ - public function has_calculated_shipping() { - return $this->get_calculated_shipping(); - } - - /** - * Get if customer is VAT exempt? - * - * @since 3.0.0 - * @return bool - */ - public function get_is_vat_exempt() { - return $this->is_vat_exempt; - } - - /** - * Get password (only used when updating the user object). - * - * @return string - */ - public function get_password() { - return $this->password; - } - - /** - * Has customer calculated shipping? - * - * @return bool - */ - public function get_calculated_shipping() { - return $this->calculated_shipping; - } - - /** - * Set if customer has tax exemption. - * - * @param bool $is_vat_exempt If is vat exempt. - */ - public function set_is_vat_exempt( $is_vat_exempt ) { - $this->is_vat_exempt = wc_string_to_bool( $is_vat_exempt ); - } - - /** - * Calculated shipping? - * - * @param bool $calculated If shipping is calculated. - */ - public function set_calculated_shipping( $calculated = true ) { - $this->calculated_shipping = wc_string_to_bool( $calculated ); - } - - /** - * Set customer's password. - * - * @since 3.0.0 - * @param string $password Password. - */ - public function set_password( $password ) { - $this->password = $password; - } - - /** - * Gets the customers last order. - * - * @return WC_Order|false - */ - public function get_last_order() { - return $this->data_store->get_last_order( $this ); - } - - /** - * Return the number of orders this customer has. - * - * @return integer - */ - public function get_order_count() { - return $this->data_store->get_order_count( $this ); - } - - /** - * Return how much money this customer has spent. - * - * @return float - */ - public function get_total_spent() { - return $this->data_store->get_total_spent( $this ); - } - - /* - |-------------------------------------------------------------------------- - | Getters - |-------------------------------------------------------------------------- - */ - - /** - * Return the customer's username. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_username( $context = 'view' ) { - return $this->get_prop( 'username', $context ); - } - - /** - * Return the customer's email. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_email( $context = 'view' ) { - return $this->get_prop( 'email', $context ); - } - - /** - * Return customer's first name. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_first_name( $context = 'view' ) { - return $this->get_prop( 'first_name', $context ); - } - - /** - * Return customer's last name. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_last_name( $context = 'view' ) { - return $this->get_prop( 'last_name', $context ); - } - - /** - * Return customer's display name. - * - * @since 3.1.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_display_name( $context = 'view' ) { - return $this->get_prop( 'display_name', $context ); - } - - /** - * Return customer's user role. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_role( $context = 'view' ) { - return $this->get_prop( 'role', $context ); - } - - /** - * Return the date this customer was created. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return WC_DateTime|null object if the date is set or null if there is no date. - */ - public function get_date_created( $context = 'view' ) { - return $this->get_prop( 'date_created', $context ); - } - - /** - * Return the date this customer was last updated. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return WC_DateTime|null object if the date is set or null if there is no date. - */ - public function get_date_modified( $context = 'view' ) { - return $this->get_prop( 'date_modified', $context ); - } - - /** - * Gets a prop for a getter method. - * - * @since 3.0.0 - * @param string $prop Name of prop to get. - * @param string $address billing or shipping. - * @param string $context What the value is for. Valid values are 'view' and 'edit'. What the value is for. Valid values are view and edit. - * @return mixed - */ - protected function get_address_prop( $prop, $address = 'billing', $context = 'view' ) { - $value = null; - - if ( array_key_exists( $prop, $this->data[ $address ] ) ) { - $value = isset( $this->changes[ $address ][ $prop ] ) ? $this->changes[ $address ][ $prop ] : $this->data[ $address ][ $prop ]; - - if ( 'view' === $context ) { - $value = apply_filters( $this->get_hook_prefix() . $address . '_' . $prop, $value, $this ); - } - } - return $value; - } - - /** - * Get billing. - * - * @since 3.2.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return array - */ - public function get_billing( $context = 'view' ) { - return $this->get_prop( 'billing', $context ); - } - - /** - * Get billing_first_name. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_billing_first_name( $context = 'view' ) { - return $this->get_address_prop( 'first_name', 'billing', $context ); - } - - /** - * Get billing_last_name. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_billing_last_name( $context = 'view' ) { - return $this->get_address_prop( 'last_name', 'billing', $context ); - } - - /** - * Get billing_company. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_billing_company( $context = 'view' ) { - return $this->get_address_prop( 'company', 'billing', $context ); - } - - /** - * Get billing_address_1. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_billing_address( $context = 'view' ) { - return $this->get_billing_address_1( $context ); - } - - /** - * Get billing_address_1. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_billing_address_1( $context = 'view' ) { - return $this->get_address_prop( 'address_1', 'billing', $context ); - } - - /** - * Get billing_address_2. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string $value - */ - public function get_billing_address_2( $context = 'view' ) { - return $this->get_address_prop( 'address_2', 'billing', $context ); - } - - /** - * Get billing_city. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string $value - */ - public function get_billing_city( $context = 'view' ) { - return $this->get_address_prop( 'city', 'billing', $context ); - } - - /** - * Get billing_state. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_billing_state( $context = 'view' ) { - return $this->get_address_prop( 'state', 'billing', $context ); - } - - /** - * Get billing_postcode. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_billing_postcode( $context = 'view' ) { - return $this->get_address_prop( 'postcode', 'billing', $context ); - } - - /** - * Get billing_country. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_billing_country( $context = 'view' ) { - return $this->get_address_prop( 'country', 'billing', $context ); - } - - /** - * Get billing_email. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_billing_email( $context = 'view' ) { - return $this->get_address_prop( 'email', 'billing', $context ); - } - - /** - * Get billing_phone. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_billing_phone( $context = 'view' ) { - return $this->get_address_prop( 'phone', 'billing', $context ); - } - - /** - * Get shipping. - * - * @since 3.2.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return array - */ - public function get_shipping( $context = 'view' ) { - return $this->get_prop( 'shipping', $context ); - } - - /** - * Get shipping_first_name. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_shipping_first_name( $context = 'view' ) { - return $this->get_address_prop( 'first_name', 'shipping', $context ); - } - - /** - * Get shipping_last_name. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_shipping_last_name( $context = 'view' ) { - return $this->get_address_prop( 'last_name', 'shipping', $context ); - } - - /** - * Get shipping_company. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_shipping_company( $context = 'view' ) { - return $this->get_address_prop( 'company', 'shipping', $context ); - } - - /** - * Get shipping_address_1. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_shipping_address( $context = 'view' ) { - return $this->get_shipping_address_1( $context ); - } - - /** - * Get shipping_address_1. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_shipping_address_1( $context = 'view' ) { - return $this->get_address_prop( 'address_1', 'shipping', $context ); - } - - /** - * Get shipping_address_2. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_shipping_address_2( $context = 'view' ) { - return $this->get_address_prop( 'address_2', 'shipping', $context ); - } - - /** - * Get shipping_city. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_shipping_city( $context = 'view' ) { - return $this->get_address_prop( 'city', 'shipping', $context ); - } - - /** - * Get shipping_state. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_shipping_state( $context = 'view' ) { - return $this->get_address_prop( 'state', 'shipping', $context ); - } - - /** - * Get shipping_postcode. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_shipping_postcode( $context = 'view' ) { - return $this->get_address_prop( 'postcode', 'shipping', $context ); - } - - /** - * Get shipping_country. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_shipping_country( $context = 'view' ) { - return $this->get_address_prop( 'country', 'shipping', $context ); - } - - /** - * Is the user a paying customer? - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return bool - */ - public function get_is_paying_customer( $context = 'view' ) { - return $this->get_prop( 'is_paying_customer', $context ); - } - - /* - |-------------------------------------------------------------------------- - | Setters - |-------------------------------------------------------------------------- - */ - - /** - * Set customer's username. - * - * @since 3.0.0 - * @param string $username Username. - */ - public function set_username( $username ) { - $this->set_prop( 'username', $username ); - } - - /** - * Set customer's email. - * - * @since 3.0.0 - * @param string $value Email. - */ - public function set_email( $value ) { - if ( $value && ! is_email( $value ) ) { - $this->error( 'customer_invalid_email', __( 'Invalid email address', 'woocommerce' ) ); - } - $this->set_prop( 'email', sanitize_email( $value ) ); - } - - /** - * Set customer's first name. - * - * @since 3.0.0 - * @param string $first_name First name. - */ - public function set_first_name( $first_name ) { - $this->set_prop( 'first_name', $first_name ); - } - - /** - * Set customer's last name. - * - * @since 3.0.0 - * @param string $last_name Last name. - */ - public function set_last_name( $last_name ) { - $this->set_prop( 'last_name', $last_name ); - } - - /** - * Set customer's display name. - * - * @since 3.1.0 - * @param string $display_name Display name. - */ - public function set_display_name( $display_name ) { - /* translators: 1: first name 2: last name */ - $this->set_prop( 'display_name', is_email( $display_name ) ? sprintf( _x( '%1$s %2$s', 'display name', 'woocommerce' ), $this->get_first_name(), $this->get_last_name() ) : $display_name ); - } - - /** - * Set customer's user role(s). - * - * @since 3.0.0 - * @param mixed $role User role. - */ - public function set_role( $role ) { - global $wp_roles; - - if ( $role && ! empty( $wp_roles->roles ) && ! in_array( $role, array_keys( $wp_roles->roles ), true ) ) { - $this->error( 'customer_invalid_role', __( 'Invalid role', 'woocommerce' ) ); - } - $this->set_prop( 'role', $role ); - } - - /** - * Set the date this customer was last updated. - * - * @since 3.0.0 - * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. - */ - public function set_date_created( $date = null ) { - $this->set_date_prop( 'date_created', $date ); - } - - /** - * Set the date this customer was last updated. - * - * @since 3.0.0 - * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. - */ - public function set_date_modified( $date = null ) { - $this->set_date_prop( 'date_modified', $date ); - } - - /** - * Set customer address to match shop base address. - * - * @since 3.0.0 - */ - public function set_billing_address_to_base() { - $base = wc_get_customer_default_location(); - $this->set_billing_location( $base['country'], $base['state'], '', '' ); - } - - /** - * Set customer shipping address to base address. - * - * @since 3.0.0 - */ - public function set_shipping_address_to_base() { - $base = wc_get_customer_default_location(); - $this->set_shipping_location( $base['country'], $base['state'], '', '' ); - } - - /** - * Sets all address info at once. - * - * @param string $country Country. - * @param string $state State. - * @param string $postcode Postcode. - * @param string $city City. - */ - public function set_billing_location( $country, $state = '', $postcode = '', $city = '' ) { - $address_data = $this->get_prop( 'billing', 'edit' ); - - $address_data['address_1'] = ''; - $address_data['address_2'] = ''; - $address_data['city'] = $city; - $address_data['state'] = $state; - $address_data['postcode'] = $postcode; - $address_data['country'] = $country; - - $this->set_prop( 'billing', $address_data ); - } - - /** - * Sets all shipping info at once. - * - * @param string $country Country. - * @param string $state State. - * @param string $postcode Postcode. - * @param string $city City. - */ - public function set_shipping_location( $country, $state = '', $postcode = '', $city = '' ) { - $address_data = $this->get_prop( 'shipping', 'edit' ); - - $address_data['address_1'] = ''; - $address_data['address_2'] = ''; - $address_data['city'] = $city; - $address_data['state'] = $state; - $address_data['postcode'] = $postcode; - $address_data['country'] = $country; - - $this->set_prop( 'shipping', $address_data ); - } - - /** - * Sets a prop for a setter method. - * - * @since 3.0.0 - * @param string $prop Name of prop to set. - * @param string $address Name of address to set. billing or shipping. - * @param mixed $value Value of the prop. - */ - protected function set_address_prop( $prop, $address, $value ) { - if ( array_key_exists( $prop, $this->data[ $address ] ) ) { - if ( true === $this->object_read ) { - if ( $value !== $this->data[ $address ][ $prop ] || ( isset( $this->changes[ $address ] ) && array_key_exists( $prop, $this->changes[ $address ] ) ) ) { - $this->changes[ $address ][ $prop ] = $value; - } - } else { - $this->data[ $address ][ $prop ] = $value; - } - } - } - - /** - * Set billing_first_name. - * - * @param string $value Billing first name. - */ - public function set_billing_first_name( $value ) { - $this->set_address_prop( 'first_name', 'billing', $value ); - } - - /** - * Set billing_last_name. - * - * @param string $value Billing last name. - */ - public function set_billing_last_name( $value ) { - $this->set_address_prop( 'last_name', 'billing', $value ); - } - - /** - * Set billing_company. - * - * @param string $value Billing company. - */ - public function set_billing_company( $value ) { - $this->set_address_prop( 'company', 'billing', $value ); - } - - /** - * Set billing_address_1. - * - * @param string $value Billing address line 1. - */ - public function set_billing_address( $value ) { - $this->set_billing_address_1( $value ); - } - - /** - * Set billing_address_1. - * - * @param string $value Billing address line 1. - */ - public function set_billing_address_1( $value ) { - $this->set_address_prop( 'address_1', 'billing', $value ); - } - - /** - * Set billing_address_2. - * - * @param string $value Billing address line 2. - */ - public function set_billing_address_2( $value ) { - $this->set_address_prop( 'address_2', 'billing', $value ); - } - - /** - * Set billing_city. - * - * @param string $value Billing city. - */ - public function set_billing_city( $value ) { - $this->set_address_prop( 'city', 'billing', $value ); - } - - /** - * Set billing_state. - * - * @param string $value Billing state. - */ - public function set_billing_state( $value ) { - $this->set_address_prop( 'state', 'billing', $value ); - } - - /** - * Set billing_postcode. - * - * @param string $value Billing postcode. - */ - public function set_billing_postcode( $value ) { - $this->set_address_prop( 'postcode', 'billing', $value ); - } - - /** - * Set billing_country. - * - * @param string $value Billing country. - */ - public function set_billing_country( $value ) { - $this->set_address_prop( 'country', 'billing', $value ); - } - - /** - * Set billing_email. - * - * @param string $value Billing email. - */ - public function set_billing_email( $value ) { - if ( $value && ! is_email( $value ) ) { - $this->error( 'customer_invalid_billing_email', __( 'Invalid billing email address', 'woocommerce' ) ); - } - $this->set_address_prop( 'email', 'billing', sanitize_email( $value ) ); - } - - /** - * Set billing_phone. - * - * @param string $value Billing phone. - */ - public function set_billing_phone( $value ) { - $this->set_address_prop( 'phone', 'billing', $value ); - } - - /** - * Set shipping_first_name. - * - * @param string $value Shipping first name. - */ - public function set_shipping_first_name( $value ) { - $this->set_address_prop( 'first_name', 'shipping', $value ); - } - - /** - * Set shipping_last_name. - * - * @param string $value Shipping last name. - */ - public function set_shipping_last_name( $value ) { - $this->set_address_prop( 'last_name', 'shipping', $value ); - } - - /** - * Set shipping_company. - * - * @param string $value Shipping company. - */ - public function set_shipping_company( $value ) { - $this->set_address_prop( 'company', 'shipping', $value ); - } - - /** - * Set shipping_address_1. - * - * @param string $value Shipping address line 1. - */ - public function set_shipping_address( $value ) { - $this->set_shipping_address_1( $value ); - } - - /** - * Set shipping_address_1. - * - * @param string $value Shipping address line 1. - */ - public function set_shipping_address_1( $value ) { - $this->set_address_prop( 'address_1', 'shipping', $value ); - } - - /** - * Set shipping_address_2. - * - * @param string $value Shipping address line 2. - */ - public function set_shipping_address_2( $value ) { - $this->set_address_prop( 'address_2', 'shipping', $value ); - } - - /** - * Set shipping_city. - * - * @param string $value Shipping city. - */ - public function set_shipping_city( $value ) { - $this->set_address_prop( 'city', 'shipping', $value ); - } - - /** - * Set shipping_state. - * - * @param string $value Shipping state. - */ - public function set_shipping_state( $value ) { - $this->set_address_prop( 'state', 'shipping', $value ); - } - - /** - * Set shipping_postcode. - * - * @param string $value Shipping postcode. - */ - public function set_shipping_postcode( $value ) { - $this->set_address_prop( 'postcode', 'shipping', $value ); - } - - /** - * Set shipping_country. - * - * @param string $value Shipping country. - */ - public function set_shipping_country( $value ) { - $this->set_address_prop( 'country', 'shipping', $value ); - } - - /** - * Set if the user a paying customer. - * - * @since 3.0.0 - * @param bool $is_paying_customer If is a paying customer. - */ - public function set_is_paying_customer( $is_paying_customer ) { - $this->set_prop( 'is_paying_customer', (bool) $is_paying_customer ); - } -} diff --git a/includes/class-wc-datetime.php b/includes/class-wc-datetime.php deleted file mode 100644 index 1778503d5d1..00000000000 --- a/includes/class-wc-datetime.php +++ /dev/null @@ -1,103 +0,0 @@ -format( DATE_ATOM ); - } - - /** - * Set UTC offset - this is a fixed offset instead of a timezone. - * - * @param int $offset Offset. - */ - public function set_utc_offset( $offset ) { - $this->utc_offset = intval( $offset ); - } - - /** - * Get UTC offset if set, or default to the DateTime object's offset. - */ - public function getOffset() { - return $this->utc_offset ? $this->utc_offset : parent::getOffset(); - } - - /** - * Set timezone. - * - * @param DateTimeZone $timezone DateTimeZone instance. - * @return DateTime - */ - public function setTimezone( $timezone ) { - $this->utc_offset = 0; - return parent::setTimezone( $timezone ); - } - - /** - * Missing in PHP 5.2 so just here so it can be supported consistently. - * - * @since 3.0.0 - * @return int - */ - public function getTimestamp() { - return method_exists( 'DateTime', 'getTimestamp' ) ? parent::getTimestamp() : $this->format( 'U' ); - } - - /** - * Get the timestamp with the WordPress timezone offset added or subtracted. - * - * @since 3.0.0 - * @return int - */ - public function getOffsetTimestamp() { - return $this->getTimestamp() + $this->getOffset(); - } - - /** - * Format a date based on the offset timestamp. - * - * @since 3.0.0 - * @param string $format Date format. - * @return string - */ - public function date( $format ) { - return gmdate( $format, $this->getOffsetTimestamp() ); - } - - /** - * Return a localised date based on offset timestamp. Wrapper for date_i18n function. - * - * @since 3.0.0 - * @param string $format Date format. - * @return string - */ - public function date_i18n( $format = 'Y-m-d' ) { - return date_i18n( $format, $this->getOffsetTimestamp() ); - } -} diff --git a/includes/class-wc-discounts.php b/includes/class-wc-discounts.php deleted file mode 100644 index 522495fee8d..00000000000 --- a/includes/class-wc-discounts.php +++ /dev/null @@ -1,1021 +0,0 @@ - Item Key => Value - */ - protected $discounts = array(); - - /** - * WC_Discounts Constructor. - * - * @param WC_Cart|WC_Order $object Cart or order object. - */ - public function __construct( $object = null ) { - if ( is_a( $object, 'WC_Cart' ) ) { - $this->set_items_from_cart( $object ); - } elseif ( is_a( $object, 'WC_Order' ) ) { - $this->set_items_from_order( $object ); - } - } - - /** - * Set items directly. Used by WC_Cart_Totals. - * - * @since 3.2.3 - * @param array $items Items to set. - */ - public function set_items( $items ) { - $this->items = $items; - $this->discounts = array(); - uasort( $this->items, array( $this, 'sort_by_price' ) ); - } - - /** - * Normalise cart items which will be discounted. - * - * @since 3.2.0 - * @param WC_Cart $cart Cart object. - */ - public function set_items_from_cart( $cart ) { - $this->items = array(); - $this->discounts = array(); - - if ( ! is_a( $cart, 'WC_Cart' ) ) { - return; - } - - $this->object = $cart; - - foreach ( $cart->get_cart() as $key => $cart_item ) { - $item = new stdClass(); - $item->key = $key; - $item->object = $cart_item; - $item->product = $cart_item['data']; - $item->quantity = $cart_item['quantity']; - $item->price = wc_add_number_precision_deep( $item->product->get_price() * $item->quantity ); - $this->items[ $key ] = $item; - } - - uasort( $this->items, array( $this, 'sort_by_price' ) ); - } - - /** - * Normalise order items which will be discounted. - * - * @since 3.2.0 - * @param WC_Order $order Order object. - */ - public function set_items_from_order( $order ) { - $this->items = array(); - $this->discounts = array(); - - if ( ! is_a( $order, 'WC_Order' ) ) { - return; - } - - $this->object = $order; - - foreach ( $order->get_items() as $order_item ) { - $item = new stdClass(); - $item->key = $order_item->get_id(); - $item->object = $order_item; - $item->product = $order_item->get_product(); - $item->quantity = $order_item->get_quantity(); - $item->price = wc_add_number_precision_deep( $order_item->get_subtotal() ); - - if ( $order->get_prices_include_tax() ) { - $item->price += wc_add_number_precision_deep( $order_item->get_subtotal_tax() ); - } - - $this->items[ $order_item->get_id() ] = $item; - } - - uasort( $this->items, array( $this, 'sort_by_price' ) ); - } - - /** - * Get the object concerned. - * - * @since 3.3.2 - * @return object - */ - public function get_object() { - return $this->object; - } - - /** - * Get items. - * - * @since 3.2.0 - * @return object[] - */ - public function get_items() { - return $this->items; - } - - /** - * Get items to validate. - * - * @since 3.3.2 - * @return object[] - */ - public function get_items_to_validate() { - return apply_filters( 'woocommerce_coupon_get_items_to_validate', $this->get_items(), $this ); - } - - /** - * Get discount by key with or without precision. - * - * @since 3.2.0 - * @param string $key name of discount row to return. - * @param bool $in_cents Should the totals be returned in cents, or without precision. - * @return float - */ - public function get_discount( $key, $in_cents = false ) { - $item_discount_totals = $this->get_discounts_by_item( $in_cents ); - return isset( $item_discount_totals[ $key ] ) ? $item_discount_totals[ $key ] : 0; - } - - /** - * Get all discount totals. - * - * @since 3.2.0 - * @param bool $in_cents Should the totals be returned in cents, or without precision. - * @return array - */ - public function get_discounts( $in_cents = false ) { - $discounts = $this->discounts; - return $in_cents ? $discounts : wc_remove_number_precision_deep( $discounts ); - } - - /** - * Get all discount totals per item. - * - * @since 3.2.0 - * @param bool $in_cents Should the totals be returned in cents, or without precision. - * @return array - */ - public function get_discounts_by_item( $in_cents = false ) { - $discounts = $this->discounts; - $item_discount_totals = (array) array_shift( $discounts ); - - foreach ( $discounts as $item_discounts ) { - foreach ( $item_discounts as $item_key => $item_discount ) { - $item_discount_totals[ $item_key ] += $item_discount; - } - } - - return $in_cents ? $item_discount_totals : wc_remove_number_precision_deep( $item_discount_totals ); - } - - /** - * Get all discount totals per coupon. - * - * @since 3.2.0 - * @param bool $in_cents Should the totals be returned in cents, or without precision. - * @return array - */ - public function get_discounts_by_coupon( $in_cents = false ) { - $coupon_discount_totals = array_map( 'array_sum', $this->discounts ); - - return $in_cents ? $coupon_discount_totals : wc_remove_number_precision_deep( $coupon_discount_totals ); - } - - /** - * Get discounted price of an item without precision. - * - * @since 3.2.0 - * @param object $item Get data for this item. - * @return float - */ - public function get_discounted_price( $item ) { - return wc_remove_number_precision_deep( $this->get_discounted_price_in_cents( $item ) ); - } - - /** - * Get discounted price of an item to precision (in cents). - * - * @since 3.2.0 - * @param object $item Get data for this item. - * @return int - */ - public function get_discounted_price_in_cents( $item ) { - return absint( NumberUtil::round( $item->price - $this->get_discount( $item->key, true ) ) ); - } - - /** - * Apply a discount to all items using a coupon. - * - * @since 3.2.0 - * @param WC_Coupon $coupon Coupon object being applied to the items. - * @param bool $validate Set to false to skip coupon validation. - * @throws Exception Error message when coupon isn't valid. - * @return bool|WP_Error True if applied or WP_Error instance in failure. - */ - public function apply_coupon( $coupon, $validate = true ) { - if ( ! is_a( $coupon, 'WC_Coupon' ) ) { - return new WP_Error( 'invalid_coupon', __( 'Invalid coupon', 'woocommerce' ) ); - } - - $is_coupon_valid = $validate ? $this->is_coupon_valid( $coupon ) : true; - - if ( is_wp_error( $is_coupon_valid ) ) { - return $is_coupon_valid; - } - - if ( ! isset( $this->discounts[ $coupon->get_code() ] ) ) { - $this->discounts[ $coupon->get_code() ] = array_fill_keys( array_keys( $this->items ), 0 ); - } - - $items_to_apply = $this->get_items_to_apply_coupon( $coupon ); - - // Core discounts are handled here as of 3.2. - switch ( $coupon->get_discount_type() ) { - case 'percent': - $this->apply_coupon_percent( $coupon, $items_to_apply ); - break; - case 'fixed_product': - $this->apply_coupon_fixed_product( $coupon, $items_to_apply ); - break; - case 'fixed_cart': - $this->apply_coupon_fixed_cart( $coupon, $items_to_apply ); - break; - default: - $this->apply_coupon_custom( $coupon, $items_to_apply ); - break; - } - - return true; - } - - /** - * Sort by price. - * - * @since 3.2.0 - * @param array $a First element. - * @param array $b Second element. - * @return int - */ - protected function sort_by_price( $a, $b ) { - $price_1 = $a->price * $a->quantity; - $price_2 = $b->price * $b->quantity; - if ( $price_1 === $price_2 ) { - return 0; - } - return ( $price_1 < $price_2 ) ? 1 : -1; - } - - /** - * Filter out all products which have been fully discounted to 0. - * Used as array_filter callback. - * - * @since 3.2.0 - * @param object $item Get data for this item. - * @return bool - */ - protected function filter_products_with_price( $item ) { - return $this->get_discounted_price_in_cents( $item ) > 0; - } - - /** - * Get items which the coupon should be applied to. - * - * @since 3.2.0 - * @param object $coupon Coupon object. - * @return array - */ - protected function get_items_to_apply_coupon( $coupon ) { - $items_to_apply = array(); - - foreach ( $this->get_items_to_validate() as $item ) { - $item_to_apply = clone $item; // Clone the item so changes to this item do not affect the originals. - - if ( 0 === $this->get_discounted_price_in_cents( $item_to_apply ) || 0 >= $item_to_apply->quantity ) { - continue; - } - - if ( ! $coupon->is_valid_for_product( $item_to_apply->product, $item_to_apply->object ) && ! $coupon->is_valid_for_cart() ) { - continue; - } - - $items_to_apply[] = $item_to_apply; - } - return $items_to_apply; - } - - /** - * Apply percent discount to items and return an array of discounts granted. - * - * @since 3.2.0 - * @param WC_Coupon $coupon Coupon object. Passed through filters. - * @param array $items_to_apply Array of items to apply the coupon to. - * @return int Total discounted. - */ - protected function apply_coupon_percent( $coupon, $items_to_apply ) { - $total_discount = 0; - $cart_total = 0; - $limit_usage_qty = 0; - $applied_count = 0; - $adjust_final_discount = true; - - if ( null !== $coupon->get_limit_usage_to_x_items() ) { - $limit_usage_qty = $coupon->get_limit_usage_to_x_items(); - } - - $coupon_amount = $coupon->get_amount(); - - foreach ( $items_to_apply as $item ) { - // Find out how much price is available to discount for the item. - $discounted_price = $this->get_discounted_price_in_cents( $item ); - - // Get the price we actually want to discount, based on settings. - $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : NumberUtil::round( $item->price ); - - // See how many and what price to apply to. - $apply_quantity = $limit_usage_qty && ( $limit_usage_qty - $applied_count ) < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity; - $apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) ); - $price_to_discount = ( $price_to_discount / $item->quantity ) * $apply_quantity; - - // Run coupon calculations. - $discount = floor( $price_to_discount * ( $coupon_amount / 100 ) ); - - if ( is_a( $this->object, 'WC_Cart' ) && has_filter( 'woocommerce_coupon_get_discount_amount' ) ) { - // Send through the legacy filter, but not as cents. - $filtered_discount = wc_add_number_precision( apply_filters( 'woocommerce_coupon_get_discount_amount', wc_remove_number_precision( $discount ), wc_remove_number_precision( $price_to_discount ), $item->object, false, $coupon ) ); - - if ( $filtered_discount !== $discount ) { - $discount = $filtered_discount; - $adjust_final_discount = false; - } - } - - $discount = wc_round_discount( min( $discounted_price, $discount ), 0 ); - $cart_total = $cart_total + $price_to_discount; - $total_discount = $total_discount + $discount; - $applied_count = $applied_count + $apply_quantity; - - // Store code and discount amount per item. - $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; - } - - // Work out how much discount would have been given to the cart as a whole and compare to what was discounted on all line items. - $cart_total_discount = wc_round_discount( $cart_total * ( $coupon_amount / 100 ), 0 ); - - if ( $total_discount < $cart_total_discount && $adjust_final_discount ) { - $total_discount += $this->apply_coupon_remainder( $coupon, $items_to_apply, $cart_total_discount - $total_discount ); - } - - return $total_discount; - } - - /** - * Apply fixed product discount to items. - * - * @since 3.2.0 - * @param WC_Coupon $coupon Coupon object. Passed through filters. - * @param array $items_to_apply Array of items to apply the coupon to. - * @param int $amount Fixed discount amount to apply in cents. Leave blank to pull from coupon. - * @return int Total discounted. - */ - protected function apply_coupon_fixed_product( $coupon, $items_to_apply, $amount = null ) { - $total_discount = 0; - $amount = $amount ? $amount : wc_add_number_precision( $coupon->get_amount() ); - $limit_usage_qty = 0; - $applied_count = 0; - - if ( null !== $coupon->get_limit_usage_to_x_items() ) { - $limit_usage_qty = $coupon->get_limit_usage_to_x_items(); - } - - foreach ( $items_to_apply as $item ) { - // Find out how much price is available to discount for the item. - $discounted_price = $this->get_discounted_price_in_cents( $item ); - - // Get the price we actually want to discount, based on settings. - $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : $item->price; - - // Run coupon calculations. - if ( $limit_usage_qty ) { - $apply_quantity = $limit_usage_qty - $applied_count < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity; - $apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) ); - $discount = min( $amount, $item->price / $item->quantity ) * $apply_quantity; - } else { - $apply_quantity = apply_filters( 'woocommerce_coupon_get_apply_quantity', $item->quantity, $item, $coupon, $this ); - $discount = $amount * $apply_quantity; - } - - if ( is_a( $this->object, 'WC_Cart' ) && has_filter( 'woocommerce_coupon_get_discount_amount' ) ) { - // Send through the legacy filter, but not as cents. - $discount = wc_add_number_precision( apply_filters( 'woocommerce_coupon_get_discount_amount', wc_remove_number_precision( $discount ), wc_remove_number_precision( $price_to_discount ), $item->object, false, $coupon ) ); - } - - $discount = min( $discounted_price, $discount ); - $total_discount = $total_discount + $discount; - $applied_count = $applied_count + $apply_quantity; - - // Store code and discount amount per item. - $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; - } - return $total_discount; - } - - /** - * Apply fixed cart discount to items. - * - * @since 3.2.0 - * @param WC_Coupon $coupon Coupon object. Passed through filters. - * @param array $items_to_apply Array of items to apply the coupon to. - * @param int $amount Fixed discount amount to apply in cents. Leave blank to pull from coupon. - * @return int Total discounted. - */ - protected function apply_coupon_fixed_cart( $coupon, $items_to_apply, $amount = null ) { - $total_discount = 0; - $amount = $amount ? $amount : wc_add_number_precision( $coupon->get_amount() ); - $items_to_apply = array_filter( $items_to_apply, array( $this, 'filter_products_with_price' ) ); - $item_count = array_sum( wp_list_pluck( $items_to_apply, 'quantity' ) ); - - if ( ! $item_count ) { - return $total_discount; - } - - if ( ! $amount ) { - // If there is no amount we still send it through so filters are fired. - $total_discount = $this->apply_coupon_fixed_product( $coupon, $items_to_apply, 0 ); - } else { - $per_item_discount = absint( $amount / $item_count ); // round it down to the nearest cent. - - if ( $per_item_discount > 0 ) { - $total_discount = $this->apply_coupon_fixed_product( $coupon, $items_to_apply, $per_item_discount ); - - /** - * If there is still discount remaining, repeat the process. - */ - if ( $total_discount > 0 && $total_discount < $amount ) { - $total_discount += $this->apply_coupon_fixed_cart( $coupon, $items_to_apply, $amount - $total_discount ); - } - } elseif ( $amount > 0 ) { - $total_discount += $this->apply_coupon_remainder( $coupon, $items_to_apply, $amount ); - } - } - return $total_discount; - } - - /** - * Apply custom coupon discount to items. - * - * @since 3.3 - * @param WC_Coupon $coupon Coupon object. Passed through filters. - * @param array $items_to_apply Array of items to apply the coupon to. - * @return int Total discounted. - */ - protected function apply_coupon_custom( $coupon, $items_to_apply ) { - $limit_usage_qty = 0; - $applied_count = 0; - - if ( null !== $coupon->get_limit_usage_to_x_items() ) { - $limit_usage_qty = $coupon->get_limit_usage_to_x_items(); - } - - // Apply the coupon to each item. - foreach ( $items_to_apply as $item ) { - // Find out how much price is available to discount for the item. - $discounted_price = $this->get_discounted_price_in_cents( $item ); - - // Get the price we actually want to discount, based on settings. - $price_to_discount = wc_remove_number_precision( ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : $item->price ); - - // See how many and what price to apply to. - $apply_quantity = $limit_usage_qty && ( $limit_usage_qty - $applied_count ) < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity; - $apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) ); - - // Run coupon calculations. - $discount = wc_add_number_precision( $coupon->get_discount_amount( $price_to_discount / $item->quantity, $item->object, true ) ) * $apply_quantity; - $discount = wc_round_discount( min( $discounted_price, $discount ), 0 ); - $applied_count = $applied_count + $apply_quantity; - - // Store code and discount amount per item. - $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; - } - - // Allow post-processing for custom coupon types (e.g. calculating discrepancy, etc). - $this->discounts[ $coupon->get_code() ] = apply_filters( 'woocommerce_coupon_custom_discounts_array', $this->discounts[ $coupon->get_code() ], $coupon ); - - return array_sum( $this->discounts[ $coupon->get_code() ] ); - } - - /** - * Deal with remaining fractional discounts by splitting it over items - * until the amount is expired, discounting 1 cent at a time. - * - * @since 3.2.0 - * @param WC_Coupon $coupon Coupon object if appliable. Passed through filters. - * @param array $items_to_apply Array of items to apply the coupon to. - * @param int $amount Fixed discount amount to apply. - * @return int Total discounted. - */ - protected function apply_coupon_remainder( $coupon, $items_to_apply, $amount ) { - $total_discount = 0; - - foreach ( $items_to_apply as $item ) { - for ( $i = 0; $i < $item->quantity; $i ++ ) { - // Find out how much price is available to discount for the item. - $price_to_discount = $this->get_discounted_price_in_cents( $item ); - - // Run coupon calculations. - $discount = min( $price_to_discount, 1 ); - - // Store totals. - $total_discount += $discount; - - // Store code and discount amount per item. - $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; - - if ( $total_discount >= $amount ) { - break 2; - } - } - if ( $total_discount >= $amount ) { - break; - } - } - return $total_discount; - } - - /** - * Ensure coupon exists or throw exception. - * - * @since 3.2.0 - * @throws Exception Error message. - * @param WC_Coupon $coupon Coupon data. - * @return bool - */ - protected function validate_coupon_exists( $coupon ) { - if ( ! $coupon->get_id() && ! $coupon->get_virtual() ) { - /* translators: %s: coupon code */ - throw new Exception( sprintf( __( 'Coupon "%s" does not exist!', 'woocommerce' ), esc_html( $coupon->get_code() ) ), 105 ); - } - - return true; - } - - /** - * Ensure coupon usage limit is valid or throw exception. - * - * @since 3.2.0 - * @throws Exception Error message. - * @param WC_Coupon $coupon Coupon data. - * @return bool - */ - protected function validate_coupon_usage_limit( $coupon ) { - if ( ! $coupon->get_usage_limit() ) { - return true; - } - $usage_count = $coupon->get_usage_count(); - $data_store = $coupon->get_data_store(); - $tentative_usage_count = is_callable( array( $data_store, 'get_tentative_usage_count' ) ) ? $data_store->get_tentative_usage_count( $coupon->get_id() ) : 0; - if ( $usage_count + $tentative_usage_count < $coupon->get_usage_limit() ) { - // All good. - return true; - } - // Coupon usage limit is reached. Let's show as informative error message as we can. - if ( 0 === $tentative_usage_count ) { - // No held coupon, usage limit is indeed reached. - $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED; - } elseif ( is_user_logged_in() ) { - $recent_pending_orders = wc_get_orders( - array( - 'limit' => 1, - 'post_status' => array( 'wc-failed', 'wc-pending' ), - 'customer' => get_current_user_id(), - 'return' => 'ids', - ) - ); - if ( count( $recent_pending_orders ) > 0 ) { - // User logged in and have a pending order, maybe they are trying to use the coupon. - $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK; - } else { - $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED; - } - } else { - // Maybe this user was trying to use the coupon but got stuck. We can't know for sure (performantly). Show a slightly better error message. - $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST; - } - throw new Exception( $coupon->get_coupon_error( $error_code ), $error_code ); - } - - /** - * Ensure coupon user usage limit is valid or throw exception. - * - * Per user usage limit - check here if user is logged in (against user IDs). - * Checked again for emails later on in WC_Cart::check_customer_coupons(). - * - * @since 3.2.0 - * @throws Exception Error message. - * @param WC_Coupon $coupon Coupon data. - * @param int $user_id User ID. - * @return bool - */ - protected function validate_coupon_user_usage_limit( $coupon, $user_id = 0 ) { - if ( empty( $user_id ) ) { - if ( $this->object instanceof WC_Order ) { - $user_id = $this->object->get_customer_id(); - } else { - $user_id = get_current_user_id(); - } - } - - if ( $coupon && $user_id && apply_filters( 'woocommerce_coupon_validate_user_usage_limit', $coupon->get_usage_limit_per_user() > 0, $user_id, $coupon, $this ) && $coupon->get_id() && $coupon->get_data_store() ) { - $data_store = $coupon->get_data_store(); - $usage_count = $data_store->get_usage_by_user_id( $coupon, $user_id ); - if ( $usage_count >= $coupon->get_usage_limit_per_user() ) { - if ( $data_store->get_tentative_usages_for_user( $coupon->get_id(), array( $user_id ) ) > 0 ) { - $error_message = $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK ); - $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK; - } else { - $error_message = $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED ); - $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED; - } - throw new Exception( $error_message, $error_code ); - } - } - - return true; - } - - /** - * Ensure coupon date is valid or throw exception. - * - * @since 3.2.0 - * @throws Exception Error message. - * @param WC_Coupon $coupon Coupon data. - * @return bool - */ - protected function validate_coupon_expiry_date( $coupon ) { - if ( $coupon->get_date_expires() && apply_filters( 'woocommerce_coupon_validate_expiry_date', time() > $coupon->get_date_expires()->getTimestamp(), $coupon, $this ) ) { - throw new Exception( __( 'This coupon has expired.', 'woocommerce' ), 107 ); - } - - return true; - } - - /** - * Ensure coupon amount is valid or throw exception. - * - * @since 3.2.0 - * @throws Exception Error message. - * @param WC_Coupon $coupon Coupon data. - * @return bool - */ - protected function validate_coupon_minimum_amount( $coupon ) { - $subtotal = wc_remove_number_precision( $this->get_object_subtotal() ); - - if ( $coupon->get_minimum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_minimum_amount', $coupon->get_minimum_amount() > $subtotal, $coupon, $subtotal ) ) { - /* translators: %s: coupon minimum amount */ - throw new Exception( sprintf( __( 'The minimum spend for this coupon is %s.', 'woocommerce' ), wc_price( $coupon->get_minimum_amount() ) ), 108 ); - } - - return true; - } - - /** - * Ensure coupon amount is valid or throw exception. - * - * @since 3.2.0 - * @throws Exception Error message. - * @param WC_Coupon $coupon Coupon data. - * @return bool - */ - protected function validate_coupon_maximum_amount( $coupon ) { - $subtotal = wc_remove_number_precision( $this->get_object_subtotal() ); - - if ( $coupon->get_maximum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_maximum_amount', $coupon->get_maximum_amount() < $subtotal, $coupon ) ) { - /* translators: %s: coupon maximum amount */ - throw new Exception( sprintf( __( 'The maximum spend for this coupon is %s.', 'woocommerce' ), wc_price( $coupon->get_maximum_amount() ) ), 112 ); - } - - return true; - } - - /** - * Ensure coupon is valid for products in the list is valid or throw exception. - * - * @since 3.2.0 - * @throws Exception Error message. - * @param WC_Coupon $coupon Coupon data. - * @return bool - */ - protected function validate_coupon_product_ids( $coupon ) { - if ( count( $coupon->get_product_ids() ) > 0 ) { - $valid = false; - - foreach ( $this->get_items_to_validate() as $item ) { - if ( $item->product && in_array( $item->product->get_id(), $coupon->get_product_ids(), true ) || in_array( $item->product->get_parent_id(), $coupon->get_product_ids(), true ) ) { - $valid = true; - break; - } - } - - if ( ! $valid ) { - throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 ); - } - } - - return true; - } - - /** - * Ensure coupon is valid for product categories in the list is valid or throw exception. - * - * @since 3.2.0 - * @throws Exception Error message. - * @param WC_Coupon $coupon Coupon data. - * @return bool - */ - protected function validate_coupon_product_categories( $coupon ) { - if ( count( $coupon->get_product_categories() ) > 0 ) { - $valid = false; - - foreach ( $this->get_items_to_validate() as $item ) { - if ( $coupon->get_exclude_sale_items() && $item->product && $item->product->is_on_sale() ) { - continue; - } - - $product_cats = wc_get_product_cat_ids( $item->product->get_id() ); - - if ( $item->product->get_parent_id() ) { - $product_cats = array_merge( $product_cats, wc_get_product_cat_ids( $item->product->get_parent_id() ) ); - } - - // If we find an item with a cat in our allowed cat list, the coupon is valid. - if ( count( array_intersect( $product_cats, $coupon->get_product_categories() ) ) > 0 ) { - $valid = true; - break; - } - } - - if ( ! $valid ) { - throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 ); - } - } - - return true; - } - - /** - * Ensure coupon is valid for sale items in the list is valid or throw exception. - * - * @since 3.2.0 - * @throws Exception Error message. - * @param WC_Coupon $coupon Coupon data. - * @return bool - */ - protected function validate_coupon_sale_items( $coupon ) { - if ( $coupon->get_exclude_sale_items() ) { - $valid = true; - - foreach ( $this->get_items_to_validate() as $item ) { - if ( $item->product && $item->product->is_on_sale() ) { - $valid = false; - break; - } - } - - if ( ! $valid ) { - throw new Exception( __( 'Sorry, this coupon is not valid for sale items.', 'woocommerce' ), 110 ); - } - } - - return true; - } - - /** - * All exclusion rules must pass at the same time for a product coupon to be valid. - * - * @since 3.2.0 - * @throws Exception Error message. - * @param WC_Coupon $coupon Coupon data. - * @return bool - */ - protected function validate_coupon_excluded_items( $coupon ) { - $items = $this->get_items_to_validate(); - if ( ! empty( $items ) && $coupon->is_type( wc_get_product_coupon_types() ) ) { - $valid = false; - - foreach ( $items as $item ) { - if ( $item->product && $coupon->is_valid_for_product( $item->product, $item->object ) ) { - $valid = true; - break; - } - } - - if ( ! $valid ) { - throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 ); - } - } - - return true; - } - - /** - * Cart discounts cannot be added if non-eligible product is found. - * - * @since 3.2.0 - * @throws Exception Error message. - * @param WC_Coupon $coupon Coupon data. - * @return bool - */ - protected function validate_coupon_eligible_items( $coupon ) { - if ( ! $coupon->is_type( wc_get_product_coupon_types() ) ) { - $this->validate_coupon_sale_items( $coupon ); - $this->validate_coupon_excluded_product_ids( $coupon ); - $this->validate_coupon_excluded_product_categories( $coupon ); - } - - return true; - } - - /** - * Exclude products. - * - * @since 3.2.0 - * @throws Exception Error message. - * @param WC_Coupon $coupon Coupon data. - * @return bool - */ - protected function validate_coupon_excluded_product_ids( $coupon ) { - // Exclude Products. - if ( count( $coupon->get_excluded_product_ids() ) > 0 ) { - $products = array(); - - foreach ( $this->get_items_to_validate() as $item ) { - if ( $item->product && in_array( $item->product->get_id(), $coupon->get_excluded_product_ids(), true ) || in_array( $item->product->get_parent_id(), $coupon->get_excluded_product_ids(), true ) ) { - $products[] = $item->product->get_name(); - } - } - - if ( ! empty( $products ) ) { - /* translators: %s: products list */ - throw new Exception( sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) ), 113 ); - } - } - - return true; - } - - /** - * Exclude categories from product list. - * - * @since 3.2.0 - * @throws Exception Error message. - * @param WC_Coupon $coupon Coupon data. - * @return bool - */ - protected function validate_coupon_excluded_product_categories( $coupon ) { - if ( count( $coupon->get_excluded_product_categories() ) > 0 ) { - $categories = array(); - - foreach ( $this->get_items_to_validate() as $item ) { - if ( ! $item->product ) { - continue; - } - - $product_cats = wc_get_product_cat_ids( $item->product->get_id() ); - - if ( $item->product->get_parent_id() ) { - $product_cats = array_merge( $product_cats, wc_get_product_cat_ids( $item->product->get_parent_id() ) ); - } - - $cat_id_list = array_intersect( $product_cats, $coupon->get_excluded_product_categories() ); - if ( count( $cat_id_list ) > 0 ) { - foreach ( $cat_id_list as $cat_id ) { - $cat = get_term( $cat_id, 'product_cat' ); - $categories[] = $cat->name; - } - } - } - - if ( ! empty( $categories ) ) { - /* translators: %s: categories list */ - throw new Exception( sprintf( __( 'Sorry, this coupon is not applicable to the categories: %s.', 'woocommerce' ), implode( ', ', array_unique( $categories ) ) ), 114 ); - } - } - - return true; - } - - /** - * Get the object subtotal - * - * @return int - */ - protected function get_object_subtotal() { - if ( is_a( $this->object, 'WC_Cart' ) ) { - return wc_add_number_precision( $this->object->get_displayed_subtotal() ); - } elseif ( is_a( $this->object, 'WC_Order' ) ) { - $subtotal = wc_add_number_precision( $this->object->get_subtotal() ); - - if ( $this->object->get_prices_include_tax() ) { - // Add tax to tax-exclusive subtotal. - $subtotal = $subtotal + wc_add_number_precision( NumberUtil::round( $this->object->get_total_tax(), wc_get_price_decimals() ) ); - } - - return $subtotal; - } else { - return array_sum( wp_list_pluck( $this->items, 'price' ) ); - } - } - - /** - * Check if a coupon is valid. - * - * Error Codes: - * - 100: Invalid filtered. - * - 101: Invalid removed. - * - 102: Not yours removed. - * - 103: Already applied. - * - 104: Individual use only. - * - 105: Not exists. - * - 106: Usage limit reached. - * - 107: Expired. - * - 108: Minimum spend limit not met. - * - 109: Not applicable. - * - 110: Not valid for sale items. - * - 111: Missing coupon code. - * - 112: Maximum spend limit met. - * - 113: Excluded products. - * - 114: Excluded categories. - * - * @since 3.2.0 - * @throws Exception Error message. - * @param WC_Coupon $coupon Coupon data. - * @return bool|WP_Error - */ - public function is_coupon_valid( $coupon ) { - try { - $this->validate_coupon_exists( $coupon ); - $this->validate_coupon_usage_limit( $coupon ); - $this->validate_coupon_user_usage_limit( $coupon ); - $this->validate_coupon_expiry_date( $coupon ); - $this->validate_coupon_minimum_amount( $coupon ); - $this->validate_coupon_maximum_amount( $coupon ); - $this->validate_coupon_product_ids( $coupon ); - $this->validate_coupon_product_categories( $coupon ); - $this->validate_coupon_excluded_items( $coupon ); - $this->validate_coupon_eligible_items( $coupon ); - - if ( ! apply_filters( 'woocommerce_coupon_is_valid', true, $coupon, $this ) ) { - throw new Exception( __( 'Coupon is not valid.', 'woocommerce' ), 100 ); - } - } catch ( Exception $e ) { - /** - * Filter the coupon error message. - * - * @param string $error_message Error message. - * @param int $error_code Error code. - * @param WC_Coupon $coupon Coupon data. - */ - $message = apply_filters( 'woocommerce_coupon_error', is_numeric( $e->getMessage() ) ? $coupon->get_coupon_error( $e->getMessage() ) : $e->getMessage(), $e->getCode(), $coupon ); - - return new WP_Error( - 'invalid_coupon', - $message, - array( - 'status' => 400, - ) - ); - } - return true; - } -} diff --git a/includes/class-wc-download-handler.php b/includes/class-wc-download-handler.php deleted file mode 100644 index fa279c8c6fd..00000000000 --- a/includes/class-wc-download-handler.php +++ /dev/null @@ -1,630 +0,0 @@ -get_billing_email() : null; - - // Prepare email address hash. - $email_hash = function_exists( 'hash' ) ? hash( 'sha256', $email_address ) : sha1( $email_address ); - - if ( is_null( $email_address ) || ! hash_equals( wp_unslash( $_GET['uid'] ), $email_hash ) ) { // WPCS: input var ok, CSRF ok, sanitization ok. - self::download_error( __( 'Invalid download link.', 'woocommerce' ) ); - } - } - - $download_ids = $data_store->get_downloads( - array( - 'user_email' => sanitize_email( str_replace( ' ', '+', $email_address ) ), - 'order_key' => wc_clean( wp_unslash( $_GET['order'] ) ), // WPCS: input var ok, CSRF ok. - 'product_id' => $product_id, - 'download_id' => wc_clean( preg_replace( '/\s+/', ' ', wp_unslash( $_GET['key'] ) ) ), // WPCS: input var ok, CSRF ok, sanitization ok. - 'orderby' => 'downloads_remaining', - 'order' => 'DESC', - 'limit' => 1, - 'return' => 'ids', - ) - ); - - if ( empty( $download_ids ) ) { - self::download_error( __( 'Invalid download link.', 'woocommerce' ) ); - } - - $download = new WC_Customer_Download( current( $download_ids ) ); - - /** - * Filter download filepath. - * - * @since 4.0.0 - * @param string $file_path File path. - * @param string $email_address Email address. - * @param WC_Order|bool $order Order object or false. - * @param WC_Product $product Product object. - * @param WC_Customer_Download $download Download data. - */ - $file_path = apply_filters( - 'woocommerce_download_product_filepath', - $product->get_file_download_path( $download->get_download_id() ), - $email_address, - $order, - $product, - $download - ); - - $parsed_file_path = self::parse_file_path( $file_path ); - $download_range = self::get_download_range( @filesize( $parsed_file_path['file_path'] ) ); // @codingStandardsIgnoreLine. - - self::check_order_is_valid( $download ); - if ( ! $download_range['is_range_request'] ) { - // If the remaining download count goes to 0, allow range requests to be able to finish streaming from iOS devices. - self::check_downloads_remaining( $download ); - } - self::check_download_expiry( $download ); - self::check_download_login_required( $download ); - - do_action( - 'woocommerce_download_product', - $download->get_user_email(), - $download->get_order_key(), - $download->get_product_id(), - $download->get_user_id(), - $download->get_download_id(), - $download->get_order_id() - ); - $download->save(); - - // Track the download in logs and change remaining/counts. - $current_user_id = get_current_user_id(); - $ip_address = WC_Geolocation::get_ip_address(); - if ( ! $download_range['is_range_request'] ) { - $download->track_download( $current_user_id > 0 ? $current_user_id : null, ! empty( $ip_address ) ? $ip_address : null ); - } - - self::download( $file_path, $download->get_product_id() ); - } - - /** - * Check if an order is valid for downloading from. - * - * @param WC_Customer_Download $download Download instance. - */ - private static function check_order_is_valid( $download ) { - if ( $download->get_order_id() ) { - $order = wc_get_order( $download->get_order_id() ); - - if ( $order && ! $order->is_download_permitted() ) { - self::download_error( __( 'Invalid order.', 'woocommerce' ), '', 403 ); - } - } - } - - /** - * Check if there are downloads remaining. - * - * @param WC_Customer_Download $download Download instance. - */ - private static function check_downloads_remaining( $download ) { - if ( '' !== $download->get_downloads_remaining() && 0 >= $download->get_downloads_remaining() ) { - self::download_error( __( 'Sorry, you have reached your download limit for this file', 'woocommerce' ), '', 403 ); - } - } - - /** - * Check if the download has expired. - * - * @param WC_Customer_Download $download Download instance. - */ - private static function check_download_expiry( $download ) { - if ( ! is_null( $download->get_access_expires() ) && $download->get_access_expires()->getTimestamp() < strtotime( 'midnight', time() ) ) { - self::download_error( __( 'Sorry, this download has expired', 'woocommerce' ), '', 403 ); - } - } - - /** - * Check if a download requires the user to login first. - * - * @param WC_Customer_Download $download Download instance. - */ - private static function check_download_login_required( $download ) { - if ( $download->get_user_id() && 'yes' === get_option( 'woocommerce_downloads_require_login' ) ) { - if ( ! is_user_logged_in() ) { - if ( wc_get_page_id( 'myaccount' ) ) { - wp_safe_redirect( add_query_arg( 'wc_error', rawurlencode( __( 'You must be logged in to download files.', 'woocommerce' ) ), wc_get_page_permalink( 'myaccount' ) ) ); - exit; - } else { - self::download_error( __( 'You must be logged in to download files.', 'woocommerce' ) . ' ' . __( 'Login', 'woocommerce' ) . '', __( 'Log in to Download Files', 'woocommerce' ), 403 ); - } - } elseif ( ! current_user_can( 'download_file', $download ) ) { - self::download_error( __( 'This is not your download link.', 'woocommerce' ), '', 403 ); - } - } - } - - /** - * Count download. - * - * @deprecated 4.4.0 - * @param array $download_data Download data. - */ - public static function count_download( $download_data ) { - wc_deprecated_function( 'WC_Download_Handler::count_download', '4.4.0', '' ); - } - - /** - * Download a file - hook into init function. - * - * @param string $file_path URL to file. - * @param integer $product_id Product ID of the product being downloaded. - */ - public static function download( $file_path, $product_id ) { - if ( ! $file_path ) { - self::download_error( __( 'No file defined', 'woocommerce' ) ); - } - - $filename = basename( $file_path ); - - if ( strstr( $filename, '?' ) ) { - $filename = current( explode( '?', $filename ) ); - } - - $filename = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id ); - - /** - * Filter download method. - * - * @since 4.5.0 - * @param string $method Download method. - * @param int $product_id Product ID. - * @param string $file_path URL to file. - */ - $file_download_method = apply_filters( 'woocommerce_file_download_method', get_option( 'woocommerce_file_download_method', 'force' ), $product_id, $file_path ); - - // Add action to prevent issues in IE. - add_action( 'nocache_headers', array( __CLASS__, 'ie_nocache_headers_fix' ) ); - - // Trigger download via one of the methods. - do_action( 'woocommerce_download_file_' . $file_download_method, $file_path, $filename ); - } - - /** - * Redirect to a file to start the download. - * - * @param string $file_path File path. - * @param string $filename File name. - */ - public static function download_file_redirect( $file_path, $filename = '' ) { - header( 'Location: ' . $file_path ); - exit; - } - - /** - * Parse file path and see if its remote or local. - * - * @param string $file_path File path. - * @return array - */ - public static function parse_file_path( $file_path ) { - $wp_uploads = wp_upload_dir(); - $wp_uploads_dir = $wp_uploads['basedir']; - $wp_uploads_url = $wp_uploads['baseurl']; - - /** - * Replace uploads dir, site url etc with absolute counterparts if we can. - * Note the str_replace on site_url is on purpose, so if https is forced - * via filters we can still do the string replacement on a HTTP file. - */ - $replacements = array( - $wp_uploads_url => $wp_uploads_dir, - network_site_url( '/', 'https' ) => ABSPATH, - str_replace( 'https:', 'http:', network_site_url( '/', 'http' ) ) => ABSPATH, - site_url( '/', 'https' ) => ABSPATH, - str_replace( 'https:', 'http:', site_url( '/', 'http' ) ) => ABSPATH, - ); - - $file_path = str_replace( array_keys( $replacements ), array_values( $replacements ), $file_path ); - $parsed_file_path = wp_parse_url( $file_path ); - $remote_file = true; - - // Paths that begin with '//' are always remote URLs. - if ( '//' === substr( $file_path, 0, 2 ) ) { - return array( - 'remote_file' => true, - 'file_path' => is_ssl() ? 'https:' . $file_path : 'http:' . $file_path, - ); - } - - // See if path needs an abspath prepended to work. - if ( file_exists( ABSPATH . $file_path ) ) { - $remote_file = false; - $file_path = ABSPATH . $file_path; - - } elseif ( '/wp-content' === substr( $file_path, 0, 11 ) ) { - $remote_file = false; - $file_path = realpath( WP_CONTENT_DIR . substr( $file_path, 11 ) ); - - // Check if we have an absolute path. - } elseif ( ( ! isset( $parsed_file_path['scheme'] ) || ! in_array( $parsed_file_path['scheme'], array( 'http', 'https', 'ftp' ), true ) ) && isset( $parsed_file_path['path'] ) && file_exists( $parsed_file_path['path'] ) ) { - $remote_file = false; - $file_path = $parsed_file_path['path']; - } - - return array( - 'remote_file' => $remote_file, - 'file_path' => $file_path, - ); - } - - /** - * Download a file using X-Sendfile, X-Lighttpd-Sendfile, or X-Accel-Redirect if available. - * - * @param string $file_path File path. - * @param string $filename File name. - */ - public static function download_file_xsendfile( $file_path, $filename ) { - $parsed_file_path = self::parse_file_path( $file_path ); - - /** - * Fallback on force download method for remote files. This is because: - * 1. xsendfile needs proxy configuration to work for remote files, which cannot be assumed to be available on most hosts. - * 2. Force download method is more secure than redirect method if `allow_url_fopen` is enabled in `php.ini`. We fallback to redirect method in force download method anyway in case `allow_url_fopen` is not enabled. - */ - if ( $parsed_file_path['remote_file'] && ! apply_filters( 'woocommerce_use_xsendfile_for_remote', false ) ) { - do_action( 'woocommerce_download_file_force', $file_path, $filename ); - return; - } - - if ( function_exists( 'apache_get_modules' ) && in_array( 'mod_xsendfile', apache_get_modules(), true ) ) { - self::download_headers( $parsed_file_path['file_path'], $filename ); - $filepath = apply_filters( 'woocommerce_download_file_xsendfile_file_path', $parsed_file_path['file_path'], $file_path, $filename, $parsed_file_path ); - header( 'X-Sendfile: ' . $filepath ); - exit; - } elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'lighttpd' ) ) { - self::download_headers( $parsed_file_path['file_path'], $filename ); - $filepath = apply_filters( 'woocommerce_download_file_xsendfile_lighttpd_file_path', $parsed_file_path['file_path'], $file_path, $filename, $parsed_file_path ); - header( 'X-Lighttpd-Sendfile: ' . $filepath ); - exit; - } elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'nginx' ) || stristr( getenv( 'SERVER_SOFTWARE' ), 'cherokee' ) ) { - self::download_headers( $parsed_file_path['file_path'], $filename ); - $xsendfile_path = trim( preg_replace( '`^' . str_replace( '\\', '/', getcwd() ) . '`', '', $parsed_file_path['file_path'] ), '/' ); - $xsendfile_path = apply_filters( 'woocommerce_download_file_xsendfile_x_accel_redirect_file_path', $xsendfile_path, $file_path, $filename, $parsed_file_path ); - header( "X-Accel-Redirect: /$xsendfile_path" ); - exit; - } - - // Fallback. - self::download_file_force( $file_path, $filename ); - } - - /** - * Parse the HTTP_RANGE request from iOS devices. - * Does not support multi-range requests. - * - * @param int $file_size Size of file in bytes. - * @return array { - * Information about range download request: beginning and length of - * file chunk, whether the range is valid/supported and whether the request is a range request. - * - * @type int $start Byte offset of the beginning of the range. Default 0. - * @type int $length Length of the requested file chunk in bytes. Optional. - * @type bool $is_range_valid Whether the requested range is a valid and supported range. - * @type bool $is_range_request Whether the request is a range request. - * } - */ - protected static function get_download_range( $file_size ) { - $start = 0; - $download_range = array( - 'start' => $start, - 'is_range_valid' => false, - 'is_range_request' => false, - ); - - if ( ! $file_size ) { - return $download_range; - } - - $end = $file_size - 1; - $download_range['length'] = $file_size; - - if ( isset( $_SERVER['HTTP_RANGE'] ) ) { // @codingStandardsIgnoreLine. - $http_range = sanitize_text_field( wp_unslash( $_SERVER['HTTP_RANGE'] ) ); // WPCS: input var ok. - $download_range['is_range_request'] = true; - - $c_start = $start; - $c_end = $end; - // Extract the range string. - list( , $range ) = explode( '=', $http_range, 2 ); - // Make sure the client hasn't sent us a multibyte range. - if ( strpos( $range, ',' ) !== false ) { - return $download_range; - } - - /* - * If the range starts with an '-' we start from the beginning. - * If not, we forward the file pointer - * and make sure to get the end byte if specified. - */ - if ( '-' === $range[0] ) { - // The n-number of the last bytes is requested. - $c_start = $file_size - substr( $range, 1 ); - } else { - $range = explode( '-', $range ); - $c_start = ( isset( $range[0] ) && is_numeric( $range[0] ) ) ? (int) $range[0] : 0; - $c_end = ( isset( $range[1] ) && is_numeric( $range[1] ) ) ? (int) $range[1] : $file_size; - } - - /* - * Check the range and make sure it's treated according to the specs: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html. - * End bytes can not be larger than $end. - */ - $c_end = ( $c_end > $end ) ? $end : $c_end; - // Validate the requested range and return an error if it's not correct. - if ( $c_start > $c_end || $c_start > $file_size - 1 || $c_end >= $file_size ) { - return $download_range; - } - $start = $c_start; - $end = $c_end; - $length = $end - $start + 1; - - $download_range['start'] = $start; - $download_range['length'] = $length; - $download_range['is_range_valid'] = true; - } - return $download_range; - } - - /** - * Force download - this is the default method. - * - * @param string $file_path File path. - * @param string $filename File name. - */ - public static function download_file_force( $file_path, $filename ) { - $parsed_file_path = self::parse_file_path( $file_path ); - $download_range = self::get_download_range( @filesize( $parsed_file_path['file_path'] ) ); // @codingStandardsIgnoreLine. - - self::download_headers( $parsed_file_path['file_path'], $filename, $download_range ); - - $start = isset( $download_range['start'] ) ? $download_range['start'] : 0; - $length = isset( $download_range['length'] ) ? $download_range['length'] : 0; - if ( ! self::readfile_chunked( $parsed_file_path['file_path'], $start, $length ) ) { - if ( $parsed_file_path['remote_file'] ) { - self::download_file_redirect( $file_path ); - } else { - self::download_error( __( 'File not found', 'woocommerce' ) ); - } - } - - exit; - } - - /** - * Get content type of a download. - * - * @param string $file_path File path. - * @return string - */ - private static function get_download_content_type( $file_path ) { - $file_extension = strtolower( substr( strrchr( $file_path, '.' ), 1 ) ); - $ctype = 'application/force-download'; - - foreach ( get_allowed_mime_types() as $mime => $type ) { - $mimes = explode( '|', $mime ); - if ( in_array( $file_extension, $mimes, true ) ) { - $ctype = $type; - break; - } - } - - return $ctype; - } - - /** - * Set headers for the download. - * - * @param string $file_path File path. - * @param string $filename File name. - * @param array $download_range Array containing info about range download request (see {@see get_download_range} for structure). - */ - private static function download_headers( $file_path, $filename, $download_range = array() ) { - self::check_server_config(); - self::clean_buffers(); - wc_nocache_headers(); - - header( 'X-Robots-Tag: noindex, nofollow', true ); - header( 'Content-Type: ' . self::get_download_content_type( $file_path ) ); - header( 'Content-Description: File Transfer' ); - header( 'Content-Disposition: attachment; filename="' . $filename . '";' ); - header( 'Content-Transfer-Encoding: binary' ); - - $file_size = @filesize( $file_path ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged - if ( ! $file_size ) { - return; - } - - if ( isset( $download_range['is_range_request'] ) && true === $download_range['is_range_request'] ) { - if ( false === $download_range['is_range_valid'] ) { - header( 'HTTP/1.1 416 Requested Range Not Satisfiable' ); - header( 'Content-Range: bytes 0-' . ( $file_size - 1 ) . '/' . $file_size ); - exit; - } - - $start = $download_range['start']; - $end = $download_range['start'] + $download_range['length'] - 1; - $length = $download_range['length']; - - header( 'HTTP/1.1 206 Partial Content' ); - header( "Accept-Ranges: 0-$file_size" ); - header( "Content-Range: bytes $start-$end/$file_size" ); - header( "Content-Length: $length" ); - } else { - header( 'Content-Length: ' . $file_size ); - } - } - - /** - * Check and set certain server config variables to ensure downloads work as intended. - */ - private static function check_server_config() { - wc_set_time_limit( 0 ); - if ( function_exists( 'apache_setenv' ) ) { - @apache_setenv( 'no-gzip', 1 ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_apache_setenv - } - @ini_set( 'zlib.output_compression', 'Off' ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_ini_set - @session_write_close(); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.VIP.SessionFunctionsUsage.session_session_write_close - } - - /** - * Clean all output buffers. - * - * Can prevent errors, for example: transfer closed with 3 bytes remaining to read. - */ - private static function clean_buffers() { - if ( ob_get_level() ) { - $levels = ob_get_level(); - for ( $i = 0; $i < $levels; $i++ ) { - @ob_end_clean(); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged - } - } else { - @ob_end_clean(); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged - } - } - - /** - * Read file chunked. - * - * Reads file in chunks so big downloads are possible without changing PHP.INI - http://codeigniter.com/wiki/Download_helper_for_large_files/. - * - * @param string $file File. - * @param int $start Byte offset/position of the beginning from which to read from the file. - * @param int $length Length of the chunk to be read from the file in bytes, 0 means full file. - * @return bool Success or fail - */ - public static function readfile_chunked( $file, $start = 0, $length = 0 ) { - if ( ! defined( 'WC_CHUNK_SIZE' ) ) { - define( 'WC_CHUNK_SIZE', 1024 * 1024 ); - } - $handle = @fopen( $file, 'r' ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen - - if ( false === $handle ) { - return false; - } - - if ( ! $length ) { - $length = @filesize( $file ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged - } - - $read_length = (int) WC_CHUNK_SIZE; - - if ( $length ) { - $end = $start + $length - 1; - - @fseek( $handle, $start ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged - $p = @ftell( $handle ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged - - while ( ! @feof( $handle ) && $p <= $end ) { // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged - // Don't run past the end of file. - if ( $p + $read_length > $end ) { - $read_length = $end - $p + 1; - } - - echo @fread( $handle, $read_length ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.WP.AlternativeFunctions.file_system_read_fread - $p = @ftell( $handle ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged - - if ( ob_get_length() ) { - ob_flush(); - flush(); - } - } - } else { - while ( ! @feof( $handle ) ) { // @codingStandardsIgnoreLine. - echo @fread( $handle, $read_length ); // @codingStandardsIgnoreLine. - if ( ob_get_length() ) { - ob_flush(); - flush(); - } - } - } - - return @fclose( $handle ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fclose - } - - /** - * Filter headers for IE to fix issues over SSL. - * - * IE bug prevents download via SSL when Cache Control and Pragma no-cache headers set. - * - * @param array $headers HTTP headers. - * @return array - */ - public static function ie_nocache_headers_fix( $headers ) { - if ( is_ssl() && ! empty( $GLOBALS['is_IE'] ) ) { - $headers['Cache-Control'] = 'private'; - unset( $headers['Pragma'] ); - } - return $headers; - } - - /** - * Die with an error message if the download fails. - * - * @param string $message Error message. - * @param string $title Error title. - * @param integer $status Error status. - */ - private static function download_error( $message, $title = '', $status = 404 ) { - if ( ! strstr( $message, '' . esc_html__( 'Go to shop', 'woocommerce' ) . ''; - } - wp_die( $message, $title, array( 'response' => $status ) ); // WPCS: XSS ok. - } -} - -WC_Download_Handler::init(); diff --git a/includes/class-wc-form-handler.php b/includes/class-wc-form-handler.php deleted file mode 100644 index 374668efcf8..00000000000 --- a/includes/class-wc-form-handler.php +++ /dev/null @@ -1,1126 +0,0 @@ -ID : 0; - } else { - $user_id = absint( $_GET['id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - } - - // If the reset token is not for the current user, ignore the reset request (don't redirect). - $logged_in_user_id = get_current_user_id(); - if ( $logged_in_user_id && $logged_in_user_id !== $user_id ) { - wc_add_notice( __( 'This password reset key is for a different user account. Please log out and try again.', 'woocommerce' ), 'error' ); - return; - } - - $action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : ''; - $value = sprintf( '%d:%s', $user_id, wp_unslash( $_GET['key'] ) ); // phpcs:ignore - WC_Shortcode_My_Account::set_reset_password_cookie( $value ); - wp_safe_redirect( - add_query_arg( - array( - 'show-reset-form' => 'true', - 'action' => $action, - ), - wc_lostpassword_url() - ) - ); - exit; - } - } - - /** - * Save and and update a billing or shipping address if the - * form was submitted through the user account page. - */ - public static function save_address() { - global $wp; - - $nonce_value = wc_get_var( $_REQUEST['woocommerce-edit-address-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. - - if ( ! wp_verify_nonce( $nonce_value, 'woocommerce-edit_address' ) ) { - return; - } - - if ( empty( $_POST['action'] ) || 'edit_address' !== $_POST['action'] ) { - return; - } - - wc_nocache_headers(); - - $user_id = get_current_user_id(); - - if ( $user_id <= 0 ) { - return; - } - - $customer = new WC_Customer( $user_id ); - - if ( ! $customer ) { - return; - } - - $load_address = isset( $wp->query_vars['edit-address'] ) ? wc_edit_address_i18n( sanitize_title( $wp->query_vars['edit-address'] ), true ) : 'billing'; - - if ( ! isset( $_POST[ $load_address . '_country' ] ) ) { - return; - } - - $address = WC()->countries->get_address_fields( wc_clean( wp_unslash( $_POST[ $load_address . '_country' ] ) ), $load_address . '_' ); - - foreach ( $address as $key => $field ) { - if ( ! isset( $field['type'] ) ) { - $field['type'] = 'text'; - } - - // Get Value. - if ( 'checkbox' === $field['type'] ) { - $value = (int) isset( $_POST[ $key ] ); - } else { - $value = isset( $_POST[ $key ] ) ? wc_clean( wp_unslash( $_POST[ $key ] ) ) : ''; - } - - // Hook to allow modification of value. - $value = apply_filters( 'woocommerce_process_myaccount_field_' . $key, $value ); - - // Validation: Required fields. - if ( ! empty( $field['required'] ) && empty( $value ) ) { - /* translators: %s: Field name. */ - wc_add_notice( sprintf( __( '%s is a required field.', 'woocommerce' ), $field['label'] ), 'error', array( 'id' => $key ) ); - } - - if ( ! empty( $value ) ) { - // Validation and formatting rules. - if ( ! empty( $field['validate'] ) && is_array( $field['validate'] ) ) { - foreach ( $field['validate'] as $rule ) { - switch ( $rule ) { - case 'postcode': - $country = wc_clean( wp_unslash( $_POST[ $load_address . '_country' ] ) ); - $value = wc_format_postcode( $value, $country ); - - if ( '' !== $value && ! WC_Validation::is_postcode( $value, $country ) ) { - switch ( $country ) { - case 'IE': - $postcode_validation_notice = __( 'Please enter a valid Eircode.', 'woocommerce' ); - break; - default: - $postcode_validation_notice = __( 'Please enter a valid postcode / ZIP.', 'woocommerce' ); - } - wc_add_notice( $postcode_validation_notice, 'error' ); - } - break; - case 'phone': - if ( '' !== $value && ! WC_Validation::is_phone( $value ) ) { - /* translators: %s: Phone number. */ - wc_add_notice( sprintf( __( '%s is not a valid phone number.', 'woocommerce' ), '' . $field['label'] . '' ), 'error' ); - } - break; - case 'email': - $value = strtolower( $value ); - - if ( ! is_email( $value ) ) { - /* translators: %s: Email address. */ - wc_add_notice( sprintf( __( '%s is not a valid email address.', 'woocommerce' ), '' . $field['label'] . '' ), 'error' ); - } - break; - } - } - } - } - - try { - // Set prop in customer object. - if ( is_callable( array( $customer, "set_$key" ) ) ) { - $customer->{"set_$key"}( $value ); - } else { - $customer->update_meta_data( $key, $value ); - } - } catch ( WC_Data_Exception $e ) { - // Set notices. Ignore invalid billing email, since is already validated. - if ( 'customer_invalid_billing_email' !== $e->getErrorCode() ) { - wc_add_notice( $e->getMessage(), 'error' ); - } - } - } - - /** - * Hook: woocommerce_after_save_address_validation. - * - * Allow developers to add custom validation logic and throw an error to prevent save. - * - * @param int $user_id User ID being saved. - * @param string $load_address Type of address e.g. billing or shipping. - * @param array $address The address fields. - * @param WC_Customer $customer The customer object being saved. @since 3.6.0 - */ - do_action( 'woocommerce_after_save_address_validation', $user_id, $load_address, $address, $customer ); - - if ( 0 < wc_notice_count( 'error' ) ) { - return; - } - - $customer->save(); - - wc_add_notice( __( 'Address changed successfully.', 'woocommerce' ) ); - - do_action( 'woocommerce_customer_save_address', $user_id, $load_address ); - - wp_safe_redirect( wc_get_endpoint_url( 'edit-address', '', wc_get_page_permalink( 'myaccount' ) ) ); - exit; - } - - /** - * Save the password/account details and redirect back to the my account page. - */ - public static function save_account_details() { - $nonce_value = wc_get_var( $_REQUEST['save-account-details-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. - - if ( ! wp_verify_nonce( $nonce_value, 'save_account_details' ) ) { - return; - } - - if ( empty( $_POST['action'] ) || 'save_account_details' !== $_POST['action'] ) { - return; - } - - wc_nocache_headers(); - - $user_id = get_current_user_id(); - - if ( $user_id <= 0 ) { - return; - } - - $account_first_name = ! empty( $_POST['account_first_name'] ) ? wc_clean( wp_unslash( $_POST['account_first_name'] ) ) : ''; - $account_last_name = ! empty( $_POST['account_last_name'] ) ? wc_clean( wp_unslash( $_POST['account_last_name'] ) ) : ''; - $account_display_name = ! empty( $_POST['account_display_name'] ) ? wc_clean( wp_unslash( $_POST['account_display_name'] ) ) : ''; - $account_email = ! empty( $_POST['account_email'] ) ? wc_clean( wp_unslash( $_POST['account_email'] ) ) : ''; - $pass_cur = ! empty( $_POST['password_current'] ) ? $_POST['password_current'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash - $pass1 = ! empty( $_POST['password_1'] ) ? $_POST['password_1'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash - $pass2 = ! empty( $_POST['password_2'] ) ? $_POST['password_2'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash - $save_pass = true; - - // Current user data. - $current_user = get_user_by( 'id', $user_id ); - $current_first_name = $current_user->first_name; - $current_last_name = $current_user->last_name; - $current_email = $current_user->user_email; - - // New user data. - $user = new stdClass(); - $user->ID = $user_id; - $user->first_name = $account_first_name; - $user->last_name = $account_last_name; - $user->display_name = $account_display_name; - - // Prevent display name to be changed to email. - if ( is_email( $account_display_name ) ) { - wc_add_notice( __( 'Display name cannot be changed to email address due to privacy concern.', 'woocommerce' ), 'error' ); - } - - // Handle required fields. - $required_fields = apply_filters( - 'woocommerce_save_account_details_required_fields', - array( - 'account_first_name' => __( 'First name', 'woocommerce' ), - 'account_last_name' => __( 'Last name', 'woocommerce' ), - 'account_display_name' => __( 'Display name', 'woocommerce' ), - 'account_email' => __( 'Email address', 'woocommerce' ), - ) - ); - - foreach ( $required_fields as $field_key => $field_name ) { - if ( empty( $_POST[ $field_key ] ) ) { - /* translators: %s: Field name. */ - wc_add_notice( sprintf( __( '%s is a required field.', 'woocommerce' ), '' . esc_html( $field_name ) . '' ), 'error', array( 'id' => $field_key ) ); - } - } - - if ( $account_email ) { - $account_email = sanitize_email( $account_email ); - if ( ! is_email( $account_email ) ) { - wc_add_notice( __( 'Please provide a valid email address.', 'woocommerce' ), 'error' ); - } elseif ( email_exists( $account_email ) && $account_email !== $current_user->user_email ) { - wc_add_notice( __( 'This email address is already registered.', 'woocommerce' ), 'error' ); - } - $user->user_email = $account_email; - } - - if ( ! empty( $pass_cur ) && empty( $pass1 ) && empty( $pass2 ) ) { - wc_add_notice( __( 'Please fill out all password fields.', 'woocommerce' ), 'error' ); - $save_pass = false; - } elseif ( ! empty( $pass1 ) && empty( $pass_cur ) ) { - wc_add_notice( __( 'Please enter your current password.', 'woocommerce' ), 'error' ); - $save_pass = false; - } elseif ( ! empty( $pass1 ) && empty( $pass2 ) ) { - wc_add_notice( __( 'Please re-enter your password.', 'woocommerce' ), 'error' ); - $save_pass = false; - } elseif ( ( ! empty( $pass1 ) || ! empty( $pass2 ) ) && $pass1 !== $pass2 ) { - wc_add_notice( __( 'New passwords do not match.', 'woocommerce' ), 'error' ); - $save_pass = false; - } elseif ( ! empty( $pass1 ) && ! wp_check_password( $pass_cur, $current_user->user_pass, $current_user->ID ) ) { - wc_add_notice( __( 'Your current password is incorrect.', 'woocommerce' ), 'error' ); - $save_pass = false; - } - - if ( $pass1 && $save_pass ) { - $user->user_pass = $pass1; - } - - // Allow plugins to return their own errors. - $errors = new WP_Error(); - do_action_ref_array( 'woocommerce_save_account_details_errors', array( &$errors, &$user ) ); - - if ( $errors->get_error_messages() ) { - foreach ( $errors->get_error_messages() as $error ) { - wc_add_notice( $error, 'error' ); - } - } - - if ( wc_notice_count( 'error' ) === 0 ) { - wp_update_user( $user ); - - // Update customer object to keep data in sync. - $customer = new WC_Customer( $user->ID ); - - if ( $customer ) { - // Keep billing data in sync if data changed. - if ( is_email( $user->user_email ) && $current_email !== $user->user_email ) { - $customer->set_billing_email( $user->user_email ); - } - - if ( $current_first_name !== $user->first_name ) { - $customer->set_billing_first_name( $user->first_name ); - } - - if ( $current_last_name !== $user->last_name ) { - $customer->set_billing_last_name( $user->last_name ); - } - - $customer->save(); - } - - wc_add_notice( __( 'Account details changed successfully.', 'woocommerce' ) ); - - do_action( 'woocommerce_save_account_details', $user->ID ); - - wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) ); - exit; - } - } - - /** - * Process the checkout form. - */ - public static function checkout_action() { - if ( isset( $_POST['woocommerce_checkout_place_order'] ) || isset( $_POST['woocommerce_checkout_update_totals'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing - wc_nocache_headers(); - - if ( WC()->cart->is_empty() ) { - wp_safe_redirect( wc_get_cart_url() ); - exit; - } - - wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true ); - - WC()->checkout()->process_checkout(); - } - } - - /** - * Process the pay form. - * - * @throws Exception On payment error. - */ - public static function pay_action() { - global $wp; - - if ( isset( $_POST['woocommerce_pay'], $_GET['key'] ) ) { - wc_nocache_headers(); - - $nonce_value = wc_get_var( $_REQUEST['woocommerce-pay-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. - - if ( ! wp_verify_nonce( $nonce_value, 'woocommerce-pay' ) ) { - return; - } - - ob_start(); - - // Pay for existing order. - $order_key = wp_unslash( $_GET['key'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - $order_id = absint( $wp->query_vars['order-pay'] ); - $order = wc_get_order( $order_id ); - - if ( $order_id === $order->get_id() && hash_equals( $order->get_order_key(), $order_key ) && $order->needs_payment() ) { - - do_action( 'woocommerce_before_pay_action', $order ); - - WC()->customer->set_props( - array( - 'billing_country' => $order->get_billing_country() ? $order->get_billing_country() : null, - 'billing_state' => $order->get_billing_state() ? $order->get_billing_state() : null, - 'billing_postcode' => $order->get_billing_postcode() ? $order->get_billing_postcode() : null, - 'billing_city' => $order->get_billing_city() ? $order->get_billing_city() : null, - ) - ); - WC()->customer->save(); - - if ( ! empty( $_POST['terms-field'] ) && empty( $_POST['terms'] ) ) { - wc_add_notice( __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ), 'error' ); - return; - } - - // Update payment method. - if ( $order->needs_payment() ) { - try { - $payment_method_id = isset( $_POST['payment_method'] ) ? wc_clean( wp_unslash( $_POST['payment_method'] ) ) : false; - - if ( ! $payment_method_id ) { - throw new Exception( __( 'Invalid payment method.', 'woocommerce' ) ); - } - - $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); - $payment_method = isset( $available_gateways[ $payment_method_id ] ) ? $available_gateways[ $payment_method_id ] : false; - - if ( ! $payment_method ) { - throw new Exception( __( 'Invalid payment method.', 'woocommerce' ) ); - } - - $order->set_payment_method( $payment_method ); - $order->save(); - - $payment_method->validate_fields(); - - if ( 0 === wc_notice_count( 'error' ) ) { - - $result = $payment_method->process_payment( $order_id ); - - // Redirect to success/confirmation/payment page. - if ( isset( $result['result'] ) && 'success' === $result['result'] ) { - $result = apply_filters( 'woocommerce_payment_successful_result', $result, $order_id ); - - wp_redirect( $result['redirect'] ); //phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect - exit; - } - } - } catch ( Exception $e ) { - wc_add_notice( $e->getMessage(), 'error' ); - } - } else { - // No payment was required for order. - $order->payment_complete(); - wp_safe_redirect( $order->get_checkout_order_received_url() ); - exit; - } - - do_action( 'woocommerce_after_pay_action', $order ); - - } - } - } - - /** - * Process the add payment method form. - */ - public static function add_payment_method_action() { - if ( isset( $_POST['woocommerce_add_payment_method'], $_POST['payment_method'] ) ) { - wc_nocache_headers(); - - $nonce_value = wc_get_var( $_REQUEST['woocommerce-add-payment-method-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. - - if ( ! wp_verify_nonce( $nonce_value, 'woocommerce-add-payment-method' ) ) { - return; - } - - if ( ! apply_filters( 'woocommerce_add_payment_method_form_is_valid', true ) ) { - return; - } - - // Test rate limit. - $current_user_id = get_current_user_id(); - $rate_limit_id = 'add_payment_method_' . $current_user_id; - $delay = (int) apply_filters( 'woocommerce_payment_gateway_add_payment_method_delay', 20 ); - - if ( WC_Rate_Limiter::retried_too_soon( $rate_limit_id ) ) { - wc_add_notice( - sprintf( - /* translators: %d number of seconds */ - _n( - 'You cannot add a new payment method so soon after the previous one. Please wait for %d second.', - 'You cannot add a new payment method so soon after the previous one. Please wait for %d seconds.', - $delay, - 'woocommerce' - ), - $delay - ), - 'error' - ); - return; - } - - WC_Rate_Limiter::set_rate_limit( $rate_limit_id, $delay ); - - ob_start(); - - $payment_method_id = wc_clean( wp_unslash( $_POST['payment_method'] ) ); - $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); - - if ( isset( $available_gateways[ $payment_method_id ] ) ) { - $gateway = $available_gateways[ $payment_method_id ]; - - if ( ! $gateway->supports( 'add_payment_method' ) && ! $gateway->supports( 'tokenization' ) ) { - wc_add_notice( __( 'Invalid payment gateway.', 'woocommerce' ), 'error' ); - return; - } - - $gateway->validate_fields(); - - if ( wc_notice_count( 'error' ) > 0 ) { - return; - } - - $result = $gateway->add_payment_method(); - - if ( 'success' === $result['result'] ) { - wc_add_notice( __( 'Payment method successfully added.', 'woocommerce' ) ); - } - - if ( 'failure' === $result['result'] ) { - wc_add_notice( __( 'Unable to add payment method to your account.', 'woocommerce' ), 'error' ); - } - - if ( ! empty( $result['redirect'] ) ) { - wp_redirect( $result['redirect'] ); //phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect - exit(); - } - } - } - } - - /** - * Process the delete payment method form. - */ - public static function delete_payment_method_action() { - global $wp; - - if ( isset( $wp->query_vars['delete-payment-method'] ) ) { - wc_nocache_headers(); - - $token_id = absint( $wp->query_vars['delete-payment-method'] ); - $token = WC_Payment_Tokens::get( $token_id ); - - if ( is_null( $token ) || get_current_user_id() !== $token->get_user_id() || ! isset( $_REQUEST['_wpnonce'] ) || false === wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'delete-payment-method-' . $token_id ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - wc_add_notice( __( 'Invalid payment method.', 'woocommerce' ), 'error' ); - } else { - WC_Payment_Tokens::delete( $token_id ); - wc_add_notice( __( 'Payment method deleted.', 'woocommerce' ) ); - } - - wp_safe_redirect( wc_get_account_endpoint_url( 'payment-methods' ) ); - exit(); - } - - } - - /** - * Process the delete payment method form. - */ - public static function set_default_payment_method_action() { - global $wp; - - if ( isset( $wp->query_vars['set-default-payment-method'] ) ) { - wc_nocache_headers(); - - $token_id = absint( $wp->query_vars['set-default-payment-method'] ); - $token = WC_Payment_Tokens::get( $token_id ); - - if ( is_null( $token ) || get_current_user_id() !== $token->get_user_id() || ! isset( $_REQUEST['_wpnonce'] ) || false === wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'set-default-payment-method-' . $token_id ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - wc_add_notice( __( 'Invalid payment method.', 'woocommerce' ), 'error' ); - } else { - WC_Payment_Tokens::set_users_default( $token->get_user_id(), intval( $token_id ) ); - wc_add_notice( __( 'This payment method was successfully set as your default.', 'woocommerce' ) ); - } - - wp_safe_redirect( wc_get_account_endpoint_url( 'payment-methods' ) ); - exit(); - } - - } - - /** - * Remove from cart/update. - */ - public static function update_cart_action() { - if ( ! ( isset( $_REQUEST['apply_coupon'] ) || isset( $_REQUEST['remove_coupon'] ) || isset( $_REQUEST['remove_item'] ) || isset( $_REQUEST['undo_item'] ) || isset( $_REQUEST['update_cart'] ) || isset( $_REQUEST['proceed'] ) ) ) { - return; - } - - wc_nocache_headers(); - - $nonce_value = wc_get_var( $_REQUEST['woocommerce-cart-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. - - if ( ! empty( $_POST['apply_coupon'] ) && ! empty( $_POST['coupon_code'] ) ) { - WC()->cart->add_discount( wc_format_coupon_code( wp_unslash( $_POST['coupon_code'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - - } elseif ( isset( $_GET['remove_coupon'] ) ) { - WC()->cart->remove_coupon( wc_format_coupon_code( urldecode( wp_unslash( $_GET['remove_coupon'] ) ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - - } elseif ( ! empty( $_GET['remove_item'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-cart' ) ) { - $cart_item_key = sanitize_text_field( wp_unslash( $_GET['remove_item'] ) ); - $cart_item = WC()->cart->get_cart_item( $cart_item_key ); - - if ( $cart_item ) { - WC()->cart->remove_cart_item( $cart_item_key ); - - $product = wc_get_product( $cart_item['product_id'] ); - - /* translators: %s: Item name. */ - $item_removed_title = apply_filters( 'woocommerce_cart_item_removed_title', $product ? sprintf( _x( '“%s”', 'Item name in quotes', 'woocommerce' ), $product->get_name() ) : __( 'Item', 'woocommerce' ), $cart_item ); - - // Don't show undo link if removed item is out of stock. - if ( $product && $product->is_in_stock() && $product->has_enough_stock( $cart_item['quantity'] ) ) { - /* Translators: %s Product title. */ - $removed_notice = sprintf( __( '%s removed.', 'woocommerce' ), $item_removed_title ); - $removed_notice .= ' ' . __( 'Undo?', 'woocommerce' ) . ''; - } else { - /* Translators: %s Product title. */ - $removed_notice = sprintf( __( '%s removed.', 'woocommerce' ), $item_removed_title ); - } - - wc_add_notice( $removed_notice, apply_filters( 'woocommerce_cart_item_removed_notice_type', 'success' ) ); - } - - $referer = wp_get_referer() ? remove_query_arg( array( 'remove_item', 'add-to-cart', 'added-to-cart', 'order_again', '_wpnonce' ), add_query_arg( 'removed_item', '1', wp_get_referer() ) ) : wc_get_cart_url(); - wp_safe_redirect( $referer ); - exit; - - } elseif ( ! empty( $_GET['undo_item'] ) && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-cart' ) ) { - - // Undo Cart Item. - $cart_item_key = sanitize_text_field( wp_unslash( $_GET['undo_item'] ) ); - - WC()->cart->restore_cart_item( $cart_item_key ); - - $referer = wp_get_referer() ? remove_query_arg( array( 'undo_item', '_wpnonce' ), wp_get_referer() ) : wc_get_cart_url(); - wp_safe_redirect( $referer ); - exit; - - } - - // Update Cart - checks apply_coupon too because they are in the same form. - if ( ( ! empty( $_POST['apply_coupon'] ) || ! empty( $_POST['update_cart'] ) || ! empty( $_POST['proceed'] ) ) && wp_verify_nonce( $nonce_value, 'woocommerce-cart' ) ) { - - $cart_updated = false; - $cart_totals = isset( $_POST['cart'] ) ? wp_unslash( $_POST['cart'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - - if ( ! WC()->cart->is_empty() && is_array( $cart_totals ) ) { - foreach ( WC()->cart->get_cart() as $cart_item_key => $values ) { - - $_product = $values['data']; - - // Skip product if no updated quantity was posted. - if ( ! isset( $cart_totals[ $cart_item_key ] ) || ! isset( $cart_totals[ $cart_item_key ]['qty'] ) ) { - continue; - } - - // Sanitize. - $quantity = apply_filters( 'woocommerce_stock_amount_cart_item', wc_stock_amount( preg_replace( '/[^0-9\.]/', '', $cart_totals[ $cart_item_key ]['qty'] ) ), $cart_item_key ); - - if ( '' === $quantity || $quantity === $values['quantity'] ) { - continue; - } - - // Update cart validation. - $passed_validation = apply_filters( 'woocommerce_update_cart_validation', true, $cart_item_key, $values, $quantity ); - - // is_sold_individually. - if ( $_product->is_sold_individually() && $quantity > 1 ) { - /* Translators: %s Product title. */ - wc_add_notice( sprintf( __( 'You can only have 1 %s in your cart.', 'woocommerce' ), $_product->get_name() ), 'error' ); - $passed_validation = false; - } - - if ( $passed_validation ) { - WC()->cart->set_quantity( $cart_item_key, $quantity, false ); - $cart_updated = true; - } - } - } - - // Trigger action - let 3rd parties update the cart if they need to and update the $cart_updated variable. - $cart_updated = apply_filters( 'woocommerce_update_cart_action_cart_updated', $cart_updated ); - - if ( $cart_updated ) { - WC()->cart->calculate_totals(); - } - - if ( ! empty( $_POST['proceed'] ) ) { - wp_safe_redirect( wc_get_checkout_url() ); - exit; - } elseif ( $cart_updated ) { - wc_add_notice( __( 'Cart updated.', 'woocommerce' ), apply_filters( 'woocommerce_cart_updated_notice_type', 'success' ) ); - $referer = remove_query_arg( array( 'remove_coupon', 'add-to-cart' ), ( wp_get_referer() ? wp_get_referer() : wc_get_cart_url() ) ); - wp_safe_redirect( $referer ); - exit; - } - } - } - - /** - * Place a previous order again. - * - * @deprecated 3.5.0 Logic moved to cart session handling. - */ - public static function order_again() { - wc_deprecated_function( 'WC_Form_Handler::order_again', '3.5', 'This method should not be called manually.' ); - } - - /** - * Cancel a pending order. - */ - public static function cancel_order() { - if ( - isset( $_GET['cancel_order'] ) && - isset( $_GET['order'] ) && - isset( $_GET['order_id'] ) && - ( isset( $_GET['_wpnonce'] ) && wp_verify_nonce( wp_unslash( $_GET['_wpnonce'] ), 'woocommerce-cancel_order' ) ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - ) { - wc_nocache_headers(); - - $order_key = wp_unslash( $_GET['order'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - $order_id = absint( $_GET['order_id'] ); - $order = wc_get_order( $order_id ); - $user_can_cancel = current_user_can( 'cancel_order', $order_id ); - $order_can_cancel = $order->has_status( apply_filters( 'woocommerce_valid_order_statuses_for_cancel', array( 'pending', 'failed' ), $order ) ); - $redirect = isset( $_GET['redirect'] ) ? wp_unslash( $_GET['redirect'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - - if ( $user_can_cancel && $order_can_cancel && $order->get_id() === $order_id && hash_equals( $order->get_order_key(), $order_key ) ) { - - // Cancel the order + restore stock. - WC()->session->set( 'order_awaiting_payment', false ); - $order->update_status( 'cancelled', __( 'Order cancelled by customer.', 'woocommerce' ) ); - - wc_add_notice( apply_filters( 'woocommerce_order_cancelled_notice', __( 'Your order was cancelled.', 'woocommerce' ) ), apply_filters( 'woocommerce_order_cancelled_notice_type', 'notice' ) ); - - do_action( 'woocommerce_cancelled_order', $order->get_id() ); - - } elseif ( $user_can_cancel && ! $order_can_cancel ) { - wc_add_notice( __( 'Your order can no longer be cancelled. Please contact us if you need assistance.', 'woocommerce' ), 'error' ); - } else { - wc_add_notice( __( 'Invalid order.', 'woocommerce' ), 'error' ); - } - - if ( $redirect ) { - wp_safe_redirect( $redirect ); - exit; - } - } - } - - /** - * Add to cart action. - * - * Checks for a valid request, does validation (via hooks) and then redirects if valid. - * - * @param bool $url (default: false) URL to redirect to. - */ - public static function add_to_cart_action( $url = false ) { - if ( ! isset( $_REQUEST['add-to-cart'] ) || ! is_numeric( wp_unslash( $_REQUEST['add-to-cart'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - return; - } - - wc_nocache_headers(); - - $product_id = apply_filters( 'woocommerce_add_to_cart_product_id', absint( wp_unslash( $_REQUEST['add-to-cart'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $was_added_to_cart = false; - $adding_to_cart = wc_get_product( $product_id ); - - if ( ! $adding_to_cart ) { - return; - } - - $add_to_cart_handler = apply_filters( 'woocommerce_add_to_cart_handler', $adding_to_cart->get_type(), $adding_to_cart ); - - if ( 'variable' === $add_to_cart_handler || 'variation' === $add_to_cart_handler ) { - $was_added_to_cart = self::add_to_cart_handler_variable( $product_id ); - } elseif ( 'grouped' === $add_to_cart_handler ) { - $was_added_to_cart = self::add_to_cart_handler_grouped( $product_id ); - } elseif ( has_action( 'woocommerce_add_to_cart_handler_' . $add_to_cart_handler ) ) { - do_action( 'woocommerce_add_to_cart_handler_' . $add_to_cart_handler, $url ); // Custom handler. - } else { - $was_added_to_cart = self::add_to_cart_handler_simple( $product_id ); - } - - // If we added the product to the cart we can now optionally do a redirect. - if ( $was_added_to_cart && 0 === wc_notice_count( 'error' ) ) { - $url = apply_filters( 'woocommerce_add_to_cart_redirect', $url, $adding_to_cart ); - - if ( $url ) { - wp_safe_redirect( $url ); - exit; - } elseif ( 'yes' === get_option( 'woocommerce_cart_redirect_after_add' ) ) { - wp_safe_redirect( wc_get_cart_url() ); - exit; - } - } - } - - /** - * Handle adding simple products to the cart. - * - * @since 2.4.6 Split from add_to_cart_action. - * @param int $product_id Product ID to add to the cart. - * @return bool success or not - */ - private static function add_to_cart_handler_simple( $product_id ) { - $quantity = empty( $_REQUEST['quantity'] ) ? 1 : wc_stock_amount( wp_unslash( $_REQUEST['quantity'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity ); - - if ( $passed_validation && false !== WC()->cart->add_to_cart( $product_id, $quantity ) ) { - wc_add_to_cart_message( array( $product_id => $quantity ), true ); - return true; - } - return false; - } - - /** - * Handle adding grouped products to the cart. - * - * @since 2.4.6 Split from add_to_cart_action. - * @param int $product_id Product ID to add to the cart. - * @return bool success or not - */ - private static function add_to_cart_handler_grouped( $product_id ) { - $was_added_to_cart = false; - $added_to_cart = array(); - $items = isset( $_REQUEST['quantity'] ) && is_array( $_REQUEST['quantity'] ) ? wp_unslash( $_REQUEST['quantity'] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - - if ( ! empty( $items ) ) { - $quantity_set = false; - - foreach ( $items as $item => $quantity ) { - $quantity = wc_stock_amount( $quantity ); - if ( $quantity <= 0 ) { - continue; - } - $quantity_set = true; - - // Add to cart validation. - $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $item, $quantity ); - - // Suppress total recalculation until finished. - remove_action( 'woocommerce_add_to_cart', array( WC()->cart, 'calculate_totals' ), 20, 0 ); - - if ( $passed_validation && false !== WC()->cart->add_to_cart( $item, $quantity ) ) { - $was_added_to_cart = true; - $added_to_cart[ $item ] = $quantity; - } - - add_action( 'woocommerce_add_to_cart', array( WC()->cart, 'calculate_totals' ), 20, 0 ); - } - - if ( ! $was_added_to_cart && ! $quantity_set ) { - wc_add_notice( __( 'Please choose the quantity of items you wish to add to your cart…', 'woocommerce' ), 'error' ); - } elseif ( $was_added_to_cart ) { - wc_add_to_cart_message( $added_to_cart ); - WC()->cart->calculate_totals(); - return true; - } - } elseif ( $product_id ) { - /* Link on product archives */ - wc_add_notice( __( 'Please choose a product to add to your cart…', 'woocommerce' ), 'error' ); - } - return false; - } - - /** - * Handle adding variable products to the cart. - * - * @since 2.4.6 Split from add_to_cart_action. - * @throws Exception If add to cart fails. - * @param int $product_id Product ID to add to the cart. - * @return bool success or not - */ - private static function add_to_cart_handler_variable( $product_id ) { - $variation_id = empty( $_REQUEST['variation_id'] ) ? '' : absint( wp_unslash( $_REQUEST['variation_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $quantity = empty( $_REQUEST['quantity'] ) ? 1 : wc_stock_amount( wp_unslash( $_REQUEST['quantity'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $variations = array(); - - $product = wc_get_product( $product_id ); - - foreach ( $_REQUEST as $key => $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - if ( 'attribute_' !== substr( $key, 0, 10 ) ) { - continue; - } - - $variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value ); - } - - $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity, $variation_id, $variations ); - - if ( ! $passed_validation ) { - return false; - } - - // Prevent parent variable product from being added to cart. - if ( empty( $variation_id ) && $product && $product->is_type( 'variable' ) ) { - /* translators: 1: product link, 2: product name */ - wc_add_notice( sprintf( __( 'Please choose product options by visiting %2$s.', 'woocommerce' ), esc_url( get_permalink( $product_id ) ), esc_html( $product->get_name() ) ), 'error' ); - - return false; - } - - if ( false !== WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $variations ) ) { - wc_add_to_cart_message( array( $product_id => $quantity ), true ); - return true; - } - - return false; - } - - /** - * Process the login form. - * - * @throws Exception On login error. - */ - public static function process_login() { - // The global form-login.php template used `_wpnonce` in template versions < 3.3.0. - $nonce_value = wc_get_var( $_REQUEST['woocommerce-login-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. - - if ( isset( $_POST['login'], $_POST['username'], $_POST['password'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-login' ) ) { - - try { - $creds = array( - 'user_login' => trim( wp_unslash( $_POST['username'] ) ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - 'user_password' => $_POST['password'], // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash - 'remember' => isset( $_POST['rememberme'] ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - ); - - $validation_error = new WP_Error(); - $validation_error = apply_filters( 'woocommerce_process_login_errors', $validation_error, $creds['user_login'], $creds['user_password'] ); - - if ( $validation_error->get_error_code() ) { - throw new Exception( '' . __( 'Error:', 'woocommerce' ) . ' ' . $validation_error->get_error_message() ); - } - - if ( empty( $creds['user_login'] ) ) { - throw new Exception( '' . __( 'Error:', 'woocommerce' ) . ' ' . __( 'Username is required.', 'woocommerce' ) ); - } - - // On multisite, ensure user exists on current site, if not add them before allowing login. - if ( is_multisite() ) { - $user_data = get_user_by( is_email( $creds['user_login'] ) ? 'email' : 'login', $creds['user_login'] ); - - if ( $user_data && ! is_user_member_of_blog( $user_data->ID, get_current_blog_id() ) ) { - add_user_to_blog( get_current_blog_id(), $user_data->ID, 'customer' ); - } - } - - // Perform the login. - $user = wp_signon( apply_filters( 'woocommerce_login_credentials', $creds ), is_ssl() ); - - if ( is_wp_error( $user ) ) { - throw new Exception( $user->get_error_message() ); - } else { - - if ( ! empty( $_POST['redirect'] ) ) { - $redirect = wp_unslash( $_POST['redirect'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - } elseif ( wc_get_raw_referer() ) { - $redirect = wc_get_raw_referer(); - } else { - $redirect = wc_get_page_permalink( 'myaccount' ); - } - - wp_redirect( wp_validate_redirect( apply_filters( 'woocommerce_login_redirect', remove_query_arg( 'wc_error', $redirect ), $user ), wc_get_page_permalink( 'myaccount' ) ) ); // phpcs:ignore - exit; - } - } catch ( Exception $e ) { - wc_add_notice( apply_filters( 'login_errors', $e->getMessage() ), 'error' ); - do_action( 'woocommerce_login_failed' ); - } - } - } - - /** - * Handle lost password form. - */ - public static function process_lost_password() { - if ( isset( $_POST['wc_reset_password'], $_POST['user_login'] ) ) { - $nonce_value = wc_get_var( $_REQUEST['woocommerce-lost-password-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. - - if ( ! wp_verify_nonce( $nonce_value, 'lost_password' ) ) { - return; - } - - $success = WC_Shortcode_My_Account::retrieve_password(); - - // If successful, redirect to my account with query arg set. - if ( $success ) { - wp_safe_redirect( add_query_arg( 'reset-link-sent', 'true', wc_get_account_endpoint_url( 'lost-password' ) ) ); - exit; - } - } - } - - /** - * Handle reset password form. - */ - public static function process_reset_password() { - $nonce_value = wc_get_var( $_REQUEST['woocommerce-reset-password-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. - - if ( ! wp_verify_nonce( $nonce_value, 'reset_password' ) ) { - return; - } - - $posted_fields = array( 'wc_reset_password', 'password_1', 'password_2', 'reset_key', 'reset_login' ); - - foreach ( $posted_fields as $field ) { - if ( ! isset( $_POST[ $field ] ) ) { - return; - } - - if ( in_array( $field, array( 'password_1', 'password_2' ), true ) ) { - // Don't unslash password fields - // @see https://github.com/woocommerce/woocommerce/issues/23922. - $posted_fields[ $field ] = $_POST[ $field ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash - } else { - $posted_fields[ $field ] = wp_unslash( $_POST[ $field ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - } - } - - $user = WC_Shortcode_My_Account::check_password_reset_key( $posted_fields['reset_key'], $posted_fields['reset_login'] ); - - if ( $user instanceof WP_User ) { - if ( empty( $posted_fields['password_1'] ) ) { - wc_add_notice( __( 'Please enter your password.', 'woocommerce' ), 'error' ); - } - - if ( $posted_fields['password_1'] !== $posted_fields['password_2'] ) { - wc_add_notice( __( 'Passwords do not match.', 'woocommerce' ), 'error' ); - } - - $errors = new WP_Error(); - - do_action( 'validate_password_reset', $errors, $user ); - - wc_add_wp_error_notices( $errors ); - - if ( 0 === wc_notice_count( 'error' ) ) { - WC_Shortcode_My_Account::reset_password( $user, $posted_fields['password_1'] ); - - do_action( 'woocommerce_customer_reset_password', $user ); - - wp_safe_redirect( add_query_arg( 'password-reset', 'true', wc_get_page_permalink( 'myaccount' ) ) ); - exit; - } - } - } - - /** - * Process the registration form. - * - * @throws Exception On registration error. - */ - public static function process_registration() { - $nonce_value = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - $nonce_value = isset( $_POST['woocommerce-register-nonce'] ) ? wp_unslash( $_POST['woocommerce-register-nonce'] ) : $nonce_value; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - - if ( isset( $_POST['register'], $_POST['email'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-register' ) ) { - $username = 'no' === get_option( 'woocommerce_registration_generate_username' ) && isset( $_POST['username'] ) ? wp_unslash( $_POST['username'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - $password = 'no' === get_option( 'woocommerce_registration_generate_password' ) && isset( $_POST['password'] ) ? $_POST['password'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash - $email = wp_unslash( $_POST['email'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - - try { - $validation_error = new WP_Error(); - $validation_error = apply_filters( 'woocommerce_process_registration_errors', $validation_error, $username, $password, $email ); - $validation_errors = $validation_error->get_error_messages(); - - if ( 1 === count( $validation_errors ) ) { - throw new Exception( $validation_error->get_error_message() ); - } elseif ( $validation_errors ) { - foreach ( $validation_errors as $message ) { - wc_add_notice( '' . __( 'Error:', 'woocommerce' ) . ' ' . $message, 'error' ); - } - throw new Exception(); - } - - $new_customer = wc_create_new_customer( sanitize_email( $email ), wc_clean( $username ), $password ); - - if ( is_wp_error( $new_customer ) ) { - throw new Exception( $new_customer->get_error_message() ); - } - - if ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) ) { - wc_add_notice( __( 'Your account was created successfully and a password has been sent to your email address.', 'woocommerce' ) ); - } else { - wc_add_notice( __( 'Your account was created successfully. Your login details have been sent to your email address.', 'woocommerce' ) ); - } - - // Only redirect after a forced login - otherwise output a success notice. - if ( apply_filters( 'woocommerce_registration_auth_new_customer', true, $new_customer ) ) { - wc_set_customer_auth_cookie( $new_customer ); - - if ( ! empty( $_POST['redirect'] ) ) { - $redirect = wp_sanitize_redirect( wp_unslash( $_POST['redirect'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - } elseif ( wc_get_raw_referer() ) { - $redirect = wc_get_raw_referer(); - } else { - $redirect = wc_get_page_permalink( 'myaccount' ); - } - - wp_redirect( wp_validate_redirect( apply_filters( 'woocommerce_registration_redirect', $redirect ), wc_get_page_permalink( 'myaccount' ) ) ); //phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect - exit; - } - } catch ( Exception $e ) { - if ( $e->getMessage() ) { - wc_add_notice( '' . __( 'Error:', 'woocommerce' ) . ' ' . $e->getMessage(), 'error' ); - } - } - } - } -} - -WC_Form_Handler::init(); diff --git a/includes/class-wc-frontend-scripts.php b/includes/class-wc-frontend-scripts.php deleted file mode 100644 index d9b1f7d9a61..00000000000 --- a/includes/class-wc-frontend-scripts.php +++ /dev/null @@ -1,618 +0,0 @@ - array( - 'src' => self::get_asset_url( 'assets/css/woocommerce-layout.css' ), - 'deps' => '', - 'version' => $version, - 'media' => 'all', - 'has_rtl' => true, - ), - 'woocommerce-smallscreen' => array( - 'src' => self::get_asset_url( 'assets/css/woocommerce-smallscreen.css' ), - 'deps' => 'woocommerce-layout', - 'version' => $version, - 'media' => 'only screen and (max-width: ' . apply_filters( 'woocommerce_style_smallscreen_breakpoint', '768px' ) . ')', - 'has_rtl' => true, - ), - 'woocommerce-general' => array( - 'src' => self::get_asset_url( 'assets/css/woocommerce.css' ), - 'deps' => '', - 'version' => $version, - 'media' => 'all', - 'has_rtl' => true, - ), - ) - ); - } - - /** - * Return asset URL. - * - * @param string $path Assets path. - * @return string - */ - private static function get_asset_url( $path ) { - return apply_filters( 'woocommerce_get_asset_url', plugins_url( $path, WC_PLUGIN_FILE ), $path ); - } - - /** - * Register a script for use. - * - * @uses wp_register_script() - * @param string $handle Name of the script. Should be unique. - * @param string $path Full URL of the script, or path of the script relative to the WordPress root directory. - * @param string[] $deps An array of registered script handles this script depends on. - * @param string $version String specifying script version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added. - * @param boolean $in_footer Whether to enqueue the script before instead of in the . Default 'false'. - */ - private static function register_script( $handle, $path, $deps = array( 'jquery' ), $version = WC_VERSION, $in_footer = true ) { - self::$scripts[] = $handle; - wp_register_script( $handle, $path, $deps, $version, $in_footer ); - } - - /** - * Register and enqueue a script for use. - * - * @uses wp_enqueue_script() - * @param string $handle Name of the script. Should be unique. - * @param string $path Full URL of the script, or path of the script relative to the WordPress root directory. - * @param string[] $deps An array of registered script handles this script depends on. - * @param string $version String specifying script version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added. - * @param boolean $in_footer Whether to enqueue the script before instead of in the . Default 'false'. - */ - private static function enqueue_script( $handle, $path = '', $deps = array( 'jquery' ), $version = WC_VERSION, $in_footer = true ) { - if ( ! in_array( $handle, self::$scripts, true ) && $path ) { - self::register_script( $handle, $path, $deps, $version, $in_footer ); - } - wp_enqueue_script( $handle ); - } - - /** - * Register a style for use. - * - * @uses wp_register_style() - * @param string $handle Name of the stylesheet. Should be unique. - * @param string $path Full URL of the stylesheet, or path of the stylesheet relative to the WordPress root directory. - * @param string[] $deps An array of registered stylesheet handles this stylesheet depends on. - * @param string $version String specifying stylesheet version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added. - * @param string $media The media for which this stylesheet has been defined. Accepts media types like 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'. - * @param boolean $has_rtl If has RTL version to load too. - */ - private static function register_style( $handle, $path, $deps = array(), $version = WC_VERSION, $media = 'all', $has_rtl = false ) { - self::$styles[] = $handle; - wp_register_style( $handle, $path, $deps, $version, $media ); - - if ( $has_rtl ) { - wp_style_add_data( $handle, 'rtl', 'replace' ); - } - } - - /** - * Register and enqueue a styles for use. - * - * @uses wp_enqueue_style() - * @param string $handle Name of the stylesheet. Should be unique. - * @param string $path Full URL of the stylesheet, or path of the stylesheet relative to the WordPress root directory. - * @param string[] $deps An array of registered stylesheet handles this stylesheet depends on. - * @param string $version String specifying stylesheet version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added. - * @param string $media The media for which this stylesheet has been defined. Accepts media types like 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'. - * @param boolean $has_rtl If has RTL version to load too. - */ - private static function enqueue_style( $handle, $path = '', $deps = array(), $version = WC_VERSION, $media = 'all', $has_rtl = false ) { - if ( ! in_array( $handle, self::$styles, true ) && $path ) { - self::register_style( $handle, $path, $deps, $version, $media, $has_rtl ); - } - wp_enqueue_style( $handle ); - } - - /** - * Register all WC scripts. - */ - private static function register_scripts() { - $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; - $version = Constants::get_constant( 'WC_VERSION' ); - - $register_scripts = array( - 'flexslider' => array( - 'src' => self::get_asset_url( 'assets/js/flexslider/jquery.flexslider' . $suffix . '.js' ), - 'deps' => array( 'jquery' ), - 'version' => '2.7.2', - ), - 'js-cookie' => array( - 'src' => self::get_asset_url( 'assets/js/js-cookie/js.cookie' . $suffix . '.js' ), - 'deps' => array(), - 'version' => '2.1.4', - ), - 'jquery-blockui' => array( - 'src' => self::get_asset_url( 'assets/js/jquery-blockui/jquery.blockUI' . $suffix . '.js' ), - 'deps' => array( 'jquery' ), - 'version' => '2.70', - ), - 'jquery-cookie' => array( // deprecated. - 'src' => self::get_asset_url( 'assets/js/jquery-cookie/jquery.cookie' . $suffix . '.js' ), - 'deps' => array( 'jquery' ), - 'version' => '1.4.1', - ), - 'jquery-payment' => array( - 'src' => self::get_asset_url( 'assets/js/jquery-payment/jquery.payment' . $suffix . '.js' ), - 'deps' => array( 'jquery' ), - 'version' => '3.0.0', - ), - 'photoswipe' => array( - 'src' => self::get_asset_url( 'assets/js/photoswipe/photoswipe' . $suffix . '.js' ), - 'deps' => array(), - 'version' => '4.1.1', - ), - 'photoswipe-ui-default' => array( - 'src' => self::get_asset_url( 'assets/js/photoswipe/photoswipe-ui-default' . $suffix . '.js' ), - 'deps' => array( 'photoswipe' ), - 'version' => '4.1.1', - ), - 'prettyPhoto' => array( // deprecated. - 'src' => self::get_asset_url( 'assets/js/prettyPhoto/jquery.prettyPhoto' . $suffix . '.js' ), - 'deps' => array( 'jquery' ), - 'version' => '3.1.6', - ), - 'prettyPhoto-init' => array( // deprecated. - 'src' => self::get_asset_url( 'assets/js/prettyPhoto/jquery.prettyPhoto.init' . $suffix . '.js' ), - 'deps' => array( 'jquery', 'prettyPhoto' ), - 'version' => $version, - ), - 'select2' => array( - 'src' => self::get_asset_url( 'assets/js/select2/select2.full' . $suffix . '.js' ), - 'deps' => array( 'jquery' ), - 'version' => '4.0.3', - ), - 'selectWoo' => array( - 'src' => self::get_asset_url( 'assets/js/selectWoo/selectWoo.full' . $suffix . '.js' ), - 'deps' => array( 'jquery' ), - 'version' => '1.0.6', - ), - 'wc-address-i18n' => array( - 'src' => self::get_asset_url( 'assets/js/frontend/address-i18n' . $suffix . '.js' ), - 'deps' => array( 'jquery', 'wc-country-select' ), - 'version' => $version, - ), - 'wc-add-payment-method' => array( - 'src' => self::get_asset_url( 'assets/js/frontend/add-payment-method' . $suffix . '.js' ), - 'deps' => array( 'jquery', 'woocommerce' ), - 'version' => $version, - ), - 'wc-cart' => array( - 'src' => self::get_asset_url( 'assets/js/frontend/cart' . $suffix . '.js' ), - 'deps' => array( 'jquery', 'woocommerce', 'wc-country-select', 'wc-address-i18n' ), - 'version' => $version, - ), - 'wc-cart-fragments' => array( - 'src' => self::get_asset_url( 'assets/js/frontend/cart-fragments' . $suffix . '.js' ), - 'deps' => array( 'jquery', 'js-cookie' ), - 'version' => $version, - ), - 'wc-checkout' => array( - 'src' => self::get_asset_url( 'assets/js/frontend/checkout' . $suffix . '.js' ), - 'deps' => array( 'jquery', 'woocommerce', 'wc-country-select', 'wc-address-i18n' ), - 'version' => $version, - ), - 'wc-country-select' => array( - 'src' => self::get_asset_url( 'assets/js/frontend/country-select' . $suffix . '.js' ), - 'deps' => array( 'jquery' ), - 'version' => $version, - ), - 'wc-credit-card-form' => array( - 'src' => self::get_asset_url( 'assets/js/frontend/credit-card-form' . $suffix . '.js' ), - 'deps' => array( 'jquery', 'jquery-payment' ), - 'version' => $version, - ), - 'wc-add-to-cart' => array( - 'src' => self::get_asset_url( 'assets/js/frontend/add-to-cart' . $suffix . '.js' ), - 'deps' => array( 'jquery', 'jquery-blockui' ), - 'version' => $version, - ), - 'wc-add-to-cart-variation' => array( - 'src' => self::get_asset_url( 'assets/js/frontend/add-to-cart-variation' . $suffix . '.js' ), - 'deps' => array( 'jquery', 'wp-util', 'jquery-blockui' ), - 'version' => $version, - ), - 'wc-geolocation' => array( - 'src' => self::get_asset_url( 'assets/js/frontend/geolocation' . $suffix . '.js' ), - 'deps' => array( 'jquery' ), - 'version' => $version, - ), - 'wc-lost-password' => array( - 'src' => self::get_asset_url( 'assets/js/frontend/lost-password' . $suffix . '.js' ), - 'deps' => array( 'jquery', 'woocommerce' ), - 'version' => $version, - ), - 'wc-password-strength-meter' => array( - 'src' => self::get_asset_url( 'assets/js/frontend/password-strength-meter' . $suffix . '.js' ), - 'deps' => array( 'jquery', 'password-strength-meter' ), - 'version' => $version, - ), - 'wc-single-product' => array( - 'src' => self::get_asset_url( 'assets/js/frontend/single-product' . $suffix . '.js' ), - 'deps' => array( 'jquery' ), - 'version' => $version, - ), - 'woocommerce' => array( - 'src' => self::get_asset_url( 'assets/js/frontend/woocommerce' . $suffix . '.js' ), - 'deps' => array( 'jquery', 'jquery-blockui', 'js-cookie' ), - 'version' => $version, - ), - 'zoom' => array( - 'src' => self::get_asset_url( 'assets/js/zoom/jquery.zoom' . $suffix . '.js' ), - 'deps' => array( 'jquery' ), - 'version' => '1.7.21', - ), - ); - foreach ( $register_scripts as $name => $props ) { - self::register_script( $name, $props['src'], $props['deps'], $props['version'] ); - } - } - - /** - * Register all WC styles. - */ - private static function register_styles() { - $version = Constants::get_constant( 'WC_VERSION' ); - - $register_styles = array( - 'photoswipe' => array( - 'src' => self::get_asset_url( 'assets/css/photoswipe/photoswipe.min.css' ), - 'deps' => array(), - 'version' => $version, - 'has_rtl' => false, - ), - 'photoswipe-default-skin' => array( - 'src' => self::get_asset_url( 'assets/css/photoswipe/default-skin/default-skin.min.css' ), - 'deps' => array( 'photoswipe' ), - 'version' => $version, - 'has_rtl' => false, - ), - 'select2' => array( - 'src' => self::get_asset_url( 'assets/css/select2.css' ), - 'deps' => array(), - 'version' => $version, - 'has_rtl' => false, - ), - 'woocommerce_prettyPhoto_css' => array( // deprecated. - 'src' => self::get_asset_url( 'assets/css/prettyPhoto.css' ), - 'deps' => array(), - 'version' => $version, - 'has_rtl' => true, - ), - ); - foreach ( $register_styles as $name => $props ) { - self::register_style( $name, $props['src'], $props['deps'], $props['version'], 'all', $props['has_rtl'] ); - } - } - - /** - * Register/queue frontend scripts. - */ - public static function load_scripts() { - global $post; - - if ( ! did_action( 'before_woocommerce_init' ) ) { - return; - } - - self::register_scripts(); - self::register_styles(); - - if ( 'yes' === get_option( 'woocommerce_enable_ajax_add_to_cart' ) ) { - self::enqueue_script( 'wc-add-to-cart' ); - } - if ( is_cart() ) { - self::enqueue_script( 'wc-cart' ); - } - if ( is_cart() || is_checkout() || is_account_page() ) { - self::enqueue_script( 'selectWoo' ); - self::enqueue_style( 'select2' ); - - // Password strength meter. Load in checkout, account login and edit account page. - if ( ( 'no' === get_option( 'woocommerce_registration_generate_password' ) && ! is_user_logged_in() ) || is_edit_account_page() || is_lost_password_page() ) { - self::enqueue_script( 'wc-password-strength-meter' ); - } - } - if ( is_checkout() ) { - self::enqueue_script( 'wc-checkout' ); - } - if ( is_add_payment_method_page() ) { - self::enqueue_script( 'wc-add-payment-method' ); - } - if ( is_lost_password_page() ) { - self::enqueue_script( 'wc-lost-password' ); - } - - // Load gallery scripts on product pages only if supported. - if ( is_product() || ( ! empty( $post->post_content ) && strstr( $post->post_content, '[product_page' ) ) ) { - if ( current_theme_supports( 'wc-product-gallery-zoom' ) ) { - self::enqueue_script( 'zoom' ); - } - if ( current_theme_supports( 'wc-product-gallery-slider' ) ) { - self::enqueue_script( 'flexslider' ); - } - if ( current_theme_supports( 'wc-product-gallery-lightbox' ) ) { - self::enqueue_script( 'photoswipe-ui-default' ); - self::enqueue_style( 'photoswipe-default-skin' ); - add_action( 'wp_footer', 'woocommerce_photoswipe' ); - } - self::enqueue_script( 'wc-single-product' ); - } - - if ( 'geolocation_ajax' === get_option( 'woocommerce_default_customer_address' ) ) { - $ua = strtolower( wc_get_user_agent() ); // Exclude common bots from geolocation by user agent. - - if ( ! strstr( $ua, 'bot' ) && ! strstr( $ua, 'spider' ) && ! strstr( $ua, 'crawl' ) ) { - self::enqueue_script( 'wc-geolocation' ); - } - } - - // Global frontend scripts. - self::enqueue_script( 'woocommerce' ); - self::enqueue_script( 'wc-cart-fragments' ); - - // CSS Styles. - $enqueue_styles = self::get_styles(); - if ( $enqueue_styles ) { - foreach ( $enqueue_styles as $handle => $args ) { - if ( ! isset( $args['has_rtl'] ) ) { - $args['has_rtl'] = false; - } - - self::enqueue_style( $handle, $args['src'], $args['deps'], $args['version'], $args['media'], $args['has_rtl'] ); - } - } - - // Placeholder style. - wp_register_style( 'woocommerce-inline', false ); // phpcs:ignore - wp_enqueue_style( 'woocommerce-inline' ); - - if ( true === wc_string_to_bool( get_option( 'woocommerce_checkout_highlight_required_fields', 'yes' ) ) ) { - wp_add_inline_style( 'woocommerce-inline', '.woocommerce form .form-row .required { visibility: visible; }' ); - } else { - wp_add_inline_style( 'woocommerce-inline', '.woocommerce form .form-row .required { visibility: hidden; }' ); - } - } - - /** - * Localize a WC script once. - * - * @since 2.3.0 this needs less wp_script_is() calls due to https://core.trac.wordpress.org/ticket/28404 being added in WP 4.0. - * @param string $handle Script handle the data will be attached to. - */ - private static function localize_script( $handle ) { - if ( ! in_array( $handle, self::$wp_localize_scripts, true ) && wp_script_is( $handle ) ) { - $data = self::get_script_data( $handle ); - - if ( ! $data ) { - return; - } - - $name = str_replace( '-', '_', $handle ) . '_params'; - self::$wp_localize_scripts[] = $handle; - wp_localize_script( $handle, $name, apply_filters( $name, $data ) ); - } - } - - /** - * Return data for script handles. - * - * @param string $handle Script handle the data will be attached to. - * @return array|bool - */ - private static function get_script_data( $handle ) { - global $wp; - - switch ( $handle ) { - case 'woocommerce': - $params = array( - 'ajax_url' => WC()->ajax_url(), - 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), - ); - break; - case 'wc-geolocation': - $params = array( - 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), - 'home_url' => remove_query_arg( 'lang', home_url() ), // FIX for WPML compatibility. - 'is_available' => ! ( is_cart() || is_account_page() || is_checkout() || is_customize_preview() ) ? '1' : '0', - 'hash' => isset( $_GET['v'] ) ? wc_clean( wp_unslash( $_GET['v'] ) ) : '', // WPCS: input var ok, CSRF ok. - ); - break; - case 'wc-single-product': - $params = array( - 'i18n_required_rating_text' => esc_attr__( 'Please select a rating', 'woocommerce' ), - 'review_rating_required' => wc_review_ratings_required() ? 'yes' : 'no', - 'flexslider' => apply_filters( - 'woocommerce_single_product_carousel_options', - array( - 'rtl' => is_rtl(), - 'animation' => 'slide', - 'smoothHeight' => true, - 'directionNav' => false, - 'controlNav' => 'thumbnails', - 'slideshow' => false, - 'animationSpeed' => 500, - 'animationLoop' => false, // Breaks photoswipe pagination if true. - 'allowOneSlide' => false, - ) - ), - 'zoom_enabled' => apply_filters( 'woocommerce_single_product_zoom_enabled', get_theme_support( 'wc-product-gallery-zoom' ) ), - 'zoom_options' => apply_filters( 'woocommerce_single_product_zoom_options', array() ), - 'photoswipe_enabled' => apply_filters( 'woocommerce_single_product_photoswipe_enabled', get_theme_support( 'wc-product-gallery-lightbox' ) ), - 'photoswipe_options' => apply_filters( - 'woocommerce_single_product_photoswipe_options', - array( - 'shareEl' => false, - 'closeOnScroll' => false, - 'history' => false, - 'hideAnimationDuration' => 0, - 'showAnimationDuration' => 0, - ) - ), - 'flexslider_enabled' => apply_filters( 'woocommerce_single_product_flexslider_enabled', get_theme_support( 'wc-product-gallery-slider' ) ), - ); - break; - case 'wc-checkout': - $params = array( - 'ajax_url' => WC()->ajax_url(), - 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), - 'update_order_review_nonce' => wp_create_nonce( 'update-order-review' ), - 'apply_coupon_nonce' => wp_create_nonce( 'apply-coupon' ), - 'remove_coupon_nonce' => wp_create_nonce( 'remove-coupon' ), - 'option_guest_checkout' => get_option( 'woocommerce_enable_guest_checkout' ), - 'checkout_url' => WC_AJAX::get_endpoint( 'checkout' ), - 'is_checkout' => is_checkout() && empty( $wp->query_vars['order-pay'] ) && ! isset( $wp->query_vars['order-received'] ) ? 1 : 0, - 'debug_mode' => Constants::is_true( 'WP_DEBUG' ), - 'i18n_checkout_error' => esc_attr__( 'Error processing checkout. Please try again.', 'woocommerce' ), - ); - break; - case 'wc-address-i18n': - $params = array( - 'locale' => wp_json_encode( WC()->countries->get_country_locale() ), - 'locale_fields' => wp_json_encode( WC()->countries->get_country_locale_field_selectors() ), - 'i18n_required_text' => esc_attr__( 'required', 'woocommerce' ), - 'i18n_optional_text' => esc_html__( 'optional', 'woocommerce' ), - ); - break; - case 'wc-cart': - $params = array( - 'ajax_url' => WC()->ajax_url(), - 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), - 'update_shipping_method_nonce' => wp_create_nonce( 'update-shipping-method' ), - 'apply_coupon_nonce' => wp_create_nonce( 'apply-coupon' ), - 'remove_coupon_nonce' => wp_create_nonce( 'remove-coupon' ), - ); - break; - case 'wc-cart-fragments': - $params = array( - 'ajax_url' => WC()->ajax_url(), - 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), - 'cart_hash_key' => apply_filters( 'woocommerce_cart_hash_key', 'wc_cart_hash_' . md5( get_current_blog_id() . '_' . get_site_url( get_current_blog_id(), '/' ) . get_template() ) ), - 'fragment_name' => apply_filters( 'woocommerce_cart_fragment_name', 'wc_fragments_' . md5( get_current_blog_id() . '_' . get_site_url( get_current_blog_id(), '/' ) . get_template() ) ), - 'request_timeout' => 5000, - ); - break; - case 'wc-add-to-cart': - $params = array( - 'ajax_url' => WC()->ajax_url(), - 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), - 'i18n_view_cart' => esc_attr__( 'View cart', 'woocommerce' ), - 'cart_url' => apply_filters( 'woocommerce_add_to_cart_redirect', wc_get_cart_url(), null ), - 'is_cart' => is_cart(), - 'cart_redirect_after_add' => get_option( 'woocommerce_cart_redirect_after_add' ), - ); - break; - case 'wc-add-to-cart-variation': - // We also need the wp.template for this script :). - wc_get_template( 'single-product/add-to-cart/variation.php' ); - - $params = array( - 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), - 'i18n_no_matching_variations_text' => esc_attr__( 'Sorry, no products matched your selection. Please choose a different combination.', 'woocommerce' ), - 'i18n_make_a_selection_text' => esc_attr__( 'Please select some product options before adding this product to your cart.', 'woocommerce' ), - 'i18n_unavailable_text' => esc_attr__( 'Sorry, this product is unavailable. Please choose a different combination.', 'woocommerce' ), - ); - break; - case 'wc-country-select': - $params = array( - 'countries' => wp_json_encode( array_merge( WC()->countries->get_allowed_country_states(), WC()->countries->get_shipping_country_states() ) ), - 'i18n_select_state_text' => esc_attr__( 'Select an option…', 'woocommerce' ), - 'i18n_no_matches' => _x( 'No matches found', 'enhanced select', 'woocommerce' ), - 'i18n_ajax_error' => _x( 'Loading failed', 'enhanced select', 'woocommerce' ), - 'i18n_input_too_short_1' => _x( 'Please enter 1 or more characters', 'enhanced select', 'woocommerce' ), - 'i18n_input_too_short_n' => _x( 'Please enter %qty% or more characters', 'enhanced select', 'woocommerce' ), - 'i18n_input_too_long_1' => _x( 'Please delete 1 character', 'enhanced select', 'woocommerce' ), - 'i18n_input_too_long_n' => _x( 'Please delete %qty% characters', 'enhanced select', 'woocommerce' ), - 'i18n_selection_too_long_1' => _x( 'You can only select 1 item', 'enhanced select', 'woocommerce' ), - 'i18n_selection_too_long_n' => _x( 'You can only select %qty% items', 'enhanced select', 'woocommerce' ), - 'i18n_load_more' => _x( 'Loading more results…', 'enhanced select', 'woocommerce' ), - 'i18n_searching' => _x( 'Searching…', 'enhanced select', 'woocommerce' ), - ); - break; - case 'wc-password-strength-meter': - $params = array( - 'min_password_strength' => apply_filters( 'woocommerce_min_password_strength', 3 ), - 'stop_checkout' => apply_filters( 'woocommerce_enforce_password_strength_meter_on_checkout', false ), - 'i18n_password_error' => esc_attr__( 'Please enter a stronger password.', 'woocommerce' ), - 'i18n_password_hint' => esc_attr( wp_get_password_hint() ), - ); - break; - default: - $params = false; - } - - $params = apply_filters_deprecated( $handle . '_params', array( $params ), '3.0.0', 'woocommerce_get_script_data' ); - - return apply_filters( 'woocommerce_get_script_data', $params, $handle ); - } - - /** - * Localize scripts only when enqueued. - */ - public static function localize_printed_scripts() { - foreach ( self::$scripts as $handle ) { - self::localize_script( $handle ); - } - } -} - -WC_Frontend_Scripts::init(); diff --git a/includes/class-wc-geo-ip.php b/includes/class-wc-geo-ip.php deleted file mode 100644 index c79be25b985..00000000000 --- a/includes/class-wc-geo-ip.php +++ /dev/null @@ -1,1814 +0,0 @@ -log( $level, $message, array( 'source' => 'geoip' ) ); - } - - /** - * Open geoip file. - * - * @param string $filename - * @param int $flags - */ - public function geoip_open( $filename, $flags ) { - $this->flags = $flags; - if ( $this->flags & self::GEOIP_SHARED_MEMORY ) { - $this->shmid = @shmop_open( self::GEOIP_SHM_KEY, 'a', 0, 0 ); - } else { - if ( $this->filehandle = fopen( $filename, 'rb' ) ) { - if ( $this->flags & self::GEOIP_MEMORY_CACHE ) { - $s_array = fstat( $this->filehandle ); - $this->memory_buffer = fread( $this->filehandle, $s_array['size'] ); - } - } else { - $this->log( 'GeoIP API: Can not open ' . $filename, 'error' ); - } - } - - $this->_setup_segments(); - } - - /** - * Setup segments. - * - * @return WC_Geo_IP instance - */ - private function _setup_segments() { - $this->databaseType = self::GEOIP_COUNTRY_EDITION; - $this->record_length = self::STANDARD_RECORD_LENGTH; - - if ( $this->flags & self::GEOIP_SHARED_MEMORY ) { - $offset = @shmop_size( $this->shmid ) - 3; - - for ( $i = 0; $i < self::STRUCTURE_INFO_MAX_SIZE; $i++ ) { - $delim = @shmop_read( $this->shmid, $offset, 3 ); - $offset += 3; - - if ( ( chr( 255 ) . chr( 255 ) . chr( 255 ) ) == $delim ) { - $this->databaseType = ord( @shmop_read( $this->shmid, $offset, 1 ) ); - - if ( $this->databaseType >= 106 ) { - $this->databaseType -= 105; - } - - $offset++; - - if ( self::GEOIP_REGION_EDITION_REV0 == $this->databaseType ) { - $this->databaseSegments = self::GEOIP_STATE_BEGIN_REV0; - } elseif ( self::GEOIP_REGION_EDITION_REV1 == $this->databaseType ) { - $this->databaseSegments = self::GEOIP_STATE_BEGIN_REV1; - } elseif ( ( self::GEOIP_CITY_EDITION_REV0 == $this->databaseType ) - || ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) - || ( self::GEOIP_ORG_EDITION == $this->databaseType ) - || ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType ) - || ( self::GEOIP_DOMAIN_EDITION == $this->databaseType ) - || ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType ) - || ( self::GEOIP_ISP_EDITION == $this->databaseType ) - || ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType ) - || ( self::GEOIP_USERTYPE_EDITION == $this->databaseType ) - || ( self::GEOIP_USERTYPE_EDITION_V6 == $this->databaseType ) - || ( self::GEOIP_LOCATIONA_EDITION == $this->databaseType ) - || ( self::GEOIP_ACCURACYRADIUS_EDITION == $this->databaseType ) - || ( self::GEOIP_CITY_EDITION_REV0_V6 == $this->databaseType ) - || ( self::GEOIP_CITY_EDITION_REV1_V6 == $this->databaseType ) - || ( self::GEOIP_NETSPEED_EDITION_REV1 == $this->databaseType ) - || ( self::GEOIP_NETSPEED_EDITION_REV1_V6 == $this->databaseType ) - || ( self::GEOIP_ASNUM_EDITION == $this->databaseType ) - || ( self::GEOIP_ASNUM_EDITION_V6 == $this->databaseType ) - ) { - $this->databaseSegments = 0; - $buf = @shmop_read( $this->shmid, $offset, self::SEGMENT_RECORD_LENGTH ); - - for ( $j = 0; $j < self::SEGMENT_RECORD_LENGTH; $j++ ) { - $this->databaseSegments += ( ord( $buf[ $j ] ) << ( $j * 8 ) ); - } - - if ( ( self::GEOIP_ORG_EDITION == $this->databaseType ) - || ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType ) - || ( self::GEOIP_DOMAIN_EDITION == $this->databaseType ) - || ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType ) - || ( self::GEOIP_ISP_EDITION == $this->databaseType ) - || ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType ) - ) { - $this->record_length = self::ORG_RECORD_LENGTH; - } - } - - break; - } else { - $offset -= 4; - } - } - if ( ( self::GEOIP_COUNTRY_EDITION == $this->databaseType ) - || ( self::GEOIP_COUNTRY_EDITION_V6 == $this->databaseType ) - || ( self::GEOIP_PROXY_EDITION == $this->databaseType ) - || ( self::GEOIP_NETSPEED_EDITION == $this->databaseType ) - ) { - $this->databaseSegments = self::GEOIP_COUNTRY_BEGIN; - } - } else { - $filepos = ftell( $this->filehandle ); - fseek( $this->filehandle, -3, SEEK_END ); - - for ( $i = 0; $i < self::STRUCTURE_INFO_MAX_SIZE; $i++ ) { - - $delim = fread( $this->filehandle, 3 ); - if ( ( chr( 255 ) . chr( 255 ) . chr( 255 ) ) == $delim ) { - - $this->databaseType = ord( fread( $this->filehandle, 1 ) ); - if ( $this->databaseType >= 106 ) { - $this->databaseType -= 105; - } - - if ( self::GEOIP_REGION_EDITION_REV0 == $this->databaseType ) { - $this->databaseSegments = self::GEOIP_STATE_BEGIN_REV0; - } elseif ( self::GEOIP_REGION_EDITION_REV1 == $this->databaseType ) { - $this->databaseSegments = self::GEOIP_STATE_BEGIN_REV1; - } elseif ( ( self::GEOIP_CITY_EDITION_REV0 == $this->databaseType ) - || ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) - || ( self::GEOIP_CITY_EDITION_REV0_V6 == $this->databaseType ) - || ( self::GEOIP_CITY_EDITION_REV1_V6 == $this->databaseType ) - || ( self::GEOIP_ORG_EDITION == $this->databaseType ) - || ( self::GEOIP_DOMAIN_EDITION == $this->databaseType ) - || ( self::GEOIP_ISP_EDITION == $this->databaseType ) - || ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType ) - || ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType ) - || ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType ) - || ( self::GEOIP_LOCATIONA_EDITION == $this->databaseType ) - || ( self::GEOIP_ACCURACYRADIUS_EDITION == $this->databaseType ) - || ( self::GEOIP_NETSPEED_EDITION_REV1 == $this->databaseType ) - || ( self::GEOIP_NETSPEED_EDITION_REV1_V6 == $this->databaseType ) - || ( self::GEOIP_USERTYPE_EDITION == $this->databaseType ) - || ( self::GEOIP_USERTYPE_EDITION_V6 == $this->databaseType ) - || ( self::GEOIP_ASNUM_EDITION == $this->databaseType ) - || ( self::GEOIP_ASNUM_EDITION_V6 == $this->databaseType ) - ) { - $this->databaseSegments = 0; - $buf = fread( $this->filehandle, self::SEGMENT_RECORD_LENGTH ); - - for ( $j = 0; $j < self::SEGMENT_RECORD_LENGTH; $j++ ) { - $this->databaseSegments += ( ord( $buf[ $j ] ) << ( $j * 8 ) ); - } - - if ( ( self::GEOIP_ORG_EDITION == $this->databaseType ) - || ( self::GEOIP_DOMAIN_EDITION == $this->databaseType ) - || ( self::GEOIP_ISP_EDITION == $this->databaseType ) - || ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType ) - || ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType ) - || ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType ) - ) { - $this->record_length = self::ORG_RECORD_LENGTH; - } - } - - break; - } else { - fseek( $this->filehandle, -4, SEEK_CUR ); - } - } - - if ( ( self::GEOIP_COUNTRY_EDITION == $this->databaseType ) - || ( self::GEOIP_COUNTRY_EDITION_V6 == $this->databaseType ) - || ( self::GEOIP_PROXY_EDITION == $this->databaseType ) - || ( self::GEOIP_NETSPEED_EDITION == $this->databaseType ) - ) { - $this->databaseSegments = self::GEOIP_COUNTRY_BEGIN; - } - - fseek( $this->filehandle, $filepos, SEEK_SET ); - } - - return $this; - } - - /** - * Close geoip file. - * - * @return bool - */ - public function geoip_close() { - if ( $this->flags & self::GEOIP_SHARED_MEMORY ) { - return true; - } - - return fclose( $this->filehandle ); - } - - /** - * Common get record. - * - * @param string $seek_country - * @return WC_Geo_IP_Record instance - */ - private function _common_get_record( $seek_country ) { - // workaround php's broken substr, strpos, etc handling with - // mbstring.func_overload and mbstring.internal_encoding - $mbExists = extension_loaded( 'mbstring' ); - if ( $mbExists ) { - $enc = mb_internal_encoding(); - mb_internal_encoding( 'ISO-8859-1' ); - } - - $record_pointer = $seek_country + ( 2 * $this->record_length - 1 ) * $this->databaseSegments; - - if ( $this->flags & self::GEOIP_MEMORY_CACHE ) { - $record_buf = substr( $this->memory_buffer, $record_pointer, FULL_RECORD_LENGTH ); - } elseif ( $this->flags & self::GEOIP_SHARED_MEMORY ) { - $record_buf = @shmop_read( $this->shmid, $record_pointer, FULL_RECORD_LENGTH ); - } else { - fseek( $this->filehandle, $record_pointer, SEEK_SET ); - $record_buf = fread( $this->filehandle, FULL_RECORD_LENGTH ); - } - - $record = new WC_Geo_IP_Record(); - $record_buf_pos = 0; - $char = ord( substr( $record_buf, $record_buf_pos, 1 ) ); - $record->country_code = $this->GEOIP_COUNTRY_CODES[ $char ]; - $record->country_code3 = $this->GEOIP_COUNTRY_CODES3[ $char ]; - $record->country_name = $this->GEOIP_COUNTRY_NAMES[ $char ]; - $record->continent_code = $this->GEOIP_CONTINENT_CODES[ $char ]; - $str_length = 0; - - $record_buf_pos++; - - // Get region - $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); - while ( 0 != $char ) { - $str_length++; - $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); - } - - if ( $str_length > 0 ) { - $record->region = substr( $record_buf, $record_buf_pos, $str_length ); - } - - $record_buf_pos += $str_length + 1; - $str_length = 0; - - // Get city - $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); - while ( 0 != $char ) { - $str_length++; - $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); - } - - if ( $str_length > 0 ) { - $record->city = substr( $record_buf, $record_buf_pos, $str_length ); - } - - $record_buf_pos += $str_length + 1; - $str_length = 0; - - // Get postal code - $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); - while ( 0 != $char ) { - $str_length++; - $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); - } - - if ( $str_length > 0 ) { - $record->postal_code = substr( $record_buf, $record_buf_pos, $str_length ); - } - - $record_buf_pos += $str_length + 1; - - // Get latitude and longitude - $latitude = 0; - $longitude = 0; - for ( $j = 0; $j < 3; ++$j ) { - $char = ord( substr( $record_buf, $record_buf_pos++, 1 ) ); - $latitude += ( $char << ( $j * 8 ) ); - } - - $record->latitude = ( $latitude / 10000 ) - 180; - - for ( $j = 0; $j < 3; ++$j ) { - $char = ord( substr( $record_buf, $record_buf_pos++, 1 ) ); - $longitude += ( $char << ( $j * 8 ) ); - } - - $record->longitude = ( $longitude / 10000 ) - 180; - - if ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) { - $metroarea_combo = 0; - if ( 'US' === $record->country_code ) { - for ( $j = 0; $j < 3; ++$j ) { - $char = ord( substr( $record_buf, $record_buf_pos++, 1 ) ); - $metroarea_combo += ( $char << ( $j * 8 ) ); - } - - $record->metro_code = $record->dma_code = floor( $metroarea_combo / 1000 ); - $record->area_code = $metroarea_combo % 1000; - } - } - - if ( $mbExists ) { - mb_internal_encoding( $enc ); - } - - return $record; - } - - /** - * Get record. - * - * @param int $ipnum - * @return WC_Geo_IP_Record instance - */ - private function _get_record( $ipnum ) { - $seek_country = $this->_geoip_seek_country( $ipnum ); - if ( $seek_country == $this->databaseSegments ) { - return null; - } - - return $this->_common_get_record( $seek_country ); - } - - /** - * Seek country IPv6. - * - * @param int $ipnum - * @return string - */ - public function _geoip_seek_country_v6( $ipnum ) { - // arrays from unpack start with offset 1 - // yet another php mystery. array_merge work around - // this broken behaviour - $v6vec = array_merge( unpack( 'C16', $ipnum ) ); - - $offset = 0; - for ( $depth = 127; $depth >= 0; --$depth ) { - if ( $this->flags & self::GEOIP_MEMORY_CACHE ) { - $buf = $this->_safe_substr( - $this->memory_buffer, - 2 * $this->record_length * $offset, - 2 * $this->record_length - ); - } elseif ( $this->flags & self::GEOIP_SHARED_MEMORY ) { - $buf = @shmop_read( - $this->shmid, - 2 * $this->record_length * $offset, - 2 * $this->record_length - ); - } else { - if ( 0 != fseek( $this->filehandle, 2 * $this->record_length * $offset, SEEK_SET ) ) { - break; - } - - $buf = fread( $this->filehandle, 2 * $this->record_length ); - } - $x = array( 0, 0 ); - for ( $i = 0; $i < 2; ++$i ) { - for ( $j = 0; $j < $this->record_length; ++$j ) { - $x[ $i ] += ord( $buf[ $this->record_length * $i + $j ] ) << ( $j * 8 ); - } - } - - $bnum = 127 - $depth; - $idx = $bnum >> 3; - $b_mask = 1 << ( $bnum & 7 ^ 7 ); - if ( ( $v6vec[ $idx ] & $b_mask ) > 0 ) { - if ( $x[1] >= $this->databaseSegments ) { - return $x[1]; - } - $offset = $x[1]; - } else { - if ( $x[0] >= $this->databaseSegments ) { - return $x[0]; - } - $offset = $x[0]; - } - } - - $this->log( 'GeoIP API: Error traversing database - perhaps it is corrupt?', 'error' ); - - return false; - } - - /** - * Seek country. - * - * @param int $ipnum - * @return string - */ - private function _geoip_seek_country( $ipnum ) { - $offset = 0; - for ( $depth = 31; $depth >= 0; --$depth ) { - if ( $this->flags & self::GEOIP_MEMORY_CACHE ) { - $buf = $this->_safe_substr( - $this->memory_buffer, - 2 * $this->record_length * $offset, - 2 * $this->record_length - ); - } elseif ( $this->flags & self::GEOIP_SHARED_MEMORY ) { - $buf = @shmop_read( - $this->shmid, - 2 * $this->record_length * $offset, - 2 * $this->record_length - ); - } else { - if ( 0 != fseek( $this->filehandle, 2 * $this->record_length * $offset, SEEK_SET ) ) { - break; - } - - $buf = fread( $this->filehandle, 2 * $this->record_length ); - } - - $x = array( 0, 0 ); - for ( $i = 0; $i < 2; ++$i ) { - for ( $j = 0; $j < $this->record_length; ++$j ) { - $x[ $i ] += ord( $buf[ $this->record_length * $i + $j ] ) << ( $j * 8 ); - } - } - if ( $ipnum & ( 1 << $depth ) ) { - if ( $x[1] >= $this->databaseSegments ) { - return $x[1]; - } - - $offset = $x[1]; - } else { - if ( $x[0] >= $this->databaseSegments ) { - return $x[0]; - } - - $offset = $x[0]; - } - } - - $this->log( 'GeoIP API: Error traversing database - perhaps it is corrupt?', 'error' ); - - return false; - } - - /** - * Record by addr. - * - * @param string $addr - * - * @return WC_Geo_IP_Record - */ - public function geoip_record_by_addr( $addr ) { - if ( null == $addr ) { - return 0; - } - - $ipnum = ip2long( $addr ); - return $this->_get_record( $ipnum ); - } - - /** - * Country ID by addr IPv6. - * - * @param string $addr - * @return int|bool - */ - public function geoip_country_id_by_addr_v6( $addr ) { - if ( ! defined( 'AF_INET6' ) ) { - $this->log( 'GEOIP (geoip_country_id_by_addr_v6): PHP was compiled with --disable-ipv6 option' ); - return false; - } - $ipnum = inet_pton( $addr ); - return $this->_geoip_seek_country_v6( $ipnum ) - self::GEOIP_COUNTRY_BEGIN; - } - - /** - * Country ID by addr. - * - * @param string $addr - * @return int - */ - public function geoip_country_id_by_addr( $addr ) { - $ipnum = ip2long( $addr ); - return $this->_geoip_seek_country( $ipnum ) - self::GEOIP_COUNTRY_BEGIN; - } - - /** - * Country code by addr IPv6. - * - * @param string $addr - * @return string - */ - public function geoip_country_code_by_addr_v6( $addr ) { - $country_id = $this->geoip_country_id_by_addr_v6( $addr ); - if ( false !== $country_id && isset( $this->GEOIP_COUNTRY_CODES[ $country_id ] ) ) { - return $this->GEOIP_COUNTRY_CODES[ $country_id ]; - } - - return false; - } - - /** - * Country code by addr. - * - * @param string $addr - * @return string - */ - public function geoip_country_code_by_addr( $addr ) { - if ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) { - $record = $this->geoip_record_by_addr( $addr ); - if ( false !== $record ) { - return $record->country_code; - } - } else { - $country_id = $this->geoip_country_id_by_addr( $addr ); - if ( false !== $country_id && isset( $this->GEOIP_COUNTRY_CODES[ $country_id ] ) ) { - return $this->GEOIP_COUNTRY_CODES[ $country_id ]; - } - } - - return false; - } - - /** - * Encode string. - * - * @param string $string - * @param int $start - * @param int $length - * @return string - */ - private function _safe_substr( $string, $start, $length ) { - // workaround php's broken substr, strpos, etc handling with - // mbstring.func_overload and mbstring.internal_encoding - $mb_exists = extension_loaded( 'mbstring' ); - - if ( $mb_exists ) { - $enc = mb_internal_encoding(); - mb_internal_encoding( 'ISO-8859-1' ); - } - - $buf = substr( $string, $start, $length ); - - if ( $mb_exists ) { - mb_internal_encoding( $enc ); - } - - return $buf; - } -} - -/** - * Geo IP Record class. - */ -class WC_Geo_IP_Record { - - /** - * Country code. - * - * @var string - */ - public $country_code; - - /** - * 3 letters country code. - * - * @var string - */ - public $country_code3; - - /** - * Country name. - * - * @var string - */ - public $country_name; - - /** - * Region. - * - * @var string - */ - public $region; - - /** - * City. - * - * @var string - */ - public $city; - - /** - * Postal code. - * - * @var string - */ - public $postal_code; - - /** - * Latitude - * - * @var int - */ - public $latitude; - - /** - * Longitude. - * - * @var int - */ - public $longitude; - - /** - * Area code. - * - * @var int - */ - public $area_code; - - /** - * DMA Code. - * - * Metro and DMA code are the same. - * Use metro code instead. - * - * @var float - */ - public $dma_code; - - /** - * Metro code. - * - * @var float - */ - public $metro_code; - - /** - * Continent code. - * - * @var string - */ - public $continent_code; -} diff --git a/includes/class-wc-geolocation.php b/includes/class-wc-geolocation.php deleted file mode 100644 index 97d918404ab..00000000000 --- a/includes/class-wc-geolocation.php +++ /dev/null @@ -1,356 +0,0 @@ - 'http://api.ipify.org/', - 'ipecho' => 'http://ipecho.net/plain', - 'ident' => 'http://ident.me', - 'whatismyipaddress' => 'http://bot.whatismyipaddress.com', - ); - - /** - * API endpoints for geolocating an IP address - * - * @var array - */ - private static $geoip_apis = array( - 'ipinfo.io' => 'https://ipinfo.io/%s/json', - 'ip-api.com' => 'http://ip-api.com/json/%s', - ); - - /** - * Check if geolocation is enabled. - * - * @since 3.4.0 - * @param string $current_settings Current geolocation settings. - * @return bool - */ - private static function is_geolocation_enabled( $current_settings ) { - return in_array( $current_settings, array( 'geolocation', 'geolocation_ajax' ), true ); - } - - /** - * Get current user IP Address. - * - * @return string - */ - public static function get_ip_address() { - if ( isset( $_SERVER['HTTP_X_REAL_IP'] ) ) { - return sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_REAL_IP'] ) ); - } elseif ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { - // Proxy servers can send through this header like this: X-Forwarded-For: client1, proxy1, proxy2 - // Make sure we always only send through the first IP in the list which should always be the client IP. - return (string) rest_is_ip_address( trim( current( preg_split( '/,/', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) ) ) ) ); - } elseif ( isset( $_SERVER['REMOTE_ADDR'] ) ) { - return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); - } - return ''; - } - - /** - * Get user IP Address using an external service. - * This can be used as a fallback for users on localhost where - * get_ip_address() will be a local IP and non-geolocatable. - * - * @return string - */ - public static function get_external_ip_address() { - $external_ip_address = '0.0.0.0'; - - if ( '' !== self::get_ip_address() ) { - $transient_name = 'external_ip_address_' . self::get_ip_address(); - $external_ip_address = get_transient( $transient_name ); - } - - if ( false === $external_ip_address ) { - $external_ip_address = '0.0.0.0'; - $ip_lookup_services = apply_filters( 'woocommerce_geolocation_ip_lookup_apis', self::$ip_lookup_apis ); - $ip_lookup_services_keys = array_keys( $ip_lookup_services ); - shuffle( $ip_lookup_services_keys ); - - foreach ( $ip_lookup_services_keys as $service_name ) { - $service_endpoint = $ip_lookup_services[ $service_name ]; - $response = wp_safe_remote_get( $service_endpoint, array( 'timeout' => 2 ) ); - - if ( ! is_wp_error( $response ) && rest_is_ip_address( $response['body'] ) ) { - $external_ip_address = apply_filters( 'woocommerce_geolocation_ip_lookup_api_response', wc_clean( $response['body'] ), $service_name ); - break; - } - } - - set_transient( $transient_name, $external_ip_address, WEEK_IN_SECONDS ); - } - - return $external_ip_address; - } - - /** - * Geolocate an IP address. - * - * @param string $ip_address IP Address. - * @param bool $fallback If true, fallbacks to alternative IP detection (can be slower). - * @param bool $api_fallback If true, uses geolocation APIs if the database file doesn't exist (can be slower). - * @return array - */ - public static function geolocate_ip( $ip_address = '', $fallback = false, $api_fallback = true ) { - // Filter to allow custom geolocation of the IP address. - $country_code = apply_filters( 'woocommerce_geolocate_ip', false, $ip_address, $fallback, $api_fallback ); - - if ( false !== $country_code ) { - return array( - 'country' => $country_code, - 'state' => '', - 'city' => '', - 'postcode' => '', - ); - } - - if ( empty( $ip_address ) ) { - $ip_address = self::get_ip_address(); - } - - $country_code = self::get_country_code_from_headers(); - - /** - * Get geolocation filter. - * - * @since 3.9.0 - * @param array $geolocation Geolocation data, including country, state, city, and postcode. - * @param string $ip_address IP Address. - */ - $geolocation = apply_filters( - 'woocommerce_get_geolocation', - array( - 'country' => $country_code, - 'state' => '', - 'city' => '', - 'postcode' => '', - ), - $ip_address - ); - - // If we still haven't found a country code, let's consider doing an API lookup. - if ( '' === $geolocation['country'] && $api_fallback ) { - $geolocation['country'] = self::geolocate_via_api( $ip_address ); - } - - // It's possible that we're in a local environment, in which case the geolocation needs to be done from the - // external address. - if ( '' === $geolocation['country'] && $fallback ) { - $external_ip_address = self::get_external_ip_address(); - - // Only bother with this if the external IP differs. - if ( '0.0.0.0' !== $external_ip_address && $external_ip_address !== $ip_address ) { - return self::geolocate_ip( $external_ip_address, false, $api_fallback ); - } - } - - return array( - 'country' => $geolocation['country'], - 'state' => $geolocation['state'], - 'city' => $geolocation['city'], - 'postcode' => $geolocation['postcode'], - ); - } - - /** - * Path to our local db. - * - * @deprecated 3.9.0 - * @param string $deprecated Deprecated since 3.4.0. - * @return string - */ - public static function get_local_database_path( $deprecated = '2' ) { - wc_deprecated_function( 'WC_Geolocation::get_local_database_path', '3.9.0' ); - $integration = wc()->integrations->get_integration( 'maxmind_geolocation' ); - return $integration->get_database_service()->get_database_path(); - } - - /** - * Update geoip database. - * - * @deprecated 3.9.0 - * Extract files with PharData. Tool built into PHP since 5.3. - */ - public static function update_database() { - wc_deprecated_function( 'WC_Geolocation::update_database', '3.9.0' ); - $integration = wc()->integrations->get_integration( 'maxmind_geolocation' ); - $integration->update_database(); - } - - /** - * Fetches the country code from the request headers, if one is available. - * - * @since 3.9.0 - * @return string The country code pulled from the headers, or empty string if one was not found. - */ - private static function get_country_code_from_headers() { - $country_code = ''; - - $headers = array( - 'MM_COUNTRY_CODE', - 'GEOIP_COUNTRY_CODE', - 'HTTP_CF_IPCOUNTRY', - 'HTTP_X_COUNTRY_CODE', - ); - - foreach ( $headers as $header ) { - if ( empty( $_SERVER[ $header ] ) ) { - continue; - } - - $country_code = strtoupper( sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) ) ); - break; - } - - return $country_code; - } - - /** - * Use APIs to Geolocate the user. - * - * Geolocation APIs can be added through the use of the woocommerce_geolocation_geoip_apis filter. - * Provide a name=>value pair for service-slug=>endpoint. - * - * If APIs are defined, one will be chosen at random to fulfil the request. After completing, the result - * will be cached in a transient. - * - * @param string $ip_address IP address. - * @return string - */ - private static function geolocate_via_api( $ip_address ) { - $country_code = get_transient( 'geoip_' . $ip_address ); - - if ( false === $country_code ) { - $geoip_services = apply_filters( 'woocommerce_geolocation_geoip_apis', self::$geoip_apis ); - - if ( empty( $geoip_services ) ) { - return ''; - } - - $geoip_services_keys = array_keys( $geoip_services ); - - shuffle( $geoip_services_keys ); - - foreach ( $geoip_services_keys as $service_name ) { - $service_endpoint = $geoip_services[ $service_name ]; - $response = wp_safe_remote_get( sprintf( $service_endpoint, $ip_address ), array( 'timeout' => 2 ) ); - - if ( ! is_wp_error( $response ) && $response['body'] ) { - switch ( $service_name ) { - case 'ipinfo.io': - $data = json_decode( $response['body'] ); - $country_code = isset( $data->country ) ? $data->country : ''; - break; - case 'ip-api.com': - $data = json_decode( $response['body'] ); - $country_code = isset( $data->countryCode ) ? $data->countryCode : ''; // @codingStandardsIgnoreLine - break; - default: - $country_code = apply_filters( 'woocommerce_geolocation_geoip_response_' . $service_name, '', $response['body'] ); - break; - } - - $country_code = sanitize_text_field( strtoupper( $country_code ) ); - - if ( $country_code ) { - break; - } - } - } - - set_transient( 'geoip_' . $ip_address, $country_code, WEEK_IN_SECONDS ); - } - - return $country_code; - } - - /** - * Hook in geolocation functionality. - * - * @deprecated 3.9.0 - * @return null - */ - public static function init() { - wc_deprecated_function( 'WC_Geolocation::init', '3.9.0' ); - return null; - } - - /** - * Prevent geolocation via MaxMind when using legacy versions of php. - * - * @deprecated 3.9.0 - * @since 3.4.0 - * @param string $default_customer_address current value. - * @return string - */ - public static function disable_geolocation_on_legacy_php( $default_customer_address ) { - wc_deprecated_function( 'WC_Geolocation::disable_geolocation_on_legacy_php', '3.9.0' ); - - if ( self::is_geolocation_enabled( $default_customer_address ) ) { - $default_customer_address = 'base'; - } - - return $default_customer_address; - } - - /** - * Maybe trigger a DB update for the first time. - * - * @deprecated 3.9.0 - * @param string $new_value New value. - * @param string $old_value Old value. - * @return string - */ - public static function maybe_update_database( $new_value, $old_value ) { - wc_deprecated_function( 'WC_Geolocation::maybe_update_database', '3.9.0' ); - if ( $new_value !== $old_value && self::is_geolocation_enabled( $new_value ) ) { - self::update_database(); - } - - return $new_value; - } -} diff --git a/includes/class-wc-https.php b/includes/class-wc-https.php deleted file mode 100644 index 84e3aeddd7b..00000000000 --- a/includes/class-wc-https.php +++ /dev/null @@ -1,138 +0,0 @@ - array( - 'wc_update_200_file_paths', - 'wc_update_200_permalinks', - 'wc_update_200_subcat_display', - 'wc_update_200_taxrates', - 'wc_update_200_line_items', - 'wc_update_200_images', - 'wc_update_200_db_version', - ), - '2.0.9' => array( - 'wc_update_209_brazillian_state', - 'wc_update_209_db_version', - ), - '2.1.0' => array( - 'wc_update_210_remove_pages', - 'wc_update_210_file_paths', - 'wc_update_210_db_version', - ), - '2.2.0' => array( - 'wc_update_220_shipping', - 'wc_update_220_order_status', - 'wc_update_220_variations', - 'wc_update_220_attributes', - 'wc_update_220_db_version', - ), - '2.3.0' => array( - 'wc_update_230_options', - 'wc_update_230_db_version', - ), - '2.4.0' => array( - 'wc_update_240_options', - 'wc_update_240_shipping_methods', - 'wc_update_240_api_keys', - 'wc_update_240_refunds', - 'wc_update_240_db_version', - ), - '2.4.1' => array( - 'wc_update_241_variations', - 'wc_update_241_db_version', - ), - '2.5.0' => array( - 'wc_update_250_currency', - 'wc_update_250_db_version', - ), - '2.6.0' => array( - 'wc_update_260_options', - 'wc_update_260_termmeta', - 'wc_update_260_zones', - 'wc_update_260_zone_methods', - 'wc_update_260_refunds', - 'wc_update_260_db_version', - ), - '3.0.0' => array( - 'wc_update_300_grouped_products', - 'wc_update_300_settings', - 'wc_update_300_product_visibility', - 'wc_update_300_db_version', - ), - '3.1.0' => array( - 'wc_update_310_downloadable_products', - 'wc_update_310_old_comments', - 'wc_update_310_db_version', - ), - '3.1.2' => array( - 'wc_update_312_shop_manager_capabilities', - 'wc_update_312_db_version', - ), - '3.2.0' => array( - 'wc_update_320_mexican_states', - 'wc_update_320_db_version', - ), - '3.3.0' => array( - 'wc_update_330_image_options', - 'wc_update_330_webhooks', - 'wc_update_330_product_stock_status', - 'wc_update_330_set_default_product_cat', - 'wc_update_330_clear_transients', - 'wc_update_330_set_paypal_sandbox_credentials', - 'wc_update_330_db_version', - ), - '3.4.0' => array( - 'wc_update_340_states', - 'wc_update_340_state', - 'wc_update_340_last_active', - 'wc_update_340_db_version', - ), - '3.4.3' => array( - 'wc_update_343_cleanup_foreign_keys', - 'wc_update_343_db_version', - ), - '3.4.4' => array( - 'wc_update_344_recreate_roles', - 'wc_update_344_db_version', - ), - '3.5.0' => array( - 'wc_update_350_reviews_comment_type', - 'wc_update_350_db_version', - ), - '3.5.2' => array( - 'wc_update_352_drop_download_log_fk', - ), - '3.5.4' => array( - 'wc_update_354_modify_shop_manager_caps', - 'wc_update_354_db_version', - ), - '3.6.0' => array( - 'wc_update_360_product_lookup_tables', - 'wc_update_360_term_meta', - 'wc_update_360_downloadable_product_permissions_index', - 'wc_update_360_db_version', - ), - '3.7.0' => array( - 'wc_update_370_tax_rate_classes', - 'wc_update_370_mro_std_currency', - 'wc_update_370_db_version', - ), - '3.9.0' => array( - 'wc_update_390_move_maxmind_database', - 'wc_update_390_change_geolocation_database_update_cron', - 'wc_update_390_db_version', - ), - '4.0.0' => array( - 'wc_update_product_lookup_tables', - 'wc_update_400_increase_size_of_column', - 'wc_update_400_reset_action_scheduler_migration_status', - 'wc_update_400_db_version', - ), - '4.4.0' => array( - 'wc_update_440_insert_attribute_terms_for_variable_products', - 'wc_update_440_db_version', - ), - '4.5.0' => array( - 'wc_update_450_sanitize_coupons_code', - 'wc_update_450_db_version', - ), - '5.0.0' => array( - 'wc_update_500_fix_product_review_count', - 'wc_update_500_db_version', - ), - ); - - /** - * Hook in tabs. - */ - public static function init() { - add_action( 'init', array( __CLASS__, 'check_version' ), 5 ); - add_action( 'init', array( __CLASS__, 'manual_database_update' ), 20 ); - add_action( 'admin_init', array( __CLASS__, 'wc_admin_db_update_notice' ) ); - add_action( 'woocommerce_run_update_callback', array( __CLASS__, 'run_update_callback' ) ); - add_action( 'admin_init', array( __CLASS__, 'install_actions' ) ); - add_filter( 'plugin_action_links_' . WC_PLUGIN_BASENAME, array( __CLASS__, 'plugin_action_links' ) ); - add_filter( 'plugin_row_meta', array( __CLASS__, 'plugin_row_meta' ), 10, 2 ); - add_filter( 'wpmu_drop_tables', array( __CLASS__, 'wpmu_drop_tables' ) ); - add_filter( 'cron_schedules', array( __CLASS__, 'cron_schedules' ) ); - } - - /** - * Check WooCommerce version and run the updater is required. - * - * This check is done on all requests and runs if the versions do not match. - */ - public static function check_version() { - if ( ! Constants::is_defined( 'IFRAME_REQUEST' ) && version_compare( get_option( 'woocommerce_version' ), WC()->version, '<' ) ) { - self::install(); - do_action( 'woocommerce_updated' ); - } - } - - /** - * Performan manual database update when triggered by WooCommerce System Tools. - * - * @since 3.6.5 - */ - public static function manual_database_update() { - $blog_id = get_current_blog_id(); - - add_action( 'wp_' . $blog_id . '_wc_updater_cron', array( __CLASS__, 'run_manual_database_update' ) ); - } - - /** - * Add WC Admin based db update notice. - * - * @since 4.0.0 - */ - public static function wc_admin_db_update_notice() { - if ( - WC()->is_wc_admin_active() && - false !== get_option( 'woocommerce_admin_install_timestamp' ) - ) { - new WC_Notes_Run_Db_Update(); - } - } - - /** - * Run manual database update. - */ - public static function run_manual_database_update() { - self::update(); - } - - /** - * Run an update callback when triggered by ActionScheduler. - * - * @param string $update_callback Callback name. - * - * @since 3.6.0 - */ - public static function run_update_callback( $update_callback ) { - include_once dirname( __FILE__ ) . '/wc-update-functions.php'; - - if ( is_callable( $update_callback ) ) { - self::run_update_callback_start( $update_callback ); - $result = (bool) call_user_func( $update_callback ); - self::run_update_callback_end( $update_callback, $result ); - } - } - - /** - * Triggered when a callback will run. - * - * @since 3.6.0 - * @param string $callback Callback name. - */ - protected static function run_update_callback_start( $callback ) { - wc_maybe_define_constant( 'WC_UPDATING', true ); - } - - /** - * Triggered when a callback has ran. - * - * @since 3.6.0 - * @param string $callback Callback name. - * @param bool $result Return value from callback. Non-false need to run again. - */ - protected static function run_update_callback_end( $callback, $result ) { - if ( $result ) { - WC()->queue()->add( - 'woocommerce_run_update_callback', - array( - 'update_callback' => $callback, - ), - 'woocommerce-db-updates' - ); - } - } - - /** - * Install actions when a update button is clicked within the admin area. - * - * This function is hooked into admin_init to affect admin only. - */ - public static function install_actions() { - if ( ! empty( $_GET['do_update_woocommerce'] ) ) { // WPCS: input var ok. - check_admin_referer( 'wc_db_update', 'wc_db_update_nonce' ); - self::update(); - WC_Admin_Notices::add_notice( 'update', true ); - } - } - - /** - * Install WC. - */ - public static function install() { - if ( ! is_blog_installed() ) { - return; - } - - // Check if we are not already running this routine. - if ( 'yes' === get_transient( 'wc_installing' ) ) { - return; - } - - // If we made it till here nothing is running yet, lets set the transient now. - set_transient( 'wc_installing', 'yes', MINUTE_IN_SECONDS * 10 ); - wc_maybe_define_constant( 'WC_INSTALLING', true ); - - WC()->wpdb_table_fix(); - self::remove_admin_notices(); - self::create_tables(); - self::verify_base_tables(); - self::create_options(); - self::create_roles(); - self::setup_environment(); - self::create_terms(); - self::create_cron_jobs(); - self::create_files(); - self::maybe_create_pages(); - self::maybe_set_activation_transients(); - self::update_wc_version(); - self::maybe_update_db_version(); - - delete_transient( 'wc_installing' ); - - do_action( 'woocommerce_flush_rewrite_rules' ); - do_action( 'woocommerce_installed' ); - } - - /** - * Check if all the base tables are present. - * - * @param bool $modify_notice Whether to modify notice based on if all tables are present. - * @param bool $execute Whether to execute get_schema queries as well. - * - * @return array List of querues. - */ - public static function verify_base_tables( $modify_notice = true, $execute = false ) { - require_once ABSPATH . 'wp-admin/includes/upgrade.php'; - - if ( $execute ) { - self::create_tables(); - } - $queries = dbDelta( self::get_schema(), false ); - $missing_tables = array(); - foreach ( $queries as $table_name => $result ) { - if ( "Created table $table_name" === $result ) { - $missing_tables[] = $table_name; - } - } - - if ( 0 < count( $missing_tables ) ) { - if ( $modify_notice ) { - WC_Admin_Notices::add_notice( 'base_tables_missing' ); - } - update_option( 'woocommerce_schema_missing_tables', $missing_tables ); - } else { - if ( $modify_notice ) { - WC_Admin_Notices::remove_notice( 'base_tables_missing' ); - } - update_option( 'woocommerce_schema_version', WC()->db_version ); - delete_option( 'woocommerce_schema_missing_tables' ); - } - return $missing_tables; - } - - /** - * Reset any notices added to admin. - * - * @since 3.2.0 - */ - private static function remove_admin_notices() { - include_once dirname( __FILE__ ) . '/admin/class-wc-admin-notices.php'; - WC_Admin_Notices::remove_all_notices(); - } - - /** - * Setup WC environment - post types, taxonomies, endpoints. - * - * @since 3.2.0 - */ - private static function setup_environment() { - WC_Post_types::register_post_types(); - WC_Post_types::register_taxonomies(); - WC()->query->init_query_vars(); - WC()->query->add_endpoints(); - WC_API::add_endpoint(); - WC_Auth::add_endpoint(); - } - - /** - * Is this a brand new WC install? - * - * A brand new install has no version yet. Also treat empty installs as 'new'. - * - * @since 3.2.0 - * @return boolean - */ - public static function is_new_install() { - $product_count = array_sum( (array) wp_count_posts( 'product' ) ); - - return is_null( get_option( 'woocommerce_version', null ) ) || ( 0 === $product_count && -1 === wc_get_page_id( 'shop' ) ); - } - - /** - * Is a DB update needed? - * - * @since 3.2.0 - * @return boolean - */ - public static function needs_db_update() { - $current_db_version = get_option( 'woocommerce_db_version', null ); - $updates = self::get_db_update_callbacks(); - $update_versions = array_keys( $updates ); - usort( $update_versions, 'version_compare' ); - - return ! is_null( $current_db_version ) && version_compare( $current_db_version, end( $update_versions ), '<' ); - } - - /** - * See if we need to set redirect transients for activation or not. - * - * @since 4.6.0 - */ - private static function maybe_set_activation_transients() { - if ( self::is_new_install() ) { - set_transient( '_wc_activation_redirect', 1, 30 ); - } - } - - /** - * See if we need to show or run database updates during install. - * - * @since 3.2.0 - */ - private static function maybe_update_db_version() { - if ( self::needs_db_update() ) { - if ( apply_filters( 'woocommerce_enable_auto_update_db', false ) ) { - self::update(); - } else { - WC_Admin_Notices::add_notice( 'update', true ); - } - } else { - self::update_db_version(); - } - } - - /** - * Update WC version to current. - */ - private static function update_wc_version() { - update_option( 'woocommerce_version', WC()->version ); - } - - /** - * Get list of DB update callbacks. - * - * @since 3.0.0 - * @return array - */ - public static function get_db_update_callbacks() { - return self::$db_updates; - } - - /** - * Push all needed DB updates to the queue for processing. - */ - private static function update() { - $current_db_version = get_option( 'woocommerce_db_version' ); - $loop = 0; - - foreach ( self::get_db_update_callbacks() as $version => $update_callbacks ) { - if ( version_compare( $current_db_version, $version, '<' ) ) { - foreach ( $update_callbacks as $update_callback ) { - WC()->queue()->schedule_single( - time() + $loop, - 'woocommerce_run_update_callback', - array( - 'update_callback' => $update_callback, - ), - 'woocommerce-db-updates' - ); - $loop++; - } - } - } - } - - /** - * Update DB version to current. - * - * @param string|null $version New WooCommerce DB version or null. - */ - public static function update_db_version( $version = null ) { - update_option( 'woocommerce_db_version', is_null( $version ) ? WC()->version : $version ); - } - - /** - * Add more cron schedules. - * - * @param array $schedules List of WP scheduled cron jobs. - * - * @return array - */ - public static function cron_schedules( $schedules ) { - $schedules['monthly'] = array( - 'interval' => 2635200, - 'display' => __( 'Monthly', 'woocommerce' ), - ); - $schedules['fifteendays'] = array( - 'interval' => 1296000, - 'display' => __( 'Every 15 Days', 'woocommerce' ), - ); - return $schedules; - } - - /** - * Create cron jobs (clear them first). - */ - private static function create_cron_jobs() { - wp_clear_scheduled_hook( 'woocommerce_scheduled_sales' ); - wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' ); - wp_clear_scheduled_hook( 'woocommerce_cleanup_sessions' ); - wp_clear_scheduled_hook( 'woocommerce_cleanup_personal_data' ); - wp_clear_scheduled_hook( 'woocommerce_cleanup_logs' ); - wp_clear_scheduled_hook( 'woocommerce_geoip_updater' ); - wp_clear_scheduled_hook( 'woocommerce_tracker_send_event' ); - - $ve = get_option( 'gmt_offset' ) > 0 ? '-' : '+'; - - wp_schedule_event( strtotime( '00:00 tomorrow ' . $ve . absint( get_option( 'gmt_offset' ) ) . ' HOURS' ), 'daily', 'woocommerce_scheduled_sales' ); - - $held_duration = get_option( 'woocommerce_hold_stock_minutes', '60' ); - - if ( '' !== $held_duration ) { - $cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $held_duration ) ); - wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' ); - } - - // Delay the first run of `woocommerce_cleanup_personal_data` by 10 seconds - // so it doesn't occur in the same request. WooCommerce Admin also schedules - // a daily cron that gets lost due to a race condition. WC_Privacy's background - // processing instance updates the cron schedule from within a cron job. - wp_schedule_event( time() + 10, 'daily', 'woocommerce_cleanup_personal_data' ); - wp_schedule_event( time() + ( 3 * HOUR_IN_SECONDS ), 'daily', 'woocommerce_cleanup_logs' ); - wp_schedule_event( time() + ( 6 * HOUR_IN_SECONDS ), 'twicedaily', 'woocommerce_cleanup_sessions' ); - wp_schedule_event( time() + MINUTE_IN_SECONDS, 'fifteendays', 'woocommerce_geoip_updater' ); - wp_schedule_event( time() + 10, apply_filters( 'woocommerce_tracker_event_recurrence', 'daily' ), 'woocommerce_tracker_send_event' ); - } - - /** - * Create pages on installation. - */ - public static function maybe_create_pages() { - if ( empty( get_option( 'woocommerce_db_version' ) ) ) { - self::create_pages(); - } - } - - /** - * Create pages that the plugin relies on, storing page IDs in variables. - */ - public static function create_pages() { - include_once dirname( __FILE__ ) . '/admin/wc-admin-functions.php'; - - $pages = apply_filters( - 'woocommerce_create_pages', - array( - 'shop' => array( - 'name' => _x( 'shop', 'Page slug', 'woocommerce' ), - 'title' => _x( 'Shop', 'Page title', 'woocommerce' ), - 'content' => '', - ), - 'cart' => array( - 'name' => _x( 'cart', 'Page slug', 'woocommerce' ), - 'title' => _x( 'Cart', 'Page title', 'woocommerce' ), - 'content' => '[' . apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) . ']', - ), - 'checkout' => array( - 'name' => _x( 'checkout', 'Page slug', 'woocommerce' ), - 'title' => _x( 'Checkout', 'Page title', 'woocommerce' ), - 'content' => '[' . apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) . ']', - ), - 'myaccount' => array( - 'name' => _x( 'my-account', 'Page slug', 'woocommerce' ), - 'title' => _x( 'My account', 'Page title', 'woocommerce' ), - 'content' => '[' . apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) . ']', - ), - ) - ); - - foreach ( $pages as $key => $page ) { - wc_create_page( esc_sql( $page['name'] ), 'woocommerce_' . $key . '_page_id', $page['title'], $page['content'], ! empty( $page['parent'] ) ? wc_get_page_id( $page['parent'] ) : '' ); - } - } - - /** - * Default options. - * - * Sets up the default options used on the settings page. - */ - private static function create_options() { - // Include settings so that we can run through defaults. - include_once dirname( __FILE__ ) . '/admin/class-wc-admin-settings.php'; - - $settings = WC_Admin_Settings::get_settings_pages(); - - foreach ( $settings as $section ) { - if ( ! method_exists( $section, 'get_settings' ) ) { - continue; - } - $subsections = array_unique( array_merge( array( '' ), array_keys( $section->get_sections() ) ) ); - - foreach ( $subsections as $subsection ) { - foreach ( $section->get_settings( $subsection ) as $value ) { - if ( isset( $value['default'] ) && isset( $value['id'] ) ) { - $autoload = isset( $value['autoload'] ) ? (bool) $value['autoload'] : true; - add_option( $value['id'], $value['default'], '', ( $autoload ? 'yes' : 'no' ) ); - } - } - } - } - - // Define other defaults if not in setting screens. - add_option( 'woocommerce_single_image_width', '600', '', 'yes' ); - add_option( 'woocommerce_thumbnail_image_width', '300', '', 'yes' ); - add_option( 'woocommerce_checkout_highlight_required_fields', 'yes', '', 'yes' ); - add_option( 'woocommerce_demo_store', 'no', '', 'no' ); - - // Define initial tax classes. - WC_Tax::create_tax_class( __( 'Reduced rate', 'woocommerce' ) ); - WC_Tax::create_tax_class( __( 'Zero rate', 'woocommerce' ) ); - } - - /** - * Add the default terms for WC taxonomies - product types and order statuses. Modify this at your own risk. - */ - public static function create_terms() { - $taxonomies = array( - 'product_type' => array( - 'simple', - 'grouped', - 'variable', - 'external', - ), - 'product_visibility' => array( - 'exclude-from-search', - 'exclude-from-catalog', - 'featured', - 'outofstock', - 'rated-1', - 'rated-2', - 'rated-3', - 'rated-4', - 'rated-5', - ), - ); - - foreach ( $taxonomies as $taxonomy => $terms ) { - foreach ( $terms as $term ) { - if ( ! get_term_by( 'name', $term, $taxonomy ) ) { // @codingStandardsIgnoreLine. - wp_insert_term( $term, $taxonomy ); - } - } - } - - $woocommerce_default_category = (int) get_option( 'default_product_cat', 0 ); - - if ( ! $woocommerce_default_category || ! term_exists( $woocommerce_default_category, 'product_cat' ) ) { - $default_product_cat_id = 0; - $default_product_cat_slug = sanitize_title( _x( 'Uncategorized', 'Default category slug', 'woocommerce' ) ); - $default_product_cat = get_term_by( 'slug', $default_product_cat_slug, 'product_cat' ); // @codingStandardsIgnoreLine. - - if ( $default_product_cat ) { - $default_product_cat_id = absint( $default_product_cat->term_taxonomy_id ); - } else { - $result = wp_insert_term( _x( 'Uncategorized', 'Default category slug', 'woocommerce' ), 'product_cat', array( 'slug' => $default_product_cat_slug ) ); - - if ( ! is_wp_error( $result ) && ! empty( $result['term_taxonomy_id'] ) ) { - $default_product_cat_id = absint( $result['term_taxonomy_id'] ); - } - } - - if ( $default_product_cat_id ) { - update_option( 'default_product_cat', $default_product_cat_id ); - } - } - } - - /** - * Set up the database tables which the plugin needs to function. - * WARNING: If you are modifying this method, make sure that its safe to call regardless of the state of database. - * - * This is called from `install` method and is executed in-sync when WC is installed or updated. This can also be called optionally from `verify_base_tables`. - * - * TODO: Add all crucial tables that we have created from workers in the past. - * - * Tables: - * woocommerce_attribute_taxonomies - Table for storing attribute taxonomies - these are user defined - * woocommerce_downloadable_product_permissions - Table for storing user and guest download permissions. - * KEY(order_id, product_id, download_id) used for organizing downloads on the My Account page - * woocommerce_order_items - Order line items are stored in a table to make them easily queryable for reports - * woocommerce_order_itemmeta - Order line item meta is stored in a table for storing extra data. - * woocommerce_tax_rates - Tax Rates are stored inside 2 tables making tax queries simple and efficient. - * woocommerce_tax_rate_locations - Each rate can be applied to more than one postcode/city hence the second table. - */ - private static function create_tables() { - global $wpdb; - - $wpdb->hide_errors(); - - require_once ABSPATH . 'wp-admin/includes/upgrade.php'; - - /** - * Before updating with DBDELTA, remove any primary keys which could be - * modified due to schema updates. - */ - if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_downloadable_product_permissions';" ) ) { - if ( ! $wpdb->get_var( "SHOW COLUMNS FROM `{$wpdb->prefix}woocommerce_downloadable_product_permissions` LIKE 'permission_id';" ) ) { - $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions DROP PRIMARY KEY, ADD `permission_id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT;" ); - } - } - - /** - * Change wp_woocommerce_sessions schema to use a bigint auto increment field instead of char(32) field as - * the primary key as it is not a good practice to use a char(32) field as the primary key of a table and as - * there were reports of issues with this table (see https://github.com/woocommerce/woocommerce/issues/20912). - * - * This query needs to run before dbDelta() as this WP function is not able to handle primary key changes - * (see https://github.com/woocommerce/woocommerce/issues/21534 and https://core.trac.wordpress.org/ticket/40357). - */ - if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_sessions'" ) ) { - if ( ! $wpdb->get_var( "SHOW KEYS FROM {$wpdb->prefix}woocommerce_sessions WHERE Key_name = 'PRIMARY' AND Column_name = 'session_id'" ) ) { - $wpdb->query( - "ALTER TABLE `{$wpdb->prefix}woocommerce_sessions` DROP PRIMARY KEY, DROP KEY `session_id`, ADD PRIMARY KEY(`session_id`), ADD UNIQUE KEY(`session_key`)" - ); - } - } - - dbDelta( self::get_schema() ); - - $index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->comments} WHERE column_name = 'comment_type' and key_name = 'woo_idx_comment_type'" ); - - if ( is_null( $index_exists ) ) { - // Add an index to the field comment_type to improve the response time of the query - // used by WC_Comments::wp_count_comments() to get the number of comments by type. - $wpdb->query( "ALTER TABLE {$wpdb->comments} ADD INDEX woo_idx_comment_type (comment_type)" ); - } - - // Get tables data types and check it matches before adding constraint. - $download_log_columns = $wpdb->get_results( "SHOW COLUMNS FROM {$wpdb->prefix}wc_download_log WHERE Field = 'permission_id'", ARRAY_A ); - $download_log_column_type = ''; - if ( isset( $download_log_columns[0]['Type'] ) ) { - $download_log_column_type = $download_log_columns[0]['Type']; - } - - $download_permissions_columns = $wpdb->get_results( "SHOW COLUMNS FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE Field = 'permission_id'", ARRAY_A ); - $download_permissions_column_type = ''; - if ( isset( $download_permissions_columns[0]['Type'] ) ) { - $download_permissions_column_type = $download_permissions_columns[0]['Type']; - } - - // Add constraint to download logs if the columns matches. - if ( ! empty( $download_permissions_column_type ) && ! empty( $download_log_column_type ) && $download_permissions_column_type === $download_log_column_type ) { - $fk_result = $wpdb->get_row( "SHOW CREATE TABLE {$wpdb->prefix}wc_download_log" ); - if ( false === strpos( $fk_result->{'Create Table'}, "fk_{$wpdb->prefix}wc_download_log_permission_id" ) ) { - $wpdb->query( - "ALTER TABLE `{$wpdb->prefix}wc_download_log` - ADD CONSTRAINT `fk_{$wpdb->prefix}wc_download_log_permission_id` - FOREIGN KEY (`permission_id`) - REFERENCES `{$wpdb->prefix}woocommerce_downloadable_product_permissions` (`permission_id`) ON DELETE CASCADE;" - ); - } - } - - // Clear table caches. - delete_transient( 'wc_attribute_taxonomies' ); - } - - /** - * Get Table schema. - * - * See https://github.com/woocommerce/woocommerce/wiki/Database-Description/ - * - * A note on indexes; Indexes have a maximum size of 767 bytes. Historically, we haven't need to be concerned about that. - * As of WordPress 4.2, however, we moved to utf8mb4, which uses 4 bytes per character. This means that an index which - * used to have room for floor(767/3) = 255 characters, now only has room for floor(767/4) = 191 characters. - * - * Changing indexes may cause duplicate index notices in logs due to https://core.trac.wordpress.org/ticket/34870 but dropping - * indexes first causes too much load on some servers/larger DB. - * - * When adding or removing a table, make sure to update the list of tables in WC_Install::get_tables(). - * - * @return string - */ - private static function get_schema() { - global $wpdb; - - $collate = ''; - - if ( $wpdb->has_cap( 'collation' ) ) { - $collate = $wpdb->get_charset_collate(); - } - - /* - * Indexes have a maximum size of 767 bytes. Historically, we haven't need to be concerned about that. - * As of WP 4.2, however, they moved to utf8mb4, which uses 4 bytes per character. This means that an index which - * used to have room for floor(767/3) = 255 characters, now only has room for floor(767/4) = 191 characters. - */ - $max_index_length = 191; - - $tables = " -CREATE TABLE {$wpdb->prefix}woocommerce_sessions ( - session_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - session_key char(32) NOT NULL, - session_value longtext NOT NULL, - session_expiry BIGINT UNSIGNED NOT NULL, - PRIMARY KEY (session_id), - UNIQUE KEY session_key (session_key) -) $collate; -CREATE TABLE {$wpdb->prefix}woocommerce_api_keys ( - key_id BIGINT UNSIGNED NOT NULL auto_increment, - user_id BIGINT UNSIGNED NOT NULL, - description varchar(200) NULL, - permissions varchar(10) NOT NULL, - consumer_key char(64) NOT NULL, - consumer_secret char(43) NOT NULL, - nonces longtext NULL, - truncated_key char(7) NOT NULL, - last_access datetime NULL default null, - PRIMARY KEY (key_id), - KEY consumer_key (consumer_key), - KEY consumer_secret (consumer_secret) -) $collate; -CREATE TABLE {$wpdb->prefix}woocommerce_attribute_taxonomies ( - attribute_id BIGINT UNSIGNED NOT NULL auto_increment, - attribute_name varchar(200) NOT NULL, - attribute_label varchar(200) NULL, - attribute_type varchar(20) NOT NULL, - attribute_orderby varchar(20) NOT NULL, - attribute_public int(1) NOT NULL DEFAULT 1, - PRIMARY KEY (attribute_id), - KEY attribute_name (attribute_name(20)) -) $collate; -CREATE TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions ( - permission_id BIGINT UNSIGNED NOT NULL auto_increment, - download_id varchar(36) NOT NULL, - product_id BIGINT UNSIGNED NOT NULL, - order_id BIGINT UNSIGNED NOT NULL DEFAULT 0, - order_key varchar(200) NOT NULL, - user_email varchar(200) NOT NULL, - user_id BIGINT UNSIGNED NULL, - downloads_remaining varchar(9) NULL, - access_granted datetime NOT NULL default '0000-00-00 00:00:00', - access_expires datetime NULL default null, - download_count BIGINT UNSIGNED NOT NULL DEFAULT 0, - PRIMARY KEY (permission_id), - KEY download_order_key_product (product_id,order_id,order_key(16),download_id), - KEY download_order_product (download_id,order_id,product_id), - KEY order_id (order_id), - KEY user_order_remaining_expires (user_id,order_id,downloads_remaining,access_expires) -) $collate; -CREATE TABLE {$wpdb->prefix}woocommerce_order_items ( - order_item_id BIGINT UNSIGNED NOT NULL auto_increment, - order_item_name TEXT NOT NULL, - order_item_type varchar(200) NOT NULL DEFAULT '', - order_id BIGINT UNSIGNED NOT NULL, - PRIMARY KEY (order_item_id), - KEY order_id (order_id) -) $collate; -CREATE TABLE {$wpdb->prefix}woocommerce_order_itemmeta ( - meta_id BIGINT UNSIGNED NOT NULL auto_increment, - order_item_id BIGINT UNSIGNED NOT NULL, - meta_key varchar(255) default NULL, - meta_value longtext NULL, - PRIMARY KEY (meta_id), - KEY order_item_id (order_item_id), - KEY meta_key (meta_key(32)) -) $collate; -CREATE TABLE {$wpdb->prefix}woocommerce_tax_rates ( - tax_rate_id BIGINT UNSIGNED NOT NULL auto_increment, - tax_rate_country varchar(2) NOT NULL DEFAULT '', - tax_rate_state varchar(200) NOT NULL DEFAULT '', - tax_rate varchar(8) NOT NULL DEFAULT '', - tax_rate_name varchar(200) NOT NULL DEFAULT '', - tax_rate_priority BIGINT UNSIGNED NOT NULL, - tax_rate_compound int(1) NOT NULL DEFAULT 0, - tax_rate_shipping int(1) NOT NULL DEFAULT 1, - tax_rate_order BIGINT UNSIGNED NOT NULL, - tax_rate_class varchar(200) NOT NULL DEFAULT '', - PRIMARY KEY (tax_rate_id), - KEY tax_rate_country (tax_rate_country), - KEY tax_rate_state (tax_rate_state(2)), - KEY tax_rate_class (tax_rate_class(10)), - KEY tax_rate_priority (tax_rate_priority) -) $collate; -CREATE TABLE {$wpdb->prefix}woocommerce_tax_rate_locations ( - location_id BIGINT UNSIGNED NOT NULL auto_increment, - location_code varchar(200) NOT NULL, - tax_rate_id BIGINT UNSIGNED NOT NULL, - location_type varchar(40) NOT NULL, - PRIMARY KEY (location_id), - KEY tax_rate_id (tax_rate_id), - KEY location_type_code (location_type(10),location_code(20)) -) $collate; -CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zones ( - zone_id BIGINT UNSIGNED NOT NULL auto_increment, - zone_name varchar(200) NOT NULL, - zone_order BIGINT UNSIGNED NOT NULL, - PRIMARY KEY (zone_id) -) $collate; -CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zone_locations ( - location_id BIGINT UNSIGNED NOT NULL auto_increment, - zone_id BIGINT UNSIGNED NOT NULL, - location_code varchar(200) NOT NULL, - location_type varchar(40) NOT NULL, - PRIMARY KEY (location_id), - KEY location_id (location_id), - KEY location_type_code (location_type(10),location_code(20)) -) $collate; -CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zone_methods ( - zone_id BIGINT UNSIGNED NOT NULL, - instance_id BIGINT UNSIGNED NOT NULL auto_increment, - method_id varchar(200) NOT NULL, - method_order BIGINT UNSIGNED NOT NULL, - is_enabled tinyint(1) NOT NULL DEFAULT '1', - PRIMARY KEY (instance_id) -) $collate; -CREATE TABLE {$wpdb->prefix}woocommerce_payment_tokens ( - token_id BIGINT UNSIGNED NOT NULL auto_increment, - gateway_id varchar(200) NOT NULL, - token text NOT NULL, - user_id BIGINT UNSIGNED NOT NULL DEFAULT '0', - type varchar(200) NOT NULL, - is_default tinyint(1) NOT NULL DEFAULT '0', - PRIMARY KEY (token_id), - KEY user_id (user_id) -) $collate; -CREATE TABLE {$wpdb->prefix}woocommerce_payment_tokenmeta ( - meta_id BIGINT UNSIGNED NOT NULL auto_increment, - payment_token_id BIGINT UNSIGNED NOT NULL, - meta_key varchar(255) NULL, - meta_value longtext NULL, - PRIMARY KEY (meta_id), - KEY payment_token_id (payment_token_id), - KEY meta_key (meta_key(32)) -) $collate; -CREATE TABLE {$wpdb->prefix}woocommerce_log ( - log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - timestamp datetime NOT NULL, - level smallint(4) NOT NULL, - source varchar(200) NOT NULL, - message longtext NOT NULL, - context longtext NULL, - PRIMARY KEY (log_id), - KEY level (level) -) $collate; -CREATE TABLE {$wpdb->prefix}wc_webhooks ( - webhook_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - status varchar(200) NOT NULL, - name text NOT NULL, - user_id BIGINT UNSIGNED NOT NULL, - delivery_url text NOT NULL, - secret text NOT NULL, - topic varchar(200) NOT NULL, - date_created datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - date_created_gmt datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - date_modified datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - date_modified_gmt datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - api_version smallint(4) NOT NULL, - failure_count smallint(10) NOT NULL DEFAULT '0', - pending_delivery tinyint(1) NOT NULL DEFAULT '0', - PRIMARY KEY (webhook_id), - KEY user_id (user_id) -) $collate; -CREATE TABLE {$wpdb->prefix}wc_download_log ( - download_log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - timestamp datetime NOT NULL, - permission_id BIGINT UNSIGNED NOT NULL, - user_id BIGINT UNSIGNED NULL, - user_ip_address VARCHAR(100) NULL DEFAULT '', - PRIMARY KEY (download_log_id), - KEY permission_id (permission_id), - KEY timestamp (timestamp) -) $collate; -CREATE TABLE {$wpdb->prefix}wc_product_meta_lookup ( - `product_id` bigint(20) NOT NULL, - `sku` varchar(100) NULL default '', - `virtual` tinyint(1) NULL default 0, - `downloadable` tinyint(1) NULL default 0, - `min_price` decimal(19,4) NULL default NULL, - `max_price` decimal(19,4) NULL default NULL, - `onsale` tinyint(1) NULL default 0, - `stock_quantity` double NULL default NULL, - `stock_status` varchar(100) NULL default 'instock', - `rating_count` bigint(20) NULL default 0, - `average_rating` decimal(3,2) NULL default 0.00, - `total_sales` bigint(20) NULL default 0, - `tax_status` varchar(100) NULL default 'taxable', - `tax_class` varchar(100) NULL default '', - PRIMARY KEY (`product_id`), - KEY `virtual` (`virtual`), - KEY `downloadable` (`downloadable`), - KEY `stock_status` (`stock_status`), - KEY `stock_quantity` (`stock_quantity`), - KEY `onsale` (`onsale`), - KEY min_max_price (`min_price`, `max_price`) -) $collate; -CREATE TABLE {$wpdb->prefix}wc_tax_rate_classes ( - tax_rate_class_id BIGINT UNSIGNED NOT NULL auto_increment, - name varchar(200) NOT NULL DEFAULT '', - slug varchar(200) NOT NULL DEFAULT '', - PRIMARY KEY (tax_rate_class_id), - UNIQUE KEY slug (slug($max_index_length)) -) $collate; -CREATE TABLE {$wpdb->prefix}wc_reserved_stock ( - `order_id` bigint(20) NOT NULL, - `product_id` bigint(20) NOT NULL, - `stock_quantity` double NOT NULL DEFAULT 0, - `timestamp` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - `expires` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - PRIMARY KEY (`order_id`, `product_id`) -) $collate; - "; - - return $tables; - } - - /** - * Return a list of WooCommerce tables. Used to make sure all WC tables are dropped when uninstalling the plugin - * in a single site or multi site environment. - * - * @return array WC tables. - */ - public static function get_tables() { - global $wpdb; - - $tables = array( - "{$wpdb->prefix}wc_download_log", - "{$wpdb->prefix}wc_product_meta_lookup", - "{$wpdb->prefix}wc_tax_rate_classes", - "{$wpdb->prefix}wc_webhooks", - "{$wpdb->prefix}woocommerce_api_keys", - "{$wpdb->prefix}woocommerce_attribute_taxonomies", - "{$wpdb->prefix}woocommerce_downloadable_product_permissions", - "{$wpdb->prefix}woocommerce_log", - "{$wpdb->prefix}woocommerce_order_itemmeta", - "{$wpdb->prefix}woocommerce_order_items", - "{$wpdb->prefix}woocommerce_payment_tokenmeta", - "{$wpdb->prefix}woocommerce_payment_tokens", - "{$wpdb->prefix}woocommerce_sessions", - "{$wpdb->prefix}woocommerce_shipping_zone_locations", - "{$wpdb->prefix}woocommerce_shipping_zone_methods", - "{$wpdb->prefix}woocommerce_shipping_zones", - "{$wpdb->prefix}woocommerce_tax_rate_locations", - "{$wpdb->prefix}woocommerce_tax_rates", - "{$wpdb->prefix}wc_reserved_stock", - ); - - /** - * Filter the list of known WooCommerce tables. - * - * If WooCommerce plugins need to add new tables, they can inject them here. - * - * @param array $tables An array of WooCommerce-specific database table names. - */ - $tables = apply_filters( 'woocommerce_install_get_tables', $tables ); - - return $tables; - } - - /** - * Drop WooCommerce tables. - * - * @return void - */ - public static function drop_tables() { - global $wpdb; - - $tables = self::get_tables(); - - foreach ( $tables as $table ) { - $wpdb->query( "DROP TABLE IF EXISTS {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - } - } - - /** - * Uninstall tables when MU blog is deleted. - * - * @param array $tables List of tables that will be deleted by WP. - * - * @return string[] - */ - public static function wpmu_drop_tables( $tables ) { - return array_merge( $tables, self::get_tables() ); - } - - /** - * Create roles and capabilities. - */ - public static function create_roles() { - global $wp_roles; - - if ( ! class_exists( 'WP_Roles' ) ) { - return; - } - - if ( ! isset( $wp_roles ) ) { - $wp_roles = new WP_Roles(); // @codingStandardsIgnoreLine - } - - // Dummy gettext calls to get strings in the catalog. - /* translators: user role */ - _x( 'Customer', 'User role', 'woocommerce' ); - /* translators: user role */ - _x( 'Shop manager', 'User role', 'woocommerce' ); - - // Customer role. - add_role( - 'customer', - 'Customer', - array( - 'read' => true, - ) - ); - - // Shop manager role. - add_role( - 'shop_manager', - 'Shop manager', - array( - 'level_9' => true, - 'level_8' => true, - 'level_7' => true, - 'level_6' => true, - 'level_5' => true, - 'level_4' => true, - 'level_3' => true, - 'level_2' => true, - 'level_1' => true, - 'level_0' => true, - 'read' => true, - 'read_private_pages' => true, - 'read_private_posts' => true, - 'edit_posts' => true, - 'edit_pages' => true, - 'edit_published_posts' => true, - 'edit_published_pages' => true, - 'edit_private_pages' => true, - 'edit_private_posts' => true, - 'edit_others_posts' => true, - 'edit_others_pages' => true, - 'publish_posts' => true, - 'publish_pages' => true, - 'delete_posts' => true, - 'delete_pages' => true, - 'delete_private_pages' => true, - 'delete_private_posts' => true, - 'delete_published_pages' => true, - 'delete_published_posts' => true, - 'delete_others_posts' => true, - 'delete_others_pages' => true, - 'manage_categories' => true, - 'manage_links' => true, - 'moderate_comments' => true, - 'upload_files' => true, - 'export' => true, - 'import' => true, - 'list_users' => true, - 'edit_theme_options' => true, - ) - ); - - $capabilities = self::get_core_capabilities(); - - foreach ( $capabilities as $cap_group ) { - foreach ( $cap_group as $cap ) { - $wp_roles->add_cap( 'shop_manager', $cap ); - $wp_roles->add_cap( 'administrator', $cap ); - } - } - } - - /** - * Get capabilities for WooCommerce - these are assigned to admin/shop manager during installation or reset. - * - * @return array - */ - public static function get_core_capabilities() { - $capabilities = array(); - - $capabilities['core'] = array( - 'manage_woocommerce', - 'view_woocommerce_reports', - ); - - $capability_types = array( 'product', 'shop_order', 'shop_coupon' ); - - foreach ( $capability_types as $capability_type ) { - - $capabilities[ $capability_type ] = array( - // Post type. - "edit_{$capability_type}", - "read_{$capability_type}", - "delete_{$capability_type}", - "edit_{$capability_type}s", - "edit_others_{$capability_type}s", - "publish_{$capability_type}s", - "read_private_{$capability_type}s", - "delete_{$capability_type}s", - "delete_private_{$capability_type}s", - "delete_published_{$capability_type}s", - "delete_others_{$capability_type}s", - "edit_private_{$capability_type}s", - "edit_published_{$capability_type}s", - - // Terms. - "manage_{$capability_type}_terms", - "edit_{$capability_type}_terms", - "delete_{$capability_type}_terms", - "assign_{$capability_type}_terms", - ); - } - - return $capabilities; - } - - /** - * Remove WooCommerce roles. - */ - public static function remove_roles() { - global $wp_roles; - - if ( ! class_exists( 'WP_Roles' ) ) { - return; - } - - if ( ! isset( $wp_roles ) ) { - $wp_roles = new WP_Roles(); // @codingStandardsIgnoreLine - } - - $capabilities = self::get_core_capabilities(); - - foreach ( $capabilities as $cap_group ) { - foreach ( $cap_group as $cap ) { - $wp_roles->remove_cap( 'shop_manager', $cap ); - $wp_roles->remove_cap( 'administrator', $cap ); - } - } - - remove_role( 'customer' ); - remove_role( 'shop_manager' ); - } - - /** - * Create files/directories. - */ - private static function create_files() { - // Bypass if filesystem is read-only and/or non-standard upload system is used. - if ( apply_filters( 'woocommerce_install_skip_create_files', false ) ) { - return; - } - - // Install files and folders for uploading files and prevent hotlinking. - $upload_dir = wp_get_upload_dir(); - $download_method = get_option( 'woocommerce_file_download_method', 'force' ); - - $files = array( - array( - 'base' => $upload_dir['basedir'] . '/woocommerce_uploads', - 'file' => 'index.html', - 'content' => '', - ), - array( - 'base' => WC_LOG_DIR, - 'file' => '.htaccess', - 'content' => 'deny from all', - ), - array( - 'base' => WC_LOG_DIR, - 'file' => 'index.html', - 'content' => '', - ), - array( - 'base' => $upload_dir['basedir'] . '/woocommerce_uploads', - 'file' => '.htaccess', - 'content' => 'redirect' === $download_method ? 'Options -Indexes' : 'deny from all', - ), - ); - - foreach ( $files as $file ) { - if ( wp_mkdir_p( $file['base'] ) && ! file_exists( trailingslashit( $file['base'] ) . $file['file'] ) ) { - $file_handle = @fopen( trailingslashit( $file['base'] ) . $file['file'], 'wb' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen - if ( $file_handle ) { - fwrite( $file_handle, $file['content'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite - fclose( $file_handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose - } - } - } - - // Create attachment for placeholders. - self::create_placeholder_image(); - } - - /** - * Create a placeholder image in the media library. - * - * @since 3.5.0 - */ - private static function create_placeholder_image() { - $placeholder_image = get_option( 'woocommerce_placeholder_image', 0 ); - - // Validate current setting if set. If set, return. - if ( ! empty( $placeholder_image ) ) { - if ( ! is_numeric( $placeholder_image ) ) { - return; - } elseif ( $placeholder_image && wp_attachment_is_image( $placeholder_image ) ) { - return; - } - } - - $upload_dir = wp_upload_dir(); - $source = WC()->plugin_path() . '/assets/images/placeholder-attachment.png'; - $filename = $upload_dir['basedir'] . '/woocommerce-placeholder.png'; - - if ( ! file_exists( $filename ) ) { - copy( $source, $filename ); // @codingStandardsIgnoreLine. - } - - if ( ! file_exists( $filename ) ) { - update_option( 'woocommerce_placeholder_image', 0 ); - return; - } - - $filetype = wp_check_filetype( basename( $filename ), null ); - $attachment = array( - 'guid' => $upload_dir['url'] . '/' . basename( $filename ), - 'post_mime_type' => $filetype['type'], - 'post_title' => preg_replace( '/\.[^.]+$/', '', basename( $filename ) ), - 'post_content' => '', - 'post_status' => 'inherit', - ); - $attach_id = wp_insert_attachment( $attachment, $filename ); - - update_option( 'woocommerce_placeholder_image', $attach_id ); - - // Make sure that this file is included, as wp_generate_attachment_metadata() depends on it. - require_once ABSPATH . 'wp-admin/includes/image.php'; - - // Generate the metadata for the attachment, and update the database record. - $attach_data = wp_generate_attachment_metadata( $attach_id, $filename ); - wp_update_attachment_metadata( $attach_id, $attach_data ); - } - - /** - * Show action links on the plugin screen. - * - * @param mixed $links Plugin Action links. - * - * @return array - */ - public static function plugin_action_links( $links ) { - $action_links = array( - 'settings' => '' . esc_html__( 'Settings', 'woocommerce' ) . '', - ); - - return array_merge( $action_links, $links ); - } - - /** - * Show row meta on the plugin screen. - * - * @param mixed $links Plugin Row Meta. - * @param mixed $file Plugin Base file. - * - * @return array - */ - public static function plugin_row_meta( $links, $file ) { - if ( WC_PLUGIN_BASENAME !== $file ) { - return $links; - } - - $row_meta = array( - 'docs' => '' . esc_html__( 'Docs', 'woocommerce' ) . '', - 'apidocs' => '' . esc_html__( 'API docs', 'woocommerce' ) . '', - 'support' => '' . esc_html__( 'Community support', 'woocommerce' ) . '', - ); - - if ( WCConnectionHelper::is_connected() ) { - $row_meta['premium_support'] = '' . esc_html__( 'Premium support', 'woocommerce' ) . ''; - } - - return array_merge( $links, $row_meta ); - } - - /** - * Get slug from path and associate it with the path. - * - * @param array $plugins Associative array of plugin files to paths. - * @param string $key Plugin relative path. Example: woocommerce/woocommerce.php. - */ - private static function associate_plugin_file( $plugins, $key ) { - $path = explode( '/', $key ); - $filename = end( $path ); - $plugins[ $filename ] = $key; - return $plugins; - } - - /** - * Install a plugin from .org in the background via a cron job (used by - * installer - opt in). - * - * @param string $plugin_to_install_id Plugin ID. - * @param array $plugin_to_install Plugin information. - * - * @throws Exception If unable to proceed with plugin installation. - * @since 2.6.0 - */ - public static function background_installer( $plugin_to_install_id, $plugin_to_install ) { - // Explicitly clear the event. - $args = func_get_args(); - - if ( ! empty( $plugin_to_install['repo-slug'] ) ) { - require_once ABSPATH . 'wp-admin/includes/file.php'; - require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; - require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; - require_once ABSPATH . 'wp-admin/includes/plugin.php'; - - WP_Filesystem(); - - $skin = new Automatic_Upgrader_Skin(); - $upgrader = new WP_Upgrader( $skin ); - $installed_plugins = array_reduce( array_keys( get_plugins() ), array( __CLASS__, 'associate_plugin_file' ) ); - if ( empty( $installed_plugins ) ) { - $installed_plugins = array(); - } - $plugin_slug = $plugin_to_install['repo-slug']; - $plugin_file = isset( $plugin_to_install['file'] ) ? $plugin_to_install['file'] : $plugin_slug . '.php'; - $installed = false; - $activate = false; - - // See if the plugin is installed already. - if ( isset( $installed_plugins[ $plugin_file ] ) ) { - $installed = true; - $activate = ! is_plugin_active( $installed_plugins[ $plugin_file ] ); - } - - // Install this thing! - if ( ! $installed ) { - // Suppress feedback. - ob_start(); - - try { - $plugin_information = plugins_api( - 'plugin_information', - array( - 'slug' => $plugin_slug, - 'fields' => array( - 'short_description' => false, - 'sections' => false, - 'requires' => false, - 'rating' => false, - 'ratings' => false, - 'downloaded' => false, - 'last_updated' => false, - 'added' => false, - 'tags' => false, - 'homepage' => false, - 'donate_link' => false, - 'author_profile' => false, - 'author' => false, - ), - ) - ); - - if ( is_wp_error( $plugin_information ) ) { - throw new Exception( $plugin_information->get_error_message() ); - } - - $package = $plugin_information->download_link; - $download = $upgrader->download_package( $package ); - - if ( is_wp_error( $download ) ) { - throw new Exception( $download->get_error_message() ); - } - - $working_dir = $upgrader->unpack_package( $download, true ); - - if ( is_wp_error( $working_dir ) ) { - throw new Exception( $working_dir->get_error_message() ); - } - - $result = $upgrader->install_package( - array( - 'source' => $working_dir, - 'destination' => WP_PLUGIN_DIR, - 'clear_destination' => false, - 'abort_if_destination_exists' => false, - 'clear_working' => true, - 'hook_extra' => array( - 'type' => 'plugin', - 'action' => 'install', - ), - ) - ); - - if ( is_wp_error( $result ) ) { - throw new Exception( $result->get_error_message() ); - } - - $activate = true; - - } catch ( Exception $e ) { - WC_Admin_Notices::add_custom_notice( - $plugin_to_install_id . '_install_error', - sprintf( - // translators: 1: plugin name, 2: error message, 3: URL to install plugin manually. - __( '%1$s could not be installed (%2$s). Please install it manually by clicking here.', 'woocommerce' ), - $plugin_to_install['name'], - $e->getMessage(), - esc_url( admin_url( 'index.php?wc-install-plugin-redirect=' . $plugin_slug ) ) - ) - ); - } - - // Discard feedback. - ob_end_clean(); - } - - wp_clean_plugins_cache(); - - // Activate this thing. - if ( $activate ) { - try { - add_action( 'add_option_mailchimp_woocommerce_plugin_do_activation_redirect', array( __CLASS__, 'remove_mailchimps_redirect' ), 10, 2 ); - $result = activate_plugin( $installed ? $installed_plugins[ $plugin_file ] : $plugin_slug . '/' . $plugin_file ); - - if ( is_wp_error( $result ) ) { - throw new Exception( $result->get_error_message() ); - } - } catch ( Exception $e ) { - WC_Admin_Notices::add_custom_notice( - $plugin_to_install_id . '_install_error', - sprintf( - // translators: 1: plugin name, 2: URL to WP plugin page. - __( '%1$s was installed but could not be activated. Please activate it manually by clicking here.', 'woocommerce' ), - $plugin_to_install['name'], - admin_url( 'plugins.php' ) - ) - ); - } - } - } - } - - /** - * Removes redirect added during MailChimp plugin's activation. - * - * @param string $option Option name. - * @param string $value Option value. - */ - public static function remove_mailchimps_redirect( $option, $value ) { - // Remove this action to prevent infinite looping. - remove_action( 'add_option_mailchimp_woocommerce_plugin_do_activation_redirect', array( __CLASS__, 'remove_mailchimps_redirect' ) ); - - // Update redirect back to false. - update_option( 'mailchimp_woocommerce_plugin_do_activation_redirect', false ); - } - - /** - * Install a theme from .org in the background via a cron job (used by installer - opt in). - * - * @param string $theme_slug Theme slug. - * - * @throws Exception If unable to proceed with theme installation. - * @since 3.1.0 - */ - public static function theme_background_installer( $theme_slug ) { - // Explicitly clear the event. - $args = func_get_args(); - - if ( ! empty( $theme_slug ) ) { - // Suppress feedback. - ob_start(); - - try { - $theme = wp_get_theme( $theme_slug ); - - if ( ! $theme->exists() ) { - require_once ABSPATH . 'wp-admin/includes/file.php'; - include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; - include_once ABSPATH . 'wp-admin/includes/theme.php'; - - WP_Filesystem(); - - $skin = new Automatic_Upgrader_Skin(); - $upgrader = new Theme_Upgrader( $skin ); - $api = themes_api( - 'theme_information', - array( - 'slug' => $theme_slug, - 'fields' => array( 'sections' => false ), - ) - ); - $result = $upgrader->install( $api->download_link ); - - if ( is_wp_error( $result ) ) { - throw new Exception( $result->get_error_message() ); - } elseif ( is_wp_error( $skin->result ) ) { - throw new Exception( $skin->result->get_error_message() ); - } elseif ( is_null( $result ) ) { - throw new Exception( 'Unable to connect to the filesystem. Please confirm your credentials.' ); - } - } - - switch_theme( $theme_slug ); - } catch ( Exception $e ) { - WC_Admin_Notices::add_custom_notice( - $theme_slug . '_install_error', - sprintf( - // translators: 1: theme slug, 2: error message, 3: URL to install theme manually. - __( '%1$s could not be installed (%2$s). Please install it manually by clicking here.', 'woocommerce' ), - $theme_slug, - $e->getMessage(), - esc_url( admin_url( 'update.php?action=install-theme&theme=' . $theme_slug . '&_wpnonce=' . wp_create_nonce( 'install-theme_' . $theme_slug ) ) ) - ) - ); - } - - // Discard feedback. - ob_end_clean(); - } - } -} - -WC_Install::init(); diff --git a/includes/class-wc-logger.php b/includes/class-wc-logger.php deleted file mode 100644 index 040dd580529..00000000000 --- a/includes/class-wc-logger.php +++ /dev/null @@ -1,303 +0,0 @@ -' . esc_html( is_object( $handler ) ? get_class( $handler ) : $handler ) . '', - 'WC_Log_Handler_Interface' - ), - '3.0' - ); - } - } - } - - // Support the constant as long as a valid log level has been set for it. - if ( null === $threshold ) { - $threshold = Constants::get_constant( 'WC_LOG_THRESHOLD' ); - if ( null !== $threshold && ! WC_Log_Levels::is_valid_level( $threshold ) ) { - $threshold = null; - } - } - - if ( null !== $threshold ) { - $threshold = WC_Log_Levels::get_level_severity( $threshold ); - } - - $this->handlers = $register_handlers; - $this->threshold = $threshold; - } - - /** - * Determine whether to handle or ignore log. - * - * @param string $level emergency|alert|critical|error|warning|notice|info|debug. - * @return bool True if the log should be handled. - */ - protected function should_handle( $level ) { - if ( null === $this->threshold ) { - return true; - } - return $this->threshold <= WC_Log_Levels::get_level_severity( $level ); - } - - /** - * Add a log entry. - * - * This is not the preferred method for adding log messages. Please use log() or any one of - * the level methods (debug(), info(), etc.). This method may be deprecated in the future. - * - * @param string $handle File handle. - * @param string $message Message to log. - * @param string $level Logging level. - * @return bool - */ - public function add( $handle, $message, $level = WC_Log_Levels::NOTICE ) { - $message = apply_filters( 'woocommerce_logger_add_message', $message, $handle ); - $this->log( - $level, - $message, - array( - 'source' => $handle, - '_legacy' => true, - ) - ); - wc_do_deprecated_action( 'woocommerce_log_add', array( $handle, $message ), '3.0', 'This action has been deprecated with no alternative.' ); - return true; - } - - /** - * Add a log entry. - * - * @param string $level One of the following: - * 'emergency': System is unusable. - * 'alert': Action must be taken immediately. - * 'critical': Critical conditions. - * 'error': Error conditions. - * 'warning': Warning conditions. - * 'notice': Normal but significant condition. - * 'info': Informational messages. - * 'debug': Debug-level messages. - * @param string $message Log message. - * @param array $context Optional. Additional information for log handlers. - */ - public function log( $level, $message, $context = array() ) { - if ( ! WC_Log_Levels::is_valid_level( $level ) ) { - /* translators: 1: WC_Logger::log 2: level */ - wc_doing_it_wrong( __METHOD__, sprintf( __( '%1$s was called with an invalid level "%2$s".', 'woocommerce' ), 'WC_Logger::log', $level ), '3.0' ); - } - - if ( $this->should_handle( $level ) ) { - $timestamp = current_time( 'timestamp', 1 ); - $message = apply_filters( 'woocommerce_logger_log_message', $message, $level, $context ); - - foreach ( $this->handlers as $handler ) { - $handler->handle( $timestamp, $level, $message, $context ); - } - } - } - - /** - * Adds an emergency level message. - * - * System is unusable. - * - * @see WC_Logger::log - * - * @param string $message Message to log. - * @param array $context Log context. - */ - public function emergency( $message, $context = array() ) { - $this->log( WC_Log_Levels::EMERGENCY, $message, $context ); - } - - /** - * Adds an alert level message. - * - * Action must be taken immediately. - * Example: Entire website down, database unavailable, etc. - * - * @see WC_Logger::log - * - * @param string $message Message to log. - * @param array $context Log context. - */ - public function alert( $message, $context = array() ) { - $this->log( WC_Log_Levels::ALERT, $message, $context ); - } - - /** - * Adds a critical level message. - * - * Critical conditions. - * Example: Application component unavailable, unexpected exception. - * - * @see WC_Logger::log - * - * @param string $message Message to log. - * @param array $context Log context. - */ - public function critical( $message, $context = array() ) { - $this->log( WC_Log_Levels::CRITICAL, $message, $context ); - } - - /** - * Adds an error level message. - * - * Runtime errors that do not require immediate action but should typically be logged - * and monitored. - * - * @see WC_Logger::log - * - * @param string $message Message to log. - * @param array $context Log context. - */ - public function error( $message, $context = array() ) { - $this->log( WC_Log_Levels::ERROR, $message, $context ); - } - - /** - * Adds a warning level message. - * - * Exceptional occurrences that are not errors. - * - * Example: Use of deprecated APIs, poor use of an API, undesirable things that are not - * necessarily wrong. - * - * @see WC_Logger::log - * - * @param string $message Message to log. - * @param array $context Log context. - */ - public function warning( $message, $context = array() ) { - $this->log( WC_Log_Levels::WARNING, $message, $context ); - } - - /** - * Adds a notice level message. - * - * Normal but significant events. - * - * @see WC_Logger::log - * - * @param string $message Message to log. - * @param array $context Log context. - */ - public function notice( $message, $context = array() ) { - $this->log( WC_Log_Levels::NOTICE, $message, $context ); - } - - /** - * Adds a info level message. - * - * Interesting events. - * Example: User logs in, SQL logs. - * - * @see WC_Logger::log - * - * @param string $message Message to log. - * @param array $context Log context. - */ - public function info( $message, $context = array() ) { - $this->log( WC_Log_Levels::INFO, $message, $context ); - } - - /** - * Adds a debug level message. - * - * Detailed debug information. - * - * @see WC_Logger::log - * - * @param string $message Message to log. - * @param array $context Log context. - */ - public function debug( $message, $context = array() ) { - $this->log( WC_Log_Levels::DEBUG, $message, $context ); - } - - /** - * Clear entries for a chosen file/source. - * - * @param string $source Source/handle to clear. - * @return bool - */ - public function clear( $source = '' ) { - if ( ! $source ) { - return false; - } - foreach ( $this->handlers as $handler ) { - if ( is_callable( array( $handler, 'clear' ) ) ) { - $handler->clear( $source ); - } - } - return true; - } - - /** - * Clear all logs older than a defined number of days. Defaults to 30 days. - * - * @since 3.4.0 - */ - public function clear_expired_logs() { - $days = absint( apply_filters( 'woocommerce_logger_days_to_retain_logs', 30 ) ); - $timestamp = strtotime( "-{$days} days" ); - - foreach ( $this->handlers as $handler ) { - if ( is_callable( array( $handler, 'delete_logs_before_timestamp' ) ) ) { - $handler->delete_logs_before_timestamp( $timestamp ); - } - } - } -} diff --git a/includes/class-wc-meta-data.php b/includes/class-wc-meta-data.php deleted file mode 100644 index c21d98d650c..00000000000 --- a/includes/class-wc-meta-data.php +++ /dev/null @@ -1,119 +0,0 @@ -current_data = $meta; - $this->apply_changes(); - } - - /** - * When converted to JSON. - * - * @return object|array - */ - public function jsonSerialize() { - return $this->get_data(); - } - - /** - * Merge changes with data and clear. - */ - public function apply_changes() { - $this->data = $this->current_data; - } - - /** - * Creates or updates a property in the metadata object. - * - * @param string $key Key to set. - * @param mixed $value Value to set. - */ - public function __set( $key, $value ) { - $this->current_data[ $key ] = $value; - } - - /** - * Checks if a given key exists in our data. This is called internally - * by `empty` and `isset`. - * - * @param string $key Key to check if set. - * - * @return bool - */ - public function __isset( $key ) { - return array_key_exists( $key, $this->current_data ); - } - - /** - * Returns the value of any property. - * - * @param string $key Key to get. - * @return mixed Property value or NULL if it does not exists - */ - public function __get( $key ) { - if ( array_key_exists( $key, $this->current_data ) ) { - return $this->current_data[ $key ]; - } - return null; - } - - /** - * Return data changes only. - * - * @return array - */ - public function get_changes() { - $changes = array(); - foreach ( $this->current_data as $id => $value ) { - if ( ! array_key_exists( $id, $this->data ) || $value !== $this->data[ $id ] ) { - $changes[ $id ] = $value; - } - } - return $changes; - } - - /** - * Return all data as an array. - * - * @return array - */ - public function get_data() { - return $this->data; - } -} diff --git a/includes/class-wc-order-item-coupon.php b/includes/class-wc-order-item-coupon.php deleted file mode 100644 index 1a8aa50702a..00000000000 --- a/includes/class-wc-order-item-coupon.php +++ /dev/null @@ -1,182 +0,0 @@ - '', - 'discount' => 0, - 'discount_tax' => 0, - ); - - /* - |-------------------------------------------------------------------------- - | Setters - |-------------------------------------------------------------------------- - */ - - /** - * Set order item name. - * - * @param string $value Coupon code. - */ - public function set_name( $value ) { - return $this->set_code( $value ); - } - - /** - * Set code. - * - * @param string $value Coupon code. - */ - public function set_code( $value ) { - $this->set_prop( 'code', wc_format_coupon_code( $value ) ); - } - - /** - * Set discount amount. - * - * @param string $value Discount. - */ - public function set_discount( $value ) { - $this->set_prop( 'discount', wc_format_decimal( $value ) ); - } - - /** - * Set discounted tax amount. - * - * @param string $value Discount tax. - */ - public function set_discount_tax( $value ) { - $this->set_prop( 'discount_tax', wc_format_decimal( $value ) ); - } - - /* - |-------------------------------------------------------------------------- - | Getters - |-------------------------------------------------------------------------- - */ - - /** - * Get order item type. - * - * @return string - */ - public function get_type() { - return 'coupon'; - } - - /** - * Get order item name. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_name( $context = 'view' ) { - return $this->get_code( $context ); - } - - /** - * Get coupon code. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_code( $context = 'view' ) { - return $this->get_prop( 'code', $context ); - } - - /** - * Get discount amount. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_discount( $context = 'view' ) { - return $this->get_prop( 'discount', $context ); - } - - /** - * Get discounted tax amount. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * - * @return string - */ - public function get_discount_tax( $context = 'view' ) { - return $this->get_prop( 'discount_tax', $context ); - } - - /* - |-------------------------------------------------------------------------- - | Array Access Methods - |-------------------------------------------------------------------------- - | - | For backwards compatibility with legacy arrays. - | - */ - - /** - * OffsetGet for ArrayAccess/Backwards compatibility. - * - * @deprecated 4.4.0 - * @param string $offset Offset. - * @return mixed - */ - public function offsetGet( $offset ) { - wc_deprecated_function( 'WC_Order_Item_Coupon::offsetGet', '4.4.0', '' ); - if ( 'discount_amount' === $offset ) { - $offset = 'discount'; - } elseif ( 'discount_amount_tax' === $offset ) { - $offset = 'discount_tax'; - } - return parent::offsetGet( $offset ); - } - - /** - * OffsetSet for ArrayAccess/Backwards compatibility. - * - * @deprecated 4.4.0 - * @param string $offset Offset. - * @param mixed $value Value. - */ - public function offsetSet( $offset, $value ) { - wc_deprecated_function( 'WC_Order_Item_Coupon::offsetSet', '4.4.0', '' ); - if ( 'discount_amount' === $offset ) { - $offset = 'discount'; - } elseif ( 'discount_amount_tax' === $offset ) { - $offset = 'discount_tax'; - } - parent::offsetSet( $offset, $value ); - } - - /** - * OffsetExists for ArrayAccess. - * - * @param string $offset Offset. - * @return bool - */ - public function offsetExists( $offset ) { - if ( in_array( $offset, array( 'discount_amount', 'discount_amount_tax' ), true ) ) { - return true; - } - return parent::offsetExists( $offset ); - } -} diff --git a/includes/class-wc-order-item-fee.php b/includes/class-wc-order-item-fee.php deleted file mode 100644 index 44e1ee12d2c..00000000000 --- a/includes/class-wc-order-item-fee.php +++ /dev/null @@ -1,338 +0,0 @@ - '', - 'tax_status' => 'taxable', - 'amount' => '', - 'total' => '', - 'total_tax' => '', - 'taxes' => array( - 'total' => array(), - ), - ); - - /** - * Get item costs grouped by tax class. - * - * @since 3.2.0 - * @param WC_Order $order Order object. - * @return array - */ - protected function get_tax_class_costs( $order ) { - $order_item_tax_classes = $order->get_items_tax_classes(); - $costs = array_fill_keys( $order_item_tax_classes, 0 ); - $costs['non-taxable'] = 0; - - foreach ( $order->get_items( array( 'line_item', 'fee', 'shipping' ) ) as $item ) { - if ( 0 > $item->get_total() ) { - continue; - } - if ( 'taxable' !== $item->get_tax_status() ) { - $costs['non-taxable'] += $item->get_total(); - } elseif ( 'inherit' === $item->get_tax_class() ) { - $inherit_class = reset( $order_item_tax_classes ); - $costs[ $inherit_class ] += $item->get_total(); - } else { - $costs[ $item->get_tax_class() ] += $item->get_total(); - } - } - - return array_filter( $costs ); - } - /** - * Calculate item taxes. - * - * @since 3.2.0 - * @param array $calculate_tax_for Location data to get taxes for. Required. - * @return bool True if taxes were calculated. - */ - public function calculate_taxes( $calculate_tax_for = array() ) { - if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'] ) ) { - return false; - } - // Use regular calculation unless the fee is negative. - if ( 0 <= $this->get_total() ) { - return parent::calculate_taxes( $calculate_tax_for ); - } - - if ( wc_tax_enabled() && $this->get_order() ) { - // Apportion taxes to order items, shipping, and fees. - $order = $this->get_order(); - $tax_class_costs = $this->get_tax_class_costs( $order ); - $total_costs = array_sum( $tax_class_costs ); - $discount_taxes = array(); - if ( $total_costs ) { - foreach ( $tax_class_costs as $tax_class => $tax_class_cost ) { - if ( 'non-taxable' === $tax_class ) { - continue; - } - $proportion = $tax_class_cost / $total_costs; - $cart_discount_proportion = $this->get_total() * $proportion; - $calculate_tax_for['tax_class'] = $tax_class; - $tax_rates = WC_Tax::find_rates( $calculate_tax_for ); - $discount_taxes = wc_array_merge_recursive_numeric( $discount_taxes, WC_Tax::calc_tax( $cart_discount_proportion, $tax_rates ) ); - } - } - $this->set_taxes( array( 'total' => $discount_taxes ) ); - } else { - $this->set_taxes( false ); - } - - do_action( 'woocommerce_order_item_fee_after_calculate_taxes', $this, $calculate_tax_for ); - - return true; - } - - /* - |-------------------------------------------------------------------------- - | Setters - |-------------------------------------------------------------------------- - */ - - /** - * Set fee amount. - * - * @param string $value Amount. - */ - public function set_amount( $value ) { - $this->set_prop( 'amount', wc_format_decimal( $value ) ); - } - - /** - * Set tax class. - * - * @param string $value Tax class. - */ - public function set_tax_class( $value ) { - if ( $value && ! in_array( $value, WC_Tax::get_tax_class_slugs(), true ) ) { - $this->error( 'order_item_fee_invalid_tax_class', __( 'Invalid tax class', 'woocommerce' ) ); - } - $this->set_prop( 'tax_class', $value ); - } - - /** - * Set tax_status. - * - * @param string $value Tax status. - */ - public function set_tax_status( $value ) { - if ( in_array( $value, array( 'taxable', 'none' ), true ) ) { - $this->set_prop( 'tax_status', $value ); - } else { - $this->set_prop( 'tax_status', 'taxable' ); - } - } - - /** - * Set total. - * - * @param string $amount Fee amount (do not enter negative amounts). - */ - public function set_total( $amount ) { - $this->set_prop( 'total', wc_format_decimal( $amount ) ); - } - - /** - * Set total tax. - * - * @param string $amount Amount. - */ - public function set_total_tax( $amount ) { - $this->set_prop( 'total_tax', wc_format_decimal( $amount ) ); - } - - /** - * Set taxes. - * - * This is an array of tax ID keys with total amount values. - * - * @param array $raw_tax_data Raw tax data. - */ - public function set_taxes( $raw_tax_data ) { - $raw_tax_data = maybe_unserialize( $raw_tax_data ); - $tax_data = array( - 'total' => array(), - ); - if ( ! empty( $raw_tax_data['total'] ) ) { - $tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data['total'] ); - } - $this->set_prop( 'taxes', $tax_data ); - - if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { - $this->set_total_tax( array_sum( $tax_data['total'] ) ); - } else { - $this->set_total_tax( array_sum( array_map( 'wc_round_tax_total', $tax_data['total'] ) ) ); - } - } - - /* - |-------------------------------------------------------------------------- - | Getters - |-------------------------------------------------------------------------- - */ - - /** - * Get fee amount. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_amount( $context = 'view' ) { - return $this->get_prop( 'amount', $context ); - } - - /** - * Get order item name. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_name( $context = 'view' ) { - $name = $this->get_prop( 'name', $context ); - if ( 'view' === $context ) { - return $name ? $name : __( 'Fee', 'woocommerce' ); - } else { - return $name; - } - } - - /** - * Get order item type. - * - * @return string - */ - public function get_type() { - return 'fee'; - } - - /** - * Get tax class. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_tax_class( $context = 'view' ) { - return $this->get_prop( 'tax_class', $context ); - } - - /** - * Get tax status. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_tax_status( $context = 'view' ) { - return $this->get_prop( 'tax_status', $context ); - } - - /** - * Get total fee. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_total( $context = 'view' ) { - return $this->get_prop( 'total', $context ); - } - - /** - * Get total tax. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_total_tax( $context = 'view' ) { - return $this->get_prop( 'total_tax', $context ); - } - - /** - * Get fee taxes. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return array - */ - public function get_taxes( $context = 'view' ) { - return $this->get_prop( 'taxes', $context ); - } - - /* - |-------------------------------------------------------------------------- - | Array Access Methods - |-------------------------------------------------------------------------- - | - | For backwards compatibility with legacy arrays. - | - */ - - /** - * OffsetGet for ArrayAccess/Backwards compatibility. - * - * @param string $offset Offset. - * @return mixed - */ - public function offsetGet( $offset ) { - if ( 'line_total' === $offset ) { - $offset = 'total'; - } elseif ( 'line_tax' === $offset ) { - $offset = 'total_tax'; - } elseif ( 'line_tax_data' === $offset ) { - $offset = 'taxes'; - } - return parent::offsetGet( $offset ); - } - - /** - * OffsetSet for ArrayAccess/Backwards compatibility. - * - * @deprecated 4.4.0 - * @param string $offset Offset. - * @param mixed $value Value. - */ - public function offsetSet( $offset, $value ) { - wc_deprecated_function( 'WC_Order_Item_Fee::offsetSet', '4.4.0', '' ); - if ( 'line_total' === $offset ) { - $offset = 'total'; - } elseif ( 'line_tax' === $offset ) { - $offset = 'total_tax'; - } elseif ( 'line_tax_data' === $offset ) { - $offset = 'taxes'; - } - parent::offsetSet( $offset, $value ); - } - - /** - * OffsetExists for ArrayAccess - * - * @param string $offset Offset. - * @return bool - */ - public function offsetExists( $offset ) { - if ( in_array( $offset, array( 'line_total', 'line_tax', 'line_tax_data' ), true ) ) { - return true; - } - return parent::offsetExists( $offset ); - } -} diff --git a/includes/class-wc-order-item-meta.php b/includes/class-wc-order-item-meta.php deleted file mode 100644 index 693be232328..00000000000 --- a/includes/class-wc-order-item-meta.php +++ /dev/null @@ -1,215 +0,0 @@ -legacy = true; - $this->meta = array_filter( (array) $item ); - return; - } - $this->item = $item; - $this->meta = array_filter( (array) $item['item_meta'] ); - $this->product = $product; - } - - /** - * Display meta in a formatted list. - * - * @param bool $flat Flat (default: false). - * @param bool $return Return (default: false). - * @param string $hideprefix Hide prefix (default: _). - * @param string $delimiter Delimiter used to separate items when $flat is true. - * @return string|void - */ - public function display( $flat = false, $return = false, $hideprefix = '_', $delimiter = ", \n" ) { - $output = ''; - $formatted_meta = $this->get_formatted( $hideprefix ); - - if ( ! empty( $formatted_meta ) ) { - $meta_list = array(); - - foreach ( $formatted_meta as $meta ) { - if ( $flat ) { - $meta_list[] = wp_kses_post( $meta['label'] . ': ' . $meta['value'] ); - } else { - $meta_list[] = ' -
    ' . wp_kses_post( $meta['label'] ) . ':
    -
    ' . wp_kses_post( wpautop( make_clickable( $meta['value'] ) ) ) . '
    - '; - } - } - - if ( ! empty( $meta_list ) ) { - if ( $flat ) { - $output .= implode( $delimiter, $meta_list ); - } else { - $output .= '
    ' . implode( '', $meta_list ) . '
    '; - } - } - } - - $output = apply_filters( 'woocommerce_order_items_meta_display', $output, $this, $flat ); - - if ( $return ) { - return $output; - } else { - echo $output; // WPCS: XSS ok. - } - } - - /** - * Return an array of formatted item meta in format e.g. - * - * Returns: array( - * 'pa_size' => array( - * 'label' => 'Size', - * 'value' => 'Medium', - * ) - * ) - * - * @since 2.4 - * @param string $hideprefix exclude meta when key is prefixed with this, defaults to '_'. - * @return array - */ - public function get_formatted( $hideprefix = '_' ) { - if ( $this->legacy ) { - return $this->get_formatted_legacy( $hideprefix ); - } - - $formatted_meta = array(); - - if ( ! empty( $this->item['item_meta_array'] ) ) { - foreach ( $this->item['item_meta_array'] as $meta_id => $meta ) { - if ( '' === $meta->value || is_serialized( $meta->value ) || ( ! empty( $hideprefix ) && substr( $meta->key, 0, 1 ) === $hideprefix ) ) { - continue; - } - - $attribute_key = urldecode( str_replace( 'attribute_', '', $meta->key ) ); - $meta_value = $meta->value; - - // If this is a term slug, get the term's nice name. - if ( taxonomy_exists( $attribute_key ) ) { - $term = get_term_by( 'slug', $meta_value, $attribute_key ); - - if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) { - $meta_value = $term->name; - } - } - - $formatted_meta[ $meta_id ] = array( - 'key' => $meta->key, - 'label' => wc_attribute_label( $attribute_key, $this->product ), - 'value' => apply_filters( 'woocommerce_order_item_display_meta_value', $meta_value, $meta, $this->item ), - ); - } - } - - return apply_filters( 'woocommerce_order_items_meta_get_formatted', $formatted_meta, $this ); - } - - /** - * Return an array of formatted item meta in format e.g. - * Handles @deprecated args. - * - * @param string $hideprefix Hide prefix. - * - * @return array - */ - public function get_formatted_legacy( $hideprefix = '_' ) { - if ( ! is_ajax() ) { - wc_deprecated_argument( 'WC_Order_Item_Meta::get_formatted', '2.4', 'Item Meta Data is being called with legacy arguments' ); - } - - $formatted_meta = array(); - - foreach ( $this->meta as $meta_key => $meta_values ) { - if ( empty( $meta_values ) || ( ! empty( $hideprefix ) && substr( $meta_key, 0, 1 ) === $hideprefix ) ) { - continue; - } - foreach ( (array) $meta_values as $meta_value ) { - // Skip serialised meta. - if ( is_serialized( $meta_value ) ) { - continue; - } - - $attribute_key = urldecode( str_replace( 'attribute_', '', $meta_key ) ); - - // If this is a term slug, get the term's nice name. - if ( taxonomy_exists( $attribute_key ) ) { - $term = get_term_by( 'slug', $meta_value, $attribute_key ); - if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) { - $meta_value = $term->name; - } - } - - // Unique key required. - $formatted_meta_key = $meta_key; - $loop = 0; - while ( isset( $formatted_meta[ $formatted_meta_key ] ) ) { - $loop ++; - $formatted_meta_key = $meta_key . '-' . $loop; - } - - $formatted_meta[ $formatted_meta_key ] = array( - 'key' => $meta_key, - 'label' => wc_attribute_label( $attribute_key, $this->product ), - 'value' => apply_filters( 'woocommerce_order_item_display_meta_value', $meta_value, $this->meta, $this->item ), - ); - } - } - - return $formatted_meta; - } -} diff --git a/includes/class-wc-order-item-product.php b/includes/class-wc-order-item-product.php deleted file mode 100644 index a32a6268357..00000000000 --- a/includes/class-wc-order-item-product.php +++ /dev/null @@ -1,485 +0,0 @@ - 0, - 'variation_id' => 0, - 'quantity' => 1, - 'tax_class' => '', - 'subtotal' => 0, - 'subtotal_tax' => 0, - 'total' => 0, - 'total_tax' => 0, - 'taxes' => array( - 'subtotal' => array(), - 'total' => array(), - ), - ); - - /* - |-------------------------------------------------------------------------- - | Setters - |-------------------------------------------------------------------------- - */ - - /** - * Set quantity. - * - * @param int $value Quantity. - */ - public function set_quantity( $value ) { - $this->set_prop( 'quantity', wc_stock_amount( $value ) ); - } - - /** - * Set tax class. - * - * @param string $value Tax class. - */ - public function set_tax_class( $value ) { - if ( $value && ! in_array( $value, WC_Tax::get_tax_class_slugs(), true ) ) { - $this->error( 'order_item_product_invalid_tax_class', __( 'Invalid tax class', 'woocommerce' ) ); - } - $this->set_prop( 'tax_class', $value ); - } - - /** - * Set Product ID - * - * @param int $value Product ID. - */ - public function set_product_id( $value ) { - if ( $value > 0 && 'product' !== get_post_type( absint( $value ) ) ) { - $this->error( 'order_item_product_invalid_product_id', __( 'Invalid product ID', 'woocommerce' ) ); - } - $this->set_prop( 'product_id', absint( $value ) ); - } - - /** - * Set variation ID. - * - * @param int $value Variation ID. - */ - public function set_variation_id( $value ) { - if ( $value > 0 && 'product_variation' !== get_post_type( $value ) ) { - $this->error( 'order_item_product_invalid_variation_id', __( 'Invalid variation ID', 'woocommerce' ) ); - } - $this->set_prop( 'variation_id', absint( $value ) ); - } - - /** - * Line subtotal (before discounts). - * - * @param string $value Subtotal. - */ - public function set_subtotal( $value ) { - $value = wc_format_decimal( $value ); - - if ( ! is_numeric( $value ) ) { - $value = 0; - } - - $this->set_prop( 'subtotal', $value ); - } - - /** - * Line total (after discounts). - * - * @param string $value Total. - */ - public function set_total( $value ) { - $value = wc_format_decimal( $value ); - - if ( ! is_numeric( $value ) ) { - $value = 0; - } - - $this->set_prop( 'total', $value ); - - // Subtotal cannot be less than total. - if ( '' === $this->get_subtotal() || $this->get_subtotal() < $this->get_total() ) { - $this->set_subtotal( $value ); - } - } - - /** - * Line subtotal tax (before discounts). - * - * @param string $value Subtotal tax. - */ - public function set_subtotal_tax( $value ) { - $this->set_prop( 'subtotal_tax', wc_format_decimal( $value ) ); - } - - /** - * Line total tax (after discounts). - * - * @param string $value Total tax. - */ - public function set_total_tax( $value ) { - $this->set_prop( 'total_tax', wc_format_decimal( $value ) ); - } - - /** - * Set line taxes and totals for passed in taxes. - * - * @param array $raw_tax_data Raw tax data. - */ - public function set_taxes( $raw_tax_data ) { - $raw_tax_data = maybe_unserialize( $raw_tax_data ); - $tax_data = array( - 'total' => array(), - 'subtotal' => array(), - ); - if ( ! empty( $raw_tax_data['total'] ) && ! empty( $raw_tax_data['subtotal'] ) ) { - $tax_data['subtotal'] = array_map( 'wc_format_decimal', $raw_tax_data['subtotal'] ); - $tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data['total'] ); - - // Subtotal cannot be less than total! - if ( array_sum( $tax_data['subtotal'] ) < array_sum( $tax_data['total'] ) ) { - $tax_data['subtotal'] = $tax_data['total']; - } - } - $this->set_prop( 'taxes', $tax_data ); - - if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { - $this->set_total_tax( array_sum( $tax_data['total'] ) ); - $this->set_subtotal_tax( array_sum( $tax_data['subtotal'] ) ); - } else { - $this->set_total_tax( array_sum( array_map( 'wc_round_tax_total', $tax_data['total'] ) ) ); - $this->set_subtotal_tax( array_sum( array_map( 'wc_round_tax_total', $tax_data['subtotal'] ) ) ); - } - } - - /** - * Set variation data (stored as meta data - write only). - * - * @param array $data Key/Value pairs. - */ - public function set_variation( $data = array() ) { - if ( is_array( $data ) ) { - foreach ( $data as $key => $value ) { - $this->add_meta_data( str_replace( 'attribute_', '', $key ), $value, true ); - } - } - } - - /** - * Set properties based on passed in product object. - * - * @param WC_Product $product Product instance. - */ - public function set_product( $product ) { - if ( ! is_a( $product, 'WC_Product' ) ) { - $this->error( 'order_item_product_invalid_product', __( 'Invalid product', 'woocommerce' ) ); - } - if ( $product->is_type( 'variation' ) ) { - $this->set_product_id( $product->get_parent_id() ); - $this->set_variation_id( $product->get_id() ); - $this->set_variation( is_callable( array( $product, 'get_variation_attributes' ) ) ? $product->get_variation_attributes() : array() ); - } else { - $this->set_product_id( $product->get_id() ); - } - $this->set_name( $product->get_name() ); - $this->set_tax_class( $product->get_tax_class() ); - } - - /** - * Set meta data for backordered products. - */ - public function set_backorder_meta() { - $product = $this->get_product(); - if ( $product && $product->backorders_require_notification() && $product->is_on_backorder( $this->get_quantity() ) ) { - $this->add_meta_data( apply_filters( 'woocommerce_backordered_item_meta_name', __( 'Backordered', 'woocommerce' ), $this ), $this->get_quantity() - max( 0, $product->get_stock_quantity() ), true ); - } - } - - /* - |-------------------------------------------------------------------------- - | Getters - |-------------------------------------------------------------------------- - */ - - /** - * Get order item type. - * - * @return string - */ - public function get_type() { - return 'line_item'; - } - - /** - * Get product ID. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return int - */ - public function get_product_id( $context = 'view' ) { - return $this->get_prop( 'product_id', $context ); - } - - /** - * Get variation ID. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return int - */ - public function get_variation_id( $context = 'view' ) { - return $this->get_prop( 'variation_id', $context ); - } - - /** - * Get quantity. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return int - */ - public function get_quantity( $context = 'view' ) { - return $this->get_prop( 'quantity', $context ); - } - - /** - * Get tax class. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_tax_class( $context = 'view' ) { - return $this->get_prop( 'tax_class', $context ); - } - - /** - * Get subtotal. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_subtotal( $context = 'view' ) { - return $this->get_prop( 'subtotal', $context ); - } - - /** - * Get subtotal tax. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_subtotal_tax( $context = 'view' ) { - return $this->get_prop( 'subtotal_tax', $context ); - } - - /** - * Get total. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_total( $context = 'view' ) { - return $this->get_prop( 'total', $context ); - } - - /** - * Get total tax. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_total_tax( $context = 'view' ) { - return $this->get_prop( 'total_tax', $context ); - } - - /** - * Get taxes. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return array - */ - public function get_taxes( $context = 'view' ) { - return $this->get_prop( 'taxes', $context ); - } - - /** - * Get the associated product. - * - * @return WC_Product|bool - */ - public function get_product() { - if ( $this->get_variation_id() ) { - $product = wc_get_product( $this->get_variation_id() ); - } else { - $product = wc_get_product( $this->get_product_id() ); - } - - // Backwards compatible filter from WC_Order::get_product_from_item(). - if ( has_filter( 'woocommerce_get_product_from_item' ) ) { - $product = apply_filters( 'woocommerce_get_product_from_item', $product, $this, $this->get_order() ); - } - - return apply_filters( 'woocommerce_order_item_product', $product, $this ); - } - - /** - * Get the Download URL. - * - * @param int $download_id Download ID. - * @return string - */ - public function get_item_download_url( $download_id ) { - $order = $this->get_order(); - - return $order ? add_query_arg( - array( - 'download_file' => $this->get_variation_id() ? $this->get_variation_id() : $this->get_product_id(), - 'order' => $order->get_order_key(), - 'email' => rawurlencode( $order->get_billing_email() ), - 'key' => $download_id, - ), - trailingslashit( home_url() ) - ) : ''; - } - - /** - * Get any associated downloadable files. - * - * @return array - */ - public function get_item_downloads() { - $files = array(); - $product = $this->get_product(); - $order = $this->get_order(); - $product_id = $this->get_variation_id() ? $this->get_variation_id() : $this->get_product_id(); - - if ( $product && $order && $product->is_downloadable() && $order->is_download_permitted() ) { - $email_hash = function_exists( 'hash' ) ? hash( 'sha256', $order->get_billing_email() ) : sha1( $order->get_billing_email() ); - $data_store = WC_Data_Store::load( 'customer-download' ); - $customer_downloads = $data_store->get_downloads( - array( - 'user_email' => $order->get_billing_email(), - 'order_id' => $order->get_id(), - 'product_id' => $product_id, - ) - ); - foreach ( $customer_downloads as $customer_download ) { - $download_id = $customer_download->get_download_id(); - - if ( $product->has_file( $download_id ) ) { - $file = $product->get_file( $download_id ); - $files[ $download_id ] = $file->get_data(); - $files[ $download_id ]['downloads_remaining'] = $customer_download->get_downloads_remaining(); - $files[ $download_id ]['access_expires'] = $customer_download->get_access_expires(); - $files[ $download_id ]['download_url'] = add_query_arg( - array( - 'download_file' => $product_id, - 'order' => $order->get_order_key(), - 'uid' => $email_hash, - 'key' => $download_id, - ), - trailingslashit( home_url() ) - ); - } - } - } - - return apply_filters( 'woocommerce_get_item_downloads', $files, $this, $order ); - } - - /** - * Get tax status. - * - * @return string - */ - public function get_tax_status() { - $product = $this->get_product(); - return $product ? $product->get_tax_status() : 'taxable'; - } - - /* - |-------------------------------------------------------------------------- - | Array Access Methods - |-------------------------------------------------------------------------- - | - | For backwards compatibility with legacy arrays. - | - */ - - /** - * OffsetGet for ArrayAccess/Backwards compatibility. - * - * @param string $offset Offset. - * @return mixed - */ - public function offsetGet( $offset ) { - if ( 'line_subtotal' === $offset ) { - $offset = 'subtotal'; - } elseif ( 'line_subtotal_tax' === $offset ) { - $offset = 'subtotal_tax'; - } elseif ( 'line_total' === $offset ) { - $offset = 'total'; - } elseif ( 'line_tax' === $offset ) { - $offset = 'total_tax'; - } elseif ( 'line_tax_data' === $offset ) { - $offset = 'taxes'; - } elseif ( 'qty' === $offset ) { - $offset = 'quantity'; - } - return parent::offsetGet( $offset ); - } - - /** - * OffsetSet for ArrayAccess/Backwards compatibility. - * - * @deprecated 4.4.0 - * @param string $offset Offset. - * @param mixed $value Value. - */ - public function offsetSet( $offset, $value ) { - wc_deprecated_function( 'WC_Order_Item_Product::offsetSet', '4.4.0', '' ); - if ( 'line_subtotal' === $offset ) { - $offset = 'subtotal'; - } elseif ( 'line_subtotal_tax' === $offset ) { - $offset = 'subtotal_tax'; - } elseif ( 'line_total' === $offset ) { - $offset = 'total'; - } elseif ( 'line_tax' === $offset ) { - $offset = 'total_tax'; - } elseif ( 'line_tax_data' === $offset ) { - $offset = 'taxes'; - } elseif ( 'qty' === $offset ) { - $offset = 'quantity'; - } - parent::offsetSet( $offset, $value ); - } - - /** - * OffsetExists for ArrayAccess. - * - * @param string $offset Offset. - * @return bool - */ - public function offsetExists( $offset ) { - if ( in_array( $offset, array( 'line_subtotal', 'line_subtotal_tax', 'line_total', 'line_tax', 'line_tax_data', 'item_meta_array', 'item_meta', 'qty' ), true ) ) { - return true; - } - return parent::offsetExists( $offset ); - } -} diff --git a/includes/class-wc-order-item-shipping.php b/includes/class-wc-order-item-shipping.php deleted file mode 100644 index 6602e87ae24..00000000000 --- a/includes/class-wc-order-item-shipping.php +++ /dev/null @@ -1,316 +0,0 @@ - '', - 'method_id' => '', - 'instance_id' => '', - 'total' => 0, - 'total_tax' => 0, - 'taxes' => array( - 'total' => array(), - ), - ); - - /** - * Calculate item taxes. - * - * @since 3.2.0 - * @param array $calculate_tax_for Location data to get taxes for. Required. - * @return bool True if taxes were calculated. - */ - public function calculate_taxes( $calculate_tax_for = array() ) { - if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'], $calculate_tax_for['tax_class'] ) ) { - return false; - } - if ( wc_tax_enabled() ) { - $tax_rates = WC_Tax::find_shipping_rates( $calculate_tax_for ); - $taxes = WC_Tax::calc_tax( $this->get_total(), $tax_rates, false ); - $this->set_taxes( array( 'total' => $taxes ) ); - } else { - $this->set_taxes( false ); - } - - do_action( 'woocommerce_order_item_shipping_after_calculate_taxes', $this, $calculate_tax_for ); - - return true; - } - - /* - |-------------------------------------------------------------------------- - | Setters - |-------------------------------------------------------------------------- - */ - - /** - * Set order item name. - * - * @param string $value Value to set. - * @throws WC_Data_Exception May throw exception if data is invalid. - */ - public function set_name( $value ) { - $this->set_method_title( $value ); - } - - /** - * Set method title. - * - * @param string $value Value to set. - * @throws WC_Data_Exception May throw exception if data is invalid. - */ - public function set_method_title( $value ) { - $this->set_prop( 'name', wc_clean( $value ) ); - $this->set_prop( 'method_title', wc_clean( $value ) ); - } - - /** - * Set shipping method id. - * - * @param string $value Value to set. - * @throws WC_Data_Exception May throw exception if data is invalid. - */ - public function set_method_id( $value ) { - $this->set_prop( 'method_id', wc_clean( $value ) ); - } - - /** - * Set shipping instance id. - * - * @param string $value Value to set. - * @throws WC_Data_Exception May throw exception if data is invalid. - */ - public function set_instance_id( $value ) { - $this->set_prop( 'instance_id', wc_clean( $value ) ); - } - - /** - * Set total. - * - * @param string $value Value to set. - * @throws WC_Data_Exception May throw exception if data is invalid. - */ - public function set_total( $value ) { - $this->set_prop( 'total', wc_format_decimal( $value ) ); - } - - /** - * Set total tax. - * - * @param string $value Value to set. - * @throws WC_Data_Exception May throw exception if data is invalid. - */ - protected function set_total_tax( $value ) { - $this->set_prop( 'total_tax', wc_format_decimal( $value ) ); - } - - /** - * Set taxes. - * - * This is an array of tax ID keys with total amount values. - * - * @param array $raw_tax_data Value to set. - * @throws WC_Data_Exception May throw exception if data is invalid. - */ - public function set_taxes( $raw_tax_data ) { - $raw_tax_data = maybe_unserialize( $raw_tax_data ); - $tax_data = array( - 'total' => array(), - ); - if ( isset( $raw_tax_data['total'] ) ) { - $tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data['total'] ); - } elseif ( ! empty( $raw_tax_data ) && is_array( $raw_tax_data ) ) { - // Older versions just used an array. - $tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data ); - } - $this->set_prop( 'taxes', $tax_data ); - - if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { - $this->set_total_tax( array_sum( $tax_data['total'] ) ); - } else { - $this->set_total_tax( array_sum( array_map( 'wc_round_tax_total', $tax_data['total'] ) ) ); - } - } - - /** - * Set properties based on passed in shipping rate object. - * - * @param WC_Shipping_Rate $shipping_rate Shipping rate to set. - */ - public function set_shipping_rate( $shipping_rate ) { - $this->set_method_title( $shipping_rate->get_label() ); - $this->set_method_id( $shipping_rate->get_method_id() ); - $this->set_instance_id( $shipping_rate->get_instance_id() ); - $this->set_total( $shipping_rate->get_cost() ); - $this->set_taxes( $shipping_rate->get_taxes() ); - $this->set_meta_data( $shipping_rate->get_meta_data() ); - } - - /* - |-------------------------------------------------------------------------- - | Getters - |-------------------------------------------------------------------------- - */ - - /** - * Get order item type. - * - * @return string - */ - public function get_type() { - return 'shipping'; - } - - /** - * Get order item name. - * - * @param string $context View or edit context. - * @return string - */ - public function get_name( $context = 'view' ) { - return $this->get_method_title( $context ); - } - - /** - * Get title. - * - * @param string $context View or edit context. - * @return string - */ - public function get_method_title( $context = 'view' ) { - $method_title = $this->get_prop( 'method_title', $context ); - if ( 'view' === $context ) { - return $method_title ? $method_title : __( 'Shipping', 'woocommerce' ); - } else { - return $method_title; - } - } - - /** - * Get method ID. - * - * @param string $context View or edit context. - * @return string - */ - public function get_method_id( $context = 'view' ) { - return $this->get_prop( 'method_id', $context ); - } - - /** - * Get instance ID. - * - * @param string $context View or edit context. - * @return string - */ - public function get_instance_id( $context = 'view' ) { - return $this->get_prop( 'instance_id', $context ); - } - - /** - * Get total cost. - * - * @param string $context View or edit context. - * @return string - */ - public function get_total( $context = 'view' ) { - return $this->get_prop( 'total', $context ); - } - - /** - * Get total tax. - * - * @param string $context View or edit context. - * @return string - */ - public function get_total_tax( $context = 'view' ) { - return $this->get_prop( 'total_tax', $context ); - } - - /** - * Get taxes. - * - * @param string $context View or edit context. - * @return array - */ - public function get_taxes( $context = 'view' ) { - return $this->get_prop( 'taxes', $context ); - } - - /** - * Get tax class. - * - * @param string $context View or edit context. - * @return string - */ - public function get_tax_class( $context = 'view' ) { - return get_option( 'woocommerce_shipping_tax_class' ); - } - - /* - |-------------------------------------------------------------------------- - | Array Access Methods - |-------------------------------------------------------------------------- - | - | For backwards compatibility with legacy arrays. - | - */ - - /** - * Offset get: for ArrayAccess/Backwards compatibility. - * - * @param string $offset Key. - * @return mixed - */ - public function offsetGet( $offset ) { - if ( 'cost' === $offset ) { - $offset = 'total'; - } - return parent::offsetGet( $offset ); - } - - /** - * Offset set: for ArrayAccess/Backwards compatibility. - * - * @deprecated 4.4.0 - * @param string $offset Key. - * @param mixed $value Value to set. - */ - public function offsetSet( $offset, $value ) { - wc_deprecated_function( 'WC_Order_Item_Shipping::offsetSet', '4.4.0', '' ); - if ( 'cost' === $offset ) { - $offset = 'total'; - } - parent::offsetSet( $offset, $value ); - } - - /** - * Offset exists: for ArrayAccess. - * - * @param string $offset Key. - * @return bool - */ - public function offsetExists( $offset ) { - if ( in_array( $offset, array( 'cost' ), true ) ) { - return true; - } - return parent::offsetExists( $offset ); - } -} diff --git a/includes/class-wc-order-item-tax.php b/includes/class-wc-order-item-tax.php deleted file mode 100644 index c41e3881f9f..00000000000 --- a/includes/class-wc-order-item-tax.php +++ /dev/null @@ -1,288 +0,0 @@ - '', - 'rate_id' => 0, - 'label' => '', - 'compound' => false, - 'tax_total' => 0, - 'shipping_tax_total' => 0, - 'rate_percent' => null, - ); - - /* - |-------------------------------------------------------------------------- - | Setters - |-------------------------------------------------------------------------- - */ - - /** - * Set order item name. - * - * @param string $value Name. - */ - public function set_name( $value ) { - $this->set_rate_code( $value ); - } - - /** - * Set item name. - * - * @param string $value Rate code. - */ - public function set_rate_code( $value ) { - $this->set_prop( 'rate_code', wc_clean( $value ) ); - } - - /** - * Set item name. - * - * @param string $value Label. - */ - public function set_label( $value ) { - $this->set_prop( 'label', wc_clean( $value ) ); - } - - /** - * Set tax rate id. - * - * @param int $value Rate ID. - */ - public function set_rate_id( $value ) { - $this->set_prop( 'rate_id', absint( $value ) ); - } - - /** - * Set tax total. - * - * @param string $value Tax total. - */ - public function set_tax_total( $value ) { - $this->set_prop( 'tax_total', $value ? wc_format_decimal( $value ) : 0 ); - } - - /** - * Set shipping tax total. - * - * @param string $value Shipping tax total. - */ - public function set_shipping_tax_total( $value ) { - $this->set_prop( 'shipping_tax_total', $value ? wc_format_decimal( $value ) : 0 ); - } - - /** - * Set compound. - * - * @param bool $value If tax is compound. - */ - public function set_compound( $value ) { - $this->set_prop( 'compound', (bool) $value ); - } - - /** - * Set rate value. - * - * @param float $value tax rate value. - */ - public function set_rate_percent( $value ) { - $this->set_prop( 'rate_percent', (float) $value ); - } - - /** - * Set properties based on passed in tax rate by ID. - * - * @param int $tax_rate_id Tax rate ID. - */ - public function set_rate( $tax_rate_id ) { - $tax_rate = WC_Tax::_get_tax_rate( $tax_rate_id, OBJECT ); - - $this->set_rate_id( $tax_rate_id ); - $this->set_rate_code( WC_Tax::get_rate_code( $tax_rate ) ); - $this->set_label( WC_Tax::get_rate_label( $tax_rate ) ); - $this->set_compound( WC_Tax::is_compound( $tax_rate ) ); - $this->set_rate_percent( WC_Tax::get_rate_percent_value( $tax_rate ) ); - } - - /* - |-------------------------------------------------------------------------- - | Getters - |-------------------------------------------------------------------------- - */ - - /** - * Get order item type. - * - * @return string - */ - public function get_type() { - return 'tax'; - } - - /** - * Get rate code/name. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_name( $context = 'view' ) { - return $this->get_rate_code( $context ); - } - - /** - * Get rate code/name. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_rate_code( $context = 'view' ) { - return $this->get_prop( 'rate_code', $context ); - } - - /** - * Get label. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_label( $context = 'view' ) { - $label = $this->get_prop( 'label', $context ); - if ( 'view' === $context ) { - return $label ? $label : __( 'Tax', 'woocommerce' ); - } else { - return $label; - } - } - - /** - * Get tax rate ID. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return int - */ - public function get_rate_id( $context = 'view' ) { - return $this->get_prop( 'rate_id', $context ); - } - - /** - * Get tax_total - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_tax_total( $context = 'view' ) { - return $this->get_prop( 'tax_total', $context ); - } - - /** - * Get shipping_tax_total - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_shipping_tax_total( $context = 'view' ) { - return $this->get_prop( 'shipping_tax_total', $context ); - } - - /** - * Get compound. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return bool - */ - public function get_compound( $context = 'view' ) { - return $this->get_prop( 'compound', $context ); - } - - /** - * Is this a compound tax rate? - * - * @return boolean - */ - public function is_compound() { - return $this->get_compound(); - } - - /** - * Get rate value - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return float - */ - public function get_rate_percent( $context = 'view' ) { - return $this->get_prop( 'rate_percent', $context ); - } - - /* - |-------------------------------------------------------------------------- - | Array Access Methods - |-------------------------------------------------------------------------- - | - | For backwards compatibility with legacy arrays. - | - */ - - /** - * O for ArrayAccess/Backwards compatibility. - * - * @param string $offset Offset. - * @return mixed - */ - public function offsetGet( $offset ) { - if ( 'tax_amount' === $offset ) { - $offset = 'tax_total'; - } elseif ( 'shipping_tax_amount' === $offset ) { - $offset = 'shipping_tax_total'; - } - return parent::offsetGet( $offset ); - } - - /** - * OffsetSet for ArrayAccess/Backwards compatibility. - * - * @deprecated 4.4.0 - * @param string $offset Offset. - * @param mixed $value Value. - */ - public function offsetSet( $offset, $value ) { - wc_deprecated_function( 'WC_Order_Item_Tax::offsetSet', '4.4.0', '' ); - if ( 'tax_amount' === $offset ) { - $offset = 'tax_total'; - } elseif ( 'shipping_tax_amount' === $offset ) { - $offset = 'shipping_tax_total'; - } - parent::offsetSet( $offset, $value ); - } - - /** - * OffsetExists for ArrayAccess. - * - * @param string $offset Offset. - * @return bool - */ - public function offsetExists( $offset ) { - if ( in_array( $offset, array( 'tax_amount', 'shipping_tax_amount' ), true ) ) { - return true; - } - return parent::offsetExists( $offset ); - } -} diff --git a/includes/class-wc-order-item.php b/includes/class-wc-order-item.php deleted file mode 100644 index e7c916157cb..00000000000 --- a/includes/class-wc-order-item.php +++ /dev/null @@ -1,409 +0,0 @@ - 0, - 'name' => '', - ); - - /** - * Stores meta in cache for future reads. - * A group must be set to to enable caching. - * - * @var string - */ - protected $cache_group = 'order-items'; - - /** - * Meta type. This should match up with - * the types available at https://developer.wordpress.org/reference/functions/add_metadata/. - * WP defines 'post', 'user', 'comment', and 'term'. - * - * @var string - */ - protected $meta_type = 'order_item'; - - /** - * This is the name of this object type. - * - * @var string - */ - protected $object_type = 'order_item'; - - /** - * Constructor. - * - * @param int|object|array $item ID to load from the DB, or WC_Order_Item object. - */ - public function __construct( $item = 0 ) { - parent::__construct( $item ); - - if ( $item instanceof WC_Order_Item ) { - $this->set_id( $item->get_id() ); - } elseif ( is_numeric( $item ) && $item > 0 ) { - $this->set_id( $item ); - } else { - $this->set_object_read( true ); - } - - $type = 'line_item' === $this->get_type() ? 'product' : $this->get_type(); - $this->data_store = WC_Data_Store::load( 'order-item-' . $type ); - if ( $this->get_id() > 0 ) { - $this->data_store->read( $this ); - } - } - - /** - * Merge changes with data and clear. - * Overrides WC_Data::apply_changes. - * array_replace_recursive does not work well for order items because it merges taxes instead - * of replacing them. - * - * @since 3.2.0 - */ - public function apply_changes() { - if ( function_exists( 'array_replace' ) ) { - $this->data = array_replace( $this->data, $this->changes ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.array_replaceFound - } else { // PHP 5.2 compatibility. - foreach ( $this->changes as $key => $change ) { - $this->data[ $key ] = $change; - } - } - $this->changes = array(); - } - - /* - |-------------------------------------------------------------------------- - | Getters - |-------------------------------------------------------------------------- - */ - - /** - * Get order ID this meta belongs to. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return int - */ - public function get_order_id( $context = 'view' ) { - return $this->get_prop( 'order_id', $context ); - } - - /** - * Get order item name. - * - * @param string $context What the value is for. Valid values are 'view' and 'edit'. - * @return string - */ - public function get_name( $context = 'view' ) { - return $this->get_prop( 'name', $context ); - } - - /** - * Get order item type. Overridden by child classes. - * - * @return string - */ - public function get_type() { - return ''; - } - - /** - * Get quantity. - * - * @return int - */ - public function get_quantity() { - return 1; - } - - /** - * Get tax status. - * - * @return string - */ - public function get_tax_status() { - return 'taxable'; - } - - /** - * Get tax class. - * - * @return string - */ - public function get_tax_class() { - return ''; - } - - /** - * Get parent order object. - * - * @return WC_Order - */ - public function get_order() { - return wc_get_order( $this->get_order_id() ); - } - - /* - |-------------------------------------------------------------------------- - | Setters - |-------------------------------------------------------------------------- - */ - - /** - * Set order ID. - * - * @param int $value Order ID. - */ - public function set_order_id( $value ) { - $this->set_prop( 'order_id', absint( $value ) ); - } - - /** - * Set order item name. - * - * @param string $value Item name. - */ - public function set_name( $value ) { - $this->set_prop( 'name', wp_check_invalid_utf8( $value ) ); - } - - /* - |-------------------------------------------------------------------------- - | Other Methods - |-------------------------------------------------------------------------- - */ - - /** - * Type checking. - * - * @param string|array $type Type. - * @return boolean - */ - public function is_type( $type ) { - return is_array( $type ) ? in_array( $this->get_type(), $type, true ) : $type === $this->get_type(); - } - - /** - * Calculate item taxes. - * - * @since 3.2.0 - * @param array $calculate_tax_for Location data to get taxes for. Required. - * @return bool True if taxes were calculated. - */ - public function calculate_taxes( $calculate_tax_for = array() ) { - if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'] ) ) { - return false; - } - if ( '0' !== $this->get_tax_class() && 'taxable' === $this->get_tax_status() && wc_tax_enabled() ) { - $calculate_tax_for['tax_class'] = $this->get_tax_class(); - $tax_rates = WC_Tax::find_rates( $calculate_tax_for ); - $taxes = WC_Tax::calc_tax( $this->get_total(), $tax_rates, false ); - - if ( method_exists( $this, 'get_subtotal' ) ) { - $subtotal_taxes = WC_Tax::calc_tax( $this->get_subtotal(), $tax_rates, false ); - $this->set_taxes( - array( - 'total' => $taxes, - 'subtotal' => $subtotal_taxes, - ) - ); - } else { - $this->set_taxes( array( 'total' => $taxes ) ); - } - } else { - $this->set_taxes( false ); - } - - do_action( 'woocommerce_order_item_after_calculate_taxes', $this, $calculate_tax_for ); - - return true; - } - - /* - |-------------------------------------------------------------------------- - | Meta Data Handling - |-------------------------------------------------------------------------- - */ - - /** - * Expands things like term slugs before return. - * - * @param string $hideprefix Meta data prefix, (default: _). - * @param bool $include_all Include all meta data, this stop skip items with values already in the product name. - * @return array - */ - public function get_formatted_meta_data( $hideprefix = '_', $include_all = false ) { - $formatted_meta = array(); - $meta_data = $this->get_meta_data(); - $hideprefix_length = ! empty( $hideprefix ) ? strlen( $hideprefix ) : 0; - $product = is_callable( array( $this, 'get_product' ) ) ? $this->get_product() : false; - $order_item_name = $this->get_name(); - - foreach ( $meta_data as $meta ) { - if ( empty( $meta->id ) || '' === $meta->value || ! is_scalar( $meta->value ) || ( $hideprefix_length && substr( $meta->key, 0, $hideprefix_length ) === $hideprefix ) ) { - continue; - } - - $meta->key = rawurldecode( (string) $meta->key ); - $meta->value = rawurldecode( (string) $meta->value ); - $attribute_key = str_replace( 'attribute_', '', $meta->key ); - $display_key = wc_attribute_label( $attribute_key, $product ); - $display_value = wp_kses_post( $meta->value ); - - if ( taxonomy_exists( $attribute_key ) ) { - $term = get_term_by( 'slug', $meta->value, $attribute_key ); - if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) { - $display_value = $term->name; - } - } - - // Skip items with values already in the product details area of the product name. - if ( ! $include_all && $product && $product->is_type( 'variation' ) && wc_is_attribute_in_product_name( $display_value, $order_item_name ) ) { - continue; - } - - $formatted_meta[ $meta->id ] = (object) array( - 'key' => $meta->key, - 'value' => $meta->value, - 'display_key' => apply_filters( 'woocommerce_order_item_display_meta_key', $display_key, $meta, $this ), - 'display_value' => wpautop( make_clickable( apply_filters( 'woocommerce_order_item_display_meta_value', $display_value, $meta, $this ) ) ), - ); - } - - return apply_filters( 'woocommerce_order_item_get_formatted_meta_data', $formatted_meta, $this ); - } - - /* - |-------------------------------------------------------------------------- - | Array Access Methods - |-------------------------------------------------------------------------- - | - | For backwards compatibility with legacy arrays. - | - */ - - /** - * OffsetSet for ArrayAccess. - * - * @param string $offset Offset. - * @param mixed $value Value. - */ - public function offsetSet( $offset, $value ) { - if ( 'item_meta_array' === $offset ) { - foreach ( $value as $meta_id => $meta ) { - $this->update_meta_data( $meta->key, $meta->value, $meta_id ); - } - return; - } - - if ( array_key_exists( $offset, $this->data ) ) { - $setter = "set_$offset"; - if ( is_callable( array( $this, $setter ) ) ) { - $this->$setter( $value ); - } - return; - } - - $this->update_meta_data( $offset, $value ); - } - - /** - * OffsetUnset for ArrayAccess. - * - * @param string $offset Offset. - */ - public function offsetUnset( $offset ) { - $this->maybe_read_meta_data(); - - if ( 'item_meta_array' === $offset || 'item_meta' === $offset ) { - $this->meta_data = array(); - return; - } - - if ( array_key_exists( $offset, $this->data ) ) { - unset( $this->data[ $offset ] ); - } - - if ( array_key_exists( $offset, $this->changes ) ) { - unset( $this->changes[ $offset ] ); - } - - $this->delete_meta_data( $offset ); - } - - /** - * OffsetExists for ArrayAccess. - * - * @param string $offset Offset. - * @return bool - */ - public function offsetExists( $offset ) { - $this->maybe_read_meta_data(); - if ( 'item_meta_array' === $offset || 'item_meta' === $offset || array_key_exists( $offset, $this->data ) ) { - return true; - } - return array_key_exists( $offset, wp_list_pluck( $this->meta_data, 'value', 'key' ) ) || array_key_exists( '_' . $offset, wp_list_pluck( $this->meta_data, 'value', 'key' ) ); - } - - /** - * OffsetGet for ArrayAccess. - * - * @param string $offset Offset. - * @return mixed - */ - public function offsetGet( $offset ) { - $this->maybe_read_meta_data(); - - if ( 'item_meta_array' === $offset ) { - $return = array(); - - foreach ( $this->meta_data as $meta ) { - $return[ $meta->id ] = $meta; - } - - return $return; - } - - $meta_values = wp_list_pluck( $this->meta_data, 'value', 'key' ); - - if ( 'item_meta' === $offset ) { - return $meta_values; - } elseif ( 'type' === $offset ) { - return $this->get_type(); - } elseif ( array_key_exists( $offset, $this->data ) ) { - $getter = "get_$offset"; - if ( is_callable( array( $this, $getter ) ) ) { - return $this->$getter(); - } - } elseif ( array_key_exists( '_' . $offset, $meta_values ) ) { - // Item meta was expanded in previous versions, with prefixes removed. This maintains support. - return $meta_values[ '_' . $offset ]; - } elseif ( array_key_exists( $offset, $meta_values ) ) { - return $meta_values[ $offset ]; - } - - return null; - } -} diff --git a/includes/class-wc-order-query.php b/includes/class-wc-order-query.php deleted file mode 100644 index 4f481c8d0ef..00000000000 --- a/includes/class-wc-order-query.php +++ /dev/null @@ -1,89 +0,0 @@ - array_keys( wc_get_order_statuses() ), - 'type' => wc_get_order_types( 'view-orders' ), - 'currency' => '', - 'version' => '', - 'prices_include_tax' => '', - 'date_created' => '', - 'date_modified' => '', - 'date_completed' => '', - 'date_paid' => '', - 'discount_total' => '', - 'discount_tax' => '', - 'shipping_total' => '', - 'shipping_tax' => '', - 'cart_tax' => '', - 'total' => '', - 'total_tax' => '', - 'customer' => '', - 'customer_id' => '', - 'order_key' => '', - 'billing_first_name' => '', - 'billing_last_name' => '', - 'billing_company' => '', - 'billing_address_1' => '', - 'billing_address_2' => '', - 'billing_city' => '', - 'billing_state' => '', - 'billing_postcode' => '', - 'billing_country' => '', - 'billing_email' => '', - 'billing_phone' => '', - 'shipping_first_name' => '', - 'shipping_last_name' => '', - 'shipping_company' => '', - 'shipping_address_1' => '', - 'shipping_address_2' => '', - 'shipping_city' => '', - 'shipping_state' => '', - 'shipping_postcode' => '', - 'shipping_country' => '', - 'payment_method' => '', - 'payment_method_title' => '', - 'transaction_id' => '', - 'customer_ip_address' => '', - 'customer_user_agent' => '', - 'created_via' => '', - 'customer_note' => '', - ) - ); - } - - /** - * Get orders matching the current query vars. - * - * @return array|object of WC_Order objects - * - * @throws Exception When WC_Data_Store validation fails. - */ - public function get_orders() { - $args = apply_filters( 'woocommerce_order_query_args', $this->get_query_vars() ); - $results = WC_Data_Store::load( 'order' )->query( $args ); - return apply_filters( 'woocommerce_order_query', $results, $args ); - } -} diff --git a/includes/class-wc-order-refund.php b/includes/class-wc-order-refund.php deleted file mode 100644 index c384d044d65..00000000000 --- a/includes/class-wc-order-refund.php +++ /dev/null @@ -1,228 +0,0 @@ - '', - 'reason' => '', - 'refunded_by' => 0, - 'refunded_payment' => false, - ); - - /** - * Get internal type (post type.) - * - * @return string - */ - public function get_type() { - return 'shop_order_refund'; - } - - /** - * Get status - always completed for refunds. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_status( $context = 'view' ) { - return 'completed'; - } - - /** - * Get a title for the new post type. - */ - public function get_post_title() { - // @codingStandardsIgnoreStart - return sprintf( __( 'Refund – %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Order date parsed by strftime', 'woocommerce' ) ) ); - // @codingStandardsIgnoreEnd - } - - /** - * Get refunded amount. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return int|float - */ - public function get_amount( $context = 'view' ) { - return $this->get_prop( 'amount', $context ); - } - - /** - * Get refund reason. - * - * @since 2.2 - * @param string $context What the value is for. Valid values are view and edit. - * @return int|float - */ - public function get_reason( $context = 'view' ) { - return $this->get_prop( 'reason', $context ); - } - - /** - * Get ID of user who did the refund. - * - * @since 3.0 - * @param string $context What the value is for. Valid values are view and edit. - * @return int - */ - public function get_refunded_by( $context = 'view' ) { - return $this->get_prop( 'refunded_by', $context ); - } - - /** - * Return if the payment was refunded via API. - * - * @since 3.3 - * @param string $context What the value is for. Valid values are view and edit. - * @return bool - */ - public function get_refunded_payment( $context = 'view' ) { - return $this->get_prop( 'refunded_payment', $context ); - } - - /** - * Get formatted refunded amount. - * - * @since 2.4 - * @return string - */ - public function get_formatted_refund_amount() { - return apply_filters( 'woocommerce_formatted_refund_amount', wc_price( $this->get_amount(), array( 'currency' => $this->get_currency() ) ), $this ); - } - - /** - * Set refunded amount. - * - * @param string $value Value to set. - * @throws WC_Data_Exception Exception if the amount is invalid. - */ - public function set_amount( $value ) { - $this->set_prop( 'amount', wc_format_decimal( $value ) ); - } - - /** - * Set refund reason. - * - * @param string $value Value to set. - * @throws WC_Data_Exception Exception if the amount is invalid. - */ - public function set_reason( $value ) { - $this->set_prop( 'reason', $value ); - } - - /** - * Set refunded by. - * - * @param int $value Value to set. - * @throws WC_Data_Exception Exception if the amount is invalid. - */ - public function set_refunded_by( $value ) { - $this->set_prop( 'refunded_by', absint( $value ) ); - } - - /** - * Set if the payment was refunded via API. - * - * @since 3.3 - * @param bool $value Value to set. - */ - public function set_refunded_payment( $value ) { - $this->set_prop( 'refunded_payment', (bool) $value ); - } - - /** - * Magic __get method for backwards compatibility. - * - * @param string $key Value to get. - * @return mixed - */ - public function __get( $key ) { - wc_doing_it_wrong( $key, 'Refund properties should not be accessed directly.', '3.0' ); - /** - * Maps legacy vars to new getters. - */ - if ( 'reason' === $key ) { - return $this->get_reason(); - } elseif ( 'refund_amount' === $key ) { - return $this->get_amount(); - } - return parent::__get( $key ); - } - - /** - * Gets an refund from the database. - * - * @deprecated 3.0 - * @param int $id (default: 0). - * @return bool - */ - public function get_refund( $id = 0 ) { - wc_deprecated_function( 'get_refund', '3.0', 'read' ); - - if ( ! $id ) { - return false; - } - - $result = get_post( $id ); - - if ( $result ) { - $this->populate( $result ); - return true; - } - - return false; - } - - /** - * Get refund amount. - * - * @deprecated 3.0 - * @return int|float - */ - public function get_refund_amount() { - wc_deprecated_function( 'get_refund_amount', '3.0', 'get_amount' ); - return $this->get_amount(); - } - - /** - * Get refund reason. - * - * @deprecated 3.0 - * @return int|float - */ - public function get_refund_reason() { - wc_deprecated_function( 'get_refund_reason', '3.0', 'get_reason' ); - return $this->get_reason(); - } -} diff --git a/includes/class-wc-order.php b/includes/class-wc-order.php deleted file mode 100644 index f33c5db1b36..00000000000 --- a/includes/class-wc-order.php +++ /dev/null @@ -1,2091 +0,0 @@ - 0, - 'status' => '', - 'currency' => '', - 'version' => '', - 'prices_include_tax' => false, - 'date_created' => null, - 'date_modified' => null, - 'discount_total' => 0, - 'discount_tax' => 0, - 'shipping_total' => 0, - 'shipping_tax' => 0, - 'cart_tax' => 0, - 'total' => 0, - 'total_tax' => 0, - - // Order props. - 'customer_id' => 0, - 'order_key' => '', - 'billing' => array( - 'first_name' => '', - 'last_name' => '', - 'company' => '', - 'address_1' => '', - 'address_2' => '', - 'city' => '', - 'state' => '', - 'postcode' => '', - 'country' => '', - 'email' => '', - 'phone' => '', - ), - 'shipping' => array( - 'first_name' => '', - 'last_name' => '', - 'company' => '', - 'address_1' => '', - 'address_2' => '', - 'city' => '', - 'state' => '', - 'postcode' => '', - 'country' => '', - ), - 'payment_method' => '', - 'payment_method_title' => '', - 'transaction_id' => '', - 'customer_ip_address' => '', - 'customer_user_agent' => '', - 'created_via' => '', - 'customer_note' => '', - 'date_completed' => null, - 'date_paid' => null, - 'cart_hash' => '', - ); - - /** - * When a payment is complete this function is called. - * - * Most of the time this should mark an order as 'processing' so that admin can process/post the items. - * If the cart contains only downloadable items then the order is 'completed' since the admin needs to take no action. - * Stock levels are reduced at this point. - * Sales are also recorded for products. - * Finally, record the date of payment. - * - * @param string $transaction_id Optional transaction id to store in post meta. - * @return bool success - */ - public function payment_complete( $transaction_id = '' ) { - if ( ! $this->get_id() ) { // Order must exist. - return false; - } - - try { - do_action( 'woocommerce_pre_payment_complete', $this->get_id() ); - - if ( WC()->session ) { - WC()->session->set( 'order_awaiting_payment', false ); - } - - if ( $this->has_status( apply_filters( 'woocommerce_valid_order_statuses_for_payment_complete', array( 'on-hold', 'pending', 'failed', 'cancelled' ), $this ) ) ) { - if ( ! empty( $transaction_id ) ) { - $this->set_transaction_id( $transaction_id ); - } - if ( ! $this->get_date_paid( 'edit' ) ) { - $this->set_date_paid( time() ); - } - $this->set_status( apply_filters( 'woocommerce_payment_complete_order_status', $this->needs_processing() ? 'processing' : 'completed', $this->get_id(), $this ) ); - $this->save(); - - do_action( 'woocommerce_payment_complete', $this->get_id() ); - } else { - do_action( 'woocommerce_payment_complete_order_status_' . $this->get_status(), $this->get_id() ); - } - } catch ( Exception $e ) { - /** - * If there was an error completing the payment, log to a file and add an order note so the admin can take action. - */ - $logger = wc_get_logger(); - $logger->error( - sprintf( - 'Error completing payment for order #%d', - $this->get_id() - ), - array( - 'order' => $this, - 'error' => $e, - ) - ); - $this->add_order_note( __( 'Payment complete event failed.', 'woocommerce' ) . ' ' . $e->getMessage() ); - return false; - } - return true; - } - - /** - * Gets order total - formatted for display. - * - * @param string $tax_display Type of tax display. - * @param bool $display_refunded If should include refunded value. - * - * @return string - */ - public function get_formatted_order_total( $tax_display = '', $display_refunded = true ) { - $formatted_total = wc_price( $this->get_total(), array( 'currency' => $this->get_currency() ) ); - $order_total = $this->get_total(); - $total_refunded = $this->get_total_refunded(); - $tax_string = ''; - - // Tax for inclusive prices. - if ( wc_tax_enabled() && 'incl' === $tax_display ) { - $tax_string_array = array(); - $tax_totals = $this->get_tax_totals(); - - if ( 'itemized' === get_option( 'woocommerce_tax_total_display' ) ) { - foreach ( $tax_totals as $code => $tax ) { - $tax_amount = ( $total_refunded && $display_refunded ) ? wc_price( WC_Tax::round( $tax->amount - $this->get_total_tax_refunded_by_rate_id( $tax->rate_id ) ), array( 'currency' => $this->get_currency() ) ) : $tax->formatted_amount; - $tax_string_array[] = sprintf( '%s %s', $tax_amount, $tax->label ); - } - } elseif ( ! empty( $tax_totals ) ) { - $tax_amount = ( $total_refunded && $display_refunded ) ? $this->get_total_tax() - $this->get_total_tax_refunded() : $this->get_total_tax(); - $tax_string_array[] = sprintf( '%s %s', wc_price( $tax_amount, array( 'currency' => $this->get_currency() ) ), WC()->countries->tax_or_vat() ); - } - - if ( ! empty( $tax_string_array ) ) { - /* translators: %s: taxes */ - $tax_string = ' ' . sprintf( __( '(includes %s)', 'woocommerce' ), implode( ', ', $tax_string_array ) ) . ''; - } - } - - if ( $total_refunded && $display_refunded ) { - $formatted_total = '' . wp_strip_all_tags( $formatted_total ) . ' ' . wc_price( $order_total - $total_refunded, array( 'currency' => $this->get_currency() ) ) . $tax_string . ''; - } else { - $formatted_total .= $tax_string; - } - - /** - * Filter WooCommerce formatted order total. - * - * @param string $formatted_total Total to display. - * @param WC_Order $order Order data. - * @param string $tax_display Type of tax display. - * @param bool $display_refunded If should include refunded value. - */ - return apply_filters( 'woocommerce_get_formatted_order_total', $formatted_total, $this, $tax_display, $display_refunded ); - } - - /* - |-------------------------------------------------------------------------- - | CRUD methods - |-------------------------------------------------------------------------- - | - | Methods which create, read, update and delete orders from the database. - | Written in abstract fashion so that the way orders are stored can be - | changed more easily in the future. - | - | A save method is included for convenience (chooses update or create based - | on if the order exists yet). - | - */ - - /** - * Save data to the database. - * - * @since 3.0.0 - * @return int order ID - */ - public function save() { - $this->maybe_set_user_billing_email(); - parent::save(); - $this->status_transition(); - - return $this->get_id(); - } - - /** - * Log an error about this order is exception is encountered. - * - * @param Exception $e Exception object. - * @param string $message Message regarding exception thrown. - * @since 3.7.0 - */ - protected function handle_exception( $e, $message = 'Error' ) { - wc_get_logger()->error( - $message, - array( - 'order' => $this, - 'error' => $e, - ) - ); - $this->add_order_note( $message . ' ' . $e->getMessage() ); - } - - /** - * Set order status. - * - * @since 3.0.0 - * @param string $new_status Status to change the order to. No internal wc- prefix is required. - * @param string $note Optional note to add. - * @param bool $manual_update Is this a manual order status change?. - * @return array - */ - public function set_status( $new_status, $note = '', $manual_update = false ) { - $result = parent::set_status( $new_status ); - - if ( true === $this->object_read && ! empty( $result['from'] ) && $result['from'] !== $result['to'] ) { - $this->status_transition = array( - 'from' => ! empty( $this->status_transition['from'] ) ? $this->status_transition['from'] : $result['from'], - 'to' => $result['to'], - 'note' => $note, - 'manual' => (bool) $manual_update, - ); - - if ( $manual_update ) { - do_action( 'woocommerce_order_edit_status', $this->get_id(), $result['to'] ); - } - - $this->maybe_set_date_paid(); - $this->maybe_set_date_completed(); - } - - return $result; - } - - /** - * Maybe set date paid. - * - * Sets the date paid variable when transitioning to the payment complete - * order status. This is either processing or completed. This is not filtered - * to avoid infinite loops e.g. if loading an order via the filter. - * - * Date paid is set once in this manner - only when it is not already set. - * This ensures the data exists even if a gateway does not use the - * `payment_complete` method. - * - * @since 3.0.0 - */ - public function maybe_set_date_paid() { - // This logic only runs if the date_paid prop has not been set yet. - if ( ! $this->get_date_paid( 'edit' ) ) { - $payment_completed_status = apply_filters( 'woocommerce_payment_complete_order_status', $this->needs_processing() ? 'processing' : 'completed', $this->get_id(), $this ); - - if ( $this->has_status( $payment_completed_status ) ) { - // If payment complete status is reached, set paid now. - $this->set_date_paid( time() ); - - } elseif ( 'processing' === $payment_completed_status && $this->has_status( 'completed' ) ) { - // If payment complete status was processing, but we've passed that and still have no date, set it now. - $this->set_date_paid( time() ); - } - } - } - - /** - * Maybe set date completed. - * - * Sets the date completed variable when transitioning to completed status. - * - * @since 3.0.0 - */ - protected function maybe_set_date_completed() { - if ( $this->has_status( 'completed' ) ) { - $this->set_date_completed( time() ); - } - } - - /** - * Updates status of order immediately. - * - * @uses WC_Order::set_status() - * @param string $new_status Status to change the order to. No internal wc- prefix is required. - * @param string $note Optional note to add. - * @param bool $manual Is this a manual order status change?. - * @return bool - */ - public function update_status( $new_status, $note = '', $manual = false ) { - if ( ! $this->get_id() ) { // Order must exist. - return false; - } - - try { - $this->set_status( $new_status, $note, $manual ); - $this->save(); - } catch ( Exception $e ) { - $logger = wc_get_logger(); - $logger->error( - sprintf( - 'Error updating status for order #%d', - $this->get_id() - ), - array( - 'order' => $this, - 'error' => $e, - ) - ); - $this->add_order_note( __( 'Update status event failed.', 'woocommerce' ) . ' ' . $e->getMessage() ); - return false; - } - return true; - } - - /** - * Handle the status transition. - */ - protected function status_transition() { - $status_transition = $this->status_transition; - - // Reset status transition variable. - $this->status_transition = false; - - if ( $status_transition ) { - try { - do_action( 'woocommerce_order_status_' . $status_transition['to'], $this->get_id(), $this ); - - if ( ! empty( $status_transition['from'] ) ) { - /* translators: 1: old order status 2: new order status */ - $transition_note = sprintf( __( 'Order status changed from %1$s to %2$s.', 'woocommerce' ), wc_get_order_status_name( $status_transition['from'] ), wc_get_order_status_name( $status_transition['to'] ) ); - - // Note the transition occurred. - $this->add_status_transition_note( $transition_note, $status_transition ); - - do_action( 'woocommerce_order_status_' . $status_transition['from'] . '_to_' . $status_transition['to'], $this->get_id(), $this ); - do_action( 'woocommerce_order_status_changed', $this->get_id(), $status_transition['from'], $status_transition['to'], $this ); - - // Work out if this was for a payment, and trigger a payment_status hook instead. - if ( - in_array( $status_transition['from'], apply_filters( 'woocommerce_valid_order_statuses_for_payment', array( 'pending', 'failed' ), $this ), true ) - && in_array( $status_transition['to'], wc_get_is_paid_statuses(), true ) - ) { - /** - * Fires when the order progresses from a pending payment status to a paid one. - * - * @since 3.9.0 - * @param int Order ID - * @param WC_Order Order object - */ - do_action( 'woocommerce_order_payment_status_changed', $this->get_id(), $this ); - } - } else { - /* translators: %s: new order status */ - $transition_note = sprintf( __( 'Order status set to %s.', 'woocommerce' ), wc_get_order_status_name( $status_transition['to'] ) ); - - // Note the transition occurred. - $this->add_status_transition_note( $transition_note, $status_transition ); - } - } catch ( Exception $e ) { - $logger = wc_get_logger(); - $logger->error( - sprintf( - 'Status transition of order #%d errored!', - $this->get_id() - ), - array( - 'order' => $this, - 'error' => $e, - ) - ); - $this->add_order_note( __( 'Error during status transition.', 'woocommerce' ) . ' ' . $e->getMessage() ); - } - } - } - - /* - |-------------------------------------------------------------------------- - | Getters - |-------------------------------------------------------------------------- - | - | Methods for getting data from the order object. - | - */ - - /** - * Get basic order data in array format. - * - * @return array - */ - public function get_base_data() { - return array_merge( - array( 'id' => $this->get_id() ), - $this->data, - array( 'number' => $this->get_order_number() ) - ); - } - - /** - * Get all class data in array format. - * - * @since 3.0.0 - * @return array - */ - public function get_data() { - return array_merge( - $this->get_base_data(), - array( - 'meta_data' => $this->get_meta_data(), - 'line_items' => $this->get_items( 'line_item' ), - 'tax_lines' => $this->get_items( 'tax' ), - 'shipping_lines' => $this->get_items( 'shipping' ), - 'fee_lines' => $this->get_items( 'fee' ), - 'coupon_lines' => $this->get_items( 'coupon' ), - ) - ); - } - - /** - * Expands the shipping and billing information in the changes array. - */ - public function get_changes() { - $changed_props = parent::get_changes(); - $subs = array( 'shipping', 'billing' ); - foreach ( $subs as $sub ) { - if ( ! empty( $changed_props[ $sub ] ) ) { - foreach ( $changed_props[ $sub ] as $sub_prop => $value ) { - $changed_props[ $sub . '_' . $sub_prop ] = $value; - } - } - } - if ( isset( $changed_props['customer_note'] ) ) { - $changed_props['post_excerpt'] = $changed_props['customer_note']; - } - return $changed_props; - } - - /** - * Gets the order number for display (by default, order ID). - * - * @return string - */ - public function get_order_number() { - return (string) apply_filters( 'woocommerce_order_number', $this->get_id(), $this ); - } - - /** - * Get order key. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_order_key( $context = 'view' ) { - return $this->get_prop( 'order_key', $context ); - } - - /** - * Get customer_id. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return int - */ - public function get_customer_id( $context = 'view' ) { - return $this->get_prop( 'customer_id', $context ); - } - - /** - * Alias for get_customer_id(). - * - * @param string $context What the value is for. Valid values are view and edit. - * @return int - */ - public function get_user_id( $context = 'view' ) { - return $this->get_customer_id( $context ); - } - - /** - * Get the user associated with the order. False for guests. - * - * @return WP_User|false - */ - public function get_user() { - return $this->get_user_id() ? get_user_by( 'id', $this->get_user_id() ) : false; - } - - /** - * Gets a prop for a getter method. - * - * @since 3.0.0 - * @param string $prop Name of prop to get. - * @param string $address billing or shipping. - * @param string $context What the value is for. Valid values are view and edit. - * @return mixed - */ - protected function get_address_prop( $prop, $address = 'billing', $context = 'view' ) { - $value = null; - - if ( array_key_exists( $prop, $this->data[ $address ] ) ) { - $value = isset( $this->changes[ $address ][ $prop ] ) ? $this->changes[ $address ][ $prop ] : $this->data[ $address ][ $prop ]; - - if ( 'view' === $context ) { - $value = apply_filters( $this->get_hook_prefix() . $address . '_' . $prop, $value, $this ); - } - } - return $value; - } - - /** - * Get billing first name. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_billing_first_name( $context = 'view' ) { - return $this->get_address_prop( 'first_name', 'billing', $context ); - } - - /** - * Get billing last name. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_billing_last_name( $context = 'view' ) { - return $this->get_address_prop( 'last_name', 'billing', $context ); - } - - /** - * Get billing company. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_billing_company( $context = 'view' ) { - return $this->get_address_prop( 'company', 'billing', $context ); - } - - /** - * Get billing address line 1. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_billing_address_1( $context = 'view' ) { - return $this->get_address_prop( 'address_1', 'billing', $context ); - } - - /** - * Get billing address line 2. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_billing_address_2( $context = 'view' ) { - return $this->get_address_prop( 'address_2', 'billing', $context ); - } - - /** - * Get billing city. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_billing_city( $context = 'view' ) { - return $this->get_address_prop( 'city', 'billing', $context ); - } - - /** - * Get billing state. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_billing_state( $context = 'view' ) { - return $this->get_address_prop( 'state', 'billing', $context ); - } - - /** - * Get billing postcode. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_billing_postcode( $context = 'view' ) { - return $this->get_address_prop( 'postcode', 'billing', $context ); - } - - /** - * Get billing country. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_billing_country( $context = 'view' ) { - return $this->get_address_prop( 'country', 'billing', $context ); - } - - /** - * Get billing email. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_billing_email( $context = 'view' ) { - return $this->get_address_prop( 'email', 'billing', $context ); - } - - /** - * Get billing phone. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_billing_phone( $context = 'view' ) { - return $this->get_address_prop( 'phone', 'billing', $context ); - } - - /** - * Get shipping first name. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_shipping_first_name( $context = 'view' ) { - return $this->get_address_prop( 'first_name', 'shipping', $context ); - } - - /** - * Get shipping_last_name. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_shipping_last_name( $context = 'view' ) { - return $this->get_address_prop( 'last_name', 'shipping', $context ); - } - - /** - * Get shipping company. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_shipping_company( $context = 'view' ) { - return $this->get_address_prop( 'company', 'shipping', $context ); - } - - /** - * Get shipping address line 1. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_shipping_address_1( $context = 'view' ) { - return $this->get_address_prop( 'address_1', 'shipping', $context ); - } - - /** - * Get shipping address line 2. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_shipping_address_2( $context = 'view' ) { - return $this->get_address_prop( 'address_2', 'shipping', $context ); - } - - /** - * Get shipping city. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_shipping_city( $context = 'view' ) { - return $this->get_address_prop( 'city', 'shipping', $context ); - } - - /** - * Get shipping state. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_shipping_state( $context = 'view' ) { - return $this->get_address_prop( 'state', 'shipping', $context ); - } - - /** - * Get shipping postcode. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_shipping_postcode( $context = 'view' ) { - return $this->get_address_prop( 'postcode', 'shipping', $context ); - } - - /** - * Get shipping country. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_shipping_country( $context = 'view' ) { - return $this->get_address_prop( 'country', 'shipping', $context ); - } - - /** - * Get the payment method. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_payment_method( $context = 'view' ) { - return $this->get_prop( 'payment_method', $context ); - } - - /** - * Get payment method title. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_payment_method_title( $context = 'view' ) { - return $this->get_prop( 'payment_method_title', $context ); - } - - /** - * Get transaction d. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_transaction_id( $context = 'view' ) { - return $this->get_prop( 'transaction_id', $context ); - } - - /** - * Get customer ip address. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_customer_ip_address( $context = 'view' ) { - return $this->get_prop( 'customer_ip_address', $context ); - } - - /** - * Get customer user agent. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_customer_user_agent( $context = 'view' ) { - return $this->get_prop( 'customer_user_agent', $context ); - } - - /** - * Get created via. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_created_via( $context = 'view' ) { - return $this->get_prop( 'created_via', $context ); - } - - /** - * Get customer note. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_customer_note( $context = 'view' ) { - return $this->get_prop( 'customer_note', $context ); - } - - /** - * Get date completed. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return WC_DateTime|NULL object if the date is set or null if there is no date. - */ - public function get_date_completed( $context = 'view' ) { - return $this->get_prop( 'date_completed', $context ); - } - - /** - * Get date paid. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return WC_DateTime|NULL object if the date is set or null if there is no date. - */ - public function get_date_paid( $context = 'view' ) { - $date_paid = $this->get_prop( 'date_paid', $context ); - - if ( 'view' === $context && ! $date_paid && version_compare( $this->get_version( 'edit' ), '3.0', '<' ) && $this->has_status( apply_filters( 'woocommerce_payment_complete_order_status', $this->needs_processing() ? 'processing' : 'completed', $this->get_id(), $this ) ) ) { - // In view context, return a date if missing. - $date_paid = $this->get_date_created( 'edit' ); - } - return $date_paid; - } - - /** - * Get cart hash. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_cart_hash( $context = 'view' ) { - return $this->get_prop( 'cart_hash', $context ); - } - - /** - * Returns the requested address in raw, non-formatted way. - * Note: Merges raw data with get_prop data so changes are returned too. - * - * @since 2.4.0 - * @param string $type Billing or shipping. Anything else besides 'billing' will return shipping address. - * @return array The stored address after filter. - */ - public function get_address( $type = 'billing' ) { - return apply_filters( 'woocommerce_get_order_address', array_merge( $this->data[ $type ], $this->get_prop( $type, 'view' ) ), $type, $this ); - } - - /** - * Get a formatted shipping address for the order. - * - * @return string - */ - public function get_shipping_address_map_url() { - $address = $this->get_address( 'shipping' ); - - // Remove name and company before generate the Google Maps URL. - unset( $address['first_name'], $address['last_name'], $address['company'] ); - - $address = apply_filters( 'woocommerce_shipping_address_map_url_parts', $address, $this ); - - return apply_filters( 'woocommerce_shipping_address_map_url', 'https://maps.google.com/maps?&q=' . rawurlencode( implode( ', ', $address ) ) . '&z=16', $this ); - } - - /** - * Get a formatted billing full name. - * - * @return string - */ - public function get_formatted_billing_full_name() { - /* translators: 1: first name 2: last name */ - return sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $this->get_billing_first_name(), $this->get_billing_last_name() ); - } - - /** - * Get a formatted shipping full name. - * - * @return string - */ - public function get_formatted_shipping_full_name() { - /* translators: 1: first name 2: last name */ - return sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $this->get_shipping_first_name(), $this->get_shipping_last_name() ); - } - - /** - * Get a formatted billing address for the order. - * - * @param string $empty_content Content to show if no address is present. @since 3.3.0. - * @return string - */ - public function get_formatted_billing_address( $empty_content = '' ) { - $raw_address = apply_filters( 'woocommerce_order_formatted_billing_address', $this->get_address( 'billing' ), $this ); - $address = WC()->countries->get_formatted_address( $raw_address ); - - /** - * Filter orders formatterd billing address. - * - * @since 3.8.0 - * @param string $address Formatted billing address string. - * @param array $raw_address Raw billing address. - * @param WC_Order $order Order data. @since 3.9.0 - */ - return apply_filters( 'woocommerce_order_get_formatted_billing_address', $address ? $address : $empty_content, $raw_address, $this ); - } - - /** - * Get a formatted shipping address for the order. - * - * @param string $empty_content Content to show if no address is present. @since 3.3.0. - * @return string - */ - public function get_formatted_shipping_address( $empty_content = '' ) { - $address = ''; - $raw_address = $this->get_address( 'shipping' ); - - if ( $this->has_shipping_address() ) { - $raw_address = apply_filters( 'woocommerce_order_formatted_shipping_address', $raw_address, $this ); - $address = WC()->countries->get_formatted_address( $raw_address ); - } - - /** - * Filter orders formatterd shipping address. - * - * @since 3.8.0 - * @param string $address Formatted billing address string. - * @param array $raw_address Raw billing address. - * @param WC_Order $order Order data. @since 3.9.0 - */ - return apply_filters( 'woocommerce_order_get_formatted_shipping_address', $address ? $address : $empty_content, $raw_address, $this ); - } - - /** - * Returns true if the order has a billing address. - * - * @since 3.0.4 - * @return boolean - */ - public function has_billing_address() { - return $this->get_billing_address_1() || $this->get_billing_address_2(); - } - - /** - * Returns true if the order has a shipping address. - * - * @since 3.0.4 - * @return boolean - */ - public function has_shipping_address() { - return $this->get_shipping_address_1() || $this->get_shipping_address_2(); - } - - /* - |-------------------------------------------------------------------------- - | Setters - |-------------------------------------------------------------------------- - | - | Functions for setting order data. These should not update anything in the - | database itself and should only change what is stored in the class - | object. However, for backwards compatibility pre 3.0.0 some of these - | setters may handle both. - | - */ - - /** - * Sets a prop for a setter method. - * - * @since 3.0.0 - * @param string $prop Name of prop to set. - * @param string $address Name of address to set. billing or shipping. - * @param mixed $value Value of the prop. - */ - protected function set_address_prop( $prop, $address, $value ) { - if ( array_key_exists( $prop, $this->data[ $address ] ) ) { - if ( true === $this->object_read ) { - if ( $value !== $this->data[ $address ][ $prop ] || ( isset( $this->changes[ $address ] ) && array_key_exists( $prop, $this->changes[ $address ] ) ) ) { - $this->changes[ $address ][ $prop ] = $value; - } - } else { - $this->data[ $address ][ $prop ] = $value; - } - } - } - - /** - * Set order key. - * - * @param string $value Max length 22 chars. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_order_key( $value ) { - $this->set_prop( 'order_key', substr( $value, 0, 22 ) ); - } - - /** - * Set customer id. - * - * @param int $value Customer ID. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_customer_id( $value ) { - $this->set_prop( 'customer_id', absint( $value ) ); - } - - /** - * Set billing first name. - * - * @param string $value Billing first name. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_billing_first_name( $value ) { - $this->set_address_prop( 'first_name', 'billing', $value ); - } - - /** - * Set billing last name. - * - * @param string $value Billing last name. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_billing_last_name( $value ) { - $this->set_address_prop( 'last_name', 'billing', $value ); - } - - /** - * Set billing company. - * - * @param string $value Billing company. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_billing_company( $value ) { - $this->set_address_prop( 'company', 'billing', $value ); - } - - /** - * Set billing address line 1. - * - * @param string $value Billing address line 1. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_billing_address_1( $value ) { - $this->set_address_prop( 'address_1', 'billing', $value ); - } - - /** - * Set billing address line 2. - * - * @param string $value Billing address line 2. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_billing_address_2( $value ) { - $this->set_address_prop( 'address_2', 'billing', $value ); - } - - /** - * Set billing city. - * - * @param string $value Billing city. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_billing_city( $value ) { - $this->set_address_prop( 'city', 'billing', $value ); - } - - /** - * Set billing state. - * - * @param string $value Billing state. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_billing_state( $value ) { - $this->set_address_prop( 'state', 'billing', $value ); - } - - /** - * Set billing postcode. - * - * @param string $value Billing postcode. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_billing_postcode( $value ) { - $this->set_address_prop( 'postcode', 'billing', $value ); - } - - /** - * Set billing country. - * - * @param string $value Billing country. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_billing_country( $value ) { - $this->set_address_prop( 'country', 'billing', $value ); - } - - /** - * Maybe set empty billing email to that of the user who owns the order. - */ - protected function maybe_set_user_billing_email() { - $user = $this->get_user(); - if ( ! $this->get_billing_email() && $user ) { - try { - $this->set_billing_email( $user->user_email ); - } catch ( WC_Data_Exception $e ) { - unset( $e ); - } - } - } - - /** - * Set billing email. - * - * @param string $value Billing email. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_billing_email( $value ) { - if ( $value && ! is_email( $value ) ) { - $this->error( 'order_invalid_billing_email', __( 'Invalid billing email address', 'woocommerce' ) ); - } - $this->set_address_prop( 'email', 'billing', sanitize_email( $value ) ); - } - - /** - * Set billing phone. - * - * @param string $value Billing phone. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_billing_phone( $value ) { - $this->set_address_prop( 'phone', 'billing', $value ); - } - - /** - * Set shipping first name. - * - * @param string $value Shipping first name. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_shipping_first_name( $value ) { - $this->set_address_prop( 'first_name', 'shipping', $value ); - } - - /** - * Set shipping last name. - * - * @param string $value Shipping last name. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_shipping_last_name( $value ) { - $this->set_address_prop( 'last_name', 'shipping', $value ); - } - - /** - * Set shipping company. - * - * @param string $value Shipping company. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_shipping_company( $value ) { - $this->set_address_prop( 'company', 'shipping', $value ); - } - - /** - * Set shipping address line 1. - * - * @param string $value Shipping address line 1. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_shipping_address_1( $value ) { - $this->set_address_prop( 'address_1', 'shipping', $value ); - } - - /** - * Set shipping address line 2. - * - * @param string $value Shipping address line 2. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_shipping_address_2( $value ) { - $this->set_address_prop( 'address_2', 'shipping', $value ); - } - - /** - * Set shipping city. - * - * @param string $value Shipping city. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_shipping_city( $value ) { - $this->set_address_prop( 'city', 'shipping', $value ); - } - - /** - * Set shipping state. - * - * @param string $value Shipping state. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_shipping_state( $value ) { - $this->set_address_prop( 'state', 'shipping', $value ); - } - - /** - * Set shipping postcode. - * - * @param string $value Shipping postcode. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_shipping_postcode( $value ) { - $this->set_address_prop( 'postcode', 'shipping', $value ); - } - - /** - * Set shipping country. - * - * @param string $value Shipping country. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_shipping_country( $value ) { - $this->set_address_prop( 'country', 'shipping', $value ); - } - - /** - * Set the payment method. - * - * @param string $payment_method Supports WC_Payment_Gateway for bw compatibility with < 3.0. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_payment_method( $payment_method = '' ) { - if ( is_object( $payment_method ) ) { - $this->set_payment_method( $payment_method->id ); - $this->set_payment_method_title( $payment_method->get_title() ); - } elseif ( '' === $payment_method ) { - $this->set_prop( 'payment_method', '' ); - $this->set_prop( 'payment_method_title', '' ); - } else { - $this->set_prop( 'payment_method', $payment_method ); - } - } - - /** - * Set payment method title. - * - * @param string $value Payment method title. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_payment_method_title( $value ) { - $this->set_prop( 'payment_method_title', $value ); - } - - /** - * Set transaction id. - * - * @param string $value Transaction id. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_transaction_id( $value ) { - $this->set_prop( 'transaction_id', $value ); - } - - /** - * Set customer ip address. - * - * @param string $value Customer ip address. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_customer_ip_address( $value ) { - $this->set_prop( 'customer_ip_address', $value ); - } - - /** - * Set customer user agent. - * - * @param string $value Customer user agent. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_customer_user_agent( $value ) { - $this->set_prop( 'customer_user_agent', $value ); - } - - /** - * Set created via. - * - * @param string $value Created via. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_created_via( $value ) { - $this->set_prop( 'created_via', $value ); - } - - /** - * Set customer note. - * - * @param string $value Customer note. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_customer_note( $value ) { - $this->set_prop( 'customer_note', $value ); - } - - /** - * Set date completed. - * - * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_date_completed( $date = null ) { - $this->set_date_prop( 'date_completed', $date ); - } - - /** - * Set date paid. - * - * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_date_paid( $date = null ) { - $this->set_date_prop( 'date_paid', $date ); - } - - /** - * Set cart hash. - * - * @param string $value Cart hash. - * @throws WC_Data_Exception Throws exception when invalid data is found. - */ - public function set_cart_hash( $value ) { - $this->set_prop( 'cart_hash', $value ); - } - - /* - |-------------------------------------------------------------------------- - | Conditionals - |-------------------------------------------------------------------------- - | - | Checks if a condition is true or false. - | - */ - - /** - * Check if an order key is valid. - * - * @param string $key Order key. - * @return bool - */ - public function key_is_valid( $key ) { - return hash_equals( $this->get_order_key(), $key ); - } - - /** - * See if order matches cart_hash. - * - * @param string $cart_hash Cart hash. - * @return bool - */ - public function has_cart_hash( $cart_hash = '' ) { - return hash_equals( $this->get_cart_hash(), $cart_hash ); // @codingStandardsIgnoreLine - } - - /** - * Checks if an order can be edited, specifically for use on the Edit Order screen. - * - * @return bool - */ - public function is_editable() { - return apply_filters( 'wc_order_is_editable', in_array( $this->get_status(), array( 'pending', 'on-hold', 'auto-draft' ), true ), $this ); - } - - /** - * Returns if an order has been paid for based on the order status. - * - * @since 2.5.0 - * @return bool - */ - public function is_paid() { - return apply_filters( 'woocommerce_order_is_paid', $this->has_status( wc_get_is_paid_statuses() ), $this ); - } - - /** - * Checks if product download is permitted. - * - * @return bool - */ - public function is_download_permitted() { - return apply_filters( 'woocommerce_order_is_download_permitted', $this->has_status( 'completed' ) || ( 'yes' === get_option( 'woocommerce_downloads_grant_access_after_payment' ) && $this->has_status( 'processing' ) ), $this ); - } - - /** - * Checks if an order needs display the shipping address, based on shipping method. - * - * @return bool - */ - public function needs_shipping_address() { - if ( 'no' === get_option( 'woocommerce_calc_shipping' ) ) { - return false; - } - - $hide = apply_filters( 'woocommerce_order_hide_shipping_address', array( 'local_pickup' ), $this ); - $needs_address = false; - - foreach ( $this->get_shipping_methods() as $shipping_method ) { - $shipping_method_id = $shipping_method->get_method_id(); - - if ( ! in_array( $shipping_method_id, $hide, true ) ) { - $needs_address = true; - break; - } - } - - return apply_filters( 'woocommerce_order_needs_shipping_address', $needs_address, $hide, $this ); - } - - /** - * Returns true if the order contains a downloadable product. - * - * @return bool - */ - public function has_downloadable_item() { - foreach ( $this->get_items() as $item ) { - if ( $item->is_type( 'line_item' ) ) { - $product = $item->get_product(); - - if ( $product && $product->has_file() ) { - return true; - } - } - } - return false; - } - - /** - * Get downloads from all line items for this order. - * - * @since 3.2.0 - * @return array - */ - public function get_downloadable_items() { - $downloads = array(); - - foreach ( $this->get_items() as $item ) { - if ( ! is_object( $item ) ) { - continue; - } - - // Check item refunds. - $refunded_qty = abs( $this->get_qty_refunded_for_item( $item->get_id() ) ); - if ( $refunded_qty && $item->get_quantity() === $refunded_qty ) { - continue; - } - - if ( $item->is_type( 'line_item' ) ) { - $item_downloads = $item->get_item_downloads(); - $product = $item->get_product(); - if ( $product && $item_downloads ) { - foreach ( $item_downloads as $file ) { - $downloads[] = array( - 'download_url' => $file['download_url'], - 'download_id' => $file['id'], - 'product_id' => $product->get_id(), - 'product_name' => $product->get_name(), - 'product_url' => $product->is_visible() ? $product->get_permalink() : '', // Since 3.3.0. - 'download_name' => $file['name'], - 'order_id' => $this->get_id(), - 'order_key' => $this->get_order_key(), - 'downloads_remaining' => $file['downloads_remaining'], - 'access_expires' => $file['access_expires'], - 'file' => array( - 'name' => $file['name'], - 'file' => $file['file'], - ), - ); - } - } - } - } - - return apply_filters( 'woocommerce_order_get_downloadable_items', $downloads, $this ); - } - - /** - * Checks if an order needs payment, based on status and order total. - * - * @return bool - */ - public function needs_payment() { - $valid_order_statuses = apply_filters( 'woocommerce_valid_order_statuses_for_payment', array( 'pending', 'failed' ), $this ); - return apply_filters( 'woocommerce_order_needs_payment', ( $this->has_status( $valid_order_statuses ) && $this->get_total() > 0 ), $this, $valid_order_statuses ); - } - - /** - * See if the order needs processing before it can be completed. - * - * Orders which only contain virtual, downloadable items do not need admin - * intervention. - * - * Uses a transient so these calls are not repeated multiple times, and because - * once the order is processed this code/transient does not need to persist. - * - * @since 3.0.0 - * @return bool - */ - public function needs_processing() { - $transient_name = 'wc_order_' . $this->get_id() . '_needs_processing'; - $needs_processing = get_transient( $transient_name ); - - if ( false === $needs_processing ) { - $needs_processing = 0; - - if ( count( $this->get_items() ) > 0 ) { - foreach ( $this->get_items() as $item ) { - if ( $item->is_type( 'line_item' ) ) { - $product = $item->get_product(); - - if ( ! $product ) { - continue; - } - - $virtual_downloadable_item = $product->is_downloadable() && $product->is_virtual(); - - if ( apply_filters( 'woocommerce_order_item_needs_processing', ! $virtual_downloadable_item, $product, $this->get_id() ) ) { - $needs_processing = 1; - break; - } - } - } - } - - set_transient( $transient_name, $needs_processing, DAY_IN_SECONDS ); - } - - return 1 === absint( $needs_processing ); - } - - /* - |-------------------------------------------------------------------------- - | URLs and Endpoints - |-------------------------------------------------------------------------- - */ - - /** - * Generates a URL so that a customer can pay for their (unpaid - pending) order. Pass 'true' for the checkout version which doesn't offer gateway choices. - * - * @param bool $on_checkout If on checkout. - * @return string - */ - public function get_checkout_payment_url( $on_checkout = false ) { - $pay_url = wc_get_endpoint_url( 'order-pay', $this->get_id(), wc_get_checkout_url() ); - - if ( $on_checkout ) { - $pay_url = add_query_arg( 'key', $this->get_order_key(), $pay_url ); - } else { - $pay_url = add_query_arg( - array( - 'pay_for_order' => 'true', - 'key' => $this->get_order_key(), - ), - $pay_url - ); - } - - return apply_filters( 'woocommerce_get_checkout_payment_url', $pay_url, $this ); - } - - /** - * Generates a URL for the thanks page (order received). - * - * @return string - */ - public function get_checkout_order_received_url() { - $order_received_url = wc_get_endpoint_url( 'order-received', $this->get_id(), wc_get_checkout_url() ); - $order_received_url = add_query_arg( 'key', $this->get_order_key(), $order_received_url ); - - return apply_filters( 'woocommerce_get_checkout_order_received_url', $order_received_url, $this ); - } - - /** - * Generates a URL so that a customer can cancel their (unpaid - pending) order. - * - * @param string $redirect Redirect URL. - * @return string - */ - public function get_cancel_order_url( $redirect = '' ) { - return apply_filters( - 'woocommerce_get_cancel_order_url', - wp_nonce_url( - add_query_arg( - array( - 'cancel_order' => 'true', - 'order' => $this->get_order_key(), - 'order_id' => $this->get_id(), - 'redirect' => $redirect, - ), - $this->get_cancel_endpoint() - ), - 'woocommerce-cancel_order' - ) - ); - } - - /** - * Generates a raw (unescaped) cancel-order URL for use by payment gateways. - * - * @param string $redirect Redirect URL. - * @return string The unescaped cancel-order URL. - */ - public function get_cancel_order_url_raw( $redirect = '' ) { - return apply_filters( - 'woocommerce_get_cancel_order_url_raw', - add_query_arg( - array( - 'cancel_order' => 'true', - 'order' => $this->get_order_key(), - 'order_id' => $this->get_id(), - 'redirect' => $redirect, - '_wpnonce' => wp_create_nonce( 'woocommerce-cancel_order' ), - ), - $this->get_cancel_endpoint() - ) - ); - } - - /** - * Helper method to return the cancel endpoint. - * - * @return string the cancel endpoint; either the cart page or the home page. - */ - public function get_cancel_endpoint() { - $cancel_endpoint = wc_get_cart_url(); - if ( ! $cancel_endpoint ) { - $cancel_endpoint = home_url(); - } - - if ( false === strpos( $cancel_endpoint, '?' ) ) { - $cancel_endpoint = trailingslashit( $cancel_endpoint ); - } - - return $cancel_endpoint; - } - - /** - * Generates a URL to view an order from the my account page. - * - * @return string - */ - public function get_view_order_url() { - return apply_filters( 'woocommerce_get_view_order_url', wc_get_endpoint_url( 'view-order', $this->get_id(), wc_get_page_permalink( 'myaccount' ) ), $this ); - } - - /** - * Get's the URL to edit the order in the backend. - * - * @since 3.3.0 - * @return string - */ - public function get_edit_order_url() { - return apply_filters( 'woocommerce_get_edit_order_url', get_admin_url( null, 'post.php?post=' . $this->get_id() . '&action=edit' ), $this ); - } - - /* - |-------------------------------------------------------------------------- - | Order notes. - |-------------------------------------------------------------------------- - */ - - /** - * Adds a note (comment) to the order. Order must exist. - * - * @param string $note Note to add. - * @param int $is_customer_note Is this a note for the customer?. - * @param bool $added_by_user Was the note added by a user?. - * @return int Comment ID. - */ - public function add_order_note( $note, $is_customer_note = 0, $added_by_user = false ) { - if ( ! $this->get_id() ) { - return 0; - } - - if ( is_user_logged_in() && current_user_can( 'edit_shop_orders', $this->get_id() ) && $added_by_user ) { - $user = get_user_by( 'id', get_current_user_id() ); - $comment_author = $user->display_name; - $comment_author_email = $user->user_email; - } else { - $comment_author = __( 'WooCommerce', 'woocommerce' ); - $comment_author_email = strtolower( __( 'WooCommerce', 'woocommerce' ) ) . '@'; - $comment_author_email .= isset( $_SERVER['HTTP_HOST'] ) ? str_replace( 'www.', '', sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) ) : 'noreply.com'; // WPCS: input var ok. - $comment_author_email = sanitize_email( $comment_author_email ); - } - $commentdata = apply_filters( - 'woocommerce_new_order_note_data', - array( - 'comment_post_ID' => $this->get_id(), - 'comment_author' => $comment_author, - 'comment_author_email' => $comment_author_email, - 'comment_author_url' => '', - 'comment_content' => $note, - 'comment_agent' => 'WooCommerce', - 'comment_type' => 'order_note', - 'comment_parent' => 0, - 'comment_approved' => 1, - ), - array( - 'order_id' => $this->get_id(), - 'is_customer_note' => $is_customer_note, - ) - ); - - $comment_id = wp_insert_comment( $commentdata ); - - if ( $is_customer_note ) { - add_comment_meta( $comment_id, 'is_customer_note', 1 ); - - do_action( - 'woocommerce_new_customer_note', - array( - 'order_id' => $this->get_id(), - 'customer_note' => $commentdata['comment_content'], - ) - ); - } - - /** - * Action hook fired after an order note is added. - * - * @param int $order_note_id Order note ID. - * @param WC_Order $order Order data. - * - * @since 4.4.0 - */ - do_action( 'woocommerce_order_note_added', $comment_id, $this ); - - return $comment_id; - } - - /** - * Add an order note for status transition - * - * @since 3.9.0 - * @uses WC_Order::add_order_note() - * @param string $note Note to be added giving status transition from and to details. - * @param bool $transition Details of the status transition. - * @return int Comment ID. - */ - private function add_status_transition_note( $note, $transition ) { - return $this->add_order_note( trim( $transition['note'] . ' ' . $note ), 0, $transition['manual'] ); - } - - /** - * List order notes (public) for the customer. - * - * @return array - */ - public function get_customer_order_notes() { - $notes = array(); - $args = array( - 'post_id' => $this->get_id(), - 'approve' => 'approve', - 'type' => '', - ); - - remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ) ); - - $comments = get_comments( $args ); - - foreach ( $comments as $comment ) { - if ( ! get_comment_meta( $comment->comment_ID, 'is_customer_note', true ) ) { - continue; - } - $comment->comment_content = make_clickable( $comment->comment_content ); - $notes[] = $comment; - } - - add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ) ); - - return $notes; - } - - /* - |-------------------------------------------------------------------------- - | Refunds - |-------------------------------------------------------------------------- - */ - - /** - * Get order refunds. - * - * @since 2.2 - * @return array of WC_Order_Refund objects - */ - public function get_refunds() { - $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $this->get_id(); - $cached_data = wp_cache_get( $cache_key, $this->cache_group ); - - if ( false !== $cached_data ) { - return $cached_data; - } - - $this->refunds = wc_get_orders( - array( - 'type' => 'shop_order_refund', - 'parent' => $this->get_id(), - 'limit' => -1, - ) - ); - - wp_cache_set( $cache_key, $this->refunds, $this->cache_group ); - - return $this->refunds; - } - - /** - * Get amount already refunded. - * - * @since 2.2 - * @return string - */ - public function get_total_refunded() { - $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'total_refunded' . $this->get_id(); - $cached_data = wp_cache_get( $cache_key, $this->cache_group ); - - if ( false !== $cached_data ) { - return $cached_data; - } - - $total_refunded = $this->data_store->get_total_refunded( $this ); - - wp_cache_set( $cache_key, $total_refunded, $this->cache_group ); - - return $total_refunded; - } - - /** - * Get the total tax refunded. - * - * @since 2.3 - * @return float - */ - public function get_total_tax_refunded() { - $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'total_tax_refunded' . $this->get_id(); - $cached_data = wp_cache_get( $cache_key, $this->cache_group ); - - if ( false !== $cached_data ) { - return $cached_data; - } - - $total_refunded = $this->data_store->get_total_tax_refunded( $this ); - - wp_cache_set( $cache_key, $total_refunded, $this->cache_group ); - - return $total_refunded; - } - - /** - * Get the total shipping refunded. - * - * @since 2.4 - * @return float - */ - public function get_total_shipping_refunded() { - $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'total_shipping_refunded' . $this->get_id(); - $cached_data = wp_cache_get( $cache_key, $this->cache_group ); - - if ( false !== $cached_data ) { - return $cached_data; - } - - $total_refunded = $this->data_store->get_total_shipping_refunded( $this ); - - wp_cache_set( $cache_key, $total_refunded, $this->cache_group ); - - return $total_refunded; - } - - /** - * Gets the count of order items of a certain type that have been refunded. - * - * @since 2.4.0 - * @param string $item_type Item type. - * @return string - */ - public function get_item_count_refunded( $item_type = '' ) { - if ( empty( $item_type ) ) { - $item_type = array( 'line_item' ); - } - if ( ! is_array( $item_type ) ) { - $item_type = array( $item_type ); - } - $count = 0; - - foreach ( $this->get_refunds() as $refund ) { - foreach ( $refund->get_items( $item_type ) as $refunded_item ) { - $count += abs( $refunded_item->get_quantity() ); - } - } - - return apply_filters( 'woocommerce_get_item_count_refunded', $count, $item_type, $this ); - } - - /** - * Get the total number of items refunded. - * - * @since 2.4.0 - * - * @param string $item_type Type of the item we're checking, if not a line_item. - * @return int - */ - public function get_total_qty_refunded( $item_type = 'line_item' ) { - $qty = 0; - foreach ( $this->get_refunds() as $refund ) { - foreach ( $refund->get_items( $item_type ) as $refunded_item ) { - $qty += $refunded_item->get_quantity(); - } - } - return $qty; - } - - /** - * Get the refunded amount for a line item. - * - * @param int $item_id ID of the item we're checking. - * @param string $item_type Type of the item we're checking, if not a line_item. - * @return int - */ - public function get_qty_refunded_for_item( $item_id, $item_type = 'line_item' ) { - $qty = 0; - foreach ( $this->get_refunds() as $refund ) { - foreach ( $refund->get_items( $item_type ) as $refunded_item ) { - if ( absint( $refunded_item->get_meta( '_refunded_item_id' ) ) === $item_id ) { - $qty += $refunded_item->get_quantity(); - } - } - } - return $qty; - } - - /** - * Get the refunded amount for a line item. - * - * @param int $item_id ID of the item we're checking. - * @param string $item_type Type of the item we're checking, if not a line_item. - * @return int - */ - public function get_total_refunded_for_item( $item_id, $item_type = 'line_item' ) { - $total = 0; - foreach ( $this->get_refunds() as $refund ) { - foreach ( $refund->get_items( $item_type ) as $refunded_item ) { - if ( absint( $refunded_item->get_meta( '_refunded_item_id' ) ) === $item_id ) { - $total += $refunded_item->get_total(); - } - } - } - return $total * -1; - } - - /** - * Get the refunded tax amount for a line item. - * - * @param int $item_id ID of the item we're checking. - * @param int $tax_id ID of the tax we're checking. - * @param string $item_type Type of the item we're checking, if not a line_item. - * @return double - */ - public function get_tax_refunded_for_item( $item_id, $tax_id, $item_type = 'line_item' ) { - $total = 0; - foreach ( $this->get_refunds() as $refund ) { - foreach ( $refund->get_items( $item_type ) as $refunded_item ) { - $refunded_item_id = (int) $refunded_item->get_meta( '_refunded_item_id' ); - if ( $refunded_item_id === $item_id ) { - $taxes = $refunded_item->get_taxes(); - $total += isset( $taxes['total'][ $tax_id ] ) ? (float) $taxes['total'][ $tax_id ] : 0; - break; - } - } - } - return wc_round_tax_total( $total ) * -1; - } - - /** - * Get total tax refunded by rate ID. - * - * @param int $rate_id Rate ID. - * @return float - */ - public function get_total_tax_refunded_by_rate_id( $rate_id ) { - $total = 0; - foreach ( $this->get_refunds() as $refund ) { - foreach ( $refund->get_items( 'tax' ) as $refunded_item ) { - if ( absint( $refunded_item->get_rate_id() ) === $rate_id ) { - $total += abs( $refunded_item->get_tax_total() ) + abs( $refunded_item->get_shipping_tax_total() ); - } - } - } - - return $total; - } - - /** - * How much money is left to refund? - * - * @return string - */ - public function get_remaining_refund_amount() { - return wc_format_decimal( $this->get_total() - $this->get_total_refunded(), wc_get_price_decimals() ); - } - - /** - * How many items are left to refund? - * - * @return int - */ - public function get_remaining_refund_items() { - return absint( $this->get_item_count() - $this->get_item_count_refunded() ); - } - - /** - * Add total row for the payment method. - * - * @param array $total_rows Total rows. - * @param string $tax_display Tax to display. - */ - protected function add_order_item_totals_payment_method_row( &$total_rows, $tax_display ) { - if ( $this->get_total() > 0 && $this->get_payment_method_title() && 'other' !== $this->get_payment_method_title() ) { - $total_rows['payment_method'] = array( - 'label' => __( 'Payment method:', 'woocommerce' ), - 'value' => $this->get_payment_method_title(), - ); - } - } - - /** - * Add total row for refunds. - * - * @param array $total_rows Total rows. - * @param string $tax_display Tax to display. - */ - protected function add_order_item_totals_refund_rows( &$total_rows, $tax_display ) { - $refunds = $this->get_refunds(); - if ( $refunds ) { - foreach ( $refunds as $id => $refund ) { - $total_rows[ 'refund_' . $id ] = array( - 'label' => $refund->get_reason() ? $refund->get_reason() : __( 'Refund', 'woocommerce' ) . ':', - 'value' => wc_price( '-' . $refund->get_amount(), array( 'currency' => $this->get_currency() ) ), - ); - } - } - } - - /** - * Get totals for display on pages and in emails. - * - * @param string $tax_display Tax to display. - * @return array - */ - public function get_order_item_totals( $tax_display = '' ) { - $tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' ); - $total_rows = array(); - - $this->add_order_item_totals_subtotal_row( $total_rows, $tax_display ); - $this->add_order_item_totals_discount_row( $total_rows, $tax_display ); - $this->add_order_item_totals_shipping_row( $total_rows, $tax_display ); - $this->add_order_item_totals_fee_rows( $total_rows, $tax_display ); - $this->add_order_item_totals_tax_rows( $total_rows, $tax_display ); - $this->add_order_item_totals_payment_method_row( $total_rows, $tax_display ); - $this->add_order_item_totals_refund_rows( $total_rows, $tax_display ); - $this->add_order_item_totals_total_row( $total_rows, $tax_display ); - - return apply_filters( 'woocommerce_get_order_item_totals', $total_rows, $this, $tax_display ); - } - - /** - * Check if order has been created via admin, checkout, or in another way. - * - * @since 4.0.0 - * @param string $modus Way of creating the order to test for. - * @return bool - */ - public function is_created_via( $modus ) { - return apply_filters( 'woocommerce_order_is_created_via', $modus === $this->get_created_via(), $this, $modus ); - } -} diff --git a/includes/class-wc-payment-gateways.php b/includes/class-wc-payment-gateways.php deleted file mode 100644 index c6114ef68c7..00000000000 --- a/includes/class-wc-payment-gateways.php +++ /dev/null @@ -1,222 +0,0 @@ -init(); - } - - /** - * Load gateways and hook in functions. - */ - public function init() { - $load_gateways = array( - 'WC_Gateway_BACS', - 'WC_Gateway_Cheque', - 'WC_Gateway_COD', - 'WC_Gateway_Paypal', - ); - - // Filter. - $load_gateways = apply_filters( 'woocommerce_payment_gateways', $load_gateways ); - - // Get sort order option. - $ordering = (array) get_option( 'woocommerce_gateway_order' ); - $order_end = 999; - - // Load gateways in order. - foreach ( $load_gateways as $gateway ) { - if ( is_string( $gateway ) && class_exists( $gateway ) ) { - $gateway = new $gateway(); - } - - // Gateways need to be valid and extend WC_Payment_Gateway. - if ( ! is_a( $gateway, 'WC_Payment_Gateway' ) ) { - continue; - } - - if ( isset( $ordering[ $gateway->id ] ) && is_numeric( $ordering[ $gateway->id ] ) ) { - // Add in position. - $this->payment_gateways[ $ordering[ $gateway->id ] ] = $gateway; - } else { - // Add to end of the array. - $this->payment_gateways[ $order_end ] = $gateway; - $order_end++; - } - } - - ksort( $this->payment_gateways ); - } - - /** - * Get gateways. - * - * @return array - */ - public function payment_gateways() { - $_available_gateways = array(); - - if ( count( $this->payment_gateways ) > 0 ) { - foreach ( $this->payment_gateways as $gateway ) { - $_available_gateways[ $gateway->id ] = $gateway; - } - } - - return $_available_gateways; - } - - /** - * Get array of registered gateway ids - * - * @since 2.6.0 - * @return array of strings - */ - public function get_payment_gateway_ids() { - return wp_list_pluck( $this->payment_gateways, 'id' ); - } - - /** - * Get available gateways. - * - * @return array - */ - public function get_available_payment_gateways() { - $_available_gateways = array(); - - foreach ( $this->payment_gateways as $gateway ) { - if ( $gateway->is_available() ) { - if ( ! is_add_payment_method_page() ) { - $_available_gateways[ $gateway->id ] = $gateway; - } elseif ( $gateway->supports( 'add_payment_method' ) || $gateway->supports( 'tokenization' ) ) { - $_available_gateways[ $gateway->id ] = $gateway; - } - } - } - - return array_filter( (array) apply_filters( 'woocommerce_available_payment_gateways', $_available_gateways ), array( $this, 'filter_valid_gateway_class' ) ); - } - - /** - * Callback for array filter. Returns true if gateway is of correct type. - * - * @since 3.6.0 - * @param object $gateway Gateway to check. - * @return bool - */ - protected function filter_valid_gateway_class( $gateway ) { - return $gateway && is_a( $gateway, 'WC_Payment_Gateway' ); - } - - /** - * Set the current, active gateway. - * - * @param array $gateways Available payment gateways. - */ - public function set_current_gateway( $gateways ) { - // Be on the defensive. - if ( ! is_array( $gateways ) || empty( $gateways ) ) { - return; - } - - $current_gateway = false; - - if ( WC()->session ) { - $current = WC()->session->get( 'chosen_payment_method' ); - - if ( $current && isset( $gateways[ $current ] ) ) { - $current_gateway = $gateways[ $current ]; - } - } - - if ( ! $current_gateway ) { - $current_gateway = current( $gateways ); - } - - // Ensure we can make a call to set_current() without triggering an error. - if ( $current_gateway && is_callable( array( $current_gateway, 'set_current' ) ) ) { - $current_gateway->set_current(); - } - } - - /** - * Save options in admin. - */ - public function process_admin_options() { - $gateway_order = isset( $_POST['gateway_order'] ) ? wc_clean( wp_unslash( $_POST['gateway_order'] ) ) : ''; // WPCS: input var ok, CSRF ok. - $order = array(); - - if ( is_array( $gateway_order ) && count( $gateway_order ) > 0 ) { - $loop = 0; - foreach ( $gateway_order as $gateway_id ) { - $order[ esc_attr( $gateway_id ) ] = $loop; - $loop++; - } - } - - update_option( 'woocommerce_gateway_order', $order ); - } -} diff --git a/includes/class-wc-post-data.php b/includes/class-wc-post-data.php deleted file mode 100644 index 5ea84ff351f..00000000000 --- a/includes/class-wc-post-data.php +++ /dev/null @@ -1,564 +0,0 @@ -ID, $post->post_type ) && 'product_variation' === $post->post_type ) { - $variation = wc_get_product( $post->ID ); - - if ( $variation && $variation->get_parent_id() ) { - return $variation->get_permalink(); - } - } - return $permalink; - } - - /** - * Sync products queued to sync. - */ - public static function do_deferred_product_sync() { - global $wc_deferred_product_sync; - - if ( ! empty( $wc_deferred_product_sync ) ) { - $wc_deferred_product_sync = wp_parse_id_list( $wc_deferred_product_sync ); - array_walk( $wc_deferred_product_sync, array( __CLASS__, 'deferred_product_sync' ) ); - } - } - - /** - * Sync a product. - * - * @param int $product_id Product ID. - */ - public static function deferred_product_sync( $product_id ) { - $product = wc_get_product( $product_id ); - - if ( is_callable( array( $product, 'sync' ) ) ) { - $product->sync( $product ); - } - } - - /** - * When a post status changes. - * - * @param string $new_status New status. - * @param string $old_status Old status. - * @param WP_Post $post Post data. - */ - public static function transition_post_status( $new_status, $old_status, $post ) { - if ( ( 'publish' === $new_status || 'publish' === $old_status ) && in_array( $post->post_type, array( 'product', 'product_variation' ), true ) ) { - self::delete_product_query_transients(); - } - } - - /** - * Delete product view transients when needed e.g. when post status changes, or visibility/stock status is modified. - */ - public static function delete_product_query_transients() { - WC_Cache_Helper::get_transient_version( 'product_query', true ); - } - - /** - * Handle type changes. - * - * @since 3.0.0 - * - * @param WC_Product $product Product data. - * @param string $from Origin type. - * @param string $to New type. - */ - public static function product_type_changed( $product, $from, $to ) { - /** - * Filter to prevent variations from being deleted while switching from a variable product type to a variable product type. - * - * @since 5.0.0 - * - * @param bool A boolean value of true will delete the variations. - * @param WC_Product $product Product data. - * @return string $from Origin type. - * @param string $to New type. - */ - if ( apply_filters( 'woocommerce_delete_variations_on_product_type_change', 'variable' === $from && 'variable' !== $to, $product, $from, $to ) ) { - // If the product is no longer variable, we should ensure all variations are removed. - $data_store = WC_Data_Store::load( 'product-variable' ); - $data_store->delete_variations( $product->get_id(), true ); - } - } - - /** - * When editing a term, check for product attributes. - * - * @param int $term_id Term ID. - * @param int $tt_id Term taxonomy ID. - * @param string $taxonomy Taxonomy slug. - */ - public static function edit_term( $term_id, $tt_id, $taxonomy ) { - if ( strpos( $taxonomy, 'pa_' ) === 0 ) { - self::$editing_term = get_term_by( 'id', $term_id, $taxonomy ); - } else { - self::$editing_term = null; - } - } - - /** - * When a term is edited, check for product attributes and update variations. - * - * @param int $term_id Term ID. - * @param int $tt_id Term taxonomy ID. - * @param string $taxonomy Taxonomy slug. - */ - public static function edited_term( $term_id, $tt_id, $taxonomy ) { - if ( ! is_null( self::$editing_term ) && strpos( $taxonomy, 'pa_' ) === 0 ) { - $edited_term = get_term_by( 'id', $term_id, $taxonomy ); - - if ( $edited_term->slug !== self::$editing_term->slug ) { - global $wpdb; - - $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = %s WHERE meta_key = %s AND meta_value = %s;", $edited_term->slug, 'attribute_' . sanitize_title( $taxonomy ), self::$editing_term->slug ) ); - - $wpdb->query( - $wpdb->prepare( - "UPDATE {$wpdb->postmeta} SET meta_value = REPLACE( meta_value, %s, %s ) WHERE meta_key = '_default_attributes'", - serialize( self::$editing_term->taxonomy ) . serialize( self::$editing_term->slug ), - serialize( $edited_term->taxonomy ) . serialize( $edited_term->slug ) - ) - ); - } - } else { - self::$editing_term = null; - } - } - - /** - * Ensure floats are correctly converted to strings based on PHP locale. - * - * @param null $check Whether to allow updating metadata for the given type. - * @param int $object_id Object ID. - * @param string $meta_key Meta key. - * @param mixed $meta_value Meta value. Must be serializable if non-scalar. - * @param mixed $prev_value If specified, only update existing metadata entries with the specified value. Otherwise, update all entries. - * @return null|bool - */ - public static function update_order_item_metadata( $check, $object_id, $meta_key, $meta_value, $prev_value ) { - if ( ! empty( $meta_value ) && is_float( $meta_value ) ) { - - // Convert float to string. - $meta_value = wc_float_to_string( $meta_value ); - - // Update meta value with new string. - update_metadata( 'order_item', $object_id, $meta_key, $meta_value, $prev_value ); - - return true; - } - return $check; - } - - /** - * Ensure floats are correctly converted to strings based on PHP locale. - * - * @param null $check Whether to allow updating metadata for the given type. - * @param int $object_id Object ID. - * @param string $meta_key Meta key. - * @param mixed $meta_value Meta value. Must be serializable if non-scalar. - * @param mixed $prev_value If specified, only update existing metadata entries with the specified value. Otherwise, update all entries. - * @return null|bool - */ - public static function update_post_metadata( $check, $object_id, $meta_key, $meta_value, $prev_value ) { - // Delete product cache if someone uses meta directly. - if ( in_array( get_post_type( $object_id ), array( 'product', 'product_variation' ), true ) ) { - wp_cache_delete( 'product-' . $object_id, 'products' ); - } - - if ( ! empty( $meta_value ) && is_float( $meta_value ) && ! registered_meta_key_exists( 'post', $meta_key ) && in_array( get_post_type( $object_id ), array_merge( wc_get_order_types(), array( 'shop_coupon', 'product', 'product_variation' ) ), true ) ) { - - // Convert float to string. - $meta_value = wc_float_to_string( $meta_value ); - - // Update meta value with new string. - update_metadata( 'post', $object_id, $meta_key, $meta_value, $prev_value ); - - return true; - } - return $check; - } - - /** - * Forces the order posts to have a title in a certain format (containing the date). - * Forces certain product data based on the product's type, e.g. grouped products cannot have a parent. - * - * @param array $data An array of slashed post data. - * @return array - */ - public static function wp_insert_post_data( $data ) { - if ( 'shop_order' === $data['post_type'] && isset( $data['post_date'] ) ) { - $order_title = 'Order'; - if ( $data['post_date'] ) { - $order_title .= ' – ' . date_i18n( 'F j, Y @ h:i A', strtotime( $data['post_date'] ) ); - } - $data['post_title'] = $order_title; - } elseif ( 'product' === $data['post_type'] && isset( $_POST['product-type'] ) ) { // WPCS: input var ok, CSRF ok. - $product_type = wc_clean( wp_unslash( $_POST['product-type'] ) ); // WPCS: input var ok, CSRF ok. - switch ( $product_type ) { - case 'grouped': - case 'variable': - $data['post_parent'] = 0; - break; - } - } elseif ( 'product' === $data['post_type'] && 'auto-draft' === $data['post_status'] ) { - $data['post_title'] = 'AUTO-DRAFT'; - } elseif ( 'shop_coupon' === $data['post_type'] ) { - // Coupons should never allow unfiltered HTML. - $data['post_title'] = wp_filter_kses( $data['post_title'] ); - } - - return $data; - } - - /** - * Change embed data for certain post types. - * - * @since 3.2.0 - * @param array $data The response data. - * @param WP_Post $post The post object. - * @return array - */ - public static function filter_oembed_response_data( $data, $post ) { - if ( in_array( $post->post_type, array( 'shop_order', 'shop_coupon' ), true ) ) { - return array(); - } - return $data; - } - - /** - * Removes variations etc belonging to a deleted post, and clears transients. - * - * @param mixed $id ID of post being deleted. - */ - public static function delete_post( $id ) { - if ( ! current_user_can( 'delete_posts' ) || ! $id ) { - return; - } - - $post_type = get_post_type( $id ); - - switch ( $post_type ) { - case 'product': - $data_store = WC_Data_Store::load( 'product-variable' ); - $data_store->delete_variations( $id, true ); - $data_store->delete_from_lookup_table( $id, 'wc_product_meta_lookup' ); - $parent_id = wp_get_post_parent_id( $id ); - - if ( $parent_id ) { - wc_delete_product_transients( $parent_id ); - } - break; - case 'product_variation': - $data_store = WC_Data_Store::load( 'product' ); - $data_store->delete_from_lookup_table( $id, 'wc_product_meta_lookup' ); - wc_delete_product_transients( wp_get_post_parent_id( $id ) ); - break; - case 'shop_order': - global $wpdb; - - $refunds = $wpdb->get_results( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type = 'shop_order_refund' AND post_parent = %d", $id ) ); - - if ( ! is_null( $refunds ) ) { - foreach ( $refunds as $refund ) { - wp_delete_post( $refund->ID, true ); - } - } - break; - } - } - - /** - * Trash post. - * - * @param mixed $id Post ID. - */ - public static function trash_post( $id ) { - if ( ! $id ) { - return; - } - - $post_type = get_post_type( $id ); - - // If this is an order, trash any refunds too. - if ( in_array( $post_type, wc_get_order_types( 'order-count' ), true ) ) { - global $wpdb; - - $refunds = $wpdb->get_results( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type = 'shop_order_refund' AND post_parent = %d", $id ) ); - - foreach ( $refunds as $refund ) { - $wpdb->update( $wpdb->posts, array( 'post_status' => 'trash' ), array( 'ID' => $refund->ID ) ); - } - - wc_delete_shop_order_transients( $id ); - - // If this is a product, trash children variations. - } elseif ( 'product' === $post_type ) { - $data_store = WC_Data_Store::load( 'product-variable' ); - $data_store->delete_variations( $id, false ); - } - } - - /** - * Untrash post. - * - * @param mixed $id Post ID. - */ - public static function untrash_post( $id ) { - if ( ! $id ) { - return; - } - - $post_type = get_post_type( $id ); - - if ( in_array( $post_type, wc_get_order_types( 'order-count' ), true ) ) { - global $wpdb; - - $refunds = $wpdb->get_results( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type = 'shop_order_refund' AND post_parent = %d", $id ) ); - - foreach ( $refunds as $refund ) { - $wpdb->update( $wpdb->posts, array( 'post_status' => 'wc-completed' ), array( 'ID' => $refund->ID ) ); - } - - wc_delete_shop_order_transients( $id ); - - } elseif ( 'product' === $post_type ) { - $data_store = WC_Data_Store::load( 'product-variable' ); - $data_store->untrash_variations( $id ); - - wc_product_force_unique_sku( $id ); - } - } - - /** - * Before deleting an order, do some cleanup. - * - * @since 3.2.0 - * @param int $order_id Order ID. - */ - public static function before_delete_order( $order_id ) { - if ( in_array( get_post_type( $order_id ), wc_get_order_types(), true ) ) { - // Clean up user. - $order = wc_get_order( $order_id ); - - // Check for `get_customer_id`, since this may be e.g. a refund order (which doesn't implement it). - $customer_id = is_callable( array( $order, 'get_customer_id' ) ) ? $order->get_customer_id() : 0; - - if ( $customer_id > 0 && 'shop_order' === $order->get_type() ) { - $customer = new WC_Customer( $customer_id ); - $order_count = $customer->get_order_count(); - $order_count --; - - if ( 0 === $order_count ) { - $customer->set_is_paying_customer( false ); - $customer->save(); - } - - // Delete order count and last order meta. - delete_user_meta( $customer_id, '_order_count' ); - delete_user_meta( $customer_id, '_last_order' ); - } - - // Clean up items. - self::delete_order_items( $order_id ); - self::delete_order_downloadable_permissions( $order_id ); - } - } - - /** - * Remove item meta on permanent deletion. - * - * @param int $postid Post ID. - */ - public static function delete_order_items( $postid ) { - global $wpdb; - - if ( in_array( get_post_type( $postid ), wc_get_order_types(), true ) ) { - do_action( 'woocommerce_delete_order_items', $postid ); - - $wpdb->query( - " - DELETE {$wpdb->prefix}woocommerce_order_items, {$wpdb->prefix}woocommerce_order_itemmeta - FROM {$wpdb->prefix}woocommerce_order_items - JOIN {$wpdb->prefix}woocommerce_order_itemmeta ON {$wpdb->prefix}woocommerce_order_items.order_item_id = {$wpdb->prefix}woocommerce_order_itemmeta.order_item_id - WHERE {$wpdb->prefix}woocommerce_order_items.order_id = '{$postid}'; - " - ); // WPCS: unprepared SQL ok. - - do_action( 'woocommerce_deleted_order_items', $postid ); - } - } - - /** - * Remove downloadable permissions on permanent order deletion. - * - * @param int $postid Post ID. - */ - public static function delete_order_downloadable_permissions( $postid ) { - if ( in_array( get_post_type( $postid ), wc_get_order_types(), true ) ) { - do_action( 'woocommerce_delete_order_downloadable_permissions', $postid ); - - $data_store = WC_Data_Store::load( 'customer-download' ); - $data_store->delete_by_order_id( $postid ); - - do_action( 'woocommerce_deleted_order_downloadable_permissions', $postid ); - } - } - - /** - * Flush meta cache for CRUD objects on direct update. - * - * @param int $meta_id Meta ID. - * @param int $object_id Object ID. - * @param string $meta_key Meta key. - * @param string $meta_value Meta value. - */ - public static function flush_object_meta_cache( $meta_id, $object_id, $meta_key, $meta_value ) { - WC_Cache_Helper::invalidate_cache_group( 'object_' . $object_id ); - } - - /** - * Ensure default category gets set. - * - * @since 3.3.0 - * @param int $object_id Product ID. - * @param array $terms Terms array. - * @param array $tt_ids Term ids array. - * @param string $taxonomy Taxonomy name. - * @param bool $append Are we appending or setting terms. - */ - public static function force_default_term( $object_id, $terms, $tt_ids, $taxonomy, $append ) { - if ( ! $append && 'product_cat' === $taxonomy && empty( $tt_ids ) && 'product' === get_post_type( $object_id ) ) { - $default_term = absint( get_option( 'default_product_cat', 0 ) ); - $tt_ids = array_map( 'absint', $tt_ids ); - - if ( $default_term && ! in_array( $default_term, $tt_ids, true ) ) { - wp_set_post_terms( $object_id, array( $default_term ), 'product_cat', true ); - } - } - } - - /** - * Ensure statuses are correctly reassigned when restoring orders and products. - * - * @param string $new_status The new status of the post being restored. - * @param int $post_id The ID of the post being restored. - * @param string $previous_status The status of the post at the point where it was trashed. - * @return string - */ - public static function wp_untrash_post_status( $new_status, $post_id, $previous_status ) { - $post_types = array( 'shop_order', 'shop_coupon', 'product', 'product_variation' ); - - if ( in_array( get_post_type( $post_id ), $post_types, true ) ) { - $new_status = $previous_status; - } - - return $new_status; - } - - /** - * When setting stock level, ensure the stock status is kept in sync. - * - * @param int $meta_id Meta ID. - * @param int $object_id Object ID. - * @param string $meta_key Meta key. - * @param mixed $meta_value Meta value. - * @deprecated 3.3 - */ - public static function sync_product_stock_status( $meta_id, $object_id, $meta_key, $meta_value ) {} - - /** - * Update changed downloads. - * - * @deprecated 3.3.0 No action is necessary on changes to download paths since download_id is no longer based on file hash. - * @param int $product_id Product ID. - * @param int $variation_id Variation ID. Optional product variation identifier. - * @param array $downloads Newly set files. - */ - public static function process_product_file_download_paths( $product_id, $variation_id, $downloads ) { - wc_deprecated_function( __FUNCTION__, '3.3' ); - } - - /** - * Delete transients when terms are set. - * - * @deprecated 3.6 - * @param int $object_id Object ID. - * @param mixed $terms An array of object terms. - * @param array $tt_ids An array of term taxonomy IDs. - * @param string $taxonomy Taxonomy slug. - * @param mixed $append Whether to append new terms to the old terms. - * @param array $old_tt_ids Old array of term taxonomy IDs. - */ - public static function set_object_terms( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) { - if ( in_array( get_post_type( $object_id ), array( 'product', 'product_variation' ), true ) ) { - self::delete_product_query_transients(); - } - } -} - -WC_Post_Data::init(); diff --git a/includes/class-wc-post-types.php b/includes/class-wc-post-types.php deleted file mode 100644 index aabd27432c1..00000000000 --- a/includes/class-wc-post-types.php +++ /dev/null @@ -1,677 +0,0 @@ - false, - 'show_ui' => false, - 'show_in_nav_menus' => false, - 'query_var' => is_admin(), - 'rewrite' => false, - 'public' => false, - 'label' => _x( 'Product type', 'Taxonomy name', 'woocommerce' ), - ) - ) - ); - - register_taxonomy( - 'product_visibility', - apply_filters( 'woocommerce_taxonomy_objects_product_visibility', array( 'product', 'product_variation' ) ), - apply_filters( - 'woocommerce_taxonomy_args_product_visibility', - array( - 'hierarchical' => false, - 'show_ui' => false, - 'show_in_nav_menus' => false, - 'query_var' => is_admin(), - 'rewrite' => false, - 'public' => false, - 'label' => _x( 'Product visibility', 'Taxonomy name', 'woocommerce' ), - ) - ) - ); - - register_taxonomy( - 'product_cat', - apply_filters( 'woocommerce_taxonomy_objects_product_cat', array( 'product' ) ), - apply_filters( - 'woocommerce_taxonomy_args_product_cat', - array( - 'hierarchical' => true, - 'update_count_callback' => '_wc_term_recount', - 'label' => __( 'Categories', 'woocommerce' ), - 'labels' => array( - 'name' => __( 'Product categories', 'woocommerce' ), - 'singular_name' => __( 'Category', 'woocommerce' ), - 'menu_name' => _x( 'Categories', 'Admin menu name', 'woocommerce' ), - 'search_items' => __( 'Search categories', 'woocommerce' ), - 'all_items' => __( 'All categories', 'woocommerce' ), - 'parent_item' => __( 'Parent category', 'woocommerce' ), - 'parent_item_colon' => __( 'Parent category:', 'woocommerce' ), - 'edit_item' => __( 'Edit category', 'woocommerce' ), - 'update_item' => __( 'Update category', 'woocommerce' ), - 'add_new_item' => __( 'Add new category', 'woocommerce' ), - 'new_item_name' => __( 'New category name', 'woocommerce' ), - 'not_found' => __( 'No categories found', 'woocommerce' ), - ), - 'show_ui' => true, - 'query_var' => true, - 'capabilities' => array( - 'manage_terms' => 'manage_product_terms', - 'edit_terms' => 'edit_product_terms', - 'delete_terms' => 'delete_product_terms', - 'assign_terms' => 'assign_product_terms', - ), - 'rewrite' => array( - 'slug' => $permalinks['category_rewrite_slug'], - 'with_front' => false, - 'hierarchical' => true, - ), - ) - ) - ); - - register_taxonomy( - 'product_tag', - apply_filters( 'woocommerce_taxonomy_objects_product_tag', array( 'product' ) ), - apply_filters( - 'woocommerce_taxonomy_args_product_tag', - array( - 'hierarchical' => false, - 'update_count_callback' => '_wc_term_recount', - 'label' => __( 'Product tags', 'woocommerce' ), - 'labels' => array( - 'name' => __( 'Product tags', 'woocommerce' ), - 'singular_name' => __( 'Tag', 'woocommerce' ), - 'menu_name' => _x( 'Tags', 'Admin menu name', 'woocommerce' ), - 'search_items' => __( 'Search tags', 'woocommerce' ), - 'all_items' => __( 'All tags', 'woocommerce' ), - 'edit_item' => __( 'Edit tag', 'woocommerce' ), - 'update_item' => __( 'Update tag', 'woocommerce' ), - 'add_new_item' => __( 'Add new tag', 'woocommerce' ), - 'new_item_name' => __( 'New tag name', 'woocommerce' ), - 'popular_items' => __( 'Popular tags', 'woocommerce' ), - 'separate_items_with_commas' => __( 'Separate tags with commas', 'woocommerce' ), - 'add_or_remove_items' => __( 'Add or remove tags', 'woocommerce' ), - 'choose_from_most_used' => __( 'Choose from the most used tags', 'woocommerce' ), - 'not_found' => __( 'No tags found', 'woocommerce' ), - ), - 'show_ui' => true, - 'query_var' => true, - 'capabilities' => array( - 'manage_terms' => 'manage_product_terms', - 'edit_terms' => 'edit_product_terms', - 'delete_terms' => 'delete_product_terms', - 'assign_terms' => 'assign_product_terms', - ), - 'rewrite' => array( - 'slug' => $permalinks['tag_rewrite_slug'], - 'with_front' => false, - ), - ) - ) - ); - - register_taxonomy( - 'product_shipping_class', - apply_filters( 'woocommerce_taxonomy_objects_product_shipping_class', array( 'product', 'product_variation' ) ), - apply_filters( - 'woocommerce_taxonomy_args_product_shipping_class', - array( - 'hierarchical' => false, - 'update_count_callback' => '_update_post_term_count', - 'label' => __( 'Shipping classes', 'woocommerce' ), - 'labels' => array( - 'name' => __( 'Product shipping classes', 'woocommerce' ), - 'singular_name' => __( 'Shipping class', 'woocommerce' ), - 'menu_name' => _x( 'Shipping classes', 'Admin menu name', 'woocommerce' ), - 'search_items' => __( 'Search shipping classes', 'woocommerce' ), - 'all_items' => __( 'All shipping classes', 'woocommerce' ), - 'parent_item' => __( 'Parent shipping class', 'woocommerce' ), - 'parent_item_colon' => __( 'Parent shipping class:', 'woocommerce' ), - 'edit_item' => __( 'Edit shipping class', 'woocommerce' ), - 'update_item' => __( 'Update shipping class', 'woocommerce' ), - 'add_new_item' => __( 'Add new shipping class', 'woocommerce' ), - 'new_item_name' => __( 'New shipping class Name', 'woocommerce' ), - ), - 'show_ui' => false, - 'show_in_quick_edit' => false, - 'show_in_nav_menus' => false, - 'query_var' => is_admin(), - 'capabilities' => array( - 'manage_terms' => 'manage_product_terms', - 'edit_terms' => 'edit_product_terms', - 'delete_terms' => 'delete_product_terms', - 'assign_terms' => 'assign_product_terms', - ), - 'rewrite' => false, - ) - ) - ); - - global $wc_product_attributes; - - $wc_product_attributes = array(); - $attribute_taxonomies = wc_get_attribute_taxonomies(); - - if ( $attribute_taxonomies ) { - foreach ( $attribute_taxonomies as $tax ) { - $name = wc_attribute_taxonomy_name( $tax->attribute_name ); - - if ( $name ) { - $tax->attribute_public = absint( isset( $tax->attribute_public ) ? $tax->attribute_public : 1 ); - $label = ! empty( $tax->attribute_label ) ? $tax->attribute_label : $tax->attribute_name; - $wc_product_attributes[ $name ] = $tax; - $taxonomy_data = array( - 'hierarchical' => false, - 'update_count_callback' => '_update_post_term_count', - 'labels' => array( - /* translators: %s: attribute name */ - 'name' => sprintf( _x( 'Product %s', 'Product Attribute', 'woocommerce' ), $label ), - 'singular_name' => $label, - /* translators: %s: attribute name */ - 'search_items' => sprintf( __( 'Search %s', 'woocommerce' ), $label ), - /* translators: %s: attribute name */ - 'all_items' => sprintf( __( 'All %s', 'woocommerce' ), $label ), - /* translators: %s: attribute name */ - 'parent_item' => sprintf( __( 'Parent %s', 'woocommerce' ), $label ), - /* translators: %s: attribute name */ - 'parent_item_colon' => sprintf( __( 'Parent %s:', 'woocommerce' ), $label ), - /* translators: %s: attribute name */ - 'edit_item' => sprintf( __( 'Edit %s', 'woocommerce' ), $label ), - /* translators: %s: attribute name */ - 'update_item' => sprintf( __( 'Update %s', 'woocommerce' ), $label ), - /* translators: %s: attribute name */ - 'add_new_item' => sprintf( __( 'Add new %s', 'woocommerce' ), $label ), - /* translators: %s: attribute name */ - 'new_item_name' => sprintf( __( 'New %s', 'woocommerce' ), $label ), - /* translators: %s: attribute name */ - 'not_found' => sprintf( __( 'No "%s" found', 'woocommerce' ), $label ), - /* translators: %s: attribute name */ - 'back_to_items' => sprintf( __( '← Back to "%s" attributes', 'woocommerce' ), $label ), - ), - 'show_ui' => true, - 'show_in_quick_edit' => false, - 'show_in_menu' => false, - 'meta_box_cb' => false, - 'query_var' => 1 === $tax->attribute_public, - 'rewrite' => false, - 'sort' => false, - 'public' => 1 === $tax->attribute_public, - 'show_in_nav_menus' => 1 === $tax->attribute_public && apply_filters( 'woocommerce_attribute_show_in_nav_menus', false, $name ), - 'capabilities' => array( - 'manage_terms' => 'manage_product_terms', - 'edit_terms' => 'edit_product_terms', - 'delete_terms' => 'delete_product_terms', - 'assign_terms' => 'assign_product_terms', - ), - ); - - if ( 1 === $tax->attribute_public && sanitize_title( $tax->attribute_name ) ) { - $taxonomy_data['rewrite'] = array( - 'slug' => trailingslashit( $permalinks['attribute_rewrite_slug'] ) . sanitize_title( $tax->attribute_name ), - 'with_front' => false, - 'hierarchical' => true, - ); - } - - register_taxonomy( $name, apply_filters( "woocommerce_taxonomy_objects_{$name}", array( 'product' ) ), apply_filters( "woocommerce_taxonomy_args_{$name}", $taxonomy_data ) ); - } - } - } - - do_action( 'woocommerce_after_register_taxonomy' ); - } - - /** - * Register core post types. - */ - public static function register_post_types() { - if ( ! is_blog_installed() || post_type_exists( 'product' ) ) { - return; - } - - do_action( 'woocommerce_register_post_type' ); - - $permalinks = wc_get_permalink_structure(); - $supports = array( 'title', 'editor', 'excerpt', 'thumbnail', 'custom-fields', 'publicize', 'wpcom-markdown' ); - - if ( 'yes' === get_option( 'woocommerce_enable_reviews', 'yes' ) ) { - $supports[] = 'comments'; - } - - $shop_page_id = wc_get_page_id( 'shop' ); - - if ( current_theme_supports( 'woocommerce' ) ) { - $has_archive = $shop_page_id && get_post( $shop_page_id ) ? urldecode( get_page_uri( $shop_page_id ) ) : 'shop'; - } else { - $has_archive = false; - } - - // If theme support changes, we may need to flush permalinks since some are changed based on this flag. - $theme_support = current_theme_supports( 'woocommerce' ) ? 'yes' : 'no'; - if ( get_option( 'current_theme_supports_woocommerce' ) !== $theme_support && update_option( 'current_theme_supports_woocommerce', $theme_support ) ) { - update_option( 'woocommerce_queue_flush_rewrite_rules', 'yes' ); - } - - register_post_type( - 'product', - apply_filters( - 'woocommerce_register_post_type_product', - array( - 'labels' => array( - 'name' => __( 'Products', 'woocommerce' ), - 'singular_name' => __( 'Product', 'woocommerce' ), - 'all_items' => __( 'All Products', 'woocommerce' ), - 'menu_name' => _x( 'Products', 'Admin menu name', 'woocommerce' ), - 'add_new' => __( 'Add New', 'woocommerce' ), - 'add_new_item' => __( 'Add new product', 'woocommerce' ), - 'edit' => __( 'Edit', 'woocommerce' ), - 'edit_item' => __( 'Edit product', 'woocommerce' ), - 'new_item' => __( 'New product', 'woocommerce' ), - 'view_item' => __( 'View product', 'woocommerce' ), - 'view_items' => __( 'View products', 'woocommerce' ), - 'search_items' => __( 'Search products', 'woocommerce' ), - 'not_found' => __( 'No products found', 'woocommerce' ), - 'not_found_in_trash' => __( 'No products found in trash', 'woocommerce' ), - 'parent' => __( 'Parent product', 'woocommerce' ), - 'featured_image' => __( 'Product image', 'woocommerce' ), - 'set_featured_image' => __( 'Set product image', 'woocommerce' ), - 'remove_featured_image' => __( 'Remove product image', 'woocommerce' ), - 'use_featured_image' => __( 'Use as product image', 'woocommerce' ), - 'insert_into_item' => __( 'Insert into product', 'woocommerce' ), - 'uploaded_to_this_item' => __( 'Uploaded to this product', 'woocommerce' ), - 'filter_items_list' => __( 'Filter products', 'woocommerce' ), - 'items_list_navigation' => __( 'Products navigation', 'woocommerce' ), - 'items_list' => __( 'Products list', 'woocommerce' ), - ), - 'description' => __( 'This is where you can add new products to your store.', 'woocommerce' ), - 'public' => true, - 'show_ui' => true, - 'menu_icon' => 'dashicons-archive', - 'capability_type' => 'product', - 'map_meta_cap' => true, - 'publicly_queryable' => true, - 'exclude_from_search' => false, - 'hierarchical' => false, // Hierarchical causes memory issues - WP loads all records! - 'rewrite' => $permalinks['product_rewrite_slug'] ? array( - 'slug' => $permalinks['product_rewrite_slug'], - 'with_front' => false, - 'feeds' => true, - ) : false, - 'query_var' => true, - 'supports' => $supports, - 'has_archive' => $has_archive, - 'show_in_nav_menus' => true, - 'show_in_rest' => true, - ) - ) - ); - - register_post_type( - 'product_variation', - apply_filters( - 'woocommerce_register_post_type_product_variation', - array( - 'label' => __( 'Variations', 'woocommerce' ), - 'public' => false, - 'hierarchical' => false, - 'supports' => false, - 'capability_type' => 'product', - 'rewrite' => false, - ) - ) - ); - - wc_register_order_type( - 'shop_order', - apply_filters( - 'woocommerce_register_post_type_shop_order', - array( - 'labels' => array( - 'name' => __( 'Orders', 'woocommerce' ), - 'singular_name' => _x( 'Order', 'shop_order post type singular name', 'woocommerce' ), - 'add_new' => __( 'Add order', 'woocommerce' ), - 'add_new_item' => __( 'Add new order', 'woocommerce' ), - 'edit' => __( 'Edit', 'woocommerce' ), - 'edit_item' => __( 'Edit order', 'woocommerce' ), - 'new_item' => __( 'New order', 'woocommerce' ), - 'view_item' => __( 'View order', 'woocommerce' ), - 'search_items' => __( 'Search orders', 'woocommerce' ), - 'not_found' => __( 'No orders found', 'woocommerce' ), - 'not_found_in_trash' => __( 'No orders found in trash', 'woocommerce' ), - 'parent' => __( 'Parent orders', 'woocommerce' ), - 'menu_name' => _x( 'Orders', 'Admin menu name', 'woocommerce' ), - 'filter_items_list' => __( 'Filter orders', 'woocommerce' ), - 'items_list_navigation' => __( 'Orders navigation', 'woocommerce' ), - 'items_list' => __( 'Orders list', 'woocommerce' ), - ), - 'description' => __( 'This is where store orders are stored.', 'woocommerce' ), - 'public' => false, - 'show_ui' => true, - 'capability_type' => 'shop_order', - 'map_meta_cap' => true, - 'publicly_queryable' => false, - 'exclude_from_search' => true, - 'show_in_menu' => current_user_can( 'edit_others_shop_orders' ) ? 'woocommerce' : true, - 'hierarchical' => false, - 'show_in_nav_menus' => false, - 'rewrite' => false, - 'query_var' => false, - 'supports' => array( 'title', 'comments', 'custom-fields' ), - 'has_archive' => false, - ) - ) - ); - - wc_register_order_type( - 'shop_order_refund', - apply_filters( - 'woocommerce_register_post_type_shop_order_refund', - array( - 'label' => __( 'Refunds', 'woocommerce' ), - 'capability_type' => 'shop_order', - 'public' => false, - 'hierarchical' => false, - 'supports' => false, - 'exclude_from_orders_screen' => false, - 'add_order_meta_boxes' => false, - 'exclude_from_order_count' => true, - 'exclude_from_order_views' => false, - 'exclude_from_order_reports' => false, - 'exclude_from_order_sales_reports' => true, - 'class_name' => 'WC_Order_Refund', - 'rewrite' => false, - ) - ) - ); - - if ( 'yes' === get_option( 'woocommerce_enable_coupons' ) ) { - register_post_type( - 'shop_coupon', - apply_filters( - 'woocommerce_register_post_type_shop_coupon', - array( - 'labels' => array( - 'name' => __( 'Coupons', 'woocommerce' ), - 'singular_name' => __( 'Coupon', 'woocommerce' ), - 'menu_name' => _x( 'Coupons', 'Admin menu name', 'woocommerce' ), - 'add_new' => __( 'Add coupon', 'woocommerce' ), - 'add_new_item' => __( 'Add new coupon', 'woocommerce' ), - 'edit' => __( 'Edit', 'woocommerce' ), - 'edit_item' => __( 'Edit coupon', 'woocommerce' ), - 'new_item' => __( 'New coupon', 'woocommerce' ), - 'view_item' => __( 'View coupon', 'woocommerce' ), - 'search_items' => __( 'Search coupons', 'woocommerce' ), - 'not_found' => __( 'No coupons found', 'woocommerce' ), - 'not_found_in_trash' => __( 'No coupons found in trash', 'woocommerce' ), - 'parent' => __( 'Parent coupon', 'woocommerce' ), - 'filter_items_list' => __( 'Filter coupons', 'woocommerce' ), - 'items_list_navigation' => __( 'Coupons navigation', 'woocommerce' ), - 'items_list' => __( 'Coupons list', 'woocommerce' ), - ), - 'description' => __( 'This is where you can add new coupons that customers can use in your store.', 'woocommerce' ), - 'public' => false, - 'show_ui' => true, - 'capability_type' => 'shop_coupon', - 'map_meta_cap' => true, - 'publicly_queryable' => false, - 'exclude_from_search' => true, - 'show_in_menu' => current_user_can( 'edit_others_shop_orders' ) ? 'woocommerce' : true, - 'hierarchical' => false, - 'rewrite' => false, - 'query_var' => false, - 'supports' => array( 'title' ), - 'show_in_nav_menus' => false, - 'show_in_admin_bar' => true, - ) - ) - ); - } - - do_action( 'woocommerce_after_register_post_type' ); - } - - /** - * Customize taxonomies update messages. - * - * @param array $messages The list of available messages. - * @since 4.4.0 - * @return bool - */ - public static function updated_term_messages( $messages ) { - $messages['product_cat'] = array( - 0 => '', - 1 => __( 'Category added.', 'woocommerce' ), - 2 => __( 'Category deleted.', 'woocommerce' ), - 3 => __( 'Category updated.', 'woocommerce' ), - 4 => __( 'Category not added.', 'woocommerce' ), - 5 => __( 'Category not updated.', 'woocommerce' ), - 6 => __( 'Categories deleted.', 'woocommerce' ), - ); - - $messages['product_tag'] = array( - 0 => '', - 1 => __( 'Tag added.', 'woocommerce' ), - 2 => __( 'Tag deleted.', 'woocommerce' ), - 3 => __( 'Tag updated.', 'woocommerce' ), - 4 => __( 'Tag not added.', 'woocommerce' ), - 5 => __( 'Tag not updated.', 'woocommerce' ), - 6 => __( 'Tags deleted.', 'woocommerce' ), - ); - - $wc_product_attributes = array(); - $attribute_taxonomies = wc_get_attribute_taxonomies(); - - if ( $attribute_taxonomies ) { - foreach ( $attribute_taxonomies as $tax ) { - $name = wc_attribute_taxonomy_name( $tax->attribute_name ); - - if ( $name ) { - $label = ! empty( $tax->attribute_label ) ? $tax->attribute_label : $tax->attribute_name; - - $messages[ $name ] = array( - 0 => '', - /* translators: %s: taxonomy label */ - 1 => sprintf( _x( '%s added', 'taxonomy term messages', 'woocommerce' ), $label ), - /* translators: %s: taxonomy label */ - 2 => sprintf( _x( '%s deleted', 'taxonomy term messages', 'woocommerce' ), $label ), - /* translators: %s: taxonomy label */ - 3 => sprintf( _x( '%s updated', 'taxonomy term messages', 'woocommerce' ), $label ), - /* translators: %s: taxonomy label */ - 4 => sprintf( _x( '%s not added', 'taxonomy term messages', 'woocommerce' ), $label ), - /* translators: %s: taxonomy label */ - 5 => sprintf( _x( '%s not updated', 'taxonomy term messages', 'woocommerce' ), $label ), - /* translators: %s: taxonomy label */ - 6 => sprintf( _x( '%s deleted', 'taxonomy term messages', 'woocommerce' ), $label ), - ); - } - } - } - - return $messages; - } - - /** - * Register our custom post statuses, used for order status. - */ - public static function register_post_status() { - - $order_statuses = apply_filters( - 'woocommerce_register_shop_order_post_statuses', - array( - 'wc-pending' => array( - 'label' => _x( 'Pending payment', 'Order status', 'woocommerce' ), - 'public' => false, - 'exclude_from_search' => false, - 'show_in_admin_all_list' => true, - 'show_in_admin_status_list' => true, - /* translators: %s: number of orders */ - 'label_count' => _n_noop( 'Pending payment (%s)', 'Pending payment (%s)', 'woocommerce' ), - ), - 'wc-processing' => array( - 'label' => _x( 'Processing', 'Order status', 'woocommerce' ), - 'public' => false, - 'exclude_from_search' => false, - 'show_in_admin_all_list' => true, - 'show_in_admin_status_list' => true, - /* translators: %s: number of orders */ - 'label_count' => _n_noop( 'Processing (%s)', 'Processing (%s)', 'woocommerce' ), - ), - 'wc-on-hold' => array( - 'label' => _x( 'On hold', 'Order status', 'woocommerce' ), - 'public' => false, - 'exclude_from_search' => false, - 'show_in_admin_all_list' => true, - 'show_in_admin_status_list' => true, - /* translators: %s: number of orders */ - 'label_count' => _n_noop( 'On hold (%s)', 'On hold (%s)', 'woocommerce' ), - ), - 'wc-completed' => array( - 'label' => _x( 'Completed', 'Order status', 'woocommerce' ), - 'public' => false, - 'exclude_from_search' => false, - 'show_in_admin_all_list' => true, - 'show_in_admin_status_list' => true, - /* translators: %s: number of orders */ - 'label_count' => _n_noop( 'Completed (%s)', 'Completed (%s)', 'woocommerce' ), - ), - 'wc-cancelled' => array( - 'label' => _x( 'Cancelled', 'Order status', 'woocommerce' ), - 'public' => false, - 'exclude_from_search' => false, - 'show_in_admin_all_list' => true, - 'show_in_admin_status_list' => true, - /* translators: %s: number of orders */ - 'label_count' => _n_noop( 'Cancelled (%s)', 'Cancelled (%s)', 'woocommerce' ), - ), - 'wc-refunded' => array( - 'label' => _x( 'Refunded', 'Order status', 'woocommerce' ), - 'public' => false, - 'exclude_from_search' => false, - 'show_in_admin_all_list' => true, - 'show_in_admin_status_list' => true, - /* translators: %s: number of orders */ - 'label_count' => _n_noop( 'Refunded (%s)', 'Refunded (%s)', 'woocommerce' ), - ), - 'wc-failed' => array( - 'label' => _x( 'Failed', 'Order status', 'woocommerce' ), - 'public' => false, - 'exclude_from_search' => false, - 'show_in_admin_all_list' => true, - 'show_in_admin_status_list' => true, - /* translators: %s: number of orders */ - 'label_count' => _n_noop( 'Failed (%s)', 'Failed (%s)', 'woocommerce' ), - ), - ) - ); - - foreach ( $order_statuses as $order_status => $values ) { - register_post_status( $order_status, $values ); - } - } - - /** - * Flush rules if the event is queued. - * - * @since 3.3.0 - */ - public static function maybe_flush_rewrite_rules() { - if ( 'yes' === get_option( 'woocommerce_queue_flush_rewrite_rules' ) ) { - update_option( 'woocommerce_queue_flush_rewrite_rules', 'no' ); - self::flush_rewrite_rules(); - } - } - - /** - * Flush rewrite rules. - */ - public static function flush_rewrite_rules() { - flush_rewrite_rules(); - } - - /** - * Disable Gutenberg for products. - * - * @param bool $can_edit Whether the post type can be edited or not. - * @param string $post_type The post type being checked. - * @return bool - */ - public static function gutenberg_can_edit_post_type( $can_edit, $post_type ) { - return 'product' === $post_type ? false : $can_edit; - } - - /** - * Add Product Support to Jetpack Omnisearch. - */ - public static function support_jetpack_omnisearch() { - if ( class_exists( 'Jetpack_Omnisearch_Posts' ) ) { - new Jetpack_Omnisearch_Posts( 'product' ); - } - } - - /** - * Added product for Jetpack related posts. - * - * @param array $post_types Post types. - * @return array - */ - public static function rest_api_allowed_post_types( $post_types ) { - $post_types[] = 'product'; - - return $post_types; - } -} - -WC_Post_types::init(); diff --git a/includes/class-wc-privacy-erasers.php b/includes/class-wc-privacy-erasers.php deleted file mode 100644 index 463e236b524..00000000000 --- a/includes/class-wc-privacy-erasers.php +++ /dev/null @@ -1,412 +0,0 @@ - false, - 'items_retained' => false, - 'messages' => array(), - 'done' => true, - ); - - $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. - - if ( ! $user instanceof WP_User ) { - return $response; - } - - $customer = new WC_Customer( $user->ID ); - - if ( ! $customer ) { - return $response; - } - - $props_to_erase = apply_filters( - 'woocommerce_privacy_erase_customer_personal_data_props', - array( - 'billing_first_name' => __( 'Billing First Name', 'woocommerce' ), - 'billing_last_name' => __( 'Billing Last Name', 'woocommerce' ), - 'billing_company' => __( 'Billing Company', 'woocommerce' ), - 'billing_address_1' => __( 'Billing Address 1', 'woocommerce' ), - 'billing_address_2' => __( 'Billing Address 2', 'woocommerce' ), - 'billing_city' => __( 'Billing City', 'woocommerce' ), - 'billing_postcode' => __( 'Billing Postal/Zip Code', 'woocommerce' ), - 'billing_state' => __( 'Billing State', 'woocommerce' ), - 'billing_country' => __( 'Billing Country / Region', 'woocommerce' ), - 'billing_phone' => __( 'Phone Number', 'woocommerce' ), - 'billing_email' => __( 'Email Address', 'woocommerce' ), - 'shipping_first_name' => __( 'Shipping First Name', 'woocommerce' ), - 'shipping_last_name' => __( 'Shipping Last Name', 'woocommerce' ), - 'shipping_company' => __( 'Shipping Company', 'woocommerce' ), - 'shipping_address_1' => __( 'Shipping Address 1', 'woocommerce' ), - 'shipping_address_2' => __( 'Shipping Address 2', 'woocommerce' ), - 'shipping_city' => __( 'Shipping City', 'woocommerce' ), - 'shipping_postcode' => __( 'Shipping Postal/Zip Code', 'woocommerce' ), - 'shipping_state' => __( 'Shipping State', 'woocommerce' ), - 'shipping_country' => __( 'Shipping Country / Region', 'woocommerce' ), - ), - $customer - ); - - foreach ( $props_to_erase as $prop => $label ) { - $erased = false; - - if ( is_callable( array( $customer, 'get_' . $prop ) ) && is_callable( array( $customer, 'set_' . $prop ) ) ) { - $value = $customer->{"get_$prop"}( 'edit' ); - - if ( $value ) { - $customer->{"set_$prop"}( '' ); - $erased = true; - } - } - - $erased = apply_filters( 'woocommerce_privacy_erase_customer_personal_data_prop', $erased, $prop, $customer ); - - if ( $erased ) { - /* Translators: %s Prop name. */ - $response['messages'][] = sprintf( __( 'Removed customer "%s"', 'woocommerce' ), $label ); - $response['items_removed'] = true; - } - } - - $customer->save(); - - /** - * Allow extensions to remove data for this customer and adjust the response. - * - * @since 3.4.0 - * @param array $response Array resonse data. Must include messages, num_items_removed, num_items_retained, done. - * @param WC_Order $order A customer object. - */ - return apply_filters( 'woocommerce_privacy_erase_personal_data_customer', $response, $customer ); - } - - /** - * Finds and erases data which could be used to identify a person from WooCommerce data assocated with an email address. - * - * Orders are erased in blocks of 10 to avoid timeouts. - * - * @since 3.4.0 - * @param string $email_address The user email address. - * @param int $page Page. - * @return array An array of personal data in name value pairs - */ - public static function order_data_eraser( $email_address, $page ) { - $page = (int) $page; - $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. - $erasure_enabled = wc_string_to_bool( get_option( 'woocommerce_erasure_request_removes_order_data', 'no' ) ); - $response = array( - 'items_removed' => false, - 'items_retained' => false, - 'messages' => array(), - 'done' => true, - ); - - $order_query = array( - 'limit' => 10, - 'page' => $page, - 'customer' => array( $email_address ), - ); - - if ( $user instanceof WP_User ) { - $order_query['customer'][] = (int) $user->ID; - } - - $orders = wc_get_orders( $order_query ); - - if ( 0 < count( $orders ) ) { - foreach ( $orders as $order ) { - if ( apply_filters( 'woocommerce_privacy_erase_order_personal_data', $erasure_enabled, $order ) ) { - self::remove_order_personal_data( $order ); - - /* Translators: %s Order number. */ - $response['messages'][] = sprintf( __( 'Removed personal data from order %s.', 'woocommerce' ), $order->get_order_number() ); - $response['items_removed'] = true; - } else { - /* Translators: %s Order number. */ - $response['messages'][] = sprintf( __( 'Personal data within order %s has been retained.', 'woocommerce' ), $order->get_order_number() ); - $response['items_retained'] = true; - } - } - $response['done'] = 10 > count( $orders ); - } else { - $response['done'] = true; - } - - return $response; - } - - /** - * Finds and removes customer download logs by email address. - * - * @since 3.4.0 - * @param string $email_address The user email address. - * @param int $page Page. - * @return array An array of personal data in name value pairs - */ - public static function download_data_eraser( $email_address, $page ) { - $page = (int) $page; - $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. - $erasure_enabled = wc_string_to_bool( get_option( 'woocommerce_erasure_request_removes_download_data', 'no' ) ); - $response = array( - 'items_removed' => false, - 'items_retained' => false, - 'messages' => array(), - 'done' => true, - ); - - $downloads_query = array( - 'limit' => -1, - 'page' => $page, - 'return' => 'ids', - ); - - if ( $user instanceof WP_User ) { - $downloads_query['user_id'] = (int) $user->ID; - } else { - $downloads_query['user_email'] = $email_address; - } - - $customer_download_data_store = WC_Data_Store::load( 'customer-download' ); - - // Revoke download permissions. - if ( apply_filters( 'woocommerce_privacy_erase_download_personal_data', $erasure_enabled, $email_address ) ) { - if ( $user instanceof WP_User ) { - $result = $customer_download_data_store->delete_by_user_id( (int) $user->ID ); - } else { - $result = $customer_download_data_store->delete_by_user_email( $email_address ); - } - if ( $result ) { - $response['messages'][] = __( 'Removed access to downloadable files.', 'woocommerce' ); - $response['items_removed'] = true; - } - } else { - $response['messages'][] = __( 'Customer download permissions have been retained.', 'woocommerce' ); - $response['items_retained'] = true; - } - - return $response; - } - - /** - * Remove personal data specific to WooCommerce from an order object. - * - * Note; this will hinder order processing for obvious reasons! - * - * @param WC_Order $order Order object. - */ - public static function remove_order_personal_data( $order ) { - $anonymized_data = array(); - - /** - * Allow extensions to remove their own personal data for this order first, so order data is still available. - * - * @since 3.4.0 - * @param WC_Order $order A customer object. - */ - do_action( 'woocommerce_privacy_before_remove_order_personal_data', $order ); - - /** - * Expose props and data types we'll be anonymizing. - * - * @since 3.4.0 - * @param array $props Keys are the prop names, values are the data type we'll be passing to wp_privacy_anonymize_data(). - * @param WC_Order $order A customer object. - */ - $props_to_remove = apply_filters( - 'woocommerce_privacy_remove_order_personal_data_props', - array( - 'customer_ip_address' => 'ip', - 'customer_user_agent' => 'text', - 'billing_first_name' => 'text', - 'billing_last_name' => 'text', - 'billing_company' => 'text', - 'billing_address_1' => 'text', - 'billing_address_2' => 'text', - 'billing_city' => 'text', - 'billing_postcode' => 'text', - 'billing_state' => 'address_state', - 'billing_country' => 'address_country', - 'billing_phone' => 'phone', - 'billing_email' => 'email', - 'shipping_first_name' => 'text', - 'shipping_last_name' => 'text', - 'shipping_company' => 'text', - 'shipping_address_1' => 'text', - 'shipping_address_2' => 'text', - 'shipping_city' => 'text', - 'shipping_postcode' => 'text', - 'shipping_state' => 'address_state', - 'shipping_country' => 'address_country', - 'customer_id' => 'numeric_id', - 'transaction_id' => 'numeric_id', - ), - $order - ); - - if ( ! empty( $props_to_remove ) && is_array( $props_to_remove ) ) { - foreach ( $props_to_remove as $prop => $data_type ) { - // Get the current value in edit context. - $value = $order->{"get_$prop"}( 'edit' ); - - // If the value is empty, it does not need to be anonymized. - if ( empty( $value ) || empty( $data_type ) ) { - continue; - } - - $anon_value = function_exists( 'wp_privacy_anonymize_data' ) ? wp_privacy_anonymize_data( $data_type, $value ) : ''; - - /** - * Expose a way to control the anonymized value of a prop via 3rd party code. - * - * @since 3.4.0 - * @param string $anon_value Value of this prop after anonymization. - * @param string $prop Name of the prop being removed. - * @param string $value Current value of the data. - * @param string $data_type Type of data. - * @param WC_Order $order An order object. - */ - $anonymized_data[ $prop ] = apply_filters( 'woocommerce_privacy_remove_order_personal_data_prop_value', $anon_value, $prop, $value, $data_type, $order ); - } - } - - // Set all new props and persist the new data to the database. - $order->set_props( $anonymized_data ); - - // Remove meta data. - $meta_to_remove = apply_filters( - 'woocommerce_privacy_remove_order_personal_data_meta', - array( - 'Payer first name' => 'text', - 'Payer last name' => 'text', - 'Payer PayPal address' => 'email', - 'Transaction ID' => 'numeric_id', - ) - ); - - if ( ! empty( $meta_to_remove ) && is_array( $meta_to_remove ) ) { - foreach ( $meta_to_remove as $meta_key => $data_type ) { - $value = $order->get_meta( $meta_key ); - - // If the value is empty, it does not need to be anonymized. - if ( empty( $value ) || empty( $data_type ) ) { - continue; - } - - $anon_value = function_exists( 'wp_privacy_anonymize_data' ) ? wp_privacy_anonymize_data( $data_type, $value ) : ''; - - /** - * Expose a way to control the anonymized value of a value via 3rd party code. - * - * @since 3.4.0 - * @param string $anon_value Value of this data after anonymization. - * @param string $prop meta_key key being removed. - * @param string $value Current value of the data. - * @param string $data_type Type of data. - * @param WC_Order $order An order object. - */ - $anon_value = apply_filters( 'woocommerce_privacy_remove_order_personal_data_meta_value', $anon_value, $meta_key, $value, $data_type, $order ); - - if ( $anon_value ) { - $order->update_meta_data( $meta_key, $anon_value ); - } else { - $order->delete_meta_data( $meta_key ); - } - } - } - - $order->update_meta_data( '_anonymized', 'yes' ); - $order->save(); - - // Delete order notes which can contain PII. - $notes = wc_get_order_notes( - array( - 'order_id' => $order->get_id(), - ) - ); - - foreach ( $notes as $note ) { - wc_delete_order_note( $note->id ); - } - - // Add note that this event occured. - $order->add_order_note( __( 'Personal data removed.', 'woocommerce' ) ); - - /** - * Allow extensions to remove their own personal data for this order. - * - * @since 3.4.0 - * @param WC_Order $order A customer object. - */ - do_action( 'woocommerce_privacy_remove_order_personal_data', $order ); - } - - /** - * Finds and erases customer tokens by email address. - * - * @since 3.4.0 - * @param string $email_address The user email address. - * @param int $page Page. - * @return array An array of personal data in name value pairs - */ - public static function customer_tokens_eraser( $email_address, $page ) { - $response = array( - 'items_removed' => false, - 'items_retained' => false, - 'messages' => array(), - 'done' => true, - ); - - $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. - - if ( ! $user instanceof WP_User ) { - return $response; - } - - $tokens = WC_Payment_Tokens::get_tokens( - array( - 'user_id' => $user->ID, - ) - ); - - if ( empty( $tokens ) ) { - return $response; - } - - foreach ( $tokens as $token ) { - WC_Payment_Tokens::delete( $token->get_id() ); - - /* Translators: %s Prop name. */ - $response['messages'][] = sprintf( __( 'Removed payment token "%d"', 'woocommerce' ), $token->get_id() ); - $response['items_removed'] = true; - } - - /** - * Allow extensions to remove data for tokens and adjust the response. - * - * @since 3.4.0 - * @param array $response Array resonse data. Must include messages, num_items_removed, num_items_retained, done. - * @param array $tokens Array of tokens. - */ - return apply_filters( 'woocommerce_privacy_erase_personal_data_tokens', $response, $tokens ); - } -} diff --git a/includes/class-wc-privacy-exporters.php b/includes/class-wc-privacy-exporters.php deleted file mode 100644 index b248de85d83..00000000000 --- a/includes/class-wc-privacy-exporters.php +++ /dev/null @@ -1,442 +0,0 @@ - 'woocommerce_customer', - 'group_label' => __( 'Customer Data', 'woocommerce' ), - 'group_description' => __( 'User’s WooCommerce customer data.', 'woocommerce' ), - 'item_id' => 'user', - 'data' => $customer_personal_data, - ); - } - } - - return array( - 'data' => $data_to_export, - 'done' => true, - ); - } - - /** - * Finds and exports data which could be used to identify a person from WooCommerce data associated with an email address. - * - * Orders are exported in blocks of 10 to avoid timeouts. - * - * @since 3.4.0 - * @param string $email_address The user email address. - * @param int $page Page. - * @return array An array of personal data in name value pairs - */ - public static function order_data_exporter( $email_address, $page ) { - $done = true; - $page = (int) $page; - $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. - $data_to_export = array(); - $order_query = array( - 'limit' => 10, - 'page' => $page, - 'customer' => array( $email_address ), - ); - - if ( $user instanceof WP_User ) { - $order_query['customer'][] = (int) $user->ID; - } - - $orders = wc_get_orders( $order_query ); - - if ( 0 < count( $orders ) ) { - foreach ( $orders as $order ) { - $data_to_export[] = array( - 'group_id' => 'woocommerce_orders', - 'group_label' => __( 'Orders', 'woocommerce' ), - 'group_description' => __( 'User’s WooCommerce orders data.', 'woocommerce' ), - 'item_id' => 'order-' . $order->get_id(), - 'data' => self::get_order_personal_data( $order ), - ); - } - $done = 10 > count( $orders ); - } - - return array( - 'data' => $data_to_export, - 'done' => $done, - ); - } - - /** - * Finds and exports customer download logs by email address. - * - * @since 3.4.0 - * @param string $email_address The user email address. - * @param int $page Page. - * @throws Exception When WC_Data_Store validation fails. - * @return array An array of personal data in name value pairs - */ - public static function download_data_exporter( $email_address, $page ) { - $done = true; - $page = (int) $page; - $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. - $data_to_export = array(); - $downloads_query = array( - 'limit' => 10, - 'page' => $page, - ); - - if ( $user instanceof WP_User ) { - $downloads_query['user_id'] = (int) $user->ID; - } else { - $downloads_query['user_email'] = $email_address; - } - - $customer_download_data_store = WC_Data_Store::load( 'customer-download' ); - $customer_download_log_data_store = WC_Data_Store::load( 'customer-download-log' ); - $downloads = $customer_download_data_store->get_downloads( $downloads_query ); - - if ( 0 < count( $downloads ) ) { - foreach ( $downloads as $download ) { - $data_to_export[] = array( - 'group_id' => 'woocommerce_downloads', - /* translators: This is the headline for a list of downloads purchased from the store for a given user. */ - 'group_label' => __( 'Purchased Downloads', 'woocommerce' ), - 'group_description' => __( 'User’s WooCommerce purchased downloads data.', 'woocommerce' ), - 'item_id' => 'download-' . $download->get_id(), - 'data' => self::get_download_personal_data( $download ), - ); - - $download_logs = $customer_download_log_data_store->get_download_logs_for_permission( $download->get_id() ); - - foreach ( $download_logs as $download_log ) { - $data_to_export[] = array( - 'group_id' => 'woocommerce_download_logs', - /* translators: This is the headline for a list of access logs for downloads purchased from the store for a given user. */ - 'group_label' => __( 'Access to Purchased Downloads', 'woocommerce' ), - 'group_description' => __( 'User’s WooCommerce access to purchased downloads data.', 'woocommerce' ), - 'item_id' => 'download-log-' . $download_log->get_id(), - 'data' => array( - array( - 'name' => __( 'Download ID', 'woocommerce' ), - 'value' => $download_log->get_permission_id(), - ), - array( - 'name' => __( 'Timestamp', 'woocommerce' ), - 'value' => $download_log->get_timestamp(), - ), - array( - 'name' => __( 'IP Address', 'woocommerce' ), - 'value' => $download_log->get_user_ip_address(), - ), - ), - ); - } - } - $done = 10 > count( $downloads ); - } - - return array( - 'data' => $data_to_export, - 'done' => $done, - ); - } - - /** - * Get personal data (key/value pairs) for a user object. - * - * @since 3.4.0 - * @param WP_User $user user object. - * @throws Exception If customer cannot be read/found and $data is set to WC_Customer class. - * @return array - */ - protected static function get_customer_personal_data( $user ) { - $personal_data = array(); - $customer = new WC_Customer( $user->ID ); - - if ( ! $customer ) { - return array(); - } - - $props_to_export = apply_filters( - 'woocommerce_privacy_export_customer_personal_data_props', - array( - 'billing_first_name' => __( 'Billing First Name', 'woocommerce' ), - 'billing_last_name' => __( 'Billing Last Name', 'woocommerce' ), - 'billing_company' => __( 'Billing Company', 'woocommerce' ), - 'billing_address_1' => __( 'Billing Address 1', 'woocommerce' ), - 'billing_address_2' => __( 'Billing Address 2', 'woocommerce' ), - 'billing_city' => __( 'Billing City', 'woocommerce' ), - 'billing_postcode' => __( 'Billing Postal/Zip Code', 'woocommerce' ), - 'billing_state' => __( 'Billing State', 'woocommerce' ), - 'billing_country' => __( 'Billing Country / Region', 'woocommerce' ), - 'billing_phone' => __( 'Phone Number', 'woocommerce' ), - 'billing_email' => __( 'Email Address', 'woocommerce' ), - 'shipping_first_name' => __( 'Shipping First Name', 'woocommerce' ), - 'shipping_last_name' => __( 'Shipping Last Name', 'woocommerce' ), - 'shipping_company' => __( 'Shipping Company', 'woocommerce' ), - 'shipping_address_1' => __( 'Shipping Address 1', 'woocommerce' ), - 'shipping_address_2' => __( 'Shipping Address 2', 'woocommerce' ), - 'shipping_city' => __( 'Shipping City', 'woocommerce' ), - 'shipping_postcode' => __( 'Shipping Postal/Zip Code', 'woocommerce' ), - 'shipping_state' => __( 'Shipping State', 'woocommerce' ), - 'shipping_country' => __( 'Shipping Country / Region', 'woocommerce' ), - ), - $customer - ); - - foreach ( $props_to_export as $prop => $description ) { - $value = ''; - - if ( is_callable( array( $customer, 'get_' . $prop ) ) ) { - $value = $customer->{"get_$prop"}( 'edit' ); - } - - $value = apply_filters( 'woocommerce_privacy_export_customer_personal_data_prop_value', $value, $prop, $customer ); - - if ( $value ) { - $personal_data[] = array( - 'name' => $description, - 'value' => $value, - ); - } - } - - /** - * Allow extensions to register their own personal data for this customer for the export. - * - * @since 3.4.0 - * @param array $personal_data Array of name value pairs. - * @param WC_Order $order A customer object. - */ - $personal_data = apply_filters( 'woocommerce_privacy_export_customer_personal_data', $personal_data, $customer ); - - return $personal_data; - } - - /** - * Get personal data (key/value pairs) for an order object. - * - * @since 3.4.0 - * @param WC_Order $order Order object. - * @return array - */ - protected static function get_order_personal_data( $order ) { - $personal_data = array(); - $props_to_export = apply_filters( - 'woocommerce_privacy_export_order_personal_data_props', - array( - 'order_number' => __( 'Order Number', 'woocommerce' ), - 'date_created' => __( 'Order Date', 'woocommerce' ), - 'total' => __( 'Order Total', 'woocommerce' ), - 'items' => __( 'Items Purchased', 'woocommerce' ), - 'customer_ip_address' => __( 'IP Address', 'woocommerce' ), - 'customer_user_agent' => __( 'Browser User Agent', 'woocommerce' ), - 'formatted_billing_address' => __( 'Billing Address', 'woocommerce' ), - 'formatted_shipping_address' => __( 'Shipping Address', 'woocommerce' ), - 'billing_phone' => __( 'Phone Number', 'woocommerce' ), - 'billing_email' => __( 'Email Address', 'woocommerce' ), - ), - $order - ); - - foreach ( $props_to_export as $prop => $name ) { - $value = ''; - - switch ( $prop ) { - case 'items': - $item_names = array(); - foreach ( $order->get_items() as $item ) { - $item_names[] = $item->get_name() . ' x ' . $item->get_quantity(); - } - $value = implode( ', ', $item_names ); - break; - case 'date_created': - $value = wc_format_datetime( $order->get_date_created(), get_option( 'date_format' ) . ', ' . get_option( 'time_format' ) ); - break; - case 'formatted_billing_address': - case 'formatted_shipping_address': - $value = preg_replace( '##i', ', ', $order->{"get_$prop"}() ); - break; - default: - if ( is_callable( array( $order, 'get_' . $prop ) ) ) { - $value = $order->{"get_$prop"}(); - } - break; - } - - $value = apply_filters( 'woocommerce_privacy_export_order_personal_data_prop', $value, $prop, $order ); - - if ( $value ) { - $personal_data[] = array( - 'name' => $name, - 'value' => $value, - ); - } - } - - // Export meta data. - $meta_to_export = apply_filters( - 'woocommerce_privacy_export_order_personal_data_meta', - array( - 'Payer first name' => __( 'Payer first name', 'woocommerce' ), - 'Payer last name' => __( 'Payer last name', 'woocommerce' ), - 'Payer PayPal address' => __( 'Payer PayPal address', 'woocommerce' ), - 'Transaction ID' => __( 'Transaction ID', 'woocommerce' ), - ) - ); - - if ( ! empty( $meta_to_export ) && is_array( $meta_to_export ) ) { - foreach ( $meta_to_export as $meta_key => $name ) { - $value = apply_filters( 'woocommerce_privacy_export_order_personal_data_meta_value', $order->get_meta( $meta_key ), $meta_key, $order ); - - if ( $value ) { - $personal_data[] = array( - 'name' => $name, - 'value' => $value, - ); - } - } - } - - /** - * Allow extensions to register their own personal data for this order for the export. - * - * @since 3.4.0 - * @param array $personal_data Array of name value pairs to expose in the export. - * @param WC_Order $order An order object. - */ - $personal_data = apply_filters( 'woocommerce_privacy_export_order_personal_data', $personal_data, $order ); - - return $personal_data; - } - - /** - * Get personal data (key/value pairs) for a download object. - * - * @since 3.4.0 - * @param WC_Order $download Download object. - * @return array - */ - protected static function get_download_personal_data( $download ) { - $personal_data = array( - array( - 'name' => __( 'Download ID', 'woocommerce' ), - 'value' => $download->get_id(), - ), - array( - 'name' => __( 'Order ID', 'woocommerce' ), - 'value' => $download->get_order_id(), - ), - array( - 'name' => __( 'Product', 'woocommerce' ), - 'value' => get_the_title( $download->get_product_id() ), - ), - array( - 'name' => __( 'User email', 'woocommerce' ), - 'value' => $download->get_user_email(), - ), - array( - 'name' => __( 'Downloads remaining', 'woocommerce' ), - 'value' => $download->get_downloads_remaining(), - ), - array( - 'name' => __( 'Download count', 'woocommerce' ), - 'value' => $download->get_download_count(), - ), - array( - 'name' => __( 'Access granted', 'woocommerce' ), - 'value' => date( 'Y-m-d', $download->get_access_granted( 'edit' )->getTimestamp() ), - ), - array( - 'name' => __( 'Access expires', 'woocommerce' ), - 'value' => ! is_null( $download->get_access_expires( 'edit' ) ) ? date( 'Y-m-d', $download->get_access_expires( 'edit' )->getTimestamp() ) : null, - ), - ); - - /** - * Allow extensions to register their own personal data for this download for the export. - * - * @since 3.4.0 - * @param array $personal_data Array of name value pairs to expose in the export. - * @param WC_Order $order An order object. - */ - $personal_data = apply_filters( 'woocommerce_privacy_export_download_personal_data', $personal_data, $download ); - - return $personal_data; - } - - /** - * Finds and exports payment tokens by email address for a customer. - * - * @since 3.4.0 - * @param string $email_address The user email address. - * @param int $page Page. - * @return array An array of personal data in name value pairs - */ - public static function customer_tokens_exporter( $email_address, $page ) { - $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. - $data_to_export = array(); - - if ( ! $user instanceof WP_User ) { - return array( - 'data' => $data_to_export, - 'done' => true, - ); - } - - $tokens = WC_Payment_Tokens::get_tokens( - array( - 'user_id' => $user->ID, - 'limit' => 10, - 'page' => $page, - ) - ); - - if ( 0 < count( $tokens ) ) { - foreach ( $tokens as $token ) { - $data_to_export[] = array( - 'group_id' => 'woocommerce_tokens', - 'group_label' => __( 'Payment Tokens', 'woocommerce' ), - 'group_description' => __( 'User’s WooCommerce payment tokens data.', 'woocommerce' ), - 'item_id' => 'token-' . $token->get_id(), - 'data' => array( - array( - 'name' => __( 'Token', 'woocommerce' ), - 'value' => $token->get_display_name(), - ), - ), - ); - } - $done = 10 > count( $tokens ); - } else { - $done = true; - } - - return array( - 'data' => $data_to_export, - 'done' => $done, - ); - } -} diff --git a/includes/class-wc-privacy.php b/includes/class-wc-privacy.php deleted file mode 100644 index 232e1916503..00000000000 --- a/includes/class-wc-privacy.php +++ /dev/null @@ -1,393 +0,0 @@ -name = __( 'WooCommerce', 'woocommerce' ); - - if ( ! self::$background_process ) { - self::$background_process = new WC_Privacy_Background_Process(); - } - - // Include supporting classes. - include_once __DIR__ . '/class-wc-privacy-erasers.php'; - include_once __DIR__ . '/class-wc-privacy-exporters.php'; - - // This hook registers WooCommerce data exporters. - $this->add_exporter( 'woocommerce-customer-data', __( 'WooCommerce Customer Data', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'customer_data_exporter' ) ); - $this->add_exporter( 'woocommerce-customer-orders', __( 'WooCommerce Customer Orders', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'order_data_exporter' ) ); - $this->add_exporter( 'woocommerce-customer-downloads', __( 'WooCommerce Customer Downloads', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'download_data_exporter' ) ); - $this->add_exporter( 'woocommerce-customer-tokens', __( 'WooCommerce Customer Payment Tokens', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'customer_tokens_exporter' ) ); - - // This hook registers WooCommerce data erasers. - $this->add_eraser( 'woocommerce-customer-data', __( 'WooCommerce Customer Data', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'customer_data_eraser' ) ); - $this->add_eraser( 'woocommerce-customer-orders', __( 'WooCommerce Customer Orders', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'order_data_eraser' ) ); - $this->add_eraser( 'woocommerce-customer-downloads', __( 'WooCommerce Customer Downloads', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'download_data_eraser' ) ); - $this->add_eraser( 'woocommerce-customer-tokens', __( 'WooCommerce Customer Payment Tokens', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'customer_tokens_eraser' ) ); - } - - /** - * Add privacy policy content for the privacy policy page. - * - * @since 3.4.0 - */ - public function get_privacy_message() { - $content = '
    ' . - '

    ' . - __( 'This sample language includes the basics around what personal data your store may be collecting, storing and sharing, as well as who may have access to that data. Depending on what settings are enabled and which additional plugins are used, the specific information shared by your store will vary. We recommend consulting with a lawyer when deciding what information to disclose on your privacy policy.', 'woocommerce' ) . - '

    ' . - '

    ' . __( 'We collect information about you during the checkout process on our store.', 'woocommerce' ) . '

    ' . - '

    ' . __( 'What we collect and store', 'woocommerce' ) . '

    ' . - '

    ' . __( 'While you visit our site, we’ll track:', 'woocommerce' ) . '

    ' . - '
      ' . - '
    • ' . __( 'Products you’ve viewed: we’ll use this to, for example, show you products you’ve recently viewed', 'woocommerce' ) . '
    • ' . - '
    • ' . __( 'Location, IP address and browser type: we’ll use this for purposes like estimating taxes and shipping', 'woocommerce' ) . '
    • ' . - '
    • ' . __( 'Shipping address: we’ll ask you to enter this so we can, for instance, estimate shipping before you place an order, and send you the order!', 'woocommerce' ) . '
    • ' . - '
    ' . - '

    ' . __( 'We’ll also use cookies to keep track of cart contents while you’re browsing our site.', 'woocommerce' ) . '

    ' . - '

    ' . - __( 'Note: you may want to further detail your cookie policy, and link to that section from here.', 'woocommerce' ) . - '

    ' . - '

    ' . __( 'When you purchase from us, we’ll ask you to provide information including your name, billing address, shipping address, email address, phone number, credit card/payment details and optional account information like username and password. We’ll use this information for purposes, such as, to:', 'woocommerce' ) . '

    ' . - '
      ' . - '
    • ' . __( 'Send you information about your account and order', 'woocommerce' ) . '
    • ' . - '
    • ' . __( 'Respond to your requests, including refunds and complaints', 'woocommerce' ) . '
    • ' . - '
    • ' . __( 'Process payments and prevent fraud', 'woocommerce' ) . '
    • ' . - '
    • ' . __( 'Set up your account for our store', 'woocommerce' ) . '
    • ' . - '
    • ' . __( 'Comply with any legal obligations we have, such as calculating taxes', 'woocommerce' ) . '
    • ' . - '
    • ' . __( 'Improve our store offerings', 'woocommerce' ) . '
    • ' . - '
    • ' . __( 'Send you marketing messages, if you choose to receive them', 'woocommerce' ) . '
    • ' . - '
    ' . - '

    ' . __( 'If you create an account, we will store your name, address, email and phone number, which will be used to populate the checkout for future orders.', 'woocommerce' ) . '

    ' . - '

    ' . __( 'We generally store information about you for as long as we need the information for the purposes for which we collect and use it, and we are not legally required to continue to keep it. For example, we will store order information for XXX years for tax and accounting purposes. This includes your name, email address and billing and shipping addresses.', 'woocommerce' ) . '

    ' . - '

    ' . __( 'We will also store comments or reviews, if you choose to leave them.', 'woocommerce' ) . '

    ' . - '

    ' . __( 'Who on our team has access', 'woocommerce' ) . '

    ' . - '

    ' . __( 'Members of our team have access to the information you provide us. For example, both Administrators and Shop Managers can access:', 'woocommerce' ) . '

    ' . - '
      ' . - '
    • ' . __( 'Order information like what was purchased, when it was purchased and where it should be sent, and', 'woocommerce' ) . '
    • ' . - '
    • ' . __( 'Customer information like your name, email address, and billing and shipping information.', 'woocommerce' ) . '
    • ' . - '
    ' . - '

    ' . __( 'Our team members have access to this information to help fulfill orders, process refunds and support you.', 'woocommerce' ) . '

    ' . - '

    ' . __( 'What we share with others', 'woocommerce' ) . '

    ' . - '

    ' . - __( 'In this section you should list who you’re sharing data with, and for what purpose. This could include, but may not be limited to, analytics, marketing, payment gateways, shipping providers, and third party embeds.', 'woocommerce' ) . - '

    ' . - '

    ' . __( 'We share information with third parties who help us provide our orders and store services to you; for example --', 'woocommerce' ) . '

    ' . - '

    ' . __( 'Payments', 'woocommerce' ) . '

    ' . - '

    ' . - __( 'In this subsection you should list which third party payment processors you’re using to take payments on your store since these may handle customer data. We’ve included PayPal as an example, but you should remove this if you’re not using PayPal.', 'woocommerce' ) . - '

    ' . - '

    ' . __( 'We accept payments through PayPal. When processing payments, some of your data will be passed to PayPal, including information required to process or support the payment, such as the purchase total and billing information.', 'woocommerce' ) . '

    ' . - '

    ' . __( 'Please see the PayPal Privacy Policy for more details.', 'woocommerce' ) . '

    ' . - '
    '; - - return apply_filters( 'wc_privacy_policy_content', $content ); - } - - /** - * Spawn events for order cleanup. - */ - public function queue_cleanup_personal_data() { - self::$background_process->push_to_queue( array( 'task' => 'trash_pending_orders' ) ); - self::$background_process->push_to_queue( array( 'task' => 'trash_failed_orders' ) ); - self::$background_process->push_to_queue( array( 'task' => 'trash_cancelled_orders' ) ); - self::$background_process->push_to_queue( array( 'task' => 'anonymize_completed_orders' ) ); - self::$background_process->push_to_queue( array( 'task' => 'delete_inactive_accounts' ) ); - self::$background_process->save()->dispatch(); - } - - /** - * Handle some custom types of data and anonymize them. - * - * @param string $anonymous Anonymized string. - * @param string $type Type of data. - * @param string $data The data being anonymized. - * @return string Anonymized string. - */ - public function anonymize_custom_data_types( $anonymous, $type, $data ) { - switch ( $type ) { - case 'address_state': - case 'address_country': - $anonymous = ''; // Empty string - we don't want to store anything after removal. - break; - case 'phone': - $anonymous = preg_replace( '/\d/u', '0', $data ); - break; - case 'numeric_id': - $anonymous = 0; - break; - } - return $anonymous; - } - - /** - * Find and trash old orders. - * - * @since 3.4.0 - * @param int $limit Limit orders to process per batch. - * @return int Number of orders processed. - */ - public static function trash_pending_orders( $limit = 20 ) { - $option = wc_parse_relative_date_option( get_option( 'woocommerce_trash_pending_orders' ) ); - - if ( empty( $option['number'] ) ) { - return 0; - } - - return self::trash_orders_query( - apply_filters( - 'woocommerce_trash_pending_orders_query_args', - array( - 'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), - 'limit' => $limit, // Batches of 20. - 'status' => 'wc-pending', - 'type' => 'shop_order', - ) - ) - ); - } - - /** - * Find and trash old orders. - * - * @since 3.4.0 - * @param int $limit Limit orders to process per batch. - * @return int Number of orders processed. - */ - public static function trash_failed_orders( $limit = 20 ) { - $option = wc_parse_relative_date_option( get_option( 'woocommerce_trash_failed_orders' ) ); - - if ( empty( $option['number'] ) ) { - return 0; - } - - return self::trash_orders_query( - apply_filters( - 'woocommerce_trash_failed_orders_query_args', - array( - 'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), - 'limit' => $limit, // Batches of 20. - 'status' => 'wc-failed', - 'type' => 'shop_order', - ) - ) - ); - } - - /** - * Find and trash old orders. - * - * @since 3.4.0 - * @param int $limit Limit orders to process per batch. - * @return int Number of orders processed. - */ - public static function trash_cancelled_orders( $limit = 20 ) { - $option = wc_parse_relative_date_option( get_option( 'woocommerce_trash_cancelled_orders' ) ); - - if ( empty( $option['number'] ) ) { - return 0; - } - - return self::trash_orders_query( - apply_filters( - 'woocommerce_trash_cancelled_orders_query_args', - array( - 'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), - 'limit' => $limit, // Batches of 20. - 'status' => 'wc-cancelled', - 'type' => 'shop_order', - ) - ) - ); - } - - /** - * For a given query trash all matches. - * - * @since 3.4.0 - * @param array $query Query array to pass to wc_get_orders(). - * @return int Count of orders that were trashed. - */ - protected static function trash_orders_query( $query ) { - $orders = wc_get_orders( $query ); - $count = 0; - - if ( $orders ) { - foreach ( $orders as $order ) { - $order->delete( false ); - $count ++; - } - } - - return $count; - } - - /** - * Anonymize old completed orders. - * - * @since 3.4.0 - * @param int $limit Limit orders to process per batch. - * @return int Number of orders processed. - */ - public static function anonymize_completed_orders( $limit = 20 ) { - $option = wc_parse_relative_date_option( get_option( 'woocommerce_anonymize_completed_orders' ) ); - - if ( empty( $option['number'] ) ) { - return 0; - } - - return self::anonymize_orders_query( - apply_filters( - 'woocommerce_anonymize_completed_orders_query_args', - array( - 'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), - 'limit' => $limit, // Batches of 20. - 'status' => 'wc-completed', - 'anonymized' => false, - 'type' => 'shop_order', - ) - ) - ); - } - - /** - * For a given query, anonymize all matches. - * - * @since 3.4.0 - * @param array $query Query array to pass to wc_get_orders(). - * @return int Count of orders that were anonymized. - */ - protected static function anonymize_orders_query( $query ) { - $orders = wc_get_orders( $query ); - $count = 0; - - if ( $orders ) { - foreach ( $orders as $order ) { - WC_Privacy_Erasers::remove_order_personal_data( $order ); - $count ++; - } - } - - return $count; - } - - /** - * Delete inactive accounts. - * - * @since 3.4.0 - * @param int $limit Limit users to process per batch. - * @return int Number of users processed. - */ - public static function delete_inactive_accounts( $limit = 20 ) { - $option = wc_parse_relative_date_option( get_option( 'woocommerce_delete_inactive_accounts' ) ); - - if ( empty( $option['number'] ) ) { - return 0; - } - - return self::delete_inactive_accounts_query( strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), $limit ); - } - - /** - * Delete inactive accounts. - * - * @since 3.4.0 - * @param int $timestamp Timestamp to delete customers before. - * @param int $limit Limit number of users to delete per run. - * @return int Count of customers that were deleted. - */ - protected static function delete_inactive_accounts_query( $timestamp, $limit = 20 ) { - $count = 0; - $user_query = new WP_User_Query( - array( - 'fields' => 'ID', - 'number' => $limit, - 'role__in' => apply_filters( - 'woocommerce_delete_inactive_account_roles', - array( - 'Customer', - 'Subscriber', - ) - ), - 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - 'relation' => 'AND', - array( - 'key' => 'wc_last_active', - 'value' => (string) $timestamp, - 'compare' => '<', - 'type' => 'NUMERIC', - ), - array( - 'key' => 'wc_last_active', - 'value' => '0', - 'compare' => '>', - 'type' => 'NUMERIC', - ), - ), - ) - ); - - $user_ids = $user_query->get_results(); - - if ( $user_ids ) { - if ( ! function_exists( 'wp_delete_user' ) ) { - require_once ABSPATH . 'wp-admin/includes/user.php'; - } - - foreach ( $user_ids as $user_id ) { - wp_delete_user( $user_id ); - $count ++; - } - } - - return $count; - } -} - -new WC_Privacy(); diff --git a/includes/class-wc-product-attribute.php b/includes/class-wc-product-attribute.php deleted file mode 100644 index 14f1901c7aa..00000000000 --- a/includes/class-wc-product-attribute.php +++ /dev/null @@ -1,329 +0,0 @@ - 0, - 'name' => '', - 'options' => array(), - 'position' => 0, - 'visible' => false, - 'variation' => false, - ); - - /** - * Return if this attribute is a taxonomy. - * - * @return boolean - */ - public function is_taxonomy() { - return 0 < $this->get_id(); - } - - /** - * Get taxonomy name if applicable. - * - * @return string - */ - public function get_taxonomy() { - return $this->is_taxonomy() ? $this->get_name() : ''; - } - - /** - * Get taxonomy object. - * - * @return array|null - */ - public function get_taxonomy_object() { - global $wc_product_attributes; - return $this->is_taxonomy() ? $wc_product_attributes[ $this->get_name() ] : null; - } - - /** - * Gets terms from the stored options. - * - * @return array|null - */ - public function get_terms() { - if ( ! $this->is_taxonomy() || ! taxonomy_exists( $this->get_name() ) ) { - return null; - } - $terms = array(); - foreach ( $this->get_options() as $option ) { - if ( is_int( $option ) ) { - $term = get_term_by( 'id', $option, $this->get_name() ); - } else { - // Term names get escaped in WP. See sanitize_term_field. - $term = get_term_by( 'name', $option, $this->get_name() ); - - if ( ! $term || is_wp_error( $term ) ) { - $new_term = wp_insert_term( $option, $this->get_name() ); - $term = is_wp_error( $new_term ) ? false : get_term_by( 'id', $new_term['term_id'], $this->get_name() ); - } - } - if ( $term && ! is_wp_error( $term ) ) { - $terms[] = $term; - } - } - return $terms; - } - - /** - * Gets slugs from the stored options, or just the string if text based. - * - * @return array - */ - public function get_slugs() { - if ( ! $this->is_taxonomy() || ! taxonomy_exists( $this->get_name() ) ) { - return $this->get_options(); - } - $terms = array(); - foreach ( $this->get_options() as $option ) { - if ( is_int( $option ) ) { - $term = get_term_by( 'id', $option, $this->get_name() ); - } else { - $term = get_term_by( 'name', $option, $this->get_name() ); - - if ( ! $term || is_wp_error( $term ) ) { - $new_term = wp_insert_term( $option, $this->get_name() ); - $term = is_wp_error( $new_term ) ? false : get_term_by( 'id', $new_term['term_id'], $this->get_name() ); - } - } - if ( $term && ! is_wp_error( $term ) ) { - $terms[] = $term->slug; - } - } - return $terms; - } - - /** - * Returns all data for this object. - * - * @return array - */ - public function get_data() { - return array_merge( - $this->data, - array( - 'is_visible' => $this->get_visible() ? 1 : 0, - 'is_variation' => $this->get_variation() ? 1 : 0, - 'is_taxonomy' => $this->is_taxonomy() ? 1 : 0, - 'value' => $this->is_taxonomy() ? '' : wc_implode_text_attributes( $this->get_options() ), - ) - ); - } - - /* - |-------------------------------------------------------------------------- - | Setters - |-------------------------------------------------------------------------- - */ - - /** - * Set ID (this is the attribute ID). - * - * @param int $value Attribute ID. - */ - public function set_id( $value ) { - $this->data['id'] = absint( $value ); - } - - /** - * Set name (this is the attribute name or taxonomy). - * - * @param int $value Attribute name. - */ - public function set_name( $value ) { - $this->data['name'] = $value; - } - - /** - * Set options. - * - * @param array $value Attribute options. - */ - public function set_options( $value ) { - $this->data['options'] = $value; - } - - /** - * Set position. - * - * @param int $value Attribute position. - */ - public function set_position( $value ) { - $this->data['position'] = absint( $value ); - } - - /** - * Set if visible. - * - * @param bool $value If is visible on Product's additional info tab. - */ - public function set_visible( $value ) { - $this->data['visible'] = wc_string_to_bool( $value ); - } - - /** - * Set if variation. - * - * @param bool $value If is used for variations. - */ - public function set_variation( $value ) { - $this->data['variation'] = wc_string_to_bool( $value ); - } - - /* - |-------------------------------------------------------------------------- - | Getters - |-------------------------------------------------------------------------- - */ - - /** - * Get the ID. - * - * @return int - */ - public function get_id() { - return $this->data['id']; - } - - /** - * Get name. - * - * @return string - */ - public function get_name() { - return $this->data['name']; - } - - /** - * Get options. - * - * @return array - */ - public function get_options() { - return $this->data['options']; - } - - /** - * Get position. - * - * @return int - */ - public function get_position() { - return $this->data['position']; - } - - /** - * Get if visible. - * - * @return bool - */ - public function get_visible() { - return $this->data['visible']; - } - - /** - * Get if variation. - * - * @return bool - */ - public function get_variation() { - return $this->data['variation']; - } - - /* - |-------------------------------------------------------------------------- - | ArrayAccess/Backwards compatibility. - |-------------------------------------------------------------------------- - */ - - /** - * OffsetGet. - * - * @param string $offset Offset. - * @return mixed - */ - public function offsetGet( $offset ) { - switch ( $offset ) { - case 'is_variation': - return $this->get_variation() ? 1 : 0; - case 'is_visible': - return $this->get_visible() ? 1 : 0; - case 'is_taxonomy': - return $this->is_taxonomy() ? 1 : 0; - case 'value': - return $this->is_taxonomy() ? '' : wc_implode_text_attributes( $this->get_options() ); - default: - if ( is_callable( array( $this, "get_$offset" ) ) ) { - return $this->{"get_$offset"}(); - } - break; - } - return ''; - } - - /** - * OffsetSet. - * - * @param string $offset Offset. - * @param mixed $value Value. - */ - public function offsetSet( $offset, $value ) { - switch ( $offset ) { - case 'is_variation': - $this->set_variation( $value ); - break; - case 'is_visible': - $this->set_visible( $value ); - break; - case 'value': - $this->set_options( $value ); - break; - default: - if ( is_callable( array( $this, "set_$offset" ) ) ) { - return $this->{"set_$offset"}( $value ); - } - break; - } - } - - /** - * OffsetUnset. - * - * @param string $offset Offset. - */ - public function offsetUnset( $offset ) {} - - /** - * OffsetExists. - * - * @param string $offset Offset. - * @return bool - */ - public function offsetExists( $offset ) { - return in_array( $offset, array_merge( array( 'is_variation', 'is_visible', 'is_taxonomy', 'value' ), array_keys( $this->data ) ), true ); - } -} diff --git a/includes/class-wc-product-download.php b/includes/class-wc-product-download.php deleted file mode 100644 index 3b4344bd9be..00000000000 --- a/includes/class-wc-product-download.php +++ /dev/null @@ -1,289 +0,0 @@ - '', - 'name' => '', - 'file' => '', - ); - - /** - * Returns all data for this object. - * - * @return array - */ - public function get_data() { - return $this->data; - } - - /** - * Get allowed mime types. - * - * @return array - */ - public function get_allowed_mime_types() { - return apply_filters( 'woocommerce_downloadable_file_allowed_mime_types', get_allowed_mime_types() ); - } - - /** - * Get type of file path set. - * - * @param string $file_path optional. - * @return string absolute, relative, or shortcode. - */ - public function get_type_of_file_path( $file_path = '' ) { - $file_path = $file_path ? $file_path : $this->get_file(); - $parsed_url = parse_url( $file_path ); - if ( - $parsed_url && - isset( $parsed_url['host'] ) && // Absolute url means that it has a host. - ( // Theoretically we could permit any scheme (like ftp as well), but that has not been the case before. So we allow none or http(s). - ! isset( $parsed_url['scheme'] ) || - in_array( $parsed_url['scheme'], array( 'http', 'https' ) ) - ) - ) { - return 'absolute'; - } elseif ( '[' === substr( $file_path, 0, 1 ) && ']' === substr( $file_path, -1 ) ) { - return 'shortcode'; - } else { - return 'relative'; - } - } - - /** - * Get file type. - * - * @return string - */ - public function get_file_type() { - $type = wp_check_filetype( strtok( $this->get_file(), '?' ), $this->get_allowed_mime_types() ); - return $type['type']; - } - - /** - * Get file extension. - * - * @return string - */ - public function get_file_extension() { - $parsed_url = wp_parse_url( $this->get_file(), PHP_URL_PATH ); - return pathinfo( $parsed_url, PATHINFO_EXTENSION ); - } - - /** - * Check if file is allowed. - * - * @return boolean - */ - public function is_allowed_filetype() { - $file_path = $this->get_file(); - - // File types for URL-based files located on the server should get validated. - $is_file_on_server = false; - if ( false !== stripos( $file_path, network_site_url( '/', 'https' ) ) || - false !== stripos( $file_path, network_site_url( '/', 'http' ) ) || - false !== stripos( $file_path, site_url( '/', 'https' ) ) || - false !== stripos( $file_path, site_url( '/', 'http' ) ) - ) { - $is_file_on_server = true; - } - - if ( ! $is_file_on_server && 'relative' !== $this->get_type_of_file_path() ) { - return true; - } - return ! $this->get_file_extension() || in_array( $this->get_file_type(), $this->get_allowed_mime_types(), true ); - } - - /** - * Validate file exists. - * - * @return boolean - */ - public function file_exists() { - if ( 'relative' !== $this->get_type_of_file_path() ) { - return true; - } - $file_url = $this->get_file(); - if ( '..' === substr( $file_url, 0, 2 ) || '/' !== substr( $file_url, 0, 1 ) ) { - $file_url = realpath( ABSPATH . $file_url ); - } elseif ( substr( WP_CONTENT_DIR, strlen( untrailingslashit( ABSPATH ) ) ) === substr( $file_url, 0, strlen( substr( WP_CONTENT_DIR, strlen( untrailingslashit( ABSPATH ) ) ) ) ) ) { - $file_url = realpath( WP_CONTENT_DIR . substr( $file_url, 11 ) ); - } - return apply_filters( 'woocommerce_downloadable_file_exists', file_exists( $file_url ), $this->get_file() ); - } - - /* - |-------------------------------------------------------------------------- - | Setters - |-------------------------------------------------------------------------- - */ - - /** - * Set ID. - * - * @param string $value Download ID. - */ - public function set_id( $value ) { - $this->data['id'] = wc_clean( $value ); - } - - /** - * Set name. - * - * @param string $value Download name. - */ - public function set_name( $value ) { - $this->data['name'] = wc_clean( $value ); - } - - /** - * Set previous_hash. - * - * @deprecated 3.3.0 No longer using filename based hashing to keep track of files. - * @param string $value Previous hash. - */ - public function set_previous_hash( $value ) { - wc_deprecated_function( __FUNCTION__, '3.3' ); - $this->data['previous_hash'] = wc_clean( $value ); - } - - /** - * Set file. - * - * @param string $value File URL/Path. - */ - public function set_file( $value ) { - // A `///` is recognized as an "absolute", but on the filesystem, so it bypasses the mime check in `self::is_allowed_filetype`. - // This will strip extra prepending / to the maximum of 2. - if ( preg_match( '#^//+(/[^/].+)$#i', $value, $matches ) ) { - $value = $matches[1]; - } - switch ( $this->get_type_of_file_path( $value ) ) { - case 'absolute': - $this->data['file'] = esc_url_raw( $value ); - break; - default: - $this->data['file'] = wc_clean( $value ); - break; - } - } - - /* - |-------------------------------------------------------------------------- - | Getters - |-------------------------------------------------------------------------- - */ - - /** - * Get id. - * - * @return string - */ - public function get_id() { - return $this->data['id']; - } - - /** - * Get name. - * - * @return string - */ - public function get_name() { - return $this->data['name']; - } - - /** - * Get previous_hash. - * - * @deprecated 3.3.0 No longer using filename based hashing to keep track of files. - * @return string - */ - public function get_previous_hash() { - wc_deprecated_function( __FUNCTION__, '3.3' ); - return $this->data['previous_hash']; - } - - /** - * Get file. - * - * @return string - */ - public function get_file() { - return $this->data['file']; - } - - /* - |-------------------------------------------------------------------------- - | ArrayAccess/Backwards compatibility. - |-------------------------------------------------------------------------- - */ - - /** - * OffsetGet. - * - * @param string $offset Offset. - * @return mixed - */ - public function offsetGet( $offset ) { - switch ( $offset ) { - default: - if ( is_callable( array( $this, "get_$offset" ) ) ) { - return $this->{"get_$offset"}(); - } - break; - } - return ''; - } - - /** - * OffsetSet. - * - * @param string $offset Offset. - * @param mixed $value Offset value. - */ - public function offsetSet( $offset, $value ) { - switch ( $offset ) { - default: - if ( is_callable( array( $this, "set_$offset" ) ) ) { - return $this->{"set_$offset"}( $value ); - } - break; - } - } - - /** - * OffsetUnset. - * - * @param string $offset Offset. - */ - public function offsetUnset( $offset ) {} - - /** - * OffsetExists. - * - * @param string $offset Offset. - * @return bool - */ - public function offsetExists( $offset ) { - return in_array( $offset, array_keys( $this->data ), true ); - } -} diff --git a/includes/class-wc-product-variable.php b/includes/class-wc-product-variable.php deleted file mode 100644 index 78900dd9fc1..00000000000 --- a/includes/class-wc-product-variable.php +++ /dev/null @@ -1,675 +0,0 @@ -is_purchasable() ? __( 'Select options', 'woocommerce' ) : __( 'Read more', 'woocommerce' ), $this ); - } - - /** - * Get the add to cart button text description - used in aria tags. - * - * @since 3.3.0 - * @return string - */ - public function add_to_cart_description() { - /* translators: %s: Product title */ - return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( __( 'Select options for “%s”', 'woocommerce' ), $this->get_name() ), $this ); - } - - /** - * Get an array of all sale and regular prices from all variations. This is used for example when displaying the price range at variable product level or seeing if the variable product is on sale. - * - * @param bool $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes). - * @return array Array of RAW prices, regular prices, and sale prices with keys set to variation ID. - */ - public function get_variation_prices( $for_display = false ) { - $prices = $this->data_store->read_price_data( $this, $for_display ); - - foreach ( $prices as $price_key => $variation_prices ) { - $prices[ $price_key ] = $this->sort_variation_prices( $variation_prices ); - } - - return $prices; - } - - /** - * Get the min or max variation regular price. - * - * @param string $min_or_max Min or max price. - * @param boolean $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes). - * @return string - */ - public function get_variation_regular_price( $min_or_max = 'min', $for_display = false ) { - $prices = $this->get_variation_prices( $for_display ); - $price = 'min' === $min_or_max ? current( $prices['regular_price'] ) : end( $prices['regular_price'] ); - - return apply_filters( 'woocommerce_get_variation_regular_price', $price, $this, $min_or_max, $for_display ); - } - - /** - * Get the min or max variation sale price. - * - * @param string $min_or_max Min or max price. - * @param boolean $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes). - * @return string - */ - public function get_variation_sale_price( $min_or_max = 'min', $for_display = false ) { - $prices = $this->get_variation_prices( $for_display ); - $price = 'min' === $min_or_max ? current( $prices['sale_price'] ) : end( $prices['sale_price'] ); - - return apply_filters( 'woocommerce_get_variation_sale_price', $price, $this, $min_or_max, $for_display ); - } - - /** - * Get the min or max variation (active) price. - * - * @param string $min_or_max Min or max price. - * @param boolean $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes). - * @return string - */ - public function get_variation_price( $min_or_max = 'min', $for_display = false ) { - $prices = $this->get_variation_prices( $for_display ); - $price = 'min' === $min_or_max ? current( $prices['price'] ) : end( $prices['price'] ); - - return apply_filters( 'woocommerce_get_variation_price', $price, $this, $min_or_max, $for_display ); - } - - /** - * Returns the price in html format. - * - * Note: Variable prices do not show suffixes like other product types. This - * is due to some things like tax classes being set at variation level which - * could differ from the parent price. The only way to show accurate prices - * would be to load the variation and get it's price, which adds extra - * overhead and still has edge cases where the values would be inaccurate. - * - * Additionally, ranges of prices no longer show 'striked out' sale prices - * due to the strings being very long and unclear/confusing. A single range - * is shown instead. - * - * @param string $price Price (default: ''). - * @return string - */ - public function get_price_html( $price = '' ) { - $prices = $this->get_variation_prices( true ); - - if ( empty( $prices['price'] ) ) { - $price = apply_filters( 'woocommerce_variable_empty_price_html', '', $this ); - } else { - $min_price = current( $prices['price'] ); - $max_price = end( $prices['price'] ); - $min_reg_price = current( $prices['regular_price'] ); - $max_reg_price = end( $prices['regular_price'] ); - - if ( $min_price !== $max_price ) { - $price = wc_format_price_range( $min_price, $max_price ); - } elseif ( $this->is_on_sale() && $min_reg_price === $max_reg_price ) { - $price = wc_format_sale_price( wc_price( $max_reg_price ), wc_price( $min_price ) ); - } else { - $price = wc_price( $min_price ); - } - - $price = apply_filters( 'woocommerce_variable_price_html', $price . $this->get_price_suffix(), $this ); - } - - return apply_filters( 'woocommerce_get_price_html', $price, $this ); - } - - /** - * Get the suffix to display after prices > 0. - * - * This is skipped if the suffix - * has dynamic values such as {price_excluding_tax} for variable products. - * - * @see get_price_html for an explanation as to why. - * @param string $price Price to calculate, left blank to just use get_price(). - * @param integer $qty Quantity passed on to get_price_including_tax() or get_price_excluding_tax(). - * @return string - */ - public function get_price_suffix( $price = '', $qty = 1 ) { - $suffix = get_option( 'woocommerce_price_display_suffix' ); - - if ( strstr( $suffix, '{' ) ) { - return apply_filters( 'woocommerce_get_price_suffix', '', $this, $price, $qty ); - } else { - return parent::get_price_suffix( $price, $qty ); - } - } - - /** - * Return a products child ids. - * - * This is lazy loaded as it's not used often and does require several queries. - * - * @param bool|string $visible_only Visible only. - * @return array Children ids - */ - public function get_children( $visible_only = '' ) { - if ( is_bool( $visible_only ) ) { - wc_deprecated_argument( 'visible_only', '3.0', 'WC_Product_Variable::get_visible_children' ); - - return $visible_only ? $this->get_visible_children() : $this->get_children(); - } - - if ( null === $this->children ) { - $children = $this->data_store->read_children( $this ); - $this->set_children( $children['all'] ); - $this->set_visible_children( $children['visible'] ); - } - - return apply_filters( 'woocommerce_get_children', $this->children, $this, false ); - } - - /** - * Return a products child ids - visible only. - * - * This is lazy loaded as it's not used often and does require several queries. - * - * @since 3.0.0 - * @return array Children ids - */ - public function get_visible_children() { - if ( null === $this->visible_children ) { - $children = $this->data_store->read_children( $this ); - $this->set_children( $children['all'] ); - $this->set_visible_children( $children['visible'] ); - } - return apply_filters( 'woocommerce_get_children', $this->visible_children, $this, true ); - } - - /** - * Return an array of attributes used for variations, as well as their possible values. - * - * This is lazy loaded as it's not used often and does require several queries. - * - * @return array Attributes and their available values - */ - public function get_variation_attributes() { - if ( null === $this->variation_attributes ) { - $this->variation_attributes = $this->data_store->read_variation_attributes( $this ); - } - return $this->variation_attributes; - } - - /** - * If set, get the default attributes for a variable product. - * - * @param string $attribute_name Attribute name. - * @return string - */ - public function get_variation_default_attribute( $attribute_name ) { - $defaults = $this->get_default_attributes(); - $attribute_name = sanitize_title( $attribute_name ); - - return isset( $defaults[ $attribute_name ] ) ? $defaults[ $attribute_name ] : ''; - } - - /** - * Variable products themselves cannot be downloadable. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return bool - */ - public function get_downloadable( $context = 'view' ) { - return false; - } - - /** - * Variable products themselves cannot be virtual. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return bool - */ - public function get_virtual( $context = 'view' ) { - return false; - } - - /** - * Get an array of available variations for the current product. - * - * @param string $return Optional. The format to return the results in. Can be 'array' to return an array of variation data or 'objects' for the product objects. Default 'array'. - * - * @return array[]|WC_Product_Variation[] - */ - public function get_available_variations( $return = 'array' ) { - $variation_ids = $this->get_children(); - $available_variations = array(); - - if ( is_callable( '_prime_post_caches' ) ) { - _prime_post_caches( $variation_ids ); - } - - foreach ( $variation_ids as $variation_id ) { - - $variation = wc_get_product( $variation_id ); - - // Hide out of stock variations if 'Hide out of stock items from the catalog' is checked. - if ( ! $variation || ! $variation->exists() || ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $variation->is_in_stock() ) ) { - continue; - } - - // Filter 'woocommerce_hide_invisible_variations' to optionally hide invisible variations (disabled variations and variations with empty price). - if ( apply_filters( 'woocommerce_hide_invisible_variations', true, $this->get_id(), $variation ) && ! $variation->variation_is_visible() ) { - continue; - } - - if ( 'array' === $return ) { - $available_variations[] = $this->get_available_variation( $variation ); - } else { - $available_variations[] = $variation; - } - } - - if ( 'array' === $return ) { - $available_variations = array_values( array_filter( $available_variations ) ); - } - - return $available_variations; - } - - /** - * Check if a given variation is currently available. - * - * @param WC_Product_Variation $variation Variation to check. - * - * @return bool True if the variation is available, false otherwise. - */ - private function variation_is_available( WC_Product_Variation $variation ) { - // Hide out of stock variations if 'Hide out of stock items from the catalog' is checked. - if ( ! $variation || ! $variation->exists() || ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $variation->is_in_stock() ) ) { - return false; - } - - // Filter 'woocommerce_hide_invisible_variations' to optionally hide invisible variations (disabled variations and variations with empty price). - if ( apply_filters( 'woocommerce_hide_invisible_variations', true, $this->get_id(), $variation ) && ! $variation->variation_is_visible() ) { - return false; - } - - return true; - } - - /** - * Returns an array of data for a variation. Used in the add to cart form. - * - * @since 2.4.0 - * @param WC_Product $variation Variation product object or ID. - * @return array|bool - */ - public function get_available_variation( $variation ) { - if ( is_numeric( $variation ) ) { - $variation = wc_get_product( $variation ); - } - if ( ! $variation instanceof WC_Product_Variation ) { - return false; - } - // See if prices should be shown for each variation after selection. - $show_variation_price = apply_filters( 'woocommerce_show_variation_price', $variation->get_price() === '' || $this->get_variation_sale_price( 'min' ) !== $this->get_variation_sale_price( 'max' ) || $this->get_variation_regular_price( 'min' ) !== $this->get_variation_regular_price( 'max' ), $this, $variation ); - - return apply_filters( - 'woocommerce_available_variation', - array( - 'attributes' => $variation->get_variation_attributes(), - 'availability_html' => wc_get_stock_html( $variation ), - 'backorders_allowed' => $variation->backorders_allowed(), - 'dimensions' => $variation->get_dimensions( false ), - 'dimensions_html' => wc_format_dimensions( $variation->get_dimensions( false ) ), - 'display_price' => wc_get_price_to_display( $variation ), - 'display_regular_price' => wc_get_price_to_display( $variation, array( 'price' => $variation->get_regular_price() ) ), - 'image' => wc_get_product_attachment_props( $variation->get_image_id() ), - 'image_id' => $variation->get_image_id(), - 'is_downloadable' => $variation->is_downloadable(), - 'is_in_stock' => $variation->is_in_stock(), - 'is_purchasable' => $variation->is_purchasable(), - 'is_sold_individually' => $variation->is_sold_individually() ? 'yes' : 'no', - 'is_virtual' => $variation->is_virtual(), - 'max_qty' => 0 < $variation->get_max_purchase_quantity() ? $variation->get_max_purchase_quantity() : '', - 'min_qty' => $variation->get_min_purchase_quantity(), - 'price_html' => $show_variation_price ? '' . $variation->get_price_html() . '' : '', - 'sku' => $variation->get_sku(), - 'variation_description' => wc_format_content( $variation->get_description() ), - 'variation_id' => $variation->get_id(), - 'variation_is_active' => $variation->variation_is_active(), - 'variation_is_visible' => $variation->variation_is_visible(), - 'weight' => $variation->get_weight(), - 'weight_html' => wc_format_weight( $variation->get_weight() ), - ), - $this, - $variation - ); - } - - /* - |-------------------------------------------------------------------------- - | Setters - |-------------------------------------------------------------------------- - */ - - /** - * Sets an array of variation attributes. - * - * @since 3.0.0 - * @param array $variation_attributes Attributes list. - */ - public function set_variation_attributes( $variation_attributes ) { - $this->variation_attributes = $variation_attributes; - } - - /** - * Sets an array of children for the product. - * - * @since 3.0.0 - * @param array $children Children products. - */ - public function set_children( $children ) { - $this->children = array_filter( wp_parse_id_list( (array) $children ) ); - } - - /** - * Sets an array of visible children only. - * - * @since 3.0.0 - * @param array $visible_children List of visible children products. - */ - public function set_visible_children( $visible_children ) { - $this->visible_children = array_filter( wp_parse_id_list( (array) $visible_children ) ); - } - - /* - |-------------------------------------------------------------------------- - | CRUD methods - |-------------------------------------------------------------------------- - */ - - /** - * Ensure properties are set correctly before save. - * - * @since 3.0.0 - */ - public function validate_props() { - parent::validate_props(); - - if ( ! $this->get_manage_stock() ) { - $this->data_store->sync_stock_status( $this ); - } - } - - /** - * Save data (either create or update depending on if we are working on an existing product). - * - * @since 3.0.0 - */ - public function save() { - $this->validate_props(); - - if ( ! $this->data_store ) { - return $this->get_id(); - } - - /** - * Trigger action before saving to the DB. Allows you to adjust object props before save. - * - * @param WC_Data $this The object being saved. - * @param WC_Data_Store_WP $data_store The data store persisting the data. - */ - do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store ); - - // Get names before save. - $previous_name = $this->data['name']; - $new_name = $this->get_name( 'edit' ); - - if ( $this->get_id() ) { - $this->data_store->update( $this ); - } else { - $this->data_store->create( $this ); - } - - $this->data_store->sync_variation_names( $this, $previous_name, $new_name ); - $this->data_store->sync_managed_variation_stock_status( $this ); - - /** - * Trigger action after saving to the DB. - * - * @param WC_Data $this The object being saved. - * @param WC_Data_Store_WP $data_store The data store persisting the data. - */ - do_action( 'woocommerce_after_' . $this->object_type . '_object_save', $this, $this->data_store ); - - return $this->get_id(); - } - - /* - |-------------------------------------------------------------------------- - | Conditionals - |-------------------------------------------------------------------------- - */ - - /** - * Returns whether or not the product is on sale. - * - * @param string $context What the value is for. Valid values are view and edit. What the value is for. Valid values are view and edit. - * @return bool - */ - public function is_on_sale( $context = 'view' ) { - $prices = $this->get_variation_prices(); - $on_sale = $prices['regular_price'] !== $prices['sale_price'] && $prices['sale_price'] === $prices['price']; - - return 'view' === $context ? apply_filters( 'woocommerce_product_is_on_sale', $on_sale, $this ) : $on_sale; - } - - /** - * Is a child in stock? - * - * @return boolean - */ - public function child_is_in_stock() { - return $this->data_store->child_is_in_stock( $this ); - } - - /** - * Is a child on backorder? - * - * @since 3.3.0 - * @return boolean - */ - public function child_is_on_backorder() { - return $this->data_store->child_has_stock_status( $this, 'onbackorder' ); - } - - /** - * Does a child have a weight set? - * - * @return boolean - */ - public function child_has_weight() { - $transient_name = 'wc_child_has_weight_' . $this->get_id(); - $has_weight = get_transient( $transient_name ); - - if ( false === $has_weight ) { - $has_weight = $this->data_store->child_has_weight( $this ); - set_transient( $transient_name, (int) $has_weight, DAY_IN_SECONDS * 30 ); - } - - return (bool) $has_weight; - } - - /** - * Does a child have dimensions set? - * - * @return boolean - */ - public function child_has_dimensions() { - $transient_name = 'wc_child_has_dimensions_' . $this->get_id(); - $has_dimension = get_transient( $transient_name ); - - if ( false === $has_dimension ) { - $has_dimension = $this->data_store->child_has_dimensions( $this ); - set_transient( $transient_name, (int) $has_dimension, DAY_IN_SECONDS * 30 ); - } - - return (bool) $has_dimension; - } - - /** - * Returns whether or not the product has dimensions set. - * - * @return bool - */ - public function has_dimensions() { - return parent::has_dimensions() || $this->child_has_dimensions(); - } - - /** - * Returns whether or not the product has weight set. - * - * @return bool - */ - public function has_weight() { - return parent::has_weight() || $this->child_has_weight(); - } - - /** - * Returns whether or not the product has additional options that need - * selecting before adding to cart. - * - * @since 3.0.0 - * @return boolean - */ - public function has_options() { - return apply_filters( 'woocommerce_product_has_options', true, $this ); - } - - - /* - |-------------------------------------------------------------------------- - | Sync with child variations. - |-------------------------------------------------------------------------- - */ - - /** - * Sync a variable product with it's children. These sync functions sync - * upwards (from child to parent) when the variation is saved. - * - * @param WC_Product|int $product Product object or ID for which you wish to sync. - * @param bool $save If true, the product object will be saved to the DB before returning it. - * @return WC_Product Synced product object. - */ - public static function sync( $product, $save = true ) { - if ( ! is_a( $product, 'WC_Product' ) ) { - $product = wc_get_product( $product ); - } - if ( is_a( $product, 'WC_Product_Variable' ) ) { - $data_store = WC_Data_Store::load( 'product-' . $product->get_type() ); - $data_store->sync_price( $product ); - $data_store->sync_stock_status( $product ); - self::sync_attributes( $product ); // Legacy update of attributes. - - do_action( 'woocommerce_variable_product_sync_data', $product ); - - if ( $save ) { - $product->save(); - } - - wc_do_deprecated_action( - 'woocommerce_variable_product_sync', - array( - $product->get_id(), - $product->get_visible_children(), - ), - '3.0', - 'woocommerce_variable_product_sync_data, woocommerce_new_product or woocommerce_update_product' - ); - } - - return $product; - } - - /** - * Sync parent stock status with the status of all children and save. - * - * @param WC_Product|int $product Product object or ID for which you wish to sync. - * @param bool $save If true, the product object will be saved to the DB before returning it. - * @return WC_Product Synced product object. - */ - public static function sync_stock_status( $product, $save = true ) { - if ( ! is_a( $product, 'WC_Product' ) ) { - $product = wc_get_product( $product ); - } - if ( is_a( $product, 'WC_Product_Variable' ) ) { - $data_store = WC_Data_Store::load( 'product-' . $product->get_type() ); - $data_store->sync_stock_status( $product ); - - if ( $save ) { - $product->save(); - } - } - - return $product; - } - - /** - * Sort an associative array of $variation_id => $price pairs in order of min and max prices. - * - * @param array $prices associative array of $variation_id => $price pairs. - * @return array - */ - protected function sort_variation_prices( $prices ) { - asort( $prices ); - - return $prices; - } -} diff --git a/includes/class-wc-product-variation.php b/includes/class-wc-product-variation.php deleted file mode 100644 index ebd2838b46b..00000000000 --- a/includes/class-wc-product-variation.php +++ /dev/null @@ -1,603 +0,0 @@ - '', - 'sku' => '', - 'manage_stock' => '', - 'backorders' => '', - 'stock_quantity' => '', - 'weight' => '', - 'length' => '', - 'width' => '', - 'height' => '', - 'tax_class' => '', - 'shipping_class_id' => '', - 'image_id' => '', - 'purchase_note' => '', - ); - - /** - * Override the default constructor to set custom defaults. - * - * @param int|WC_Product|object $product Product to init. - */ - public function __construct( $product = 0 ) { - $this->data['tax_class'] = 'parent'; - $this->data['attribute_summary'] = ''; - parent::__construct( $product ); - } - - /** - * Prefix for action and filter hooks on data. - * - * @since 3.0.0 - * @return string - */ - protected function get_hook_prefix() { - return 'woocommerce_product_variation_get_'; - } - - /** - * Get internal type. - * - * @return string - */ - public function get_type() { - return 'variation'; - } - - /** - * If the stock level comes from another product ID. - * - * @since 3.0.0 - * @return int - */ - public function get_stock_managed_by_id() { - return 'parent' === $this->get_manage_stock() ? $this->get_parent_id() : $this->get_id(); - } - - /** - * Get the product's title. For variations this is the parent product name. - * - * @return string - */ - public function get_title() { - return apply_filters( 'woocommerce_product_title', $this->parent_data['title'], $this ); - } - - /** - * Get product name with SKU or ID. Used within admin. - * - * @return string Formatted product name - */ - public function get_formatted_name() { - if ( $this->get_sku() ) { - $identifier = $this->get_sku(); - } else { - $identifier = '#' . $this->get_id(); - } - - $formatted_variation_list = wc_get_formatted_variation( $this, true, true, true ); - - return sprintf( '%2$s (%1$s)', $identifier, $this->get_name() ) . '' . $formatted_variation_list . ''; - } - - /** - * Get variation attribute values. Keys are prefixed with attribute_, as stored, unless $with_prefix is false. - * - * @param bool $with_prefix Whether keys should be prepended with attribute_ or not, default is true. - * @return array of attributes and their values for this variation. - */ - public function get_variation_attributes( $with_prefix = true ) { - $attributes = $this->get_attributes(); - $variation_attributes = array(); - $prefix = $with_prefix ? 'attribute_' : ''; - - foreach ( $attributes as $key => $value ) { - $variation_attributes[ $prefix . $key ] = $value; - } - return $variation_attributes; - } - - /** - * Returns a single product attribute as a string. - * - * @param string $attribute to get. - * @return string - */ - public function get_attribute( $attribute ) { - $attributes = $this->get_attributes(); - $attribute = sanitize_title( $attribute ); - - if ( isset( $attributes[ $attribute ] ) ) { - $value = $attributes[ $attribute ]; - $term = taxonomy_exists( $attribute ) ? get_term_by( 'slug', $value, $attribute ) : false; - return ! is_wp_error( $term ) && $term ? $term->name : $value; - } - - $att_str = 'pa_' . $attribute; - if ( isset( $attributes[ $att_str ] ) ) { - $value = $attributes[ $att_str ]; - $term = taxonomy_exists( $att_str ) ? get_term_by( 'slug', $value, $att_str ) : false; - return ! is_wp_error( $term ) && $term ? $term->name : $value; - } - - return ''; - } - - /** - * Wrapper for get_permalink. Adds this variations attributes to the URL. - * - * @param array|null $item_object item array If a cart or order item is passed, we can get a link containing the exact attributes selected for the variation, rather than the default attributes. - * @return string - */ - public function get_permalink( $item_object = null ) { - $url = get_permalink( $this->get_parent_id() ); - - if ( ! empty( $item_object['variation'] ) ) { - $data = $item_object['variation']; - } elseif ( ! empty( $item_object['item_meta_array'] ) ) { - $data_keys = array_map( 'wc_variation_attribute_name', wp_list_pluck( $item_object['item_meta_array'], 'key' ) ); - $data_values = wp_list_pluck( $item_object['item_meta_array'], 'value' ); - $data = array_intersect_key( array_combine( $data_keys, $data_values ), $this->get_variation_attributes() ); - } else { - $data = $this->get_variation_attributes(); - } - - $data = array_filter( $data, 'wc_array_filter_default_attributes' ); - - if ( empty( $data ) ) { - return $url; - } - - // Filter and encode keys and values so this is not broken by add_query_arg. - $data = array_map( 'urlencode', $data ); - $keys = array_map( 'urlencode', array_keys( $data ) ); - - return add_query_arg( array_combine( $keys, $data ), $url ); - } - - /** - * Get the add to url used mainly in loops. - * - * @return string - */ - public function add_to_cart_url() { - $url = $this->is_purchasable() ? remove_query_arg( - 'added-to-cart', - add_query_arg( - array( - 'variation_id' => $this->get_id(), - 'add-to-cart' => $this->get_parent_id(), - ), - $this->get_permalink() - ) - ) : $this->get_permalink(); - return apply_filters( 'woocommerce_product_add_to_cart_url', $url, $this ); - } - - /** - * Get SKU (Stock-keeping unit) - product unique ID. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_sku( $context = 'view' ) { - $value = $this->get_prop( 'sku', $context ); - - // Inherit value from parent. - if ( 'view' === $context && empty( $value ) ) { - $value = apply_filters( $this->get_hook_prefix() . 'sku', $this->parent_data['sku'], $this ); - } - return $value; - } - - /** - * Returns the product's weight. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_weight( $context = 'view' ) { - $value = $this->get_prop( 'weight', $context ); - - // Inherit value from parent. - if ( 'view' === $context && empty( $value ) ) { - $value = apply_filters( $this->get_hook_prefix() . 'weight', $this->parent_data['weight'], $this ); - } - return $value; - } - - /** - * Returns the product length. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_length( $context = 'view' ) { - $value = $this->get_prop( 'length', $context ); - - // Inherit value from parent. - if ( 'view' === $context && empty( $value ) ) { - $value = apply_filters( $this->get_hook_prefix() . 'length', $this->parent_data['length'], $this ); - } - return $value; - } - - /** - * Returns the product width. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_width( $context = 'view' ) { - $value = $this->get_prop( 'width', $context ); - - // Inherit value from parent. - if ( 'view' === $context && empty( $value ) ) { - $value = apply_filters( $this->get_hook_prefix() . 'width', $this->parent_data['width'], $this ); - } - return $value; - } - - /** - * Returns the product height. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_height( $context = 'view' ) { - $value = $this->get_prop( 'height', $context ); - - // Inherit value from parent. - if ( 'view' === $context && empty( $value ) ) { - $value = apply_filters( $this->get_hook_prefix() . 'height', $this->parent_data['height'], $this ); - } - return $value; - } - - /** - * Returns the tax class. - * - * Does not use get_prop so it can handle 'parent' inheritance correctly. - * - * @param string $context view, edit, or unfiltered. - * @return string - */ - public function get_tax_class( $context = 'view' ) { - $value = null; - - if ( array_key_exists( 'tax_class', $this->data ) ) { - $value = array_key_exists( 'tax_class', $this->changes ) ? $this->changes['tax_class'] : $this->data['tax_class']; - - if ( 'edit' !== $context && 'parent' === $value ) { - $value = $this->parent_data['tax_class']; - } - - if ( 'view' === $context ) { - $value = apply_filters( $this->get_hook_prefix() . 'tax_class', $value, $this ); - } - } - return $value; - } - - /** - * Return if product manage stock. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are view and edit. - * @return boolean|string true, false, or parent. - */ - public function get_manage_stock( $context = 'view' ) { - $value = $this->get_prop( 'manage_stock', $context ); - - // Inherit value from parent. - if ( 'view' === $context && false === $value && true === wc_string_to_bool( $this->parent_data['manage_stock'] ) ) { - $value = 'parent'; - } - return $value; - } - - /** - * Returns number of items available for sale. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return int|null - */ - public function get_stock_quantity( $context = 'view' ) { - $value = $this->get_prop( 'stock_quantity', $context ); - - // Inherit value from parent. - if ( 'view' === $context && 'parent' === $this->get_manage_stock() ) { - $value = apply_filters( $this->get_hook_prefix() . 'stock_quantity', $this->parent_data['stock_quantity'], $this ); - } - return $value; - } - - /** - * Get backorders. - * - * @param string $context What the value is for. Valid values are view and edit. - * @since 3.0.0 - * @return string yes no or notify - */ - public function get_backorders( $context = 'view' ) { - $value = $this->get_prop( 'backorders', $context ); - - // Inherit value from parent. - if ( 'view' === $context && 'parent' === $this->get_manage_stock() ) { - $value = apply_filters( $this->get_hook_prefix() . 'backorders', $this->parent_data['backorders'], $this ); - } - return $value; - } - - /** - * Get main image ID. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_image_id( $context = 'view' ) { - $image_id = $this->get_prop( 'image_id', $context ); - - if ( 'view' === $context && ! $image_id ) { - $image_id = apply_filters( $this->get_hook_prefix() . 'image_id', $this->parent_data['image_id'], $this ); - } - - return $image_id; - } - - /** - * Get purchase note. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_purchase_note( $context = 'view' ) { - $value = $this->get_prop( 'purchase_note', $context ); - - // Inherit value from parent. - if ( 'view' === $context && empty( $value ) ) { - $value = apply_filters( $this->get_hook_prefix() . 'purchase_note', $this->parent_data['purchase_note'], $this ); - } - return $value; - } - - /** - * Get shipping class ID. - * - * @since 3.0.0 - * @param string $context What the value is for. Valid values are view and edit. - * @return int - */ - public function get_shipping_class_id( $context = 'view' ) { - $shipping_class_id = $this->get_prop( 'shipping_class_id', $context ); - - if ( 'view' === $context && ! $shipping_class_id ) { - $shipping_class_id = apply_filters( $this->get_hook_prefix() . 'shipping_class_id', $this->parent_data['shipping_class_id'], $this ); - } - - return $shipping_class_id; - } - - /** - * Get catalog visibility. - * - * @param string $context What the value is for. Valid values are view and edit. - * @return string - */ - public function get_catalog_visibility( $context = 'view' ) { - return apply_filters( $this->get_hook_prefix() . 'catalog_visibility', $this->parent_data['catalog_visibility'], $this ); - } - - /** - * Get attribute summary. - * - * By default, attribute summary contains comma-delimited 'attribute_name: attribute_value' pairs for all attributes. - * - * @param string $context What the value is for. Valid values are view and edit. - * - * @since 3.6.0 - * @return string - */ - public function get_attribute_summary( $context = 'view' ) { - return $this->get_prop( 'attribute_summary', $context ); - } - - - /** - * Set attribute summary. - * - * By default, attribute summary contains comma-delimited 'attribute_name: attribute_value' pairs for all attributes. - * - * @since 3.6.0 - * @param string $attribute_summary Summary of attribute names and values assigned to the variation. - */ - public function set_attribute_summary( $attribute_summary ) { - $this->set_prop( 'attribute_summary', $attribute_summary ); - } - - /* - |-------------------------------------------------------------------------- - | CRUD methods - |-------------------------------------------------------------------------- - */ - - /** - * Set the parent data array for this variation. - * - * @since 3.0.0 - * @param array $parent_data parent data array for this variation. - */ - public function set_parent_data( $parent_data ) { - $parent_data = wp_parse_args( - $parent_data, - array( - 'title' => '', - 'status' => '', - 'sku' => '', - 'manage_stock' => 'no', - 'backorders' => 'no', - 'stock_quantity' => '', - 'weight' => '', - 'length' => '', - 'width' => '', - 'height' => '', - 'tax_class' => '', - 'shipping_class_id' => 0, - 'image_id' => 0, - 'purchase_note' => '', - 'catalog_visibility' => 'visible', - ) - ); - - // Normalize tax class. - $parent_data['tax_class'] = sanitize_title( $parent_data['tax_class'] ); - $parent_data['tax_class'] = 'standard' === $parent_data['tax_class'] ? '' : $parent_data['tax_class']; - $valid_classes = $this->get_valid_tax_classes(); - - if ( ! in_array( $parent_data['tax_class'], $valid_classes, true ) ) { - $parent_data['tax_class'] = ''; - } - - $this->parent_data = $parent_data; - } - - /** - * Get the parent data array for this variation. - * - * @since 3.0.0 - * @return array - */ - public function get_parent_data() { - return $this->parent_data; - } - - /** - * Set attributes. Unlike the parent product which uses terms, variations are assigned - * specific attributes using name value pairs. - * - * @param array $raw_attributes array of raw attributes. - */ - public function set_attributes( $raw_attributes ) { - $raw_attributes = (array) $raw_attributes; - $attributes = array(); - - foreach ( $raw_attributes as $key => $value ) { - // Remove attribute prefix which meta gets stored with. - if ( 0 === strpos( $key, 'attribute_' ) ) { - $key = substr( $key, 10 ); - } - $attributes[ $key ] = $value; - } - $this->set_prop( 'attributes', $attributes ); - } - - /** - * Returns whether or not the product has any visible attributes. - * - * Variations are mapped to specific attributes unlike products, and the return - * value of ->get_attributes differs. Therefore this returns false. - * - * @return boolean - */ - public function has_attributes() { - return false; - } - - /* - |-------------------------------------------------------------------------- - | Conditionals - |-------------------------------------------------------------------------- - */ - - /** - * Returns false if the product cannot be bought. - * Override abstract method so that: i) Disabled variations are not be purchasable by admins. ii) Enabled variations are not purchasable if the parent product is not purchasable. - * - * @return bool - */ - public function is_purchasable() { - return apply_filters( 'woocommerce_variation_is_purchasable', $this->variation_is_visible() && parent::is_purchasable() && ( 'publish' === $this->parent_data['status'] || current_user_can( 'edit_post', $this->get_parent_id() ) ), $this ); - } - - /** - * Controls whether this particular variation will appear greyed-out (inactive) or not (active). - * Used by extensions to make incompatible variations appear greyed-out, etc. - * Other possible uses: prevent out-of-stock variations from being selected. - * - * @return bool - */ - public function variation_is_active() { - return apply_filters( 'woocommerce_variation_is_active', true, $this ); - } - - /** - * Checks if this particular variation is visible. Invisible variations are enabled and can be selected, but no price / stock info is displayed. - * Instead, a suitable 'unavailable' message is displayed. - * Invisible by default: Disabled variations and variations with an empty price. - * - * @return bool - */ - public function variation_is_visible() { - return apply_filters( 'woocommerce_variation_is_visible', 'publish' === get_post_status( $this->get_id() ) && '' !== $this->get_price(), $this->get_id(), $this->get_parent_id(), $this ); - } - - /** - * Return valid tax classes. Adds 'parent' to the default list of valid tax classes. - * - * @return array valid tax classes - */ - protected function get_valid_tax_classes() { - $valid_classes = WC_Tax::get_tax_class_slugs(); - $valid_classes[] = 'parent'; - - return $valid_classes; - } - - /** - * Delete variation, set the ID to 0, and return result. - * - * @since 4.4.0 - * @param bool $force_delete Should the variation be deleted permanently. - * @return bool result - */ - public function delete( $force_delete = false ) { - $variation_id = $this->get_id(); - - if ( ! parent::delete( $force_delete ) ) { - return false; - } - - return true; - } -} diff --git a/includes/class-wc-query.php b/includes/class-wc-query.php deleted file mode 100644 index 3575e228de0..00000000000 --- a/includes/class-wc-query.php +++ /dev/null @@ -1,971 +0,0 @@ -init_query_vars(); - } - - /** - * Get any errors from querystring. - */ - public function get_errors() { - // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $error = ! empty( $_GET['wc_error'] ) ? sanitize_text_field( wp_unslash( $_GET['wc_error'] ) ) : ''; - - if ( $error && ! wc_has_notice( $error, 'error' ) ) { - wc_add_notice( $error, 'error' ); - } - } - - /** - * Init query vars by loading options. - */ - public function init_query_vars() { - // Query vars to add to WP. - $this->query_vars = array( - // Checkout actions. - 'order-pay' => get_option( 'woocommerce_checkout_pay_endpoint', 'order-pay' ), - 'order-received' => get_option( 'woocommerce_checkout_order_received_endpoint', 'order-received' ), - // My account actions. - 'orders' => get_option( 'woocommerce_myaccount_orders_endpoint', 'orders' ), - 'view-order' => get_option( 'woocommerce_myaccount_view_order_endpoint', 'view-order' ), - 'downloads' => get_option( 'woocommerce_myaccount_downloads_endpoint', 'downloads' ), - 'edit-account' => get_option( 'woocommerce_myaccount_edit_account_endpoint', 'edit-account' ), - 'edit-address' => get_option( 'woocommerce_myaccount_edit_address_endpoint', 'edit-address' ), - 'payment-methods' => get_option( 'woocommerce_myaccount_payment_methods_endpoint', 'payment-methods' ), - 'lost-password' => get_option( 'woocommerce_myaccount_lost_password_endpoint', 'lost-password' ), - 'customer-logout' => get_option( 'woocommerce_logout_endpoint', 'customer-logout' ), - 'add-payment-method' => get_option( 'woocommerce_myaccount_add_payment_method_endpoint', 'add-payment-method' ), - 'delete-payment-method' => get_option( 'woocommerce_myaccount_delete_payment_method_endpoint', 'delete-payment-method' ), - 'set-default-payment-method' => get_option( 'woocommerce_myaccount_set_default_payment_method_endpoint', 'set-default-payment-method' ), - ); - } - - /** - * Get page title for an endpoint. - * - * @param string $endpoint Endpoint key. - * @param string $action Optional action or variation within the endpoint. - * - * @since 2.3.0 - * @since 4.6.0 Added $action parameter. - * @return string The page title. - */ - public function get_endpoint_title( $endpoint, $action = '' ) { - global $wp; - - switch ( $endpoint ) { - case 'order-pay': - $title = __( 'Pay for order', 'woocommerce' ); - break; - case 'order-received': - $title = __( 'Order received', 'woocommerce' ); - break; - case 'orders': - if ( ! empty( $wp->query_vars['orders'] ) ) { - /* translators: %s: page */ - $title = sprintf( __( 'Orders (page %d)', 'woocommerce' ), intval( $wp->query_vars['orders'] ) ); - } else { - $title = __( 'Orders', 'woocommerce' ); - } - break; - case 'view-order': - $order = wc_get_order( $wp->query_vars['view-order'] ); - /* translators: %s: order number */ - $title = ( $order ) ? sprintf( __( 'Order #%s', 'woocommerce' ), $order->get_order_number() ) : ''; - break; - case 'downloads': - $title = __( 'Downloads', 'woocommerce' ); - break; - case 'edit-account': - $title = __( 'Account details', 'woocommerce' ); - break; - case 'edit-address': - $title = __( 'Addresses', 'woocommerce' ); - break; - case 'payment-methods': - $title = __( 'Payment methods', 'woocommerce' ); - break; - case 'add-payment-method': - $title = __( 'Add payment method', 'woocommerce' ); - break; - case 'lost-password': - if ( in_array( $action, array( 'rp', 'resetpass', 'newaccount' ) ) ) { - $title = __( 'Set password', 'woocommerce' ); - } else { - $title = __( 'Lost password', 'woocommerce' ); - } - break; - default: - $title = ''; - break; - } - - /** - * Filters the page title used for my-account endpoints. - * - * @since 2.6.0 - * @since 4.6.0 Added $action parameter. - * - * @see get_endpoint_title() - * - * @param string $title Default title. - * @param string $endpoint Endpoint key. - * @param string $action Optional action or variation within the endpoint. - */ - return apply_filters( 'woocommerce_endpoint_' . $endpoint . '_title', $title, $endpoint, $action ); - } - - /** - * Endpoint mask describing the places the endpoint should be added. - * - * @since 2.6.2 - * @return int - */ - public function get_endpoints_mask() { - if ( 'page' === get_option( 'show_on_front' ) ) { - $page_on_front = get_option( 'page_on_front' ); - $myaccount_page_id = get_option( 'woocommerce_myaccount_page_id' ); - $checkout_page_id = get_option( 'woocommerce_checkout_page_id' ); - - if ( in_array( $page_on_front, array( $myaccount_page_id, $checkout_page_id ), true ) ) { - return EP_ROOT | EP_PAGES; - } - } - - return EP_PAGES; - } - - /** - * Add endpoints for query vars. - */ - public function add_endpoints() { - $mask = $this->get_endpoints_mask(); - - foreach ( $this->get_query_vars() as $key => $var ) { - if ( ! empty( $var ) ) { - add_rewrite_endpoint( $var, $mask ); - } - } - } - - /** - * Add query vars. - * - * @param array $vars Query vars. - * @return array - */ - public function add_query_vars( $vars ) { - foreach ( $this->get_query_vars() as $key => $var ) { - $vars[] = $key; - } - return $vars; - } - - /** - * Get query vars. - * - * @return array - */ - public function get_query_vars() { - return apply_filters( 'woocommerce_get_query_vars', $this->query_vars ); - } - - /** - * Get query current active query var. - * - * @return string - */ - public function get_current_endpoint() { - global $wp; - - foreach ( $this->get_query_vars() as $key => $value ) { - if ( isset( $wp->query_vars[ $key ] ) ) { - return $key; - } - } - return ''; - } - - /** - * Parse the request and look for query vars - endpoints may not be supported. - */ - public function parse_request() { - global $wp; - - // phpcs:disable WordPress.Security.NonceVerification.Recommended - // Map query vars to their keys, or get them if endpoints are not supported. - foreach ( $this->get_query_vars() as $key => $var ) { - if ( isset( $_GET[ $var ] ) ) { - $wp->query_vars[ $key ] = sanitize_text_field( wp_unslash( $_GET[ $var ] ) ); - } elseif ( isset( $wp->query_vars[ $var ] ) ) { - $wp->query_vars[ $key ] = $wp->query_vars[ $var ]; - } - } - // phpcs:enable WordPress.Security.NonceVerification.Recommended - } - - /** - * Are we currently on the front page? - * - * @param WP_Query $q Query instance. - * @return bool - */ - private function is_showing_page_on_front( $q ) { - return ( $q->is_home() && ! $q->is_posts_page ) && 'page' === get_option( 'show_on_front' ); - } - - /** - * Is the front page a page we define? - * - * @param int $page_id Page ID. - * @return bool - */ - private function page_on_front_is( $page_id ) { - return absint( get_option( 'page_on_front' ) ) === absint( $page_id ); - } - - /** - * Hook into pre_get_posts to do the main product query. - * - * @param WP_Query $q Query instance. - */ - public function pre_get_posts( $q ) { - // We only want to affect the main query. - if ( ! $q->is_main_query() ) { - return; - } - - // Fixes for queries on static homepages. - if ( $this->is_showing_page_on_front( $q ) ) { - - // Fix for endpoints on the homepage. - if ( ! $this->page_on_front_is( $q->get( 'page_id' ) ) ) { - $_query = wp_parse_args( $q->query ); - if ( ! empty( $_query ) && array_intersect( array_keys( $_query ), array_keys( $this->get_query_vars() ) ) ) { - $q->is_page = true; - $q->is_home = false; - $q->is_singular = true; - $q->set( 'page_id', (int) get_option( 'page_on_front' ) ); - add_filter( 'redirect_canonical', '__return_false' ); - } - } - - // When orderby is set, WordPress shows posts on the front-page. Get around that here. - if ( $this->page_on_front_is( wc_get_page_id( 'shop' ) ) ) { - $_query = wp_parse_args( $q->query ); - if ( empty( $_query ) || ! array_diff( array_keys( $_query ), array( 'preview', 'page', 'paged', 'cpage', 'orderby' ) ) ) { - $q->set( 'page_id', (int) get_option( 'page_on_front' ) ); - $q->is_page = true; - $q->is_home = false; - - // WP supporting themes show post type archive. - if ( current_theme_supports( 'woocommerce' ) ) { - $q->set( 'post_type', 'product' ); - } else { - $q->is_singular = true; - } - } - } elseif ( ! empty( $_GET['orderby'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $q->set( 'page_id', (int) get_option( 'page_on_front' ) ); - $q->is_page = true; - $q->is_home = false; - $q->is_singular = true; - } - } - - // Fix product feeds. - if ( $q->is_feed() && $q->is_post_type_archive( 'product' ) ) { - $q->is_comment_feed = false; - } - - // Special check for shops with the PRODUCT POST TYPE ARCHIVE on front. - if ( current_theme_supports( 'woocommerce' ) && $q->is_page() && 'page' === get_option( 'show_on_front' ) && absint( $q->get( 'page_id' ) ) === wc_get_page_id( 'shop' ) ) { - // This is a front-page shop. - $q->set( 'post_type', 'product' ); - $q->set( 'page_id', '' ); - - if ( isset( $q->query['paged'] ) ) { - $q->set( 'paged', $q->query['paged'] ); - } - - // Define a variable so we know this is the front page shop later on. - wc_maybe_define_constant( 'SHOP_IS_ON_FRONT', true ); - - // Get the actual WP page to avoid errors and let us use is_front_page(). - // This is hacky but works. Awaiting https://core.trac.wordpress.org/ticket/21096. - global $wp_post_types; - - $shop_page = get_post( wc_get_page_id( 'shop' ) ); - - $wp_post_types['product']->ID = $shop_page->ID; - $wp_post_types['product']->post_title = $shop_page->post_title; - $wp_post_types['product']->post_name = $shop_page->post_name; - $wp_post_types['product']->post_type = $shop_page->post_type; - $wp_post_types['product']->ancestors = get_ancestors( $shop_page->ID, $shop_page->post_type ); - - // Fix conditional Functions like is_front_page. - $q->is_singular = false; - $q->is_post_type_archive = true; - $q->is_archive = true; - $q->is_page = true; - - // Remove post type archive name from front page title tag. - add_filter( 'post_type_archive_title', '__return_empty_string', 5 ); - - // Fix WP SEO. - if ( class_exists( 'WPSEO_Meta' ) ) { - add_filter( 'wpseo_metadesc', array( $this, 'wpseo_metadesc' ) ); - add_filter( 'wpseo_metakey', array( $this, 'wpseo_metakey' ) ); - } - } elseif ( ! $q->is_post_type_archive( 'product' ) && ! $q->is_tax( get_object_taxonomies( 'product' ) ) ) { - // Only apply to product categories, the product post archive, the shop page, product tags, and product attribute taxonomies. - return; - } - - $this->product_query( $q ); - } - - /** - * Handler for the 'the_posts' WP filter. - * - * @param array $posts Posts from WP Query. - * @param WP_Query $query Current query. - * - * @return array - */ - public function handle_get_posts( $posts, $query ) { - if ( 'product_query' !== $query->get( 'wc_query' ) ) { - return $posts; - } - $this->remove_product_query_filters( $posts ); - return $posts; - } - - - /** - * Pre_get_posts above may adjust the main query to add WooCommerce logic. When this query is done, we need to ensure - * all custom filters are removed. - * - * This is done here during the_posts filter. The input is not changed. - * - * @param array $posts Posts from WP Query. - * @return array - */ - public function remove_product_query_filters( $posts ) { - $this->remove_ordering_args(); - remove_filter( 'posts_clauses', array( $this, 'price_filter_post_clauses' ), 10, 2 ); - return $posts; - } - - /** - * This function used to be hooked to found_posts and adjust the posts count when the filtering by attribute - * widget was used and variable products were present. Now it isn't hooked anymore and does nothing but return - * the input unchanged, since the pull request in which it was introduced has been reverted. - * - * @since 4.4.0 - * @param int $count Original posts count, as supplied by the found_posts filter. - * @param WP_Query $query The current WP_Query object. - * - * @return int Adjusted posts count. - */ - public function adjust_posts_count( $count, $query ) { - return $count; - } - - /** - * Instance version of get_layered_nav_chosen_attributes, needed for unit tests. - * - * @return array - */ - protected function get_layered_nav_chosen_attributes_inst() { - return self::get_layered_nav_chosen_attributes(); - } - - /** - * Get the posts (or the ids of the posts) found in the current WP loop. - * - * @return array Array of posts or post ids. - */ - protected function get_current_posts() { - return $GLOBALS['wp_query']->posts; - } - - /** - * WP SEO meta description. - * - * Hooked into wpseo_ hook already, so no need for function_exist. - * - * @return string - */ - public function wpseo_metadesc() { - return WPSEO_Meta::get_value( 'metadesc', wc_get_page_id( 'shop' ) ); - } - - /** - * WP SEO meta key. - * - * Hooked into wpseo_ hook already, so no need for function_exist. - * - * @return string - */ - public function wpseo_metakey() { - return WPSEO_Meta::get_value( 'metakey', wc_get_page_id( 'shop' ) ); - } - - /** - * Query the products, applying sorting/ordering etc. - * This applies to the main WordPress loop. - * - * @param WP_Query $q Query instance. - */ - public function product_query( $q ) { - if ( ! is_feed() ) { - $ordering = $this->get_catalog_ordering_args(); - $q->set( 'orderby', $ordering['orderby'] ); - $q->set( 'order', $ordering['order'] ); - - if ( isset( $ordering['meta_key'] ) ) { - $q->set( 'meta_key', $ordering['meta_key'] ); - } - } - - // Query vars that affect posts shown. - $q->set( 'meta_query', $this->get_meta_query( $q->get( 'meta_query' ), true ) ); - $q->set( 'tax_query', $this->get_tax_query( $q->get( 'tax_query' ), true ) ); - $q->set( 'wc_query', 'product_query' ); - $q->set( 'post__in', array_unique( (array) apply_filters( 'loop_shop_post_in', array() ) ) ); - - // Work out how many products to query. - $q->set( 'posts_per_page', $q->get( 'posts_per_page' ) ? $q->get( 'posts_per_page' ) : apply_filters( 'loop_shop_per_page', wc_get_default_products_per_row() * wc_get_default_product_rows_per_page() ) ); - - // Store reference to this query. - self::$product_query = $q; - - // Additonal hooks to change WP Query. - add_filter( 'posts_clauses', array( $this, 'price_filter_post_clauses' ), 10, 2 ); - add_filter( 'the_posts', array( $this, 'handle_get_posts' ), 10, 2 ); - - do_action( 'woocommerce_product_query', $q, $this ); - } - - /** - * Remove the query. - */ - public function remove_product_query() { - remove_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) ); - } - - /** - * Remove ordering queries. - */ - public function remove_ordering_args() { - remove_filter( 'posts_clauses', array( $this, 'order_by_price_asc_post_clauses' ) ); - remove_filter( 'posts_clauses', array( $this, 'order_by_price_desc_post_clauses' ) ); - remove_filter( 'posts_clauses', array( $this, 'order_by_popularity_post_clauses' ) ); - remove_filter( 'posts_clauses', array( $this, 'order_by_rating_post_clauses' ) ); - } - - /** - * Returns an array of arguments for ordering products based on the selected values. - * - * @param string $orderby Order by param. - * @param string $order Order param. - * @return array - */ - public function get_catalog_ordering_args( $orderby = '', $order = '' ) { - // Get ordering from query string unless defined. - if ( ! $orderby ) { - // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $orderby_value = isset( $_GET['orderby'] ) ? wc_clean( (string) wp_unslash( $_GET['orderby'] ) ) : wc_clean( get_query_var( 'orderby' ) ); - - if ( ! $orderby_value ) { - if ( is_search() ) { - $orderby_value = 'relevance'; - } else { - $orderby_value = apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby', 'menu_order' ) ); - } - } - - // Get order + orderby args from string. - $orderby_value = is_array( $orderby_value ) ? $orderby_value : explode( '-', $orderby_value ); - $orderby = esc_attr( $orderby_value[0] ); - $order = ! empty( $orderby_value[1] ) ? $orderby_value[1] : $order; - } - - // Convert to correct format. - $orderby = strtolower( is_array( $orderby ) ? (string) current( $orderby ) : (string) $orderby ); - $order = strtoupper( is_array( $order ) ? (string) current( $order ) : (string) $order ); - $args = array( - 'orderby' => $orderby, - 'order' => ( 'DESC' === $order ) ? 'DESC' : 'ASC', - 'meta_key' => '', // @codingStandardsIgnoreLine - ); - - switch ( $orderby ) { - case 'id': - $args['orderby'] = 'ID'; - break; - case 'menu_order': - $args['orderby'] = 'menu_order title'; - break; - case 'title': - $args['orderby'] = 'title'; - $args['order'] = ( 'DESC' === $order ) ? 'DESC' : 'ASC'; - break; - case 'relevance': - $args['orderby'] = 'relevance'; - $args['order'] = 'DESC'; - break; - case 'rand': - $args['orderby'] = 'rand'; // @codingStandardsIgnoreLine - break; - case 'date': - $args['orderby'] = 'date ID'; - $args['order'] = ( 'ASC' === $order ) ? 'ASC' : 'DESC'; - break; - case 'price': - $callback = 'DESC' === $order ? 'order_by_price_desc_post_clauses' : 'order_by_price_asc_post_clauses'; - add_filter( 'posts_clauses', array( $this, $callback ) ); - break; - case 'popularity': - add_filter( 'posts_clauses', array( $this, 'order_by_popularity_post_clauses' ) ); - break; - case 'rating': - add_filter( 'posts_clauses', array( $this, 'order_by_rating_post_clauses' ) ); - break; - } - - return apply_filters( 'woocommerce_get_catalog_ordering_args', $args, $orderby, $order ); - } - - /** - * Custom query used to filter products by price. - * - * @since 3.6.0 - * - * @param array $args Query args. - * @param WP_Query $wp_query WP_Query object. - * - * @return array - */ - public function price_filter_post_clauses( $args, $wp_query ) { - global $wpdb; - - // phpcs:ignore WordPress.Security.NonceVerification.Recommended - if ( ! $wp_query->is_main_query() || ( ! isset( $_GET['max_price'] ) && ! isset( $_GET['min_price'] ) ) ) { - return $args; - } - - // phpcs:disable WordPress.Security.NonceVerification.Recommended - $current_min_price = isset( $_GET['min_price'] ) ? floatval( wp_unslash( $_GET['min_price'] ) ) : 0; - $current_max_price = isset( $_GET['max_price'] ) ? floatval( wp_unslash( $_GET['max_price'] ) ) : PHP_INT_MAX; - // phpcs:enable WordPress.Security.NonceVerification.Recommended - - /** - * Adjust if the store taxes are not displayed how they are stored. - * Kicks in when prices excluding tax are displayed including tax. - */ - if ( wc_tax_enabled() && 'incl' === get_option( 'woocommerce_tax_display_shop' ) && ! wc_prices_include_tax() ) { - $tax_class = apply_filters( 'woocommerce_price_filter_widget_tax_class', '' ); // Uses standard tax class. - $tax_rates = WC_Tax::get_rates( $tax_class ); - - if ( $tax_rates ) { - $current_min_price -= WC_Tax::get_tax_total( WC_Tax::calc_inclusive_tax( $current_min_price, $tax_rates ) ); - $current_max_price -= WC_Tax::get_tax_total( WC_Tax::calc_inclusive_tax( $current_max_price, $tax_rates ) ); - } - } - - $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); - $args['where'] .= $wpdb->prepare( - ' AND NOT (%fwc_product_meta_lookup.max_price ) ', - $current_max_price, - $current_min_price - ); - return $args; - } - - /** - * Handle numeric price sorting. - * - * @param array $args Query args. - * @return array - */ - public function order_by_price_asc_post_clauses( $args ) { - $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); - $args['orderby'] = ' wc_product_meta_lookup.min_price ASC, wc_product_meta_lookup.product_id ASC '; - return $args; - } - - /** - * Handle numeric price sorting. - * - * @param array $args Query args. - * @return array - */ - public function order_by_price_desc_post_clauses( $args ) { - $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); - $args['orderby'] = ' wc_product_meta_lookup.max_price DESC, wc_product_meta_lookup.product_id DESC '; - return $args; - } - - /** - * WP Core does not let us change the sort direction for individual orderby params - https://core.trac.wordpress.org/ticket/17065. - * - * This lets us sort by meta value desc, and have a second orderby param. - * - * @param array $args Query args. - * @return array - */ - public function order_by_popularity_post_clauses( $args ) { - $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); - $args['orderby'] = ' wc_product_meta_lookup.total_sales DESC, wc_product_meta_lookup.product_id DESC '; - return $args; - } - - /** - * Order by rating post clauses. - * - * @param array $args Query args. - * @return array - */ - public function order_by_rating_post_clauses( $args ) { - $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); - $args['orderby'] = ' wc_product_meta_lookup.average_rating DESC, wc_product_meta_lookup.rating_count DESC, wc_product_meta_lookup.product_id DESC '; - return $args; - } - - /** - * Join wc_product_meta_lookup to posts if not already joined. - * - * @param string $sql SQL join. - * @return string - */ - private function append_product_sorting_table_join( $sql ) { - global $wpdb; - - if ( ! strstr( $sql, 'wc_product_meta_lookup' ) ) { - $sql .= " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON $wpdb->posts.ID = wc_product_meta_lookup.product_id "; - } - return $sql; - } - - /** - * Appends meta queries to an array. - * - * @param array $meta_query Meta query. - * @param bool $main_query If is main query. - * @return array - */ - public function get_meta_query( $meta_query = array(), $main_query = false ) { - if ( ! is_array( $meta_query ) ) { - $meta_query = array(); - } - return array_filter( apply_filters( 'woocommerce_product_query_meta_query', $meta_query, $this ) ); - } - - /** - * Appends tax queries to an array. - * - * @param array $tax_query Tax query. - * @param bool $main_query If is main query. - * @return array - */ - public function get_tax_query( $tax_query = array(), $main_query = false ) { - if ( ! is_array( $tax_query ) ) { - $tax_query = array( - 'relation' => 'AND', - ); - } - - // Layered nav filters on terms. - if ( $main_query ) { - foreach ( $this->get_layered_nav_chosen_attributes() as $taxonomy => $data ) { - $tax_query[] = array( - 'taxonomy' => $taxonomy, - 'field' => 'slug', - 'terms' => $data['terms'], - 'operator' => 'and' === $data['query_type'] ? 'AND' : 'IN', - 'include_children' => false, - ); - } - } - - $product_visibility_terms = wc_get_product_visibility_term_ids(); - $product_visibility_not_in = array( is_search() && $main_query ? $product_visibility_terms['exclude-from-search'] : $product_visibility_terms['exclude-from-catalog'] ); - - // Hide out of stock products. - if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { - $product_visibility_not_in[] = $product_visibility_terms['outofstock']; - } - - // phpcs:disable WordPress.Security.NonceVerification.Recommended - // Filter by rating. - if ( isset( $_GET['rating_filter'] ) ) { - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - $rating_filter = array_filter( array_map( 'absint', explode( ',', wp_unslash( $_GET['rating_filter'] ) ) ) ); - $rating_terms = array(); - for ( $i = 1; $i <= 5; $i ++ ) { - if ( in_array( $i, $rating_filter, true ) && isset( $product_visibility_terms[ 'rated-' . $i ] ) ) { - $rating_terms[] = $product_visibility_terms[ 'rated-' . $i ]; - } - } - if ( ! empty( $rating_terms ) ) { - $tax_query[] = array( - 'taxonomy' => 'product_visibility', - 'field' => 'term_taxonomy_id', - 'terms' => $rating_terms, - 'operator' => 'IN', - 'rating_filter' => true, - ); - } - } - // phpcs:enable WordPress.Security.NonceVerification.Recommended - - if ( ! empty( $product_visibility_not_in ) ) { - $tax_query[] = array( - 'taxonomy' => 'product_visibility', - 'field' => 'term_taxonomy_id', - 'terms' => $product_visibility_not_in, - 'operator' => 'NOT IN', - ); - } - - return array_filter( apply_filters( 'woocommerce_product_query_tax_query', $tax_query, $this ) ); - } - - /** - * Get the main query which product queries ran against. - * - * @return WP_Query - */ - public static function get_main_query() { - return self::$product_query; - } - - /** - * Get the tax query which was used by the main query. - * - * @return array - */ - public static function get_main_tax_query() { - $tax_query = isset( self::$product_query->tax_query, self::$product_query->tax_query->queries ) ? self::$product_query->tax_query->queries : array(); - - return $tax_query; - } - - /** - * Get the meta query which was used by the main query. - * - * @return array - */ - public static function get_main_meta_query() { - $args = self::$product_query->query_vars; - $meta_query = isset( $args['meta_query'] ) ? $args['meta_query'] : array(); - - return $meta_query; - } - - /** - * Based on WP_Query::parse_search - */ - public static function get_main_search_query_sql() { - global $wpdb; - - $args = self::$product_query->query_vars; - $search_terms = isset( $args['search_terms'] ) ? $args['search_terms'] : array(); - $sql = array(); - - foreach ( $search_terms as $term ) { - // Terms prefixed with '-' should be excluded. - $include = '-' !== substr( $term, 0, 1 ); - - if ( $include ) { - $like_op = 'LIKE'; - $andor_op = 'OR'; - } else { - $like_op = 'NOT LIKE'; - $andor_op = 'AND'; - $term = substr( $term, 1 ); - } - - $like = '%' . $wpdb->esc_like( $term ) . '%'; - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $sql[] = $wpdb->prepare( "(($wpdb->posts.post_title $like_op %s) $andor_op ($wpdb->posts.post_excerpt $like_op %s) $andor_op ($wpdb->posts.post_content $like_op %s))", $like, $like, $like ); - } - - if ( ! empty( $sql ) && ! is_user_logged_in() ) { - $sql[] = "($wpdb->posts.post_password = '')"; - } - - return implode( ' AND ', $sql ); - } - - /** - * Get an array of attributes and terms selected with the layered nav widget. - * - * @return array - */ - public static function get_layered_nav_chosen_attributes() { - // phpcs:disable WordPress.Security.NonceVerification.Recommended - if ( ! is_array( self::$chosen_attributes ) ) { - self::$chosen_attributes = array(); - - if ( ! empty( $_GET ) ) { - foreach ( $_GET as $key => $value ) { - if ( 0 === strpos( $key, 'filter_' ) ) { - $attribute = wc_sanitize_taxonomy_name( str_replace( 'filter_', '', $key ) ); - $taxonomy = wc_attribute_taxonomy_name( $attribute ); - $filter_terms = ! empty( $value ) ? explode( ',', wc_clean( wp_unslash( $value ) ) ) : array(); - - if ( empty( $filter_terms ) || ! taxonomy_exists( $taxonomy ) || ! wc_attribute_taxonomy_id_by_name( $attribute ) ) { - continue; - } - - $query_type = ! empty( $_GET[ 'query_type_' . $attribute ] ) && in_array( $_GET[ 'query_type_' . $attribute ], array( 'and', 'or' ), true ) ? wc_clean( wp_unslash( $_GET[ 'query_type_' . $attribute ] ) ) : ''; - self::$chosen_attributes[ $taxonomy ]['terms'] = array_map( 'sanitize_title', $filter_terms ); // Ensures correct encoding. - self::$chosen_attributes[ $taxonomy ]['query_type'] = $query_type ? $query_type : apply_filters( 'woocommerce_layered_nav_default_query_type', 'and' ); - } - } - } - } - return self::$chosen_attributes; - // phpcs:disable WordPress.Security.NonceVerification.Recommended - } - - /** - * Remove the add-to-cart param from pagination urls. - * - * @param string $url URL. - * @return string - */ - public function remove_add_to_cart_pagination( $url ) { - return remove_query_arg( 'add-to-cart', $url ); - } - - /** - * Return a meta query for filtering by rating. - * - * @deprecated 3.0.0 Replaced with taxonomy. - * @return array - */ - public function rating_filter_meta_query() { - return array(); - } - - /** - * Returns a meta query to handle product visibility. - * - * @deprecated 3.0.0 Replaced with taxonomy. - * @param string $compare (default: 'IN'). - * @return array - */ - public function visibility_meta_query( $compare = 'IN' ) { - return array(); - } - - /** - * Returns a meta query to handle product stock status. - * - * @deprecated 3.0.0 Replaced with taxonomy. - * @param string $status (default: 'instock'). - * @return array - */ - public function stock_status_meta_query( $status = 'instock' ) { - return array(); - } - - /** - * Layered nav init. - * - * @deprecated 2.6.0 - */ - public function layered_nav_init() { - wc_deprecated_function( 'layered_nav_init', '2.6' ); - } - - /** - * Get an unpaginated list all product IDs (both filtered and unfiltered). Makes use of transients. - * - * @deprecated 2.6.0 due to performance concerns - */ - public function get_products_in_view() { - wc_deprecated_function( 'get_products_in_view', '2.6' ); - } - - /** - * Layered Nav post filter. - * - * @deprecated 2.6.0 due to performance concerns - * - * @param mixed $deprecated Deprecated. - */ - public function layered_nav_query( $deprecated ) { - wc_deprecated_function( 'layered_nav_query', '2.6' ); - } - - /** - * Search post excerpt. - * - * @param string $where Where clause. - * - * @deprecated 3.2.0 - Not needed anymore since WordPress 4.5. - */ - public function search_post_excerpt( $where = '' ) { - wc_deprecated_function( 'WC_Query::search_post_excerpt', '3.2.0', 'Excerpt added to search query by default since WordPress 4.5.' ); - return $where; - } - - /** - * Remove the posts_where filter. - * - * @deprecated 3.2.0 - Nothing to remove anymore because search_post_excerpt() is deprecated. - */ - public function remove_posts_where() { - wc_deprecated_function( 'WC_Query::remove_posts_where', '3.2.0', 'Nothing to remove anymore because search_post_excerpt() is deprecated.' ); - } -} diff --git a/includes/class-wc-rate-limiter.php b/includes/class-wc-rate-limiter.php deleted file mode 100644 index aba4fb01489..00000000000 --- a/includes/class-wc-rate-limiter.php +++ /dev/null @@ -1,79 +0,0 @@ -object = $object; - - if ( 'page' === $type ) { - add_filter( 'woocommerce_settings_groups', array( $this, 'register_page_group' ) ); - add_filter( 'woocommerce_settings-' . $this->object->get_id(), array( $this, 'register_page_settings' ) ); - } elseif ( 'email' === $type ) { - add_filter( 'woocommerce_settings_groups', array( $this, 'register_email_group' ) ); - add_filter( 'woocommerce_settings-email_' . $this->object->id, array( $this, 'register_email_settings' ) ); - } - } - - /** - * Register's all of our different notification emails as sub groups - * of email settings. - * - * @since 3.0.0 - * @param array $groups Existing registered groups. - * @return array - */ - public function register_email_group( $groups ) { - $groups[] = array( - 'id' => 'email_' . $this->object->id, - 'label' => $this->object->title, - 'description' => $this->object->description, - 'parent_id' => 'email', - ); - return $groups; - } - - /** - * Registers all of the setting form fields for emails to each email type's group. - * - * @since 3.0.0 - * @param array $settings Existing registered settings. - * @return array - */ - public function register_email_settings( $settings ) { - foreach ( $this->object->form_fields as $id => $setting ) { - $setting['id'] = $id; - $setting['option_key'] = array( $this->object->get_option_key(), $id ); - $new_setting = $this->register_setting( $setting ); - if ( $new_setting ) { - $settings[] = $new_setting; - } - } - return $settings; - } - - /** - * Registers a setting group, based on admin page ID & label as parent group. - * - * @since 3.0.0 - * @param array $groups Array of previously registered groups. - * @return array - */ - public function register_page_group( $groups ) { - $groups[] = array( - 'id' => $this->object->get_id(), - 'label' => $this->object->get_label(), - ); - return $groups; - } - - /** - * Registers settings to a specific group. - * - * @since 3.0.0 - * @param array $settings Existing registered settings. - * @return array - */ - public function register_page_settings( $settings ) { - /** - * WP admin settings can be broken down into separate sections from - * a UI standpoint. This will grab all the sections associated with - * a particular setting group (like 'products') and register them - * to the REST API. - */ - $sections = $this->object->get_sections(); - if ( empty( $sections ) ) { - // Default section is just an empty string, per admin page classes. - $sections = array( '' ); - } - - foreach ( $sections as $section => $section_label ) { - $settings_from_section = $this->object->get_settings( $section ); - foreach ( $settings_from_section as $setting ) { - if ( ! isset( $setting['id'] ) ) { - continue; - } - $setting['option_key'] = $setting['id']; - $new_setting = $this->register_setting( $setting ); - if ( $new_setting ) { - $settings[] = $new_setting; - } - } - } - return $settings; - } - - /** - * Register a setting into the format expected for the Settings REST API. - * - * @since 3.0.0 - * @param array $setting Setting data. - * @return array|bool - */ - public function register_setting( $setting ) { - if ( ! isset( $setting['id'] ) ) { - return false; - } - - $description = ''; - if ( ! empty( $setting['desc'] ) ) { - $description = $setting['desc']; - } elseif ( ! empty( $setting['description'] ) ) { - $description = $setting['description']; - } - - $new_setting = array( - 'id' => $setting['id'], - 'label' => ( ! empty( $setting['title'] ) ? $setting['title'] : '' ), - 'description' => $description, - 'type' => $setting['type'], - 'option_key' => $setting['option_key'], - ); - - if ( isset( $setting['default'] ) ) { - $new_setting['default'] = $setting['default']; - } - if ( isset( $setting['options'] ) ) { - $new_setting['options'] = $setting['options']; - } - if ( isset( $setting['desc_tip'] ) ) { - if ( true === $setting['desc_tip'] ) { - $new_setting['tip'] = $description; - } elseif ( ! empty( $setting['desc_tip'] ) ) { - $new_setting['tip'] = $setting['desc_tip']; - } - } - - return $new_setting; - } - -} diff --git a/includes/class-wc-session-handler.php b/includes/class-wc-session-handler.php deleted file mode 100644 index cacda9e59a2..00000000000 --- a/includes/class-wc-session-handler.php +++ /dev/null @@ -1,384 +0,0 @@ -_cookie = apply_filters( 'woocommerce_cookie', 'wp_woocommerce_session_' . COOKIEHASH ); - $this->_table = $GLOBALS['wpdb']->prefix . 'woocommerce_sessions'; - } - - /** - * Init hooks and session data. - * - * @since 3.3.0 - */ - public function init() { - $this->init_session_cookie(); - - add_action( 'woocommerce_set_cart_cookies', array( $this, 'set_customer_session_cookie' ), 10 ); - add_action( 'shutdown', array( $this, 'save_data' ), 20 ); - add_action( 'wp_logout', array( $this, 'destroy_session' ) ); - - if ( ! is_user_logged_in() ) { - add_filter( 'nonce_user_logged_out', array( $this, 'nonce_user_logged_out' ) ); - } - } - - /** - * Setup cookie and customer ID. - * - * @since 3.6.0 - */ - public function init_session_cookie() { - $cookie = $this->get_session_cookie(); - - if ( $cookie ) { - $this->_customer_id = $cookie[0]; - $this->_session_expiration = $cookie[1]; - $this->_session_expiring = $cookie[2]; - $this->_has_cookie = true; - $this->_data = $this->get_session_data(); - - // If the user logs in, update session. - if ( is_user_logged_in() && strval( get_current_user_id() ) !== $this->_customer_id ) { - $guest_session_id = $this->_customer_id; - $this->_customer_id = strval( get_current_user_id() ); - $this->_dirty = true; - $this->save_data( $guest_session_id ); - $this->set_customer_session_cookie( true ); - } - - // Update session if its close to expiring. - if ( time() > $this->_session_expiring ) { - $this->set_session_expiration(); - $this->update_session_timestamp( $this->_customer_id, $this->_session_expiration ); - } - } else { - $this->set_session_expiration(); - $this->_customer_id = $this->generate_customer_id(); - $this->_data = $this->get_session_data(); - } - } - - /** - * Sets the session cookie on-demand (usually after adding an item to the cart). - * - * Since the cookie name (as of 2.1) is prepended with wp, cache systems like batcache will not cache pages when set. - * - * Warning: Cookies will only be set if this is called before the headers are sent. - * - * @param bool $set Should the session cookie be set. - */ - public function set_customer_session_cookie( $set ) { - if ( $set ) { - $to_hash = $this->_customer_id . '|' . $this->_session_expiration; - $cookie_hash = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) ); - $cookie_value = $this->_customer_id . '||' . $this->_session_expiration . '||' . $this->_session_expiring . '||' . $cookie_hash; - $this->_has_cookie = true; - - if ( ! isset( $_COOKIE[ $this->_cookie ] ) || $_COOKIE[ $this->_cookie ] !== $cookie_value ) { - wc_setcookie( $this->_cookie, $cookie_value, $this->_session_expiration, $this->use_secure_cookie(), true ); - } - } - } - - /** - * Should the session cookie be secure? - * - * @since 3.6.0 - * @return bool - */ - protected function use_secure_cookie() { - return apply_filters( 'wc_session_use_secure_cookie', wc_site_is_https() && is_ssl() ); - } - - /** - * Return true if the current user has an active session, i.e. a cookie to retrieve values. - * - * @return bool - */ - public function has_session() { - return isset( $_COOKIE[ $this->_cookie ] ) || $this->_has_cookie || is_user_logged_in(); // @codingStandardsIgnoreLine. - } - - /** - * Set session expiration. - */ - public function set_session_expiration() { - $this->_session_expiring = time() + intval( apply_filters( 'wc_session_expiring', 60 * 60 * 47 ) ); // 47 Hours. - $this->_session_expiration = time() + intval( apply_filters( 'wc_session_expiration', 60 * 60 * 48 ) ); // 48 Hours. - } - - /** - * Generate a unique customer ID for guests, or return user ID if logged in. - * - * Uses Portable PHP password hashing framework to generate a unique cryptographically strong ID. - * - * @return string - */ - public function generate_customer_id() { - $customer_id = ''; - - if ( is_user_logged_in() ) { - $customer_id = strval( get_current_user_id() ); - } - - if ( empty( $customer_id ) ) { - require_once ABSPATH . 'wp-includes/class-phpass.php'; - $hasher = new PasswordHash( 8, false ); - $customer_id = md5( $hasher->get_random_bytes( 32 ) ); - } - - return $customer_id; - } - - /** - * Get the session cookie, if set. Otherwise return false. - * - * Session cookies without a customer ID are invalid. - * - * @return bool|array - */ - public function get_session_cookie() { - $cookie_value = isset( $_COOKIE[ $this->_cookie ] ) ? wp_unslash( $_COOKIE[ $this->_cookie ] ) : false; // @codingStandardsIgnoreLine. - - if ( empty( $cookie_value ) || ! is_string( $cookie_value ) ) { - return false; - } - - list( $customer_id, $session_expiration, $session_expiring, $cookie_hash ) = explode( '||', $cookie_value ); - - if ( empty( $customer_id ) ) { - return false; - } - - // Validate hash. - $to_hash = $customer_id . '|' . $session_expiration; - $hash = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) ); - - if ( empty( $cookie_hash ) || ! hash_equals( $hash, $cookie_hash ) ) { - return false; - } - - return array( $customer_id, $session_expiration, $session_expiring, $cookie_hash ); - } - - /** - * Get session data. - * - * @return array - */ - public function get_session_data() { - return $this->has_session() ? (array) $this->get_session( $this->_customer_id, array() ) : array(); - } - - /** - * Gets a cache prefix. This is used in session names so the entire cache can be invalidated with 1 function call. - * - * @return string - */ - private function get_cache_prefix() { - return WC_Cache_Helper::get_cache_prefix( WC_SESSION_CACHE_GROUP ); - } - - /** - * Save data and delete guest session. - * - * @param int $old_session_key session ID before user logs in. - */ - public function save_data( $old_session_key = 0 ) { - // Dirty if something changed - prevents saving nothing new. - if ( $this->_dirty && $this->has_session() ) { - global $wpdb; - - $wpdb->query( - $wpdb->prepare( - "INSERT INTO {$wpdb->prefix}woocommerce_sessions (`session_key`, `session_value`, `session_expiry`) VALUES (%s, %s, %d) - ON DUPLICATE KEY UPDATE `session_value` = VALUES(`session_value`), `session_expiry` = VALUES(`session_expiry`)", - $this->_customer_id, - maybe_serialize( $this->_data ), - $this->_session_expiration - ) - ); - - wp_cache_set( $this->get_cache_prefix() . $this->_customer_id, $this->_data, WC_SESSION_CACHE_GROUP, $this->_session_expiration - time() ); - $this->_dirty = false; - if ( get_current_user_id() != $old_session_key && ! is_object( get_user_by( 'id', $old_session_key ) ) ) { - $this->delete_session( $old_session_key ); - } - } - } - - /** - * Destroy all session data. - */ - public function destroy_session() { - $this->delete_session( $this->_customer_id ); - $this->forget_session(); - } - - /** - * Forget all session data without destroying it. - */ - public function forget_session() { - wc_setcookie( $this->_cookie, '', time() - YEAR_IN_SECONDS, $this->use_secure_cookie(), true ); - - wc_empty_cart(); - - $this->_data = array(); - $this->_dirty = false; - $this->_customer_id = $this->generate_customer_id(); - } - - /** - * When a user is logged out, ensure they have a unique nonce by using the customer/session ID. - * - * @param int $uid User ID. - * @return string - */ - public function nonce_user_logged_out( $uid ) { - return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid; - } - - /** - * Cleanup session data from the database and clear caches. - */ - public function cleanup_sessions() { - global $wpdb; - - $wpdb->query( $wpdb->prepare( "DELETE FROM $this->_table WHERE session_expiry < %d", time() ) ); // @codingStandardsIgnoreLine. - - if ( class_exists( 'WC_Cache_Helper' ) ) { - WC_Cache_Helper::invalidate_cache_group( WC_SESSION_CACHE_GROUP ); - } - } - - /** - * Returns the session. - * - * @param string $customer_id Custo ID. - * @param mixed $default Default session value. - * @return string|array - */ - public function get_session( $customer_id, $default = false ) { - global $wpdb; - - if ( Constants::is_defined( 'WP_SETUP_CONFIG' ) ) { - return false; - } - - // Try to get it from the cache, it will return false if not present or if object cache not in use. - $value = wp_cache_get( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP ); - - if ( false === $value ) { - $value = $wpdb->get_var( $wpdb->prepare( "SELECT session_value FROM $this->_table WHERE session_key = %s", $customer_id ) ); // @codingStandardsIgnoreLine. - - if ( is_null( $value ) ) { - $value = $default; - } - - $cache_duration = $this->_session_expiration - time(); - if ( 0 < $cache_duration ) { - wp_cache_add( $this->get_cache_prefix() . $customer_id, $value, WC_SESSION_CACHE_GROUP, $cache_duration ); - } - } - - return maybe_unserialize( $value ); - } - - /** - * Delete the session from the cache and database. - * - * @param int $customer_id Customer ID. - */ - public function delete_session( $customer_id ) { - global $wpdb; - - wp_cache_delete( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP ); - - $wpdb->delete( - $this->_table, - array( - 'session_key' => $customer_id, - ) - ); - } - - /** - * Update the session expiry timestamp. - * - * @param string $customer_id Customer ID. - * @param int $timestamp Timestamp to expire the cookie. - */ - public function update_session_timestamp( $customer_id, $timestamp ) { - global $wpdb; - - $wpdb->update( - $this->_table, - array( - 'session_expiry' => $timestamp, - ), - array( - 'session_key' => $customer_id, - ), - array( - '%d', - ) - ); - } -} diff --git a/includes/class-wc-shipping-rate.php b/includes/class-wc-shipping-rate.php deleted file mode 100644 index 1248a70a423..00000000000 --- a/includes/class-wc-shipping-rate.php +++ /dev/null @@ -1,252 +0,0 @@ - '', - 'method_id' => '', - 'instance_id' => 0, - 'label' => '', - 'cost' => 0, - 'taxes' => array(), - ); - - /** - * Stores meta data for this rate. - * - * @since 2.6.0 - * @var array - */ - protected $meta_data = array(); - - /** - * Constructor. - * - * @param string $id Shipping rate ID. - * @param string $label Shipping rate label. - * @param integer $cost Cost. - * @param array $taxes Taxes applied to shipping rate. - * @param string $method_id Shipping method ID. - * @param int $instance_id Shipping instance ID. - */ - public function __construct( $id = '', $label = '', $cost = 0, $taxes = array(), $method_id = '', $instance_id = 0 ) { - $this->set_id( $id ); - $this->set_label( $label ); - $this->set_cost( $cost ); - $this->set_taxes( $taxes ); - $this->set_method_id( $method_id ); - $this->set_instance_id( $instance_id ); - } - - /** - * Magic methods to support direct access to props. - * - * @since 3.2.0 - * @param string $key Key. - * @return bool - */ - public function __isset( $key ) { - return isset( $this->data[ $key ] ); - } - - /** - * Magic methods to support direct access to props. - * - * @since 3.2.0 - * @param string $key Key. - * @return mixed - */ - public function __get( $key ) { - if ( is_callable( array( $this, "get_{$key}" ) ) ) { - return $this->{"get_{$key}"}(); - } elseif ( isset( $this->data[ $key ] ) ) { - return $this->data[ $key ]; - } else { - return ''; - } - } - - /** - * Magic methods to support direct access to props. - * - * @since 3.2.0 - * @param string $key Key. - * @param mixed $value Value. - */ - public function __set( $key, $value ) { - if ( is_callable( array( $this, "set_{$key}" ) ) ) { - $this->{"set_{$key}"}( $value ); - } else { - $this->data[ $key ] = $value; - } - } - - /** - * Set ID for the rate. This is usually a combination of the method and instance IDs. - * - * @since 3.2.0 - * @param string $id Shipping rate ID. - */ - public function set_id( $id ) { - $this->data['id'] = (string) $id; - } - - /** - * Set shipping method ID the rate belongs to. - * - * @since 3.2.0 - * @param string $method_id Shipping method ID. - */ - public function set_method_id( $method_id ) { - $this->data['method_id'] = (string) $method_id; - } - - /** - * Set instance ID the rate belongs to. - * - * @since 3.2.0 - * @param int $instance_id Instance ID. - */ - public function set_instance_id( $instance_id ) { - $this->data['instance_id'] = absint( $instance_id ); - } - - /** - * Set rate label. - * - * @since 3.2.0 - * @param string $label Shipping rate label. - */ - public function set_label( $label ) { - $this->data['label'] = (string) $label; - } - - /** - * Set rate cost. - * - * @todo 4.0 Prevent negative value being set. #19293 - * @since 3.2.0 - * @param string $cost Shipping rate cost. - */ - public function set_cost( $cost ) { - $this->data['cost'] = $cost; - } - - /** - * Set rate taxes. - * - * @since 3.2.0 - * @param array $taxes List of taxes applied to shipping rate. - */ - public function set_taxes( $taxes ) { - $this->data['taxes'] = ! empty( $taxes ) && is_array( $taxes ) ? $taxes : array(); - } - - /** - * Set ID for the rate. This is usually a combination of the method and instance IDs. - * - * @since 3.2.0 - * @return string - */ - public function get_id() { - return apply_filters( 'woocommerce_shipping_rate_id', $this->data['id'], $this ); - } - - /** - * Set shipping method ID the rate belongs to. - * - * @since 3.2.0 - * @return string - */ - public function get_method_id() { - return apply_filters( 'woocommerce_shipping_rate_method_id', $this->data['method_id'], $this ); - } - - /** - * Set instance ID the rate belongs to. - * - * @since 3.2.0 - * @return int - */ - public function get_instance_id() { - return apply_filters( 'woocommerce_shipping_rate_instance_id', $this->data['instance_id'], $this ); - } - - /** - * Set rate label. - * - * @return string - */ - public function get_label() { - return apply_filters( 'woocommerce_shipping_rate_label', $this->data['label'], $this ); - } - - /** - * Set rate cost. - * - * @since 3.2.0 - * @return string - */ - public function get_cost() { - return apply_filters( 'woocommerce_shipping_rate_cost', $this->data['cost'], $this ); - } - - /** - * Set rate taxes. - * - * @since 3.2.0 - * @return array - */ - public function get_taxes() { - return apply_filters( 'woocommerce_shipping_rate_taxes', $this->data['taxes'], $this ); - } - - /** - * Get shipping tax. - * - * @return array - */ - public function get_shipping_tax() { - return apply_filters( 'woocommerce_get_shipping_tax', count( $this->taxes ) > 0 && ! WC()->customer->get_is_vat_exempt() ? array_sum( $this->taxes ) : 0, $this ); - } - - /** - * Add some meta data for this rate. - * - * @since 2.6.0 - * @param string $key Key. - * @param string $value Value. - */ - public function add_meta_data( $key, $value ) { - $this->meta_data[ wc_clean( $key ) ] = wc_clean( $value ); - } - - /** - * Get all meta data for this rate. - * - * @since 2.6.0 - * @return array - */ - public function get_meta_data() { - return $this->meta_data; - } -} diff --git a/includes/class-wc-shipping.php b/includes/class-wc-shipping.php deleted file mode 100644 index d9157983a44..00000000000 --- a/includes/class-wc-shipping.php +++ /dev/null @@ -1,407 +0,0 @@ -cart->get_shipping_total(); - } - if ( 'shipping_taxes' === $name ) { - return WC()->cart->get_shipping_taxes(); - } - } - - /** - * Initialize shipping. - */ - public function __construct() { - $this->enabled = wc_shipping_enabled(); - - if ( $this->enabled ) { - $this->init(); - } - } - - /** - * Initialize shipping. - */ - public function init() { - do_action( 'woocommerce_shipping_init' ); - } - - /** - * Shipping methods register themselves by returning their main class name through the woocommerce_shipping_methods filter. - * - * @return array - */ - public function get_shipping_method_class_names() { - // Unique Method ID => Method Class name. - $shipping_methods = array( - 'flat_rate' => 'WC_Shipping_Flat_Rate', - 'free_shipping' => 'WC_Shipping_Free_Shipping', - 'local_pickup' => 'WC_Shipping_Local_Pickup', - ); - - // For backwards compatibility with 2.5.x we load any ENABLED legacy shipping methods here. - $maybe_load_legacy_methods = array( 'flat_rate', 'free_shipping', 'international_delivery', 'local_delivery', 'local_pickup' ); - - foreach ( $maybe_load_legacy_methods as $method ) { - $options = get_option( 'woocommerce_' . $method . '_settings' ); - if ( $options && isset( $options['enabled'] ) && 'yes' === $options['enabled'] ) { - $shipping_methods[ 'legacy_' . $method ] = 'WC_Shipping_Legacy_' . $method; - } - } - - return apply_filters( 'woocommerce_shipping_methods', $shipping_methods ); - } - - /** - * Loads all shipping methods which are hooked in. - * If a $package is passed, some methods may add themselves conditionally and zones will be used. - * - * @param array $package Package information. - * @return WC_Shipping_Method[] - */ - public function load_shipping_methods( $package = array() ) { - if ( ! empty( $package ) ) { - $debug_mode = 'yes' === get_option( 'woocommerce_shipping_debug_mode', 'no' ); - $shipping_zone = WC_Shipping_Zones::get_zone_matching_package( $package ); - $this->shipping_methods = $shipping_zone->get_shipping_methods( true ); - - // translators: %s: shipping zone name. - $matched_zone_notice = sprintf( __( 'Customer matched zone "%s"', 'woocommerce' ), $shipping_zone->get_zone_name() ); - - // Debug output. - if ( $debug_mode && ! Constants::is_defined( 'WOOCOMMERCE_CHECKOUT' ) && ! Constants::is_defined( 'WC_DOING_AJAX' ) && ! wc_has_notice( $matched_zone_notice ) ) { - wc_add_notice( $matched_zone_notice ); - } - } else { - $this->shipping_methods = array(); - } - - // For the settings in the backend, and for non-shipping zone methods, we still need to load any registered classes here. - foreach ( $this->get_shipping_method_class_names() as $method_id => $method_class ) { - $this->register_shipping_method( $method_class ); - } - - // Methods can register themselves manually through this hook if necessary. - do_action( 'woocommerce_load_shipping_methods', $package ); - - // Return loaded methods. - return $this->get_shipping_methods(); - } - - /** - * Register a shipping method. - * - * @param object|string $method Either the name of the method's class, or an instance of the method's class. - * - * @return bool|void - */ - public function register_shipping_method( $method ) { - if ( ! is_object( $method ) ) { - if ( ! class_exists( $method ) ) { - return false; - } - $method = new $method(); - } - if ( is_null( $this->shipping_methods ) ) { - $this->shipping_methods = array(); - } - $this->shipping_methods[ $method->id ] = $method; - } - - /** - * Unregister shipping methods. - */ - public function unregister_shipping_methods() { - $this->shipping_methods = null; - } - - /** - * Returns all registered shipping methods for usage. - * - * @return WC_Shipping_Method[] - */ - public function get_shipping_methods() { - if ( is_null( $this->shipping_methods ) ) { - $this->load_shipping_methods(); - } - return $this->shipping_methods; - } - - /** - * Get an array of shipping classes. - * - * @return array - */ - public function get_shipping_classes() { - if ( empty( $this->shipping_classes ) ) { - $classes = get_terms( - 'product_shipping_class', - array( - 'hide_empty' => '0', - 'orderby' => 'name', - ) - ); - $this->shipping_classes = ! is_wp_error( $classes ) ? $classes : array(); - } - return apply_filters( 'woocommerce_get_shipping_classes', $this->shipping_classes ); - } - - /** - * Calculate shipping for (multiple) packages of cart items. - * - * @param array $packages multi-dimensional array of cart items to calc shipping for. - * @return array Array of calculated packages. - */ - public function calculate_shipping( $packages = array() ) { - $this->packages = array(); - - if ( ! $this->enabled || empty( $packages ) ) { - return array(); - } - - // Calculate costs for passed packages. - foreach ( $packages as $package_key => $package ) { - $this->packages[ $package_key ] = $this->calculate_shipping_for_package( $package, $package_key ); - } - - /** - * Allow packages to be reorganized after calculating the shipping. - * - * This filter can be used to apply some extra manipulation after the shipping costs are calculated for the packages - * but before WooCommerce does anything with them. A good example of usage is to merge the shipping methods for multiple - * packages for marketplaces. - * - * @since 2.6.0 - * - * @param array $packages The array of packages after shipping costs are calculated. - */ - $this->packages = array_filter( (array) apply_filters( 'woocommerce_shipping_packages', $this->packages ) ); - - return $this->packages; - } - - /** - * See if package is shippable. - * - * Packages are shippable until proven otherwise e.g. after getting a shipping country. - * - * @param array $package Package of cart items. - * @return bool - */ - public function is_package_shippable( $package ) { - // Packages are shippable until proven otherwise. - if ( empty( $package['destination']['country'] ) ) { - return true; - } - - $allowed = array_keys( WC()->countries->get_shipping_countries() ); - return in_array( $package['destination']['country'], $allowed, true ); - } - - /** - * Calculate shipping rates for a package, - * - * Calculates each shipping methods cost. Rates are stored in the session based on the package hash to avoid re-calculation every page load. - * - * @param array $package Package of cart items. - * @param int $package_key Index of the package being calculated. Used to cache multiple package rates. - * - * @return array|bool - */ - public function calculate_shipping_for_package( $package = array(), $package_key = 0 ) { - // If shipping is disabled or the package is invalid, return false. - if ( ! $this->enabled || empty( $package ) ) { - return false; - } - - $package['rates'] = array(); - - // If the package is not shippable, e.g. trying to ship to an invalid country, do not calculate rates. - if ( ! $this->is_package_shippable( $package ) ) { - return $package; - } - - // Check if we need to recalculate shipping for this package. - $package_to_hash = $package; - - // Remove data objects so hashes are consistent. - foreach ( $package_to_hash['contents'] as $item_id => $item ) { - unset( $package_to_hash['contents'][ $item_id ]['data'] ); - } - - // Get rates stored in the WC session data for this package. - $wc_session_key = 'shipping_for_package_' . $package_key; - $stored_rates = WC()->session->get( $wc_session_key ); - - // Calculate the hash for this package so we can tell if it's changed since last calculation. - $package_hash = 'wc_ship_' . md5( wp_json_encode( $package_to_hash ) . WC_Cache_Helper::get_transient_version( 'shipping' ) ); - - if ( ! is_array( $stored_rates ) || $package_hash !== $stored_rates['package_hash'] || 'yes' === get_option( 'woocommerce_shipping_debug_mode', 'no' ) ) { - foreach ( $this->load_shipping_methods( $package ) as $shipping_method ) { - if ( ! $shipping_method->supports( 'shipping-zones' ) || $shipping_method->get_instance_id() ) { - /** - * Fires before getting shipping rates for a package. - * - * @since 4.3.0 - * @param array $package Package of cart items. - * @param WC_Shipping_Method $shipping_method Shipping method instance. - */ - do_action( 'woocommerce_before_get_rates_for_package', $package, $shipping_method ); - - // Use + instead of array_merge to maintain numeric keys. - $package['rates'] = $package['rates'] + $shipping_method->get_rates_for_package( $package ); - - /** - * Fires after getting shipping rates for a package. - * - * @since 4.3.0 - * @param array $package Package of cart items. - * @param WC_Shipping_Method $shipping_method Shipping method instance. - */ - do_action( 'woocommerce_after_get_rates_for_package', $package, $shipping_method ); - } - } - - // Filter the calculated rates. - $package['rates'] = apply_filters( 'woocommerce_package_rates', $package['rates'], $package ); - - // Store in session to avoid recalculation. - WC()->session->set( - $wc_session_key, - array( - 'package_hash' => $package_hash, - 'rates' => $package['rates'], - ) - ); - } else { - $package['rates'] = $stored_rates['rates']; - } - - return $package; - } - - /** - * Get packages. - * - * @return array - */ - public function get_packages() { - return $this->packages; - } - - /** - * Reset shipping. - * - * Reset the totals for shipping as a whole. - */ - public function reset_shipping() { - unset( WC()->session->chosen_shipping_methods ); - $this->packages = array(); - } - - /** - * Deprecated - * - * @deprecated 2.6.0 Was previously used to determine sort order of methods, but this is now controlled by zones and thus unused. - */ - public function sort_shipping_methods() { - wc_deprecated_function( 'sort_shipping_methods', '2.6' ); - return $this->shipping_methods; - } -} diff --git a/includes/class-wc-structured-data.php b/includes/class-wc-structured-data.php deleted file mode 100644 index 070bea646b0..00000000000 --- a/includes/class-wc-structured-data.php +++ /dev/null @@ -1,538 +0,0 @@ -_data ) ) { - unset( $this->_data ); - } - - $this->_data[] = $data; - - return true; - } - - /** - * Gets data. - * - * @return array - */ - public function get_data() { - return $this->_data; - } - - /** - * Structures and returns data. - * - * List of types available by default for specific request: - * - * 'product', - * 'review', - * 'breadcrumblist', - * 'website', - * 'order', - * - * @param array $types Structured data types. - * @return array - */ - public function get_structured_data( $types ) { - $data = array(); - - // Put together the values of same type of structured data. - foreach ( $this->get_data() as $value ) { - $data[ strtolower( $value['@type'] ) ][] = $value; - } - - // Wrap the multiple values of each type inside a graph... Then add context to each type. - foreach ( $data as $type => $value ) { - $data[ $type ] = count( $value ) > 1 ? array( '@graph' => $value ) : $value[0]; - $data[ $type ] = apply_filters( 'woocommerce_structured_data_context', array( '@context' => 'https://schema.org/' ), $data, $type, $value ) + $data[ $type ]; - } - - // If requested types, pick them up... Finally change the associative array to an indexed one. - $data = $types ? array_values( array_intersect_key( $data, array_flip( $types ) ) ) : array_values( $data ); - - if ( ! empty( $data ) ) { - if ( 1 < count( $data ) ) { - $data = apply_filters( 'woocommerce_structured_data_context', array( '@context' => 'https://schema.org/' ), $data, '', '' ) + array( '@graph' => $data ); - } else { - $data = $data[0]; - } - } - - return $data; - } - - /** - * Get data types for pages. - * - * @return array - */ - protected function get_data_type_for_page() { - $types = array(); - $types[] = is_shop() || is_product_category() || is_product() ? 'product' : ''; - $types[] = is_shop() && is_front_page() ? 'website' : ''; - $types[] = is_product() ? 'review' : ''; - $types[] = 'breadcrumblist'; - $types[] = 'order'; - - return array_filter( apply_filters( 'woocommerce_structured_data_type_for_page', $types ) ); - } - - /** - * Makes sure email structured data only outputs on non-plain text versions. - * - * @param WP_Order $order Order data. - * @param bool $sent_to_admin Send to admin (default: false). - * @param bool $plain_text Plain text email (default: false). - */ - public function output_email_structured_data( $order, $sent_to_admin = false, $plain_text = false ) { - if ( $plain_text ) { - return; - } - echo '
    '; - $this->output_structured_data(); - echo '
    '; - } - - /** - * Sanitizes, encodes and outputs structured data. - * - * Hooked into `wp_footer` action hook. - * Hooked into `woocommerce_email_order_details` action hook. - */ - public function output_structured_data() { - $types = $this->get_data_type_for_page(); - $data = $this->get_structured_data( $types ); - - if ( $data ) { - echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } - } - - /* - |-------------------------------------------------------------------------- - | Generators - |-------------------------------------------------------------------------- - | - | Methods for generating specific structured data types: - | - | - Product - | - Review - | - BreadcrumbList - | - WebSite - | - Order - | - | The generated data is stored into `$this->_data`. - | See the methods above for handling `$this->_data`. - | - */ - - /** - * Generates Product structured data. - * - * Hooked into `woocommerce_single_product_summary` action hook. - * - * @param WC_Product $product Product data (default: null). - */ - public function generate_product_data( $product = null ) { - if ( ! is_object( $product ) ) { - global $product; - } - - if ( ! is_a( $product, 'WC_Product' ) ) { - return; - } - - $shop_name = get_bloginfo( 'name' ); - $shop_url = home_url(); - $currency = get_woocommerce_currency(); - $permalink = get_permalink( $product->get_id() ); - $image = wp_get_attachment_url( $product->get_image_id() ); - - $markup = array( - '@type' => 'Product', - '@id' => $permalink . '#product', // Append '#product' to differentiate between this @id and the @id generated for the Breadcrumblist. - 'name' => $product->get_name(), - 'url' => $permalink, - 'description' => wp_strip_all_tags( do_shortcode( $product->get_short_description() ? $product->get_short_description() : $product->get_description() ) ), - ); - - if ( $image ) { - $markup['image'] = $image; - } - - // Declare SKU or fallback to ID. - if ( $product->get_sku() ) { - $markup['sku'] = $product->get_sku(); - } else { - $markup['sku'] = $product->get_id(); - } - - if ( '' !== $product->get_price() ) { - // Assume prices will be valid until the end of next year, unless on sale and there is an end date. - $price_valid_until = gmdate( 'Y-12-31', time() + YEAR_IN_SECONDS ); - - if ( $product->is_type( 'variable' ) ) { - $lowest = $product->get_variation_price( 'min', false ); - $highest = $product->get_variation_price( 'max', false ); - - if ( $lowest === $highest ) { - $markup_offer = array( - '@type' => 'Offer', - 'price' => wc_format_decimal( $lowest, wc_get_price_decimals() ), - 'priceValidUntil' => $price_valid_until, - 'priceSpecification' => array( - 'price' => wc_format_decimal( $lowest, wc_get_price_decimals() ), - 'priceCurrency' => $currency, - 'valueAddedTaxIncluded' => wc_prices_include_tax() ? 'true' : 'false', - ), - ); - } else { - $markup_offer = array( - '@type' => 'AggregateOffer', - 'lowPrice' => wc_format_decimal( $lowest, wc_get_price_decimals() ), - 'highPrice' => wc_format_decimal( $highest, wc_get_price_decimals() ), - 'offerCount' => count( $product->get_children() ), - ); - } - } else { - if ( $product->is_on_sale() && $product->get_date_on_sale_to() ) { - $price_valid_until = gmdate( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ); - } - $markup_offer = array( - '@type' => 'Offer', - 'price' => wc_format_decimal( $product->get_price(), wc_get_price_decimals() ), - 'priceValidUntil' => $price_valid_until, - 'priceSpecification' => array( - 'price' => wc_format_decimal( $product->get_price(), wc_get_price_decimals() ), - 'priceCurrency' => $currency, - 'valueAddedTaxIncluded' => wc_prices_include_tax() ? 'true' : 'false', - ), - ); - } - - $markup_offer += array( - 'priceCurrency' => $currency, - 'availability' => 'http://schema.org/' . ( $product->is_in_stock() ? 'InStock' : 'OutOfStock' ), - 'url' => $permalink, - 'seller' => array( - '@type' => 'Organization', - 'name' => $shop_name, - 'url' => $shop_url, - ), - ); - - $markup['offers'] = array( apply_filters( 'woocommerce_structured_data_product_offer', $markup_offer, $product ) ); - } - - if ( $product->get_rating_count() && wc_review_ratings_enabled() ) { - $markup['aggregateRating'] = array( - '@type' => 'AggregateRating', - 'ratingValue' => $product->get_average_rating(), - 'reviewCount' => $product->get_review_count(), - ); - - // Markup 5 most recent rating/review. - $comments = get_comments( - array( - 'number' => 5, - 'post_id' => $product->get_id(), - 'status' => 'approve', - 'post_status' => 'publish', - 'post_type' => 'product', - 'parent' => 0, - 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - array( - 'key' => 'rating', - 'type' => 'NUMERIC', - 'compare' => '>', - 'value' => 0, - ), - ), - ) - ); - - if ( $comments ) { - $markup['review'] = array(); - foreach ( $comments as $comment ) { - $markup['review'][] = array( - '@type' => 'Review', - 'reviewRating' => array( - '@type' => 'Rating', - 'bestRating' => '5', - 'ratingValue' => get_comment_meta( $comment->comment_ID, 'rating', true ), - 'worstRating' => '1', - ), - 'author' => array( - '@type' => 'Person', - 'name' => get_comment_author( $comment ), - ), - 'reviewBody' => get_comment_text( $comment ), - 'datePublished' => get_comment_date( 'c', $comment ), - ); - } - } - } - - // Check we have required data. - if ( empty( $markup['aggregateRating'] ) && empty( $markup['offers'] ) && empty( $markup['review'] ) ) { - return; - } - - $this->set_data( apply_filters( 'woocommerce_structured_data_product', $markup, $product ) ); - } - - /** - * Generates Review structured data. - * - * Hooked into `woocommerce_review_meta` action hook. - * - * @param WP_Comment $comment Comment data. - */ - public function generate_review_data( $comment ) { - $markup = array(); - $markup['@type'] = 'Review'; - $markup['@id'] = get_comment_link( $comment->comment_ID ); - $markup['datePublished'] = get_comment_date( 'c', $comment->comment_ID ); - $markup['description'] = get_comment_text( $comment->comment_ID ); - $markup['itemReviewed'] = array( - '@type' => 'Product', - 'name' => get_the_title( $comment->comment_post_ID ), - ); - - // Skip replies unless they have a rating. - $rating = get_comment_meta( $comment->comment_ID, 'rating', true ); - - if ( $rating ) { - $markup['reviewRating'] = array( - '@type' => 'Rating', - 'bestRating' => '5', - 'ratingValue' => $rating, - 'worstRating' => '1', - ); - } elseif ( $comment->comment_parent ) { - return; - } - - $markup['author'] = array( - '@type' => 'Person', - 'name' => get_comment_author( $comment->comment_ID ), - ); - - $this->set_data( apply_filters( 'woocommerce_structured_data_review', $markup, $comment ) ); - } - - /** - * Generates BreadcrumbList structured data. - * - * Hooked into `woocommerce_breadcrumb` action hook. - * - * @param WC_Breadcrumb $breadcrumbs Breadcrumb data. - */ - public function generate_breadcrumblist_data( $breadcrumbs ) { - $crumbs = $breadcrumbs->get_breadcrumb(); - - if ( empty( $crumbs ) || ! is_array( $crumbs ) ) { - return; - } - - $markup = array(); - $markup['@type'] = 'BreadcrumbList'; - $markup['itemListElement'] = array(); - - foreach ( $crumbs as $key => $crumb ) { - $markup['itemListElement'][ $key ] = array( - '@type' => 'ListItem', - 'position' => $key + 1, - 'item' => array( - 'name' => $crumb[0], - ), - ); - - if ( ! empty( $crumb[1] ) ) { - $markup['itemListElement'][ $key ]['item'] += array( '@id' => $crumb[1] ); - } elseif ( isset( $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'] ) ) { - $current_url = set_url_scheme( 'http://' . wp_unslash( $_SERVER['HTTP_HOST'] ) . wp_unslash( $_SERVER['REQUEST_URI'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - - $markup['itemListElement'][ $key ]['item'] += array( '@id' => $current_url ); - } - } - - $this->set_data( apply_filters( 'woocommerce_structured_data_breadcrumblist', $markup, $breadcrumbs ) ); - } - - /** - * Generates WebSite structured data. - * - * Hooked into `woocommerce_before_main_content` action hook. - */ - public function generate_website_data() { - $markup = array(); - $markup['@type'] = 'WebSite'; - $markup['name'] = get_bloginfo( 'name' ); - $markup['url'] = home_url(); - $markup['potentialAction'] = array( - '@type' => 'SearchAction', - 'target' => home_url( '?s={search_term_string}&post_type=product' ), - 'query-input' => 'required name=search_term_string', - ); - - $this->set_data( apply_filters( 'woocommerce_structured_data_website', $markup ) ); - } - - /** - * Generates Order structured data. - * - * Hooked into `woocommerce_email_order_details` action hook. - * - * @param WP_Order $order Order data. - * @param bool $sent_to_admin Send to admin (default: false). - * @param bool $plain_text Plain text email (default: false). - */ - public function generate_order_data( $order, $sent_to_admin = false, $plain_text = false ) { - if ( $plain_text || ! is_a( $order, 'WC_Order' ) ) { - return; - } - - $shop_name = get_bloginfo( 'name' ); - $shop_url = home_url(); - $order_url = $sent_to_admin ? $order->get_edit_order_url() : $order->get_view_order_url(); - $order_statuses = array( - 'pending' => 'https://schema.org/OrderPaymentDue', - 'processing' => 'https://schema.org/OrderProcessing', - 'on-hold' => 'https://schema.org/OrderProblem', - 'completed' => 'https://schema.org/OrderDelivered', - 'cancelled' => 'https://schema.org/OrderCancelled', - 'refunded' => 'https://schema.org/OrderReturned', - 'failed' => 'https://schema.org/OrderProblem', - ); - - $markup_offers = array(); - foreach ( $order->get_items() as $item ) { - if ( ! apply_filters( 'woocommerce_order_item_visible', true, $item ) ) { - continue; - } - - $product = $item->get_product(); - $product_exists = is_object( $product ); - $is_visible = $product_exists && $product->is_visible(); - - $markup_offers[] = array( - '@type' => 'Offer', - 'price' => $order->get_line_subtotal( $item ), - 'priceCurrency' => $order->get_currency(), - 'priceSpecification' => array( - 'price' => $order->get_line_subtotal( $item ), - 'priceCurrency' => $order->get_currency(), - 'eligibleQuantity' => array( - '@type' => 'QuantitativeValue', - 'value' => apply_filters( 'woocommerce_email_order_item_quantity', $item->get_quantity(), $item ), - ), - ), - 'itemOffered' => array( - '@type' => 'Product', - 'name' => apply_filters( 'woocommerce_order_item_name', $item->get_name(), $item, $is_visible ), - 'sku' => $product_exists ? $product->get_sku() : '', - 'image' => $product_exists ? wp_get_attachment_image_url( $product->get_image_id() ) : '', - 'url' => $is_visible ? get_permalink( $product->get_id() ) : get_home_url(), - ), - 'seller' => array( - '@type' => 'Organization', - 'name' => $shop_name, - 'url' => $shop_url, - ), - ); - } - - $markup = array(); - $markup['@type'] = 'Order'; - $markup['url'] = $order_url; - $markup['orderStatus'] = isset( $order_statuses[ $order->get_status() ] ) ? $order_statuses[ $order->get_status() ] : ''; - $markup['orderNumber'] = $order->get_order_number(); - $markup['orderDate'] = $order->get_date_created()->format( 'c' ); - $markup['acceptedOffer'] = $markup_offers; - $markup['discount'] = $order->get_total_discount(); - $markup['discountCurrency'] = $order->get_currency(); - $markup['price'] = $order->get_total(); - $markup['priceCurrency'] = $order->get_currency(); - $markup['priceSpecification'] = array( - 'price' => $order->get_total(), - 'priceCurrency' => $order->get_currency(), - 'valueAddedTaxIncluded' => 'true', - ); - $markup['billingAddress'] = array( - '@type' => 'PostalAddress', - 'name' => $order->get_formatted_billing_full_name(), - 'streetAddress' => $order->get_billing_address_1(), - 'postalCode' => $order->get_billing_postcode(), - 'addressLocality' => $order->get_billing_city(), - 'addressRegion' => $order->get_billing_state(), - 'addressCountry' => $order->get_billing_country(), - 'email' => $order->get_billing_email(), - 'telephone' => $order->get_billing_phone(), - ); - $markup['customer'] = array( - '@type' => 'Person', - 'name' => $order->get_formatted_billing_full_name(), - ); - $markup['merchant'] = array( - '@type' => 'Organization', - 'name' => $shop_name, - 'url' => $shop_url, - ); - $markup['potentialAction'] = array( - '@type' => 'ViewAction', - 'name' => 'View Order', - 'url' => $order_url, - 'target' => $order_url, - ); - - $this->set_data( apply_filters( 'woocommerce_structured_data_order', $markup, $sent_to_admin, $order ), true ); - } -} diff --git a/includes/class-wc-tax.php b/includes/class-wc-tax.php deleted file mode 100644 index 65a4978cc2b..00000000000 --- a/includes/class-wc-tax.php +++ /dev/null @@ -1,1242 +0,0 @@ - $rate ) { - $taxes[ $key ] = 0; - - if ( 'yes' === $rate['compound'] ) { - $compound_rates[ $key ] = $rate['rate']; - } else { - $regular_rates[ $key ] = $rate['rate']; - } - } - - $compound_rates = array_reverse( $compound_rates, true ); // Working backwards. - - $non_compound_price = $price; - - foreach ( $compound_rates as $key => $compound_rate ) { - $tax_amount = apply_filters( 'woocommerce_price_inc_tax_amount', $non_compound_price - ( $non_compound_price / ( 1 + ( $compound_rate / 100 ) ) ), $key, $rates[ $key ], $price ); - $taxes[ $key ] += $tax_amount; - $non_compound_price = $non_compound_price - $tax_amount; - } - - // Regular taxes. - $regular_tax_rate = 1 + ( array_sum( $regular_rates ) / 100 ); - - foreach ( $regular_rates as $key => $regular_rate ) { - $the_rate = ( $regular_rate / 100 ) / $regular_tax_rate; - $net_price = $price - ( $the_rate * $non_compound_price ); - $tax_amount = apply_filters( 'woocommerce_price_inc_tax_amount', $price - $net_price, $key, $rates[ $key ], $price ); - $taxes[ $key ] += $tax_amount; - } - - /** - * Round all taxes to precision (4DP) before passing them back. Note, this is not the same rounding - * as in the cart calculation class which, depending on settings, will round to 2DP when calculating - * final totals. Also unlike that class, this rounds .5 up for all cases. - */ - $taxes = array_map( array( __CLASS__, 'round' ), $taxes ); - - return $taxes; - } - - /** - * Calc tax from exclusive price. - * - * @param float $price Price to calculate tax for. - * @param array $rates Array of tax rates. - * @return array - */ - public static function calc_exclusive_tax( $price, $rates ) { - $taxes = array(); - - if ( ! empty( $rates ) ) { - foreach ( $rates as $key => $rate ) { - if ( 'yes' === $rate['compound'] ) { - continue; - } - - $tax_amount = $price * ( $rate['rate'] / 100 ); - $tax_amount = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price ); // ADVANCED: Allow third parties to modify this rate. - - if ( ! isset( $taxes[ $key ] ) ) { - $taxes[ $key ] = $tax_amount; - } else { - $taxes[ $key ] += $tax_amount; - } - } - - $pre_compound_total = array_sum( $taxes ); - - // Compound taxes. - foreach ( $rates as $key => $rate ) { - if ( 'no' === $rate['compound'] ) { - continue; - } - $the_price_inc_tax = $price + ( $pre_compound_total ); - $tax_amount = $the_price_inc_tax * ( $rate['rate'] / 100 ); - $tax_amount = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price, $the_price_inc_tax, $pre_compound_total ); // ADVANCED: Allow third parties to modify this rate. - - if ( ! isset( $taxes[ $key ] ) ) { - $taxes[ $key ] = $tax_amount; - } else { - $taxes[ $key ] += $tax_amount; - } - - $pre_compound_total = array_sum( $taxes ); - } - } - - /** - * Round all taxes to precision (4DP) before passing them back. Note, this is not the same rounding - * as in the cart calculation class which, depending on settings, will round to 2DP when calculating - * final totals. Also unlike that class, this rounds .5 up for all cases. - */ - $taxes = array_map( array( __CLASS__, 'round' ), $taxes ); - - return $taxes; - } - - /** - * Searches for all matching country/state/postcode tax rates. - * - * @param array $args Args that determine the rate to find. - * @return array - */ - public static function find_rates( $args = array() ) { - $args = wp_parse_args( - $args, - array( - 'country' => '', - 'state' => '', - 'city' => '', - 'postcode' => '', - 'tax_class' => '', - ) - ); - - $country = $args['country']; - $state = $args['state']; - $city = $args['city']; - $postcode = wc_normalize_postcode( wc_clean( $args['postcode'] ) ); - $tax_class = $args['tax_class']; - - if ( ! $country ) { - return array(); - } - - $cache_key = WC_Cache_Helper::get_cache_prefix( 'taxes' ) . 'wc_tax_rates_' . md5( sprintf( '%s+%s+%s+%s+%s', $country, $state, $city, $postcode, $tax_class ) ); - $matched_tax_rates = wp_cache_get( $cache_key, 'taxes' ); - - if ( false === $matched_tax_rates ) { - $matched_tax_rates = self::get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class ); - wp_cache_set( $cache_key, $matched_tax_rates, 'taxes' ); - } - - return apply_filters( 'woocommerce_find_rates', $matched_tax_rates, $args ); - } - - /** - * Searches for all matching country/state/postcode tax rates. - * - * @param array $args Args that determine the rate to find. - * @return array - */ - public static function find_shipping_rates( $args = array() ) { - $rates = self::find_rates( $args ); - $shipping_rates = array(); - - if ( is_array( $rates ) ) { - foreach ( $rates as $key => $rate ) { - if ( 'yes' === $rate['shipping'] ) { - $shipping_rates[ $key ] = $rate; - } - } - } - - return $shipping_rates; - } - - /** - * Does the sort comparison. Compares (in this order): - * - Priority - * - Country - * - State - * - Number of postcodes - * - Number of cities - * - ID - * - * @param object $rate1 First rate to compare. - * @param object $rate2 Second rate to compare. - * @return int - */ - private static function sort_rates_callback( $rate1, $rate2 ) { - if ( $rate1->tax_rate_priority !== $rate2->tax_rate_priority ) { - return $rate1->tax_rate_priority < $rate2->tax_rate_priority ? -1 : 1; // ASC. - } - - if ( $rate1->tax_rate_country !== $rate2->tax_rate_country ) { - if ( '' === $rate1->tax_rate_country ) { - return 1; - } - if ( '' === $rate2->tax_rate_country ) { - return -1; - } - return strcmp( $rate1->tax_rate_country, $rate2->tax_rate_country ) > 0 ? 1 : -1; - } - - if ( $rate1->tax_rate_state !== $rate2->tax_rate_state ) { - if ( '' === $rate1->tax_rate_state ) { - return 1; - } - if ( '' === $rate2->tax_rate_state ) { - return -1; - } - return strcmp( $rate1->tax_rate_state, $rate2->tax_rate_state ) > 0 ? 1 : -1; - } - - if ( isset( $rate1->postcode_count, $rate2->postcode_count ) && $rate1->postcode_count !== $rate2->postcode_count ) { - return $rate1->postcode_count < $rate2->postcode_count ? 1 : -1; - } - - if ( isset( $rate1->city_count, $rate2->city_count ) && $rate1->city_count !== $rate2->city_count ) { - return $rate1->city_count < $rate2->city_count ? 1 : -1; - } - - return $rate1->tax_rate_id < $rate2->tax_rate_id ? -1 : 1; - } - - /** - * Logical sort order for tax rates based on the following in order of priority. - * - * @param array $rates Rates to be sorted. - * @return array - */ - private static function sort_rates( $rates ) { - uasort( $rates, __CLASS__ . '::sort_rates_callback' ); - $i = 0; - foreach ( $rates as $key => $rate ) { - $rates[ $key ]->tax_rate_order = $i++; - } - return $rates; - } - - /** - * Loop through a set of tax rates and get the matching rates (1 per priority). - * - * @param string $country Country code to match against. - * @param string $state State code to match against. - * @param string $postcode Postcode to match against. - * @param string $city City to match against. - * @param string $tax_class Tax class to match against. - * @return array - */ - private static function get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class ) { - global $wpdb; - - // Query criteria - these will be ANDed. - $criteria = array(); - $criteria[] = $wpdb->prepare( "tax_rate_country IN ( %s, '' )", strtoupper( $country ) ); - $criteria[] = $wpdb->prepare( "tax_rate_state IN ( %s, '' )", strtoupper( $state ) ); - $criteria[] = $wpdb->prepare( 'tax_rate_class = %s', sanitize_title( $tax_class ) ); - - // Pre-query postcode ranges for PHP based matching. - $postcode_search = wc_get_wildcard_postcodes( $postcode, $country ); - $postcode_ranges = $wpdb->get_results( "SELECT tax_rate_id, location_code FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE location_type = 'postcode' AND location_code LIKE '%...%';" ); - - if ( $postcode_ranges ) { - $matches = wc_postcode_location_matcher( $postcode, $postcode_ranges, 'tax_rate_id', 'location_code', $country ); - if ( ! empty( $matches ) ) { - foreach ( $matches as $matched_postcodes ) { - $postcode_search = array_merge( $postcode_search, $matched_postcodes ); - } - } - } - - $postcode_search = array_unique( $postcode_search ); - - /** - * Location matching criteria - ORed - * Needs to match: - * - rates with no postcodes and cities - * - rates with a matching postcode and city - * - rates with matching postcode, no city - * - rates with matching city, no postcode - */ - $locations_criteria = array(); - $locations_criteria[] = 'locations.location_type IS NULL'; - $locations_criteria[] = " - locations.location_type = 'postcode' AND locations.location_code IN ('" . implode( "','", array_map( 'esc_sql', $postcode_search ) ) . "') - AND ( - ( locations2.location_type = 'city' AND locations2.location_code = '" . esc_sql( strtoupper( $city ) ) . "' ) - OR NOT EXISTS ( - SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub - WHERE sub.location_type = 'city' - AND sub.tax_rate_id = tax_rates.tax_rate_id - ) - ) - "; - $locations_criteria[] = " - locations.location_type = 'city' AND locations.location_code = '" . esc_sql( strtoupper( $city ) ) . "' - AND NOT EXISTS ( - SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub - WHERE sub.location_type = 'postcode' - AND sub.tax_rate_id = tax_rates.tax_rate_id - ) - "; - - $criteria[] = '( ( ' . implode( ' ) OR ( ', $locations_criteria ) . ' ) )'; - - $criteria_string = implode( ' AND ', $criteria ); - - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $found_rates = $wpdb->get_results( - " - SELECT tax_rates.*, COUNT( locations.location_id ) as postcode_count, COUNT( locations2.location_id ) as city_count - FROM {$wpdb->prefix}woocommerce_tax_rates as tax_rates - LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations ON tax_rates.tax_rate_id = locations.tax_rate_id - LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations2 ON tax_rates.tax_rate_id = locations2.tax_rate_id - WHERE 1=1 AND {$criteria_string} - GROUP BY tax_rates.tax_rate_id - ORDER BY tax_rates.tax_rate_priority - " - ); - // phpcs:enable - - $found_rates = self::sort_rates( $found_rates ); - $matched_tax_rates = array(); - $found_priority = array(); - - foreach ( $found_rates as $found_rate ) { - if ( in_array( $found_rate->tax_rate_priority, $found_priority, true ) ) { - continue; - } - - $matched_tax_rates[ $found_rate->tax_rate_id ] = array( - 'rate' => (float) $found_rate->tax_rate, - 'label' => $found_rate->tax_rate_name, - 'shipping' => $found_rate->tax_rate_shipping ? 'yes' : 'no', - 'compound' => $found_rate->tax_rate_compound ? 'yes' : 'no', - ); - - $found_priority[] = $found_rate->tax_rate_priority; - } - - return apply_filters( 'woocommerce_matched_tax_rates', $matched_tax_rates, $country, $state, $postcode, $city, $tax_class ); - } - - /** - * Get the customer tax location based on their status and the current page. - * - * Used by get_rates(), get_shipping_rates(). - * - * @param string $tax_class string Optional, passed to the filter for advanced tax setups. - * @param object $customer Override the customer object to get their location. - * @return array - */ - public static function get_tax_location( $tax_class = '', $customer = null ) { - $location = array(); - - if ( is_null( $customer ) && WC()->customer ) { - $customer = WC()->customer; - } - - if ( ! empty( $customer ) ) { - $location = $customer->get_taxable_address(); - } elseif ( wc_prices_include_tax() || 'base' === get_option( 'woocommerce_default_customer_address' ) || 'base' === get_option( 'woocommerce_tax_based_on' ) ) { - $location = array( - WC()->countries->get_base_country(), - WC()->countries->get_base_state(), - WC()->countries->get_base_postcode(), - WC()->countries->get_base_city(), - ); - } - - return apply_filters( 'woocommerce_get_tax_location', $location, $tax_class, $customer ); - } - - /** - * Get's an array of matching rates for a tax class. - * - * @param string $tax_class Tax class to get rates for. - * @param object $customer Override the customer object to get their location. - * @return array - */ - public static function get_rates( $tax_class = '', $customer = null ) { - $tax_class = sanitize_title( $tax_class ); - $location = self::get_tax_location( $tax_class, $customer ); - return self::get_rates_from_location( $tax_class, $location, $customer ); - } - - /** - * Get's an arrau of matching rates from location and tax class. $customer parameter is used to preserve backward compatibility for filter. - * - * @param string $tax_class Tax class to get rates for. - * @param array $location Location to compute rates for. Should be in form: array( country, state, postcode, city). - * @param object $customer Only used to maintain backward compatibility for filter `woocommerce-matched_rates`. - * - * @return mixed|void Tax rates. - */ - public static function get_rates_from_location( $tax_class, $location, $customer = null ) { - $tax_class = sanitize_title( $tax_class ); - $matched_tax_rates = array(); - - if ( count( $location ) === 4 ) { - list( $country, $state, $postcode, $city ) = $location; - - $matched_tax_rates = self::find_rates( - array( - 'country' => $country, - 'state' => $state, - 'postcode' => $postcode, - 'city' => $city, - 'tax_class' => $tax_class, - ) - ); - } - - return apply_filters( 'woocommerce_matched_rates', $matched_tax_rates, $tax_class, $customer ); - } - - /** - * Get's an array of matching rates for the shop's base country. - * - * @param string $tax_class Tax Class. - * @return array - */ - public static function get_base_tax_rates( $tax_class = '' ) { - return apply_filters( - 'woocommerce_base_tax_rates', - self::find_rates( - array( - 'country' => WC()->countries->get_base_country(), - 'state' => WC()->countries->get_base_state(), - 'postcode' => WC()->countries->get_base_postcode(), - 'city' => WC()->countries->get_base_city(), - 'tax_class' => $tax_class, - ) - ), - $tax_class - ); - } - - /** - * Alias for get_base_tax_rates(). - * - * @deprecated 2.3 - * @param string $tax_class Tax Class. - * @return array - */ - public static function get_shop_base_rate( $tax_class = '' ) { - return self::get_base_tax_rates( $tax_class ); - } - - /** - * Gets an array of matching shipping tax rates for a given class. - * - * @param string $tax_class Tax class to get rates for. - * @param object $customer Override the customer object to get their location. - * @return mixed - */ - public static function get_shipping_tax_rates( $tax_class = null, $customer = null ) { - // See if we have an explicitly set shipping tax class. - $shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' ); - - if ( 'inherit' !== $shipping_tax_class ) { - $tax_class = $shipping_tax_class; - } - - $location = self::get_tax_location( $tax_class, $customer ); - $matched_tax_rates = array(); - - if ( 4 === count( $location ) ) { - list( $country, $state, $postcode, $city ) = $location; - - if ( ! is_null( $tax_class ) ) { - // This will be per item shipping. - $matched_tax_rates = self::find_shipping_rates( - array( - 'country' => $country, - 'state' => $state, - 'postcode' => $postcode, - 'city' => $city, - 'tax_class' => $tax_class, - ) - ); - - } elseif ( WC()->cart->get_cart() ) { - - // This will be per order shipping - loop through the order and find the highest tax class rate. - $cart_tax_classes = WC()->cart->get_cart_item_tax_classes_for_shipping(); - - // No tax classes = no taxable items. - if ( empty( $cart_tax_classes ) ) { - return array(); - } - - // If multiple classes are found, use the first one found unless a standard rate item is found. This will be the first listed in the 'additional tax class' section. - if ( count( $cart_tax_classes ) > 1 && ! in_array( '', $cart_tax_classes, true ) ) { - $tax_classes = self::get_tax_class_slugs(); - - foreach ( $tax_classes as $tax_class ) { - if ( in_array( $tax_class, $cart_tax_classes, true ) ) { - $matched_tax_rates = self::find_shipping_rates( - array( - 'country' => $country, - 'state' => $state, - 'postcode' => $postcode, - 'city' => $city, - 'tax_class' => $tax_class, - ) - ); - break; - } - } - } elseif ( 1 === count( $cart_tax_classes ) ) { - // If a single tax class is found, use it. - $matched_tax_rates = self::find_shipping_rates( - array( - 'country' => $country, - 'state' => $state, - 'postcode' => $postcode, - 'city' => $city, - 'tax_class' => $cart_tax_classes[0], - ) - ); - } - } - - // Get standard rate if no taxes were found. - if ( ! count( $matched_tax_rates ) ) { - $matched_tax_rates = self::find_shipping_rates( - array( - 'country' => $country, - 'state' => $state, - 'postcode' => $postcode, - 'city' => $city, - ) - ); - } - } - - return $matched_tax_rates; - } - - /** - * Return true/false depending on if a rate is a compound rate. - * - * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format. - * @return bool - */ - public static function is_compound( $key_or_rate ) { - global $wpdb; - - if ( is_object( $key_or_rate ) ) { - $key = $key_or_rate->tax_rate_id; - $compound = $key_or_rate->tax_rate_compound; - } else { - $key = $key_or_rate; - $compound = (bool) $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_compound FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); - } - - return (bool) apply_filters( 'woocommerce_rate_compound', $compound, $key ); - } - - /** - * Return a given rates label. - * - * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format. - * @return string - */ - public static function get_rate_label( $key_or_rate ) { - global $wpdb; - - if ( is_object( $key_or_rate ) ) { - $key = $key_or_rate->tax_rate_id; - $rate_name = $key_or_rate->tax_rate_name; - } else { - $key = $key_or_rate; - $rate_name = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_name FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); - } - - if ( ! $rate_name ) { - $rate_name = WC()->countries->tax_or_vat(); - } - - return apply_filters( 'woocommerce_rate_label', $rate_name, $key ); - } - - /** - * Return a given rates percent. - * - * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format. - * @return string - */ - public static function get_rate_percent( $key_or_rate ) { - $rate_percent_value = self::get_rate_percent_value( $key_or_rate ); - $tax_rate_id = is_object( $key_or_rate ) ? $key_or_rate->tax_rate_id : $key_or_rate; - return apply_filters( 'woocommerce_rate_percent', $rate_percent_value . '%', $tax_rate_id ); - } - - /** - * Return a given rates percent. - * - * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format. - * @return float - */ - public static function get_rate_percent_value( $key_or_rate ) { - global $wpdb; - - if ( is_object( $key_or_rate ) ) { - $tax_rate = $key_or_rate->tax_rate; - } else { - $key = $key_or_rate; - $tax_rate = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); - } - - return floatval( $tax_rate ); - } - - - /** - * Get a rates code. Code is made up of COUNTRY-STATE-NAME-Priority. E.g GB-VAT-1, US-AL-TAX-1. - * - * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format. - * @return string - */ - public static function get_rate_code( $key_or_rate ) { - global $wpdb; - - if ( is_object( $key_or_rate ) ) { - $key = $key_or_rate->tax_rate_id; - $rate = $key_or_rate; - } else { - $key = $key_or_rate; - $rate = $wpdb->get_row( $wpdb->prepare( "SELECT tax_rate_country, tax_rate_state, tax_rate_name, tax_rate_priority FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); - } - - $code_string = ''; - - if ( null !== $rate ) { - $code = array(); - $code[] = $rate->tax_rate_country; - $code[] = $rate->tax_rate_state; - $code[] = $rate->tax_rate_name ? $rate->tax_rate_name : 'TAX'; - $code[] = absint( $rate->tax_rate_priority ); - $code_string = strtoupper( implode( '-', array_filter( $code ) ) ); - } - - return apply_filters( 'woocommerce_rate_code', $code_string, $key ); - } - - /** - * Sums a set of taxes to form a single total. Values are pre-rounded to precision from 3.6.0. - * - * @param array $taxes Array of taxes. - * @return float - */ - public static function get_tax_total( $taxes ) { - return array_sum( $taxes ); - } - - /** - * Gets all tax rate classes from the database. - * - * @since 3.7.0 - * @return array Array of tax class objects consisting of tax_rate_class_id, name, and slug. - */ - public static function get_tax_rate_classes() { - global $wpdb; - - $cache_key = 'tax-rate-classes'; - $tax_rate_classes = wp_cache_get( $cache_key, 'taxes' ); - - if ( ! is_array( $tax_rate_classes ) ) { - $tax_rate_classes = $wpdb->get_results( - " - SELECT * FROM {$wpdb->wc_tax_rate_classes} ORDER BY name; - " - ); - wp_cache_set( $cache_key, $tax_rate_classes, 'taxes' ); - } - - return $tax_rate_classes; - } - - /** - * Get store tax class names. - * - * @return array Array of class names ("Reduced rate", "Zero rate", etc). - */ - public static function get_tax_classes() { - return wp_list_pluck( self::get_tax_rate_classes(), 'name' ); - } - - /** - * Get store tax classes as slugs. - * - * @since 3.0.0 - * @return array Array of class slugs ("reduced-rate", "zero-rate", etc). - */ - public static function get_tax_class_slugs() { - return wp_list_pluck( self::get_tax_rate_classes(), 'slug' ); - } - - /** - * Create a new tax class. - * - * @since 3.7.0 - * @param string $name Name of the tax class to add. - * @param string $slug (optional) Slug of the tax class to add. Defaults to sanitized name. - * @return WP_Error|array Returns name and slug (array) if the tax class is created, or WP_Error if something went wrong. - */ - public static function create_tax_class( $name, $slug = '' ) { - global $wpdb; - - if ( empty( $name ) ) { - return new WP_Error( 'tax_class_invalid_name', __( 'Tax class requires a valid name', 'woocommerce' ) ); - } - - $existing = self::get_tax_classes(); - $existing_slugs = self::get_tax_class_slugs(); - - if ( in_array( $name, $existing, true ) ) { - return new WP_Error( 'tax_class_exists', __( 'Tax class already exists', 'woocommerce' ) ); - } - - if ( ! $slug ) { - $slug = sanitize_title( $name ); - } - - if ( in_array( $slug, $existing_slugs, true ) ) { - return new WP_Error( 'tax_class_slug_exists', __( 'Tax class slug already exists', 'woocommerce' ) ); - } - - $insert = $wpdb->insert( - $wpdb->wc_tax_rate_classes, - array( - 'name' => $name, - 'slug' => $slug, - ) - ); - - if ( is_wp_error( $insert ) ) { - return new WP_Error( 'tax_class_insert_error', $insert->get_error_message() ); - } - - wp_cache_delete( 'tax-rate-classes', 'taxes' ); - - return array( - 'name' => $name, - 'slug' => $slug, - ); - } - - /** - * Get an existing tax class. - * - * @since 3.7.0 - * @param string $field Field to get by. Valid values are id, name, or slug. - * @param string|int $item Item to get. - * @return array|bool Returns the tax class as an array. False if not found. - */ - public static function get_tax_class_by( $field, $item ) { - if ( ! in_array( $field, array( 'id', 'name', 'slug' ), true ) ) { - return new WP_Error( 'invalid_field', __( 'Invalid field', 'woocommerce' ) ); - } - - if ( 'id' === $field ) { - $field = 'tax_rate_class_id'; - } - - $matches = wp_list_filter( - self::get_tax_rate_classes(), - array( - $field => $item, - ) - ); - - if ( ! $matches ) { - return false; - } - - $tax_class = current( $matches ); - - return array( - 'name' => $tax_class->name, - 'slug' => $tax_class->slug, - ); - } - - /** - * Delete an existing tax class. - * - * @since 3.7.0 - * @param string $field Field to delete by. Valid values are id, name, or slug. - * @param string|int $item Item to delete. - * @return WP_Error|bool Returns true if deleted successfully, false if nothing was deleted, or WP_Error if there is an invalid request. - */ - public static function delete_tax_class_by( $field, $item ) { - global $wpdb; - - if ( ! in_array( $field, array( 'id', 'name', 'slug' ), true ) ) { - return new WP_Error( 'invalid_field', __( 'Invalid field', 'woocommerce' ) ); - } - - $tax_class = self::get_tax_class_by( $field, $item ); - - if ( ! $tax_class ) { - return new WP_Error( 'invalid_tax_class', __( 'Invalid tax class', 'woocommerce' ) ); - } - - if ( 'id' === $field ) { - $field = 'tax_rate_class_id'; - } - - $delete = $wpdb->delete( - $wpdb->wc_tax_rate_classes, - array( - $field => $item, - ) - ); - - if ( $delete ) { - // Delete associated tax rates. - $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_class = %s;", $tax_class['slug'] ) ); - $wpdb->query( "DELETE locations FROM {$wpdb->prefix}woocommerce_tax_rate_locations locations LEFT JOIN {$wpdb->prefix}woocommerce_tax_rates rates ON rates.tax_rate_id = locations.tax_rate_id WHERE rates.tax_rate_id IS NULL;" ); - } - - wp_cache_delete( 'tax-rate-classes', 'taxes' ); - WC_Cache_Helper::invalidate_cache_group( 'taxes' ); - - return (bool) $delete; - } - - /** - * Format the city. - * - * @param string $city Value to format. - * @return string - */ - private static function format_tax_rate_city( $city ) { - return strtoupper( trim( $city ) ); - } - - /** - * Format the state. - * - * @param string $state Value to format. - * @return string - */ - private static function format_tax_rate_state( $state ) { - $state = strtoupper( $state ); - return ( '*' === $state ) ? '' : $state; - } - - /** - * Format the country. - * - * @param string $country Value to format. - * @return string - */ - private static function format_tax_rate_country( $country ) { - $country = strtoupper( $country ); - return ( '*' === $country ) ? '' : $country; - } - - /** - * Format the tax rate name. - * - * @param string $name Value to format. - * @return string - */ - private static function format_tax_rate_name( $name ) { - return $name ? $name : __( 'Tax', 'woocommerce' ); - } - - /** - * Format the rate. - * - * @param float $rate Value to format. - * @return string - */ - private static function format_tax_rate( $rate ) { - return number_format( (float) $rate, 4, '.', '' ); - } - - /** - * Format the priority. - * - * @param string $priority Value to format. - * @return int - */ - private static function format_tax_rate_priority( $priority ) { - return absint( $priority ); - } - - /** - * Format the class. - * - * @param string $class Value to format. - * @return string - */ - public static function format_tax_rate_class( $class ) { - $class = sanitize_title( $class ); - $classes = self::get_tax_class_slugs(); - if ( ! in_array( $class, $classes, true ) ) { - $class = ''; - } - return ( 'standard' === $class ) ? '' : $class; - } - - /** - * Prepare and format tax rate for DB insertion. - * - * @param array $tax_rate Tax rate to format. - * @return array - */ - private static function prepare_tax_rate( $tax_rate ) { - foreach ( $tax_rate as $key => $value ) { - if ( method_exists( __CLASS__, 'format_' . $key ) ) { - if ( 'tax_rate_state' === $key ) { - $tax_rate[ $key ] = call_user_func( array( __CLASS__, 'format_' . $key ), sanitize_key( $value ) ); - } else { - $tax_rate[ $key ] = call_user_func( array( __CLASS__, 'format_' . $key ), $value ); - } - } - } - return $tax_rate; - } - - /** - * Insert a new tax rate. - * - * Internal use only. - * - * @since 2.3.0 - * - * @param array $tax_rate Tax rate to insert. - * @return int tax rate id - */ - public static function _insert_tax_rate( $tax_rate ) { - global $wpdb; - - $wpdb->insert( $wpdb->prefix . 'woocommerce_tax_rates', self::prepare_tax_rate( $tax_rate ) ); - - $tax_rate_id = $wpdb->insert_id; - - WC_Cache_Helper::invalidate_cache_group( 'taxes' ); - - do_action( 'woocommerce_tax_rate_added', $tax_rate_id, $tax_rate ); - - return $tax_rate_id; - } - - /** - * Get tax rate. - * - * Internal use only. - * - * @since 2.5.0 - * - * @param int $tax_rate_id Tax rate ID. - * @param string $output_type Type of output. - * @return array|object - */ - public static function _get_tax_rate( $tax_rate_id, $output_type = ARRAY_A ) { - global $wpdb; - - return $wpdb->get_row( - $wpdb->prepare( - " - SELECT * - FROM {$wpdb->prefix}woocommerce_tax_rates - WHERE tax_rate_id = %d - ", - $tax_rate_id - ), - $output_type - ); - } - - /** - * Update a tax rate. - * - * Internal use only. - * - * @since 2.3.0 - * - * @param int $tax_rate_id Tax rate to update. - * @param array $tax_rate Tax rate values. - */ - public static function _update_tax_rate( $tax_rate_id, $tax_rate ) { - global $wpdb; - - $tax_rate_id = absint( $tax_rate_id ); - - $wpdb->update( - $wpdb->prefix . 'woocommerce_tax_rates', - self::prepare_tax_rate( $tax_rate ), - array( - 'tax_rate_id' => $tax_rate_id, - ) - ); - - WC_Cache_Helper::invalidate_cache_group( 'taxes' ); - - do_action( 'woocommerce_tax_rate_updated', $tax_rate_id, $tax_rate ); - } - - /** - * Delete a tax rate from the database. - * - * Internal use only. - * - * @since 2.3.0 - * @param int $tax_rate_id Tax rate to delete. - */ - public static function _delete_tax_rate( $tax_rate_id ) { - global $wpdb; - - $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d;", $tax_rate_id ) ); - $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d;", $tax_rate_id ) ); - - WC_Cache_Helper::invalidate_cache_group( 'taxes' ); - - do_action( 'woocommerce_tax_rate_deleted', $tax_rate_id ); - } - - /** - * Update postcodes for a tax rate in the DB. - * - * Internal use only. - * - * @since 2.3.0 - * - * @param int $tax_rate_id Tax rate to update. - * @param string $postcodes String of postcodes separated by ; characters. - */ - public static function _update_tax_rate_postcodes( $tax_rate_id, $postcodes ) { - if ( ! is_array( $postcodes ) ) { - $postcodes = explode( ';', $postcodes ); - } - // No normalization - postcodes are matched against both normal and formatted versions to support wildcards. - foreach ( $postcodes as $key => $postcode ) { - $postcodes[ $key ] = strtoupper( trim( str_replace( chr( 226 ) . chr( 128 ) . chr( 166 ), '...', $postcode ) ) ); - } - self::update_tax_rate_locations( $tax_rate_id, array_diff( array_filter( $postcodes ), array( '*' ) ), 'postcode' ); - } - - /** - * Update cities for a tax rate in the DB. - * - * Internal use only. - * - * @since 2.3.0 - * - * @param int $tax_rate_id Tax rate to update. - * @param string $cities Cities to set. - */ - public static function _update_tax_rate_cities( $tax_rate_id, $cities ) { - if ( ! is_array( $cities ) ) { - $cities = explode( ';', $cities ); - } - $cities = array_filter( array_diff( array_map( array( __CLASS__, 'format_tax_rate_city' ), $cities ), array( '*' ) ) ); - - self::update_tax_rate_locations( $tax_rate_id, $cities, 'city' ); - } - - /** - * Updates locations (postcode and city). - * - * Internal use only. - * - * @since 2.3.0 - * - * @param int $tax_rate_id Tax rate ID to update. - * @param array $values Values to set. - * @param string $type Location type. - */ - private static function update_tax_rate_locations( $tax_rate_id, $values, $type ) { - global $wpdb; - - $tax_rate_id = absint( $tax_rate_id ); - - $wpdb->query( - $wpdb->prepare( - "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d AND location_type = %s;", - $tax_rate_id, - $type - ) - ); - - if ( count( $values ) > 0 ) { - $sql = "( '" . implode( "', $tax_rate_id, '" . esc_sql( $type ) . "' ),( '", array_map( 'esc_sql', $values ) ) . "', $tax_rate_id, '" . esc_sql( $type ) . "' )"; - - $wpdb->query( "INSERT INTO {$wpdb->prefix}woocommerce_tax_rate_locations ( location_code, tax_rate_id, location_type ) VALUES $sql;" ); // @codingStandardsIgnoreLine. - } - - WC_Cache_Helper::invalidate_cache_group( 'taxes' ); - } - - /** - * Used by admin settings page. - * - * @param string $tax_class Tax class slug. - * - * @return array|null|object - */ - public static function get_rates_for_tax_class( $tax_class ) { - global $wpdb; - - $tax_class = self::format_tax_rate_class( $tax_class ); - - // Get all the rates and locations. Snagging all at once should significantly cut down on the number of queries. - $rates = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rates` WHERE `tax_rate_class` = %s;", $tax_class ) ); - $locations = $wpdb->get_results( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rate_locations`" ); - - if ( ! empty( $rates ) ) { - // Set the rates keys equal to their ids. - $rates = array_combine( wp_list_pluck( $rates, 'tax_rate_id' ), $rates ); - } - - // Drop the locations into the rates array. - foreach ( $locations as $location ) { - // Don't set them for unexistent rates. - if ( ! isset( $rates[ $location->tax_rate_id ] ) ) { - continue; - } - // If the rate exists, initialize the array before appending to it. - if ( ! isset( $rates[ $location->tax_rate_id ]->{$location->location_type} ) ) { - $rates[ $location->tax_rate_id ]->{$location->location_type} = array(); - } - $rates[ $location->tax_rate_id ]->{$location->location_type}[] = $location->location_code; - } - - foreach ( $rates as $rate_id => $rate ) { - $rates[ $rate_id ]->postcode_count = isset( $rates[ $rate_id ]->postcode ) ? count( $rates[ $rate_id ]->postcode ) : 0; - $rates[ $rate_id ]->city_count = isset( $rates[ $rate_id ]->city ) ? count( $rates[ $rate_id ]->city ) : 0; - } - - $rates = self::sort_rates( $rates ); - - return $rates; - } -} -WC_Tax::init(); diff --git a/includes/class-wc-template-loader.php b/includes/class-wc-template-loader.php deleted file mode 100644 index 5a3233756fa..00000000000 --- a/includes/class-wc-template-loader.php +++ /dev/null @@ -1,586 +0,0 @@ -plugin_path() . '/templates/' . $cs_template; - } else { - $template = WC()->plugin_path() . '/templates/' . $default_file; - } - } - } - - return $template; - } - - /** - * Get the default filename for a template. - * - * @since 3.0.0 - * @return string - */ - private static function get_template_loader_default_file() { - if ( is_singular( 'product' ) ) { - $default_file = 'single-product.php'; - } elseif ( is_product_taxonomy() ) { - $object = get_queried_object(); - - if ( is_tax( 'product_cat' ) || is_tax( 'product_tag' ) ) { - $default_file = 'taxonomy-' . $object->taxonomy . '.php'; - } else { - $default_file = 'archive-product.php'; - } - } elseif ( is_post_type_archive( 'product' ) || is_page( wc_get_page_id( 'shop' ) ) ) { - $default_file = self::$theme_support ? 'archive-product.php' : ''; - } else { - $default_file = ''; - } - return $default_file; - } - - /** - * Get an array of filenames to search for a given template. - * - * @since 3.0.0 - * @param string $default_file The default file name. - * @return string[] - */ - private static function get_template_loader_files( $default_file ) { - $templates = apply_filters( 'woocommerce_template_loader_files', array(), $default_file ); - $templates[] = 'woocommerce.php'; - - if ( is_page_template() ) { - $page_template = get_page_template_slug(); - - if ( $page_template ) { - $validated_file = validate_file( $page_template ); - if ( 0 === $validated_file ) { - $templates[] = $page_template; - } else { - error_log( "WooCommerce: Unable to validate template path: \"$page_template\". Error Code: $validated_file." ); - } - } - } - - if ( is_singular( 'product' ) ) { - $object = get_queried_object(); - $name_decoded = urldecode( $object->post_name ); - if ( $name_decoded !== $object->post_name ) { - $templates[] = "single-product-{$name_decoded}.php"; - } - $templates[] = "single-product-{$object->post_name}.php"; - } - - if ( is_product_taxonomy() ) { - $object = get_queried_object(); - - $templates[] = 'taxonomy-' . $object->taxonomy . '-' . $object->slug . '.php'; - $templates[] = WC()->template_path() . 'taxonomy-' . $object->taxonomy . '-' . $object->slug . '.php'; - $templates[] = 'taxonomy-' . $object->taxonomy . '.php'; - $templates[] = WC()->template_path() . 'taxonomy-' . $object->taxonomy . '.php'; - - if ( is_tax( 'product_cat' ) || is_tax( 'product_tag' ) ) { - $cs_taxonomy = str_replace( '_', '-', $object->taxonomy ); - $cs_default = str_replace( '_', '-', $default_file ); - $templates[] = 'taxonomy-' . $object->taxonomy . '-' . $object->slug . '.php'; - $templates[] = WC()->template_path() . 'taxonomy-' . $cs_taxonomy . '-' . $object->slug . '.php'; - $templates[] = 'taxonomy-' . $object->taxonomy . '.php'; - $templates[] = WC()->template_path() . 'taxonomy-' . $cs_taxonomy . '.php'; - $templates[] = $cs_default; - } - } - - $templates[] = $default_file; - if ( isset( $cs_default ) ) { - $templates[] = WC()->template_path() . $cs_default; - } - $templates[] = WC()->template_path() . $default_file; - - return array_unique( $templates ); - } - - /** - * Load comments template. - * - * @param string $template template to load. - * @return string - */ - public static function comments_template_loader( $template ) { - if ( get_post_type() !== 'product' ) { - return $template; - } - - $check_dirs = array( - trailingslashit( get_stylesheet_directory() ) . WC()->template_path(), - trailingslashit( get_template_directory() ) . WC()->template_path(), - trailingslashit( get_stylesheet_directory() ), - trailingslashit( get_template_directory() ), - trailingslashit( WC()->plugin_path() ) . 'templates/', - ); - - if ( WC_TEMPLATE_DEBUG_MODE ) { - $check_dirs = array( array_pop( $check_dirs ) ); - } - - foreach ( $check_dirs as $dir ) { - if ( file_exists( trailingslashit( $dir ) . 'single-product-reviews.php' ) ) { - return trailingslashit( $dir ) . 'single-product-reviews.php'; - } - } - } - - /** - * Unsupported theme compatibility methods. - */ - - /** - * Hook in methods to enhance the unsupported theme experience on pages. - * - * @since 3.3.0 - */ - public static function unsupported_theme_init() { - if ( 0 < self::$shop_page_id ) { - if ( is_product_taxonomy() ) { - self::unsupported_theme_tax_archive_init(); - } elseif ( is_product() ) { - self::unsupported_theme_product_page_init(); - } else { - self::unsupported_theme_shop_page_init(); - } - } - } - - /** - * Hook in methods to enhance the unsupported theme experience on the Shop page. - * - * @since 3.3.0 - */ - private static function unsupported_theme_shop_page_init() { - add_filter( 'the_content', array( __CLASS__, 'unsupported_theme_shop_content_filter' ), 10 ); - add_filter( 'the_title', array( __CLASS__, 'unsupported_theme_title_filter' ), 10, 2 ); - add_filter( 'comments_number', array( __CLASS__, 'unsupported_theme_comments_number_filter' ) ); - } - - /** - * Hook in methods to enhance the unsupported theme experience on Product pages. - * - * @since 3.3.0 - */ - private static function unsupported_theme_product_page_init() { - add_filter( 'the_content', array( __CLASS__, 'unsupported_theme_product_content_filter' ), 10 ); - add_filter( 'post_thumbnail_html', array( __CLASS__, 'unsupported_theme_single_featured_image_filter' ) ); - add_filter( 'woocommerce_product_tabs', array( __CLASS__, 'unsupported_theme_remove_review_tab' ) ); - remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10 ); - remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10 ); - add_theme_support( 'wc-product-gallery-zoom' ); - add_theme_support( 'wc-product-gallery-lightbox' ); - add_theme_support( 'wc-product-gallery-slider' ); - } - - /** - * Enhance the unsupported theme experience on Product Category and Attribute pages by rendering - * those pages using the single template and shortcode-based content. To do this we make a dummy - * post and set a shortcode as the post content. This approach is adapted from bbPress. - * - * @since 3.3.0 - */ - private static function unsupported_theme_tax_archive_init() { - global $wp_query, $post; - - $queried_object = get_queried_object(); - $args = self::get_current_shop_view_args(); - $shortcode_args = array( - 'page' => $args->page, - 'columns' => $args->columns, - 'rows' => $args->rows, - 'orderby' => '', - 'order' => '', - 'paginate' => true, - 'cache' => false, - ); - - if ( is_product_category() ) { - $shortcode_args['category'] = sanitize_title( $queried_object->slug ); - } elseif ( taxonomy_is_product_attribute( $queried_object->taxonomy ) ) { - $shortcode_args['attribute'] = sanitize_title( $queried_object->taxonomy ); - $shortcode_args['terms'] = sanitize_title( $queried_object->slug ); - } elseif ( is_product_tag() ) { - $shortcode_args['tag'] = sanitize_title( $queried_object->slug ); - } else { - // Default theme archive for all other taxonomies. - return; - } - - // Description handling. - if ( ! empty( $queried_object->description ) && ( empty( $_GET['product-page'] ) || 1 === absint( $_GET['product-page'] ) ) ) { // WPCS: input var ok, CSRF ok. - $prefix = '
    ' . wc_format_content( $queried_object->description ) . '
    '; // WPCS: XSS ok. - } else { - $prefix = ''; - } - - add_filter( 'woocommerce_shortcode_products_query', array( __CLASS__, 'unsupported_archive_layered_nav_compatibility' ) ); - $shortcode = new WC_Shortcode_Products( $shortcode_args ); - remove_filter( 'woocommerce_shortcode_products_query', array( __CLASS__, 'unsupported_archive_layered_nav_compatibility' ) ); - $shop_page = get_post( self::$shop_page_id ); - - $dummy_post_properties = array( - 'ID' => 0, - 'post_status' => 'publish', - 'post_author' => $shop_page->post_author, - 'post_parent' => 0, - 'post_type' => 'page', - 'post_date' => $shop_page->post_date, - 'post_date_gmt' => $shop_page->post_date_gmt, - 'post_modified' => $shop_page->post_modified, - 'post_modified_gmt' => $shop_page->post_modified_gmt, - 'post_content' => $prefix . $shortcode->get_content(), - 'post_title' => wc_clean( $queried_object->name ), - 'post_excerpt' => '', - 'post_content_filtered' => '', - 'post_mime_type' => '', - 'post_password' => '', - 'post_name' => $queried_object->slug, - 'guid' => '', - 'menu_order' => 0, - 'pinged' => '', - 'to_ping' => '', - 'ping_status' => '', - 'comment_status' => 'closed', - 'comment_count' => 0, - 'filter' => 'raw', - ); - - // Set the $post global. - $post = new WP_Post( (object) $dummy_post_properties ); // @codingStandardsIgnoreLine. - - // Copy the new post global into the main $wp_query. - $wp_query->post = $post; - $wp_query->posts = array( $post ); - - // Prevent comments form from appearing. - $wp_query->post_count = 1; - $wp_query->is_404 = false; - $wp_query->is_page = true; - $wp_query->is_single = true; - $wp_query->is_archive = false; - $wp_query->is_tax = true; - $wp_query->max_num_pages = 0; - - // Prepare everything for rendering. - setup_postdata( $post ); - remove_all_filters( 'the_content' ); - remove_all_filters( 'the_excerpt' ); - add_filter( 'template_include', array( __CLASS__, 'force_single_template_filter' ) ); - } - - /** - * Add layered nav args to WP_Query args generated by the 'products' shortcode. - * - * @since 3.3.4 - * @param array $query WP_Query args. - * @return array - */ - public static function unsupported_archive_layered_nav_compatibility( $query ) { - foreach ( WC()->query->get_layered_nav_chosen_attributes() as $taxonomy => $data ) { - $query['tax_query'][] = array( - 'taxonomy' => $taxonomy, - 'field' => 'slug', - 'terms' => $data['terms'], - 'operator' => 'and' === $data['query_type'] ? 'AND' : 'IN', - 'include_children' => false, - ); - } - return $query; - } - - /** - * Force the loading of one of the single templates instead of whatever template was about to be loaded. - * - * @since 3.3.0 - * @param string $template Path to template. - * @return string - */ - public static function force_single_template_filter( $template ) { - $possible_templates = array( - 'page', - 'single', - 'singular', - 'index', - ); - - foreach ( $possible_templates as $possible_template ) { - $path = get_query_template( $possible_template ); - if ( $path ) { - return $path; - } - } - - return $template; - } - - /** - * Get information about the current shop page view. - * - * @since 3.3.0 - * @return array - */ - private static function get_current_shop_view_args() { - return (object) array( - 'page' => absint( max( 1, absint( get_query_var( 'paged' ) ) ) ), - 'columns' => wc_get_default_products_per_row(), - 'rows' => wc_get_default_product_rows_per_page(), - ); - } - - /** - * Filter the title and insert WooCommerce content on the shop page. - * - * For non-WC themes, this will setup the main shop page to be shortcode based to improve default appearance. - * - * @since 3.3.0 - * @param string $title Existing title. - * @param int $id ID of the post being filtered. - * @return string - */ - public static function unsupported_theme_title_filter( $title, $id ) { - if ( self::$theme_support || ! $id !== self::$shop_page_id ) { - return $title; - } - - if ( is_page( self::$shop_page_id ) || ( is_home() && 'page' === get_option( 'show_on_front' ) && absint( get_option( 'page_on_front' ) ) === self::$shop_page_id ) ) { - $args = self::get_current_shop_view_args(); - $title_suffix = array(); - - if ( $args->page > 1 ) { - /* translators: %d: Page number. */ - $title_suffix[] = sprintf( esc_html__( 'Page %d', 'woocommerce' ), $args->page ); - } - - if ( $title_suffix ) { - $title = $title . ' – ' . implode( ', ', $title_suffix ); - } - } - return $title; - } - - /** - * Filter the content and insert WooCommerce content on the shop page. - * - * For non-WC themes, this will setup the main shop page to be shortcode based to improve default appearance. - * - * @since 3.3.0 - * @param string $content Existing post content. - * @return string - */ - public static function unsupported_theme_shop_content_filter( $content ) { - global $wp_query; - - if ( self::$theme_support || ! is_main_query() || ! in_the_loop() ) { - return $content; - } - - self::$in_content_filter = true; - - // Remove the filter we're in to avoid nested calls. - remove_filter( 'the_content', array( __CLASS__, 'unsupported_theme_shop_content_filter' ) ); - - // Unsupported theme shop page. - if ( is_page( self::$shop_page_id ) ) { - $args = self::get_current_shop_view_args(); - $shortcode = new WC_Shortcode_Products( - array_merge( - WC()->query->get_catalog_ordering_args(), - array( - 'page' => $args->page, - 'columns' => $args->columns, - 'rows' => $args->rows, - 'orderby' => '', - 'order' => '', - 'paginate' => true, - 'cache' => false, - ) - ), - 'products' - ); - - // Allow queries to run e.g. layered nav. - add_action( 'pre_get_posts', array( WC()->query, 'product_query' ) ); - - $content = $content . $shortcode->get_content(); - - // Remove actions and self to avoid nested calls. - remove_action( 'pre_get_posts', array( WC()->query, 'product_query' ) ); - WC()->query->remove_ordering_args(); - } - - self::$in_content_filter = false; - - return $content; - } - - /** - * Filter the content and insert WooCommerce content on the shop page. - * - * For non-WC themes, this will setup the main shop page to be shortcode based to improve default appearance. - * - * @since 3.3.0 - * @param string $content Existing post content. - * @return string - */ - public static function unsupported_theme_product_content_filter( $content ) { - global $wp_query; - - if ( self::$theme_support || ! is_main_query() || ! in_the_loop() ) { - return $content; - } - - self::$in_content_filter = true; - - // Remove the filter we're in to avoid nested calls. - remove_filter( 'the_content', array( __CLASS__, 'unsupported_theme_product_content_filter' ) ); - - if ( is_product() ) { - $content = do_shortcode( '[product_page id="' . get_the_ID() . '" show_title=0 status="any"]' ); - } - - self::$in_content_filter = false; - - return $content; - } - - /** - * Suppress the comments number on the Shop page for unsupported themes since there is no commenting on the Shop page. - * - * @since 3.4.5 - * @param string $comments_number The comments number text. - * @return string - */ - public static function unsupported_theme_comments_number_filter( $comments_number ) { - if ( is_page( self::$shop_page_id ) ) { - return ''; - } - - return $comments_number; - } - - /** - * Are we filtering content for unsupported themes? - * - * @since 3.3.2 - * @return bool - */ - public static function in_content_filter() { - return (bool) self::$in_content_filter; - } - - /** - * Prevent the main featured image on product pages because there will be another featured image - * in the gallery. - * - * @since 3.3.0 - * @param string $html Img element HTML. - * @return string - */ - public static function unsupported_theme_single_featured_image_filter( $html ) { - if ( self::in_content_filter() || ! is_product() || ! is_main_query() ) { - return $html; - } - - return ''; - } - - /** - * Remove the Review tab and just use the regular comment form. - * - * @param array $tabs Tab info. - * @return array - */ - public static function unsupported_theme_remove_review_tab( $tabs ) { - unset( $tabs['reviews'] ); - return $tabs; - } -} - -add_action( 'init', array( 'WC_Template_Loader', 'init' ) ); diff --git a/includes/class-wc-tracker.php b/includes/class-wc-tracker.php deleted file mode 100644 index 4fb3719602e..00000000000 --- a/includes/class-wc-tracker.php +++ /dev/null @@ -1,730 +0,0 @@ - apply_filters( 'woocommerce_tracker_last_send_interval', strtotime( '-1 week' ) ) ) { - return; - } - } else { - // Make sure there is at least a 1 hour delay between override sends, we don't want duplicate calls due to double clicking links. - $last_send = self::get_last_send_time(); - if ( $last_send && $last_send > strtotime( '-1 hours' ) ) { - return; - } - } - - // Update time first before sending to ensure it is set. - update_option( 'woocommerce_tracker_last_send', time() ); - - $params = self::get_tracking_data(); - wp_safe_remote_post( - self::$api_url, - array( - 'method' => 'POST', - 'timeout' => 45, - 'redirection' => 5, - 'httpversion' => '1.0', - 'blocking' => false, - 'headers' => array( 'user-agent' => 'WooCommerceTracker/' . md5( esc_url_raw( home_url( '/' ) ) ) . ';' ), - 'body' => wp_json_encode( $params ), - 'cookies' => array(), - ) - ); - } - - /** - * Get the last time tracking data was sent. - * - * @return int|bool - */ - private static function get_last_send_time() { - return apply_filters( 'woocommerce_tracker_last_send_time', get_option( 'woocommerce_tracker_last_send', false ) ); - } - - /** - * Test whether this site is a staging site according to the Jetpack criteria. - * - * With Jetpack 8.1+, Jetpack::is_staging_site has been deprecated. - * \Automattic\Jetpack\Status::is_staging_site is the replacement. - * However, there are version of JP where \Automattic\Jetpack\Status exists, but does *not* contain is_staging_site method, - * so with those, code still needs to use the previous check as a fallback. - * - * @return bool - */ - private static function is_jetpack_staging_site() { - if ( class_exists( '\Automattic\Jetpack\Status' ) ) { - // Preferred way of checking with Jetpack 8.1+. - $jp_status = new \Automattic\Jetpack\Status(); - if ( is_callable( array( $jp_status, 'is_staging_site' ) ) ) { - return $jp_status->is_staging_site(); - } - } - - return ( class_exists( 'Jetpack' ) && is_callable( 'Jetpack::is_staging_site' ) && Jetpack::is_staging_site() ); - } - - /** - * Get all the tracking data. - * - * @return array - */ - private static function get_tracking_data() { - $data = array(); - - // General site info. - $data['url'] = home_url(); - $data['email'] = apply_filters( 'woocommerce_tracker_admin_email', get_option( 'admin_email' ) ); - $data['theme'] = self::get_theme_info(); - - // WordPress Info. - $data['wp'] = self::get_wordpress_info(); - - // Server Info. - $data['server'] = self::get_server_info(); - - // Plugin info. - $all_plugins = self::get_all_plugins(); - $data['active_plugins'] = $all_plugins['active_plugins']; - $data['inactive_plugins'] = $all_plugins['inactive_plugins']; - - // Jetpack & WooCommerce Connect. - - $data['jetpack_version'] = Constants::is_defined( 'JETPACK__VERSION' ) ? Constants::get_constant( 'JETPACK__VERSION' ) : 'none'; - $data['jetpack_connected'] = ( class_exists( 'Jetpack' ) && is_callable( 'Jetpack::is_active' ) && Jetpack::is_active() ) ? 'yes' : 'no'; - $data['jetpack_is_staging'] = self::is_jetpack_staging_site() ? 'yes' : 'no'; - $data['connect_installed'] = class_exists( 'WC_Connect_Loader' ) ? 'yes' : 'no'; - $data['connect_active'] = ( class_exists( 'WC_Connect_Loader' ) && wp_next_scheduled( 'wc_connect_fetch_service_schemas' ) ) ? 'yes' : 'no'; - $data['helper_connected'] = self::get_helper_connected(); - - // Store count info. - $data['users'] = self::get_user_counts(); - $data['products'] = self::get_product_counts(); - $data['orders'] = self::get_orders(); - $data['reviews'] = self::get_review_counts(); - $data['categories'] = self::get_category_counts(); - - // Payment gateway info. - $data['gateways'] = self::get_active_payment_gateways(); - - // Shipping method info. - $data['shipping_methods'] = self::get_active_shipping_methods(); - - // Get all WooCommerce options info. - $data['settings'] = self::get_all_woocommerce_options_values(); - - // Template overrides. - $data['template_overrides'] = self::get_all_template_overrides(); - - // Template overrides. - $data['admin_user_agents'] = self::get_admin_user_agents(); - - // Cart & checkout tech (blocks or shortcodes). - $data['cart_checkout'] = self::get_cart_checkout_info(); - - return apply_filters( 'woocommerce_tracker_data', $data ); - } - - /** - * Get the current theme info, theme name and version. - * - * @return array - */ - public static function get_theme_info() { - $theme_data = wp_get_theme(); - $theme_child_theme = wc_bool_to_string( is_child_theme() ); - $theme_wc_support = wc_bool_to_string( current_theme_supports( 'woocommerce' ) ); - - return array( - 'name' => $theme_data->Name, // @phpcs:ignore - 'version' => $theme_data->Version, // @phpcs:ignore - 'child_theme' => $theme_child_theme, - 'wc_support' => $theme_wc_support, - ); - } - - /** - * Get WordPress related data. - * - * @return array - */ - private static function get_wordpress_info() { - $wp_data = array(); - - $memory = wc_let_to_num( WP_MEMORY_LIMIT ); - - if ( function_exists( 'memory_get_usage' ) ) { - $system_memory = wc_let_to_num( @ini_get( 'memory_limit' ) ); - $memory = max( $memory, $system_memory ); - } - - // WordPress 5.5+ environment type specification. - // 'production' is the default in WP, thus using it as a default here, too. - $environment_type = 'production'; - if ( function_exists( 'wp_get_environment_type' ) ) { - $environment_type = wp_get_environment_type(); - } - - $wp_data['memory_limit'] = size_format( $memory ); - $wp_data['debug_mode'] = ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ? 'Yes' : 'No'; - $wp_data['locale'] = get_locale(); - $wp_data['version'] = get_bloginfo( 'version' ); - $wp_data['multisite'] = is_multisite() ? 'Yes' : 'No'; - $wp_data['env_type'] = $environment_type; - - return $wp_data; - } - - /** - * Get server related info. - * - * @return array - */ - private static function get_server_info() { - $server_data = array(); - - if ( ! empty( $_SERVER['SERVER_SOFTWARE'] ) ) { - $server_data['software'] = $_SERVER['SERVER_SOFTWARE']; // @phpcs:ignore - } - - if ( function_exists( 'phpversion' ) ) { - $server_data['php_version'] = phpversion(); - } - - if ( function_exists( 'ini_get' ) ) { - $server_data['php_post_max_size'] = size_format( wc_let_to_num( ini_get( 'post_max_size' ) ) ); - $server_data['php_time_limt'] = ini_get( 'max_execution_time' ); - $server_data['php_max_input_vars'] = ini_get( 'max_input_vars' ); - $server_data['php_suhosin'] = extension_loaded( 'suhosin' ) ? 'Yes' : 'No'; - } - - $database_version = wc_get_server_database_version(); - $server_data['mysql_version'] = $database_version['number']; - - $server_data['php_max_upload_size'] = size_format( wp_max_upload_size() ); - $server_data['php_default_timezone'] = date_default_timezone_get(); - $server_data['php_soap'] = class_exists( 'SoapClient' ) ? 'Yes' : 'No'; - $server_data['php_fsockopen'] = function_exists( 'fsockopen' ) ? 'Yes' : 'No'; - $server_data['php_curl'] = function_exists( 'curl_init' ) ? 'Yes' : 'No'; - - return $server_data; - } - - /** - * Get all plugins grouped into activated or not. - * - * @return array - */ - private static function get_all_plugins() { - // Ensure get_plugins function is loaded. - if ( ! function_exists( 'get_plugins' ) ) { - include ABSPATH . '/wp-admin/includes/plugin.php'; - } - - $plugins = get_plugins(); - $active_plugins_keys = get_option( 'active_plugins', array() ); - $active_plugins = array(); - - foreach ( $plugins as $k => $v ) { - // Take care of formatting the data how we want it. - $formatted = array(); - $formatted['name'] = strip_tags( $v['Name'] ); - if ( isset( $v['Version'] ) ) { - $formatted['version'] = strip_tags( $v['Version'] ); - } - if ( isset( $v['Author'] ) ) { - $formatted['author'] = strip_tags( $v['Author'] ); - } - if ( isset( $v['Network'] ) ) { - $formatted['network'] = strip_tags( $v['Network'] ); - } - if ( isset( $v['PluginURI'] ) ) { - $formatted['plugin_uri'] = strip_tags( $v['PluginURI'] ); - } - if ( in_array( $k, $active_plugins_keys ) ) { - // Remove active plugins from list so we can show active and inactive separately. - unset( $plugins[ $k ] ); - $active_plugins[ $k ] = $formatted; - } else { - $plugins[ $k ] = $formatted; - } - } - - return array( - 'active_plugins' => $active_plugins, - 'inactive_plugins' => $plugins, - ); - } - - /** - * Check to see if the helper is connected to woocommerce.com - * - * @return string - */ - private static function get_helper_connected() { - if ( class_exists( 'WC_Helper_Options' ) && is_callable( 'WC_Helper_Options::get' ) ) { - $authenticated = WC_Helper_Options::get( 'auth' ); - } else { - $authenticated = ''; - } - return ( ! empty( $authenticated ) ) ? 'yes' : 'no'; - } - - - /** - * Get user totals based on user role. - * - * @return array - */ - private static function get_user_counts() { - $user_count = array(); - $user_count_data = count_users(); - $user_count['total'] = $user_count_data['total_users']; - - // Get user count based on user role. - foreach ( $user_count_data['avail_roles'] as $role => $count ) { - $user_count[ $role ] = $count; - } - - return $user_count; - } - - /** - * Get product totals based on product type. - * - * @return array - */ - public static function get_product_counts() { - $product_count = array(); - $product_count_data = wp_count_posts( 'product' ); - $product_count['total'] = $product_count_data->publish; - - $product_statuses = get_terms( 'product_type', array( 'hide_empty' => 0 ) ); - foreach ( $product_statuses as $product_status ) { - $product_count[ $product_status->name ] = $product_status->count; - } - - return $product_count; - } - - /** - * Get all order data. - * - * @return array - */ - private static function get_orders() { - $args = array( - 'type' => array( 'shop_order', 'shop_order_refund' ), - 'limit' => get_option( 'posts_per_page' ), - 'paged' => 1, - ); - - $first = time(); - $processing_first = $first; - $first_time = $first; - $last = 0; - $processing_last = 0; - $order_data = array(); - - $orders = wc_get_orders( $args ); - $orders_count = count( $orders ); - - while ( $orders_count ) { - - foreach ( $orders as $order ) { - - $date_created = (int) $order->get_date_created()->getTimestamp(); - $type = $order->get_type(); - $status = $order->get_status(); - - if ( 'shop_order' == $type ) { - - // Find the first and last order dates for completed and processing statuses. - if ( 'completed' == $status && $date_created < $first ) { - $first = $date_created; - } - if ( 'completed' == $status && $date_created > $last ) { - $last = $date_created; - } - if ( 'processing' == $status && $date_created < $processing_first ) { - $processing_first = $date_created; - } - if ( 'processing' == $status && $date_created > $processing_last ) { - $processing_last = $date_created; - } - - // Get order counts by status. - $status = 'wc-' . $status; - - if ( ! isset( $order_data[ $status ] ) ) { - $order_data[ $status ] = 1; - } else { - $order_data[ $status ] += 1; - } - - // Count number of orders by gateway used. - $gateway = $order->get_payment_method(); - - if ( ! empty( $gateway ) && in_array( $status, array( 'wc-completed', 'wc-refunded', 'wc-processing' ) ) ) { - $gateway = 'gateway_' . $gateway; - - if ( ! isset( $order_data[ $gateway ] ) ) { - $order_data[ $gateway ] = 1; - } else { - $order_data[ $gateway ] += 1; - } - } - } else { - // If it is a refunded order (shop_order_refunnd type), add the prefix as this prefix gets - // added midway in the if clause. - $status = 'wc-' . $status; - } - - // Calculate the gross total for 'completed' and 'processing' orders. - $total = $order->get_total(); - - if ( in_array( $status, array( 'wc-completed', 'wc-refunded' ) ) ) { - if ( ! isset( $order_data['gross'] ) ) { - $order_data['gross'] = $total; - } else { - $order_data['gross'] += $total; - } - } elseif ( 'wc-processing' == $status ) { - if ( ! isset( $order_data['processing_gross'] ) ) { - $order_data['processing_gross'] = $total; - } else { - $order_data['processing_gross'] += $total; - } - } - } - $args['paged']++; - - $orders = wc_get_orders( $args ); - $orders_count = count( $orders ); - } - - if ( $first !== $first_time ) { - $order_data['first'] = gmdate( 'Y-m-d H:i:s', $first ); - } - - if ( $processing_first !== $first_time ) { - $order_data['processing_first'] = gmdate( 'Y-m-d H:i:s', $processing_first ); - } - - if ( $last ) { - $order_data['last'] = gmdate( 'Y-m-d H:i:s', $last ); - } - - if ( $processing_last ) { - $order_data['processing_last'] = gmdate( 'Y-m-d H:i:s', $processing_last ); - } - - foreach ( $order_data as $key => $value ) { - $order_data[ $key ] = (string) $value; - } - - return $order_data; - } - - /** - * Get review counts for different statuses. - * - * @return array - */ - private static function get_review_counts() { - global $wpdb; - $review_count = array( 'total' => 0 ); - $status_map = array( - '0' => 'pending', - '1' => 'approved', - 'trash' => 'trash', - 'spam' => 'spam', - ); - $counts = $wpdb->get_results( - " - SELECT comment_approved, COUNT(*) AS num_reviews - FROM {$wpdb->comments} - WHERE comment_type = 'review' - GROUP BY comment_approved - ", - ARRAY_A - ); - - if ( ! $counts ) { - return $review_count; - } - - foreach ( $counts as $count ) { - $status = $count['comment_approved']; - if ( array_key_exists( $status, $status_map ) ) { - $review_count[ $status_map[ $status ] ] = $count['num_reviews']; - } - $review_count['total'] += $count['num_reviews']; - } - - return $review_count; - } - - /** - * Get the number of product categories. - * - * @return int - */ - private static function get_category_counts() { - return wp_count_terms( 'product_cat' ); - } - - /** - * Get a list of all active payment gateways. - * - * @return array - */ - private static function get_active_payment_gateways() { - $active_gateways = array(); - $gateways = WC()->payment_gateways->payment_gateways(); - foreach ( $gateways as $id => $gateway ) { - if ( isset( $gateway->enabled ) && 'yes' === $gateway->enabled ) { - $active_gateways[ $id ] = array( - 'title' => $gateway->title, - 'supports' => $gateway->supports, - ); - } - } - - return $active_gateways; - } - - /** - * Get a list of all active shipping methods. - * - * @return array - */ - private static function get_active_shipping_methods() { - $active_methods = array(); - $shipping_methods = WC()->shipping()->get_shipping_methods(); - foreach ( $shipping_methods as $id => $shipping_method ) { - if ( isset( $shipping_method->enabled ) && 'yes' === $shipping_method->enabled ) { - $active_methods[ $id ] = array( - 'title' => $shipping_method->title, - 'tax_status' => $shipping_method->tax_status, - ); - } - } - - return $active_methods; - } - - /** - * Get all options starting with woocommerce_ prefix. - * - * @return array - */ - private static function get_all_woocommerce_options_values() { - return array( - 'version' => WC()->version, - 'currency' => get_woocommerce_currency(), - 'base_location' => WC()->countries->get_base_country(), - 'selling_locations' => WC()->countries->get_allowed_countries(), - 'api_enabled' => get_option( 'woocommerce_api_enabled' ), - 'weight_unit' => get_option( 'woocommerce_weight_unit' ), - 'dimension_unit' => get_option( 'woocommerce_dimension_unit' ), - 'download_method' => get_option( 'woocommerce_file_download_method' ), - 'download_require_login' => get_option( 'woocommerce_downloads_require_login' ), - 'calc_taxes' => get_option( 'woocommerce_calc_taxes' ), - 'coupons_enabled' => get_option( 'woocommerce_enable_coupons' ), - 'guest_checkout' => get_option( 'woocommerce_enable_guest_checkout' ), - 'secure_checkout' => get_option( 'woocommerce_force_ssl_checkout' ), - 'enable_signup_and_login_from_checkout' => get_option( 'woocommerce_enable_signup_and_login_from_checkout' ), - 'enable_myaccount_registration' => get_option( 'woocommerce_enable_myaccount_registration' ), - 'registration_generate_username' => get_option( 'woocommerce_registration_generate_username' ), - 'registration_generate_password' => get_option( 'woocommerce_registration_generate_password' ), - ); - } - - /** - * Look for any template override and return filenames. - * - * @return array - */ - private static function get_all_template_overrides() { - $override_data = array(); - $template_paths = apply_filters( 'woocommerce_template_overrides_scan_paths', array( 'WooCommerce' => WC()->plugin_path() . '/templates/' ) ); - $scanned_files = array(); - - require_once WC()->plugin_path() . '/includes/admin/class-wc-admin-status.php'; - - foreach ( $template_paths as $plugin_name => $template_path ) { - $scanned_files[ $plugin_name ] = WC_Admin_Status::scan_template_files( $template_path ); - } - - foreach ( $scanned_files as $plugin_name => $files ) { - foreach ( $files as $file ) { - if ( file_exists( get_stylesheet_directory() . '/' . $file ) ) { - $theme_file = get_stylesheet_directory() . '/' . $file; - } elseif ( file_exists( get_stylesheet_directory() . '/' . WC()->template_path() . $file ) ) { - $theme_file = get_stylesheet_directory() . '/' . WC()->template_path() . $file; - } elseif ( file_exists( get_template_directory() . '/' . $file ) ) { - $theme_file = get_template_directory() . '/' . $file; - } elseif ( file_exists( get_template_directory() . '/' . WC()->template_path() . $file ) ) { - $theme_file = get_template_directory() . '/' . WC()->template_path() . $file; - } else { - $theme_file = false; - } - - if ( false !== $theme_file ) { - $override_data[] = basename( $theme_file ); - } - } - } - return $override_data; - } - - /** - * When an admin user logs in, there user agent is tracked in user meta and collected here. - * - * @return array - */ - private static function get_admin_user_agents() { - return array_filter( (array) get_option( 'woocommerce_tracker_ua', array() ) ); - } - - /** - * Get order totals - * - * @deprecated 5.1.0 Logic moved to get_orders. - * @return array - */ - public static function get_order_totals() { - wc_deprecated_function( 'WC_Tracker::get_order_totals', '5.1.0', '' ); - return self::get_orders(); - } - - /** - * Search a specific post for text content. - * - * @param integer $post_id The id of the post to search. - * @param string $text The text to search for. - * @return string 'Yes' if post contains $text (otherwise 'No'). - */ - public static function post_contains_text( $post_id, $text ) { - global $wpdb; - - // Search for the text anywhere in the post. - $wildcarded = "%{$text}%"; - - $result = $wpdb->get_var( - $wpdb->prepare( - " - SELECT COUNT( * ) FROM {$wpdb->prefix}posts - WHERE ID=%d - AND {$wpdb->prefix}posts.post_content LIKE %s - ", - array( $post_id, $wildcarded ) - ) - ); - - return ( '0' !== $result ) ? 'Yes' : 'No'; - } - - - /** - * Get tracker data for a specific block type on a woocommerce page. - * - * @param string $block_name The name (id) of a block, e.g. `woocommerce/cart`. - * @param string $woo_page_name The woo page to search, e.g. `cart`. - * @return array Associative array of tracker data with keys: - * - page_contains_block - * - block_attributes - */ - public static function get_block_tracker_data( $block_name, $woo_page_name ) { - $blocks = WC_Blocks_Utils::get_blocks_from_page( $block_name, $woo_page_name ); - - $block_present = false; - $attributes = array(); - if ( $blocks && count( $blocks ) ) { - // Return any customised attributes from the first block. - $block_present = true; - $attributes = $blocks[0]['attrs']; - } - - return array( - 'page_contains_block' => $block_present ? 'Yes' : 'No', - 'block_attributes' => $attributes, - ); - } - - /** - * Get info about the cart & checkout pages. - * - * @return array - */ - public static function get_cart_checkout_info() { - $cart_page_id = wc_get_page_id( 'cart' ); - $checkout_page_id = wc_get_page_id( 'checkout' ); - - $cart_block_data = self::get_block_tracker_data( 'woocommerce/cart', 'cart' ); - $checkout_block_data = self::get_block_tracker_data( 'woocommerce/checkout', 'checkout' ); - - return array( - 'cart_page_contains_cart_shortcode' => self::post_contains_text( - $cart_page_id, - '[woocommerce_cart]' - ), - 'checkout_page_contains_checkout_shortcode' => self::post_contains_text( - $checkout_page_id, - '[woocommerce_checkout]' - ), - - 'cart_page_contains_cart_block' => $cart_block_data['page_contains_block'], - 'cart_block_attributes' => $cart_block_data['block_attributes'], - 'checkout_page_contains_checkout_block' => $checkout_block_data['page_contains_block'], - 'checkout_block_attributes' => $checkout_block_data['block_attributes'], - ); - } -} - -WC_Tracker::init(); diff --git a/includes/class-woocommerce.php b/includes/class-woocommerce.php deleted file mode 100644 index ec163d8efcf..00000000000 --- a/includes/class-woocommerce.php +++ /dev/null @@ -1,978 +0,0 @@ -$key(); - } - } - - /** - * WooCommerce Constructor. - */ - public function __construct() { - $this->define_constants(); - $this->define_tables(); - $this->includes(); - $this->init_hooks(); - } - - /** - * When WP has loaded all plugins, trigger the `woocommerce_loaded` hook. - * - * This ensures `woocommerce_loaded` is called only after all other plugins - * are loaded, to avoid issues caused by plugin directory naming changing - * the load order. See #21524 for details. - * - * @since 3.6.0 - */ - public function on_plugins_loaded() { - do_action( 'woocommerce_loaded' ); - } - - /** - * Hook into actions and filters. - * - * @since 2.3 - */ - private function init_hooks() { - register_activation_hook( WC_PLUGIN_FILE, array( 'WC_Install', 'install' ) ); - register_shutdown_function( array( $this, 'log_errors' ) ); - - add_action( 'plugins_loaded', array( $this, 'on_plugins_loaded' ), -1 ); - add_action( 'admin_notices', array( $this, 'build_dependencies_notice' ) ); - add_action( 'after_setup_theme', array( $this, 'setup_environment' ) ); - add_action( 'after_setup_theme', array( $this, 'include_template_functions' ), 11 ); - add_action( 'init', array( $this, 'init' ), 0 ); - add_action( 'init', array( 'WC_Shortcodes', 'init' ) ); - add_action( 'init', array( 'WC_Emails', 'init_transactional_emails' ) ); - add_action( 'init', array( $this, 'add_image_sizes' ) ); - add_action( 'init', array( $this, 'load_rest_api' ) ); - add_action( 'switch_blog', array( $this, 'wpdb_table_fix' ), 0 ); - add_action( 'activated_plugin', array( $this, 'activated_plugin' ) ); - add_action( 'deactivated_plugin', array( $this, 'deactivated_plugin' ) ); - add_filter( 'woocommerce_rest_prepare_note', array( 'WC_Admin_Notices', 'prepare_note_with_nonce' ) ); - - // These classes set up hooks on instantiation. - wc_get_container()->get( DownloadPermissionsAdjuster::class ); - } - - /** - * Ensures fatal errors are logged so they can be picked up in the status report. - * - * @since 3.2.0 - */ - public function log_errors() { - $error = error_get_last(); - if ( $error && in_array( $error['type'], array( E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ), true ) ) { - $logger = wc_get_logger(); - $logger->critical( - /* translators: 1: error message 2: file name and path 3: line number */ - sprintf( __( '%1$s in %2$s on line %3$s', 'woocommerce' ), $error['message'], $error['file'], $error['line'] ) . PHP_EOL, - array( - 'source' => 'fatal-errors', - ) - ); - do_action( 'woocommerce_shutdown_error', $error ); - } - } - - /** - * Define WC Constants. - */ - private function define_constants() { - $upload_dir = wp_upload_dir( null, false ); - - $this->define( 'WC_ABSPATH', dirname( WC_PLUGIN_FILE ) . '/' ); - $this->define( 'WC_PLUGIN_BASENAME', plugin_basename( WC_PLUGIN_FILE ) ); - $this->define( 'WC_VERSION', $this->version ); - $this->define( 'WOOCOMMERCE_VERSION', $this->version ); - $this->define( 'WC_ROUNDING_PRECISION', 6 ); - $this->define( 'WC_DISCOUNT_ROUNDING_MODE', 2 ); - $this->define( 'WC_TAX_ROUNDING_MODE', 'yes' === get_option( 'woocommerce_prices_include_tax', 'no' ) ? 2 : 1 ); - $this->define( 'WC_DELIMITER', '|' ); - $this->define( 'WC_LOG_DIR', $upload_dir['basedir'] . '/wc-logs/' ); - $this->define( 'WC_SESSION_CACHE_GROUP', 'wc_session_id' ); - $this->define( 'WC_TEMPLATE_DEBUG_MODE', false ); - $this->define( 'WC_NOTICE_MIN_PHP_VERSION', '7.2' ); - $this->define( 'WC_NOTICE_MIN_WP_VERSION', '5.2' ); - $this->define( 'WC_PHP_MIN_REQUIREMENTS_NOTICE', 'wp_php_min_requirements_' . WC_NOTICE_MIN_PHP_VERSION . '_' . WC_NOTICE_MIN_WP_VERSION ); - } - - /** - * Register custom tables within $wpdb object. - */ - private function define_tables() { - global $wpdb; - - // List of tables without prefixes. - $tables = array( - 'payment_tokenmeta' => 'woocommerce_payment_tokenmeta', - 'order_itemmeta' => 'woocommerce_order_itemmeta', - 'wc_product_meta_lookup' => 'wc_product_meta_lookup', - 'wc_tax_rate_classes' => 'wc_tax_rate_classes', - 'wc_reserved_stock' => 'wc_reserved_stock', - ); - - foreach ( $tables as $name => $table ) { - $wpdb->$name = $wpdb->prefix . $table; - $wpdb->tables[] = $table; - } - } - - /** - * Define constant if not already set. - * - * @param string $name Constant name. - * @param string|bool $value Constant value. - */ - private function define( $name, $value ) { - if ( ! defined( $name ) ) { - define( $name, $value ); - } - } - - /** - * Returns true if the request is a non-legacy REST API request. - * - * Legacy REST requests should still run some extra code for backwards compatibility. - * - * @todo: replace this function once core WP function is available: https://core.trac.wordpress.org/ticket/42061. - * - * @return bool - */ - public function is_rest_api_request() { - if ( empty( $_SERVER['REQUEST_URI'] ) ) { - return false; - } - - $rest_prefix = trailingslashit( rest_get_url_prefix() ); - $is_rest_api_request = ( false !== strpos( $_SERVER['REQUEST_URI'], $rest_prefix ) ); // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - - return apply_filters( 'woocommerce_is_rest_api_request', $is_rest_api_request ); - } - - /** - * Load REST API. - */ - public function load_rest_api() { - \Automattic\WooCommerce\RestApi\Server::instance()->init(); - } - - /** - * What type of request is this? - * - * @param string $type admin, ajax, cron or frontend. - * @return bool - */ - private function is_request( $type ) { - switch ( $type ) { - case 'admin': - return is_admin(); - case 'ajax': - return defined( 'DOING_AJAX' ); - case 'cron': - return defined( 'DOING_CRON' ); - case 'frontend': - return ( ! is_admin() || defined( 'DOING_AJAX' ) ) && ! defined( 'DOING_CRON' ) && ! $this->is_rest_api_request(); - } - } - - /** - * Include required core files used in admin and on the frontend. - */ - public function includes() { - /** - * Class autoloader. - */ - include_once WC_ABSPATH . 'includes/class-wc-autoloader.php'; - - /** - * Interfaces. - */ - include_once WC_ABSPATH . 'includes/interfaces/class-wc-abstract-order-data-store-interface.php'; - include_once WC_ABSPATH . 'includes/interfaces/class-wc-coupon-data-store-interface.php'; - include_once WC_ABSPATH . 'includes/interfaces/class-wc-customer-data-store-interface.php'; - include_once WC_ABSPATH . 'includes/interfaces/class-wc-customer-download-data-store-interface.php'; - include_once WC_ABSPATH . 'includes/interfaces/class-wc-customer-download-log-data-store-interface.php'; - include_once WC_ABSPATH . 'includes/interfaces/class-wc-object-data-store-interface.php'; - include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-data-store-interface.php'; - include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-item-data-store-interface.php'; - include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-item-product-data-store-interface.php'; - include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-item-type-data-store-interface.php'; - include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-refund-data-store-interface.php'; - include_once WC_ABSPATH . 'includes/interfaces/class-wc-payment-token-data-store-interface.php'; - include_once WC_ABSPATH . 'includes/interfaces/class-wc-product-data-store-interface.php'; - include_once WC_ABSPATH . 'includes/interfaces/class-wc-product-variable-data-store-interface.php'; - include_once WC_ABSPATH . 'includes/interfaces/class-wc-shipping-zone-data-store-interface.php'; - include_once WC_ABSPATH . 'includes/interfaces/class-wc-logger-interface.php'; - include_once WC_ABSPATH . 'includes/interfaces/class-wc-log-handler-interface.php'; - include_once WC_ABSPATH . 'includes/interfaces/class-wc-webhooks-data-store-interface.php'; - include_once WC_ABSPATH . 'includes/interfaces/class-wc-queue-interface.php'; - - /** - * Core traits. - */ - include_once WC_ABSPATH . 'includes/traits/trait-wc-item-totals.php'; - - /** - * Abstract classes. - */ - include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-data.php'; - include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-object-query.php'; - include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-payment-token.php'; - include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-product.php'; - include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-order.php'; - include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-settings-api.php'; - include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-shipping-method.php'; - include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-payment-gateway.php'; - include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-integration.php'; - include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-log-handler.php'; - include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-deprecated-hooks.php'; - include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-session.php'; - include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-privacy.php'; - - /** - * Core classes. - */ - include_once WC_ABSPATH . 'includes/wc-core-functions.php'; - include_once WC_ABSPATH . 'includes/class-wc-datetime.php'; - include_once WC_ABSPATH . 'includes/class-wc-post-types.php'; - include_once WC_ABSPATH . 'includes/class-wc-install.php'; - include_once WC_ABSPATH . 'includes/class-wc-geolocation.php'; - include_once WC_ABSPATH . 'includes/class-wc-download-handler.php'; - include_once WC_ABSPATH . 'includes/class-wc-comments.php'; - include_once WC_ABSPATH . 'includes/class-wc-post-data.php'; - include_once WC_ABSPATH . 'includes/class-wc-ajax.php'; - include_once WC_ABSPATH . 'includes/class-wc-emails.php'; - include_once WC_ABSPATH . 'includes/class-wc-data-exception.php'; - include_once WC_ABSPATH . 'includes/class-wc-query.php'; - include_once WC_ABSPATH . 'includes/class-wc-meta-data.php'; - include_once WC_ABSPATH . 'includes/class-wc-order-factory.php'; - include_once WC_ABSPATH . 'includes/class-wc-order-query.php'; - include_once WC_ABSPATH . 'includes/class-wc-product-factory.php'; - include_once WC_ABSPATH . 'includes/class-wc-product-query.php'; - include_once WC_ABSPATH . 'includes/class-wc-payment-tokens.php'; - include_once WC_ABSPATH . 'includes/class-wc-shipping-zone.php'; - include_once WC_ABSPATH . 'includes/gateways/class-wc-payment-gateway-cc.php'; - include_once WC_ABSPATH . 'includes/gateways/class-wc-payment-gateway-echeck.php'; - include_once WC_ABSPATH . 'includes/class-wc-countries.php'; - include_once WC_ABSPATH . 'includes/class-wc-integrations.php'; - include_once WC_ABSPATH . 'includes/class-wc-cache-helper.php'; - include_once WC_ABSPATH . 'includes/class-wc-https.php'; - include_once WC_ABSPATH . 'includes/class-wc-deprecated-action-hooks.php'; - include_once WC_ABSPATH . 'includes/class-wc-deprecated-filter-hooks.php'; - include_once WC_ABSPATH . 'includes/class-wc-background-emailer.php'; - include_once WC_ABSPATH . 'includes/class-wc-discounts.php'; - include_once WC_ABSPATH . 'includes/class-wc-cart-totals.php'; - include_once WC_ABSPATH . 'includes/customizer/class-wc-shop-customizer.php'; - include_once WC_ABSPATH . 'includes/class-wc-regenerate-images.php'; - include_once WC_ABSPATH . 'includes/class-wc-privacy.php'; - include_once WC_ABSPATH . 'includes/class-wc-structured-data.php'; - include_once WC_ABSPATH . 'includes/class-wc-shortcodes.php'; - include_once WC_ABSPATH . 'includes/class-wc-logger.php'; - include_once WC_ABSPATH . 'includes/queue/class-wc-action-queue.php'; - include_once WC_ABSPATH . 'includes/queue/class-wc-queue.php'; - include_once WC_ABSPATH . 'includes/admin/marketplace-suggestions/class-wc-marketplace-updater.php'; - include_once WC_ABSPATH . 'includes/blocks/class-wc-blocks-utils.php'; - - /** - * Data stores - used to store and retrieve CRUD object data from the database. - */ - include_once WC_ABSPATH . 'includes/class-wc-data-store.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-data-store-wp.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-coupon-data-store-cpt.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-data-store-cpt.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-grouped-data-store-cpt.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-variable-data-store-cpt.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-variation-data-store-cpt.php'; - include_once WC_ABSPATH . 'includes/data-stores/abstract-wc-order-item-type-data-store.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-data-store.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-coupon-data-store.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-fee-data-store.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-product-data-store.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-shipping-data-store.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-tax-data-store.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-payment-token-data-store.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-data-store.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-data-store-session.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-download-data-store.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-download-log-data-store.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-shipping-zone-data-store.php'; - include_once WC_ABSPATH . 'includes/data-stores/abstract-wc-order-data-store-cpt.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-data-store-cpt.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-refund-data-store-cpt.php'; - include_once WC_ABSPATH . 'includes/data-stores/class-wc-webhook-data-store.php'; - - /** - * REST API. - */ - include_once WC_ABSPATH . 'includes/legacy/class-wc-legacy-api.php'; - include_once WC_ABSPATH . 'includes/class-wc-api.php'; - include_once WC_ABSPATH . 'includes/class-wc-rest-authentication.php'; - include_once WC_ABSPATH . 'includes/class-wc-rest-exception.php'; - include_once WC_ABSPATH . 'includes/class-wc-auth.php'; - include_once WC_ABSPATH . 'includes/class-wc-register-wp-admin-settings.php'; - - /** - * WCCOM Site. - */ - include_once WC_ABSPATH . 'includes/wccom-site/class-wc-wccom-site.php'; - - /** - * Libraries and packages. - */ - include_once WC_ABSPATH . 'packages/action-scheduler/action-scheduler.php'; - - if ( defined( 'WP_CLI' ) && WP_CLI ) { - include_once WC_ABSPATH . 'includes/class-wc-cli.php'; - } - - if ( $this->is_request( 'admin' ) ) { - include_once WC_ABSPATH . 'includes/admin/class-wc-admin.php'; - } - - if ( $this->is_request( 'frontend' ) ) { - $this->frontend_includes(); - } - - if ( $this->is_request( 'cron' ) && 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ) ) { - include_once WC_ABSPATH . 'includes/class-wc-tracker.php'; - } - - $this->theme_support_includes(); - $this->query = new WC_Query(); - $this->api = new WC_API(); - $this->api->init(); - } - - /** - * Include classes for theme support. - * - * @since 3.3.0 - */ - private function theme_support_includes() { - if ( wc_is_wp_default_theme_active() ) { - switch ( get_template() ) { - case 'twentyten': - include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-ten.php'; - break; - case 'twentyeleven': - include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-eleven.php'; - break; - case 'twentytwelve': - include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twelve.php'; - break; - case 'twentythirteen': - include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-thirteen.php'; - break; - case 'twentyfourteen': - include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-fourteen.php'; - break; - case 'twentyfifteen': - include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-fifteen.php'; - break; - case 'twentysixteen': - include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-sixteen.php'; - break; - case 'twentyseventeen': - include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-seventeen.php'; - break; - case 'twentynineteen': - include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-nineteen.php'; - break; - case 'twentytwenty': - include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twenty.php'; - break; - case 'twentytwentyone': - include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twenty-one.php'; - break; - } - } - } - - /** - * Include required frontend files. - */ - public function frontend_includes() { - include_once WC_ABSPATH . 'includes/wc-cart-functions.php'; - include_once WC_ABSPATH . 'includes/wc-notice-functions.php'; - include_once WC_ABSPATH . 'includes/wc-template-hooks.php'; - include_once WC_ABSPATH . 'includes/class-wc-template-loader.php'; - include_once WC_ABSPATH . 'includes/class-wc-frontend-scripts.php'; - include_once WC_ABSPATH . 'includes/class-wc-form-handler.php'; - include_once WC_ABSPATH . 'includes/class-wc-cart.php'; - include_once WC_ABSPATH . 'includes/class-wc-tax.php'; - include_once WC_ABSPATH . 'includes/class-wc-shipping-zones.php'; - include_once WC_ABSPATH . 'includes/class-wc-customer.php'; - include_once WC_ABSPATH . 'includes/class-wc-embed.php'; - include_once WC_ABSPATH . 'includes/class-wc-session-handler.php'; - } - - /** - * Function used to Init WooCommerce Template Functions - This makes them pluggable by plugins and themes. - */ - public function include_template_functions() { - include_once WC_ABSPATH . 'includes/wc-template-functions.php'; - } - - /** - * Init WooCommerce when WordPress Initialises. - */ - public function init() { - // Before init action. - do_action( 'before_woocommerce_init' ); - - // Set up localisation. - $this->load_plugin_textdomain(); - - // Load class instances. - $this->product_factory = new WC_Product_Factory(); - $this->order_factory = new WC_Order_Factory(); - $this->countries = new WC_Countries(); - $this->integrations = new WC_Integrations(); - $this->structured_data = new WC_Structured_Data(); - $this->deprecated_hook_handlers['actions'] = new WC_Deprecated_Action_Hooks(); - $this->deprecated_hook_handlers['filters'] = new WC_Deprecated_Filter_Hooks(); - - // Classes/actions loaded for the frontend and for ajax requests. - if ( $this->is_request( 'frontend' ) ) { - wc_load_cart(); - } - - $this->load_webhooks(); - - // Init action. - do_action( 'woocommerce_init' ); - } - - /** - * Load Localisation files. - * - * Note: the first-loaded translation file overrides any following ones if the same translation is present. - * - * Locales found in: - * - WP_LANG_DIR/woocommerce/woocommerce-LOCALE.mo - * - WP_LANG_DIR/plugins/woocommerce-LOCALE.mo - */ - public function load_plugin_textdomain() { - $locale = determine_locale(); - $locale = apply_filters( 'plugin_locale', $locale, 'woocommerce' ); - - unload_textdomain( 'woocommerce' ); - load_textdomain( 'woocommerce', WP_LANG_DIR . '/woocommerce/woocommerce-' . $locale . '.mo' ); - load_plugin_textdomain( 'woocommerce', false, plugin_basename( dirname( WC_PLUGIN_FILE ) ) . '/i18n/languages' ); - } - - /** - * Ensure theme and server variable compatibility and setup image sizes. - */ - public function setup_environment() { - /** - * WC_TEMPLATE_PATH constant. - * - * @deprecated 2.2 Use WC()->template_path() instead. - */ - $this->define( 'WC_TEMPLATE_PATH', $this->template_path() ); - - $this->add_thumbnail_support(); - } - - /** - * Ensure post thumbnail support is turned on. - */ - private function add_thumbnail_support() { - if ( ! current_theme_supports( 'post-thumbnails' ) ) { - add_theme_support( 'post-thumbnails' ); - } - add_post_type_support( 'product', 'thumbnail' ); - } - - /** - * Add WC Image sizes to WP. - * - * As of 3.3, image sizes can be registered via themes using add_theme_support for woocommerce - * and defining an array of args. If these are not defined, we will use defaults. This is - * handled in wc_get_image_size function. - * - * 3.3 sizes: - * - * woocommerce_thumbnail - Used in product listings. We assume these work for a 3 column grid layout. - * woocommerce_single - Used on single product pages for the main image. - * - * @since 2.3 - */ - public function add_image_sizes() { - $thumbnail = wc_get_image_size( 'thumbnail' ); - $single = wc_get_image_size( 'single' ); - $gallery_thumbnail = wc_get_image_size( 'gallery_thumbnail' ); - - add_image_size( 'woocommerce_thumbnail', $thumbnail['width'], $thumbnail['height'], $thumbnail['crop'] ); - add_image_size( 'woocommerce_single', $single['width'], $single['height'], $single['crop'] ); - add_image_size( 'woocommerce_gallery_thumbnail', $gallery_thumbnail['width'], $gallery_thumbnail['height'], $gallery_thumbnail['crop'] ); - - /** - * Legacy image sizes. - * - * @deprecated 3.3.0 These sizes will be removed in 4.6.0. - */ - add_image_size( 'shop_catalog', $thumbnail['width'], $thumbnail['height'], $thumbnail['crop'] ); - add_image_size( 'shop_single', $single['width'], $single['height'], $single['crop'] ); - add_image_size( 'shop_thumbnail', $gallery_thumbnail['width'], $gallery_thumbnail['height'], $gallery_thumbnail['crop'] ); - } - - /** - * Get the plugin url. - * - * @return string - */ - public function plugin_url() { - return untrailingslashit( plugins_url( '/', WC_PLUGIN_FILE ) ); - } - - /** - * Get the plugin path. - * - * @return string - */ - public function plugin_path() { - return untrailingslashit( plugin_dir_path( WC_PLUGIN_FILE ) ); - } - - /** - * Get the template path. - * - * @return string - */ - public function template_path() { - return apply_filters( 'woocommerce_template_path', 'woocommerce/' ); - } - - /** - * Get Ajax URL. - * - * @return string - */ - public function ajax_url() { - return admin_url( 'admin-ajax.php', 'relative' ); - } - - /** - * Return the WC API URL for a given request. - * - * @param string $request Requested endpoint. - * @param bool|null $ssl If should use SSL, null if should auto detect. Default: null. - * @return string - */ - public function api_request_url( $request, $ssl = null ) { - if ( is_null( $ssl ) ) { - $scheme = wp_parse_url( home_url(), PHP_URL_SCHEME ); - } elseif ( $ssl ) { - $scheme = 'https'; - } else { - $scheme = 'http'; - } - - if ( strstr( get_option( 'permalink_structure' ), '/index.php/' ) ) { - $api_request_url = trailingslashit( home_url( '/index.php/wc-api/' . $request, $scheme ) ); - } elseif ( get_option( 'permalink_structure' ) ) { - $api_request_url = trailingslashit( home_url( '/wc-api/' . $request, $scheme ) ); - } else { - $api_request_url = add_query_arg( 'wc-api', $request, trailingslashit( home_url( '', $scheme ) ) ); - } - - return esc_url_raw( apply_filters( 'woocommerce_api_request_url', $api_request_url, $request, $ssl ) ); - } - - /** - * Load & enqueue active webhooks. - * - * @since 2.2 - */ - private function load_webhooks() { - - if ( ! is_blog_installed() ) { - return; - } - - /** - * Hook: woocommerce_load_webhooks_limit. - * - * @since 3.6.0 - * @param int $limit Used to limit how many webhooks are loaded. Default: no limit. - */ - $limit = apply_filters( 'woocommerce_load_webhooks_limit', null ); - - wc_load_webhooks( 'active', $limit ); - } - - /** - * Initialize the customer and cart objects and setup customer saving on shutdown. - * - * @since 3.6.4 - * @return void - */ - public function initialize_cart() { - // Cart needs customer info. - if ( is_null( $this->customer ) || ! $this->customer instanceof WC_Customer ) { - $this->customer = new WC_Customer( get_current_user_id(), true ); - // Customer should be saved during shutdown. - add_action( 'shutdown', array( $this->customer, 'save' ), 10 ); - } - if ( is_null( $this->cart ) || ! $this->cart instanceof WC_Cart ) { - $this->cart = new WC_Cart(); - } - } - - /** - * Initialize the session class. - * - * @since 3.6.4 - * @return void - */ - public function initialize_session() { - // Session class, handles session data for users - can be overwritten if custom handler is needed. - $session_class = apply_filters( 'woocommerce_session_handler', 'WC_Session_Handler' ); - if ( is_null( $this->session ) || ! $this->session instanceof $session_class ) { - $this->session = new $session_class(); - $this->session->init(); - } - } - - /** - * Set tablenames inside WPDB object. - */ - public function wpdb_table_fix() { - $this->define_tables(); - } - - /** - * Ran when any plugin is activated. - * - * @since 3.6.0 - * @param string $filename The filename of the activated plugin. - */ - public function activated_plugin( $filename ) { - include_once dirname( __FILE__ ) . '/admin/helper/class-wc-helper.php'; - - if ( '/woocommerce.php' === substr( $filename, -16 ) ) { - set_transient( 'woocommerce_activated_plugin', $filename ); - } - - WC_Helper::activated_plugin( $filename ); - } - - /** - * Ran when any plugin is deactivated. - * - * @since 3.6.0 - * @param string $filename The filename of the deactivated plugin. - */ - public function deactivated_plugin( $filename ) { - include_once dirname( __FILE__ ) . '/admin/helper/class-wc-helper.php'; - - WC_Helper::deactivated_plugin( $filename ); - } - - /** - * Get queue instance. - * - * @return WC_Queue_Interface - */ - public function queue() { - return WC_Queue::instance(); - } - - /** - * Get Checkout Class. - * - * @return WC_Checkout - */ - public function checkout() { - return WC_Checkout::instance(); - } - - /** - * Get gateways class. - * - * @return WC_Payment_Gateways - */ - public function payment_gateways() { - return WC_Payment_Gateways::instance(); - } - - /** - * Get shipping class. - * - * @return WC_Shipping - */ - public function shipping() { - return WC_Shipping::instance(); - } - - /** - * Email Class. - * - * @return WC_Emails - */ - public function mailer() { - return WC_Emails::instance(); - } - - /** - * Check if plugin assets are built and minified - * - * @return bool - */ - public function build_dependencies_satisfied() { - // Check if we have compiled CSS. - if ( ! file_exists( WC()->plugin_path() . '/assets/css/admin.css' ) ) { - return false; - } - - // Check if we have minified JS. - if ( ! file_exists( WC()->plugin_path() . '/assets/js/admin/woocommerce_admin.min.js' ) ) { - return false; - } - - return true; - } - - /** - * Output a admin notice when build dependencies not met. - * - * @return void - */ - public function build_dependencies_notice() { - if ( $this->build_dependencies_satisfied() ) { - return; - } - - $message_one = __( 'You have installed a development version of WooCommerce which requires files to be built and minified. From the plugin directory, run grunt assets to build and minify assets.', 'woocommerce' ); - $message_two = sprintf( - /* translators: 1: URL of WordPress.org Repository 2: URL of the GitHub Repository release page */ - __( 'Or you can download a pre-built version of the plugin from the WordPress.org repository or by visiting the releases page in the GitHub repository.', 'woocommerce' ), - 'https://wordpress.org/plugins/woocommerce/', - 'https://github.com/woocommerce/woocommerce/releases' - ); - printf( '

    %s %s

    ', $message_one, $message_two ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } - - /** - * Is the WooCommerce Admin actively included in the WooCommerce core? - * Based on presence of a basic WC Admin function. - * - * @return boolean - */ - public function is_wc_admin_active() { - return function_exists( 'wc_admin_url' ); - } - - /** - * Call a user function. This should be used to execute any non-idempotent function, especially - * those in the `includes` directory or provided by WordPress. - * - * This method can be useful for unit tests, since functions called using this method - * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_function_mocks. - * - * @param string $function_name The function to execute. - * @param mixed ...$parameters The parameters to pass to the function. - * - * @return mixed The result from the function. - * - * @since 4.4 - */ - public function call_function( $function_name, ...$parameters ) { - return wc_get_container()->get( LegacyProxy::class )->call_function( $function_name, ...$parameters ); - } - - /** - * Call a static method in a class. This should be used to execute any non-idempotent method in classes - * from the `includes` directory. - * - * This method can be useful for unit tests, since methods called using this method - * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_static_mocks. - * - * @param string $class_name The name of the class containing the method. - * @param string $method_name The name of the method. - * @param mixed ...$parameters The parameters to pass to the method. - * - * @return mixed The result from the method. - * - * @since 4.4 - */ - public function call_static( $class_name, $method_name, ...$parameters ) { - return wc_get_container()->get( LegacyProxy::class )->call_static( $class_name, $method_name, ...$parameters ); - } - - /** - * Gets an instance of a given legacy class. - * This must not be used to get instances of classes in the `src` directory. - * - * This method can be useful for unit tests, since objects obtained using this method - * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_class_mocks. - * - * @param string $class_name The name of the class to get an instance for. - * @param mixed ...$args Parameters to be passed to the class constructor or to the appropriate internal 'get_instance_of_' method. - * - * @return object The instance of the class. - * @throws \Exception The requested class belongs to the `src` directory, or there was an error creating an instance of the class. - * - * @since 4.4 - */ - public function get_instance_of( string $class_name, ...$args ) { - return wc_get_container()->get( LegacyProxy::class )->get_instance_of( $class_name, ...$args ); - } -} diff --git a/includes/customizer/class-wc-shop-customizer.php b/includes/customizer/class-wc-shop-customizer.php deleted file mode 100644 index 2d633188612..00000000000 --- a/includes/customizer/class-wc-shop-customizer.php +++ /dev/null @@ -1,888 +0,0 @@ -add_panel( - 'woocommerce', - array( - 'priority' => 200, - 'capability' => 'manage_woocommerce', - 'theme_supports' => '', - 'title' => __( 'WooCommerce', 'woocommerce' ), - ) - ); - - $this->add_store_notice_section( $wp_customize ); - $this->add_product_catalog_section( $wp_customize ); - $this->add_product_images_section( $wp_customize ); - $this->add_checkout_section( $wp_customize ); - } - - /** - * Frontend CSS styles. - */ - public function add_frontend_scripts() { - if ( ! is_customize_preview() || ! is_store_notice_showing() ) { - return; - } - - $css = '.woocommerce-store-notice, p.demo_store { display: block !important; }'; - wp_add_inline_style( 'customize-preview', $css ); - } - - /** - * CSS styles to improve our form. - */ - public function add_styles() { - ?> - - - - __( 'Default sorting (custom ordering + name)', 'woocommerce' ), - 'popularity' => __( 'Popularity (sales)', 'woocommerce' ), - 'rating' => __( 'Average rating', 'woocommerce' ), - 'date' => __( 'Sort by most recent', 'woocommerce' ), - 'price' => __( 'Sort by price (asc)', 'woocommerce' ), - 'price-desc' => __( 'Sort by price (desc)', 'woocommerce' ), - ) - ); - - return array_key_exists( $value, $options ) ? $value : 'menu_order'; - } - - /** - * Store notice section. - * - * @param WP_Customize_Manager $wp_customize Theme Customizer object. - */ - private function add_store_notice_section( $wp_customize ) { - $wp_customize->add_section( - 'woocommerce_store_notice', - array( - 'title' => __( 'Store Notice', 'woocommerce' ), - 'priority' => 10, - 'panel' => 'woocommerce', - ) - ); - - $wp_customize->add_setting( - 'woocommerce_demo_store', - array( - 'default' => 'no', - 'type' => 'option', - 'capability' => 'manage_woocommerce', - 'sanitize_callback' => 'wc_bool_to_string', - 'sanitize_js_callback' => 'wc_string_to_bool', - ) - ); - - $wp_customize->add_setting( - 'woocommerce_demo_store_notice', - array( - 'default' => __( 'This is a demo store for testing purposes — no orders shall be fulfilled.', 'woocommerce' ), - 'type' => 'option', - 'capability' => 'manage_woocommerce', - 'sanitize_callback' => 'wp_kses_post', - 'transport' => 'postMessage', - ) - ); - - $wp_customize->add_control( - 'woocommerce_demo_store_notice', - array( - 'label' => __( 'Store notice', 'woocommerce' ), - 'description' => __( 'If enabled, this text will be shown site-wide. You can use it to show events or promotions to visitors!', 'woocommerce' ), - 'section' => 'woocommerce_store_notice', - 'settings' => 'woocommerce_demo_store_notice', - 'type' => 'textarea', - ) - ); - - $wp_customize->add_control( - 'woocommerce_demo_store', - array( - 'label' => __( 'Enable store notice', 'woocommerce' ), - 'section' => 'woocommerce_store_notice', - 'settings' => 'woocommerce_demo_store', - 'type' => 'checkbox', - ) - ); - - if ( isset( $wp_customize->selective_refresh ) ) { - $wp_customize->selective_refresh->add_partial( - 'woocommerce_demo_store_notice', - array( - 'selector' => '.woocommerce-store-notice', - 'container_inclusive' => true, - 'render_callback' => 'woocommerce_demo_store', - ) - ); - } - } - - /** - * Product catalog section. - * - * @param WP_Customize_Manager $wp_customize Theme Customizer object. - */ - public function add_product_catalog_section( $wp_customize ) { - $wp_customize->add_section( - 'woocommerce_product_catalog', - array( - 'title' => __( 'Product Catalog', 'woocommerce' ), - 'priority' => 10, - 'panel' => 'woocommerce', - ) - ); - - $wp_customize->add_setting( - 'woocommerce_shop_page_display', - array( - 'default' => '', - 'type' => 'option', - 'capability' => 'manage_woocommerce', - 'sanitize_callback' => array( $this, 'sanitize_archive_display' ), - ) - ); - - $wp_customize->add_control( - 'woocommerce_shop_page_display', - array( - 'label' => __( 'Shop page display', 'woocommerce' ), - 'description' => __( 'Choose what to display on the main shop page.', 'woocommerce' ), - 'section' => 'woocommerce_product_catalog', - 'settings' => 'woocommerce_shop_page_display', - 'type' => 'select', - 'choices' => array( - '' => __( 'Show products', 'woocommerce' ), - 'subcategories' => __( 'Show categories', 'woocommerce' ), - 'both' => __( 'Show categories & products', 'woocommerce' ), - ), - ) - ); - - $wp_customize->add_setting( - 'woocommerce_category_archive_display', - array( - 'default' => '', - 'type' => 'option', - 'capability' => 'manage_woocommerce', - 'sanitize_callback' => array( $this, 'sanitize_archive_display' ), - ) - ); - - $wp_customize->add_control( - 'woocommerce_category_archive_display', - array( - 'label' => __( 'Category display', 'woocommerce' ), - 'description' => __( 'Choose what to display on product category pages.', 'woocommerce' ), - 'section' => 'woocommerce_product_catalog', - 'settings' => 'woocommerce_category_archive_display', - 'type' => 'select', - 'choices' => array( - '' => __( 'Show products', 'woocommerce' ), - 'subcategories' => __( 'Show subcategories', 'woocommerce' ), - 'both' => __( 'Show subcategories & products', 'woocommerce' ), - ), - ) - ); - - $wp_customize->add_setting( - 'woocommerce_default_catalog_orderby', - array( - 'default' => 'menu_order', - 'type' => 'option', - 'capability' => 'manage_woocommerce', - 'sanitize_callback' => array( $this, 'sanitize_default_catalog_orderby' ), - ) - ); - - $wp_customize->add_control( - 'woocommerce_default_catalog_orderby', - array( - 'label' => __( 'Default product sorting', 'woocommerce' ), - 'description' => __( 'How should products be sorted in the catalog by default?', 'woocommerce' ), - 'section' => 'woocommerce_product_catalog', - 'settings' => 'woocommerce_default_catalog_orderby', - 'type' => 'select', - 'choices' => apply_filters( - 'woocommerce_default_catalog_orderby_options', - array( - 'menu_order' => __( 'Default sorting (custom ordering + name)', 'woocommerce' ), - 'popularity' => __( 'Popularity (sales)', 'woocommerce' ), - 'rating' => __( 'Average rating', 'woocommerce' ), - 'date' => __( 'Sort by most recent', 'woocommerce' ), - 'price' => __( 'Sort by price (asc)', 'woocommerce' ), - 'price-desc' => __( 'Sort by price (desc)', 'woocommerce' ), - ) - ), - ) - ); - - // The following settings should be hidden if the theme is declaring the values. - if ( has_filter( 'loop_shop_columns' ) ) { - return; - } - - $wp_customize->add_setting( - 'woocommerce_catalog_columns', - array( - 'default' => 4, - 'type' => 'option', - 'capability' => 'manage_woocommerce', - 'sanitize_callback' => 'absint', - 'sanitize_js_callback' => 'absint', - ) - ); - - $wp_customize->add_control( - 'woocommerce_catalog_columns', - array( - 'label' => __( 'Products per row', 'woocommerce' ), - 'description' => __( 'How many products should be shown per row?', 'woocommerce' ), - 'section' => 'woocommerce_product_catalog', - 'settings' => 'woocommerce_catalog_columns', - 'type' => 'number', - 'input_attrs' => array( - 'min' => wc_get_theme_support( 'product_grid::min_columns', 1 ), - 'max' => wc_get_theme_support( 'product_grid::max_columns', '' ), - 'step' => 1, - ), - ) - ); - - // Only add this setting if something else isn't managing the number of products per page. - if ( ! has_filter( 'loop_shop_per_page' ) ) { - $wp_customize->add_setting( - 'woocommerce_catalog_rows', - array( - 'default' => 4, - 'type' => 'option', - 'capability' => 'manage_woocommerce', - 'sanitize_callback' => 'absint', - 'sanitize_js_callback' => 'absint', - ) - ); - } - - $wp_customize->add_control( - 'woocommerce_catalog_rows', - array( - 'label' => __( 'Rows per page', 'woocommerce' ), - 'description' => __( 'How many rows of products should be shown per page?', 'woocommerce' ), - 'section' => 'woocommerce_product_catalog', - 'settings' => 'woocommerce_catalog_rows', - 'type' => 'number', - 'input_attrs' => array( - 'min' => wc_get_theme_support( 'product_grid::min_rows', 1 ), - 'max' => wc_get_theme_support( 'product_grid::max_rows', '' ), - 'step' => 1, - ), - ) - ); - } - - /** - * Product images section. - * - * @param WP_Customize_Manager $wp_customize Theme Customizer object. - */ - private function add_product_images_section( $wp_customize ) { - if ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'photon' ) ) { - $regen_description = ''; // Nothing to report; Jetpack will handle magically. - } elseif ( apply_filters( 'woocommerce_background_image_regeneration', true ) && ! is_multisite() ) { - $regen_description = __( 'After publishing your changes, new image sizes will be generated automatically.', 'woocommerce' ); - } elseif ( apply_filters( 'woocommerce_background_image_regeneration', true ) && is_multisite() ) { - /* translators: 1: tools URL 2: regen thumbs url */ - $regen_description = sprintf( __( 'After publishing your changes, new image sizes may not be shown until you regenerate thumbnails. You can do this from the tools section in WooCommerce or by using a plugin such as Regenerate Thumbnails.', 'woocommerce' ), admin_url( 'admin.php?page=wc-status&tab=tools' ), 'https://en-gb.wordpress.org/plugins/regenerate-thumbnails/' ); - } else { - /* translators: %s: regen thumbs url */ - $regen_description = sprintf( __( 'After publishing your changes, new image sizes may not be shown until you Regenerate Thumbnails.', 'woocommerce' ), 'https://en-gb.wordpress.org/plugins/regenerate-thumbnails/' ); - } - - $wp_customize->add_section( - 'woocommerce_product_images', - array( - 'title' => __( 'Product Images', 'woocommerce' ), - 'description' => $regen_description, - 'priority' => 20, - 'panel' => 'woocommerce', - ) - ); - - if ( ! wc_get_theme_support( 'single_image_width' ) ) { - $wp_customize->add_setting( - 'woocommerce_single_image_width', - array( - 'default' => 600, - 'type' => 'option', - 'capability' => 'manage_woocommerce', - 'sanitize_callback' => 'absint', - 'sanitize_js_callback' => 'absint', - ) - ); - - $wp_customize->add_control( - 'woocommerce_single_image_width', - array( - 'label' => __( 'Main image width', 'woocommerce' ), - 'description' => __( 'Image size used for the main image on single product pages. These images will remain uncropped.', 'woocommerce' ), - 'section' => 'woocommerce_product_images', - 'settings' => 'woocommerce_single_image_width', - 'type' => 'number', - 'input_attrs' => array( - 'min' => 0, - 'step' => 1, - ), - ) - ); - } - - if ( ! wc_get_theme_support( 'thumbnail_image_width' ) ) { - $wp_customize->add_setting( - 'woocommerce_thumbnail_image_width', - array( - 'default' => 300, - 'type' => 'option', - 'capability' => 'manage_woocommerce', - 'sanitize_callback' => 'absint', - 'sanitize_js_callback' => 'absint', - ) - ); - - $wp_customize->add_control( - 'woocommerce_thumbnail_image_width', - array( - 'label' => __( 'Thumbnail width', 'woocommerce' ), - 'description' => __( 'Image size used for products in the catalog.', 'woocommerce' ), - 'section' => 'woocommerce_product_images', - 'settings' => 'woocommerce_thumbnail_image_width', - 'type' => 'number', - 'input_attrs' => array( - 'min' => 0, - 'step' => 1, - ), - ) - ); - } - - include_once WC_ABSPATH . 'includes/customizer/class-wc-customizer-control-cropping.php'; - - $wp_customize->add_setting( - 'woocommerce_thumbnail_cropping', - array( - 'default' => '1:1', - 'type' => 'option', - 'capability' => 'manage_woocommerce', - 'sanitize_callback' => 'wc_clean', - ) - ); - - $wp_customize->add_setting( - 'woocommerce_thumbnail_cropping_custom_width', - array( - 'default' => '4', - 'type' => 'option', - 'capability' => 'manage_woocommerce', - 'sanitize_callback' => 'absint', - 'sanitize_js_callback' => 'absint', - ) - ); - - $wp_customize->add_setting( - 'woocommerce_thumbnail_cropping_custom_height', - array( - 'default' => '3', - 'type' => 'option', - 'capability' => 'manage_woocommerce', - 'sanitize_callback' => 'absint', - 'sanitize_js_callback' => 'absint', - ) - ); - - $wp_customize->add_control( - new WC_Customizer_Control_Cropping( - $wp_customize, - 'woocommerce_thumbnail_cropping', - array( - 'section' => 'woocommerce_product_images', - 'settings' => array( - 'cropping' => 'woocommerce_thumbnail_cropping', - 'custom_width' => 'woocommerce_thumbnail_cropping_custom_width', - 'custom_height' => 'woocommerce_thumbnail_cropping_custom_height', - ), - 'label' => __( 'Thumbnail cropping', 'woocommerce' ), - 'choices' => array( - '1:1' => array( - 'label' => __( '1:1', 'woocommerce' ), - 'description' => __( 'Images will be cropped into a square', 'woocommerce' ), - ), - 'custom' => array( - 'label' => __( 'Custom', 'woocommerce' ), - 'description' => __( 'Images will be cropped to a custom aspect ratio', 'woocommerce' ), - ), - 'uncropped' => array( - 'label' => __( 'Uncropped', 'woocommerce' ), - 'description' => __( 'Images will display using the aspect ratio in which they were uploaded', 'woocommerce' ), - ), - ), - ) - ) - ); - } - - /** - * Checkout section. - * - * @param WP_Customize_Manager $wp_customize Theme Customizer object. - */ - public function add_checkout_section( $wp_customize ) { - $wp_customize->add_section( - 'woocommerce_checkout', - array( - 'title' => __( 'Checkout', 'woocommerce' ), - 'priority' => 20, - 'panel' => 'woocommerce', - 'description' => __( 'These options let you change the appearance of the WooCommerce checkout.', 'woocommerce' ), - ) - ); - - // Checkout field controls. - $fields = array( - 'company' => __( 'Company name', 'woocommerce' ), - 'address_2' => __( 'Address line 2', 'woocommerce' ), - 'phone' => __( 'Phone', 'woocommerce' ), - ); - foreach ( $fields as $field => $label ) { - $wp_customize->add_setting( - 'woocommerce_checkout_' . $field . '_field', - array( - 'default' => 'phone' === $field ? 'required' : 'optional', - 'type' => 'option', - 'capability' => 'manage_woocommerce', - 'sanitize_callback' => array( $this, 'sanitize_checkout_field_display' ), - ) - ); - $wp_customize->add_control( - 'woocommerce_checkout_' . $field . '_field', - array( - /* Translators: %s field name. */ - 'label' => sprintf( __( '%s field', 'woocommerce' ), $label ), - 'section' => 'woocommerce_checkout', - 'settings' => 'woocommerce_checkout_' . $field . '_field', - 'type' => 'select', - 'choices' => array( - 'hidden' => __( 'Hidden', 'woocommerce' ), - 'optional' => __( 'Optional', 'woocommerce' ), - 'required' => __( 'Required', 'woocommerce' ), - ), - ) - ); - } - - // Register settings. - $wp_customize->add_setting( - 'woocommerce_checkout_highlight_required_fields', - array( - 'default' => 'yes', - 'type' => 'option', - 'capability' => 'manage_woocommerce', - 'sanitize_callback' => 'wc_bool_to_string', - 'sanitize_js_callback' => 'wc_string_to_bool', - ) - ); - - $wp_customize->add_setting( - 'woocommerce_checkout_terms_and_conditions_checkbox_text', - array( - /* translators: %s terms and conditions page name and link */ - 'default' => sprintf( __( 'I have read and agree to the website %s', 'woocommerce' ), '[terms]' ), - 'type' => 'option', - 'capability' => 'manage_woocommerce', - 'sanitize_callback' => 'wp_kses_post', - 'transport' => 'postMessage', - ) - ); - - $wp_customize->add_setting( - 'woocommerce_checkout_privacy_policy_text', - array( - /* translators: %s privacy policy page name and link */ - 'default' => sprintf( __( 'Your personal data will be used to process your order, support your experience throughout this website, and for other purposes described in our %s.', 'woocommerce' ), '[privacy_policy]' ), - 'type' => 'option', - 'capability' => 'manage_woocommerce', - 'sanitize_callback' => 'wp_kses_post', - 'transport' => 'postMessage', - ) - ); - - // Register controls. - $wp_customize->add_control( - 'woocommerce_checkout_highlight_required_fields', - array( - 'label' => __( 'Highlight required fields with an asterisk', 'woocommerce' ), - 'section' => 'woocommerce_checkout', - 'settings' => 'woocommerce_checkout_highlight_required_fields', - 'type' => 'checkbox', - ) - ); - - if ( current_user_can( 'manage_privacy_options' ) ) { - $choose_pages = array( - 'wp_page_for_privacy_policy' => __( 'Privacy policy', 'woocommerce' ), - 'woocommerce_terms_page_id' => __( 'Terms and conditions', 'woocommerce' ), - ); - } else { - $choose_pages = array( - 'woocommerce_terms_page_id' => __( 'Terms and conditions', 'woocommerce' ), - ); - } - $pages = get_pages( - array( - 'post_type' => 'page', - 'post_status' => 'publish,private,draft', - 'child_of' => 0, - 'parent' => -1, - 'exclude' => array( - wc_get_page_id( 'cart' ), - wc_get_page_id( 'checkout' ), - wc_get_page_id( 'myaccount' ), - ), - 'sort_order' => 'asc', - 'sort_column' => 'post_title', - ) - ); - $page_choices = array( '' => __( 'No page set', 'woocommerce' ) ) + array_combine( array_map( 'strval', wp_list_pluck( $pages, 'ID' ) ), wp_list_pluck( $pages, 'post_title' ) ); - - foreach ( $choose_pages as $id => $name ) { - $wp_customize->add_setting( - $id, - array( - 'default' => '', - 'type' => 'option', - 'capability' => 'manage_woocommerce', - ) - ); - $wp_customize->add_control( - $id, - array( - /* Translators: %s: page name. */ - 'label' => sprintf( __( '%s page', 'woocommerce' ), $name ), - 'section' => 'woocommerce_checkout', - 'settings' => $id, - 'type' => 'select', - 'choices' => $page_choices, - ) - ); - } - - $wp_customize->add_control( - 'woocommerce_checkout_privacy_policy_text', - array( - 'label' => __( 'Privacy policy', 'woocommerce' ), - 'description' => __( 'Optionally add some text about your store privacy policy to show during checkout.', 'woocommerce' ), - 'section' => 'woocommerce_checkout', - 'settings' => 'woocommerce_checkout_privacy_policy_text', - 'active_callback' => array( $this, 'has_privacy_policy_page_id' ), - 'type' => 'textarea', - ) - ); - - $wp_customize->add_control( - 'woocommerce_checkout_terms_and_conditions_checkbox_text', - array( - 'label' => __( 'Terms and conditions', 'woocommerce' ), - 'description' => __( 'Optionally add some text for the terms checkbox that customers must accept.', 'woocommerce' ), - 'section' => 'woocommerce_checkout', - 'settings' => 'woocommerce_checkout_terms_and_conditions_checkbox_text', - 'active_callback' => array( $this, 'has_terms_and_conditions_page_id' ), - 'type' => 'text', - ) - ); - - if ( isset( $wp_customize->selective_refresh ) ) { - $wp_customize->selective_refresh->add_partial( - 'woocommerce_checkout_privacy_policy_text', - array( - 'selector' => '.woocommerce-privacy-policy-text', - 'container_inclusive' => true, - 'render_callback' => 'wc_checkout_privacy_policy_text', - ) - ); - $wp_customize->selective_refresh->add_partial( - 'woocommerce_checkout_terms_and_conditions_checkbox_text', - array( - 'selector' => '.woocommerce-terms-and-conditions-checkbox-text', - 'container_inclusive' => false, - 'render_callback' => 'wc_terms_and_conditions_checkbox_text', - ) - ); - } - } - - /** - * Sanitize field display. - * - * @param string $value '', 'subcategories', or 'both'. - * @return string - */ - public function sanitize_checkout_field_display( $value ) { - $options = array( 'hidden', 'optional', 'required' ); - return in_array( $value, $options, true ) ? $value : ''; - } - - /** - * Whether or not a page has been chose for the privacy policy. - * - * @return bool - */ - public function has_privacy_policy_page_id() { - return wc_privacy_policy_page_id() > 0; - } - - /** - * Whether or not a page has been chose for the terms and conditions. - * - * @return bool - */ - public function has_terms_and_conditions_page_id() { - return wc_terms_and_conditions_page_id() > 0; - } -} - -new WC_Shop_Customizer(); diff --git a/includes/data-stores/class-wc-coupon-data-store-cpt.php b/includes/data-stores/class-wc-coupon-data-store-cpt.php deleted file mode 100644 index f0bcbd7673a..00000000000 --- a/includes/data-stores/class-wc-coupon-data-store-cpt.php +++ /dev/null @@ -1,729 +0,0 @@ -get_date_created( 'edit' ) ) { - $coupon->set_date_created( time() ); - } - - $coupon_id = wp_insert_post( - apply_filters( - 'woocommerce_new_coupon_data', - array( - 'post_type' => 'shop_coupon', - 'post_status' => 'publish', - 'post_author' => get_current_user_id(), - 'post_title' => $coupon->get_code( 'edit' ), - 'post_content' => '', - 'post_excerpt' => $coupon->get_description( 'edit' ), - 'post_date' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getOffsetTimestamp() ), - 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getTimestamp() ), - ) - ), - true - ); - - if ( $coupon_id ) { - $coupon->set_id( $coupon_id ); - $this->update_post_meta( $coupon ); - $coupon->save_meta_data(); - $coupon->apply_changes(); - delete_transient( 'rest_api_coupons_type_count' ); - do_action( 'woocommerce_new_coupon', $coupon_id, $coupon ); - } - } - - /** - * Method to read a coupon. - * - * @since 3.0.0 - * - * @param WC_Coupon $coupon Coupon object. - * - * @throws Exception If invalid coupon. - */ - public function read( &$coupon ) { - $coupon->set_defaults(); - - $post_object = get_post( $coupon->get_id() ); - - if ( ! $coupon->get_id() || ! $post_object || 'shop_coupon' !== $post_object->post_type ) { - throw new Exception( __( 'Invalid coupon.', 'woocommerce' ) ); - } - - $coupon_id = $coupon->get_id(); - $coupon->set_props( - array( - 'code' => $post_object->post_title, - 'description' => $post_object->post_excerpt, - 'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ), - 'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ), - 'date_expires' => metadata_exists( 'post', $coupon_id, 'date_expires' ) ? get_post_meta( $coupon_id, 'date_expires', true ) : get_post_meta( $coupon_id, 'expiry_date', true ), // @todo: Migrate expiry_date meta to date_expires in upgrade routine. - 'discount_type' => get_post_meta( $coupon_id, 'discount_type', true ), - 'amount' => get_post_meta( $coupon_id, 'coupon_amount', true ), - 'usage_count' => get_post_meta( $coupon_id, 'usage_count', true ), - 'individual_use' => 'yes' === get_post_meta( $coupon_id, 'individual_use', true ), - 'product_ids' => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'product_ids', true ) ) ), - 'excluded_product_ids' => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'exclude_product_ids', true ) ) ), - 'usage_limit' => get_post_meta( $coupon_id, 'usage_limit', true ), - 'usage_limit_per_user' => get_post_meta( $coupon_id, 'usage_limit_per_user', true ), - 'limit_usage_to_x_items' => 0 < get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) ? get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) : null, - 'free_shipping' => 'yes' === get_post_meta( $coupon_id, 'free_shipping', true ), - 'product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'product_categories', true ) ), - 'excluded_product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'exclude_product_categories', true ) ), - 'exclude_sale_items' => 'yes' === get_post_meta( $coupon_id, 'exclude_sale_items', true ), - 'minimum_amount' => get_post_meta( $coupon_id, 'minimum_amount', true ), - 'maximum_amount' => get_post_meta( $coupon_id, 'maximum_amount', true ), - 'email_restrictions' => array_filter( (array) get_post_meta( $coupon_id, 'customer_email', true ) ), - 'used_by' => array_filter( (array) get_post_meta( $coupon_id, '_used_by' ) ), - ) - ); - $coupon->read_meta_data(); - $coupon->set_object_read( true ); - do_action( 'woocommerce_coupon_loaded', $coupon ); - } - - /** - * Updates a coupon in the database. - * - * @since 3.0.0 - * @param WC_Coupon $coupon Coupon object. - */ - public function update( &$coupon ) { - $coupon->save_meta_data(); - $changes = $coupon->get_changes(); - - if ( array_intersect( array( 'code', 'description', 'date_created', 'date_modified' ), array_keys( $changes ) ) ) { - $post_data = array( - 'post_title' => $coupon->get_code( 'edit' ), - 'post_excerpt' => $coupon->get_description( 'edit' ), - 'post_date' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created( 'edit' )->getOffsetTimestamp() ), - 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created( 'edit' )->getTimestamp() ), - 'post_modified' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $coupon->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ), - 'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $coupon->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ), - ); - - /** - * When updating this object, to prevent infinite loops, use $wpdb - * to update data, since wp_update_post spawns more calls to the - * save_post action. - * - * This ensures hooks are fired by either WP itself (admin screen save), - * or an update purely from CRUD. - */ - if ( doing_action( 'save_post' ) ) { - $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $coupon->get_id() ) ); - clean_post_cache( $coupon->get_id() ); - } else { - wp_update_post( array_merge( array( 'ID' => $coupon->get_id() ), $post_data ) ); - } - $coupon->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook. - } - $this->update_post_meta( $coupon ); - $coupon->apply_changes(); - delete_transient( 'rest_api_coupons_type_count' ); - do_action( 'woocommerce_update_coupon', $coupon->get_id(), $coupon ); - } - - /** - * Deletes a coupon from the database. - * - * @since 3.0.0 - * - * @param WC_Coupon $coupon Coupon object. - * @param array $args Array of args to pass to the delete method. - */ - public function delete( &$coupon, $args = array() ) { - $args = wp_parse_args( - $args, - array( - 'force_delete' => false, - ) - ); - - $id = $coupon->get_id(); - - if ( ! $id ) { - return; - } - - if ( $args['force_delete'] ) { - wp_delete_post( $id ); - - wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $coupon->get_code(), 'coupons' ); - - $coupon->set_id( 0 ); - do_action( 'woocommerce_delete_coupon', $id ); - } else { - wp_trash_post( $id ); - do_action( 'woocommerce_trash_coupon', $id ); - } - } - - /** - * Helper method that updates all the post meta for a coupon based on it's settings in the WC_Coupon class. - * - * @param WC_Coupon $coupon Coupon object. - * @since 3.0.0 - */ - private function update_post_meta( &$coupon ) { - $meta_key_to_props = array( - 'discount_type' => 'discount_type', - 'coupon_amount' => 'amount', - 'individual_use' => 'individual_use', - 'product_ids' => 'product_ids', - 'exclude_product_ids' => 'excluded_product_ids', - 'usage_limit' => 'usage_limit', - 'usage_limit_per_user' => 'usage_limit_per_user', - 'limit_usage_to_x_items' => 'limit_usage_to_x_items', - 'usage_count' => 'usage_count', - 'date_expires' => 'date_expires', - 'free_shipping' => 'free_shipping', - 'product_categories' => 'product_categories', - 'exclude_product_categories' => 'excluded_product_categories', - 'exclude_sale_items' => 'exclude_sale_items', - 'minimum_amount' => 'minimum_amount', - 'maximum_amount' => 'maximum_amount', - 'customer_email' => 'email_restrictions', - ); - - $props_to_update = $this->get_props_to_update( $coupon, $meta_key_to_props ); - foreach ( $props_to_update as $meta_key => $prop ) { - $value = $coupon->{"get_$prop"}( 'edit' ); - $value = is_string( $value ) ? wp_slash( $value ) : $value; - switch ( $prop ) { - case 'individual_use': - case 'free_shipping': - case 'exclude_sale_items': - $value = wc_bool_to_string( $value ); - break; - case 'product_ids': - case 'excluded_product_ids': - $value = implode( ',', array_filter( array_map( 'intval', $value ) ) ); - break; - case 'product_categories': - case 'excluded_product_categories': - $value = array_filter( array_map( 'intval', $value ) ); - break; - case 'email_restrictions': - $value = array_filter( array_map( 'sanitize_email', $value ) ); - break; - case 'date_expires': - $value = $value ? $value->getTimestamp() : null; - break; - } - - $updated = $this->update_or_delete_post_meta( $coupon, $meta_key, $value ); - - if ( $updated ) { - $this->updated_props[] = $prop; - } - } - - do_action( 'woocommerce_coupon_object_updated_props', $coupon, $this->updated_props ); - } - - /** - * Increase usage count for current coupon. - * - * @since 3.0.0 - * @param WC_Coupon $coupon Coupon object. - * @param string $used_by Either user ID or billing email. - * @param WC_Order $order (Optional) If passed, clears the hold record associated with order. - - * @return int New usage count. - */ - public function increase_usage_count( &$coupon, $used_by = '', $order = null ) { - $coupon_held_key_for_user = ''; - if ( $order instanceof WC_Order ) { - $coupon_held_key_for_user = $order->get_data_store()->get_coupon_held_keys_for_users( $order, $coupon->get_id() ); - } - - $new_count = $this->update_usage_count_meta( $coupon, 'increase' ); - - if ( $used_by ) { - $this->add_coupon_used_by( $coupon, $used_by, $coupon_held_key_for_user ); - $coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) ); - } - - do_action( 'woocommerce_increase_coupon_usage_count', $coupon, $new_count, $used_by ); - - return $new_count; - } - - /** - * Helper function to add a `_used_by` record to track coupons used by the user. - * - * @param WC_Coupon $coupon Coupon object. - * @param string $used_by Either user ID or billing email. - * @param string $coupon_held_key (Optional) Update meta key to `_used_by` instead of adding a new record. - */ - private function add_coupon_used_by( $coupon, $used_by, $coupon_held_key ) { - global $wpdb; - if ( $coupon_held_key && '' !== $coupon_held_key ) { - // Looks like we added a tentative record for this coupon getting used. - // Lets change the tentative record to a permanent one. - $result = $wpdb->query( - $wpdb->prepare( - " - UPDATE $wpdb->postmeta SET meta_key = %s, meta_value = %s WHERE meta_key = %s LIMIT 1", - '_used_by', - $used_by, - $coupon_held_key - ) - ); - if ( ! $result ) { - // If no rows were updated, then insert a `_used_by` row manually to maintain consistency. - add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) ); - } - } else { - add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) ); - } - } - - /** - * Decrease usage count for current coupon. - * - * @since 3.0.0 - * @param WC_Coupon $coupon Coupon object. - * @param string $used_by Either user ID or billing email. - * @return int New usage count. - */ - public function decrease_usage_count( &$coupon, $used_by = '' ) { - global $wpdb; - $new_count = $this->update_usage_count_meta( $coupon, 'decrease' ); - if ( $used_by ) { - /** - * We're doing this the long way because `delete_post_meta( $id, $key, $value )` deletes. - * all instances where the key and value match, and we only want to delete one. - */ - $meta_id = $wpdb->get_var( - $wpdb->prepare( - "SELECT meta_id FROM $wpdb->postmeta WHERE meta_key = '_used_by' AND meta_value = %s AND post_id = %d LIMIT 1;", - $used_by, - $coupon->get_id() - ) - ); - if ( $meta_id ) { - delete_metadata_by_mid( 'post', $meta_id ); - $coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) ); - } - } - - do_action( 'woocommerce_decrease_coupon_usage_count', $coupon, $new_count, $used_by ); - - return $new_count; - } - - /** - * Increase or decrease the usage count for a coupon by 1. - * - * @since 3.0.0 - * @param WC_Coupon $coupon Coupon object. - * @param string $operation 'increase' or 'decrease'. - * @return int New usage count - */ - private function update_usage_count_meta( &$coupon, $operation = 'increase' ) { - global $wpdb; - $id = $coupon->get_id(); - $operator = ( 'increase' === $operation ) ? '+' : '-'; - - add_post_meta( $id, 'usage_count', $coupon->get_usage_count( 'edit' ), true ); - $wpdb->query( - $wpdb->prepare( - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared - "UPDATE $wpdb->postmeta SET meta_value = meta_value {$operator} 1 WHERE meta_key = 'usage_count' AND post_id = %d;", - $id - ) - ); - - // Get the latest value direct from the DB, instead of possibly the WP meta cache. - return (int) $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = 'usage_count' AND post_id = %d;", $id ) ); - } - - /** - * Returns tentative usage count for coupon. - * - * @param int $coupon_id Coupon ID. - * - * @return int Tentative usage count. - */ - public function get_tentative_usage_count( $coupon_id ) { - global $wpdb; - return $wpdb->get_var( - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $this->get_tentative_usage_query( $coupon_id ) - ); - } - - /** - * Get the number of uses for a coupon by user ID. - * - * @since 3.0.0 - * @param WC_Coupon $coupon Coupon object. - * @param int $user_id User ID. - * @return int - */ - public function get_usage_by_user_id( &$coupon, $user_id ) { - global $wpdb; - $usage_count = $wpdb->get_var( - $wpdb->prepare( - "SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %d;", - $coupon->get_id(), - $user_id - ) - ); - $tentative_usage_count = $this->get_tentative_usages_for_user( $coupon->get_id(), array( $user_id ) ); - return $tentative_usage_count + $usage_count; - } - - /** - * Get the number of uses for a coupon by email address - * - * @since 3.6.4 - * @param WC_Coupon $coupon Coupon object. - * @param string $email Email address. - * @return int - */ - public function get_usage_by_email( &$coupon, $email ) { - global $wpdb; - $usage_count = $wpdb->get_var( - $wpdb->prepare( - "SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %s;", - $coupon->get_id(), - $email - ) - ); - $tentative_usage_count = $this->get_tentative_usages_for_user( $coupon->get_id(), array( $email ) ); - return $tentative_usage_count + $usage_count; - } - - /** - * Get tentative coupon usages for user. - * - * @param int $coupon_id Coupon ID. - * @param array $user_aliases Array of user aliases to check tentative usages for. - * - * @return string|null - */ - public function get_tentative_usages_for_user( $coupon_id, $user_aliases ) { - global $wpdb; - return $wpdb->get_var( - $this->get_tentative_usage_query_for_user( $coupon_id, $user_aliases ) - ); // WPCS: unprepared SQL ok. - - } - - /** - * Get held time for resources before cancelling the order. Use 60 minutes as sane default. - * Note that the filter `woocommerce_coupon_hold_minutes` only support minutes because it's getting used elsewhere as well, however this function returns in seconds. - * - * @return int - */ - private function get_tentative_held_time() { - return apply_filters( 'woocommerce_coupon_hold_minutes', ( (int) get_option( 'woocommerce_hold_stock_minutes', 60 ) ) ) * 60; - } - - /** - * Check and records coupon usage tentatively for short period of time so that counts validation is correct. Returns early if there is no limit defined for the coupon. - * - * @param WC_Coupon $coupon Coupon object. - * - * @return bool|int|string|null Returns meta key if coupon was held, null if returned early. - */ - public function check_and_hold_coupon( $coupon ) { - global $wpdb; - - $usage_limit = $coupon->get_usage_limit(); - $held_time = $this->get_tentative_held_time(); - - if ( 0 >= $usage_limit || 0 >= $held_time ) { - return null; - } - - if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', true ) ) { - return null; - } - - // Make sure we have usage_count meta key for this coupon because its required for `$query_for_usages`. - // We are not directly modifying `$query_for_usages` to allow for `usage_count` not present only keep that query simple. - if ( ! metadata_exists( 'post', $coupon->get_id(), 'usage_count' ) ) { - $coupon->set_usage_count( $coupon->get_usage_count() ); // Use `get_usage_count` here to write default value, which may changed by a filter. - $coupon->save(); - } - - $query_for_usages = $wpdb->prepare( - " - SELECT meta_value from $wpdb->postmeta - WHERE {$wpdb->postmeta}.meta_key = 'usage_count' - AND {$wpdb->postmeta}.post_id = %d - LIMIT 1 - FOR UPDATE - ", - $coupon->get_id() - ); - - $query_for_tentative_usages = $this->get_tentative_usage_query( $coupon->get_id() ); - $db_timestamp = $wpdb->get_var( 'SELECT UNIX_TIMESTAMP() FROM DUAL' ); - - $coupon_usage_key = '_coupon_held_' . ( (int) $db_timestamp + $held_time ) . '_' . wp_generate_password( 6, false ); - - $insert_statement = $wpdb->prepare( - " - INSERT INTO $wpdb->postmeta ( post_id, meta_key, meta_value ) - SELECT %d, %s, %s FROM DUAL - WHERE ( $query_for_usages ) + ( $query_for_tentative_usages ) < %d - ", - $coupon->get_id(), - $coupon_usage_key, - '', - $usage_limit - ); // WPCS: unprepared SQL ok. - - /** - * In some cases, specifically when there is a combined index on post_id,meta_key, the insert statement above could end up in a deadlock. - * We will try to insert 3 times before giving up to recover from deadlock. - */ - for ( $count = 0; $count < 3; $count++ ) { - $result = $wpdb->query( $insert_statement ); // WPCS: unprepared SQL ok. - if ( false !== $result ) { - break; - } - } - - return $result > 0 ? $coupon_usage_key : $result; - } - - /** - * Generate query to calculate tentative usages for the coupon. - * - * @param int $coupon_id Coupon ID to get tentative usage query for. - * - * @return string Query for tentative usages. - */ - private function get_tentative_usage_query( $coupon_id ) { - global $wpdb; - return $wpdb->prepare( - " - SELECT COUNT(meta_id) FROM $wpdb->postmeta - WHERE {$wpdb->postmeta}.meta_key like %s - AND {$wpdb->postmeta}.meta_key > %s - AND {$wpdb->postmeta}.post_id = %d - FOR UPDATE - ", - array( - '_coupon_held_%', - '_coupon_held_' . time(), - $coupon_id, - ) - ); // WPCS: unprepared SQL ok. - } - - /** - * Check and records coupon usage tentatively for passed user aliases for short period of time so that counts validation is correct. Returns early if there is no limit per user for the coupon. - * - * @param WC_Coupon $coupon Coupon object. - * @param array $user_aliases Emails or Ids to check for user. - * @param string $user_alias Email/ID to use as `used_by` value. - * - * @return null|false|int - */ - public function check_and_hold_coupon_for_user( $coupon, $user_aliases, $user_alias ) { - global $wpdb; - $limit_per_user = $coupon->get_usage_limit_per_user(); - $held_time = $this->get_tentative_held_time(); - - if ( 0 >= $limit_per_user || 0 >= $held_time ) { - // This coupon do not have any restriction for usage per customer. No need to check further, lets bail. - return null; - } - - if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', true ) ) { - return null; - } - - $format = implode( "','", array_fill( 0, count( $user_aliases ), '%s' ) ); - - $query_for_usages = $wpdb->prepare( - " - SELECT COUNT(*) FROM $wpdb->postmeta - WHERE {$wpdb->postmeta}.meta_key = '_used_by' - AND {$wpdb->postmeta}.meta_value IN ('$format') - AND {$wpdb->postmeta}.post_id = %d - FOR UPDATE - ", - array_merge( - $user_aliases, - array( $coupon->get_id() ) - ) - ); // WPCS: unprepared SQL ok. - - $query_for_tentative_usages = $this->get_tentative_usage_query_for_user( $coupon->get_id(), $user_aliases ); - $db_timestamp = $wpdb->get_var( 'SELECT UNIX_TIMESTAMP() FROM DUAL' ); - - $coupon_used_by_meta_key = '_maybe_used_by_' . ( (int) $db_timestamp + $held_time ) . '_' . wp_generate_password( 6, false ); - $insert_statement = $wpdb->prepare( - " - INSERT INTO $wpdb->postmeta ( post_id, meta_key, meta_value ) - SELECT %d, %s, %s FROM DUAL - WHERE ( $query_for_usages ) + ( $query_for_tentative_usages ) < %d - ", - $coupon->get_id(), - $coupon_used_by_meta_key, - $user_alias, - $limit_per_user - ); // WPCS: unprepared SQL ok. - - // This query can potentially be deadlocked if a combined index on post_id and meta_key is present and there is - // high concurrency, in which case DB will abort the query which has done less work to resolve deadlock. - // We will try up to 3 times before giving up. - for ( $count = 0; $count < 3; $count++ ) { - $result = $wpdb->query( $insert_statement ); // WPCS: unprepared SQL ok. - if ( false !== $result ) { - break; - } - } - - return $result > 0 ? $coupon_used_by_meta_key : $result; - } - - /** - * Generate query to calculate tentative usages for the coupon by the user. - * - * @param int $coupon_id Coupon ID. - * @param array $user_aliases List of user aliases to check for usages. - * - * @return string Tentative usages query. - */ - private function get_tentative_usage_query_for_user( $coupon_id, $user_aliases ) { - global $wpdb; - - $format = implode( "','", array_fill( 0, count( $user_aliases ), '%s' ) ); - - // Note that if you are debugging, `_maybe_used_by_%` will be converted to `_maybe_used_by_{...very long str...}` to very long string. This is expected, and is automatically corrected while running the insert query. - return $wpdb->prepare( - " - SELECT COUNT( meta_id ) FROM $wpdb->postmeta - WHERE {$wpdb->postmeta}.meta_key like %s - AND {$wpdb->postmeta}.meta_key > %s - AND {$wpdb->postmeta}.post_id = %d - AND {$wpdb->postmeta}.meta_value IN ('$format') - FOR UPDATE - ", - array_merge( - array( - '_maybe_used_by_%', - '_maybe_used_by_' . time(), - $coupon_id, - ), - $user_aliases - ) - ); // WPCS: unprepared SQL ok. - } - - /** - * Return a coupon code for a specific ID. - * - * @since 3.0.0 - * @param int $id Coupon ID. - * @return string Coupon Code - */ - public function get_code_by_id( $id ) { - global $wpdb; - return $wpdb->get_var( - $wpdb->prepare( - "SELECT post_title - FROM $wpdb->posts - WHERE ID = %d - AND post_type = 'shop_coupon' - AND post_status = 'publish'", - $id - ) - ); - } - - /** - * Return an array of IDs for for a specific coupon code. - * Can return multiple to check for existence. - * - * @since 3.0.0 - * @param string $code Coupon code. - * @return array Array of IDs. - */ - public function get_ids_by_code( $code ) { - global $wpdb; - return $wpdb->get_col( - $wpdb->prepare( - "SELECT ID FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC", - wc_sanitize_coupon_code( $code ) - ) - ); - } -} diff --git a/includes/data-stores/class-wc-customer-data-store-session.php b/includes/data-stores/class-wc-customer-data-store-session.php deleted file mode 100644 index 4a2585d20ca..00000000000 --- a/includes/data-stores/class-wc-customer-data-store-session.php +++ /dev/null @@ -1,197 +0,0 @@ -save_to_session( $customer ); - } - - /** - * Simply update the session. - * - * @param WC_Customer $customer Customer object. - */ - public function update( &$customer ) { - $this->save_to_session( $customer ); - } - - /** - * Saves all customer data to the session. - * - * @param WC_Customer $customer Customer object. - */ - public function save_to_session( $customer ) { - $data = array(); - foreach ( $this->session_keys as $session_key ) { - $function_key = $session_key; - if ( 'billing_' === substr( $session_key, 0, 8 ) ) { - $session_key = str_replace( 'billing_', '', $session_key ); - } - $data[ $session_key ] = (string) $customer->{"get_$function_key"}( 'edit' ); - } - WC()->session->set( 'customer', $data ); - } - - /** - * Read customer data from the session unless the user has logged in, in - * which case the stored ID will differ from the actual ID. - * - * @since 3.0.0 - * @param WC_Customer $customer Customer object. - */ - public function read( &$customer ) { - $data = (array) WC()->session->get( 'customer' ); - - /** - * There is a valid session if $data is not empty, and the ID matches the logged in user ID. - * - * If the user object has been updated since the session was created (based on date_modified) we should not load the session - data should be reloaded. - */ - if ( isset( $data['id'], $data['date_modified'] ) && $data['id'] === (string) $customer->get_id() && $data['date_modified'] === (string) $customer->get_date_modified( 'edit' ) ) { - foreach ( $this->session_keys as $session_key ) { - if ( in_array( $session_key, array( 'id', 'date_modified' ), true ) ) { - continue; - } - $function_key = $session_key; - if ( 'billing_' === substr( $session_key, 0, 8 ) ) { - $session_key = str_replace( 'billing_', '', $session_key ); - } - if ( isset( $data[ $session_key ] ) && is_callable( array( $customer, "set_{$function_key}" ) ) ) { - $customer->{"set_{$function_key}"}( wp_unslash( $data[ $session_key ] ) ); - } - } - } - $this->set_defaults( $customer ); - $customer->set_object_read( true ); - } - - /** - * Load default values if props are unset. - * - * @param WC_Customer $customer Customer object. - */ - protected function set_defaults( &$customer ) { - try { - $default = wc_get_customer_default_location(); - - if ( ! $customer->get_billing_country() ) { - $customer->set_billing_country( $default['country'] ); - } - - if ( ! $customer->get_shipping_country() ) { - $customer->set_shipping_country( $customer->get_billing_country() ); - } - - if ( ! $customer->get_billing_state() ) { - $customer->set_billing_state( $default['state'] ); - } - - if ( ! $customer->get_shipping_state() ) { - $customer->set_shipping_state( $customer->get_billing_state() ); - } - - if ( ! $customer->get_billing_email() && is_user_logged_in() ) { - $current_user = wp_get_current_user(); - $customer->set_billing_email( $current_user->user_email ); - } - } catch ( WC_Data_Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch - } - } - - /** - * Deletes a customer from the database. - * - * @since 3.0.0 - * @param WC_Customer $customer Customer object. - * @param array $args Array of args to pass to the delete method. - */ - public function delete( &$customer, $args = array() ) { - WC()->session->set( 'customer', null ); - } - - /** - * Gets the customers last order. - * - * @since 3.0.0 - * @param WC_Customer $customer Customer object. - * @return WC_Order|false - */ - public function get_last_order( &$customer ) { - return false; - } - - /** - * Return the number of orders this customer has. - * - * @since 3.0.0 - * @param WC_Customer $customer Customer object. - * @return integer - */ - public function get_order_count( &$customer ) { - return 0; - } - - /** - * Return how much money this customer has spent. - * - * @since 3.0.0 - * @param WC_Customer $customer Customer object. - * @return float - */ - public function get_total_spent( &$customer ) { - return 0; - } -} diff --git a/includes/data-stores/class-wc-customer-data-store.php b/includes/data-stores/class-wc-customer-data-store.php deleted file mode 100644 index 30694d86a12..00000000000 --- a/includes/data-stores/class-wc-customer-data-store.php +++ /dev/null @@ -1,527 +0,0 @@ -prefix ? $wpdb->prefix : 'wp_'; - - return ! in_array( $meta->meta_key, $this->internal_meta_keys, true ) - && 0 !== strpos( $meta->meta_key, '_woocommerce_persistent_cart' ) - && 0 !== strpos( $meta->meta_key, 'closedpostboxes_' ) - && 0 !== strpos( $meta->meta_key, 'metaboxhidden_' ) - && 0 !== strpos( $meta->meta_key, 'manageedit-' ) - && ! strstr( $meta->meta_key, $table_prefix ) - && 0 !== stripos( $meta->meta_key, 'wp_' ); - } - - /** - * Method to create a new customer in the database. - * - * @since 3.0.0 - * - * @param WC_Customer $customer Customer object. - * - * @throws WC_Data_Exception If unable to create new customer. - */ - public function create( &$customer ) { - $id = wc_create_new_customer( $customer->get_email(), $customer->get_username(), $customer->get_password() ); - - if ( is_wp_error( $id ) ) { - throw new WC_Data_Exception( $id->get_error_code(), $id->get_error_message() ); - } - - $customer->set_id( $id ); - $this->update_user_meta( $customer ); - - // Prevent wp_update_user calls in the same request and customer trigger the 'Notice of Password Changed' email. - $customer->set_password( '' ); - - wp_update_user( - apply_filters( - 'woocommerce_update_customer_args', - array( - 'ID' => $customer->get_id(), - 'role' => $customer->get_role(), - 'display_name' => $customer->get_display_name(), - ), - $customer - ) - ); - $wp_user = new WP_User( $customer->get_id() ); - $customer->set_date_created( $wp_user->user_registered ); - $customer->set_date_modified( get_user_meta( $customer->get_id(), 'last_update', true ) ); - $customer->save_meta_data(); - $customer->apply_changes(); - do_action( 'woocommerce_new_customer', $customer->get_id(), $customer ); - } - - /** - * Method to read a customer object. - * - * @since 3.0.0 - * @param WC_Customer $customer Customer object. - * @throws Exception If invalid customer. - */ - public function read( &$customer ) { - $user_object = $customer->get_id() ? get_user_by( 'id', $customer->get_id() ) : false; - - // User object is required. - if ( ! $user_object || empty( $user_object->ID ) ) { - throw new Exception( __( 'Invalid customer.', 'woocommerce' ) ); - } - - $customer_id = $customer->get_id(); - - // Load meta but exclude deprecated props and parent keys. - $user_meta = array_diff_key( - array_change_key_case( array_map( 'wc_flatten_meta_callback', get_user_meta( $customer_id ) ) ), - array_flip( array( 'country', 'state', 'postcode', 'city', 'address', 'address_2', 'default', 'location' ) ), - array_change_key_case( (array) $user_object->data ) - ); - - $customer->set_props( $user_meta ); - $customer->set_props( - array( - 'is_paying_customer' => get_user_meta( $customer_id, 'paying_customer', true ), - 'email' => $user_object->user_email, - 'username' => $user_object->user_login, - 'display_name' => $user_object->display_name, - 'date_created' => $user_object->user_registered, // Mysql string in local format. - 'date_modified' => get_user_meta( $customer_id, 'last_update', true ), - 'role' => ! empty( $user_object->roles[0] ) ? $user_object->roles[0] : 'customer', - ) - ); - $customer->read_meta_data(); - $customer->set_object_read( true ); - do_action( 'woocommerce_customer_loaded', $customer ); - } - - /** - * Updates a customer in the database. - * - * @since 3.0.0 - * @param WC_Customer $customer Customer object. - */ - public function update( &$customer ) { - wp_update_user( - apply_filters( - 'woocommerce_update_customer_args', - array( - 'ID' => $customer->get_id(), - 'user_email' => $customer->get_email(), - 'display_name' => $customer->get_display_name(), - ), - $customer - ) - ); - - // Only update password if a new one was set with set_password. - if ( $customer->get_password() ) { - wp_update_user( - array( - 'ID' => $customer->get_id(), - 'user_pass' => $customer->get_password(), - ) - ); - $customer->set_password( '' ); - } - - $this->update_user_meta( $customer ); - $customer->set_date_modified( get_user_meta( $customer->get_id(), 'last_update', true ) ); - $customer->save_meta_data(); - $customer->apply_changes(); - do_action( 'woocommerce_update_customer', $customer->get_id(), $customer ); - } - - /** - * Deletes a customer from the database. - * - * @since 3.0.0 - * @param WC_Customer $customer Customer object. - * @param array $args Array of args to pass to the delete method. - */ - public function delete( &$customer, $args = array() ) { - if ( ! $customer->get_id() ) { - return; - } - - $args = wp_parse_args( - $args, - array( - 'reassign' => 0, - ) - ); - - $id = $customer->get_id(); - wp_delete_user( $id, $args['reassign'] ); - - do_action( 'woocommerce_delete_customer', $id ); - } - - /** - * Helper method that updates all the meta for a customer. Used for update & create. - * - * @since 3.0.0 - * @param WC_Customer $customer Customer object. - */ - private function update_user_meta( $customer ) { - $updated_props = array(); - $changed_props = $customer->get_changes(); - - $meta_key_to_props = array( - 'paying_customer' => 'is_paying_customer', - 'first_name' => 'first_name', - 'last_name' => 'last_name', - ); - - foreach ( $meta_key_to_props as $meta_key => $prop ) { - if ( ! array_key_exists( $prop, $changed_props ) ) { - continue; - } - - if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) { - $updated_props[] = $prop; - } - } - - $billing_address_props = array( - 'billing_first_name' => 'billing_first_name', - 'billing_last_name' => 'billing_last_name', - 'billing_company' => 'billing_company', - 'billing_address_1' => 'billing_address_1', - 'billing_address_2' => 'billing_address_2', - 'billing_city' => 'billing_city', - 'billing_state' => 'billing_state', - 'billing_postcode' => 'billing_postcode', - 'billing_country' => 'billing_country', - 'billing_email' => 'billing_email', - 'billing_phone' => 'billing_phone', - ); - - foreach ( $billing_address_props as $meta_key => $prop ) { - $prop_key = substr( $prop, 8 ); - - if ( ! isset( $changed_props['billing'] ) || ! array_key_exists( $prop_key, $changed_props['billing'] ) ) { - continue; - } - - if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) { - $updated_props[] = $prop; - } - } - - $shipping_address_props = array( - 'shipping_first_name' => 'shipping_first_name', - 'shipping_last_name' => 'shipping_last_name', - 'shipping_company' => 'shipping_company', - 'shipping_address_1' => 'shipping_address_1', - 'shipping_address_2' => 'shipping_address_2', - 'shipping_city' => 'shipping_city', - 'shipping_state' => 'shipping_state', - 'shipping_postcode' => 'shipping_postcode', - 'shipping_country' => 'shipping_country', - ); - - foreach ( $shipping_address_props as $meta_key => $prop ) { - $prop_key = substr( $prop, 9 ); - - if ( ! isset( $changed_props['shipping'] ) || ! array_key_exists( $prop_key, $changed_props['shipping'] ) ) { - continue; - } - - if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) { - $updated_props[] = $prop; - } - } - - do_action( 'woocommerce_customer_object_updated_props', $customer, $updated_props ); - } - - /** - * Gets the customers last order. - * - * @since 3.0.0 - * @param WC_Customer $customer Customer object. - * @return WC_Order|false - */ - public function get_last_order( &$customer ) { - $last_order = apply_filters( - 'woocommerce_customer_get_last_order', - get_user_meta( $customer->get_id(), '_last_order', true ), - $customer - ); - - if ( '' === $last_order ) { - global $wpdb; - - $last_order = $wpdb->get_var( - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared - "SELECT posts.ID - FROM $wpdb->posts AS posts - LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id - WHERE meta.meta_key = '_customer_user' - AND meta.meta_value = '" . esc_sql( $customer->get_id() ) . "' - AND posts.post_type = 'shop_order' - AND posts.post_status IN ( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' ) - ORDER BY posts.ID DESC" - // phpcs:enable - ); - update_user_meta( $customer->get_id(), '_last_order', $last_order ); - } - - if ( ! $last_order ) { - return false; - } - - return wc_get_order( absint( $last_order ) ); - } - - /** - * Return the number of orders this customer has. - * - * @since 3.0.0 - * @param WC_Customer $customer Customer object. - * @return integer - */ - public function get_order_count( &$customer ) { - $count = apply_filters( - 'woocommerce_customer_get_order_count', - get_user_meta( $customer->get_id(), '_order_count', true ), - $customer - ); - - if ( '' === $count ) { - global $wpdb; - - $count = $wpdb->get_var( - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared - "SELECT COUNT(*) - FROM $wpdb->posts as posts - LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id - WHERE meta.meta_key = '_customer_user' - AND posts.post_type = 'shop_order' - AND posts.post_status IN ( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' ) - AND meta_value = '" . esc_sql( $customer->get_id() ) . "'" - // phpcs:enable - ); - update_user_meta( $customer->get_id(), '_order_count', $count ); - } - - return absint( $count ); - } - - /** - * Return how much money this customer has spent. - * - * @since 3.0.0 - * @param WC_Customer $customer Customer object. - * @return float - */ - public function get_total_spent( &$customer ) { - $spent = apply_filters( - 'woocommerce_customer_get_total_spent', - get_user_meta( $customer->get_id(), '_money_spent', true ), - $customer - ); - - if ( '' === $spent ) { - global $wpdb; - - $statuses = array_map( 'esc_sql', wc_get_is_paid_statuses() ); - $spent = $wpdb->get_var( - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared - apply_filters( - 'woocommerce_customer_get_total_spent_query', - "SELECT SUM(meta2.meta_value) - FROM $wpdb->posts as posts - LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id - LEFT JOIN {$wpdb->postmeta} AS meta2 ON posts.ID = meta2.post_id - WHERE meta.meta_key = '_customer_user' - AND meta.meta_value = '" . esc_sql( $customer->get_id() ) . "' - AND posts.post_type = 'shop_order' - AND posts.post_status IN ( 'wc-" . implode( "','wc-", $statuses ) . "' ) - AND meta2.meta_key = '_order_total'", - $customer - ) - // phpcs:enable - ); - - if ( ! $spent ) { - $spent = 0; - } - update_user_meta( $customer->get_id(), '_money_spent', $spent ); - } - - return wc_format_decimal( $spent, 2 ); - } - - /** - * Search customers and return customer IDs. - * - * @param string $term Search term. - * @param int|string $limit Limit search results. - * @since 3.0.7 - * - * @return array - */ - public function search_customers( $term, $limit = '' ) { - $results = apply_filters( 'woocommerce_customer_pre_search_customers', false, $term, $limit ); - if ( is_array( $results ) ) { - return $results; - } - - $query = new WP_User_Query( - apply_filters( - 'woocommerce_customer_search_customers', - array( - 'search' => '*' . esc_attr( $term ) . '*', - 'search_columns' => array( 'user_login', 'user_url', 'user_email', 'user_nicename', 'display_name' ), - 'fields' => 'ID', - 'number' => $limit, - ), - $term, - $limit, - 'main_query' - ) - ); - - $query2 = new WP_User_Query( - apply_filters( - 'woocommerce_customer_search_customers', - array( - 'fields' => 'ID', - 'number' => $limit, - 'meta_query' => array( - 'relation' => 'OR', - array( - 'key' => 'first_name', - 'value' => $term, - 'compare' => 'LIKE', - ), - array( - 'key' => 'last_name', - 'value' => $term, - 'compare' => 'LIKE', - ), - ), - ), - $term, - $limit, - 'meta_query' - ) - ); - - $results = wp_parse_id_list( array_merge( (array) $query->get_results(), (array) $query2->get_results() ) ); - - if ( $limit && count( $results ) > $limit ) { - $results = array_slice( $results, 0, $limit ); - } - - return $results; - } - - /** - * Get all user ids who have `billing_email` set to any of the email passed in array. - * - * @param array $emails List of emails to check against. - * - * @return array - */ - public function get_user_ids_for_billing_email( $emails ) { - $emails = array_unique( array_map( 'strtolower', array_map( 'sanitize_email', $emails ) ) ); - $users_query = new WP_User_Query( - array( - 'fields' => 'ID', - 'meta_query' => array( - array( - 'key' => 'billing_email', - 'value' => $emails, - 'compare' => 'IN', - ), - ), - ) - ); - return array_unique( $users_query->get_results() ); - } -} diff --git a/includes/data-stores/class-wc-customer-download-data-store.php b/includes/data-stores/class-wc-customer-download-data-store.php deleted file mode 100644 index f715fc6d3cb..00000000000 --- a/includes/data-stores/class-wc-customer-download-data-store.php +++ /dev/null @@ -1,521 +0,0 @@ -insert_new_download_permission( $data ); - - do_action( 'woocommerce_grant_product_download_access', $data ); - - return $id; - } - - /** - * Create download permission for a user. - * - * @param WC_Customer_Download $download WC_Customer_Download object. - */ - public function create( &$download ) { - global $wpdb; - - // Always set a access granted date. - if ( is_null( $download->get_access_granted( 'edit' ) ) ) { - $download->set_access_granted( time() ); - } - - $data = array(); - foreach ( self::DOWNLOAD_PERMISSION_DB_FIELDS as $db_field_name ) { - $value = call_user_func( array( $download, 'get_' . $db_field_name ), 'edit' ); - $data[ $db_field_name ] = $value; - } - - $inserted_id = $this->insert_new_download_permission( $data ); - if ( $inserted_id ) { - $download->set_id( $inserted_id ); - $download->apply_changes(); - } - - do_action( 'woocommerce_grant_product_download_access', $data ); - } - - /** - * Create download permission for a user, from an array of data. - * Assumes that all the keys in the passed data are valid. - * - * @param array $data Data to create the permission for. - * @return int The database id of the created permission, or false if the permission creation failed. - */ - private function insert_new_download_permission( $data ) { - global $wpdb; - - // Always set a access granted date. - if ( ! isset( $data['access_granted'] ) ) { - $data['access_granted'] = time(); - } - - $data['access_granted'] = $this->adjust_date_for_db( $data['access_granted'] ); - - if ( isset( $data['access_expires'] ) ) { - $data['access_expires'] = $this->adjust_date_for_db( $data['access_expires'] ); - } - - $format = array( - '%s', - '%s', - '%s', - '%s', - '%s', - '%s', - '%s', - '%s', - '%d', - '%s', - ); - - $result = $wpdb->insert( - $wpdb->prefix . 'woocommerce_downloadable_product_permissions', - apply_filters( 'woocommerce_downloadable_file_permission_data', $data ), - apply_filters( 'woocommerce_downloadable_file_permission_format', $format, $data ) - ); - - return $result ? $wpdb->insert_id : false; - } - - /** - * Adjust a date value to be inserted in the database. - * - * @param mixed $date The date value. Can be a WC_DateTime, a timestamp, or anything else that "date" recognizes. - * @return string The date converted to 'Y-m-d' format. - * @throws Exception The passed value can't be converted to a date. - */ - private function adjust_date_for_db( $date ) { - if ( 'WC_DateTime' === get_class( $date ) ) { - $date = $date->getTimestamp(); - } - - $adjusted_date = date( 'Y-m-d', $date ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date - - if ( $adjusted_date ) { - return $adjusted_date; - } - - $msg = sprintf( __( "I don't know how to get a date from a %s", 'woocommerce' ), is_object( $date ) ? get_class( $date ) : gettype( $date ) ); - throw new Exception( $msg ); - } - - /** - * Method to read a download permission from the database. - * - * @param WC_Customer_Download $download WC_Customer_Download object. - * - * @throws Exception Throw exception if invalid download is passed. - */ - public function read( &$download ) { - global $wpdb; - - if ( ! $download->get_id() ) { - throw new Exception( __( 'Invalid download.', 'woocommerce' ) ); - } - - $download->set_defaults(); - $raw_download = $wpdb->get_row( - $wpdb->prepare( - "SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE permission_id = %d", - $download->get_id() - ) - ); - - if ( ! $raw_download ) { - throw new Exception( __( 'Invalid download.', 'woocommerce' ) ); - } - - $download->set_props( - array( - 'download_id' => $raw_download->download_id, - 'product_id' => $raw_download->product_id, - 'user_id' => $raw_download->user_id, - 'user_email' => $raw_download->user_email, - 'order_id' => $raw_download->order_id, - 'order_key' => $raw_download->order_key, - 'downloads_remaining' => $raw_download->downloads_remaining, - 'access_granted' => strtotime( $raw_download->access_granted ), - 'download_count' => $raw_download->download_count, - 'access_expires' => is_null( $raw_download->access_expires ) ? null : strtotime( $raw_download->access_expires ), - ) - ); - $download->set_object_read( true ); - } - - /** - * Method to update a download in the database. - * - * @param WC_Customer_Download $download WC_Customer_Download object. - */ - public function update( &$download ) { - global $wpdb; - - $data = array( - 'download_id' => $download->get_download_id( 'edit' ), - 'product_id' => $download->get_product_id( 'edit' ), - 'user_id' => $download->get_user_id( 'edit' ), - 'user_email' => $download->get_user_email( 'edit' ), - 'order_id' => $download->get_order_id( 'edit' ), - 'order_key' => $download->get_order_key( 'edit' ), - 'downloads_remaining' => $download->get_downloads_remaining( 'edit' ), - // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date - 'access_granted' => date( 'Y-m-d', $download->get_access_granted( 'edit' )->getTimestamp() ), - 'download_count' => $download->get_download_count( 'edit' ), - // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date - 'access_expires' => ! is_null( $download->get_access_expires( 'edit' ) ) ? date( 'Y-m-d', $download->get_access_expires( 'edit' )->getTimestamp() ) : null, - ); - - $format = array( - '%s', - '%s', - '%s', - '%s', - '%s', - '%s', - '%s', - '%s', - '%d', - '%s', - ); - - $wpdb->update( - $wpdb->prefix . 'woocommerce_downloadable_product_permissions', - $data, - array( - 'permission_id' => $download->get_id(), - ), - $format - ); - $download->apply_changes(); - } - - /** - * Method to delete a download permission from the database. - * - * @param WC_Customer_Download $download WC_Customer_Download object. - * @param array $args Array of args to pass to the delete method. - */ - public function delete( &$download, $args = array() ) { - global $wpdb; - - $wpdb->query( - $wpdb->prepare( - "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions - WHERE permission_id = %d", - $download->get_id() - ) - ); - - $download->set_id( 0 ); - } - - /** - * Method to delete a download permission from the database by ID. - * - * @param int $id permission_id of the download to be deleted. - */ - public function delete_by_id( $id ) { - global $wpdb; - $wpdb->query( - $wpdb->prepare( - "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions - WHERE permission_id = %d", - $id - ) - ); - } - - /** - * Method to delete a download permission from the database by order ID. - * - * @param int $id Order ID of the downloads that will be deleted. - */ - public function delete_by_order_id( $id ) { - global $wpdb; - $wpdb->query( - $wpdb->prepare( - "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions - WHERE order_id = %d", - $id - ) - ); - } - - /** - * Method to delete a download permission from the database by download ID. - * - * @param int $id download_id of the downloads that will be deleted. - */ - public function delete_by_download_id( $id ) { - global $wpdb; - $wpdb->query( - $wpdb->prepare( - "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions - WHERE download_id = %s", - $id - ) - ); - } - - /** - * Method to delete a download permission from the database by user ID. - * - * @since 3.4.0 - * @param int $id user ID of the downloads that will be deleted. - * @return bool True if deleted rows. - */ - public function delete_by_user_id( $id ) { - global $wpdb; - return (bool) $wpdb->query( - $wpdb->prepare( - "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions - WHERE user_id = %d", - $id - ) - ); - } - - /** - * Method to delete a download permission from the database by user email. - * - * @since 3.4.0 - * @param string $email email of the downloads that will be deleted. - * @return bool True if deleted rows. - */ - public function delete_by_user_email( $email ) { - global $wpdb; - return (bool) $wpdb->query( - $wpdb->prepare( - "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions - WHERE user_email = %s", - $email - ) - ); - } - - /** - * Get a download object. - * - * @param array $data From the DB. - * @return WC_Customer_Download - */ - private function get_download( $data ) { - return new WC_Customer_Download( $data ); - } - - /** - * Get array of download ids by specified args. - * - * @param array $args Arguments to filter downloads. $args['return'] accepts the following values: 'objects' (default), 'ids' or a comma separeted list of fields (for example: 'order_id,user_id,user_email'). - * @return array Can be an array of permission_ids, an array of WC_Customer_Download objects or an array of arrays containing specified fields depending on the value of $args['return']. - */ - public function get_downloads( $args = array() ) { - global $wpdb; - - $args = wp_parse_args( - $args, - array( - 'user_email' => '', - 'user_id' => '', - 'order_id' => '', - 'order_key' => '', - 'product_id' => '', - 'download_id' => '', - 'orderby' => 'permission_id', - 'order' => 'ASC', - 'limit' => -1, - 'page' => 1, - 'return' => 'objects', - ) - ); - - $valid_fields = array( 'permission_id', 'download_id', 'product_id', 'order_id', 'order_key', 'user_email', 'user_id', 'downloads_remaining', 'access_granted', 'access_expires', 'download_count' ); - $get_results_output = ARRAY_A; - - if ( 'ids' === $args['return'] ) { - $fields = 'permission_id'; - } elseif ( 'objects' === $args['return'] ) { - $fields = '*'; - $get_results_output = OBJECT; - } else { - $fields = explode( ',', (string) $args['return'] ); - $fields = implode( ', ', array_intersect( $fields, $valid_fields ) ); - } - - $query = array(); - $query[] = "SELECT {$fields} FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE 1=1"; - - if ( $args['user_email'] ) { - $query[] = $wpdb->prepare( 'AND user_email = %s', sanitize_email( $args['user_email'] ) ); - } - - if ( $args['user_id'] ) { - $query[] = $wpdb->prepare( 'AND user_id = %d', absint( $args['user_id'] ) ); - } - - if ( $args['order_id'] ) { - $query[] = $wpdb->prepare( 'AND order_id = %d', $args['order_id'] ); - } - - if ( $args['order_key'] ) { - $query[] = $wpdb->prepare( 'AND order_key = %s', $args['order_key'] ); - } - - if ( $args['product_id'] ) { - $query[] = $wpdb->prepare( 'AND product_id = %d', $args['product_id'] ); - } - - if ( $args['download_id'] ) { - $query[] = $wpdb->prepare( 'AND download_id = %s', $args['download_id'] ); - } - - $orderby = in_array( $args['orderby'], $valid_fields, true ) ? $args['orderby'] : 'permission_id'; - $order = 'DESC' === strtoupper( $args['order'] ) ? 'DESC' : 'ASC'; - $orderby_sql = sanitize_sql_orderby( "{$orderby} {$order}" ); - $query[] = "ORDER BY {$orderby_sql}"; - - if ( 0 < $args['limit'] ) { - $query[] = $wpdb->prepare( 'LIMIT %d, %d', absint( $args['limit'] ) * absint( $args['page'] - 1 ), absint( $args['limit'] ) ); - } - - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $results = $wpdb->get_results( implode( ' ', $query ), $get_results_output ); - - switch ( $args['return'] ) { - case 'ids': - return wp_list_pluck( $results, 'permission_id' ); - case 'objects': - return array_map( array( $this, 'get_download' ), $results ); - default: - return $results; - } - } - - /** - * Update download ids if the hash changes. - * - * @deprecated 3.3.0 Download id is now a static UUID and should not be changed based on file hash. - * - * @param int $product_id Product ID. - * @param string $old_id Old download_id. - * @param string $new_id New download_id. - */ - public function update_download_id( $product_id, $old_id, $new_id ) { - global $wpdb; - - wc_deprecated_function( __METHOD__, '3.3' ); - - $wpdb->update( - $wpdb->prefix . 'woocommerce_downloadable_product_permissions', - array( - 'download_id' => $new_id, - ), - array( - 'download_id' => $old_id, - 'product_id' => $product_id, - ) - ); - } - - /** - * Get a customers downloads. - * - * @param int $customer_id Customer ID. - * @return array - */ - public function get_downloads_for_customer( $customer_id ) { - global $wpdb; - - return $wpdb->get_results( - $wpdb->prepare( - "SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions as permissions - WHERE user_id = %d - AND permissions.order_id > 0 - AND - ( - permissions.downloads_remaining > 0 - OR permissions.downloads_remaining = '' - ) - AND - ( - permissions.access_expires IS NULL - OR permissions.access_expires >= %s - OR permissions.access_expires = '0000-00-00 00:00:00' - ) - ORDER BY permissions.order_id, permissions.product_id, permissions.permission_id;", - $customer_id, - date( 'Y-m-d', current_time( 'timestamp' ) ) // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date - ) - ); - } - - /** - * Update user prop for downloads based on order id. - * - * @param int $order_id Order ID. - * @param int $customer_id Customer ID. - * @param string $email Customer email address. - */ - public function update_user_by_order_id( $order_id, $customer_id, $email ) { - global $wpdb; - $wpdb->update( - $wpdb->prefix . 'woocommerce_downloadable_product_permissions', - array( - 'user_id' => $customer_id, - 'user_email' => $email, - ), - array( - 'order_id' => $order_id, - ), - array( - '%d', - '%s', - ), - array( - '%d', - ) - ); - } -} diff --git a/includes/data-stores/class-wc-order-data-store-cpt.php b/includes/data-stores/class-wc-order-data-store-cpt.php deleted file mode 100644 index 0a80dddb540..00000000000 --- a/includes/data-stores/class-wc-order-data-store-cpt.php +++ /dev/null @@ -1,1114 +0,0 @@ -get_order_key() ) { - $order->set_order_key( wc_generate_order_key() ); - } - parent::create( $order ); - do_action( 'woocommerce_new_order', $order->get_id(), $order ); - } - - /** - * Read order data. Can be overridden by child classes to load other props. - * - * @param WC_Order $order Order object. - * @param object $post_object Post object. - * @since 3.0.0 - */ - protected function read_order_data( &$order, $post_object ) { - parent::read_order_data( $order, $post_object ); - $id = $order->get_id(); - $date_completed = get_post_meta( $id, '_date_completed', true ); - $date_paid = get_post_meta( $id, '_date_paid', true ); - - if ( ! $date_completed ) { - $date_completed = get_post_meta( $id, '_completed_date', true ); - } - - if ( ! $date_paid ) { - $date_paid = get_post_meta( $id, '_paid_date', true ); - } - - $order->set_props( - array( - 'order_key' => get_post_meta( $id, '_order_key', true ), - 'customer_id' => get_post_meta( $id, '_customer_user', true ), - 'billing_first_name' => get_post_meta( $id, '_billing_first_name', true ), - 'billing_last_name' => get_post_meta( $id, '_billing_last_name', true ), - 'billing_company' => get_post_meta( $id, '_billing_company', true ), - 'billing_address_1' => get_post_meta( $id, '_billing_address_1', true ), - 'billing_address_2' => get_post_meta( $id, '_billing_address_2', true ), - 'billing_city' => get_post_meta( $id, '_billing_city', true ), - 'billing_state' => get_post_meta( $id, '_billing_state', true ), - 'billing_postcode' => get_post_meta( $id, '_billing_postcode', true ), - 'billing_country' => get_post_meta( $id, '_billing_country', true ), - 'billing_email' => get_post_meta( $id, '_billing_email', true ), - 'billing_phone' => get_post_meta( $id, '_billing_phone', true ), - 'shipping_first_name' => get_post_meta( $id, '_shipping_first_name', true ), - 'shipping_last_name' => get_post_meta( $id, '_shipping_last_name', true ), - 'shipping_company' => get_post_meta( $id, '_shipping_company', true ), - 'shipping_address_1' => get_post_meta( $id, '_shipping_address_1', true ), - 'shipping_address_2' => get_post_meta( $id, '_shipping_address_2', true ), - 'shipping_city' => get_post_meta( $id, '_shipping_city', true ), - 'shipping_state' => get_post_meta( $id, '_shipping_state', true ), - 'shipping_postcode' => get_post_meta( $id, '_shipping_postcode', true ), - 'shipping_country' => get_post_meta( $id, '_shipping_country', true ), - 'payment_method' => get_post_meta( $id, '_payment_method', true ), - 'payment_method_title' => get_post_meta( $id, '_payment_method_title', true ), - 'transaction_id' => get_post_meta( $id, '_transaction_id', true ), - 'customer_ip_address' => get_post_meta( $id, '_customer_ip_address', true ), - 'customer_user_agent' => get_post_meta( $id, '_customer_user_agent', true ), - 'created_via' => get_post_meta( $id, '_created_via', true ), - 'date_completed' => $date_completed, - 'date_paid' => $date_paid, - 'cart_hash' => get_post_meta( $id, '_cart_hash', true ), - 'customer_note' => $post_object->post_excerpt, - ) - ); - } - - /** - * Method to update an order in the database. - * - * @param WC_Order $order Order object. - */ - public function update( &$order ) { - // Before updating, ensure date paid is set if missing. - if ( ! $order->get_date_paid( 'edit' ) && version_compare( $order->get_version( 'edit' ), '3.0', '<' ) && $order->has_status( apply_filters( 'woocommerce_payment_complete_order_status', $order->needs_processing() ? 'processing' : 'completed', $order->get_id(), $order ) ) ) { - $order->set_date_paid( $order->get_date_created( 'edit' ) ); - } - - // Also grab the current status so we can compare. - $previous_status = get_post_status( $order->get_id() ); - - // Update the order. - parent::update( $order ); - - // Fire a hook depending on the status - this should be considered a creation if it was previously draft status. - $new_status = $order->get_status( 'edit' ); - - if ( $new_status !== $previous_status && in_array( $previous_status, array( 'new', 'auto-draft', 'draft' ), true ) ) { - do_action( 'woocommerce_new_order', $order->get_id(), $order ); - } else { - do_action( 'woocommerce_update_order', $order->get_id(), $order ); - } - } - - /** - * Helper method that updates all the post meta for an order based on it's settings in the WC_Order class. - * - * @param WC_Order $order Order object. - * @since 3.0.0 - */ - protected function update_post_meta( &$order ) { - $updated_props = array(); - $id = $order->get_id(); - $meta_key_to_props = array( - '_order_key' => 'order_key', - '_customer_user' => 'customer_id', - '_payment_method' => 'payment_method', - '_payment_method_title' => 'payment_method_title', - '_transaction_id' => 'transaction_id', - '_customer_ip_address' => 'customer_ip_address', - '_customer_user_agent' => 'customer_user_agent', - '_created_via' => 'created_via', - '_date_completed' => 'date_completed', - '_date_paid' => 'date_paid', - '_cart_hash' => 'cart_hash', - ); - - $props_to_update = $this->get_props_to_update( $order, $meta_key_to_props ); - - foreach ( $props_to_update as $meta_key => $prop ) { - $value = $order->{"get_$prop"}( 'edit' ); - $value = is_string( $value ) ? wp_slash( $value ) : $value; - switch ( $prop ) { - case 'date_paid': - case 'date_completed': - $value = ! is_null( $value ) ? $value->getTimestamp() : ''; - break; - } - - $updated = $this->update_or_delete_post_meta( $order, $meta_key, $value ); - - if ( $updated ) { - $updated_props[] = $prop; - } - } - - $address_props = array( - 'billing' => array( - '_billing_first_name' => 'billing_first_name', - '_billing_last_name' => 'billing_last_name', - '_billing_company' => 'billing_company', - '_billing_address_1' => 'billing_address_1', - '_billing_address_2' => 'billing_address_2', - '_billing_city' => 'billing_city', - '_billing_state' => 'billing_state', - '_billing_postcode' => 'billing_postcode', - '_billing_country' => 'billing_country', - '_billing_email' => 'billing_email', - '_billing_phone' => 'billing_phone', - ), - 'shipping' => array( - '_shipping_first_name' => 'shipping_first_name', - '_shipping_last_name' => 'shipping_last_name', - '_shipping_company' => 'shipping_company', - '_shipping_address_1' => 'shipping_address_1', - '_shipping_address_2' => 'shipping_address_2', - '_shipping_city' => 'shipping_city', - '_shipping_state' => 'shipping_state', - '_shipping_postcode' => 'shipping_postcode', - '_shipping_country' => 'shipping_country', - ), - ); - - foreach ( $address_props as $props_key => $props ) { - $props_to_update = $this->get_props_to_update( $order, $props ); - foreach ( $props_to_update as $meta_key => $prop ) { - $value = $order->{"get_$prop"}( 'edit' ); - $value = is_string( $value ) ? wp_slash( $value ) : $value; - $updated = $this->update_or_delete_post_meta( $order, $meta_key, $value ); - - if ( $updated ) { - $updated_props[] = $prop; - $updated_props[] = $props_key; - } - } - } - - parent::update_post_meta( $order ); - - // If address changed, store concatenated version to make searches faster. - if ( in_array( 'billing', $updated_props, true ) || ! metadata_exists( 'post', $id, '_billing_address_index' ) ) { - update_post_meta( $id, '_billing_address_index', implode( ' ', $order->get_address( 'billing' ) ) ); - } - if ( in_array( 'shipping', $updated_props, true ) || ! metadata_exists( 'post', $id, '_shipping_address_index' ) ) { - update_post_meta( $id, '_shipping_address_index', implode( ' ', $order->get_address( 'shipping' ) ) ); - } - - // Legacy date handling. @todo remove in 4.0. - if ( in_array( 'date_paid', $updated_props, true ) ) { - $value = $order->get_date_paid( 'edit' ); - // In 2.6.x date_paid was stored as _paid_date in local mysql format. - update_post_meta( $id, '_paid_date', ! is_null( $value ) ? $value->date( 'Y-m-d H:i:s' ) : '' ); - } - - if ( in_array( 'date_completed', $updated_props, true ) ) { - $value = $order->get_date_completed( 'edit' ); - // In 2.6.x date_completed was stored as _completed_date in local mysql format. - update_post_meta( $id, '_completed_date', ! is_null( $value ) ? $value->date( 'Y-m-d H:i:s' ) : '' ); - } - - // If customer changed, update any downloadable permissions. - if ( in_array( 'customer_id', $updated_props ) || in_array( 'billing_email', $updated_props ) ) { - $data_store = WC_Data_Store::load( 'customer-download' ); - $data_store->update_user_by_order_id( $id, $order->get_customer_id(), $order->get_billing_email() ); - } - - // Mark user account as active. - if ( in_array( 'customer_id', $updated_props, true ) ) { - wc_update_user_last_active( $order->get_customer_id() ); - } - - do_action( 'woocommerce_order_object_updated_props', $order, $updated_props ); - } - - /** - * Excerpt for post. - * - * @param WC_Order $order Order object. - * @return string - */ - protected function get_post_excerpt( $order ) { - return $order->get_customer_note(); - } - - /** - * Get order key. - * - * @since 4.3.0 - * @param WC_order $order Order object. - * @return string - */ - protected function get_order_key( $order ) { - if ( '' !== $order->get_order_key() ) { - return $order->get_order_key(); - } - - return parent::get_order_key( $order ); - } - - /** - * Get amount already refunded. - * - * @param WC_Order $order Order object. - * @return float - */ - public function get_total_refunded( $order ) { - global $wpdb; - - $total = $wpdb->get_var( - $wpdb->prepare( - "SELECT SUM( postmeta.meta_value ) - FROM $wpdb->postmeta AS postmeta - INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) - WHERE postmeta.meta_key = '_refund_amount' - AND postmeta.post_id = posts.ID", - $order->get_id() - ) - ); - - return floatval( $total ); - } - - /** - * Get the total tax refunded. - * - * @param WC_Order $order Order object. - * @return float - */ - public function get_total_tax_refunded( $order ) { - global $wpdb; - - $total = $wpdb->get_var( - $wpdb->prepare( - "SELECT SUM( order_itemmeta.meta_value ) - FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta - INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) - INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'tax' ) - WHERE order_itemmeta.order_item_id = order_items.order_item_id - AND order_itemmeta.meta_key IN ('tax_amount', 'shipping_tax_amount')", - $order->get_id() - ) - ); - - return abs( $total ); - } - - /** - * Get the total shipping refunded. - * - * @param WC_Order $order Order object. - * @return float - */ - public function get_total_shipping_refunded( $order ) { - global $wpdb; - - $total = $wpdb->get_var( - $wpdb->prepare( - "SELECT SUM( order_itemmeta.meta_value ) - FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta - INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) - INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'shipping' ) - WHERE order_itemmeta.order_item_id = order_items.order_item_id - AND order_itemmeta.meta_key IN ('cost')", - $order->get_id() - ) - ); - - return abs( $total ); - } - - /** - * Finds an Order ID based on an order key. - * - * @param string $order_key An order key has generated by. - * @return int The ID of an order, or 0 if the order could not be found - */ - public function get_order_id_by_order_key( $order_key ) { - global $wpdb; - return $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->prefix}postmeta WHERE meta_key = '_order_key' AND meta_value = %s", $order_key ) ); - } - - /** - * Return count of orders with a specific status. - * - * @param string $status Order status. Function wc_get_order_statuses() returns a list of valid statuses. - * @return int - */ - public function get_order_count( $status ) { - global $wpdb; - return absint( $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( * ) FROM {$wpdb->posts} WHERE post_type = 'shop_order' AND post_status = %s", $status ) ) ); - } - - /** - * Get all orders matching the passed in args. - * - * @deprecated 3.1.0 - Use wc_get_orders instead. - * @see wc_get_orders() - * - * @param array $args List of args passed to wc_get_orders(). - * - * @return array|object - */ - public function get_orders( $args = array() ) { - wc_deprecated_function( 'WC_Order_Data_Store_CPT::get_orders', '3.1.0', 'Use wc_get_orders instead.' ); - return wc_get_orders( $args ); - } - - /** - * Generate meta query for wc_get_orders. - * - * @param array $values List of customers ids or emails. - * @param string $relation 'or' or 'and' relation used to build the WP meta_query. - * @return array - */ - private function get_orders_generate_customer_meta_query( $values, $relation = 'or' ) { - $meta_query = array( - 'relation' => strtoupper( $relation ), - 'customer_emails' => array( - 'key' => '_billing_email', - 'value' => array(), - 'compare' => 'IN', - ), - 'customer_ids' => array( - 'key' => '_customer_user', - 'value' => array(), - 'compare' => 'IN', - ), - ); - foreach ( $values as $value ) { - if ( is_array( $value ) ) { - $query_part = $this->get_orders_generate_customer_meta_query( $value, 'and' ); - if ( is_wp_error( $query_part ) ) { - return $query_part; - } - $meta_query[] = $query_part; - } elseif ( is_email( $value ) ) { - $meta_query['customer_emails']['value'][] = sanitize_email( $value ); - } elseif ( is_numeric( $value ) ) { - $meta_query['customer_ids']['value'][] = strval( absint( $value ) ); - } else { - return new WP_Error( 'woocommerce_query_invalid', __( 'Invalid customer query.', 'woocommerce' ), $values ); - } - } - - if ( empty( $meta_query['customer_emails']['value'] ) ) { - unset( $meta_query['customer_emails'] ); - unset( $meta_query['relation'] ); - } - - if ( empty( $meta_query['customer_ids']['value'] ) ) { - unset( $meta_query['customer_ids'] ); - unset( $meta_query['relation'] ); - } - - return $meta_query; - } - - /** - * Get unpaid orders after a certain date, - * - * @param int $date Timestamp. - * @return array - */ - public function get_unpaid_orders( $date ) { - global $wpdb; - - $unpaid_orders = $wpdb->get_col( - $wpdb->prepare( - // @codingStandardsIgnoreStart - "SELECT posts.ID - FROM {$wpdb->posts} AS posts - WHERE posts.post_type IN ('" . implode( "','", wc_get_order_types() ) . "') - AND posts.post_status = 'wc-pending' - AND posts.post_modified < %s", - // @codingStandardsIgnoreEnd - gmdate( 'Y-m-d H:i:s', absint( $date ) ) - ) - ); - - return $unpaid_orders; - } - - /** - * Search order data for a term and return ids. - * - * @param string $term Searched term. - * @return array of ids - */ - public function search_orders( $term ) { - global $wpdb; - - /** - * Searches on meta data can be slow - this lets you choose what fields to search. - * 3.0.0 added _billing_address and _shipping_address meta which contains all address data to make this faster. - * This however won't work on older orders unless updated, so search a few others (expand this using the filter if needed). - * - * @var array - */ - $search_fields = array_map( - 'wc_clean', - apply_filters( - 'woocommerce_shop_order_search_fields', - array( - '_billing_address_index', - '_shipping_address_index', - '_billing_last_name', - '_billing_email', - ) - ) - ); - $order_ids = array(); - - if ( is_numeric( $term ) ) { - $order_ids[] = absint( $term ); - } - - if ( ! empty( $search_fields ) ) { - $order_ids = array_unique( - array_merge( - $order_ids, - $wpdb->get_col( - $wpdb->prepare( - "SELECT DISTINCT p1.post_id FROM {$wpdb->postmeta} p1 WHERE p1.meta_value LIKE %s AND p1.meta_key IN ('" . implode( "','", array_map( 'esc_sql', $search_fields ) ) . "')", // @codingStandardsIgnoreLine - '%' . $wpdb->esc_like( wc_clean( $term ) ) . '%' - ) - ), - $wpdb->get_col( - $wpdb->prepare( - "SELECT order_id - FROM {$wpdb->prefix}woocommerce_order_items as order_items - WHERE order_item_name LIKE %s", - '%' . $wpdb->esc_like( wc_clean( $term ) ) . '%' - ) - ) - ) - ); - } - - return apply_filters( 'woocommerce_shop_order_search_results', $order_ids, $term, $search_fields ); - } - - /** - * Gets information about whether permissions were generated yet. - * - * @param WC_Order|int $order Order ID or order object. - * @return bool - */ - public function get_download_permissions_granted( $order ) { - $order_id = WC_Order_Factory::get_order_id( $order ); - return wc_string_to_bool( get_post_meta( $order_id, '_download_permissions_granted', true ) ); - } - - /** - * Stores information about whether permissions were generated yet. - * - * @param WC_Order|int $order Order ID or order object. - * @param bool $set True or false. - */ - public function set_download_permissions_granted( $order, $set ) { - $order_id = WC_Order_Factory::get_order_id( $order ); - update_post_meta( $order_id, '_download_permissions_granted', wc_bool_to_string( $set ) ); - } - - /** - * Gets information about whether sales were recorded. - * - * @param WC_Order|int $order Order ID or order object. - * @return bool - */ - public function get_recorded_sales( $order ) { - $order_id = WC_Order_Factory::get_order_id( $order ); - return wc_string_to_bool( get_post_meta( $order_id, '_recorded_sales', true ) ); - } - - /** - * Stores information about whether sales were recorded. - * - * @param WC_Order|int $order Order ID or order object. - * @param bool $set True or false. - */ - public function set_recorded_sales( $order, $set ) { - $order_id = WC_Order_Factory::get_order_id( $order ); - update_post_meta( $order_id, '_recorded_sales', wc_bool_to_string( $set ) ); - } - - /** - * Gets information about whether coupon counts were updated. - * - * @param WC_Order|int $order Order ID or order object. - * @return bool - */ - public function get_recorded_coupon_usage_counts( $order ) { - $order_id = WC_Order_Factory::get_order_id( $order ); - return wc_string_to_bool( get_post_meta( $order_id, '_recorded_coupon_usage_counts', true ) ); - } - - /** - * Stores information about whether coupon counts were updated. - * - * @param WC_Order|int $order Order ID or order object. - * @param bool $set True or false. - */ - public function set_recorded_coupon_usage_counts( $order, $set ) { - $order_id = WC_Order_Factory::get_order_id( $order ); - update_post_meta( $order_id, '_recorded_coupon_usage_counts', wc_bool_to_string( $set ) ); - } - - /** - * Return array of coupon_code => meta_key for coupon which have usage limit and have tentative keys. - * Pass $coupon_id if key for only one of the coupon is needed. - * - * @param WC_Order $order Order object. - * @param int $coupon_id If passed, will return held key for that coupon. - * - * @return array|string Key value pair for coupon code and meta key name. If $coupon_id is passed, returns meta_key for only that coupon. - */ - public function get_coupon_held_keys( $order, $coupon_id = null ) { - $held_keys = $order->get_meta( '_coupon_held_keys' ); - if ( $coupon_id ) { - return isset( $held_keys[ $coupon_id ] ) ? $held_keys[ $coupon_id ] : null; - } - return $held_keys; - } - - /** - * Return array of coupon_code => meta_key for coupon which have usage limit per customer and have tentative keys. - * - * @param WC_Order $order Order object. - * @param int $coupon_id If passed, will return held key for that coupon. - * - * @return mixed - */ - public function get_coupon_held_keys_for_users( $order, $coupon_id = null ) { - $held_keys_for_user = $order->get_meta( '_coupon_held_keys_for_users' ); - if ( $coupon_id ) { - return isset( $held_keys_for_user[ $coupon_id ] ) ? $held_keys_for_user[ $coupon_id ] : null; - } - return $held_keys_for_user; - } - - /** - * Add/Update list of meta keys that are currently being used by this order to hold a coupon. - * This is used to figure out what all meta entries we should delete when order is cancelled/completed. - * - * @param WC_Order $order Order object. - * @param array $held_keys Array of coupon_code => meta_key. - * @param array $held_keys_for_user Array of coupon_code => meta_key for held coupon for user. - * - * @return mixed - */ - public function set_coupon_held_keys( $order, $held_keys, $held_keys_for_user ) { - if ( is_array( $held_keys ) && 0 < count( $held_keys ) ) { - $order->update_meta_data( '_coupon_held_keys', $held_keys ); - } - if ( is_array( $held_keys_for_user ) && 0 < count( $held_keys_for_user ) ) { - $order->update_meta_data( '_coupon_held_keys_for_users', $held_keys_for_user ); - } - } - - /** - * Release all coupons held by this order. - * - * @param WC_Order $order Current order object. - * @param bool $save Whether to delete keys from DB right away. Could be useful to pass `false` if you are building a bulk request. - */ - public function release_held_coupons( $order, $save = true ) { - $coupon_held_keys = $this->get_coupon_held_keys( $order ); - if ( is_array( $coupon_held_keys ) ) { - foreach ( $coupon_held_keys as $coupon_id => $meta_key ) { - delete_post_meta( $coupon_id, $meta_key ); - } - } - $order->delete_meta_data( '_coupon_held_keys' ); - - $coupon_held_keys_for_users = $this->get_coupon_held_keys_for_users( $order ); - if ( is_array( $coupon_held_keys_for_users ) ) { - foreach ( $coupon_held_keys_for_users as $coupon_id => $meta_key ) { - delete_post_meta( $coupon_id, $meta_key ); - } - } - $order->delete_meta_data( '_coupon_held_keys_for_users' ); - - if ( $save ) { - $order->save_meta_data(); - } - - } - - /** - * Gets information about whether stock was reduced. - * - * @param WC_Order|int $order Order ID or order object. - * @return bool - */ - public function get_stock_reduced( $order ) { - $order_id = WC_Order_Factory::get_order_id( $order ); - return wc_string_to_bool( get_post_meta( $order_id, '_order_stock_reduced', true ) ); - } - - /** - * Stores information about whether stock was reduced. - * - * @param WC_Order|int $order Order ID or order object. - * @param bool $set True or false. - */ - public function set_stock_reduced( $order, $set ) { - $order_id = WC_Order_Factory::get_order_id( $order ); - update_post_meta( $order_id, '_order_stock_reduced', wc_bool_to_string( $set ) ); - } - - /** - * Get the order type based on Order ID. - * - * @since 3.0.0 - * @param int|WP_Post $order Order | Order id. - * - * @return string - */ - public function get_order_type( $order ) { - return get_post_type( $order ); - } - - /** - * Get valid WP_Query args from a WC_Order_Query's query variables. - * - * @since 3.1.0 - * @param array $query_vars query vars from a WC_Order_Query. - * @return array - */ - protected function get_wp_query_args( $query_vars ) { - - // Map query vars to ones that get_wp_query_args or WP_Query recognize. - $key_mapping = array( - 'customer_id' => 'customer_user', - 'status' => 'post_status', - 'currency' => 'order_currency', - 'version' => 'order_version', - 'discount_total' => 'cart_discount', - 'discount_tax' => 'cart_discount_tax', - 'shipping_total' => 'order_shipping', - 'shipping_tax' => 'order_shipping_tax', - 'cart_tax' => 'order_tax', - 'total' => 'order_total', - 'page' => 'paged', - ); - - foreach ( $key_mapping as $query_key => $db_key ) { - if ( isset( $query_vars[ $query_key ] ) ) { - $query_vars[ $db_key ] = $query_vars[ $query_key ]; - unset( $query_vars[ $query_key ] ); - } - } - - // Add the 'wc-' prefix to status if needed. - if ( ! empty( $query_vars['post_status'] ) ) { - if ( is_array( $query_vars['post_status'] ) ) { - foreach ( $query_vars['post_status'] as &$status ) { - $status = wc_is_order_status( 'wc-' . $status ) ? 'wc-' . $status : $status; - } - } else { - $query_vars['post_status'] = wc_is_order_status( 'wc-' . $query_vars['post_status'] ) ? 'wc-' . $query_vars['post_status'] : $query_vars['post_status']; - } - } - - $wp_query_args = parent::get_wp_query_args( $query_vars ); - - if ( ! isset( $wp_query_args['date_query'] ) ) { - $wp_query_args['date_query'] = array(); - } - if ( ! isset( $wp_query_args['meta_query'] ) ) { - $wp_query_args['meta_query'] = array(); - } - - $date_queries = array( - 'date_created' => 'post_date', - 'date_modified' => 'post_modified', - 'date_completed' => '_date_completed', - 'date_paid' => '_date_paid', - ); - foreach ( $date_queries as $query_var_key => $db_key ) { - if ( isset( $query_vars[ $query_var_key ] ) && '' !== $query_vars[ $query_var_key ] ) { - - // Remove any existing meta queries for the same keys to prevent conflicts. - $existing_queries = wp_list_pluck( $wp_query_args['meta_query'], 'key', true ); - $meta_query_index = array_search( $db_key, $existing_queries, true ); - if ( false !== $meta_query_index ) { - unset( $wp_query_args['meta_query'][ $meta_query_index ] ); - } - - $wp_query_args = $this->parse_date_for_wp_query( $query_vars[ $query_var_key ], $db_key, $wp_query_args ); - } - } - - if ( isset( $query_vars['customer'] ) && '' !== $query_vars['customer'] && array() !== $query_vars['customer'] ) { - $values = is_array( $query_vars['customer'] ) ? $query_vars['customer'] : array( $query_vars['customer'] ); - $customer_query = $this->get_orders_generate_customer_meta_query( $values ); - if ( is_wp_error( $customer_query ) ) { - $wp_query_args['errors'][] = $customer_query; - } else { - $wp_query_args['meta_query'][] = $customer_query; - } - } - - if ( isset( $query_vars['anonymized'] ) ) { - if ( $query_vars['anonymized'] ) { - $wp_query_args['meta_query'][] = array( - 'key' => '_anonymized', - 'value' => 'yes', - ); - } else { - $wp_query_args['meta_query'][] = array( - 'key' => '_anonymized', - 'compare' => 'NOT EXISTS', - ); - } - } - - if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) { - $wp_query_args['no_found_rows'] = true; - } - - return apply_filters( 'woocommerce_order_data_store_cpt_get_orders_query', $wp_query_args, $query_vars, $this ); - } - - /** - * Query for Orders matching specific criteria. - * - * @since 3.1.0 - * - * @param array $query_vars query vars from a WC_Order_Query. - * - * @return array|object - */ - public function query( $query_vars ) { - $args = $this->get_wp_query_args( $query_vars ); - - if ( ! empty( $args['errors'] ) ) { - $query = (object) array( - 'posts' => array(), - 'found_posts' => 0, - 'max_num_pages' => 0, - ); - } else { - $query = new WP_Query( $args ); - } - - if ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) { - $orders = $query->posts; - } else { - update_post_caches( $query->posts ); // We already fetching posts, might as well hydrate some caches. - $order_ids = wp_list_pluck( $query->posts, 'ID' ); - $orders = $this->compile_orders( $order_ids, $query_vars, $query ); - } - - if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) { - return (object) array( - 'orders' => $orders, - 'total' => $query->found_posts, - 'max_num_pages' => $query->max_num_pages, - ); - } - - return $orders; - } - - /** - * Compile order response and set caches as needed for order ids. - * - * @param array $order_ids List of order IDS to compile. - * @param array $query_vars Original query arguments. - * @param WP_Query $query Query object. - * - * @return array Orders. - */ - private function compile_orders( $order_ids, $query_vars, $query ) { - if ( empty( $order_ids ) ) { - return array(); - } - $orders = array(); - - // Lets do some cache hydrations so that we don't have to fetch data from DB for every order. - $this->prime_raw_meta_cache_for_orders( $order_ids, $query_vars ); - $this->prime_refund_caches_for_order( $order_ids, $query_vars ); - $this->prime_order_item_caches_for_orders( $order_ids, $query_vars ); - - foreach ( $query->posts as $post ) { - $order = wc_get_order( $post ); - - // If the order returns false, don't add it to the list. - if ( false === $order ) { - continue; - } - - $orders[] = $order; - } - - return $orders; - } - - /** - * Prime refund cache for orders. - * - * @param array $order_ids Order Ids to prime cache for. - * @param array $query_vars Query vars for the query. - */ - private function prime_refund_caches_for_order( $order_ids, $query_vars ) { - if ( ! isset( $query_vars['type'] ) || ! ( 'shop_order' === $query_vars['type'] ) ) { - return; - } - if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) { - if ( is_array( $query_vars['fields'] ) && ! in_array( 'refunds', $query_vars['fields'] ) ) { - return; - } - } - $cache_keys_mapping = array(); - foreach ( $order_ids as $order_id ) { - $cache_keys_mapping[ $order_id ] = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $order_id; - } - $non_cached_ids = array(); - $cache_values = wc_cache_get_multiple( array_values( $cache_keys_mapping ), 'orders' ); - foreach ( $order_ids as $order_id ) { - if ( false === $cache_values[ $cache_keys_mapping[ $order_id ] ] ) { - $non_cached_ids[] = $order_id; - } - } - if ( empty( $non_cached_ids ) ) { - return; - } - - $refunds = wc_get_orders( - array( - 'type' => 'shop_order_refund', - 'post_parent__in' => $non_cached_ids, - 'limit' => - 1, - ) - ); - $order_refunds = array_reduce( - $refunds, - function ( $order_refunds_array, WC_Order_Refund $refund ) { - if ( ! isset( $order_refunds_array[ $refund->get_parent_id() ] ) ) { - $order_refunds_array[ $refund->get_parent_id() ] = array(); - } - $order_refunds_array[ $refund->get_parent_id() ][] = $refund; - return $order_refunds_array; - }, - array() - ); - foreach ( $non_cached_ids as $order_id ) { - $refunds = array(); - if ( isset( $order_refunds[ $order_id ] ) ) { - $refunds = $order_refunds[ $order_id ]; - } - wp_cache_set( $cache_keys_mapping[ $order_id ], $refunds, 'orders' ); - } - } - - /** - * Prime following caches: - * 1. item-$order_item_id For individual items. - * 2. order-items-$order-id For fetching items associated with an order. - * 3. order-item meta. - * - * @param array $order_ids Order Ids to prime cache for. - * @param array $query_vars Query vars for the query. - */ - private function prime_order_item_caches_for_orders( $order_ids, $query_vars ) { - global $wpdb; - if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) { - $line_items = array( - 'line_items', - 'shipping_lines', - 'fee_lines', - 'coupon_lines', - ); - - if ( is_array( $query_vars['fields'] ) && 0 === count( array_intersect( $line_items, $query_vars['fields'] ) ) ) { - return; - } - } - $cache_keys = array_map( - function ( $order_id ) { - return 'order-items-' . $order_id; - }, - $order_ids - ); - $cache_values = wc_cache_get_multiple( $cache_keys, 'orders' ); - $non_cached_ids = array(); - foreach ( $order_ids as $order_id ) { - if ( false === $cache_values[ 'order-items-' . $order_id ] ) { - $non_cached_ids[] = $order_id; - } - } - if ( empty( $non_cached_ids ) ) { - return; - } - - $non_cached_ids = esc_sql( $non_cached_ids ); - $non_cached_ids_string = implode( ',', $non_cached_ids ); - $order_items = $wpdb->get_results( - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - "SELECT order_item_type, order_item_id, order_id, order_item_name FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id in ( $non_cached_ids_string ) ORDER BY order_item_id;" - ); - if ( empty( $order_items ) ) { - return; - } - - $order_items_for_all_orders = array_reduce( - $order_items, - function ( $order_items_collection, $order_item ) { - if ( ! isset( $order_items_collection[ $order_item->order_id ] ) ) { - $order_items_collection[ $order_item->order_id ] = array(); - } - $order_items_collection[ $order_item->order_id ][] = $order_item; - return $order_items_collection; - } - ); - foreach ( $order_items_for_all_orders as $order_id => $items ) { - wp_cache_set( 'order-items-' . $order_id, $items, 'orders' ); - } - foreach ( $order_items as $item ) { - wp_cache_set( 'item-' . $item->order_item_id, $item, 'order-items' ); - } - $order_item_ids = wp_list_pluck( $order_items, 'order_item_id' ); - update_meta_cache( 'order_item', $order_item_ids ); - } - - /** - * Prime cache for raw meta data for orders in bulk. Difference between this and WP built-in metadata is that this method also fetches `meta_id` field which we use and cache it. - * - * @param array $order_ids Order Ids to prime cache for. - * @param array $query_vars Query vars for the query. - */ - private function prime_raw_meta_cache_for_orders( $order_ids, $query_vars ) { - global $wpdb; - - if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) { - if ( is_array( $query_vars['fields'] ) && ! in_array( 'meta_data', $query_vars['fields'] ) ) { - return; - } - } - - $cache_keys_mapping = array(); - foreach ( $order_ids as $order_id ) { - $cache_keys_mapping[ $order_id ] = WC_Order::generate_meta_cache_key( $order_id, 'orders' ); - } - $cache_values = wc_cache_get_multiple( array_values( $cache_keys_mapping ), 'orders' ); - $non_cached_ids = array(); - foreach ( $order_ids as $order_id ) { - if ( false === $cache_values[ $cache_keys_mapping[ $order_id ] ] ) { - $non_cached_ids[] = $order_id; - } - } - if ( empty( $non_cached_ids ) ) { - return; - } - $order_ids = esc_sql( $non_cached_ids ); - $order_ids_in = "'" . implode( "', '", $order_ids ) . "'"; - $raw_meta_data_array = $wpdb->get_results( - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared - "SELECT post_id as object_id, meta_id, meta_key, meta_value - FROM {$wpdb->postmeta} - WHERE post_id IN ( $order_ids_in ) - ORDER BY post_id" - // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared - ); - $raw_meta_data_collection = array_reduce( - $raw_meta_data_array, - function ( $collection, $raw_meta_data ) { - if ( ! isset( $collection[ $raw_meta_data->object_id ] ) ) { - $collection[ $raw_meta_data->object_id ] = array(); - } - $collection[ $raw_meta_data->object_id ][] = $raw_meta_data; - return $collection; - }, - array() - ); - WC_Order::prime_raw_meta_data_cache( $raw_meta_data_collection, 'orders' ); - } - - /** - * Return the order type of a given item which belongs to WC_Order. - * - * @since 3.2.0 - * @param WC_Order $order Order Object. - * @param int $order_item_id Order item id. - * @return string Order Item type - */ - public function get_order_item_type( $order, $order_item_id ) { - global $wpdb; - return $wpdb->get_var( $wpdb->prepare( "SELECT DISTINCT order_item_type FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d and order_item_id = %d;", $order->get_id(), $order_item_id ) ); - } -} diff --git a/includes/data-stores/class-wc-payment-token-data-store.php b/includes/data-stores/class-wc-payment-token-data-store.php deleted file mode 100644 index 78ea7272dcb..00000000000 --- a/includes/data-stores/class-wc-payment-token-data-store.php +++ /dev/null @@ -1,372 +0,0 @@ -validate() ) { - throw new Exception( __( 'Invalid or missing payment token fields.', 'woocommerce' ) ); - } - - global $wpdb; - if ( ! $token->is_default() && $token->get_user_id() > 0 ) { - $default_token = WC_Payment_Tokens::get_customer_default_token( $token->get_user_id() ); - if ( is_null( $default_token ) ) { - $token->set_default( true ); - } - } - - $payment_token_data = array( - 'gateway_id' => $token->get_gateway_id( 'edit' ), - 'token' => $token->get_token( 'edit' ), - 'user_id' => $token->get_user_id( 'edit' ), - 'type' => $token->get_type( 'edit' ), - ); - - $wpdb->insert( $wpdb->prefix . 'woocommerce_payment_tokens', $payment_token_data ); - $token_id = $wpdb->insert_id; - $token->set_id( $token_id ); - $this->save_extra_data( $token, true ); - $token->save_meta_data(); - $token->apply_changes(); - - // Make sure all other tokens are not set to default. - if ( $token->is_default() && $token->get_user_id() > 0 ) { - WC_Payment_Tokens::set_users_default( $token->get_user_id(), $token_id ); - } - - do_action( 'woocommerce_new_payment_token', $token_id, $token ); - } - - /** - * Update a payment token. - * - * @since 3.0.0 - * - * @param WC_Payment_Token $token Payment token object. - * - * @throws Exception Throw exception if invalid or missing payment token fields. - */ - public function update( &$token ) { - if ( false === $token->validate() ) { - throw new Exception( __( 'Invalid or missing payment token fields.', 'woocommerce' ) ); - } - - global $wpdb; - - $updated_props = array(); - $core_props = array( 'gateway_id', 'token', 'user_id', 'type' ); - $changed_props = array_keys( $token->get_changes() ); - - foreach ( $changed_props as $prop ) { - if ( ! in_array( $prop, $core_props, true ) ) { - continue; - } - $updated_props[] = $prop; - $payment_token_data[ $prop ] = $token->{'get_' . $prop}( 'edit' ); - } - - if ( ! empty( $payment_token_data ) ) { - $wpdb->update( - $wpdb->prefix . 'woocommerce_payment_tokens', - $payment_token_data, - array( 'token_id' => $token->get_id() ) - ); - } - - $updated_extra_props = $this->save_extra_data( $token ); - $updated_props = array_merge( $updated_props, $updated_extra_props ); - $token->save_meta_data(); - $token->apply_changes(); - - // Make sure all other tokens are not set to default. - if ( $token->is_default() && $token->get_user_id() > 0 ) { - WC_Payment_Tokens::set_users_default( $token->get_user_id(), $token->get_id() ); - } - - do_action( 'woocommerce_payment_token_object_updated_props', $token, $updated_props ); - do_action( 'woocommerce_payment_token_updated', $token->get_id() ); - } - - /** - * Remove a payment token from the database. - * - * @since 3.0.0 - * @param WC_Payment_Token $token Payment token object. - * @param bool $force_delete Unused param. - */ - public function delete( &$token, $force_delete = false ) { - global $wpdb; - $wpdb->delete( $wpdb->prefix . 'woocommerce_payment_tokens', array( 'token_id' => $token->get_id() ), array( '%d' ) ); - $wpdb->delete( $wpdb->prefix . 'woocommerce_payment_tokenmeta', array( 'payment_token_id' => $token->get_id() ), array( '%d' ) ); - do_action( 'woocommerce_payment_token_deleted', $token->get_id(), $token ); - } - - /** - * Read a token from the database. - * - * @since 3.0.0 - * - * @param WC_Payment_Token $token Payment token object. - * - * @throws Exception Throw exception if invalid payment token. - */ - public function read( &$token ) { - global $wpdb; - - $data = $wpdb->get_row( - $wpdb->prepare( - "SELECT token, user_id, gateway_id, is_default FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d LIMIT 1", - $token->get_id() - ) - ); - - if ( $data ) { - $token->set_props( - array( - 'token' => $data->token, - 'user_id' => $data->user_id, - 'gateway_id' => $data->gateway_id, - 'default' => $data->is_default, - ) - ); - $this->read_extra_data( $token ); - $token->read_meta_data(); - $token->set_object_read( true ); - do_action( 'woocommerce_payment_token_loaded', $token ); - } else { - throw new Exception( __( 'Invalid payment token.', 'woocommerce' ) ); - } - } - - /** - * Read extra data associated with the token (like last4 digits of a card for expiry dates). - * - * @param WC_Payment_Token $token Payment token object. - * @since 3.0.0 - */ - protected function read_extra_data( &$token ) { - foreach ( $token->get_extra_data_keys() as $key ) { - $function = 'set_' . $key; - if ( is_callable( array( $token, $function ) ) ) { - $token->{$function}( get_metadata( 'payment_token', $token->get_id(), $key, true ) ); - } - } - } - - /** - * Saves extra token data as meta. - * - * @since 3.0.0 - * @param WC_Payment_Token $token Payment token object. - * @param bool $force By default, only changed props are updated. When this param is true all props are updated. - * @return array List of updated props. - */ - protected function save_extra_data( &$token, $force = false ) { - if ( $this->extra_data_saved ) { - return array(); - } - - $updated_props = array(); - $extra_data_keys = $token->get_extra_data_keys(); - $meta_key_to_props = ! empty( $extra_data_keys ) ? array_combine( $extra_data_keys, $extra_data_keys ) : array(); - $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $token, $meta_key_to_props ); - - foreach ( $extra_data_keys as $key ) { - if ( ! array_key_exists( $key, $props_to_update ) ) { - continue; - } - $function = 'get_' . $key; - if ( is_callable( array( $token, $function ) ) ) { - if ( update_metadata( 'payment_token', $token->get_id(), $key, $token->{$function}( 'edit' ) ) ) { - $updated_props[] = $key; - } - } - } - - return $updated_props; - } - - /** - * Returns an array of objects (stdObject) matching specific token criteria. - * Accepts token_id, user_id, gateway_id, and type. - * Each object should contain the fields token_id, gateway_id, token, user_id, type, is_default. - * - * @since 3.0.0 - * @param array $args List of accepted args: token_id, gateway_id, user_id, type. - * @return array - */ - public function get_tokens( $args ) { - global $wpdb; - $args = wp_parse_args( - $args, - array( - 'token_id' => '', - 'user_id' => '', - 'gateway_id' => '', - 'type' => '', - ) - ); - - $sql = "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens"; - $where = array( '1=1' ); - - if ( $args['token_id'] ) { - $token_ids = array_map( 'absint', is_array( $args['token_id'] ) ? $args['token_id'] : array( $args['token_id'] ) ); - $where[] = "token_id IN ('" . implode( "','", array_map( 'esc_sql', $token_ids ) ) . "')"; - } - - if ( $args['user_id'] ) { - $where[] = $wpdb->prepare( 'user_id = %d', absint( $args['user_id'] ) ); - } - - if ( $args['gateway_id'] ) { - $gateway_ids = array( $args['gateway_id'] ); - } else { - $gateways = WC_Payment_Gateways::instance(); - $gateway_ids = $gateways->get_payment_gateway_ids(); - } - - $page = isset( $args['page'] ) ? absint( $args['page'] ) : 1; - $posts_per_page = isset( $args['limit'] ) ? absint( $args['limit'] ) : get_option( 'posts_per_page' ); - - $pgstrt = absint( ( $page - 1 ) * $posts_per_page ) . ', '; - $limits = 'LIMIT ' . $pgstrt . $posts_per_page; - - $gateway_ids[] = ''; - $where[] = "gateway_id IN ('" . implode( "','", array_map( 'esc_sql', $gateway_ids ) ) . "')"; - - if ( $args['type'] ) { - $where[] = $wpdb->prepare( 'type = %s', $args['type'] ); - } - - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $token_results = $wpdb->get_results( $sql . ' WHERE ' . implode( ' AND ', $where ) . ' ' . $limits ); - - return $token_results; - } - - /** - * Returns an stdObject of a token for a user's default token. - * Should contain the fields token_id, gateway_id, token, user_id, type, is_default. - * - * @since 3.0.0 - * @param int $user_id User ID. - * @return object - */ - public function get_users_default_token( $user_id ) { - global $wpdb; - return $wpdb->get_row( - $wpdb->prepare( - "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE user_id = %d AND is_default = 1", - $user_id - ) - ); - } - - /** - * Returns an stdObject of a token. - * Should contain the fields token_id, gateway_id, token, user_id, type, is_default. - * - * @since 3.0.0 - * @param int $token_id Token ID. - * @return object - */ - public function get_token_by_id( $token_id ) { - global $wpdb; - return $wpdb->get_row( - $wpdb->prepare( - "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d", - $token_id - ) - ); - } - - /** - * Returns metadata for a specific payment token. - * - * @since 3.0.0 - * @param int $token_id Token ID. - * @return array - */ - public function get_metadata( $token_id ) { - return get_metadata( 'payment_token', $token_id ); - } - - /** - * Get a token's type by ID. - * - * @since 3.0.0 - * @param int $token_id Token ID. - * @return string - */ - public function get_token_type_by_id( $token_id ) { - global $wpdb; - return $wpdb->get_var( - $wpdb->prepare( - "SELECT type FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d", - $token_id - ) - ); - } - - /** - * Update's a tokens default status in the database. Used for quickly - * looping through tokens and setting their statuses instead of creating a bunch - * of objects. - * - * @since 3.0.0 - * - * @param int $token_id Token ID. - * @param bool $status Whether given payment token is the default payment token or not. - * - * @return void - */ - public function set_default_status( $token_id, $status = true ) { - global $wpdb; - $wpdb->update( - $wpdb->prefix . 'woocommerce_payment_tokens', - array( 'is_default' => $status ), - array( - 'token_id' => $token_id, - ) - ); - } - -} diff --git a/includes/data-stores/class-wc-product-data-store-cpt.php b/includes/data-stores/class-wc-product-data-store-cpt.php deleted file mode 100644 index c705beef402..00000000000 --- a/includes/data-stores/class-wc-product-data-store-cpt.php +++ /dev/null @@ -1,2131 +0,0 @@ -get_date_created( 'edit' ) ) { - $product->set_date_created( time() ); - } - - $id = wp_insert_post( - apply_filters( - 'woocommerce_new_product_data', - array( - 'post_type' => 'product', - 'post_status' => $product->get_status() ? $product->get_status() : 'publish', - 'post_author' => get_current_user_id(), - 'post_title' => $product->get_name() ? $product->get_name() : __( 'Product', 'woocommerce' ), - 'post_content' => $product->get_description(), - 'post_excerpt' => $product->get_short_description(), - 'post_parent' => $product->get_parent_id(), - 'comment_status' => $product->get_reviews_allowed() ? 'open' : 'closed', - 'ping_status' => 'closed', - 'menu_order' => $product->get_menu_order(), - 'post_password' => $product->get_post_password( 'edit' ), - 'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ), - 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ), - 'post_name' => $product->get_slug( 'edit' ), - ) - ), - true - ); - - if ( $id && ! is_wp_error( $id ) ) { - $product->set_id( $id ); - - $this->update_post_meta( $product, true ); - $this->update_terms( $product, true ); - $this->update_visibility( $product, true ); - $this->update_attributes( $product, true ); - $this->update_version_and_type( $product ); - $this->handle_updated_props( $product ); - $this->clear_caches( $product ); - - $product->save_meta_data(); - $product->apply_changes(); - - do_action( 'woocommerce_new_product', $id, $product ); - } - } - - /** - * Method to read a product from the database. - * - * @param WC_Product $product Product object. - * @throws Exception If invalid product. - */ - public function read( &$product ) { - $product->set_defaults(); - $post_object = get_post( $product->get_id() ); - - if ( ! $product->get_id() || ! $post_object || 'product' !== $post_object->post_type ) { - throw new Exception( __( 'Invalid product.', 'woocommerce' ) ); - } - - $product->set_props( - array( - 'name' => $post_object->post_title, - 'slug' => $post_object->post_name, - 'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ), - 'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ), - 'status' => $post_object->post_status, - 'description' => $post_object->post_content, - 'short_description' => $post_object->post_excerpt, - 'parent_id' => $post_object->post_parent, - 'menu_order' => $post_object->menu_order, - 'post_password' => $post_object->post_password, - 'reviews_allowed' => 'open' === $post_object->comment_status, - ) - ); - - $this->read_attributes( $product ); - $this->read_downloads( $product ); - $this->read_visibility( $product ); - $this->read_product_data( $product ); - $this->read_extra_data( $product ); - $product->set_object_read( true ); - - do_action( 'woocommerce_product_read', $product->get_id() ); - } - - /** - * Method to update a product in the database. - * - * @param WC_Product $product Product object. - */ - public function update( &$product ) { - $product->save_meta_data(); - $changes = $product->get_changes(); - - // Only update the post when the post data changes. - if ( array_intersect( array( 'description', 'short_description', 'name', 'parent_id', 'reviews_allowed', 'status', 'menu_order', 'date_created', 'date_modified', 'slug' ), array_keys( $changes ) ) ) { - $post_data = array( - 'post_content' => $product->get_description( 'edit' ), - 'post_excerpt' => $product->get_short_description( 'edit' ), - 'post_title' => $product->get_name( 'edit' ), - 'post_parent' => $product->get_parent_id( 'edit' ), - 'comment_status' => $product->get_reviews_allowed( 'edit' ) ? 'open' : 'closed', - 'post_status' => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish', - 'menu_order' => $product->get_menu_order( 'edit' ), - 'post_password' => $product->get_post_password( 'edit' ), - 'post_name' => $product->get_slug( 'edit' ), - 'post_type' => 'product', - ); - if ( $product->get_date_created( 'edit' ) ) { - $post_data['post_date'] = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ); - $post_data['post_date_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ); - } - if ( isset( $changes['date_modified'] ) && $product->get_date_modified( 'edit' ) ) { - $post_data['post_modified'] = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() ); - $post_data['post_modified_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getTimestamp() ); - } else { - $post_data['post_modified'] = current_time( 'mysql' ); - $post_data['post_modified_gmt'] = current_time( 'mysql', 1 ); - } - - /** - * When updating this object, to prevent infinite loops, use $wpdb - * to update data, since wp_update_post spawns more calls to the - * save_post action. - * - * This ensures hooks are fired by either WP itself (admin screen save), - * or an update purely from CRUD. - */ - if ( doing_action( 'save_post' ) ) { - $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) ); - clean_post_cache( $product->get_id() ); - } else { - wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) ); - } - $product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook. - - } else { // Only update post modified time to record this save event. - $GLOBALS['wpdb']->update( - $GLOBALS['wpdb']->posts, - array( - 'post_modified' => current_time( 'mysql' ), - 'post_modified_gmt' => current_time( 'mysql', 1 ), - ), - array( - 'ID' => $product->get_id(), - ) - ); - clean_post_cache( $product->get_id() ); - } - - $this->update_post_meta( $product ); - $this->update_terms( $product ); - $this->update_visibility( $product ); - $this->update_attributes( $product ); - $this->update_version_and_type( $product ); - $this->handle_updated_props( $product ); - $this->clear_caches( $product ); - - wc_get_container() - ->get( DownloadPermissionsAdjuster::class ) - ->maybe_schedule_adjust_download_permissions( $product ); - - $product->apply_changes(); - - // Any time we update the product, we should flush the term count cache. - $tools_controller = new WC_REST_System_Status_Tools_Controller(); - $tools_controller->execute_tool( 'recount_terms' ); - do_action( 'woocommerce_update_product', $product->get_id(), $product ); - } - - /** - * Method to delete a product from the database. - * - * @param WC_Product $product Product object. - * @param array $args Array of args to pass to the delete method. - */ - public function delete( &$product, $args = array() ) { - $id = $product->get_id(); - $post_type = $product->is_type( 'variation' ) ? 'product_variation' : 'product'; - - $args = wp_parse_args( - $args, - array( - 'force_delete' => false, - ) - ); - - if ( ! $id ) { - return; - } - - if ( $args['force_delete'] ) { - do_action( 'woocommerce_before_delete_' . $post_type, $id ); - wp_delete_post( $id ); - $product->set_id( 0 ); - do_action( 'woocommerce_delete_' . $post_type, $id ); - } else { - wp_trash_post( $id ); - $product->set_status( 'trash' ); - do_action( 'woocommerce_trash_' . $post_type, $id ); - } - } - - /* - |-------------------------------------------------------------------------- - | Additional Methods - |-------------------------------------------------------------------------- - */ - - /** - * Read product data. Can be overridden by child classes to load other props. - * - * @param WC_Product $product Product object. - * @since 3.0.0 - */ - protected function read_product_data( &$product ) { - $id = $product->get_id(); - $post_meta_values = get_post_meta( $id ); - $meta_key_to_props = array( - '_sku' => 'sku', - '_regular_price' => 'regular_price', - '_sale_price' => 'sale_price', - '_price' => 'price', - '_sale_price_dates_from' => 'date_on_sale_from', - '_sale_price_dates_to' => 'date_on_sale_to', - 'total_sales' => 'total_sales', - '_tax_status' => 'tax_status', - '_tax_class' => 'tax_class', - '_manage_stock' => 'manage_stock', - '_backorders' => 'backorders', - '_low_stock_amount' => 'low_stock_amount', - '_sold_individually' => 'sold_individually', - '_weight' => 'weight', - '_length' => 'length', - '_width' => 'width', - '_height' => 'height', - '_upsell_ids' => 'upsell_ids', - '_crosssell_ids' => 'cross_sell_ids', - '_purchase_note' => 'purchase_note', - '_default_attributes' => 'default_attributes', - '_virtual' => 'virtual', - '_downloadable' => 'downloadable', - '_download_limit' => 'download_limit', - '_download_expiry' => 'download_expiry', - '_thumbnail_id' => 'image_id', - '_stock' => 'stock_quantity', - '_stock_status' => 'stock_status', - '_wc_average_rating' => 'average_rating', - '_wc_rating_count' => 'rating_counts', - '_wc_review_count' => 'review_count', - '_product_image_gallery' => 'gallery_image_ids', - ); - - $set_props = array(); - - foreach ( $meta_key_to_props as $meta_key => $prop ) { - $meta_value = isset( $post_meta_values[ $meta_key ][0] ) ? $post_meta_values[ $meta_key ][0] : null; - $set_props[ $prop ] = maybe_unserialize( $meta_value ); // get_post_meta only unserializes single values. - } - - $set_props['category_ids'] = $this->get_term_ids( $product, 'product_cat' ); - $set_props['tag_ids'] = $this->get_term_ids( $product, 'product_tag' ); - $set_props['shipping_class_id'] = current( $this->get_term_ids( $product, 'product_shipping_class' ) ); - $set_props['gallery_image_ids'] = array_filter( explode( ',', $set_props['gallery_image_ids'] ) ); - - $product->set_props( $set_props ); - } - - /** - * Re-reads stock from the DB ignoring changes. - * - * @param WC_Product $product Product object. - * @param int|float $new_stock New stock level if already read. - */ - public function read_stock_quantity( &$product, $new_stock = null ) { - $object_read = $product->get_object_read(); - $product->set_object_read( false ); // This makes update of qty go directly to data- instead of changes-array of the product object (which is needed as the data should hold status of the object as it was read from the db). - $product->set_stock_quantity( is_null( $new_stock ) ? get_post_meta( $product->get_id(), '_stock', true ) : $new_stock ); - $product->set_object_read( $object_read ); - } - - /** - * Read extra data associated with the product, like button text or product URL for external products. - * - * @param WC_Product $product Product object. - * @since 3.0.0 - */ - protected function read_extra_data( &$product ) { - foreach ( $product->get_extra_data_keys() as $key ) { - $function = 'set_' . $key; - if ( is_callable( array( $product, $function ) ) ) { - $product->{$function}( get_post_meta( $product->get_id(), '_' . $key, true ) ); - } - } - } - - /** - * Convert visibility terms to props. - * Catalog visibility valid values are 'visible', 'catalog', 'search', and 'hidden'. - * - * @param WC_Product $product Product object. - * @since 3.0.0 - */ - protected function read_visibility( &$product ) { - $terms = get_the_terms( $product->get_id(), 'product_visibility' ); - $term_names = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array(); - $featured = in_array( 'featured', $term_names, true ); - $exclude_search = in_array( 'exclude-from-search', $term_names, true ); - $exclude_catalog = in_array( 'exclude-from-catalog', $term_names, true ); - - if ( $exclude_search && $exclude_catalog ) { - $catalog_visibility = 'hidden'; - } elseif ( $exclude_search ) { - $catalog_visibility = 'catalog'; - } elseif ( $exclude_catalog ) { - $catalog_visibility = 'search'; - } else { - $catalog_visibility = 'visible'; - } - - $product->set_props( - array( - 'featured' => $featured, - 'catalog_visibility' => $catalog_visibility, - ) - ); - } - - /** - * Read attributes from post meta. - * - * @param WC_Product $product Product object. - */ - protected function read_attributes( &$product ) { - $meta_attributes = get_post_meta( $product->get_id(), '_product_attributes', true ); - - if ( ! empty( $meta_attributes ) && is_array( $meta_attributes ) ) { - $attributes = array(); - foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) { - $meta_value = array_merge( - array( - 'name' => '', - 'value' => '', - 'position' => 0, - 'is_visible' => 0, - 'is_variation' => 0, - 'is_taxonomy' => 0, - ), - (array) $meta_attribute_value - ); - - // Check if is a taxonomy attribute. - if ( ! empty( $meta_value['is_taxonomy'] ) ) { - if ( ! taxonomy_exists( $meta_value['name'] ) ) { - continue; - } - $id = wc_attribute_taxonomy_id_by_name( $meta_value['name'] ); - $options = wc_get_object_terms( $product->get_id(), $meta_value['name'], 'term_id' ); - } else { - $id = 0; - $options = wc_get_text_attributes( $meta_value['value'] ); - } - - $attribute = new WC_Product_Attribute(); - $attribute->set_id( $id ); - $attribute->set_name( $meta_value['name'] ); - $attribute->set_options( $options ); - $attribute->set_position( $meta_value['position'] ); - $attribute->set_visible( $meta_value['is_visible'] ); - $attribute->set_variation( $meta_value['is_variation'] ); - $attributes[] = $attribute; - } - $product->set_attributes( $attributes ); - } - } - - /** - * Read downloads from post meta. - * - * @param WC_Product $product Product object. - * @since 3.0.0 - */ - protected function read_downloads( &$product ) { - $meta_values = array_filter( (array) get_post_meta( $product->get_id(), '_downloadable_files', true ) ); - - if ( $meta_values ) { - $downloads = array(); - foreach ( $meta_values as $key => $value ) { - if ( ! isset( $value['name'], $value['file'] ) ) { - continue; - } - $download = new WC_Product_Download(); - $download->set_id( $key ); - $download->set_name( $value['name'] ? $value['name'] : wc_get_filename_from_url( $value['file'] ) ); - $download->set_file( apply_filters( 'woocommerce_file_download_path', $value['file'], $product, $key ) ); - $downloads[] = $download; - } - $product->set_downloads( $downloads ); - } - } - - /** - * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class. - * - * @param WC_Product $product Product object. - * @param bool $force Force update. Used during create. - * @since 3.0.0 - */ - protected function update_post_meta( &$product, $force = false ) { - $meta_key_to_props = array( - '_sku' => 'sku', - '_regular_price' => 'regular_price', - '_sale_price' => 'sale_price', - '_sale_price_dates_from' => 'date_on_sale_from', - '_sale_price_dates_to' => 'date_on_sale_to', - 'total_sales' => 'total_sales', - '_tax_status' => 'tax_status', - '_tax_class' => 'tax_class', - '_manage_stock' => 'manage_stock', - '_backorders' => 'backorders', - '_low_stock_amount' => 'low_stock_amount', - '_sold_individually' => 'sold_individually', - '_weight' => 'weight', - '_length' => 'length', - '_width' => 'width', - '_height' => 'height', - '_upsell_ids' => 'upsell_ids', - '_crosssell_ids' => 'cross_sell_ids', - '_purchase_note' => 'purchase_note', - '_default_attributes' => 'default_attributes', - '_virtual' => 'virtual', - '_downloadable' => 'downloadable', - '_product_image_gallery' => 'gallery_image_ids', - '_download_limit' => 'download_limit', - '_download_expiry' => 'download_expiry', - '_thumbnail_id' => 'image_id', - '_stock' => 'stock_quantity', - '_stock_status' => 'stock_status', - '_wc_average_rating' => 'average_rating', - '_wc_rating_count' => 'rating_counts', - '_wc_review_count' => 'review_count', - ); - - // Make sure to take extra data (like product url or text for external products) into account. - $extra_data_keys = $product->get_extra_data_keys(); - - foreach ( $extra_data_keys as $key ) { - $meta_key_to_props[ '_' . $key ] = $key; - } - - $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props ); - - foreach ( $props_to_update as $meta_key => $prop ) { - $value = $product->{"get_$prop"}( 'edit' ); - $value = is_string( $value ) ? wp_slash( $value ) : $value; - switch ( $prop ) { - case 'virtual': - case 'downloadable': - case 'manage_stock': - case 'sold_individually': - $value = wc_bool_to_string( $value ); - break; - case 'gallery_image_ids': - $value = implode( ',', $value ); - break; - case 'date_on_sale_from': - case 'date_on_sale_to': - $value = $value ? $value->getTimestamp() : ''; - break; - case 'stock_quantity': - // Fire actions to let 3rd parties know the stock is about to be changed. - if ( $product->is_type( 'variation' ) ) { - /** - * Action to signal that the value of 'stock_quantity' for a variation is about to change. - * - * @since 4.9 - * - * @param int $product The variation whose stock is about to change. - */ - do_action( 'woocommerce_variation_before_set_stock', $product ); - } else { - /** - * Action to signal that the value of 'stock_quantity' for a product is about to change. - * - * @since 4.9 - * - * @param int $product The product whose stock is about to change. - */ - do_action( 'woocommerce_product_before_set_stock', $product ); - } - break; - } - - $updated = $this->update_or_delete_post_meta( $product, $meta_key, $value ); - - if ( $updated ) { - $this->updated_props[] = $prop; - } - } - - // Update extra data associated with the product like button text or product URL for external products. - if ( ! $this->extra_data_saved ) { - foreach ( $extra_data_keys as $key ) { - $meta_key = '_' . $key; - $function = 'get_' . $key; - if ( ! array_key_exists( $meta_key, $props_to_update ) ) { - continue; - } - if ( is_callable( array( $product, $function ) ) ) { - $value = $product->{$function}( 'edit' ); - $value = is_string( $value ) ? wp_slash( $value ) : $value; - $updated = $this->update_or_delete_post_meta( $product, $meta_key, $value ); - - if ( $updated ) { - $this->updated_props[] = $key; - } - } - } - } - - if ( $this->update_downloads( $product, $force ) ) { - $this->updated_props[] = 'downloads'; - } - } - - /** - * Handle updated meta props after updating meta data. - * - * @since 3.0.0 - * @param WC_Product $product Product Object. - */ - protected function handle_updated_props( &$product ) { - $price_is_synced = $product->is_type( array( 'variable', 'grouped' ) ); - - if ( ! $price_is_synced ) { - if ( in_array( 'regular_price', $this->updated_props, true ) || in_array( 'sale_price', $this->updated_props, true ) ) { - if ( $product->get_sale_price( 'edit' ) >= $product->get_regular_price( 'edit' ) ) { - update_post_meta( $product->get_id(), '_sale_price', '' ); - $product->set_sale_price( '' ); - } - } - - if ( in_array( 'date_on_sale_from', $this->updated_props, true ) || in_array( 'date_on_sale_to', $this->updated_props, true ) || in_array( 'regular_price', $this->updated_props, true ) || in_array( 'sale_price', $this->updated_props, true ) || in_array( 'product_type', $this->updated_props, true ) ) { - if ( $product->is_on_sale( 'edit' ) ) { - update_post_meta( $product->get_id(), '_price', $product->get_sale_price( 'edit' ) ); - $product->set_price( $product->get_sale_price( 'edit' ) ); - } else { - update_post_meta( $product->get_id(), '_price', $product->get_regular_price( 'edit' ) ); - $product->set_price( $product->get_regular_price( 'edit' ) ); - } - } - } - - if ( in_array( 'stock_quantity', $this->updated_props, true ) ) { - if ( $product->is_type( 'variation' ) ) { - do_action( 'woocommerce_variation_set_stock', $product ); - } else { - do_action( 'woocommerce_product_set_stock', $product ); - } - } - - if ( in_array( 'stock_status', $this->updated_props, true ) ) { - if ( $product->is_type( 'variation' ) ) { - do_action( 'woocommerce_variation_set_stock_status', $product->get_id(), $product->get_stock_status(), $product ); - } else { - do_action( 'woocommerce_product_set_stock_status', $product->get_id(), $product->get_stock_status(), $product ); - } - } - - if ( array_intersect( $this->updated_props, array( 'sku', 'regular_price', 'sale_price', 'date_on_sale_from', 'date_on_sale_to', 'total_sales', 'average_rating', 'stock_quantity', 'stock_status', 'manage_stock', 'downloadable', 'virtual', 'tax_status', 'tax_class' ) ) ) { - $this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' ); - } - - // Trigger action so 3rd parties can deal with updated props. - do_action( 'woocommerce_product_object_updated_props', $product, $this->updated_props ); - - // After handling, we can reset the props array. - $this->updated_props = array(); - } - - /** - * For all stored terms in all taxonomies, save them to the DB. - * - * @param WC_Product $product Product object. - * @param bool $force Force update. Used during create. - * @since 3.0.0 - */ - protected function update_terms( &$product, $force = false ) { - $changes = $product->get_changes(); - - if ( $force || array_key_exists( 'category_ids', $changes ) ) { - $categories = $product->get_category_ids( 'edit' ); - - if ( empty( $categories ) && get_option( 'default_product_cat', 0 ) ) { - $categories = array( get_option( 'default_product_cat', 0 ) ); - } - - wp_set_post_terms( $product->get_id(), $categories, 'product_cat', false ); - } - if ( $force || array_key_exists( 'tag_ids', $changes ) ) { - wp_set_post_terms( $product->get_id(), $product->get_tag_ids( 'edit' ), 'product_tag', false ); - } - if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) { - wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false ); - } - } - - /** - * Update visibility terms based on props. - * - * @since 3.0.0 - * - * @param WC_Product $product Product object. - * @param bool $force Force update. Used during create. - */ - protected function update_visibility( &$product, $force = false ) { - $changes = $product->get_changes(); - - if ( $force || array_intersect( array( 'featured', 'stock_status', 'average_rating', 'catalog_visibility' ), array_keys( $changes ) ) ) { - $terms = array(); - - if ( $product->get_featured() ) { - $terms[] = 'featured'; - } - - if ( 'outofstock' === $product->get_stock_status() ) { - $terms[] = 'outofstock'; - } - - $rating = min( 5, NumberUtil::round( $product->get_average_rating(), 0 ) ); - - if ( $rating > 0 ) { - $terms[] = 'rated-' . $rating; - } - - switch ( $product->get_catalog_visibility() ) { - case 'hidden': - $terms[] = 'exclude-from-search'; - $terms[] = 'exclude-from-catalog'; - break; - case 'catalog': - $terms[] = 'exclude-from-search'; - break; - case 'search': - $terms[] = 'exclude-from-catalog'; - break; - } - - if ( ! is_wp_error( wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false ) ) ) { - do_action( 'woocommerce_product_set_visibility', $product->get_id(), $product->get_catalog_visibility() ); - } - } - } - - /** - * Update attributes which are a mix of terms and meta data. - * - * @param WC_Product $product Product object. - * @param bool $force Force update. Used during create. - * @since 3.0.0 - */ - protected function update_attributes( &$product, $force = false ) { - $changes = $product->get_changes(); - - if ( $force || array_key_exists( 'attributes', $changes ) ) { - $attributes = $product->get_attributes(); - $meta_values = array(); - - if ( $attributes ) { - foreach ( $attributes as $attribute_key => $attribute ) { - $value = ''; - - if ( is_null( $attribute ) ) { - if ( taxonomy_exists( $attribute_key ) ) { - // Handle attributes that have been unset. - wp_set_object_terms( $product->get_id(), array(), $attribute_key ); - } elseif ( taxonomy_exists( urldecode( $attribute_key ) ) ) { - // Handle attributes that have been unset. - wp_set_object_terms( $product->get_id(), array(), urldecode( $attribute_key ) ); - } - continue; - - } elseif ( $attribute->is_taxonomy() ) { - wp_set_object_terms( $product->get_id(), wp_list_pluck( (array) $attribute->get_terms(), 'term_id' ), $attribute->get_name() ); - } else { - $value = wc_implode_text_attributes( $attribute->get_options() ); - } - - // Store in format WC uses in meta. - $meta_values[ $attribute_key ] = array( - 'name' => $attribute->get_name(), - 'value' => $value, - 'position' => $attribute->get_position(), - 'is_visible' => $attribute->get_visible() ? 1 : 0, - 'is_variation' => $attribute->get_variation() ? 1 : 0, - 'is_taxonomy' => $attribute->is_taxonomy() ? 1 : 0, - ); - } - } - // Note, we use wp_slash to add extra level of escaping. See https://codex.wordpress.org/Function_Reference/update_post_meta#Workaround. - $this->update_or_delete_post_meta( $product, '_product_attributes', wp_slash( $meta_values ) ); - } - } - - /** - * Update downloads. - * - * @since 3.0.0 - * @param WC_Product $product Product object. - * @param bool $force Force update. Used during create. - * @return bool If updated or not. - */ - protected function update_downloads( &$product, $force = false ) { - $changes = $product->get_changes(); - - if ( $force || array_key_exists( 'downloads', $changes ) ) { - $downloads = $product->get_downloads(); - $meta_values = array(); - - if ( $downloads ) { - foreach ( $downloads as $key => $download ) { - // Store in format WC uses in meta. - $meta_values[ $key ] = $download->get_data(); - } - } - - if ( $product->is_type( 'variation' ) ) { - do_action( 'woocommerce_process_product_file_download_paths', $product->get_parent_id(), $product->get_id(), $downloads ); - } else { - do_action( 'woocommerce_process_product_file_download_paths', $product->get_id(), 0, $downloads ); - } - - return $this->update_or_delete_post_meta( $product, '_downloadable_files', wp_slash( $meta_values ) ); - } - return false; - } - - /** - * Make sure we store the product type and version (to track data changes). - * - * @param WC_Product $product Product object. - * @since 3.0.0 - */ - protected function update_version_and_type( &$product ) { - $old_type = WC_Product_Factory::get_product_type( $product->get_id() ); - $new_type = $product->get_type(); - - wp_set_object_terms( $product->get_id(), $new_type, 'product_type' ); - update_post_meta( $product->get_id(), '_product_version', Constants::get_constant( 'WC_VERSION' ) ); - - // Action for the transition. - if ( $old_type !== $new_type ) { - $this->updated_props[] = 'product_type'; - do_action( 'woocommerce_product_type_changed', $product, $old_type, $new_type ); - } - } - - /** - * Clear any caches. - * - * @param WC_Product $product Product object. - * @since 3.0.0 - */ - protected function clear_caches( &$product ) { - wc_delete_product_transients( $product->get_id() ); - if ( $product->get_parent_id( 'edit' ) ) { - wc_delete_product_transients( $product->get_parent_id( 'edit' ) ); - WC_Cache_Helper::invalidate_cache_group( 'product_' . $product->get_parent_id( 'edit' ) ); - } - WC_Cache_Helper::invalidate_attribute_count( array_keys( $product->get_attributes() ) ); - WC_Cache_Helper::invalidate_cache_group( 'product_' . $product->get_id() ); - } - - /* - |-------------------------------------------------------------------------- - | wc-product-functions.php methods - |-------------------------------------------------------------------------- - */ - - /** - * Returns an array of on sale products, as an array of objects with an - * ID and parent_id present. Example: $return[0]->id, $return[0]->parent_id. - * - * @return array - * @since 3.0.0 - */ - public function get_on_sale_products() { - global $wpdb; - - $exclude_term_ids = array(); - $outofstock_join = ''; - $outofstock_where = ''; - $non_published_where = ''; - $product_visibility_term_ids = wc_get_product_visibility_term_ids(); - - if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) { - $exclude_term_ids[] = $product_visibility_term_ids['outofstock']; - } - - if ( count( $exclude_term_ids ) ) { - $outofstock_join = " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = id'; - $outofstock_where = ' AND exclude_join.object_id IS NULL'; - } - - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared - return $wpdb->get_results( - " - SELECT posts.ID as id, posts.post_parent as parent_id - FROM {$wpdb->posts} AS posts - INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id - $outofstock_join - WHERE posts.post_type IN ( 'product', 'product_variation' ) - AND posts.post_status = 'publish' - AND lookup.onsale = 1 - $outofstock_where - AND posts.post_parent NOT IN ( - SELECT ID FROM `$wpdb->posts` as posts - WHERE posts.post_type = 'product' - AND posts.post_parent = 0 - AND posts.post_status != 'publish' - ) - GROUP BY posts.ID - " - ); - // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared - } - - /** - * Returns a list of product IDs ( id as key => parent as value) that are - * featured. Uses get_posts instead of wc_get_products since we want - * some extra meta queries and ALL products (posts_per_page = -1). - * - * @return array - * @since 3.0.0 - */ - public function get_featured_product_ids() { - $product_visibility_term_ids = wc_get_product_visibility_term_ids(); - - return get_posts( - array( - 'post_type' => array( 'product', 'product_variation' ), - 'posts_per_page' => -1, - 'post_status' => 'publish', - 'tax_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query - 'relation' => 'AND', - array( - 'taxonomy' => 'product_visibility', - 'field' => 'term_taxonomy_id', - 'terms' => array( $product_visibility_term_ids['featured'] ), - ), - array( - 'taxonomy' => 'product_visibility', - 'field' => 'term_taxonomy_id', - 'terms' => array( $product_visibility_term_ids['exclude-from-catalog'] ), - 'operator' => 'NOT IN', - ), - ), - 'fields' => 'id=>parent', - ) - ); - } - - /** - * Check if product sku is found for any other product IDs. - * - * @since 3.0.0 - * @param int $product_id Product ID. - * @param string $sku Will be slashed to work around https://core.trac.wordpress.org/ticket/27421. - * @return bool - */ - public function is_existing_sku( $product_id, $sku ) { - global $wpdb; - - // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery - return (bool) $wpdb->get_var( - $wpdb->prepare( - " - SELECT posts.ID - FROM {$wpdb->posts} as posts - INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id - WHERE - posts.post_type IN ( 'product', 'product_variation' ) - AND posts.post_status != 'trash' - AND lookup.sku = %s - AND lookup.product_id <> %d - LIMIT 1 - ", - wp_slash( $sku ), - $product_id - ) - ); - } - - /** - * Return product ID based on SKU. - * - * @since 3.0.0 - * @param string $sku Product SKU. - * @return int - */ - public function get_product_id_by_sku( $sku ) { - global $wpdb; - - // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery - $id = $wpdb->get_var( - $wpdb->prepare( - " - SELECT posts.ID - FROM {$wpdb->posts} as posts - INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id - WHERE - posts.post_type IN ( 'product', 'product_variation' ) - AND posts.post_status != 'trash' - AND lookup.sku = %s - LIMIT 1 - ", - $sku - ) - ); - - return (int) apply_filters( 'woocommerce_get_product_id_by_sku', $id, $sku ); - } - - /** - * Returns an array of IDs of products that have sales starting soon. - * - * @since 3.0.0 - * @return array - */ - public function get_starting_sales() { - global $wpdb; - - // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery - return $wpdb->get_col( - $wpdb->prepare( - "SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta - LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id - LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id - WHERE postmeta.meta_key = '_sale_price_dates_from' - AND postmeta_2.meta_key = '_price' - AND postmeta_3.meta_key = '_sale_price' - AND postmeta.meta_value > 0 - AND postmeta.meta_value < %s - AND postmeta_2.meta_value != postmeta_3.meta_value", - time() - ) - ); - } - - /** - * Returns an array of IDs of products that have sales which are due to end. - * - * @since 3.0.0 - * @return array - */ - public function get_ending_sales() { - global $wpdb; - - // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery - return $wpdb->get_col( - $wpdb->prepare( - "SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta - LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id - LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id - WHERE postmeta.meta_key = '_sale_price_dates_to' - AND postmeta_2.meta_key = '_price' - AND postmeta_3.meta_key = '_regular_price' - AND postmeta.meta_value > 0 - AND postmeta.meta_value < %s - AND postmeta_2.meta_value != postmeta_3.meta_value", - time() - ) - ); - } - - /** - * Find a matching (enabled) variation within a variable product. - * - * @since 3.0.0 - * @param WC_Product $product Variable product. - * @param array $match_attributes Array of attributes we want to try to match. - * @return int Matching variation ID or 0. - */ - public function find_matching_product_variation( $product, $match_attributes = array() ) { - global $wpdb; - - $meta_attribute_names = array(); - - // Get attributes to match in meta. - foreach ( $product->get_attributes() as $attribute ) { - if ( ! $attribute->get_variation() ) { - continue; - } - $meta_attribute_names[] = 'attribute_' . sanitize_title( $attribute->get_name() ); - } - - // Get the attributes of the variations. - $query = $wpdb->prepare( - " - SELECT postmeta.post_id, postmeta.meta_key, postmeta.meta_value, posts.menu_order FROM {$wpdb->postmeta} as postmeta - LEFT JOIN {$wpdb->posts} as posts ON postmeta.post_id=posts.ID - WHERE postmeta.post_id IN ( - SELECT ID FROM {$wpdb->posts} - WHERE {$wpdb->posts}.post_parent = %d - AND {$wpdb->posts}.post_status = 'publish' - AND {$wpdb->posts}.post_type = 'product_variation' - ) - ", - $product->get_id() - ); - - $query .= ' AND postmeta.meta_key IN ( "' . implode( '","', array_map( 'esc_sql', $meta_attribute_names ) ) . '" )'; - - $query .= ' ORDER BY posts.menu_order ASC, postmeta.post_id ASC;'; - - $attributes = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - - if ( ! $attributes ) { - return 0; - } - - $sorted_meta = array(); - - foreach ( $attributes as $m ) { - $sorted_meta[ $m->post_id ][ $m->meta_key ] = $m->meta_value; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key - } - - /** - * Check each variation to find the one that matches the $match_attributes. - * - * Note: Not all meta fields will be set which is why we check existance. - */ - foreach ( $sorted_meta as $variation_id => $variation ) { - $match = true; - - // Loop over the variation meta keys and values i.e. what is saved to the products. Note: $attribute_value is empty when 'any' is in use. - foreach ( $variation as $attribute_key => $attribute_value ) { - $match_any_value = '' === $attribute_value; - - if ( ! $match_any_value && ! array_key_exists( $attribute_key, $match_attributes ) ) { - $match = false; // Requires a selection but no value was provide. - } - - if ( array_key_exists( $attribute_key, $match_attributes ) ) { // Value to match was provided. - if ( ! $match_any_value && $match_attributes[ $attribute_key ] !== $attribute_value ) { - $match = false; // Provided value does not match variation. - } - } - } - - if ( true === $match ) { - return $variation_id; - } - } - - if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) { - /** - * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute. - * Fallback is here because there are cases where data will be 'synced' but the product version will remain the same. - */ - return ( array_map( 'sanitize_title', $match_attributes ) === $match_attributes ) ? 0 : $this->find_matching_product_variation( $product, array_map( 'sanitize_title', $match_attributes ) ); - } - - return 0; - } - - /** - * Creates all possible combinations of variations from the attributes, without creating duplicates. - * - * @since 3.6.0 - * @todo Add to interface in 4.0. - * @param WC_Product $product Variable product. - * @param int $limit Limit the number of created variations. - * @return int Number of created variations. - */ - public function create_all_product_variations( $product, $limit = -1 ) { - $count = 0; - - if ( ! $product ) { - return $count; - } - - $attributes = wc_list_pluck( array_filter( $product->get_attributes(), 'wc_attributes_array_filter_variation' ), 'get_slugs' ); - - if ( empty( $attributes ) ) { - return $count; - } - - // Get existing variations so we don't create duplicates. - $existing_variations = array_map( 'wc_get_product', $product->get_children() ); - $existing_attributes = array(); - - foreach ( $existing_variations as $existing_variation ) { - $existing_attributes[] = $existing_variation->get_attributes(); - } - - $possible_attributes = array_reverse( wc_array_cartesian( $attributes ) ); - - foreach ( $possible_attributes as $possible_attribute ) { - // Allow any order if key/values -- do not use strict mode. - if ( in_array( $possible_attribute, $existing_attributes ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict - continue; - } - $variation = wc_get_product_object( 'variation' ); - $variation->set_parent_id( $product->get_id() ); - $variation->set_attributes( $possible_attribute ); - $variation_id = $variation->save(); - - do_action( 'product_variation_linked', $variation_id ); - - $count ++; - - if ( $limit > 0 && $count >= $limit ) { - break; - } - } - - return $count; - } - - /** - * Make sure all variations have a sort order set so they can be reordered correctly. - * - * @param int $parent_id Product ID. - */ - public function sort_all_product_variations( $parent_id ) { - global $wpdb; - - // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery - $ids = $wpdb->get_col( - $wpdb->prepare( - "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product_variation' AND post_parent = %d AND post_status = 'publish' ORDER BY menu_order ASC, ID ASC", - $parent_id - ) - ); - $index = 1; - - foreach ( $ids as $id ) { - // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery - $wpdb->update( $wpdb->posts, array( 'menu_order' => ( $index++ ) ), array( 'ID' => absint( $id ) ) ); - } - } - - /** - * Return a list of related products (using data like categories and IDs). - * - * @since 3.0.0 - * @param array $cats_array List of categories IDs. - * @param array $tags_array List of tags IDs. - * @param array $exclude_ids Excluded IDs. - * @param int $limit Limit of results. - * @param int $product_id Product ID. - * @return array - */ - public function get_related_products( $cats_array, $tags_array, $exclude_ids, $limit, $product_id ) { - global $wpdb; - - $args = array( - 'categories' => $cats_array, - 'tags' => $tags_array, - 'exclude_ids' => $exclude_ids, - 'limit' => $limit + 10, - ); - - $related_product_query = (array) apply_filters( 'woocommerce_product_related_posts_query', $this->get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit + 10 ), $product_id, $args ); - - // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared - return $wpdb->get_col( implode( ' ', $related_product_query ) ); - } - - /** - * Builds the related posts query. - * - * @since 3.0.0 - * - * @param array $cats_array List of categories IDs. - * @param array $tags_array List of tags IDs. - * @param array $exclude_ids Excluded IDs. - * @param int $limit Limit of results. - * - * @return array - */ - public function get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit ) { - global $wpdb; - - $include_term_ids = array_merge( $cats_array, $tags_array ); - $exclude_term_ids = array(); - $product_visibility_term_ids = wc_get_product_visibility_term_ids(); - - if ( $product_visibility_term_ids['exclude-from-catalog'] ) { - $exclude_term_ids[] = $product_visibility_term_ids['exclude-from-catalog']; - } - - if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) { - $exclude_term_ids[] = $product_visibility_term_ids['outofstock']; - } - - $query = array( - 'fields' => " - SELECT DISTINCT ID FROM {$wpdb->posts} p - ", - 'join' => '', - 'where' => " - WHERE 1=1 - AND p.post_status = 'publish' - AND p.post_type = 'product' - - ", - 'limits' => ' - LIMIT ' . absint( $limit ) . ' - ', - ); - - if ( count( $exclude_term_ids ) ) { - $query['join'] .= " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = p.ID'; - $query['where'] .= ' AND exclude_join.object_id IS NULL'; - } - - if ( count( $include_term_ids ) ) { - $query['join'] .= " INNER JOIN ( SELECT object_id FROM {$wpdb->term_relationships} INNER JOIN {$wpdb->term_taxonomy} using( term_taxonomy_id ) WHERE term_id IN ( " . implode( ',', array_map( 'absint', $include_term_ids ) ) . ' ) ) AS include_join ON include_join.object_id = p.ID'; - } - - if ( count( $exclude_ids ) ) { - $query['where'] .= ' AND p.ID NOT IN ( ' . implode( ',', array_map( 'absint', $exclude_ids ) ) . ' )'; - } - - return $query; - } - - /** - * Update a product's stock amount directly in the database. - * - * Updates both post meta and lookup tables. Ignores manage stock setting on the product. - * - * @param int $product_id_with_stock Product ID. - * @param int|float|null $stock_quantity Stock quantity. - */ - protected function set_product_stock( $product_id_with_stock, $stock_quantity ) { - global $wpdb; - - // Generate SQL. - $sql = $wpdb->prepare( - "UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'", - $stock_quantity, - $product_id_with_stock - ); - - $sql = apply_filters( 'woocommerce_update_product_stock_query', $sql, $product_id_with_stock, $stock_quantity, 'set' ); - - $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared - - // Cache delete is required (not only) to set correct data for lookup table (which reads from cache). - // Sometimes I wonder if it shouldn't be part of update_lookup_table. - wp_cache_delete( $product_id_with_stock, 'post_meta' ); - - $this->update_lookup_table( $product_id_with_stock, 'wc_product_meta_lookup' ); - } - - /** - * Update a product's stock amount directly. - * - * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues). - * Ignores manage stock setting on the product and sets quantities directly in the db: post meta and lookup tables. - * Uses locking to update the quantity. If the lock is not acquired, change is lost. - * - * @since 3.0.0 this supports set, increase and decrease. - * @param int $product_id_with_stock Product ID. - * @param int|float|null $stock_quantity Stock quantity. - * @param string $operation Set, increase and decrease. - * @return int|float New stock level. - */ - public function update_product_stock( $product_id_with_stock, $stock_quantity = null, $operation = 'set' ) { - global $wpdb; - - // Ensures a row exists to update. - add_post_meta( $product_id_with_stock, '_stock', 0, true ); - - if ( 'set' === $operation ) { - $new_stock = wc_stock_amount( $stock_quantity ); - - // Generate SQL. - $sql = $wpdb->prepare( - "UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'", - $new_stock, - $product_id_with_stock - ); - } else { - $current_stock = wc_stock_amount( - $wpdb->get_var( - $wpdb->prepare( - "SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key='_stock';", - $product_id_with_stock - ) - ) - ); - - // Calculate new value for filter below. Set multiplier to subtract or add the meta_value. - switch ( $operation ) { - case 'increase': - $new_stock = $current_stock + wc_stock_amount( $stock_quantity ); - $multiplier = 1; - break; - default: - $new_stock = $current_stock - wc_stock_amount( $stock_quantity ); - $multiplier = -1; - break; - } - - // Generate SQL. - $sql = $wpdb->prepare( - "UPDATE {$wpdb->postmeta} SET meta_value = meta_value %+f WHERE post_id = %d AND meta_key='_stock'", - wc_stock_amount( $stock_quantity ) * $multiplier, // This will either subtract or add depending on operation. - $product_id_with_stock - ); - } - - $sql = apply_filters( 'woocommerce_update_product_stock_query', $sql, $product_id_with_stock, $new_stock, $operation ); - - $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared - - // Cache delete is required (not only) to set correct data for lookup table (which reads from cache). - // Sometimes I wonder if it shouldn't be part of update_lookup_table. - wp_cache_delete( $product_id_with_stock, 'post_meta' ); - - $this->update_lookup_table( $product_id_with_stock, 'wc_product_meta_lookup' ); - - /** - * Fire an action for this direct update so it can be detected by other code. - * - * @since 3.6 - * @param int $product_id_with_stock Product ID that was updated directly. - */ - do_action( 'woocommerce_updated_product_stock', $product_id_with_stock ); - - return $new_stock; - } - - /** - * Update a product's sale count directly. - * - * Uses queries rather than update_post_meta so we can do this in one query for performance. - * - * @since 3.0.0 this supports set, increase and decrease. - * @param int $product_id Product ID. - * @param int|null $quantity Quantity. - * @param string $operation set, increase and decrease. - */ - public function update_product_sales( $product_id, $quantity = null, $operation = 'set' ) { - global $wpdb; - add_post_meta( $product_id, 'total_sales', 0, true ); - - // Update stock in DB directly. - switch ( $operation ) { - case 'increase': - // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery - $wpdb->query( - $wpdb->prepare( - "UPDATE {$wpdb->postmeta} SET meta_value = meta_value + %f WHERE post_id = %d AND meta_key='total_sales'", - $quantity, - $product_id - ) - ); - break; - case 'decrease': - // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery - $wpdb->query( - $wpdb->prepare( - "UPDATE {$wpdb->postmeta} SET meta_value = meta_value - %f WHERE post_id = %d AND meta_key='total_sales'", - $quantity, - $product_id - ) - ); - break; - default: - // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery - $wpdb->query( - $wpdb->prepare( - "UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='total_sales'", - $quantity, - $product_id - ) - ); - break; - } - - wp_cache_delete( $product_id, 'post_meta' ); - - $this->update_lookup_table( $product_id, 'wc_product_meta_lookup' ); - - /** - * Fire an action for this direct update so it can be detected by other code. - * - * @since 3.6 - * @param int $product_id Product ID that was updated directly. - */ - do_action( 'woocommerce_updated_product_sales', $product_id ); - } - - /** - * Update a products average rating meta. - * - * @since 3.0.0 - * @todo Deprecate unused function? - * @param WC_Product $product Product object. - */ - public function update_average_rating( $product ) { - update_post_meta( $product->get_id(), '_wc_average_rating', $product->get_average_rating( 'edit' ) ); - self::update_visibility( $product, true ); - } - - /** - * Update a products review count meta. - * - * @since 3.0.0 - * @todo Deprecate unused function? - * @param WC_Product $product Product object. - */ - public function update_review_count( $product ) { - update_post_meta( $product->get_id(), '_wc_review_count', $product->get_review_count( 'edit' ) ); - } - - /** - * Update a products rating counts. - * - * @since 3.0.0 - * @todo Deprecate unused function? - * @param WC_Product $product Product object. - */ - public function update_rating_counts( $product ) { - update_post_meta( $product->get_id(), '_wc_rating_count', $product->get_rating_counts( 'edit' ) ); - } - - /** - * Get shipping class ID by slug. - * - * @since 3.0.0 - * @param string $slug Product shipping class slug. - * @return int|false - */ - public function get_shipping_class_id_by_slug( $slug ) { - $shipping_class_term = get_term_by( 'slug', $slug, 'product_shipping_class' ); - if ( $shipping_class_term ) { - return $shipping_class_term->term_id; - } else { - return false; - } - } - - /** - * Returns an array of products. - * - * @param array $args Args to pass to WC_Product_Query(). - * @return array|object - * @see wc_get_products - */ - public function get_products( $args = array() ) { - $query = new WC_Product_Query( $args ); - return $query->get_products(); - } - - /** - * Search product data for a term and return ids. - * - * @param string $term Search term. - * @param string $type Type of product. - * @param bool $include_variations Include variations in search or not. - * @param bool $all_statuses Should we search all statuses or limit to published. - * @param null|int $limit Limit returned results. @since 3.5.0. - * @param null|array $include Keep specific results. @since 3.6.0. - * @param null|array $exclude Discard specific results. @since 3.6.0. - * @return array of ids - */ - public function search_products( $term, $type = '', $include_variations = false, $all_statuses = false, $limit = null, $include = null, $exclude = null ) { - global $wpdb; - - $custom_results = apply_filters( 'woocommerce_product_pre_search_products', false, $term, $type, $include_variations, $all_statuses, $limit ); - - if ( is_array( $custom_results ) ) { - return $custom_results; - } - - $post_types = $include_variations ? array( 'product', 'product_variation' ) : array( 'product' ); - $join_query = ''; - $type_where = ''; - $status_where = ''; - $limit_query = ''; - - // When searching variations we should include the parent's meta table for use in searches. - if ( $include_variations ) { - $join_query = " LEFT JOIN {$wpdb->wc_product_meta_lookup} parent_wc_product_meta_lookup - ON posts.post_type = 'product_variation' AND parent_wc_product_meta_lookup.product_id = posts.post_parent "; - } - - /** - * Hook woocommerce_search_products_post_statuses. - * - * @since 3.7.0 - * @param array $post_statuses List of post statuses. - */ - $post_statuses = apply_filters( - 'woocommerce_search_products_post_statuses', - current_user_can( 'edit_private_products' ) ? array( 'private', 'publish' ) : array( 'publish' ) - ); - - // See if search term contains OR keywords. - if ( stristr( $term, ' or ' ) ) { - $term_groups = preg_split( '/\s+or\s+/i', $term ); - } else { - $term_groups = array( $term ); - } - - $search_where = ''; - $search_queries = array(); - - foreach ( $term_groups as $term_group ) { - // Parse search terms. - if ( preg_match_all( '/".*?("|$)|((?<=[\t ",+])|^)[^\t ",+]+/', $term_group, $matches ) ) { - $search_terms = $this->get_valid_search_terms( $matches[0] ); - $count = count( $search_terms ); - - // if the search string has only short terms or stopwords, or is 10+ terms long, match it as sentence. - if ( 9 < $count || 0 === $count ) { - $search_terms = array( $term_group ); - } - } else { - $search_terms = array( $term_group ); - } - - $term_group_query = ''; - $searchand = ''; - - foreach ( $search_terms as $search_term ) { - $like = '%' . $wpdb->esc_like( $search_term ) . '%'; - - // Variations should also search the parent's meta table for fallback fields. - if ( $include_variations ) { - $variation_query = $wpdb->prepare( " OR ( wc_product_meta_lookup.sku = '' AND parent_wc_product_meta_lookup.sku LIKE %s ) ", $like ); - } else { - $variation_query = ''; - } - - $term_group_query .= $wpdb->prepare( " {$searchand} ( ( posts.post_title LIKE %s) OR ( posts.post_excerpt LIKE %s) OR ( posts.post_content LIKE %s ) OR ( wc_product_meta_lookup.sku LIKE %s ) $variation_query)", $like, $like, $like, $like ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $searchand = ' AND '; - } - - if ( $term_group_query ) { - $search_queries[] = $term_group_query; - } - } - - if ( ! empty( $search_queries ) ) { - $search_where = ' AND (' . implode( ') OR (', $search_queries ) . ') '; - } - - if ( ! empty( $include ) && is_array( $include ) ) { - $search_where .= ' AND posts.ID IN(' . implode( ',', array_map( 'absint', $include ) ) . ') '; - } - - if ( ! empty( $exclude ) && is_array( $exclude ) ) { - $search_where .= ' AND posts.ID NOT IN(' . implode( ',', array_map( 'absint', $exclude ) ) . ') '; - } - - if ( 'virtual' === $type ) { - $type_where = ' AND ( wc_product_meta_lookup.virtual = 1 ) '; - } elseif ( 'downloadable' === $type ) { - $type_where = ' AND ( wc_product_meta_lookup.downloadable = 1 ) '; - } - - if ( ! $all_statuses ) { - $status_where = " AND posts.post_status IN ('" . implode( "','", $post_statuses ) . "') "; - } - - if ( $limit ) { - $limit_query = $wpdb->prepare( ' LIMIT %d ', $limit ); - } - - // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery - $search_results = $wpdb->get_results( - // phpcs:disable - "SELECT DISTINCT posts.ID as product_id, posts.post_parent as parent_id FROM {$wpdb->posts} posts - LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON posts.ID = wc_product_meta_lookup.product_id - $join_query - WHERE posts.post_type IN ('" . implode( "','", $post_types ) . "') - $search_where - $status_where - $type_where - ORDER BY posts.post_parent ASC, posts.post_title ASC - $limit_query - " - // phpcs:enable - ); - - $product_ids = wp_parse_id_list( array_merge( wp_list_pluck( $search_results, 'product_id' ), wp_list_pluck( $search_results, 'parent_id' ) ) ); - - if ( is_numeric( $term ) ) { - $post_id = absint( $term ); - $post_type = get_post_type( $post_id ); - - if ( 'product_variation' === $post_type && $include_variations ) { - $product_ids[] = $post_id; - } elseif ( 'product' === $post_type ) { - $product_ids[] = $post_id; - } - - $product_ids[] = wp_get_post_parent_id( $post_id ); - } - - return wp_parse_id_list( $product_ids ); - } - - /** - * Get the product type based on product ID. - * - * @since 3.0.0 - * @param int $product_id Product ID. - * @return bool|string - */ - public function get_product_type( $product_id ) { - $cache_key = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . '_type_' . $product_id; - $product_type = wp_cache_get( $cache_key, 'products' ); - - if ( $product_type ) { - return $product_type; - } - - $post_type = get_post_type( $product_id ); - - if ( 'product_variation' === $post_type ) { - $product_type = 'variation'; - } elseif ( 'product' === $post_type ) { - $terms = get_the_terms( $product_id, 'product_type' ); - $product_type = ! empty( $terms ) && ! is_wp_error( $terms ) ? sanitize_title( current( $terms )->name ) : 'simple'; - } else { - $product_type = false; - } - - wp_cache_set( $cache_key, $product_type, 'products' ); - - return $product_type; - } - - /** - * Add ability to get products by 'reviews_allowed' in WC_Product_Query. - * - * @since 3.2.0 - * @param string $where Where clause. - * @param WP_Query $wp_query WP_Query instance. - * @return string - */ - public function reviews_allowed_query_where( $where, $wp_query ) { - global $wpdb; - - if ( isset( $wp_query->query_vars['reviews_allowed'] ) && is_bool( $wp_query->query_vars['reviews_allowed'] ) ) { - if ( $wp_query->query_vars['reviews_allowed'] ) { - $where .= " AND $wpdb->posts.comment_status = 'open'"; - } else { - $where .= " AND $wpdb->posts.comment_status = 'closed'"; - } - } - - return $where; - } - - /** - * Get valid WP_Query args from a WC_Product_Query's query variables. - * - * @since 3.2.0 - * @param array $query_vars Query vars from a WC_Product_Query. - * @return array - */ - protected function get_wp_query_args( $query_vars ) { - - // Map query vars to ones that get_wp_query_args or WP_Query recognize. - $key_mapping = array( - 'status' => 'post_status', - 'page' => 'paged', - 'include' => 'post__in', - 'stock_quantity' => 'stock', - 'average_rating' => 'wc_average_rating', - 'review_count' => 'wc_review_count', - ); - foreach ( $key_mapping as $query_key => $db_key ) { - if ( isset( $query_vars[ $query_key ] ) ) { - $query_vars[ $db_key ] = $query_vars[ $query_key ]; - unset( $query_vars[ $query_key ] ); - } - } - - // Map boolean queries that are stored as 'yes'/'no' in the DB to 'yes' or 'no'. - $boolean_queries = array( - 'virtual', - 'downloadable', - 'sold_individually', - 'manage_stock', - ); - foreach ( $boolean_queries as $boolean_query ) { - if ( isset( $query_vars[ $boolean_query ] ) && '' !== $query_vars[ $boolean_query ] ) { - $query_vars[ $boolean_query ] = $query_vars[ $boolean_query ] ? 'yes' : 'no'; - } - } - - // These queries cannot be auto-generated so we have to remove them and build them manually. - $manual_queries = array( - 'sku' => '', - 'featured' => '', - 'visibility' => '', - ); - foreach ( $manual_queries as $key => $manual_query ) { - if ( isset( $query_vars[ $key ] ) ) { - $manual_queries[ $key ] = $query_vars[ $key ]; - unset( $query_vars[ $key ] ); - } - } - - $wp_query_args = parent::get_wp_query_args( $query_vars ); - - if ( ! isset( $wp_query_args['date_query'] ) ) { - $wp_query_args['date_query'] = array(); - } - if ( ! isset( $wp_query_args['meta_query'] ) ) { - $wp_query_args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - } - - // Handle product types. - if ( 'variation' === $query_vars['type'] ) { - $wp_query_args['post_type'] = 'product_variation'; - } elseif ( is_array( $query_vars['type'] ) && in_array( 'variation', $query_vars['type'], true ) ) { - $wp_query_args['post_type'] = array( 'product_variation', 'product' ); - $wp_query_args['tax_query'][] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query - 'relation' => 'OR', - array( - 'taxonomy' => 'product_type', - 'field' => 'slug', - 'terms' => $query_vars['type'], - ), - array( - 'taxonomy' => 'product_type', - 'field' => 'id', - 'operator' => 'NOT EXISTS', - ), - ); - } else { - $wp_query_args['post_type'] = 'product'; - $wp_query_args['tax_query'][] = array( - 'taxonomy' => 'product_type', - 'field' => 'slug', - 'terms' => $query_vars['type'], - ); - } - - // Handle product categories. - if ( ! empty( $query_vars['category'] ) ) { - $wp_query_args['tax_query'][] = array( - 'taxonomy' => 'product_cat', - 'field' => 'slug', - 'terms' => $query_vars['category'], - ); - } - - // Handle product tags. - if ( ! empty( $query_vars['tag'] ) ) { - unset( $wp_query_args['tag'] ); - $wp_query_args['tax_query'][] = array( - 'taxonomy' => 'product_tag', - 'field' => 'slug', - 'terms' => $query_vars['tag'], - ); - } - - // Handle shipping classes. - if ( ! empty( $query_vars['shipping_class'] ) ) { - $wp_query_args['tax_query'][] = array( - 'taxonomy' => 'product_shipping_class', - 'field' => 'slug', - 'terms' => $query_vars['shipping_class'], - ); - } - - // Handle total_sales. - // This query doesn't get auto-generated since the meta key doesn't have the underscore prefix. - if ( isset( $query_vars['total_sales'] ) && '' !== $query_vars['total_sales'] ) { - $wp_query_args['meta_query'][] = array( - 'key' => 'total_sales', - 'value' => absint( $query_vars['total_sales'] ), - 'compare' => '=', - ); - } - - // Handle SKU. - if ( $manual_queries['sku'] ) { - // Check for existing values if wildcard is used. - if ( '*' === $manual_queries['sku'] ) { - $wp_query_args['meta_query'][] = array( - array( - 'key' => '_sku', - 'compare' => 'EXISTS', - ), - array( - 'key' => '_sku', - 'value' => '', - 'compare' => '!=', - ), - ); - } else { - $wp_query_args['meta_query'][] = array( - 'key' => '_sku', - 'value' => $manual_queries['sku'], - 'compare' => 'LIKE', - ); - } - } - - // Handle featured. - if ( '' !== $manual_queries['featured'] ) { - $product_visibility_term_ids = wc_get_product_visibility_term_ids(); - if ( $manual_queries['featured'] ) { - $wp_query_args['tax_query'][] = array( - 'taxonomy' => 'product_visibility', - 'field' => 'term_taxonomy_id', - 'terms' => array( $product_visibility_term_ids['featured'] ), - ); - $wp_query_args['tax_query'][] = array( - 'taxonomy' => 'product_visibility', - 'field' => 'term_taxonomy_id', - 'terms' => array( $product_visibility_term_ids['exclude-from-catalog'] ), - 'operator' => 'NOT IN', - ); - } else { - $wp_query_args['tax_query'][] = array( - 'taxonomy' => 'product_visibility', - 'field' => 'term_taxonomy_id', - 'terms' => array( $product_visibility_term_ids['featured'] ), - 'operator' => 'NOT IN', - ); - } - } - - // Handle visibility. - if ( $manual_queries['visibility'] ) { - switch ( $manual_queries['visibility'] ) { - case 'search': - $wp_query_args['tax_query'][] = array( - 'taxonomy' => 'product_visibility', - 'field' => 'slug', - 'terms' => array( 'exclude-from-search' ), - 'operator' => 'NOT IN', - ); - break; - case 'catalog': - $wp_query_args['tax_query'][] = array( - 'taxonomy' => 'product_visibility', - 'field' => 'slug', - 'terms' => array( 'exclude-from-catalog' ), - 'operator' => 'NOT IN', - ); - break; - case 'visible': - $wp_query_args['tax_query'][] = array( - 'taxonomy' => 'product_visibility', - 'field' => 'slug', - 'terms' => array( 'exclude-from-catalog', 'exclude-from-search' ), - 'operator' => 'NOT IN', - ); - break; - case 'hidden': - $wp_query_args['tax_query'][] = array( - 'taxonomy' => 'product_visibility', - 'field' => 'slug', - 'terms' => array( 'exclude-from-catalog', 'exclude-from-search' ), - 'operator' => 'AND', - ); - break; - } - } - - // Handle date queries. - $date_queries = array( - 'date_created' => 'post_date', - 'date_modified' => 'post_modified', - 'date_on_sale_from' => '_sale_price_dates_from', - 'date_on_sale_to' => '_sale_price_dates_to', - ); - foreach ( $date_queries as $query_var_key => $db_key ) { - if ( isset( $query_vars[ $query_var_key ] ) && '' !== $query_vars[ $query_var_key ] ) { - - // Remove any existing meta queries for the same keys to prevent conflicts. - $existing_queries = wp_list_pluck( $wp_query_args['meta_query'], 'key', true ); - foreach ( $existing_queries as $query_index => $query_contents ) { - unset( $wp_query_args['meta_query'][ $query_index ] ); - } - - $wp_query_args = $this->parse_date_for_wp_query( $query_vars[ $query_var_key ], $db_key, $wp_query_args ); - } - } - - // Handle paginate. - if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) { - $wp_query_args['no_found_rows'] = true; - } - - // Handle reviews_allowed. - if ( isset( $query_vars['reviews_allowed'] ) && is_bool( $query_vars['reviews_allowed'] ) ) { - add_filter( 'posts_where', array( $this, 'reviews_allowed_query_where' ), 10, 2 ); - } - - // Handle orderby. - if ( isset( $query_vars['orderby'] ) && 'include' === $query_vars['orderby'] ) { - $wp_query_args['orderby'] = 'post__in'; - } - - return apply_filters( 'woocommerce_product_data_store_cpt_get_products_query', $wp_query_args, $query_vars, $this ); - } - - /** - * Query for Products matching specific criteria. - * - * @since 3.2.0 - * - * @param array $query_vars Query vars from a WC_Product_Query. - * - * @return array|object - */ - public function query( $query_vars ) { - $args = $this->get_wp_query_args( $query_vars ); - - if ( ! empty( $args['errors'] ) ) { - $query = (object) array( - 'posts' => array(), - 'found_posts' => 0, - 'max_num_pages' => 0, - ); - } else { - $query = new WP_Query( $args ); - } - - if ( isset( $query_vars['return'] ) && 'objects' === $query_vars['return'] && ! empty( $query->posts ) ) { - // Prime caches before grabbing objects. - update_post_caches( $query->posts, array( 'product', 'product_variation' ) ); - } - - $products = ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) ? $query->posts : array_filter( array_map( 'wc_get_product', $query->posts ) ); - - if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) { - return (object) array( - 'products' => $products, - 'total' => $query->found_posts, - 'max_num_pages' => $query->max_num_pages, - ); - } - - return $products; - } - - /** - * Get data to save to a lookup table. - * - * @since 3.6.0 - * @param int $id ID of object to update. - * @param string $table Lookup table name. - * @return array - */ - protected function get_data_for_lookup_table( $id, $table ) { - if ( 'wc_product_meta_lookup' === $table ) { - $price_meta = (array) get_post_meta( $id, '_price', false ); - $manage_stock = get_post_meta( $id, '_manage_stock', true ); - $stock = 'yes' === $manage_stock ? wc_stock_amount( get_post_meta( $id, '_stock', true ) ) : null; - $price = wc_format_decimal( get_post_meta( $id, '_price', true ) ); - $sale_price = wc_format_decimal( get_post_meta( $id, '_sale_price', true ) ); - return array( - 'product_id' => absint( $id ), - 'sku' => get_post_meta( $id, '_sku', true ), - 'virtual' => 'yes' === get_post_meta( $id, '_virtual', true ) ? 1 : 0, - 'downloadable' => 'yes' === get_post_meta( $id, '_downloadable', true ) ? 1 : 0, - 'min_price' => reset( $price_meta ), - 'max_price' => end( $price_meta ), - 'onsale' => $sale_price && $price === $sale_price ? 1 : 0, - 'stock_quantity' => $stock, - 'stock_status' => get_post_meta( $id, '_stock_status', true ), - 'rating_count' => array_sum( (array) get_post_meta( $id, '_wc_rating_count', true ) ), - 'average_rating' => get_post_meta( $id, '_wc_average_rating', true ), - 'total_sales' => get_post_meta( $id, 'total_sales', true ), - 'tax_status' => get_post_meta( $id, '_tax_status', true ), - 'tax_class' => get_post_meta( $id, '_tax_class', true ), - ); - } - return array(); - } - - /** - * Get primary key name for lookup table. - * - * @since 3.6.0 - * @param string $table Lookup table name. - * @return string - */ - protected function get_primary_key_for_lookup_table( $table ) { - if ( 'wc_product_meta_lookup' === $table ) { - return 'product_id'; - } - return ''; - } - - /** - * Returns query statement for getting current `_stock` of a product. - * - * @internal MAX function below is used to make sure result is a scalar. - * @param int $product_id Product ID. - * @return string|void Query statement. - */ - public function get_query_for_stock( $product_id ) { - global $wpdb; - return $wpdb->prepare( - " - SELECT COALESCE ( MAX( meta_value ), 0 ) FROM $wpdb->postmeta as meta_table - WHERE meta_table.meta_key = '_stock' - AND meta_table.post_id = %d - ", - $product_id - ); - } -} diff --git a/includes/data-stores/class-wc-product-variable-data-store-cpt.php b/includes/data-stores/class-wc-product-variable-data-store-cpt.php deleted file mode 100644 index eeece0d2d61..00000000000 --- a/includes/data-stores/class-wc-product-variable-data-store-cpt.php +++ /dev/null @@ -1,701 +0,0 @@ -get_id(), '_product_attributes', true ); - - if ( ! empty( $meta_attributes ) && is_array( $meta_attributes ) ) { - $attributes = array(); - $force_update = false; - foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) { - $meta_value = array_merge( - array( - 'name' => '', - 'value' => '', - 'position' => 0, - 'is_visible' => 0, - 'is_variation' => 0, - 'is_taxonomy' => 0, - ), - (array) $meta_attribute_value - ); - - // Maintain data integrity. 4.9 changed sanitization functions - update the values here so variations function correctly. - if ( $meta_value['is_variation'] && strstr( $meta_value['name'], '/' ) && sanitize_title( $meta_value['name'] ) !== $meta_attribute_key ) { - global $wpdb; - - $old_slug = 'attribute_' . $meta_attribute_key; - $new_slug = 'attribute_' . sanitize_title( $meta_value['name'] ); - $old_meta_rows = $wpdb->get_results( $wpdb->prepare( "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s;", $old_slug ) ); // WPCS: db call ok, cache ok. - - if ( $old_meta_rows ) { - foreach ( $old_meta_rows as $old_meta_row ) { - update_post_meta( $old_meta_row->post_id, $new_slug, $old_meta_row->meta_value ); - } - } - - $force_update = true; - } - - // Check if is a taxonomy attribute. - if ( ! empty( $meta_value['is_taxonomy'] ) ) { - if ( ! taxonomy_exists( $meta_value['name'] ) ) { - continue; - } - $id = wc_attribute_taxonomy_id_by_name( $meta_value['name'] ); - $options = wc_get_object_terms( $product->get_id(), $meta_value['name'], 'term_id' ); - } else { - $id = 0; - $options = wc_get_text_attributes( $meta_value['value'] ); - } - - $attribute = new WC_Product_Attribute(); - $attribute->set_id( $id ); - $attribute->set_name( $meta_value['name'] ); - $attribute->set_options( $options ); - $attribute->set_position( $meta_value['position'] ); - $attribute->set_visible( $meta_value['is_visible'] ); - $attribute->set_variation( $meta_value['is_variation'] ); - $attributes[] = $attribute; - } - $product->set_attributes( $attributes ); - - if ( $force_update ) { - $this->update_attributes( $product, true ); - } - } - } - - /** - * Read product data. - * - * @param WC_Product $product Product object. - * - * @since 3.0.0 - */ - protected function read_product_data( &$product ) { - parent::read_product_data( $product ); - - // Make sure data which does not apply to variables is unset. - $product->set_regular_price( '' ); - $product->set_sale_price( '' ); - } - - /** - * Loads variation child IDs. - * - * @param WC_Product $product Product object. - * @param bool $force_read True to bypass the transient. - * - * @return array - */ - public function read_children( &$product, $force_read = false ) { - $children_transient_name = 'wc_product_children_' . $product->get_id(); - $children = get_transient( $children_transient_name ); - - if ( empty( $children ) || ! is_array( $children ) || ! isset( $children['all'] ) || ! isset( $children['visible'] ) || $force_read ) { - $all_args = array( - 'post_parent' => $product->get_id(), - 'post_type' => 'product_variation', - 'orderby' => array( - 'menu_order' => 'ASC', - 'ID' => 'ASC', - ), - 'fields' => 'ids', - 'post_status' => array( 'publish', 'private' ), - 'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts - ); - - $visible_only_args = $all_args; - $visible_only_args['post_status'] = 'publish'; - - if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { - $visible_only_args['tax_query'][] = array( - 'taxonomy' => 'product_visibility', - 'field' => 'name', - 'terms' => 'outofstock', - 'operator' => 'NOT IN', - ); - } - $children['all'] = get_posts( apply_filters( 'woocommerce_variable_children_args', $all_args, $product, false ) ); - $children['visible'] = get_posts( apply_filters( 'woocommerce_variable_children_args', $visible_only_args, $product, true ) ); - - set_transient( $children_transient_name, $children, DAY_IN_SECONDS * 30 ); - } - - $children['all'] = wp_parse_id_list( (array) $children['all'] ); - $children['visible'] = wp_parse_id_list( (array) $children['visible'] ); - - return $children; - } - - /** - * Loads an array of attributes used for variations, as well as their possible values. - * - * @param WC_Product $product Product object. - * - * @return array - */ - public function read_variation_attributes( &$product ) { - global $wpdb; - - $variation_attributes = array(); - $attributes = $product->get_attributes(); - $child_ids = $product->get_children(); - $cache_key = WC_Cache_Helper::get_cache_prefix( 'product_' . $product->get_id() ) . 'product_variation_attributes_' . $product->get_id(); - $cache_group = 'products'; - $cached_data = wp_cache_get( $cache_key, $cache_group ); - - if ( false !== $cached_data ) { - return $cached_data; - } - - if ( ! empty( $attributes ) ) { - foreach ( $attributes as $attribute ) { - if ( empty( $attribute['is_variation'] ) ) { - continue; - } - - // Get possible values for this attribute, for only visible variations. - if ( ! empty( $child_ids ) ) { - $format = array_fill( 0, count( $child_ids ), '%d' ); - $query_in = '(' . implode( ',', $format ) . ')'; - $query_args = array( 'attribute_name' => wc_variation_attribute_name( $attribute['name'] ) ) + $child_ids; - $values = array_unique( - $wpdb->get_col( - $wpdb->prepare( - "SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN {$query_in}", // @codingStandardsIgnoreLine. - $query_args - ) - ) - ); - } else { - $values = array(); - } - - // Empty value indicates that all options for given attribute are available. - if ( in_array( null, $values, true ) || in_array( '', $values, true ) || empty( $values ) ) { - $values = $attribute['is_taxonomy'] ? wc_get_object_terms( $product->get_id(), $attribute['name'], 'slug' ) : wc_get_text_attributes( $attribute['value'] ); - // Get custom attributes (non taxonomy) as defined. - } elseif ( ! $attribute['is_taxonomy'] ) { - $text_attributes = wc_get_text_attributes( $attribute['value'] ); - $assigned_text_attributes = $values; - $values = array(); - - // Pre 2.4 handling where 'slugs' were saved instead of the full text attribute. - if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) { - $assigned_text_attributes = array_map( 'sanitize_title', $assigned_text_attributes ); - foreach ( $text_attributes as $text_attribute ) { - if ( in_array( sanitize_title( $text_attribute ), $assigned_text_attributes, true ) ) { - $values[] = $text_attribute; - } - } - } else { - foreach ( $text_attributes as $text_attribute ) { - if ( in_array( $text_attribute, $assigned_text_attributes, true ) ) { - $values[] = $text_attribute; - } - } - } - } - $variation_attributes[ $attribute['name'] ] = array_unique( $values ); - } - } - - wp_cache_set( $cache_key, $variation_attributes, $cache_group ); - - return $variation_attributes; - } - - /** - * Get an array of all sale and regular prices from all variations. This is used for example when displaying the price range at variable product level or seeing if the variable product is on sale. - * - * Can be filtered by plugins which modify costs, but otherwise will include the raw meta costs unlike get_price() which runs costs through the woocommerce_get_price filter. - * This is to ensure modified prices are not cached, unless intended. - * - * @param WC_Product $product Product object. - * @param bool $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes). - * - * @return array of prices - * @since 3.0.0 - */ - public function read_price_data( &$product, $for_display = false ) { - - /** - * Transient name for storing prices for this product (note: Max transient length is 45) - * - * @since 2.5.0 a single transient is used per product for all prices, rather than many transients per product. - */ - $transient_name = 'wc_var_prices_' . $product->get_id(); - $transient_version = WC_Cache_Helper::get_transient_version( 'product' ); - $price_hash = $this->get_price_hash( $product, $for_display ); - - // Check if prices array is stale. - if ( ! isset( $this->prices_array['version'] ) || $this->prices_array['version'] !== $transient_version ) { - $this->prices_array = array( - 'version' => $transient_version, - ); - } - - /** - * $this->prices_array is an array of values which may have been modified from what is stored in transients - this may not match $transient_cached_prices_array. - * If the value has already been generated, we don't need to grab the values again so just return them. They are already filtered. - */ - if ( empty( $this->prices_array[ $price_hash ] ) ) { - $transient_cached_prices_array = array_filter( (array) json_decode( strval( get_transient( $transient_name ) ), true ) ); - - // If the product version has changed since the transient was last saved, reset the transient cache. - if ( ! isset( $transient_cached_prices_array['version'] ) || $transient_version !== $transient_cached_prices_array['version'] ) { - $transient_cached_prices_array = array( - 'version' => $transient_version, - ); - } - - // If the prices are not stored for this hash, generate them and add to the transient. - if ( empty( $transient_cached_prices_array[ $price_hash ] ) ) { - $prices_array = array( - 'price' => array(), - 'regular_price' => array(), - 'sale_price' => array(), - ); - - $variation_ids = $product->get_visible_children(); - - if ( is_callable( '_prime_post_caches' ) ) { - _prime_post_caches( $variation_ids ); - } - - foreach ( $variation_ids as $variation_id ) { - $variation = wc_get_product( $variation_id ); - - if ( $variation ) { - $price = apply_filters( 'woocommerce_variation_prices_price', $variation->get_price( 'edit' ), $variation, $product ); - $regular_price = apply_filters( 'woocommerce_variation_prices_regular_price', $variation->get_regular_price( 'edit' ), $variation, $product ); - $sale_price = apply_filters( 'woocommerce_variation_prices_sale_price', $variation->get_sale_price( 'edit' ), $variation, $product ); - - // Skip empty prices. - if ( '' === $price ) { - continue; - } - - // If sale price does not equal price, the product is not yet on sale. - if ( $sale_price === $regular_price || $sale_price !== $price ) { - $sale_price = $regular_price; - } - - // If we are getting prices for display, we need to account for taxes. - if ( $for_display ) { - if ( 'incl' === get_option( 'woocommerce_tax_display_shop' ) ) { - $price = '' === $price ? '' : wc_get_price_including_tax( - $variation, - array( - 'qty' => 1, - 'price' => $price, - ) - ); - $regular_price = '' === $regular_price ? '' : wc_get_price_including_tax( - $variation, - array( - 'qty' => 1, - 'price' => $regular_price, - ) - ); - $sale_price = '' === $sale_price ? '' : wc_get_price_including_tax( - $variation, - array( - 'qty' => 1, - 'price' => $sale_price, - ) - ); - } else { - $price = '' === $price ? '' : wc_get_price_excluding_tax( - $variation, - array( - 'qty' => 1, - 'price' => $price, - ) - ); - $regular_price = '' === $regular_price ? '' : wc_get_price_excluding_tax( - $variation, - array( - 'qty' => 1, - 'price' => $regular_price, - ) - ); - $sale_price = '' === $sale_price ? '' : wc_get_price_excluding_tax( - $variation, - array( - 'qty' => 1, - 'price' => $sale_price, - ) - ); - } - } - - $prices_array['price'][ $variation_id ] = wc_format_decimal( $price, wc_get_price_decimals() ); - $prices_array['regular_price'][ $variation_id ] = wc_format_decimal( $regular_price, wc_get_price_decimals() ); - $prices_array['sale_price'][ $variation_id ] = wc_format_decimal( $sale_price, wc_get_price_decimals() ); - - $prices_array = apply_filters( 'woocommerce_variation_prices_array', $prices_array, $variation, $for_display ); - } - } - - // Add all pricing data to the transient array. - foreach ( $prices_array as $key => $values ) { - $transient_cached_prices_array[ $price_hash ][ $key ] = $values; - } - - set_transient( $transient_name, wp_json_encode( $transient_cached_prices_array ), DAY_IN_SECONDS * 30 ); - } - - /** - * Give plugins one last chance to filter the variation prices array which has been generated and store locally to the class. - * This value may differ from the transient cache. It is filtered once before storing locally. - */ - $this->prices_array[ $price_hash ] = apply_filters( 'woocommerce_variation_prices', $transient_cached_prices_array[ $price_hash ], $product, $for_display ); - } - return $this->prices_array[ $price_hash ]; - } - - /** - * Create unique cache key based on the tax location (affects displayed/cached prices), product version and active price filters. - * DEVELOPERS should filter this hash if offering conditional pricing to keep it unique. - * - * @param WC_Product $product Product object. - * @param bool $for_display If taxes should be calculated or not. - * - * @since 3.0.0 - * @return string - */ - protected function get_price_hash( &$product, $for_display = false ) { - global $wp_filter; - - $price_hash = $for_display && wc_tax_enabled() ? array( get_option( 'woocommerce_tax_display_shop', 'excl' ), WC_Tax::get_rates() ) : array( false ); - $filter_names = array( 'woocommerce_variation_prices_price', 'woocommerce_variation_prices_regular_price', 'woocommerce_variation_prices_sale_price' ); - - foreach ( $filter_names as $filter_name ) { - if ( ! empty( $wp_filter[ $filter_name ] ) ) { - $price_hash[ $filter_name ] = array(); - - foreach ( $wp_filter[ $filter_name ] as $priority => $callbacks ) { - $price_hash[ $filter_name ][] = array_values( wp_list_pluck( $callbacks, 'function' ) ); - } - } - } - - return md5( wp_json_encode( apply_filters( 'woocommerce_get_variation_prices_hash', $price_hash, $product, $for_display ) ) ); - } - - /** - * Does a child have a weight set? - * - * @param WC_Product $product Product object. - * - * @since 3.0.0 - * @return boolean - */ - public function child_has_weight( $product ) { - global $wpdb; - $children = $product->get_visible_children(); - if ( ! $children ) { - return false; - } - - $format = array_fill( 0, count( $children ), '%d' ); - $query_in = '(' . implode( ',', $format ) . ')'; - - return null !== $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_weight' AND meta_value > 0 AND post_id IN {$query_in}", $children ) ); // @codingStandardsIgnoreLine. - } - - /** - * Does a child have dimensions set? - * - * @param WC_Product $product Product object. - * - * @since 3.0.0 - * @return boolean - */ - public function child_has_dimensions( $product ) { - global $wpdb; - $children = $product->get_visible_children(); - if ( ! $children ) { - return false; - } - - $format = array_fill( 0, count( $children ), '%d' ); - $query_in = '(' . implode( ',', $format ) . ')'; - - return null !== $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key IN ( '_length', '_width', '_height' ) AND meta_value > 0 AND post_id IN {$query_in}", $children ) ); // @codingStandardsIgnoreLine. - } - - /** - * Is a child in stock? - * - * @param WC_Product $product Product object. - * - * @since 3.0.0 - * @return boolean - */ - public function child_is_in_stock( $product ) { - return $this->child_has_stock_status( $product, 'instock' ); - } - - /** - * Does a child have a stock status? - * - * @param WC_Product $product Product object. - * @param string $status 'instock', 'outofstock', or 'onbackorder'. - * - * @since 3.3.0 - * @return boolean - */ - public function child_has_stock_status( $product, $status ) { - global $wpdb; - - $children = $product->get_children(); - - if ( $children ) { - $format = array_fill( 0, count( $children ), '%d' ); - $query_in = '(' . implode( ',', $format ) . ')'; - $query_args = array( 'stock_status' => $status ) + $children; - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared - if ( get_option( 'woocommerce_product_lookup_table_is_generating' ) ) { - $query = "SELECT COUNT( post_id ) FROM {$wpdb->postmeta} WHERE meta_key = '_stock_status' AND meta_value = %s AND post_id IN {$query_in}"; - } else { - $query = "SELECT COUNT( product_id ) FROM {$wpdb->wc_product_meta_lookup} WHERE stock_status = %s AND product_id IN {$query_in}"; - } - $children_with_status = $wpdb->get_var( - $wpdb->prepare( - $query, - $query_args - ) - ); - // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared - } else { - $children_with_status = 0; - } - - return (bool) $children_with_status; - } - - /** - * Syncs all variation names if the parent name is changed. - * - * @param WC_Product $product Product object. - * @param string $previous_name Variation previous name. - * @param string $new_name Variation new name. - * - * @since 3.0.0 - */ - public function sync_variation_names( &$product, $previous_name = '', $new_name = '' ) { - if ( $new_name !== $previous_name ) { - global $wpdb; - - $wpdb->query( - $wpdb->prepare( - "UPDATE {$wpdb->posts} - SET post_title = REPLACE( post_title, %s, %s ) - WHERE post_type = 'product_variation' - AND post_parent = %d", - $previous_name ? $previous_name : 'AUTO-DRAFT', - $new_name, - $product->get_id() - ) - ); - } - } - - /** - * Stock managed at the parent level - update children being managed by this product. - * This sync function syncs downwards (from parent to child) when the variable product is saved. - * - * @param WC_Product $product Product object. - * - * @since 3.0.0 - */ - public function sync_managed_variation_stock_status( &$product ) { - global $wpdb; - - if ( $product->get_manage_stock() ) { - $children = $product->get_children(); - $changed = false; - - if ( $children ) { - $status = $product->get_stock_status(); - $format = array_fill( 0, count( $children ), '%d' ); - $query_in = '(' . implode( ',', $format ) . ')'; - $managed_children = array_unique( $wpdb->get_col( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_manage_stock' AND meta_value != 'yes' AND post_id IN {$query_in}", $children ) ) ); // @codingStandardsIgnoreLine. - foreach ( $managed_children as $managed_child ) { - if ( update_post_meta( $managed_child, '_stock_status', $status ) ) { - $this->update_lookup_table( $managed_child, 'wc_product_meta_lookup' ); - $changed = true; - } - } - } - - if ( $changed ) { - $children = $this->read_children( $product, true ); - $product->set_children( $children['all'] ); - $product->set_visible_children( $children['visible'] ); - } - } - } - - /** - * Sync variable product prices with children. - * - * @param WC_Product $product Product object. - * - * @since 3.0.0 - */ - public function sync_price( &$product ) { - global $wpdb; - - $children = $product->get_visible_children(); - if ( $children ) { - $format = array_fill( 0, count( $children ), '%d' ); - $query_in = '(' . implode( ',', $format ) . ')'; - $prices = array_unique( $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = '_price' AND post_id IN {$query_in}", $children ) ) ); // @codingStandardsIgnoreLine. - } else { - $prices = array(); - } - - delete_post_meta( $product->get_id(), '_price' ); - delete_post_meta( $product->get_id(), '_sale_price' ); - delete_post_meta( $product->get_id(), '_regular_price' ); - - if ( $prices ) { - sort( $prices, SORT_NUMERIC ); - // To allow sorting and filtering by multiple values, we have no choice but to store child prices in this manner. - foreach ( $prices as $price ) { - if ( is_null( $price ) || '' === $price ) { - continue; - } - add_post_meta( $product->get_id(), '_price', $price, false ); - } - } - - $this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' ); - - /** - * Fire an action for this direct update so it can be detected by other code. - * - * @since 3.6 - * @param int $product_id Product ID that was updated directly. - */ - do_action( 'woocommerce_updated_product_price', $product->get_id() ); - } - - /** - * Sync variable product stock status with children. - * Change does not persist unless saved by caller. - * - * @param WC_Product $product Product object. - * - * @since 3.0.0 - */ - public function sync_stock_status( &$product ) { - if ( $product->child_is_in_stock() ) { - $product->set_stock_status( 'instock' ); - } elseif ( $product->child_is_on_backorder() ) { - $product->set_stock_status( 'onbackorder' ); - } else { - $product->set_stock_status( 'outofstock' ); - } - } - - /** - * Delete variations of a product. - * - * @param int $product_id Product ID. - * @param bool $force_delete False to trash. - * - * @since 3.0.0 - */ - public function delete_variations( $product_id, $force_delete = false ) { - if ( ! is_numeric( $product_id ) || 0 >= $product_id ) { - return; - } - - $variation_ids = wp_parse_id_list( - get_posts( - array( - 'post_parent' => $product_id, - 'post_type' => 'product_variation', - 'fields' => 'ids', - 'post_status' => array( 'any', 'trash', 'auto-draft' ), - 'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts - ) - ) - ); - - if ( ! empty( $variation_ids ) ) { - foreach ( $variation_ids as $variation_id ) { - if ( $force_delete ) { - do_action( 'woocommerce_before_delete_product_variation', $variation_id ); - wp_delete_post( $variation_id, true ); - do_action( 'woocommerce_delete_product_variation', $variation_id ); - } else { - wp_trash_post( $variation_id ); - do_action( 'woocommerce_trash_product_variation', $variation_id ); - } - } - } - - delete_transient( 'wc_product_children_' . $product_id ); - } - - /** - * Untrash variations. - * - * @param int $product_id Product ID. - */ - public function untrash_variations( $product_id ) { - $variation_ids = wp_parse_id_list( - get_posts( - array( - 'post_parent' => $product_id, - 'post_type' => 'product_variation', - 'fields' => 'ids', - 'post_status' => 'trash', - 'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts - ) - ) - ); - - if ( ! empty( $variation_ids ) ) { - foreach ( $variation_ids as $variation_id ) { - wp_untrash_post( $variation_id ); - } - } - - delete_transient( 'wc_product_children_' . $product_id ); - } -} diff --git a/includes/data-stores/class-wc-product-variation-data-store-cpt.php b/includes/data-stores/class-wc-product-variation-data-store-cpt.php deleted file mode 100644 index 534ba382ef8..00000000000 --- a/includes/data-stores/class-wc-product-variation-data-store-cpt.php +++ /dev/null @@ -1,547 +0,0 @@ -meta_key, $this->internal_meta_keys, true ) && 0 !== stripos( $meta->meta_key, 'attribute_' ) && 0 !== stripos( $meta->meta_key, 'wp_' ); - } - - /* - |-------------------------------------------------------------------------- - | CRUD Methods - |-------------------------------------------------------------------------- - */ - - /** - * Reads a product from the database and sets its data to the class. - * - * @since 3.0.0 - * @param WC_Product_Variation $product Product object. - * @throws WC_Data_Exception If WC_Product::set_tax_status() is called with an invalid tax status (via read_product_data), or when passing an invalid ID. - */ - public function read( &$product ) { - $product->set_defaults(); - - if ( ! $product->get_id() ) { - return; - } - - $post_object = get_post( $product->get_id() ); - - if ( ! $post_object ) { - return; - } - - if ( 'product_variation' !== $post_object->post_type ) { - throw new WC_Data_Exception( 'variation_invalid_id', __( 'Invalid product type: passed ID does not correspond to a product variation.', 'woocommerce' ) ); - } - - $product->set_props( - array( - 'name' => $post_object->post_title, - 'slug' => $post_object->post_name, - 'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ), - 'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ), - 'status' => $post_object->post_status, - 'menu_order' => $post_object->menu_order, - 'reviews_allowed' => 'open' === $post_object->comment_status, - 'parent_id' => $post_object->post_parent, - 'attribute_summary' => $post_object->post_excerpt, - ) - ); - - // The post parent is not a valid variable product so we should prevent this. - if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) { - $product->set_parent_id( 0 ); - } - - $this->read_downloads( $product ); - $this->read_product_data( $product ); - $this->read_extra_data( $product ); - $product->set_attributes( wc_get_product_variation_attributes( $product->get_id() ) ); - - $updates = array(); - /** - * If a variation title is not in sync with the parent e.g. saved prior to 3.0, or if the parent title has changed, detect here and update. - */ - $new_title = $this->generate_product_title( $product ); - - if ( $post_object->post_title !== $new_title ) { - $product->set_name( $new_title ); - $updates = array_merge( $updates, array( 'post_title' => $new_title ) ); - } - - /** - * If the attribute summary is not in sync, update here. Used when searching for variations by attribute values. - * This is meant to also cover the case when global attribute name or value is updated, then the attribute summary is updated - * for respective products when they're read. - */ - $new_attribute_summary = $this->generate_attribute_summary( $product ); - - if ( $new_attribute_summary !== $post_object->post_excerpt ) { - $product->set_attribute_summary( $new_attribute_summary ); - $updates = array_merge( $updates, array( 'post_excerpt' => $new_attribute_summary ) ); - } - - if ( ! empty( $updates ) ) { - $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $updates, array( 'ID' => $product->get_id() ) ); - clean_post_cache( $product->get_id() ); - } - - // Set object_read true once all data is read. - $product->set_object_read( true ); - } - - /** - * Create a new product. - * - * @since 3.0.0 - * @param WC_Product_Variation $product Product object. - */ - public function create( &$product ) { - if ( ! $product->get_date_created() ) { - $product->set_date_created( time() ); - } - - $new_title = $this->generate_product_title( $product ); - - if ( $product->get_name( 'edit' ) !== $new_title ) { - $product->set_name( $new_title ); - } - - $attribute_summary = $this->generate_attribute_summary( $product ); - $product->set_attribute_summary( $attribute_summary ); - - // The post parent is not a valid variable product so we should prevent this. - if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) { - $product->set_parent_id( 0 ); - } - - $id = wp_insert_post( - apply_filters( - 'woocommerce_new_product_variation_data', - array( - 'post_type' => 'product_variation', - 'post_status' => $product->get_status() ? $product->get_status() : 'publish', - 'post_author' => get_current_user_id(), - 'post_title' => $product->get_name( 'edit' ), - 'post_excerpt' => $product->get_attribute_summary( 'edit' ), - 'post_content' => '', - 'post_parent' => $product->get_parent_id(), - 'comment_status' => 'closed', - 'ping_status' => 'closed', - 'menu_order' => $product->get_menu_order(), - 'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ), - 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ), - 'post_name' => $product->get_slug( 'edit' ), - ) - ), - true - ); - - if ( $id && ! is_wp_error( $id ) ) { - $product->set_id( $id ); - - $this->update_post_meta( $product, true ); - $this->update_terms( $product, true ); - $this->update_visibility( $product, true ); - $this->update_attributes( $product, true ); - $this->handle_updated_props( $product ); - - $product->save_meta_data(); - $product->apply_changes(); - - $this->update_version_and_type( $product ); - $this->update_guid( $product ); - - $this->clear_caches( $product ); - - do_action( 'woocommerce_new_product_variation', $id, $product ); - } - } - - /** - * Updates an existing product. - * - * @since 3.0.0 - * @param WC_Product_Variation $product Product object. - */ - public function update( &$product ) { - $product->save_meta_data(); - - if ( ! $product->get_date_created() ) { - $product->set_date_created( time() ); - } - - $new_title = $this->generate_product_title( $product ); - - if ( $product->get_name( 'edit' ) !== $new_title ) { - $product->set_name( $new_title ); - } - - // The post parent is not a valid variable product so we should prevent this. - if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) { - $product->set_parent_id( 0 ); - } - - $changes = $product->get_changes(); - - if ( array_intersect( array( 'attributes' ), array_keys( $changes ) ) ) { - $product->set_attribute_summary( $this->generate_attribute_summary( $product ) ); - } - - // Only update the post when the post data changes. - if ( array_intersect( array( 'name', 'parent_id', 'status', 'menu_order', 'date_created', 'date_modified', 'attributes' ), array_keys( $changes ) ) ) { - $post_data = array( - 'post_title' => $product->get_name( 'edit' ), - 'post_excerpt' => $product->get_attribute_summary( 'edit' ), - 'post_parent' => $product->get_parent_id( 'edit' ), - 'comment_status' => 'closed', - 'post_status' => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish', - 'menu_order' => $product->get_menu_order( 'edit' ), - 'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ), - 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ), - 'post_modified' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ), - 'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ), - 'post_type' => 'product_variation', - 'post_name' => $product->get_slug( 'edit' ), - ); - - /** - * When updating this object, to prevent infinite loops, use $wpdb - * to update data, since wp_update_post spawns more calls to the - * save_post action. - * - * This ensures hooks are fired by either WP itself (admin screen save), - * or an update purely from CRUD. - */ - if ( doing_action( 'save_post' ) ) { - $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) ); - clean_post_cache( $product->get_id() ); - } else { - wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) ); - } - $product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook. - - } else { // Only update post modified time to record this save event. - $GLOBALS['wpdb']->update( - $GLOBALS['wpdb']->posts, - array( - 'post_modified' => current_time( 'mysql' ), - 'post_modified_gmt' => current_time( 'mysql', 1 ), - ), - array( - 'ID' => $product->get_id(), - ) - ); - clean_post_cache( $product->get_id() ); - } - - $this->update_post_meta( $product ); - $this->update_terms( $product ); - $this->update_visibility( $product, true ); - $this->update_attributes( $product ); - $this->handle_updated_props( $product ); - - $product->apply_changes(); - - $this->update_version_and_type( $product ); - - $this->clear_caches( $product ); - - do_action( 'woocommerce_update_product_variation', $product->get_id(), $product ); - } - - /* - |-------------------------------------------------------------------------- - | Additional Methods - |-------------------------------------------------------------------------- - */ - - /** - * Generates a title with attribute information for a variation. - * Products will get a title of the form "Name - Value, Value" or just "Name". - * - * @since 3.0.0 - * @param WC_Product $product Product object. - * @return string - */ - protected function generate_product_title( $product ) { - $attributes = (array) $product->get_attributes(); - - // Do not include attributes if the product has 3+ attributes. - $should_include_attributes = count( $attributes ) < 3; - - // Do not include attributes if an attribute name has 2+ words and the - // product has multiple attributes. - if ( $should_include_attributes && 1 < count( $attributes ) ) { - foreach ( $attributes as $name => $value ) { - if ( false !== strpos( $name, '-' ) ) { - $should_include_attributes = false; - break; - } - } - } - - $should_include_attributes = apply_filters( 'woocommerce_product_variation_title_include_attributes', $should_include_attributes, $product ); - $separator = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', $product ); - $title_base = get_post_field( 'post_title', $product->get_parent_id() ); - $title_suffix = $should_include_attributes ? wc_get_formatted_variation( $product, true, false ) : ''; - - return apply_filters( 'woocommerce_product_variation_title', $title_suffix ? $title_base . $separator . $title_suffix : $title_base, $product, $title_base, $title_suffix ); - } - - /** - * Generates attribute summary for the variation. - * - * Attribute summary contains comma-delimited 'attribute_name: attribute_value' pairs for all attributes. - * - * @since 3.6.0 - * @param WC_Product_Variation $product Product variation to generate the attribute summary for. - * - * @return string - */ - protected function generate_attribute_summary( $product ) { - return wc_get_formatted_variation( $product, true, true ); - } - - /** - * Make sure we store the product version (to track data changes). - * - * @param WC_Product $product Product object. - * @since 3.0.0 - */ - protected function update_version_and_type( &$product ) { - wp_set_object_terms( $product->get_id(), '', 'product_type' ); - update_post_meta( $product->get_id(), '_product_version', Constants::get_constant( 'WC_VERSION' ) ); - } - - /** - * Read post data. - * - * @since 3.0.0 - * @param WC_Product_Variation $product Product object. - * @throws WC_Data_Exception If WC_Product::set_tax_status() is called with an invalid tax status. - */ - protected function read_product_data( &$product ) { - $id = $product->get_id(); - - $product->set_props( - array( - 'description' => get_post_meta( $id, '_variation_description', true ), - 'regular_price' => get_post_meta( $id, '_regular_price', true ), - 'sale_price' => get_post_meta( $id, '_sale_price', true ), - 'date_on_sale_from' => get_post_meta( $id, '_sale_price_dates_from', true ), - 'date_on_sale_to' => get_post_meta( $id, '_sale_price_dates_to', true ), - 'manage_stock' => get_post_meta( $id, '_manage_stock', true ), - 'stock_status' => get_post_meta( $id, '_stock_status', true ), - 'shipping_class_id' => current( $this->get_term_ids( $id, 'product_shipping_class' ) ), - 'virtual' => get_post_meta( $id, '_virtual', true ), - 'downloadable' => get_post_meta( $id, '_downloadable', true ), - 'gallery_image_ids' => array_filter( explode( ',', get_post_meta( $id, '_product_image_gallery', true ) ) ), - 'download_limit' => get_post_meta( $id, '_download_limit', true ), - 'download_expiry' => get_post_meta( $id, '_download_expiry', true ), - 'image_id' => get_post_thumbnail_id( $id ), - 'backorders' => get_post_meta( $id, '_backorders', true ), - 'sku' => get_post_meta( $id, '_sku', true ), - 'stock_quantity' => get_post_meta( $id, '_stock', true ), - 'weight' => get_post_meta( $id, '_weight', true ), - 'length' => get_post_meta( $id, '_length', true ), - 'width' => get_post_meta( $id, '_width', true ), - 'height' => get_post_meta( $id, '_height', true ), - 'tax_class' => ! metadata_exists( 'post', $id, '_tax_class' ) ? 'parent' : get_post_meta( $id, '_tax_class', true ), - ) - ); - - if ( $product->is_on_sale( 'edit' ) ) { - $product->set_price( $product->get_sale_price( 'edit' ) ); - } else { - $product->set_price( $product->get_regular_price( 'edit' ) ); - } - - $parent_object = get_post( $product->get_parent_id() ); - $terms = get_the_terms( $product->get_parent_id(), 'product_visibility' ); - $term_names = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array(); - $exclude_search = in_array( 'exclude-from-search', $term_names, true ); - $exclude_catalog = in_array( 'exclude-from-catalog', $term_names, true ); - - if ( $exclude_search && $exclude_catalog ) { - $catalog_visibility = 'hidden'; - } elseif ( $exclude_search ) { - $catalog_visibility = 'catalog'; - } elseif ( $exclude_catalog ) { - $catalog_visibility = 'search'; - } else { - $catalog_visibility = 'visible'; - } - - $product->set_parent_data( - array( - 'title' => $parent_object ? $parent_object->post_title : '', - 'status' => $parent_object ? $parent_object->post_status : '', - 'sku' => get_post_meta( $product->get_parent_id(), '_sku', true ), - 'manage_stock' => get_post_meta( $product->get_parent_id(), '_manage_stock', true ), - 'backorders' => get_post_meta( $product->get_parent_id(), '_backorders', true ), - 'low_stock_amount' => get_post_meta( $product->get_parent_id(), '_low_stock_amount', true ), - 'stock_quantity' => wc_stock_amount( get_post_meta( $product->get_parent_id(), '_stock', true ) ), - 'weight' => get_post_meta( $product->get_parent_id(), '_weight', true ), - 'length' => get_post_meta( $product->get_parent_id(), '_length', true ), - 'width' => get_post_meta( $product->get_parent_id(), '_width', true ), - 'height' => get_post_meta( $product->get_parent_id(), '_height', true ), - 'tax_class' => get_post_meta( $product->get_parent_id(), '_tax_class', true ), - 'shipping_class_id' => absint( current( $this->get_term_ids( $product->get_parent_id(), 'product_shipping_class' ) ) ), - 'image_id' => get_post_thumbnail_id( $product->get_parent_id() ), - 'purchase_note' => get_post_meta( $product->get_parent_id(), '_purchase_note', true ), - 'catalog_visibility' => $catalog_visibility, - ) - ); - - // Pull data from the parent when there is no user-facing way to set props. - $product->set_sold_individually( get_post_meta( $product->get_parent_id(), '_sold_individually', true ) ); - $product->set_tax_status( get_post_meta( $product->get_parent_id(), '_tax_status', true ) ); - $product->set_cross_sell_ids( get_post_meta( $product->get_parent_id(), '_crosssell_ids', true ) ); - } - - /** - * For all stored terms in all taxonomies, save them to the DB. - * - * @since 3.0.0 - * @param WC_Product $product Product object. - * @param bool $force Force update. Used during create. - */ - protected function update_terms( &$product, $force = false ) { - $changes = $product->get_changes(); - - if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) { - wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false ); - } - } - - /** - * Update visibility terms based on props. - * - * @since 3.0.0 - * - * @param WC_Product $product Product object. - * @param bool $force Force update. Used during create. - */ - protected function update_visibility( &$product, $force = false ) { - $changes = $product->get_changes(); - - if ( $force || array_intersect( array( 'stock_status' ), array_keys( $changes ) ) ) { - $terms = array(); - - if ( 'outofstock' === $product->get_stock_status() ) { - $terms[] = 'outofstock'; - } - - wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false ); - } - } - - /** - * Update attribute meta values. - * - * @since 3.0.0 - * @param WC_Product $product Product object. - * @param bool $force Force update. Used during create. - */ - protected function update_attributes( &$product, $force = false ) { - $changes = $product->get_changes(); - - if ( $force || array_key_exists( 'attributes', $changes ) ) { - global $wpdb; - - $product_id = $product->get_id(); - $attributes = $product->get_attributes(); - $updated_attribute_keys = array(); - foreach ( $attributes as $key => $value ) { - update_post_meta( $product_id, 'attribute_' . $key, wp_slash( $value ) ); - $updated_attribute_keys[] = 'attribute_' . $key; - } - - // Remove old taxonomies attributes so data is kept up to date - first get attribute key names. - $delete_attribute_keys = $wpdb->get_col( - $wpdb->prepare( - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration - "SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE %s AND meta_key NOT IN ( '" . implode( "','", array_map( 'esc_sql', $updated_attribute_keys ) ) . "' ) AND post_id = %d", - $wpdb->esc_like( 'attribute_' ) . '%', - $product_id - ) - ); - - foreach ( $delete_attribute_keys as $key ) { - delete_post_meta( $product_id, $key ); - } - } - } - - /** - * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class. - * - * @since 3.0.0 - * @param WC_Product $product Product object. - * @param bool $force Force update. Used during create. - */ - public function update_post_meta( &$product, $force = false ) { - $meta_key_to_props = array( - '_variation_description' => 'description', - ); - - $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props ); - - foreach ( $props_to_update as $meta_key => $prop ) { - $value = $product->{"get_$prop"}( 'edit' ); - $updated = update_post_meta( $product->get_id(), $meta_key, $value ); - if ( $updated ) { - $this->updated_props[] = $prop; - } - } - - parent::update_post_meta( $product, $force ); - } - - /** - * Update product variation guid. - * - * @param WC_Product_Variation $product Product variation object. - * - * @since 3.6.0 - */ - protected function update_guid( $product ) { - global $wpdb; - - $guid = home_url( - add_query_arg( - array( - 'post_type' => 'product_variation', - 'p' => $product->get_id(), - ), - '' - ) - ); - $wpdb->update( $wpdb->posts, array( 'guid' => $guid ), array( 'ID' => $product->get_id() ) ); - } -} diff --git a/includes/data-stores/class-wc-shipping-zone-data-store.php b/includes/data-stores/class-wc-shipping-zone-data-store.php deleted file mode 100644 index fe9c54d2159..00000000000 --- a/includes/data-stores/class-wc-shipping-zone-data-store.php +++ /dev/null @@ -1,373 +0,0 @@ -insert( - $wpdb->prefix . 'woocommerce_shipping_zones', - array( - 'zone_name' => $zone->get_zone_name(), - 'zone_order' => $zone->get_zone_order(), - ) - ); - $zone->set_id( $wpdb->insert_id ); - $zone->save_meta_data(); - $this->save_locations( $zone ); - $zone->apply_changes(); - WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' ); - WC_Cache_Helper::get_transient_version( 'shipping', true ); - } - - /** - * Update zone in the database. - * - * @since 3.0.0 - * @param WC_Shipping_Zone $zone Shipping zone object. - */ - public function update( &$zone ) { - global $wpdb; - if ( $zone->get_id() ) { - $wpdb->update( - $wpdb->prefix . 'woocommerce_shipping_zones', - array( - 'zone_name' => $zone->get_zone_name(), - 'zone_order' => $zone->get_zone_order(), - ), - array( 'zone_id' => $zone->get_id() ) - ); - } - $zone->save_meta_data(); - $this->save_locations( $zone ); - $zone->apply_changes(); - WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' ); - WC_Cache_Helper::get_transient_version( 'shipping', true ); - } - - /** - * Method to read a shipping zone from the database. - * - * @since 3.0.0 - * @param WC_Shipping_Zone $zone Shipping zone object. - * @throws Exception If invalid data store. - */ - public function read( &$zone ) { - global $wpdb; - - // Zone 0 is used as a default if no other zones fit. - if ( 0 === $zone->get_id() || '0' === $zone->get_id() ) { - $this->read_zone_locations( $zone ); - $zone->set_zone_name( __( 'Locations not covered by your other zones', 'woocommerce' ) ); - $zone->read_meta_data(); - $zone->set_object_read( true ); - - /** - * Indicate that the WooCommerce shipping zone has been loaded. - * - * @param WC_Shipping_Zone $zone The shipping zone that has been loaded. - */ - do_action( 'woocommerce_shipping_zone_loaded', $zone ); - return; - } - - $zone_data = $wpdb->get_row( - $wpdb->prepare( - "SELECT zone_name, zone_order FROM {$wpdb->prefix}woocommerce_shipping_zones WHERE zone_id = %d LIMIT 1", - $zone->get_id() - ) - ); - - if ( ! $zone_data ) { - throw new Exception( __( 'Invalid data store.', 'woocommerce' ) ); - } - - $zone->set_zone_name( $zone_data->zone_name ); - $zone->set_zone_order( $zone_data->zone_order ); - $this->read_zone_locations( $zone ); - $zone->read_meta_data(); - $zone->set_object_read( true ); - - /** This action is documented in includes/datastores/class-wc-shipping-zone-data-store.php. */ - do_action( 'woocommerce_shipping_zone_loaded', $zone ); - } - - /** - * Deletes a shipping zone from the database. - * - * @since 3.0.0 - * @param WC_Shipping_Zone $zone Shipping zone object. - * @param array $args Array of args to pass to the delete method. - * @return void - */ - public function delete( &$zone, $args = array() ) { - $zone_id = $zone->get_id(); - - if ( $zone_id ) { - global $wpdb; - - // Delete methods and their settings. - $methods = $this->get_methods( $zone_id, false ); - - if ( $methods ) { - foreach ( $methods as $method ) { - $this->delete_method( $method->instance_id ); - } - } - - // Delete zone. - $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array( 'zone_id' => $zone_id ) ); - $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zones', array( 'zone_id' => $zone_id ) ); - - $zone->set_id( null ); - - WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' ); - WC_Cache_Helper::get_transient_version( 'shipping', true ); - - do_action( 'woocommerce_delete_shipping_zone', $zone_id ); - } - } - - /** - * Get a list of shipping methods for a specific zone. - * - * @since 3.0.0 - * @param int $zone_id Zone ID. - * @param bool $enabled_only True to request enabled methods only. - * @return array Array of objects containing method_id, method_order, instance_id, is_enabled - */ - public function get_methods( $zone_id, $enabled_only ) { - global $wpdb; - - if ( $enabled_only ) { - $raw_methods_sql = "SELECT method_id, method_order, instance_id, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d AND is_enabled = 1"; - } else { - $raw_methods_sql = "SELECT method_id, method_order, instance_id, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d"; - } - - return $wpdb->get_results( $wpdb->prepare( $raw_methods_sql, $zone_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - } - - /** - * Get count of methods for a zone. - * - * @since 3.0.0 - * @param int $zone_id Zone ID. - * @return int Method Count - */ - public function get_method_count( $zone_id ) { - global $wpdb; - return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d", $zone_id ) ); - } - - /** - * Add a shipping method to a zone. - * - * @since 3.0.0 - * @param int $zone_id Zone ID. - * @param string $type Method Type/ID. - * @param int $order Method Order. - * @return int Instance ID - */ - public function add_method( $zone_id, $type, $order ) { - global $wpdb; - $wpdb->insert( - $wpdb->prefix . 'woocommerce_shipping_zone_methods', - array( - 'method_id' => $type, - 'zone_id' => $zone_id, - 'method_order' => $order, - ), - array( - '%s', - '%d', - '%d', - ) - ); - return $wpdb->insert_id; - } - - /** - * Delete a method instance. - * - * @since 3.0.0 - * @param int $instance_id Instance ID. - */ - public function delete_method( $instance_id ) { - global $wpdb; - - $method = $this->get_method( $instance_id ); - - if ( ! $method ) { - return; - } - - delete_option( 'woocommerce_' . $method->method_id . '_' . $instance_id . '_settings' ); - - $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_methods', array( 'instance_id' => $instance_id ) ); - - do_action( 'woocommerce_delete_shipping_zone_method', $instance_id ); - } - - /** - * Get a shipping zone method instance. - * - * @since 3.0.0 - * @param int $instance_id Instance ID. - * @return object - */ - public function get_method( $instance_id ) { - global $wpdb; - return $wpdb->get_row( $wpdb->prepare( "SELECT zone_id, method_id, instance_id, method_order, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE instance_id = %d LIMIT 1;", $instance_id ) ); - } - - /** - * Find a matching zone ID for a given package. - * - * @since 3.0.0 - * @param object $package Package information. - * @return int - */ - public function get_zone_id_from_package( $package ) { - global $wpdb; - - $country = strtoupper( wc_clean( $package['destination']['country'] ) ); - $state = strtoupper( wc_clean( $package['destination']['state'] ) ); - $continent = strtoupper( wc_clean( WC()->countries->get_continent_code_for_country( $country ) ) ); - $postcode = wc_normalize_postcode( wc_clean( $package['destination']['postcode'] ) ); - - // Work out criteria for our zone search. - $criteria = array(); - $criteria[] = $wpdb->prepare( "( ( location_type = 'country' AND location_code = %s )", $country ); - $criteria[] = $wpdb->prepare( "OR ( location_type = 'state' AND location_code = %s )", $country . ':' . $state ); - $criteria[] = $wpdb->prepare( "OR ( location_type = 'continent' AND location_code = %s )", $continent ); - $criteria[] = 'OR ( location_type IS NULL ) )'; - - // Postcode range and wildcard matching. - $postcode_locations = $wpdb->get_results( "SELECT zone_id, location_code FROM {$wpdb->prefix}woocommerce_shipping_zone_locations WHERE location_type = 'postcode';" ); - - if ( $postcode_locations ) { - $zone_ids_with_postcode_rules = array_map( 'absint', wp_list_pluck( $postcode_locations, 'zone_id' ) ); - $matches = wc_postcode_location_matcher( $postcode, $postcode_locations, 'zone_id', 'location_code', $country ); - $do_not_match = array_unique( array_diff( $zone_ids_with_postcode_rules, array_keys( $matches ) ) ); - - if ( ! empty( $do_not_match ) ) { - $criteria[] = 'AND zones.zone_id NOT IN (' . implode( ',', $do_not_match ) . ')'; - } - } - - /** - * Get shipping zone criteria - * - * @since 3.6.6 - * @param array $criteria Get zone criteria. - * @param array $package Package information. - * @param array $postcode_locations Postcode range and wildcard matching. - */ - $criteria = apply_filters( 'woocommerce_get_zone_criteria', $criteria, $package, $postcode_locations ); - - // Get matching zones. - return $wpdb->get_var( - "SELECT zones.zone_id FROM {$wpdb->prefix}woocommerce_shipping_zones as zones - LEFT OUTER JOIN {$wpdb->prefix}woocommerce_shipping_zone_locations as locations ON zones.zone_id = locations.zone_id AND location_type != 'postcode' - WHERE " . implode( ' ', $criteria ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - . ' ORDER BY zone_order ASC, zones.zone_id ASC LIMIT 1' - ); - } - - /** - * Return an ordered list of zones. - * - * @since 3.0.0 - * @return array An array of objects containing a zone_id, zone_name, and zone_order. - */ - public function get_zones() { - global $wpdb; - return $wpdb->get_results( "SELECT zone_id, zone_name, zone_order FROM {$wpdb->prefix}woocommerce_shipping_zones order by zone_order ASC, zone_id ASC;" ); - } - - - /** - * Return a zone ID from an instance ID. - * - * @since 3.0.0 - * @param int $id Instnace ID. - * @return int - */ - public function get_zone_id_by_instance_id( $id ) { - global $wpdb; - return $wpdb->get_var( $wpdb->prepare( "SELECT zone_id FROM {$wpdb->prefix}woocommerce_shipping_zone_methods as methods WHERE methods.instance_id = %d LIMIT 1;", $id ) ); - } - - /** - * Read location data from the database. - * - * @param WC_Shipping_Zone $zone Shipping zone object. - */ - private function read_zone_locations( &$zone ) { - global $wpdb; - - $locations = $wpdb->get_results( - $wpdb->prepare( - "SELECT location_code, location_type FROM {$wpdb->prefix}woocommerce_shipping_zone_locations WHERE zone_id = %d", - $zone->get_id() - ) - ); - - if ( $locations ) { - foreach ( $locations as $location ) { - $zone->add_location( $location->location_code, $location->location_type ); - } - } - } - - /** - * Save locations to the DB. - * This function clears old locations, then re-inserts new if any changes are found. - * - * @since 3.0.0 - * - * @param WC_Shipping_Zone $zone Shipping zone object. - * - * @return bool|void - */ - private function save_locations( &$zone ) { - $changed_props = array_keys( $zone->get_changes() ); - if ( ! in_array( 'zone_locations', $changed_props, true ) ) { - return false; - } - - global $wpdb; - $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array( 'zone_id' => $zone->get_id() ) ); - - foreach ( $zone->get_zone_locations( 'edit' ) as $location ) { - $wpdb->insert( - $wpdb->prefix . 'woocommerce_shipping_zone_locations', - array( - 'zone_id' => $zone->get_id(), - 'location_code' => $location->code, - 'location_type' => $location->type, - ) - ); - } - } -} diff --git a/includes/data-stores/class-wc-webhook-data-store.php b/includes/data-stores/class-wc-webhook-data-store.php deleted file mode 100644 index 947e89edb13..00000000000 --- a/includes/data-stores/class-wc-webhook-data-store.php +++ /dev/null @@ -1,447 +0,0 @@ -get_changes(); - if ( isset( $changes['date_created'] ) ) { - $date_created = $webhook->get_date_created()->date( 'Y-m-d H:i:s' ); - $date_created_gmt = gmdate( 'Y-m-d H:i:s', $webhook->get_date_created()->getTimestamp() ); - } else { - $date_created = current_time( 'mysql' ); - $date_created_gmt = current_time( 'mysql', 1 ); - $webhook->set_date_created( $date_created ); - } - - // Pending delivery by default if not set while creating a new webhook. - if ( ! isset( $changes['pending_delivery'] ) ) { - $webhook->set_pending_delivery( true ); - } - - $data = array( - 'status' => $webhook->get_status( 'edit' ), - 'name' => $webhook->get_name( 'edit' ), - 'user_id' => $webhook->get_user_id( 'edit' ), - 'delivery_url' => $webhook->get_delivery_url( 'edit' ), - 'secret' => $webhook->get_secret( 'edit' ), - 'topic' => $webhook->get_topic( 'edit' ), - 'date_created' => $date_created, - 'date_created_gmt' => $date_created_gmt, - 'api_version' => $this->get_api_version_number( $webhook->get_api_version( 'edit' ) ), - 'failure_count' => $webhook->get_failure_count( 'edit' ), - 'pending_delivery' => $webhook->get_pending_delivery( 'edit' ), - ); - - $wpdb->insert( $wpdb->prefix . 'wc_webhooks', $data ); // WPCS: DB call ok. - - $webhook_id = $wpdb->insert_id; - $webhook->set_id( $webhook_id ); - $webhook->apply_changes(); - - $this->delete_transients( $webhook->get_status( 'edit' ) ); - WC_Cache_Helper::invalidate_cache_group( 'webhooks' ); - do_action( 'woocommerce_new_webhook', $webhook_id, $webhook ); - } - - /** - * Read a webhook from the database. - * - * @since 3.3.0 - * @param WC_Webhook $webhook Webhook instance. - * @throws Exception When webhook is invalid. - */ - public function read( &$webhook ) { - global $wpdb; - - $data = wp_cache_get( $webhook->get_id(), 'webhooks' ); - - if ( false === $data ) { - $data = $wpdb->get_row( $wpdb->prepare( "SELECT webhook_id, status, name, user_id, delivery_url, secret, topic, date_created, date_modified, api_version, failure_count, pending_delivery FROM {$wpdb->prefix}wc_webhooks WHERE webhook_id = %d LIMIT 1;", $webhook->get_id() ), ARRAY_A ); // WPCS: cache ok, DB call ok. - - wp_cache_add( $webhook->get_id(), $data, 'webhooks' ); - } - - if ( is_array( $data ) ) { - $webhook->set_props( - array( - 'id' => $data['webhook_id'], - 'status' => $data['status'], - 'name' => $data['name'], - 'user_id' => $data['user_id'], - 'delivery_url' => $data['delivery_url'], - 'secret' => $data['secret'], - 'topic' => $data['topic'], - 'date_created' => '0000-00-00 00:00:00' === $data['date_created'] ? null : $data['date_created'], - 'date_modified' => '0000-00-00 00:00:00' === $data['date_modified'] ? null : $data['date_modified'], - 'api_version' => $data['api_version'], - 'failure_count' => $data['failure_count'], - 'pending_delivery' => $data['pending_delivery'], - ) - ); - $webhook->set_object_read( true ); - - do_action( 'woocommerce_webhook_loaded', $webhook ); - } else { - throw new Exception( __( 'Invalid webhook.', 'woocommerce' ) ); - } - } - - /** - * Update a webhook. - * - * @since 3.3.0 - * @param WC_Webhook $webhook Webhook instance. - */ - public function update( &$webhook ) { - global $wpdb; - - $changes = $webhook->get_changes(); - $trigger = isset( $changes['delivery_url'] ); - - if ( isset( $changes['date_modified'] ) ) { - $date_modified = $webhook->get_date_modified()->date( 'Y-m-d H:i:s' ); - $date_modified_gmt = gmdate( 'Y-m-d H:i:s', $webhook->get_date_modified()->getTimestamp() ); - } else { - $date_modified = current_time( 'mysql' ); - $date_modified_gmt = current_time( 'mysql', 1 ); - $webhook->set_date_modified( $date_modified ); - } - - $data = array( - 'status' => $webhook->get_status( 'edit' ), - 'name' => $webhook->get_name( 'edit' ), - 'user_id' => $webhook->get_user_id( 'edit' ), - 'delivery_url' => $webhook->get_delivery_url( 'edit' ), - 'secret' => $webhook->get_secret( 'edit' ), - 'topic' => $webhook->get_topic( 'edit' ), - 'date_modified' => $date_modified, - 'date_modified_gmt' => $date_modified_gmt, - 'api_version' => $this->get_api_version_number( $webhook->get_api_version( 'edit' ) ), - 'failure_count' => $webhook->get_failure_count( 'edit' ), - 'pending_delivery' => $webhook->get_pending_delivery( 'edit' ), - ); - - $wpdb->update( - $wpdb->prefix . 'wc_webhooks', - $data, - array( - 'webhook_id' => $webhook->get_id(), - ) - ); // WPCS: DB call ok. - - $webhook->apply_changes(); - - if ( isset( $changes['status'] ) ) { - // We need to delete all transients, because we can't be sure of the old status. - $this->delete_transients( 'all' ); - } - wp_cache_delete( $webhook->get_id(), 'webhooks' ); - WC_Cache_Helper::invalidate_cache_group( 'webhooks' ); - - if ( 'active' === $webhook->get_status() && ( $trigger || $webhook->get_pending_delivery() ) ) { - $webhook->deliver_ping(); - } - - do_action( 'woocommerce_webhook_updated', $webhook->get_id() ); - } - - /** - * Remove a webhook from the database. - * - * @since 3.3.0 - * @param WC_Webhook $webhook Webhook instance. - */ - public function delete( &$webhook ) { - global $wpdb; - - $wpdb->delete( - $wpdb->prefix . 'wc_webhooks', - array( - 'webhook_id' => $webhook->get_id(), - ), - array( '%d' ) - ); // WPCS: cache ok, DB call ok. - - $this->delete_transients( 'all' ); - wp_cache_delete( $webhook->get_id(), 'webhooks' ); - WC_Cache_Helper::invalidate_cache_group( 'webhooks' ); - do_action( 'woocommerce_webhook_deleted', $webhook->get_id(), $webhook ); - } - - /** - * Get API version number. - * - * @since 3.3.0 - * @param string $api_version REST API version. - * @return int - */ - public function get_api_version_number( $api_version ) { - return 'legacy_v3' === $api_version ? -1 : intval( substr( $api_version, -1 ) ); - } - - /** - * Get webhooks IDs from the database. - * - * @since 3.3.0 - * @throws InvalidArgumentException If a $status value is passed in that is not in the known wc_get_webhook_statuses() keys. - * @param string $status Optional - status to filter results by. Must be a key in return value of @see wc_get_webhook_statuses(). @since 3.6.0. - * @return int[] - */ - public function get_webhooks_ids( $status = '' ) { - if ( ! empty( $status ) ) { - $this->validate_status( $status ); - } - - $ids = get_transient( $this->get_transient_key( $status ) ); - - if ( false === $ids ) { - $ids = $this->search_webhooks( - array( - 'limit' => -1, - 'status' => $status, - ) - ); - $ids = array_map( 'absint', $ids ); - set_transient( $this->get_transient_key( $status ), $ids ); - } - - return $ids; - } - - /** - * Search webhooks. - * - * @param array $args Search arguments. - * @return array|object - */ - public function search_webhooks( $args ) { - global $wpdb; - - $args = wp_parse_args( - $args, - array( - 'limit' => 10, - 'offset' => 0, - 'order' => 'DESC', - 'orderby' => 'id', - 'paginate' => false, - ) - ); - - // Map post statuses. - $statuses = array( - 'publish' => 'active', - 'draft' => 'paused', - 'pending' => 'disabled', - ); - - // Map orderby to support a few post keys. - $orderby_mapping = array( - 'ID' => 'webhook_id', - 'id' => 'webhook_id', - 'name' => 'name', - 'title' => 'name', - 'post_title' => 'name', - 'post_name' => 'name', - 'date_created' => 'date_created_gmt', - 'date' => 'date_created_gmt', - 'post_date' => 'date_created_gmt', - 'date_modified' => 'date_modified_gmt', - 'modified' => 'date_modified_gmt', - 'post_modified' => 'date_modified_gmt', - ); - $orderby = isset( $orderby_mapping[ $args['orderby'] ] ) ? $orderby_mapping[ $args['orderby'] ] : 'webhook_id'; - $order = "ORDER BY {$orderby} " . esc_sql( strtoupper( $args['order'] ) ); - $limit = -1 < $args['limit'] ? $wpdb->prepare( 'LIMIT %d', $args['limit'] ) : ''; - $offset = 0 < $args['offset'] ? $wpdb->prepare( 'OFFSET %d', $args['offset'] ) : ''; - $status = ! empty( $args['status'] ) ? $wpdb->prepare( 'AND `status` = %s', isset( $statuses[ $args['status'] ] ) ? $statuses[ $args['status'] ] : $args['status'] ) : ''; - $search = ! empty( $args['search'] ) ? "AND `name` LIKE '%" . $wpdb->esc_like( sanitize_text_field( $args['search'] ) ) . "%'" : ''; - $include = ''; - $exclude = ''; - $date_created = ''; - $date_modified = ''; - - if ( ! empty( $args['include'] ) ) { - $args['include'] = implode( ',', wp_parse_id_list( $args['include'] ) ); - $include = 'AND webhook_id IN (' . $args['include'] . ')'; - } - - if ( ! empty( $args['exclude'] ) ) { - $args['exclude'] = implode( ',', wp_parse_id_list( $args['exclude'] ) ); - $exclude = 'AND webhook_id NOT IN (' . $args['exclude'] . ')'; - } - - if ( ! empty( $args['after'] ) || ! empty( $args['before'] ) ) { - $args['after'] = empty( $args['after'] ) ? '0000-00-00' : $args['after']; - $args['before'] = empty( $args['before'] ) ? current_time( 'mysql', 1 ) : $args['before']; - - $date_created = "AND `date_created_gmt` BETWEEN STR_TO_DATE('" . esc_sql( $args['after'] ) . "', '%Y-%m-%d %H:%i:%s') and STR_TO_DATE('" . esc_sql( $args['before'] ) . "', '%Y-%m-%d %H:%i:%s')"; - } - - if ( ! empty( $args['modified_after'] ) || ! empty( $args['modified_before'] ) ) { - $args['modified_after'] = empty( $args['modified_after'] ) ? '0000-00-00' : $args['modified_after']; - $args['modified_before'] = empty( $args['modified_before'] ) ? current_time( 'mysql', 1 ) : $args['modified_before']; - - $date_modified = "AND `date_modified_gmt` BETWEEN STR_TO_DATE('" . esc_sql( $args['modified_after'] ) . "', '%Y-%m-%d %H:%i:%s') and STR_TO_DATE('" . esc_sql( $args['modified_before'] ) . "', '%Y-%m-%d %H:%i:%s')"; - } - - // Check for cache. - $cache_key = WC_Cache_Helper::get_cache_prefix( 'webhooks' ) . 'search_webhooks' . md5( implode( ',', $args ) ); - $cache_value = wp_cache_get( $cache_key, 'webhook_search_results' ); - - if ( $cache_value ) { - return $cache_value; - } - - if ( $args['paginate'] ) { - $query = trim( - "SELECT SQL_CALC_FOUND_ROWS webhook_id - FROM {$wpdb->prefix}wc_webhooks - WHERE 1=1 - {$status} - {$search} - {$include} - {$exclude} - {$date_created} - {$date_modified} - {$order} - {$limit} - {$offset}" - ); - - $webhook_ids = wp_parse_id_list( $wpdb->get_col( $query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $total = (int) $wpdb->get_var( 'SELECT FOUND_ROWS();' ); - $return_value = (object) array( - 'webhooks' => $webhook_ids, - 'total' => $total, - 'max_num_pages' => $args['limit'] > 1 ? ceil( $total / $args['limit'] ) : 1, - ); - } else { - $query = trim( - "SELECT webhook_id - FROM {$wpdb->prefix}wc_webhooks - WHERE 1=1 - {$status} - {$search} - {$include} - {$exclude} - {$date_created} - {$date_modified} - {$order} - {$limit} - {$offset}" - ); - - $webhook_ids = wp_parse_id_list( $wpdb->get_col( $query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $return_value = $webhook_ids; - } - - wp_cache_set( $cache_key, $return_value, 'webhook_search_results' ); - - return $return_value; - } - - /** - * Count webhooks. - * - * @since 3.6.0 - * @param string $status Status to count. - * @return int - */ - protected function get_webhook_count( $status = 'active' ) { - global $wpdb; - $cache_key = WC_Cache_Helper::get_cache_prefix( 'webhooks' ) . $status . '_count'; - $count = wp_cache_get( $cache_key, 'webhooks' ); - - if ( false === $count ) { - $count = absint( $wpdb->get_var( $wpdb->prepare( "SELECT count( webhook_id ) FROM {$wpdb->prefix}wc_webhooks WHERE `status` = %s;", $status ) ) ); - - wp_cache_add( $cache_key, $count, 'webhooks' ); - } - - return $count; - } - - /** - * Get total webhook counts by status. - * - * @return array - */ - public function get_count_webhooks_by_status() { - $statuses = array_keys( wc_get_webhook_statuses() ); - $counts = array(); - - foreach ( $statuses as $status ) { - $counts[ $status ] = $this->get_webhook_count( $status ); - } - - return $counts; - } - - /** - * Check if a given string is in known statuses, based on return value of @see wc_get_webhook_statuses(). - * - * @since 3.6.0 - * @throws InvalidArgumentException If $status is not empty and not in the known wc_get_webhook_statuses() keys. - * @param string $status Status to check. - */ - private function validate_status( $status ) { - if ( ! array_key_exists( $status, wc_get_webhook_statuses() ) ) { - throw new InvalidArgumentException( sprintf( 'Invalid status given: %s. Status must be one of: %s.', $status, implode( ', ', array_keys( wc_get_webhook_statuses() ) ) ) ); - } - } - - /** - * Get the transient key used to cache a set of webhook IDs, optionally filtered by status. - * - * @since 3.6.0 - * @param string $status Optional - status of cache key. - * @return string - */ - private function get_transient_key( $status = '' ) { - return empty( $status ) ? 'woocommerce_webhook_ids' : sprintf( 'woocommerce_webhook_ids_status_%s', $status ); - } - - /** - * Delete the transients used to cache a set of webhook IDs, optionally filtered by status. - * - * @since 3.6.0 - * @param string $status Optional - status of cache to delete, or 'all' to delete all caches. - */ - private function delete_transients( $status = '' ) { - - // Always delete the non-filtered cache. - delete_transient( $this->get_transient_key( '' ) ); - - if ( ! empty( $status ) ) { - if ( 'all' === $status ) { - foreach ( wc_get_webhook_statuses() as $status_key => $status_string ) { - delete_transient( $this->get_transient_key( $status_key ) ); - } - } else { - delete_transient( $this->get_transient_key( $status ) ); - } - } - } -} diff --git a/includes/emails/class-wc-email-customer-new-account.php b/includes/emails/class-wc-email-customer-new-account.php deleted file mode 100644 index 3cd0a825b58..00000000000 --- a/includes/emails/class-wc-email-customer-new-account.php +++ /dev/null @@ -1,173 +0,0 @@ -id = 'customer_new_account'; - $this->customer_email = true; - $this->title = __( 'New account', 'woocommerce' ); - $this->description = __( 'Customer "new account" emails are sent to the customer when a customer signs up via checkout or account pages.', 'woocommerce' ); - $this->template_html = 'emails/customer-new-account.php'; - $this->template_plain = 'emails/plain/customer-new-account.php'; - - // Call parent constructor. - parent::__construct(); - } - - /** - * Get email subject. - * - * @since 3.1.0 - * @return string - */ - public function get_default_subject() { - return __( 'Your {site_title} account has been created!', 'woocommerce' ); - } - - /** - * Get email heading. - * - * @since 3.1.0 - * @return string - */ - public function get_default_heading() { - return __( 'Welcome to {site_title}', 'woocommerce' ); - } - - /** - * Trigger. - * - * @param int $user_id User ID. - * @param string $user_pass User password. - * @param bool $password_generated Whether the password was generated automatically or not. - */ - public function trigger( $user_id, $user_pass = '', $password_generated = false ) { - $this->setup_locale(); - - if ( $user_id ) { - $this->object = new WP_User( $user_id ); - - $this->user_pass = $user_pass; - $this->user_login = stripslashes( $this->object->user_login ); - $this->user_email = stripslashes( $this->object->user_email ); - $this->recipient = $this->user_email; - $this->password_generated = $password_generated; - } - - if ( $this->is_enabled() && $this->get_recipient() ) { - $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); - } - - $this->restore_locale(); - } - - /** - * Get content html. - * - * @return string - */ - public function get_content_html() { - return wc_get_template_html( - $this->template_html, - array( - 'email_heading' => $this->get_heading(), - 'additional_content' => $this->get_additional_content(), - 'user_login' => $this->user_login, - 'user_pass' => $this->user_pass, - 'blogname' => $this->get_blogname(), - 'password_generated' => $this->password_generated, - 'sent_to_admin' => false, - 'plain_text' => false, - 'email' => $this, - ) - ); - } - - /** - * Get content plain. - * - * @return string - */ - public function get_content_plain() { - return wc_get_template_html( - $this->template_plain, - array( - 'email_heading' => $this->get_heading(), - 'additional_content' => $this->get_additional_content(), - 'user_login' => $this->user_login, - 'user_pass' => $this->user_pass, - 'blogname' => $this->get_blogname(), - 'password_generated' => $this->password_generated, - 'sent_to_admin' => false, - 'plain_text' => true, - 'email' => $this, - ) - ); - } - - /** - * Default content to show below main email content. - * - * @since 3.7.0 - * @return string - */ - public function get_default_additional_content() { - return __( 'We look forward to seeing you soon.', 'woocommerce' ); - } - } - -endif; - -return new WC_Email_Customer_New_Account(); diff --git a/includes/emails/class-wc-email-customer-on-hold-order.php b/includes/emails/class-wc-email-customer-on-hold-order.php deleted file mode 100644 index 080abb92395..00000000000 --- a/includes/emails/class-wc-email-customer-on-hold-order.php +++ /dev/null @@ -1,148 +0,0 @@ -id = 'customer_on_hold_order'; - $this->customer_email = true; - $this->title = __( 'Order on-hold', 'woocommerce' ); - $this->description = __( 'This is an order notification sent to customers containing order details after an order is placed on-hold.', 'woocommerce' ); - $this->template_html = 'emails/customer-on-hold-order.php'; - $this->template_plain = 'emails/plain/customer-on-hold-order.php'; - $this->placeholders = array( - '{order_date}' => '', - '{order_number}' => '', - ); - - // Triggers for this email. - add_action( 'woocommerce_order_status_pending_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); - add_action( 'woocommerce_order_status_failed_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); - add_action( 'woocommerce_order_status_cancelled_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); - - // Call parent constructor. - parent::__construct(); - } - - /** - * Get email subject. - * - * @since 3.1.0 - * @return string - */ - public function get_default_subject() { - return __( 'Your {site_title} order has been received!', 'woocommerce' ); - } - - /** - * Get email heading. - * - * @since 3.1.0 - * @return string - */ - public function get_default_heading() { - return __( 'Thank you for your order', 'woocommerce' ); - } - - /** - * Trigger the sending of this email. - * - * @param int $order_id The order ID. - * @param WC_Order|false $order Order object. - */ - public function trigger( $order_id, $order = false ) { - $this->setup_locale(); - - if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { - $order = wc_get_order( $order_id ); - } - - if ( is_a( $order, 'WC_Order' ) ) { - $this->object = $order; - $this->recipient = $this->object->get_billing_email(); - $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); - $this->placeholders['{order_number}'] = $this->object->get_order_number(); - } - - if ( $this->is_enabled() && $this->get_recipient() ) { - $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); - } - - $this->restore_locale(); - } - - /** - * Get content html. - * - * @return string - */ - public function get_content_html() { - return wc_get_template_html( - $this->template_html, - array( - 'order' => $this->object, - 'email_heading' => $this->get_heading(), - 'additional_content' => $this->get_additional_content(), - 'sent_to_admin' => false, - 'plain_text' => false, - 'email' => $this, - ) - ); - } - - /** - * Get content plain. - * - * @return string - */ - public function get_content_plain() { - return wc_get_template_html( - $this->template_plain, - array( - 'order' => $this->object, - 'email_heading' => $this->get_heading(), - 'additional_content' => $this->get_additional_content(), - 'sent_to_admin' => false, - 'plain_text' => true, - 'email' => $this, - ) - ); - } - - /** - * Default content to show below main email content. - * - * @since 3.7.0 - * @return string - */ - public function get_default_additional_content() { - return __( 'We look forward to fulfilling your order soon.', 'woocommerce' ); - } - } - -endif; - -return new WC_Email_Customer_On_Hold_Order(); diff --git a/includes/emails/class-wc-email-new-order.php b/includes/emails/class-wc-email-new-order.php deleted file mode 100644 index 9cc757495e3..00000000000 --- a/includes/emails/class-wc-email-new-order.php +++ /dev/null @@ -1,229 +0,0 @@ -id = 'new_order'; - $this->title = __( 'New order', 'woocommerce' ); - $this->description = __( 'New order emails are sent to chosen recipient(s) when a new order is received.', 'woocommerce' ); - $this->template_html = 'emails/admin-new-order.php'; - $this->template_plain = 'emails/plain/admin-new-order.php'; - $this->placeholders = array( - '{order_date}' => '', - '{order_number}' => '', - ); - - // Triggers for this email. - add_action( 'woocommerce_order_status_pending_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); - add_action( 'woocommerce_order_status_pending_to_completed_notification', array( $this, 'trigger' ), 10, 2 ); - add_action( 'woocommerce_order_status_pending_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); - add_action( 'woocommerce_order_status_failed_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); - add_action( 'woocommerce_order_status_failed_to_completed_notification', array( $this, 'trigger' ), 10, 2 ); - add_action( 'woocommerce_order_status_failed_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); - add_action( 'woocommerce_order_status_cancelled_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); - add_action( 'woocommerce_order_status_cancelled_to_completed_notification', array( $this, 'trigger' ), 10, 2 ); - add_action( 'woocommerce_order_status_cancelled_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); - - // Call parent constructor. - parent::__construct(); - - // Other settings. - $this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) ); - } - - /** - * Get email subject. - * - * @since 3.1.0 - * @return string - */ - public function get_default_subject() { - return __( '[{site_title}]: New order #{order_number}', 'woocommerce' ); - } - - /** - * Get email heading. - * - * @since 3.1.0 - * @return string - */ - public function get_default_heading() { - return __( 'New Order: #{order_number}', 'woocommerce' ); - } - - /** - * Trigger the sending of this email. - * - * @param int $order_id The order ID. - * @param WC_Order|false $order Order object. - */ - public function trigger( $order_id, $order = false ) { - $this->setup_locale(); - - if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { - $order = wc_get_order( $order_id ); - } - - if ( is_a( $order, 'WC_Order' ) ) { - $this->object = $order; - $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); - $this->placeholders['{order_number}'] = $this->object->get_order_number(); - - $email_already_sent = $order->get_meta( '_new_order_email_sent' ); - } - - /** - * Controls if new order emails can be resend multiple times. - * - * @since 5.0.0 - * @param bool $allows Defaults to true. - */ - if ( 'true' === $email_already_sent && ! apply_filters( 'woocommerce_new_order_email_allows_resend', false ) ) { - return; - } - - if ( $this->is_enabled() && $this->get_recipient() ) { - $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); - - $order->update_meta_data( '_new_order_email_sent', 'true' ); - $order->save(); - } - - $this->restore_locale(); - } - - /** - * Get content html. - * - * @return string - */ - public function get_content_html() { - return wc_get_template_html( - $this->template_html, - array( - 'order' => $this->object, - 'email_heading' => $this->get_heading(), - 'additional_content' => $this->get_additional_content(), - 'sent_to_admin' => true, - 'plain_text' => false, - 'email' => $this, - ) - ); - } - - /** - * Get content plain. - * - * @return string - */ - public function get_content_plain() { - return wc_get_template_html( - $this->template_plain, - array( - 'order' => $this->object, - 'email_heading' => $this->get_heading(), - 'additional_content' => $this->get_additional_content(), - 'sent_to_admin' => true, - 'plain_text' => true, - 'email' => $this, - ) - ); - } - - /** - * Default content to show below main email content. - * - * @since 3.7.0 - * @return string - */ - public function get_default_additional_content() { - return __( 'Congratulations on the sale.', 'woocommerce' ); - } - - /** - * Initialise settings form fields. - */ - public function init_form_fields() { - /* translators: %s: list of placeholders */ - $placeholder_text = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '' . implode( ', ', array_keys( $this->placeholders ) ) . '' ); - $this->form_fields = array( - 'enabled' => array( - 'title' => __( 'Enable/Disable', 'woocommerce' ), - 'type' => 'checkbox', - 'label' => __( 'Enable this email notification', 'woocommerce' ), - 'default' => 'yes', - ), - 'recipient' => array( - 'title' => __( 'Recipient(s)', 'woocommerce' ), - 'type' => 'text', - /* translators: %s: WP admin email */ - 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce' ), '' . esc_attr( get_option( 'admin_email' ) ) . '' ), - 'placeholder' => '', - 'default' => '', - 'desc_tip' => true, - ), - 'subject' => array( - 'title' => __( 'Subject', 'woocommerce' ), - 'type' => 'text', - 'desc_tip' => true, - 'description' => $placeholder_text, - 'placeholder' => $this->get_default_subject(), - 'default' => '', - ), - 'heading' => array( - 'title' => __( 'Email heading', 'woocommerce' ), - 'type' => 'text', - 'desc_tip' => true, - 'description' => $placeholder_text, - 'placeholder' => $this->get_default_heading(), - 'default' => '', - ), - 'additional_content' => array( - 'title' => __( 'Additional content', 'woocommerce' ), - 'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text, - 'css' => 'width:400px; height: 75px;', - 'placeholder' => __( 'N/A', 'woocommerce' ), - 'type' => 'textarea', - 'default' => $this->get_default_additional_content(), - 'desc_tip' => true, - ), - 'email_type' => array( - 'title' => __( 'Email type', 'woocommerce' ), - 'type' => 'select', - 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), - 'default' => 'html', - 'class' => 'email_type wc-enhanced-select', - 'options' => $this->get_email_type_options(), - 'desc_tip' => true, - ), - ); - } - } - -endif; - -return new WC_Email_New_Order(); diff --git a/includes/emails/class-wc-email.php b/includes/emails/class-wc-email.php deleted file mode 100644 index d5accd8347a..00000000000 --- a/includes/emails/class-wc-email.php +++ /dev/null @@ -1,1080 +0,0 @@ -', // Greater-than. - '<', // Less-than. - '&', // Ampersand. - '&', // Ampersand. - '(c)', // Copyright. - '(tm)', // Trademark. - '(R)', // Registered. - '--', // mdash. - '-', // ndash. - '*', // Bullet. - '£', // Pound sign. - 'EUR', // Euro sign. € ?. - '$', // Dollar sign. - '', // Unknown/unhandled entities. - ' ', // Runs of spaces, post-handling. - ); - - /** - * Strings to find/replace in subjects/headings. - * - * @var array - */ - protected $placeholders = array(); - - /** - * Strings to find in subjects/headings. - * - * @deprecated 3.2.0 in favour of placeholders - * @var array - */ - public $find = array(); - - /** - * Strings to replace in subjects/headings. - * - * @deprecated 3.2.0 in favour of placeholders - * @var array - */ - public $replace = array(); - - /** - * Constructor. - */ - public function __construct() { - // Find/replace. - $this->placeholders = array_merge( - array( - '{site_title}' => $this->get_blogname(), - '{site_address}' => wp_parse_url( home_url(), PHP_URL_HOST ), - '{site_url}' => wp_parse_url( home_url(), PHP_URL_HOST ), - ), - $this->placeholders - ); - - // Init settings. - $this->init_form_fields(); - $this->init_settings(); - - // Default template base if not declared in child constructor. - if ( is_null( $this->template_base ) ) { - $this->template_base = WC()->plugin_path() . '/templates/'; - } - - $this->email_type = $this->get_option( 'email_type' ); - $this->enabled = $this->get_option( 'enabled' ); - - add_action( 'phpmailer_init', array( $this, 'handle_multipart' ) ); - add_action( 'woocommerce_update_options_email_' . $this->id, array( $this, 'process_admin_options' ) ); - } - - /** - * Handle multipart mail. - * - * @param PHPMailer $mailer PHPMailer object. - * @return PHPMailer - */ - public function handle_multipart( $mailer ) { - if ( $this->sending && 'multipart' === $this->get_email_type() ) { - $mailer->AltBody = wordwrap( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - preg_replace( $this->plain_search, $this->plain_replace, wp_strip_all_tags( $this->get_content_plain() ) ) - ); - $this->sending = false; - } - return $mailer; - } - - /** - * Format email string. - * - * @param mixed $string Text to replace placeholders in. - * @return string - */ - public function format_string( $string ) { - $find = array_keys( $this->placeholders ); - $replace = array_values( $this->placeholders ); - - // If using legacy find replace, add those to our find/replace arrays first. @todo deprecate in 4.0.0. - $find = array_merge( (array) $this->find, $find ); - $replace = array_merge( (array) $this->replace, $replace ); - - // Take care of blogname which is no longer defined as a valid placeholder. - $find[] = '{blogname}'; - $replace[] = $this->get_blogname(); - - // If using the older style filters for find and replace, ensure the array is associative and then pass through filters. @todo deprecate in 4.0.0. - if ( has_filter( 'woocommerce_email_format_string_replace' ) || has_filter( 'woocommerce_email_format_string_find' ) ) { - $legacy_find = $this->find; - $legacy_replace = $this->replace; - - foreach ( $this->placeholders as $find => $replace ) { - $legacy_key = sanitize_title( str_replace( '_', '-', trim( $find, '{}' ) ) ); - $legacy_find[ $legacy_key ] = $find; - $legacy_replace[ $legacy_key ] = $replace; - } - - $string = str_replace( apply_filters( 'woocommerce_email_format_string_find', $legacy_find, $this ), apply_filters( 'woocommerce_email_format_string_replace', $legacy_replace, $this ), $string ); - } - - /** - * Filter for main find/replace. - * - * @since 3.2.0 - */ - return apply_filters( 'woocommerce_email_format_string', str_replace( $find, $replace, $string ), $this ); - } - - /** - * Set the locale to the store locale for customer emails to make sure emails are in the store language. - */ - public function setup_locale() { - if ( $this->is_customer_email() && apply_filters( 'woocommerce_email_setup_locale', true ) ) { - wc_switch_to_site_locale(); - } - } - - /** - * Restore the locale to the default locale. Use after finished with setup_locale. - */ - public function restore_locale() { - if ( $this->is_customer_email() && apply_filters( 'woocommerce_email_restore_locale', true ) ) { - wc_restore_locale(); - } - } - - /** - * Get email subject. - * - * @since 3.1.0 - * @return string - */ - public function get_default_subject() { - return $this->subject; - } - - /** - * Get email heading. - * - * @since 3.1.0 - * @return string - */ - public function get_default_heading() { - return $this->heading; - } - - /** - * Default content to show below main email content. - * - * @since 3.7.0 - * @return string - */ - public function get_default_additional_content() { - return ''; - } - - /** - * Return content from the additional_content field. - * - * Displayed above the footer. - * - * @since 3.7.0 - * @return string - */ - public function get_additional_content() { - $content = $this->get_option( 'additional_content', '' ); - - return apply_filters( 'woocommerce_email_additional_content_' . $this->id, $this->format_string( $content ), $this->object, $this ); - } - - /** - * Get email subject. - * - * @return string - */ - public function get_subject() { - return apply_filters( 'woocommerce_email_subject_' . $this->id, $this->format_string( $this->get_option( 'subject', $this->get_default_subject() ) ), $this->object, $this ); - } - - /** - * Get email heading. - * - * @return string - */ - public function get_heading() { - return apply_filters( 'woocommerce_email_heading_' . $this->id, $this->format_string( $this->get_option( 'heading', $this->get_default_heading() ) ), $this->object, $this ); - } - - /** - * Get valid recipients. - * - * @return string - */ - public function get_recipient() { - $recipient = apply_filters( 'woocommerce_email_recipient_' . $this->id, $this->recipient, $this->object, $this ); - $recipients = array_map( 'trim', explode( ',', $recipient ) ); - $recipients = array_filter( $recipients, 'is_email' ); - return implode( ', ', $recipients ); - } - - /** - * Get email headers. - * - * @return string - */ - public function get_headers() { - $header = 'Content-Type: ' . $this->get_content_type() . "\r\n"; - - if ( in_array( $this->id, array( 'new_order', 'cancelled_order', 'failed_order' ), true ) ) { - if ( $this->object && $this->object->get_billing_email() && ( $this->object->get_billing_first_name() || $this->object->get_billing_last_name() ) ) { - $header .= 'Reply-to: ' . $this->object->get_billing_first_name() . ' ' . $this->object->get_billing_last_name() . ' <' . $this->object->get_billing_email() . ">\r\n"; - } - } elseif ( $this->get_from_address() && $this->get_from_name() ) { - $header .= 'Reply-to: ' . $this->get_from_name() . ' <' . $this->get_from_address() . ">\r\n"; - } - - return apply_filters( 'woocommerce_email_headers', $header, $this->id, $this->object, $this ); - } - - /** - * Get email attachments. - * - * @return array - */ - public function get_attachments() { - return apply_filters( 'woocommerce_email_attachments', array(), $this->id, $this->object, $this ); - } - - /** - * Return email type. - * - * @return string - */ - public function get_email_type() { - return $this->email_type && class_exists( 'DOMDocument' ) ? $this->email_type : 'plain'; - } - - /** - * Get email content type. - * - * @param string $default_content_type Default wp_mail() content type. - * @return string - */ - public function get_content_type( $default_content_type = '' ) { - switch ( $this->get_email_type() ) { - case 'html': - $content_type = 'text/html'; - break; - case 'multipart': - $content_type = 'multipart/alternative'; - break; - default: - $content_type = 'text/plain'; - break; - } - - return apply_filters( 'woocommerce_email_content_type', $content_type, $this, $default_content_type ); - } - - /** - * Return the email's title - * - * @return string - */ - public function get_title() { - return apply_filters( 'woocommerce_email_title', $this->title, $this ); - } - - /** - * Return the email's description - * - * @return string - */ - public function get_description() { - return apply_filters( 'woocommerce_email_description', $this->description, $this ); - } - - /** - * Proxy to parent's get_option and attempt to localize the result using gettext. - * - * @param string $key Option key. - * @param mixed $empty_value Value to use when option is empty. - * @return string - */ - public function get_option( $key, $empty_value = null ) { - $value = parent::get_option( $key, $empty_value ); - return apply_filters( 'woocommerce_email_get_option', $value, $this, $value, $key, $empty_value ); - } - - /** - * Checks if this email is enabled and will be sent. - * - * @return bool - */ - public function is_enabled() { - return apply_filters( 'woocommerce_email_enabled_' . $this->id, 'yes' === $this->enabled, $this->object, $this ); - } - - /** - * Checks if this email is manually sent - * - * @return bool - */ - public function is_manual() { - return $this->manual; - } - - /** - * Checks if this email is customer focussed. - * - * @return bool - */ - public function is_customer_email() { - return $this->customer_email; - } - - /** - * Get WordPress blog name. - * - * @return string - */ - public function get_blogname() { - return wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); - } - - /** - * Get email content. - * - * @return string - */ - public function get_content() { - $this->sending = true; - - if ( 'plain' === $this->get_email_type() ) { - $email_content = wordwrap( preg_replace( $this->plain_search, $this->plain_replace, wp_strip_all_tags( $this->get_content_plain() ) ), 70 ); - } else { - $email_content = $this->get_content_html(); - } - - return $email_content; - } - - /** - * Apply inline styles to dynamic content. - * - * We only inline CSS for html emails, and to do so we use Emogrifier library (if supported). - * - * @version 4.0.0 - * @param string|null $content Content that will receive inline styles. - * @return string - */ - public function style_inline( $content ) { - if ( in_array( $this->get_content_type(), array( 'text/html', 'multipart/alternative' ), true ) ) { - ob_start(); - wc_get_template( 'emails/email-styles.php' ); - $css = apply_filters( 'woocommerce_email_styles', ob_get_clean(), $this ); - - $emogrifier_class = 'Pelago\\Emogrifier'; - - if ( $this->supports_emogrifier() && class_exists( $emogrifier_class ) ) { - try { - $emogrifier = new $emogrifier_class( $content, $css ); - - do_action( 'woocommerce_emogrifier', $emogrifier, $this ); - - $content = $emogrifier->emogrify(); - $html_prune = \Pelago\Emogrifier\HtmlProcessor\HtmlPruner::fromHtml( $content ); - $html_prune->removeElementsWithDisplayNone(); - $content = $html_prune->render(); - } catch ( Exception $e ) { - $logger = wc_get_logger(); - $logger->error( $e->getMessage(), array( 'source' => 'emogrifier' ) ); - } - } else { - $content = '' . $content; - } - } - - return $content; - } - - /** - * Return if emogrifier library is supported. - * - * @version 4.0.0 - * @since 3.5.0 - * @return bool - */ - protected function supports_emogrifier() { - return class_exists( 'DOMDocument' ); - } - - /** - * Get the email content in plain text format. - * - * @return string - */ - public function get_content_plain() { - return ''; } - - /** - * Get the email content in HTML format. - * - * @return string - */ - public function get_content_html() { - return ''; } - - /** - * Get the from name for outgoing emails. - * - * @param string $from_name Default wp_mail() name associated with the "from" email address. - * @return string - */ - public function get_from_name( $from_name = '' ) { - $from_name = apply_filters( 'woocommerce_email_from_name', get_option( 'woocommerce_email_from_name' ), $this, $from_name ); - return wp_specialchars_decode( esc_html( $from_name ), ENT_QUOTES ); - } - - /** - * Get the from address for outgoing emails. - * - * @param string $from_email Default wp_mail() email address to send from. - * @return string - */ - public function get_from_address( $from_email = '' ) { - $from_email = apply_filters( 'woocommerce_email_from_address', get_option( 'woocommerce_email_from_address' ), $this, $from_email ); - return sanitize_email( $from_email ); - } - - /** - * Send an email. - * - * @param string $to Email to. - * @param string $subject Email subject. - * @param string $message Email message. - * @param string $headers Email headers. - * @param array $attachments Email attachments. - * @return bool success - */ - public function send( $to, $subject, $message, $headers, $attachments ) { - add_filter( 'wp_mail_from', array( $this, 'get_from_address' ) ); - add_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) ); - add_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) ); - - $message = apply_filters( 'woocommerce_mail_content', $this->style_inline( $message ) ); - $mail_callback = apply_filters( 'woocommerce_mail_callback', 'wp_mail', $this ); - $mail_callback_params = apply_filters( 'woocommerce_mail_callback_params', array( $to, $subject, $message, $headers, $attachments ), $this ); - $return = $mail_callback( ...$mail_callback_params ); - - remove_filter( 'wp_mail_from', array( $this, 'get_from_address' ) ); - remove_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) ); - remove_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) ); - - return $return; - } - - /** - * Initialise Settings Form Fields - these are generic email options most will use. - */ - public function init_form_fields() { - /* translators: %s: list of placeholders */ - $placeholder_text = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '' . esc_html( implode( ', ', array_keys( $this->placeholders ) ) ) . '' ); - $this->form_fields = array( - 'enabled' => array( - 'title' => __( 'Enable/Disable', 'woocommerce' ), - 'type' => 'checkbox', - 'label' => __( 'Enable this email notification', 'woocommerce' ), - 'default' => 'yes', - ), - 'subject' => array( - 'title' => __( 'Subject', 'woocommerce' ), - 'type' => 'text', - 'desc_tip' => true, - 'description' => $placeholder_text, - 'placeholder' => $this->get_default_subject(), - 'default' => '', - ), - 'heading' => array( - 'title' => __( 'Email heading', 'woocommerce' ), - 'type' => 'text', - 'desc_tip' => true, - 'description' => $placeholder_text, - 'placeholder' => $this->get_default_heading(), - 'default' => '', - ), - 'additional_content' => array( - 'title' => __( 'Additional content', 'woocommerce' ), - 'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text, - 'css' => 'width:400px; height: 75px;', - 'placeholder' => __( 'N/A', 'woocommerce' ), - 'type' => 'textarea', - 'default' => $this->get_default_additional_content(), - 'desc_tip' => true, - ), - 'email_type' => array( - 'title' => __( 'Email type', 'woocommerce' ), - 'type' => 'select', - 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), - 'default' => 'html', - 'class' => 'email_type wc-enhanced-select', - 'options' => $this->get_email_type_options(), - 'desc_tip' => true, - ), - ); - } - - /** - * Email type options. - * - * @return array - */ - public function get_email_type_options() { - $types = array( 'plain' => __( 'Plain text', 'woocommerce' ) ); - - if ( class_exists( 'DOMDocument' ) ) { - $types['html'] = __( 'HTML', 'woocommerce' ); - $types['multipart'] = __( 'Multipart', 'woocommerce' ); - } - - return $types; - } - - /** - * Admin Panel Options Processing. - */ - public function process_admin_options() { - // Save regular options. - parent::process_admin_options(); - - $post_data = $this->get_post_data(); - - // Save templates. - if ( isset( $post_data['template_html_code'] ) ) { - $this->save_template( $post_data['template_html_code'], $this->template_html ); - } - if ( isset( $post_data['template_plain_code'] ) ) { - $this->save_template( $post_data['template_plain_code'], $this->template_plain ); - } - } - - /** - * Get template. - * - * @param string $type Template type. Can be either 'template_html' or 'template_plain'. - * @return string - */ - public function get_template( $type ) { - $type = basename( $type ); - - if ( 'template_html' === $type ) { - return $this->template_html; - } elseif ( 'template_plain' === $type ) { - return $this->template_plain; - } - return ''; - } - - /** - * Save the email templates. - * - * @since 2.4.0 - * @param string $template_code Template code. - * @param string $template_path Template path. - */ - protected function save_template( $template_code, $template_path ) { - if ( current_user_can( 'edit_themes' ) && ! empty( $template_code ) && ! empty( $template_path ) ) { - $saved = false; - $file = get_stylesheet_directory() . '/' . WC()->template_path() . $template_path; - $code = wp_unslash( $template_code ); - - if ( is_writeable( $file ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writeable - $f = fopen( $file, 'w+' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen - - if ( false !== $f ) { - fwrite( $f, $code ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite - fclose( $f ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose - $saved = true; - } - } - - if ( ! $saved ) { - $redirect = add_query_arg( 'wc_error', rawurlencode( __( 'Could not write to template file.', 'woocommerce' ) ) ); - wp_safe_redirect( $redirect ); - exit; - } - } - } - - /** - * Get the template file in the current theme. - * - * @param string $template Template name. - * - * @return string - */ - public function get_theme_template_file( $template ) { - return get_stylesheet_directory() . '/' . apply_filters( 'woocommerce_template_directory', 'woocommerce', $template ) . '/' . $template; - } - - /** - * Move template action. - * - * @param string $template_type Template type. - */ - protected function move_template_action( $template_type ) { - $template = $this->get_template( $template_type ); - if ( ! empty( $template ) ) { - $theme_file = $this->get_theme_template_file( $template ); - - if ( wp_mkdir_p( dirname( $theme_file ) ) && ! file_exists( $theme_file ) ) { - - // Locate template file. - $core_file = $this->template_base . $template; - $template_file = apply_filters( 'woocommerce_locate_core_template', $core_file, $template, $this->template_base, $this->id ); - - // Copy template file. - copy( $template_file, $theme_file ); - - /** - * Action hook fired after copying email template file. - * - * @param string $template_type The copied template type - * @param string $email The email object - */ - do_action( 'woocommerce_copy_email_template', $template_type, $this ); - - ?> -
    -

    -
    - get_template( $template_type ); - - if ( $template ) { - if ( ! empty( $template ) ) { - $theme_file = $this->get_theme_template_file( $template ); - - if ( file_exists( $theme_file ) ) { - unlink( $theme_file ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink - - /** - * Action hook fired after deleting template file. - * - * @param string $template The deleted template type - * @param string $email The email object - */ - do_action( 'woocommerce_delete_email_template', $template_type, $this ); - ?> -
    -

    -
    - template_html ) || ! empty( $this->template_plain ) ) - && ( ! empty( $_GET['move_template'] ) || ! empty( $_GET['delete_template'] ) ) - && 'GET' === $_SERVER['REQUEST_METHOD'] // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated - ) { - if ( empty( $_GET['_wc_email_nonce'] ) || ! wp_verify_nonce( wc_clean( wp_unslash( $_GET['_wc_email_nonce'] ) ), 'woocommerce_email_template_nonce' ) ) { - wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); - } - - if ( ! current_user_can( 'edit_themes' ) ) { - wp_die( esc_html__( 'You don’t have permission to do this.', 'woocommerce' ) ); - } - - if ( ! empty( $_GET['move_template'] ) ) { - $this->move_template_action( wc_clean( wp_unslash( $_GET['move_template'] ) ) ); - } - - if ( ! empty( $_GET['delete_template'] ) ) { - $this->delete_template_action( wc_clean( wp_unslash( $_GET['delete_template'] ) ) ); - } - } - } - - /** - * Admin Options. - * - * Setup the email settings screen. - * Override this in your email. - * - * @since 1.0.0 - */ - public function admin_options() { - // Do admin actions. - $this->admin_actions(); - ?> -

    get_title() ); ?>

    - - get_description() ) ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?> - - - - - generate_settings_html(); ?> -
    - - - - template_html ) || ! empty( $this->template_plain ) ) ) { - ?> -
    - __( 'HTML template', 'woocommerce' ), - 'template_plain' => __( 'Plain text template', 'woocommerce' ), - ); - - foreach ( $templates as $template_type => $title ) : - $template = $this->get_template( $template_type ); - - if ( empty( $template ) ) { - continue; - } - - $local_file = $this->get_theme_template_file( $template ); - $core_file = $this->template_base . $template; - $template_file = apply_filters( 'woocommerce_locate_core_template', $core_file, $template, $this->template_base, $this->id ); - $template_dir = apply_filters( 'woocommerce_template_directory', 'woocommerce', $template ); - ?> -
    -

    - - -

    - - - - - - - - - ' . esc_html( trailingslashit( basename( get_stylesheet_directory() ) ) . $template_dir . '/' . $template ) . '' ); - ?> -

    - - - -

    - - - - - - - - - ' . esc_html( plugin_basename( $template_file ) ) . '', '' . esc_html( trailingslashit( basename( get_stylesheet_directory() ) ) . $template_dir . '/' . $template ) . '' ); - ?> -

    - - - -

    - -
    - -
    - - column_names = $this->get_default_column_names(); - } - - /** - * Get file path to export to. - * - * @return string - */ - protected function get_file_path() { - $upload_dir = wp_upload_dir(); - return trailingslashit( $upload_dir['basedir'] ) . $this->get_filename(); - } - - /** - * Get the file contents. - * - * @since 3.1.0 - * @return string - */ - public function get_file() { - $file = ''; - if ( @file_exists( $this->get_file_path() ) ) { // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged - $file = @file_get_contents( $this->get_file_path() ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.WP.AlternativeFunctions.file_system_read_file_get_contents - } else { - @file_put_contents( $this->get_file_path(), '' ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_file_put_contents, Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents - @chmod( $this->get_file_path(), 0664 ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.chmod_chmod, WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents, Generic.PHP.NoSilencedErrors.Discouraged - } - return $file; - } - - /** - * Serve the file and remove once sent to the client. - * - * @since 3.1.0 - */ - public function export() { - $this->send_headers(); - $this->send_content( $this->get_file() ); - @unlink( $this->get_file_path() ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink, Generic.PHP.NoSilencedErrors.Discouraged - die(); - } - - /** - * Generate the CSV file. - * - * @since 3.1.0 - */ - public function generate_file() { - if ( 1 === $this->get_page() ) { - @unlink( $this->get_file_path() ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink, Generic.PHP.NoSilencedErrors.Discouraged, - } - $this->prepare_data_to_export(); - $this->write_csv_data( $this->get_csv_data() ); - } - - /** - * Write data to the file. - * - * @since 3.1.0 - * @param string $data Data. - */ - protected function write_csv_data( $data ) { - $file = $this->get_file(); - - // Add columns when finished. - if ( 100 === $this->get_percent_complete() ) { - $file = chr( 239 ) . chr( 187 ) . chr( 191 ) . $this->export_column_headers() . $file; - } - - $file .= $data; - @file_put_contents( $this->get_file_path(), $file ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_file_put_contents, Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents - } - - /** - * Get page. - * - * @since 3.1.0 - * @return int - */ - public function get_page() { - return $this->page; - } - - /** - * Set page. - * - * @since 3.1.0 - * @param int $page Page Nr. - */ - public function set_page( $page ) { - $this->page = absint( $page ); - } - - /** - * Get count of records exported. - * - * @since 3.1.0 - * @return int - */ - public function get_total_exported() { - return ( ( $this->get_page() - 1 ) * $this->get_limit() ) + $this->exported_row_count; - } - - /** - * Get total % complete. - * - * @since 3.1.0 - * @return int - */ - public function get_percent_complete() { - return $this->total_rows ? floor( ( $this->get_total_exported() / $this->total_rows ) * 100 ) : 100; - } -} diff --git a/includes/export/class-wc-product-csv-exporter.php b/includes/export/class-wc-product-csv-exporter.php deleted file mode 100644 index a2380da9bee..00000000000 --- a/includes/export/class-wc-product-csv-exporter.php +++ /dev/null @@ -1,730 +0,0 @@ -set_product_types_to_export( array_keys( WC_Admin_Exporters::get_product_types() ) ); - } - - /** - * Should meta be exported? - * - * @param bool $enable_meta_export Should meta be exported. - * - * @since 3.1.0 - */ - public function enable_meta_export( $enable_meta_export ) { - $this->enable_meta_export = (bool) $enable_meta_export; - } - - /** - * Product types to export. - * - * @param array $product_types_to_export List of types to export. - * - * @since 3.1.0 - */ - public function set_product_types_to_export( $product_types_to_export ) { - $this->product_types_to_export = array_map( 'wc_clean', $product_types_to_export ); - } - - /** - * Product category to export - * - * @param string $product_category_to_export Product category slug to export, empty string exports all. - * - * @since 3.5.0 - * @return void - */ - public function set_product_category_to_export( $product_category_to_export ) { - $this->product_category_to_export = array_map( 'sanitize_title_with_dashes', $product_category_to_export ); - } - - /** - * Return an array of columns to export. - * - * @since 3.1.0 - * @return array - */ - public function get_default_column_names() { - return apply_filters( - "woocommerce_product_export_{$this->export_type}_default_columns", - array( - 'id' => __( 'ID', 'woocommerce' ), - 'type' => __( 'Type', 'woocommerce' ), - 'sku' => __( 'SKU', 'woocommerce' ), - 'name' => __( 'Name', 'woocommerce' ), - 'published' => __( 'Published', 'woocommerce' ), - 'featured' => __( 'Is featured?', 'woocommerce' ), - 'catalog_visibility' => __( 'Visibility in catalog', 'woocommerce' ), - 'short_description' => __( 'Short description', 'woocommerce' ), - 'description' => __( 'Description', 'woocommerce' ), - 'date_on_sale_from' => __( 'Date sale price starts', 'woocommerce' ), - 'date_on_sale_to' => __( 'Date sale price ends', 'woocommerce' ), - 'tax_status' => __( 'Tax status', 'woocommerce' ), - 'tax_class' => __( 'Tax class', 'woocommerce' ), - 'stock_status' => __( 'In stock?', 'woocommerce' ), - 'stock' => __( 'Stock', 'woocommerce' ), - 'low_stock_amount' => __( 'Low stock amount', 'woocommerce' ), - 'backorders' => __( 'Backorders allowed?', 'woocommerce' ), - 'sold_individually' => __( 'Sold individually?', 'woocommerce' ), - /* translators: %s: weight */ - 'weight' => sprintf( __( 'Weight (%s)', 'woocommerce' ), get_option( 'woocommerce_weight_unit' ) ), - /* translators: %s: length */ - 'length' => sprintf( __( 'Length (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), - /* translators: %s: width */ - 'width' => sprintf( __( 'Width (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), - /* translators: %s: Height */ - 'height' => sprintf( __( 'Height (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), - 'reviews_allowed' => __( 'Allow customer reviews?', 'woocommerce' ), - 'purchase_note' => __( 'Purchase note', 'woocommerce' ), - 'sale_price' => __( 'Sale price', 'woocommerce' ), - 'regular_price' => __( 'Regular price', 'woocommerce' ), - 'category_ids' => __( 'Categories', 'woocommerce' ), - 'tag_ids' => __( 'Tags', 'woocommerce' ), - 'shipping_class_id' => __( 'Shipping class', 'woocommerce' ), - 'images' => __( 'Images', 'woocommerce' ), - 'download_limit' => __( 'Download limit', 'woocommerce' ), - 'download_expiry' => __( 'Download expiry days', 'woocommerce' ), - 'parent_id' => __( 'Parent', 'woocommerce' ), - 'grouped_products' => __( 'Grouped products', 'woocommerce' ), - 'upsell_ids' => __( 'Upsells', 'woocommerce' ), - 'cross_sell_ids' => __( 'Cross-sells', 'woocommerce' ), - 'product_url' => __( 'External URL', 'woocommerce' ), - 'button_text' => __( 'Button text', 'woocommerce' ), - 'menu_order' => __( 'Position', 'woocommerce' ), - ) - ); - } - - /** - * Prepare data for export. - * - * @since 3.1.0 - */ - public function prepare_data_to_export() { - $args = array( - 'status' => array( 'private', 'publish', 'draft', 'future', 'pending' ), - 'type' => $this->product_types_to_export, - 'limit' => $this->get_limit(), - 'page' => $this->get_page(), - 'orderby' => array( - 'ID' => 'ASC', - ), - 'return' => 'objects', - 'paginate' => true, - ); - - if ( ! empty( $this->product_category_to_export ) ) { - $args['category'] = $this->product_category_to_export; - } - $products = wc_get_products( apply_filters( "woocommerce_product_export_{$this->export_type}_query_args", $args ) ); - - $this->total_rows = $products->total; - $this->row_data = array(); - $variable_products = array(); - - foreach ( $products->products as $product ) { - // Check if the category is set, this means we need to fetch variations seperately as they are not tied to a category. - if ( ! empty( $args['category'] ) && $product->is_type( 'variable' ) ) { - $variable_products[] = $product->get_id(); - } - - $this->row_data[] = $this->generate_row_data( $product ); - } - - // If a category was selected we loop through the variations as they are not tied to a category so will be excluded by default. - if ( ! empty( $variable_products ) ) { - foreach ( $variable_products as $parent_id ) { - $products = wc_get_products( - array( - 'parent' => $parent_id, - 'type' => array( 'variation' ), - 'return' => 'objects', - 'limit' => -1, - ) - ); - - if ( ! $products ) { - continue; - } - - foreach ( $products as $product ) { - $this->row_data[] = $this->generate_row_data( $product ); - } - } - } - } - - /** - * Take a product and generate row data from it for export. - * - * @param WC_Product $product WC_Product object. - * - * @return array - */ - protected function generate_row_data( $product ) { - $columns = $this->get_column_names(); - $row = array(); - foreach ( $columns as $column_id => $column_name ) { - $column_id = strstr( $column_id, ':' ) ? current( explode( ':', $column_id ) ) : $column_id; - $value = ''; - - // Skip some columns if dynamically handled later or if we're being selective. - if ( in_array( $column_id, array( 'downloads', 'attributes', 'meta' ), true ) || ! $this->is_column_exporting( $column_id ) ) { - continue; - } - - if ( has_filter( "woocommerce_product_export_{$this->export_type}_column_{$column_id}" ) ) { - // Filter for 3rd parties. - $value = apply_filters( "woocommerce_product_export_{$this->export_type}_column_{$column_id}", '', $product, $column_id ); - - } elseif ( is_callable( array( $this, "get_column_value_{$column_id}" ) ) ) { - // Handle special columns which don't map 1:1 to product data. - $value = $this->{"get_column_value_{$column_id}"}( $product ); - - } elseif ( is_callable( array( $product, "get_{$column_id}" ) ) ) { - // Default and custom handling. - $value = $product->{"get_{$column_id}"}( 'edit' ); - } - - if ( 'description' === $column_id || 'short_description' === $column_id ) { - $value = $this->filter_description_field( $value ); - } - - $row[ $column_id ] = $value; - } - - $this->prepare_downloads_for_export( $product, $row ); - $this->prepare_attributes_for_export( $product, $row ); - $this->prepare_meta_for_export( $product, $row ); - return apply_filters( 'woocommerce_product_export_row_data', $row, $product ); - } - - /** - * Get published value. - * - * @param WC_Product $product Product being exported. - * - * @since 3.1.0 - * @return int - */ - protected function get_column_value_published( $product ) { - $statuses = array( - 'draft' => -1, - 'private' => 0, - 'publish' => 1, - ); - - // Fix display for variations when parent product is a draft. - if ( 'variation' === $product->get_type() ) { - $parent = $product->get_parent_data(); - $status = 'draft' === $parent['status'] ? $parent['status'] : $product->get_status( 'edit' ); - } else { - $status = $product->get_status( 'edit' ); - } - - return isset( $statuses[ $status ] ) ? $statuses[ $status ] : -1; - } - - /** - * Get formatted sale price. - * - * @param WC_Product $product Product being exported. - * - * @return string - */ - protected function get_column_value_sale_price( $product ) { - return wc_format_localized_price( $product->get_sale_price( 'view' ) ); - } - - /** - * Get formatted regular price. - * - * @param WC_Product $product Product being exported. - * - * @return string - */ - protected function get_column_value_regular_price( $product ) { - return wc_format_localized_price( $product->get_regular_price() ); - } - - /** - * Get product_cat value. - * - * @param WC_Product $product Product being exported. - * - * @since 3.1.0 - * @return string - */ - protected function get_column_value_category_ids( $product ) { - $term_ids = $product->get_category_ids( 'edit' ); - return $this->format_term_ids( $term_ids, 'product_cat' ); - } - - /** - * Get product_tag value. - * - * @param WC_Product $product Product being exported. - * - * @since 3.1.0 - * @return string - */ - protected function get_column_value_tag_ids( $product ) { - $term_ids = $product->get_tag_ids( 'edit' ); - return $this->format_term_ids( $term_ids, 'product_tag' ); - } - - /** - * Get product_shipping_class value. - * - * @param WC_Product $product Product being exported. - * - * @since 3.1.0 - * @return string - */ - protected function get_column_value_shipping_class_id( $product ) { - $term_ids = $product->get_shipping_class_id( 'edit' ); - return $this->format_term_ids( $term_ids, 'product_shipping_class' ); - } - - /** - * Get images value. - * - * @param WC_Product $product Product being exported. - * - * @since 3.1.0 - * @return string - */ - protected function get_column_value_images( $product ) { - $image_ids = array_merge( array( $product->get_image_id( 'edit' ) ), $product->get_gallery_image_ids( 'edit' ) ); - $images = array(); - - foreach ( $image_ids as $image_id ) { - $image = wp_get_attachment_image_src( $image_id, 'full' ); - - if ( $image ) { - $images[] = $image[0]; - } - } - - return $this->implode_values( $images ); - } - - /** - * Prepare linked products for export. - * - * @param int[] $linked_products Array of linked product ids. - * - * @since 3.1.0 - * @return string - */ - protected function prepare_linked_products_for_export( $linked_products ) { - $product_list = array(); - - foreach ( $linked_products as $linked_product ) { - if ( $linked_product->get_sku() ) { - $product_list[] = $linked_product->get_sku(); - } else { - $product_list[] = 'id:' . $linked_product->get_id(); - } - } - - return $this->implode_values( $product_list ); - } - - /** - * Get cross_sell_ids value. - * - * @param WC_Product $product Product being exported. - * - * @since 3.1.0 - * @return string - */ - protected function get_column_value_cross_sell_ids( $product ) { - return $this->prepare_linked_products_for_export( array_filter( array_map( 'wc_get_product', (array) $product->get_cross_sell_ids( 'edit' ) ) ) ); - } - - /** - * Get upsell_ids value. - * - * @param WC_Product $product Product being exported. - * - * @since 3.1.0 - * @return string - */ - protected function get_column_value_upsell_ids( $product ) { - return $this->prepare_linked_products_for_export( array_filter( array_map( 'wc_get_product', (array) $product->get_upsell_ids( 'edit' ) ) ) ); - } - - /** - * Get parent_id value. - * - * @param WC_Product $product Product being exported. - * - * @since 3.1.0 - * @return string - */ - protected function get_column_value_parent_id( $product ) { - if ( $product->get_parent_id( 'edit' ) ) { - $parent = wc_get_product( $product->get_parent_id( 'edit' ) ); - if ( ! $parent ) { - return ''; - } - - return $parent->get_sku( 'edit' ) ? $parent->get_sku( 'edit' ) : 'id:' . $parent->get_id(); - } - return ''; - } - - /** - * Get grouped_products value. - * - * @param WC_Product $product Product being exported. - * - * @since 3.1.0 - * @return string - */ - protected function get_column_value_grouped_products( $product ) { - if ( 'grouped' !== $product->get_type() ) { - return ''; - } - - $grouped_products = array(); - $child_ids = $product->get_children( 'edit' ); - foreach ( $child_ids as $child_id ) { - $child = wc_get_product( $child_id ); - if ( ! $child ) { - continue; - } - - $grouped_products[] = $child->get_sku( 'edit' ) ? $child->get_sku( 'edit' ) : 'id:' . $child_id; - } - return $this->implode_values( $grouped_products ); - } - - /** - * Get download_limit value. - * - * @param WC_Product $product Product being exported. - * - * @since 3.1.0 - * @return string - */ - protected function get_column_value_download_limit( $product ) { - return $product->is_downloadable() && $product->get_download_limit( 'edit' ) ? $product->get_download_limit( 'edit' ) : ''; - } - - /** - * Get download_expiry value. - * - * @param WC_Product $product Product being exported. - * - * @since 3.1.0 - * @return string - */ - protected function get_column_value_download_expiry( $product ) { - return $product->is_downloadable() && $product->get_download_expiry( 'edit' ) ? $product->get_download_expiry( 'edit' ) : ''; - } - - /** - * Get stock value. - * - * @param WC_Product $product Product being exported. - * - * @since 3.1.0 - * @return string - */ - protected function get_column_value_stock( $product ) { - $manage_stock = $product->get_manage_stock( 'edit' ); - $stock_quantity = $product->get_stock_quantity( 'edit' ); - - if ( $product->is_type( 'variation' ) && 'parent' === $manage_stock ) { - return 'parent'; - } elseif ( $manage_stock ) { - return $stock_quantity; - } else { - return ''; - } - } - - /** - * Get stock status value. - * - * @param WC_Product $product Product being exported. - * - * @since 3.1.0 - * @return string - */ - protected function get_column_value_stock_status( $product ) { - $status = $product->get_stock_status( 'edit' ); - - if ( 'onbackorder' === $status ) { - return 'backorder'; - } - - return 'instock' === $status ? 1 : 0; - } - - /** - * Get backorders. - * - * @param WC_Product $product Product being exported. - * - * @since 3.1.0 - * @return string - */ - protected function get_column_value_backorders( $product ) { - $backorders = $product->get_backorders( 'edit' ); - - switch ( $backorders ) { - case 'notify': - return 'notify'; - default: - return wc_string_to_bool( $backorders ) ? 1 : 0; - } - } - - /** - * Get low stock amount value. - * - * @param WC_Product $product Product being exported. - * - * @since 3.5.0 - * @return int|string Empty string if value not set - */ - protected function get_column_value_low_stock_amount( $product ) { - return $product->managing_stock() && $product->get_low_stock_amount( 'edit' ) ? $product->get_low_stock_amount( 'edit' ) : ''; - } - - /** - * Get type value. - * - * @param WC_Product $product Product being exported. - * - * @since 3.1.0 - * @return string - */ - protected function get_column_value_type( $product ) { - $types = array(); - $types[] = $product->get_type(); - - if ( $product->is_downloadable() ) { - $types[] = 'downloadable'; - } - - if ( $product->is_virtual() ) { - $types[] = 'virtual'; - } - - return $this->implode_values( $types ); - } - - /** - * Filter description field for export. - * Convert newlines to '\n'. - * - * @param string $description Product description text to filter. - * - * @since 3.5.4 - * @return string - */ - protected function filter_description_field( $description ) { - $description = str_replace( '\n', "\\\\n", $description ); - $description = str_replace( "\n", '\n', $description ); - return $description; - } - /** - * Export downloads. - * - * @param WC_Product $product Product being exported. - * @param array $row Row being exported. - * - * @since 3.1.0 - */ - protected function prepare_downloads_for_export( $product, &$row ) { - if ( $product->is_downloadable() && $this->is_column_exporting( 'downloads' ) ) { - $downloads = $product->get_downloads( 'edit' ); - - if ( $downloads ) { - $i = 1; - foreach ( $downloads as $download ) { - /* translators: %s: download number */ - $this->column_names[ 'downloads:name' . $i ] = sprintf( __( 'Download %d name', 'woocommerce' ), $i ); - /* translators: %s: download number */ - $this->column_names[ 'downloads:url' . $i ] = sprintf( __( 'Download %d URL', 'woocommerce' ), $i ); - $row[ 'downloads:name' . $i ] = $download->get_name(); - $row[ 'downloads:url' . $i ] = $download->get_file(); - $i++; - } - } - } - } - - /** - * Export attributes data. - * - * @param WC_Product $product Product being exported. - * @param array $row Row being exported. - * - * @since 3.1.0 - */ - protected function prepare_attributes_for_export( $product, &$row ) { - if ( $this->is_column_exporting( 'attributes' ) ) { - $attributes = $product->get_attributes(); - $default_attributes = $product->get_default_attributes(); - - if ( count( $attributes ) ) { - $i = 1; - foreach ( $attributes as $attribute_name => $attribute ) { - /* translators: %s: attribute number */ - $this->column_names[ 'attributes:name' . $i ] = sprintf( __( 'Attribute %d name', 'woocommerce' ), $i ); - /* translators: %s: attribute number */ - $this->column_names[ 'attributes:value' . $i ] = sprintf( __( 'Attribute %d value(s)', 'woocommerce' ), $i ); - /* translators: %s: attribute number */ - $this->column_names[ 'attributes:visible' . $i ] = sprintf( __( 'Attribute %d visible', 'woocommerce' ), $i ); - /* translators: %s: attribute number */ - $this->column_names[ 'attributes:taxonomy' . $i ] = sprintf( __( 'Attribute %d global', 'woocommerce' ), $i ); - - if ( is_a( $attribute, 'WC_Product_Attribute' ) ) { - $row[ 'attributes:name' . $i ] = wc_attribute_label( $attribute->get_name(), $product ); - - if ( $attribute->is_taxonomy() ) { - $terms = $attribute->get_terms(); - $values = array(); - - foreach ( $terms as $term ) { - $values[] = $term->name; - } - - $row[ 'attributes:value' . $i ] = $this->implode_values( $values ); - $row[ 'attributes:taxonomy' . $i ] = 1; - } else { - $row[ 'attributes:value' . $i ] = $this->implode_values( $attribute->get_options() ); - $row[ 'attributes:taxonomy' . $i ] = 0; - } - - $row[ 'attributes:visible' . $i ] = $attribute->get_visible(); - } else { - $row[ 'attributes:name' . $i ] = wc_attribute_label( $attribute_name, $product ); - - if ( 0 === strpos( $attribute_name, 'pa_' ) ) { - $option_term = get_term_by( 'slug', $attribute, $attribute_name ); // @codingStandardsIgnoreLine. - $row[ 'attributes:value' . $i ] = $option_term && ! is_wp_error( $option_term ) ? str_replace( ',', '\\,', $option_term->name ) : str_replace( ',', '\\,', $attribute ); - $row[ 'attributes:taxonomy' . $i ] = 1; - } else { - $row[ 'attributes:value' . $i ] = str_replace( ',', '\\,', $attribute ); - $row[ 'attributes:taxonomy' . $i ] = 0; - } - - $row[ 'attributes:visible' . $i ] = ''; - } - - if ( $product->is_type( 'variable' ) && isset( $default_attributes[ sanitize_title( $attribute_name ) ] ) ) { - /* translators: %s: attribute number */ - $this->column_names[ 'attributes:default' . $i ] = sprintf( __( 'Attribute %d default', 'woocommerce' ), $i ); - $default_value = $default_attributes[ sanitize_title( $attribute_name ) ]; - - if ( 0 === strpos( $attribute_name, 'pa_' ) ) { - $option_term = get_term_by( 'slug', $default_value, $attribute_name ); // @codingStandardsIgnoreLine. - $row[ 'attributes:default' . $i ] = $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $default_value; - } else { - $row[ 'attributes:default' . $i ] = $default_value; - } - } - $i++; - } - } - } - } - - /** - * Export meta data. - * - * @param WC_Product $product Product being exported. - * @param array $row Row data. - * - * @since 3.1.0 - */ - protected function prepare_meta_for_export( $product, &$row ) { - if ( $this->enable_meta_export ) { - $meta_data = $product->get_meta_data(); - - if ( count( $meta_data ) ) { - $meta_keys_to_skip = apply_filters( 'woocommerce_product_export_skip_meta_keys', array(), $product ); - - $i = 1; - foreach ( $meta_data as $meta ) { - if ( in_array( $meta->key, $meta_keys_to_skip, true ) ) { - continue; - } - - // Allow 3rd parties to process the meta, e.g. to transform non-scalar values to scalar. - $meta_value = apply_filters( 'woocommerce_product_export_meta_value', $meta->value, $meta, $product, $row ); - - if ( ! is_scalar( $meta_value ) ) { - continue; - } - - $column_key = 'meta:' . esc_attr( $meta->key ); - /* translators: %s: meta data name */ - $this->column_names[ $column_key ] = sprintf( __( 'Meta: %s', 'woocommerce' ), $meta->key ); - $row[ $column_key ] = $meta_value; - $i ++; - } - } - } - } -} diff --git a/includes/gateways/bacs/class-wc-gateway-bacs.php b/includes/gateways/bacs/class-wc-gateway-bacs.php deleted file mode 100644 index 3ec175dfd59..00000000000 --- a/includes/gateways/bacs/class-wc-gateway-bacs.php +++ /dev/null @@ -1,441 +0,0 @@ -id = 'bacs'; - $this->icon = apply_filters( 'woocommerce_bacs_icon', '' ); - $this->has_fields = false; - $this->method_title = __( 'Direct bank transfer', 'woocommerce' ); - $this->method_description = __( 'Take payments in person via BACS. More commonly known as direct bank/wire transfer', 'woocommerce' ); - - // Load the settings. - $this->init_form_fields(); - $this->init_settings(); - - // Define user set variables. - $this->title = $this->get_option( 'title' ); - $this->description = $this->get_option( 'description' ); - $this->instructions = $this->get_option( 'instructions' ); - - // BACS account fields shown on the thanks page and in emails. - $this->account_details = get_option( - 'woocommerce_bacs_accounts', - array( - array( - 'account_name' => $this->get_option( 'account_name' ), - 'account_number' => $this->get_option( 'account_number' ), - 'sort_code' => $this->get_option( 'sort_code' ), - 'bank_name' => $this->get_option( 'bank_name' ), - 'iban' => $this->get_option( 'iban' ), - 'bic' => $this->get_option( 'bic' ), - ), - ) - ); - - // Actions. - add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) ); - add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'save_account_details' ) ); - add_action( 'woocommerce_thankyou_bacs', array( $this, 'thankyou_page' ) ); - - // Customer Emails. - add_action( 'woocommerce_email_before_order_table', array( $this, 'email_instructions' ), 10, 3 ); - } - - /** - * Initialise Gateway Settings Form Fields. - */ - public function init_form_fields() { - - $this->form_fields = array( - 'enabled' => array( - 'title' => __( 'Enable/Disable', 'woocommerce' ), - 'type' => 'checkbox', - 'label' => __( 'Enable bank transfer', 'woocommerce' ), - 'default' => 'no', - ), - 'title' => array( - 'title' => __( 'Title', 'woocommerce' ), - 'type' => 'text', - 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), - 'default' => __( 'Direct bank transfer', 'woocommerce' ), - 'desc_tip' => true, - ), - 'description' => array( - 'title' => __( 'Description', 'woocommerce' ), - 'type' => 'textarea', - 'description' => __( 'Payment method description that the customer will see on your checkout.', 'woocommerce' ), - 'default' => __( 'Make your payment directly into our bank account. Please use your Order ID as the payment reference. Your order will not be shipped until the funds have cleared in our account.', 'woocommerce' ), - 'desc_tip' => true, - ), - 'instructions' => array( - 'title' => __( 'Instructions', 'woocommerce' ), - 'type' => 'textarea', - 'description' => __( 'Instructions that will be added to the thank you page and emails.', 'woocommerce' ), - 'default' => '', - 'desc_tip' => true, - ), - 'account_details' => array( - 'type' => 'account_details', - ), - ); - - } - - /** - * Generate account details html. - * - * @return string - */ - public function generate_account_details_html() { - - ob_start(); - - $country = WC()->countries->get_base_country(); - $locale = $this->get_country_locale(); - - // Get sortcode label in the $locale array and use appropriate one. - $sortcode = isset( $locale[ $country ]['sortcode']['label'] ) ? $locale[ $country ]['sortcode']['label'] : __( 'Sort code', 'woocommerce' ); - - ?> - - - -
    - - - - - - - - - - - - - - account_details ) { - foreach ( $this->account_details as $account ) { - $i++; - - echo ' - - - - - - - - '; - } - } - ?> - - - - - - -
     
    -
    - - - - $name ) { - if ( ! isset( $account_names[ $i ] ) ) { - continue; - } - - $accounts[] = array( - 'account_name' => $account_names[ $i ], - 'account_number' => $account_numbers[ $i ], - 'bank_name' => $bank_names[ $i ], - 'sort_code' => $sort_codes[ $i ], - 'iban' => $ibans[ $i ], - 'bic' => $bics[ $i ], - ); - } - } - // phpcs:enable - - update_option( 'woocommerce_bacs_accounts', $accounts ); - } - - /** - * Output for the order received page. - * - * @param int $order_id Order ID. - */ - public function thankyou_page( $order_id ) { - - if ( $this->instructions ) { - echo wp_kses_post( wpautop( wptexturize( wp_kses_post( $this->instructions ) ) ) ); - } - $this->bank_details( $order_id ); - - } - - /** - * Add content to the WC emails. - * - * @param WC_Order $order Order object. - * @param bool $sent_to_admin Sent to admin. - * @param bool $plain_text Email format: plain text or HTML. - */ - public function email_instructions( $order, $sent_to_admin, $plain_text = false ) { - - if ( ! $sent_to_admin && 'bacs' === $order->get_payment_method() && $order->has_status( 'on-hold' ) ) { - if ( $this->instructions ) { - echo wp_kses_post( wpautop( wptexturize( $this->instructions ) ) . PHP_EOL ); - } - $this->bank_details( $order->get_id() ); - } - - } - - /** - * Get bank details and place into a list format. - * - * @param int $order_id Order ID. - */ - private function bank_details( $order_id = '' ) { - - if ( empty( $this->account_details ) ) { - return; - } - - // Get order and store in $order. - $order = wc_get_order( $order_id ); - - // Get the order country and country $locale. - $country = $order->get_billing_country(); - $locale = $this->get_country_locale(); - - // Get sortcode label in the $locale array and use appropriate one. - $sortcode = isset( $locale[ $country ]['sortcode']['label'] ) ? $locale[ $country ]['sortcode']['label'] : __( 'Sort code', 'woocommerce' ); - - $bacs_accounts = apply_filters( 'woocommerce_bacs_accounts', $this->account_details, $order_id ); - - if ( ! empty( $bacs_accounts ) ) { - $account_html = ''; - $has_details = false; - - foreach ( $bacs_accounts as $bacs_account ) { - $bacs_account = (object) $bacs_account; - - if ( $bacs_account->account_name ) { - $account_html .= '' . PHP_EOL; - } - - $account_html .= '
      ' . PHP_EOL; - - // BACS account fields shown on the thanks page and in emails. - $account_fields = apply_filters( - 'woocommerce_bacs_account_fields', - array( - 'bank_name' => array( - 'label' => __( 'Bank', 'woocommerce' ), - 'value' => $bacs_account->bank_name, - ), - 'account_number' => array( - 'label' => __( 'Account number', 'woocommerce' ), - 'value' => $bacs_account->account_number, - ), - 'sort_code' => array( - 'label' => $sortcode, - 'value' => $bacs_account->sort_code, - ), - 'iban' => array( - 'label' => __( 'IBAN', 'woocommerce' ), - 'value' => $bacs_account->iban, - ), - 'bic' => array( - 'label' => __( 'BIC', 'woocommerce' ), - 'value' => $bacs_account->bic, - ), - ), - $order_id - ); - - foreach ( $account_fields as $field_key => $field ) { - if ( ! empty( $field['value'] ) ) { - $account_html .= '
    • ' . wp_kses_post( $field['label'] ) . ': ' . wp_kses_post( wptexturize( $field['value'] ) ) . '
    • ' . PHP_EOL; - $has_details = true; - } - } - - $account_html .= '
    '; - } - - if ( $has_details ) { - echo '

    ' . esc_html__( 'Our bank details', 'woocommerce' ) . '

    ' . wp_kses_post( PHP_EOL . $account_html ) . '
    '; - } - } - - } - - /** - * Process the payment and return the result. - * - * @param int $order_id Order ID. - * @return array - */ - public function process_payment( $order_id ) { - - $order = wc_get_order( $order_id ); - - if ( $order->get_total() > 0 ) { - // Mark as on-hold (we're awaiting the payment). - $order->update_status( apply_filters( 'woocommerce_bacs_process_payment_order_status', 'on-hold', $order ), __( 'Awaiting BACS payment', 'woocommerce' ) ); - } else { - $order->payment_complete(); - } - - // Remove cart. - WC()->cart->empty_cart(); - - // Return thankyou redirect. - return array( - 'result' => 'success', - 'redirect' => $this->get_return_url( $order ), - ); - - } - - /** - * Get country locale if localized. - * - * @return array - */ - public function get_country_locale() { - - if ( empty( $this->locale ) ) { - - // Locale information to be used - only those that are not 'Sort Code'. - $this->locale = apply_filters( - 'woocommerce_get_bacs_locale', - array( - 'AU' => array( - 'sortcode' => array( - 'label' => __( 'BSB', 'woocommerce' ), - ), - ), - 'CA' => array( - 'sortcode' => array( - 'label' => __( 'Bank transit number', 'woocommerce' ), - ), - ), - 'IN' => array( - 'sortcode' => array( - 'label' => __( 'IFSC', 'woocommerce' ), - ), - ), - 'IT' => array( - 'sortcode' => array( - 'label' => __( 'Branch sort', 'woocommerce' ), - ), - ), - 'NZ' => array( - 'sortcode' => array( - 'label' => __( 'Bank code', 'woocommerce' ), - ), - ), - 'SE' => array( - 'sortcode' => array( - 'label' => __( 'Bank code', 'woocommerce' ), - ), - ), - 'US' => array( - 'sortcode' => array( - 'label' => __( 'Routing number', 'woocommerce' ), - ), - ), - 'ZA' => array( - 'sortcode' => array( - 'label' => __( 'Branch code', 'woocommerce' ), - ), - ), - ) - ); - - } - - return $this->locale; - - } -} diff --git a/includes/gateways/cod/class-wc-gateway-cod.php b/includes/gateways/cod/class-wc-gateway-cod.php deleted file mode 100644 index 33c17643bf0..00000000000 --- a/includes/gateways/cod/class-wc-gateway-cod.php +++ /dev/null @@ -1,378 +0,0 @@ -setup_properties(); - - // Load the settings. - $this->init_form_fields(); - $this->init_settings(); - - // Get settings. - $this->title = $this->get_option( 'title' ); - $this->description = $this->get_option( 'description' ); - $this->instructions = $this->get_option( 'instructions' ); - $this->enable_for_methods = $this->get_option( 'enable_for_methods', array() ); - $this->enable_for_virtual = $this->get_option( 'enable_for_virtual', 'yes' ) === 'yes'; - - add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) ); - add_action( 'woocommerce_thankyou_' . $this->id, array( $this, 'thankyou_page' ) ); - add_filter( 'woocommerce_payment_complete_order_status', array( $this, 'change_payment_complete_order_status' ), 10, 3 ); - - // Customer Emails. - add_action( 'woocommerce_email_before_order_table', array( $this, 'email_instructions' ), 10, 3 ); - } - - /** - * Setup general properties for the gateway. - */ - protected function setup_properties() { - $this->id = 'cod'; - $this->icon = apply_filters( 'woocommerce_cod_icon', '' ); - $this->method_title = __( 'Cash on delivery', 'woocommerce' ); - $this->method_description = __( 'Have your customers pay with cash (or by other means) upon delivery.', 'woocommerce' ); - $this->has_fields = false; - } - - /** - * Initialise Gateway Settings Form Fields. - */ - public function init_form_fields() { - $this->form_fields = array( - 'enabled' => array( - 'title' => __( 'Enable/Disable', 'woocommerce' ), - 'label' => __( 'Enable cash on delivery', 'woocommerce' ), - 'type' => 'checkbox', - 'description' => '', - 'default' => 'no', - ), - 'title' => array( - 'title' => __( 'Title', 'woocommerce' ), - 'type' => 'text', - 'description' => __( 'Payment method description that the customer will see on your checkout.', 'woocommerce' ), - 'default' => __( 'Cash on delivery', 'woocommerce' ), - 'desc_tip' => true, - ), - 'description' => array( - 'title' => __( 'Description', 'woocommerce' ), - 'type' => 'textarea', - 'description' => __( 'Payment method description that the customer will see on your website.', 'woocommerce' ), - 'default' => __( 'Pay with cash upon delivery.', 'woocommerce' ), - 'desc_tip' => true, - ), - 'instructions' => array( - 'title' => __( 'Instructions', 'woocommerce' ), - 'type' => 'textarea', - 'description' => __( 'Instructions that will be added to the thank you page.', 'woocommerce' ), - 'default' => __( 'Pay with cash upon delivery.', 'woocommerce' ), - 'desc_tip' => true, - ), - 'enable_for_methods' => array( - 'title' => __( 'Enable for shipping methods', 'woocommerce' ), - 'type' => 'multiselect', - 'class' => 'wc-enhanced-select', - 'css' => 'width: 400px;', - 'default' => '', - 'description' => __( 'If COD is only available for certain methods, set it up here. Leave blank to enable for all methods.', 'woocommerce' ), - 'options' => $this->load_shipping_method_options(), - 'desc_tip' => true, - 'custom_attributes' => array( - 'data-placeholder' => __( 'Select shipping methods', 'woocommerce' ), - ), - ), - 'enable_for_virtual' => array( - 'title' => __( 'Accept for virtual orders', 'woocommerce' ), - 'label' => __( 'Accept COD if the order is virtual', 'woocommerce' ), - 'type' => 'checkbox', - 'default' => 'yes', - ), - ); - } - - /** - * Check If The Gateway Is Available For Use. - * - * @return bool - */ - public function is_available() { - $order = null; - $needs_shipping = false; - - // Test if shipping is needed first. - if ( WC()->cart && WC()->cart->needs_shipping() ) { - $needs_shipping = true; - } elseif ( is_page( wc_get_page_id( 'checkout' ) ) && 0 < get_query_var( 'order-pay' ) ) { - $order_id = absint( get_query_var( 'order-pay' ) ); - $order = wc_get_order( $order_id ); - - // Test if order needs shipping. - if ( 0 < count( $order->get_items() ) ) { - foreach ( $order->get_items() as $item ) { - $_product = $item->get_product(); - if ( $_product && $_product->needs_shipping() ) { - $needs_shipping = true; - break; - } - } - } - } - - $needs_shipping = apply_filters( 'woocommerce_cart_needs_shipping', $needs_shipping ); - - // Virtual order, with virtual disabled. - if ( ! $this->enable_for_virtual && ! $needs_shipping ) { - return false; - } - - // Only apply if all packages are being shipped via chosen method, or order is virtual. - if ( ! empty( $this->enable_for_methods ) && $needs_shipping ) { - $order_shipping_items = is_object( $order ) ? $order->get_shipping_methods() : false; - $chosen_shipping_methods_session = WC()->session->get( 'chosen_shipping_methods' ); - - if ( $order_shipping_items ) { - $canonical_rate_ids = $this->get_canonical_order_shipping_item_rate_ids( $order_shipping_items ); - } else { - $canonical_rate_ids = $this->get_canonical_package_rate_ids( $chosen_shipping_methods_session ); - } - - if ( ! count( $this->get_matching_rates( $canonical_rate_ids ) ) ) { - return false; - } - } - - return parent::is_available(); - } - - /** - * Checks to see whether or not the admin settings are being accessed by the current request. - * - * @return bool - */ - private function is_accessing_settings() { - if ( is_admin() ) { - // phpcs:disable WordPress.Security.NonceVerification - if ( ! isset( $_REQUEST['page'] ) || 'wc-settings' !== $_REQUEST['page'] ) { - return false; - } - if ( ! isset( $_REQUEST['tab'] ) || 'checkout' !== $_REQUEST['tab'] ) { - return false; - } - if ( ! isset( $_REQUEST['section'] ) || 'cod' !== $_REQUEST['section'] ) { - return false; - } - // phpcs:enable WordPress.Security.NonceVerification - - return true; - } - - if ( Constants::is_true( 'REST_REQUEST' ) ) { - global $wp; - if ( isset( $wp->query_vars['rest_route'] ) && false !== strpos( $wp->query_vars['rest_route'], '/payment_gateways' ) ) { - return true; - } - } - - return false; - } - - /** - * Loads all of the shipping method options for the enable_for_methods field. - * - * @return array - */ - private function load_shipping_method_options() { - // Since this is expensive, we only want to do it if we're actually on the settings page. - if ( ! $this->is_accessing_settings() ) { - return array(); - } - - $data_store = WC_Data_Store::load( 'shipping-zone' ); - $raw_zones = $data_store->get_zones(); - - foreach ( $raw_zones as $raw_zone ) { - $zones[] = new WC_Shipping_Zone( $raw_zone ); - } - - $zones[] = new WC_Shipping_Zone( 0 ); - - $options = array(); - foreach ( WC()->shipping()->load_shipping_methods() as $method ) { - - $options[ $method->get_method_title() ] = array(); - - // Translators: %1$s shipping method name. - $options[ $method->get_method_title() ][ $method->id ] = sprintf( __( 'Any "%1$s" method', 'woocommerce' ), $method->get_method_title() ); - - foreach ( $zones as $zone ) { - - $shipping_method_instances = $zone->get_shipping_methods(); - - foreach ( $shipping_method_instances as $shipping_method_instance_id => $shipping_method_instance ) { - - if ( $shipping_method_instance->id !== $method->id ) { - continue; - } - - $option_id = $shipping_method_instance->get_rate_id(); - - // Translators: %1$s shipping method title, %2$s shipping method id. - $option_instance_title = sprintf( __( '%1$s (#%2$s)', 'woocommerce' ), $shipping_method_instance->get_title(), $shipping_method_instance_id ); - - // Translators: %1$s zone name, %2$s shipping method instance name. - $option_title = sprintf( __( '%1$s – %2$s', 'woocommerce' ), $zone->get_id() ? $zone->get_zone_name() : __( 'Other locations', 'woocommerce' ), $option_instance_title ); - - $options[ $method->get_method_title() ][ $option_id ] = $option_title; - } - } - } - - return $options; - } - - /** - * Converts the chosen rate IDs generated by Shipping Methods to a canonical 'method_id:instance_id' format. - * - * @since 3.4.0 - * - * @param array $order_shipping_items Array of WC_Order_Item_Shipping objects. - * @return array $canonical_rate_ids Rate IDs in a canonical format. - */ - private function get_canonical_order_shipping_item_rate_ids( $order_shipping_items ) { - - $canonical_rate_ids = array(); - - foreach ( $order_shipping_items as $order_shipping_item ) { - $canonical_rate_ids[] = $order_shipping_item->get_method_id() . ':' . $order_shipping_item->get_instance_id(); - } - - return $canonical_rate_ids; - } - - /** - * Converts the chosen rate IDs generated by Shipping Methods to a canonical 'method_id:instance_id' format. - * - * @since 3.4.0 - * - * @param array $chosen_package_rate_ids Rate IDs as generated by shipping methods. Can be anything if a shipping method doesn't honor WC conventions. - * @return array $canonical_rate_ids Rate IDs in a canonical format. - */ - private function get_canonical_package_rate_ids( $chosen_package_rate_ids ) { - - $shipping_packages = WC()->shipping()->get_packages(); - $canonical_rate_ids = array(); - - if ( ! empty( $chosen_package_rate_ids ) && is_array( $chosen_package_rate_ids ) ) { - foreach ( $chosen_package_rate_ids as $package_key => $chosen_package_rate_id ) { - if ( ! empty( $shipping_packages[ $package_key ]['rates'][ $chosen_package_rate_id ] ) ) { - $chosen_rate = $shipping_packages[ $package_key ]['rates'][ $chosen_package_rate_id ]; - $canonical_rate_ids[] = $chosen_rate->get_method_id() . ':' . $chosen_rate->get_instance_id(); - } - } - } - - return $canonical_rate_ids; - } - - /** - * Indicates whether a rate exists in an array of canonically-formatted rate IDs that activates this gateway. - * - * @since 3.4.0 - * - * @param array $rate_ids Rate ids to check. - * @return boolean - */ - private function get_matching_rates( $rate_ids ) { - // First, match entries in 'method_id:instance_id' format. Then, match entries in 'method_id' format by stripping off the instance ID from the candidates. - return array_unique( array_merge( array_intersect( $this->enable_for_methods, $rate_ids ), array_intersect( $this->enable_for_methods, array_unique( array_map( 'wc_get_string_before_colon', $rate_ids ) ) ) ) ); - } - - /** - * Process the payment and return the result. - * - * @param int $order_id Order ID. - * @return array - */ - public function process_payment( $order_id ) { - $order = wc_get_order( $order_id ); - - if ( $order->get_total() > 0 ) { - // Mark as processing or on-hold (payment won't be taken until delivery). - $order->update_status( apply_filters( 'woocommerce_cod_process_payment_order_status', $order->has_downloadable_item() ? 'on-hold' : 'processing', $order ), __( 'Payment to be made upon delivery.', 'woocommerce' ) ); - } else { - $order->payment_complete(); - } - - // Remove cart. - WC()->cart->empty_cart(); - - // Return thankyou redirect. - return array( - 'result' => 'success', - 'redirect' => $this->get_return_url( $order ), - ); - } - - /** - * Output for the order received page. - */ - public function thankyou_page() { - if ( $this->instructions ) { - echo wp_kses_post( wpautop( wptexturize( $this->instructions ) ) ); - } - } - - /** - * Change payment complete order status to completed for COD orders. - * - * @since 3.1.0 - * @param string $status Current order status. - * @param int $order_id Order ID. - * @param WC_Order|false $order Order object. - * @return string - */ - public function change_payment_complete_order_status( $status, $order_id = 0, $order = false ) { - if ( $order && 'cod' === $order->get_payment_method() ) { - $status = 'completed'; - } - return $status; - } - - /** - * Add content to the WC emails. - * - * @param WC_Order $order Order object. - * @param bool $sent_to_admin Sent to admin. - * @param bool $plain_text Email format: plain text or HTML. - */ - public function email_instructions( $order, $sent_to_admin, $plain_text = false ) { - if ( $this->instructions && ! $sent_to_admin && $this->id === $order->get_payment_method() ) { - echo wp_kses_post( wpautop( wptexturize( $this->instructions ) ) . PHP_EOL ); - } - } -} diff --git a/includes/gateways/paypal/assets/js/paypal-admin.js b/includes/gateways/paypal/assets/js/paypal-admin.js deleted file mode 100644 index 705d0aa193a..00000000000 --- a/includes/gateways/paypal/assets/js/paypal-admin.js +++ /dev/null @@ -1,46 +0,0 @@ -jQuery( function( $ ) { - 'use strict'; - - /** - * Object to handle PayPal admin functions. - */ - var wc_paypal_admin = { - isTestMode: function() { - return $( '#woocommerce_paypal_testmode' ).is( ':checked' ); - }, - - /** - * Initialize. - */ - init: function() { - $( document.body ).on( 'change', '#woocommerce_paypal_testmode', function() { - var test_api_username = $( '#woocommerce_paypal_sandbox_api_username' ).parents( 'tr' ).eq( 0 ), - test_api_password = $( '#woocommerce_paypal_sandbox_api_password' ).parents( 'tr' ).eq( 0 ), - test_api_signature = $( '#woocommerce_paypal_sandbox_api_signature' ).parents( 'tr' ).eq( 0 ), - live_api_username = $( '#woocommerce_paypal_api_username' ).parents( 'tr' ).eq( 0 ), - live_api_password = $( '#woocommerce_paypal_api_password' ).parents( 'tr' ).eq( 0 ), - live_api_signature = $( '#woocommerce_paypal_api_signature' ).parents( 'tr' ).eq( 0 ); - - if ( $( this ).is( ':checked' ) ) { - test_api_username.show(); - test_api_password.show(); - test_api_signature.show(); - live_api_username.hide(); - live_api_password.hide(); - live_api_signature.hide(); - } else { - test_api_username.hide(); - test_api_password.hide(); - test_api_signature.hide(); - live_api_username.show(); - live_api_password.show(); - live_api_signature.show(); - } - } ); - - $( '#woocommerce_paypal_testmode' ).change(); - } - }; - - wc_paypal_admin.init(); -}); diff --git a/includes/gateways/paypal/assets/js/paypal-admin.min.js b/includes/gateways/paypal/assets/js/paypal-admin.min.js deleted file mode 100644 index 24ad8028587..00000000000 --- a/includes/gateways/paypal/assets/js/paypal-admin.min.js +++ /dev/null @@ -1 +0,0 @@ -jQuery(function($){'use strict';var wc_paypal_admin={isTestMode:function(){return $('#woocommerce_paypal_testmode').is(':checked')},init:function(){$(document.body).on('change','#woocommerce_paypal_testmode',function(){var test_api_username=$('#woocommerce_paypal_sandbox_api_username').parents('tr').eq(0),test_api_password=$('#woocommerce_paypal_sandbox_api_password').parents('tr').eq(0),test_api_signature=$('#woocommerce_paypal_sandbox_api_signature').parents('tr').eq(0),live_api_username=$('#woocommerce_paypal_api_username').parents('tr').eq(0),live_api_password=$('#woocommerce_paypal_api_password').parents('tr').eq(0),live_api_signature=$('#woocommerce_paypal_api_signature').parents('tr').eq(0);if($(this).is(':checked')){test_api_username.show();test_api_password.show();test_api_signature.show();live_api_username.hide();live_api_password.hide();live_api_signature.hide()}else{test_api_username.hide();test_api_password.hide();test_api_signature.hide();live_api_username.show();live_api_password.show();live_api_signature.show()}});$('#woocommerce_paypal_testmode').change()}};wc_paypal_admin.init()}) \ No newline at end of file diff --git a/includes/gateways/paypal/class-wc-gateway-paypal.php b/includes/gateways/paypal/class-wc-gateway-paypal.php deleted file mode 100644 index 68a8095ad22..00000000000 --- a/includes/gateways/paypal/class-wc-gateway-paypal.php +++ /dev/null @@ -1,476 +0,0 @@ -id = 'paypal'; - $this->has_fields = false; - $this->order_button_text = __( 'Proceed to PayPal', 'woocommerce' ); - $this->method_title = __( 'PayPal Standard', 'woocommerce' ); - /* translators: %s: Link to WC system status page */ - $this->method_description = __( 'PayPal Standard redirects customers to PayPal to enter their payment information.', 'woocommerce' ); - $this->supports = array( - 'products', - 'refunds', - ); - - // Load the settings. - $this->init_form_fields(); - $this->init_settings(); - - // Define user set variables. - $this->title = $this->get_option( 'title' ); - $this->description = $this->get_option( 'description' ); - $this->testmode = 'yes' === $this->get_option( 'testmode', 'no' ); - $this->debug = 'yes' === $this->get_option( 'debug', 'no' ); - $this->email = $this->get_option( 'email' ); - $this->receiver_email = $this->get_option( 'receiver_email', $this->email ); - $this->identity_token = $this->get_option( 'identity_token' ); - self::$log_enabled = $this->debug; - - if ( $this->testmode ) { - /* translators: %s: Link to PayPal sandbox testing guide page */ - $this->description .= ' ' . sprintf( __( 'SANDBOX ENABLED. You can use sandbox testing accounts only. See the PayPal Sandbox Testing Guide for more details.', 'woocommerce' ), 'https://developer.paypal.com/docs/classic/lifecycle/ug_sandbox/' ); - $this->description = trim( $this->description ); - } - - add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) ); - add_action( 'woocommerce_order_status_processing', array( $this, 'capture_payment' ) ); - add_action( 'woocommerce_order_status_completed', array( $this, 'capture_payment' ) ); - add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) ); - - if ( ! $this->is_valid_for_use() ) { - $this->enabled = 'no'; - } else { - include_once dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-ipn-handler.php'; - new WC_Gateway_Paypal_IPN_Handler( $this->testmode, $this->receiver_email ); - - if ( $this->identity_token ) { - include_once dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-pdt-handler.php'; - new WC_Gateway_Paypal_PDT_Handler( $this->testmode, $this->identity_token ); - } - } - - if ( 'yes' === $this->enabled ) { - add_filter( 'woocommerce_thankyou_order_received_text', array( $this, 'order_received_text' ), 10, 2 ); - } - } - - /** - * Return whether or not this gateway still requires setup to function. - * - * When this gateway is toggled on via AJAX, if this returns true a - * redirect will occur to the settings page instead. - * - * @since 3.4.0 - * @return bool - */ - public function needs_setup() { - return ! is_email( $this->email ); - } - - /** - * Logging method. - * - * @param string $message Log message. - * @param string $level Optional. Default 'info'. Possible values: - * emergency|alert|critical|error|warning|notice|info|debug. - */ - public static function log( $message, $level = 'info' ) { - if ( self::$log_enabled ) { - if ( empty( self::$log ) ) { - self::$log = wc_get_logger(); - } - self::$log->log( $level, $message, array( 'source' => 'paypal' ) ); - } - } - - /** - * Processes and saves options. - * If there is an error thrown, will continue to save and validate fields, but will leave the erroring field out. - * - * @return bool was anything saved? - */ - public function process_admin_options() { - $saved = parent::process_admin_options(); - - // Maybe clear logs. - if ( 'yes' !== $this->get_option( 'debug', 'no' ) ) { - if ( empty( self::$log ) ) { - self::$log = wc_get_logger(); - } - self::$log->clear( 'paypal' ); - } - - return $saved; - } - - /** - * Get gateway icon. - * - * @return string - */ - public function get_icon() { - // We need a base country for the link to work, bail if in the unlikely event no country is set. - $base_country = WC()->countries->get_base_country(); - if ( empty( $base_country ) ) { - return ''; - } - $icon_html = ''; - $icon = (array) $this->get_icon_image( $base_country ); - - foreach ( $icon as $i ) { - $icon_html .= '' . esc_attr__( 'PayPal acceptance mark', 'woocommerce' ) . ''; - } - - $icon_html .= sprintf( '' . esc_attr__( 'What is PayPal?', 'woocommerce' ) . '', esc_url( $this->get_icon_url( $base_country ) ) ); - - return apply_filters( 'woocommerce_gateway_icon', $icon_html, $this->id ); - } - - /** - * Get the link for an icon based on country. - * - * @param string $country Country two letter code. - * @return string - */ - protected function get_icon_url( $country ) { - $url = 'https://www.paypal.com/' . strtolower( $country ); - $home_counties = array( 'BE', 'CZ', 'DK', 'HU', 'IT', 'JP', 'NL', 'NO', 'ES', 'SE', 'TR', 'IN' ); - $countries = array( 'DZ', 'AU', 'BH', 'BQ', 'BW', 'CA', 'CN', 'CW', 'FI', 'FR', 'DE', 'GR', 'HK', 'ID', 'JO', 'KE', 'KW', 'LU', 'MY', 'MA', 'OM', 'PH', 'PL', 'PT', 'QA', 'IE', 'RU', 'BL', 'SX', 'MF', 'SA', 'SG', 'SK', 'KR', 'SS', 'TW', 'TH', 'AE', 'GB', 'US', 'VN' ); - - if ( in_array( $country, $home_counties, true ) ) { - return $url . '/webapps/mpp/home'; - } elseif ( in_array( $country, $countries, true ) ) { - return $url . '/webapps/mpp/paypal-popup'; - } else { - return $url . '/cgi-bin/webscr?cmd=xpt/Marketing/general/WIPaypal-outside'; - } - } - - /** - * Get PayPal images for a country. - * - * @param string $country Country code. - * @return array of image URLs - */ - protected function get_icon_image( $country ) { - switch ( $country ) { - case 'US': - case 'NZ': - case 'CZ': - case 'HU': - case 'MY': - $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo/AM_mc_vs_dc_ae.jpg'; - break; - case 'TR': - $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/logo_paypal_odeme_secenekleri.jpg'; - break; - case 'GB': - $icon = 'https://www.paypalobjects.com/webstatic/mktg/Logo/AM_mc_vs_ms_ae_UK.png'; - break; - case 'MX': - $icon = array( - 'https://www.paypal.com/es_XC/Marketing/i/banner/paypal_visa_mastercard_amex.png', - 'https://www.paypal.com/es_XC/Marketing/i/banner/paypal_debit_card_275x60.gif', - ); - break; - case 'FR': - $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/logo_paypal_moyens_paiement_fr.jpg'; - break; - case 'AU': - $icon = 'https://www.paypalobjects.com/webstatic/en_AU/mktg/logo/Solutions-graphics-1-184x80.jpg'; - break; - case 'DK': - $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/logo_PayPal_betalingsmuligheder_dk.jpg'; - break; - case 'RU': - $icon = 'https://www.paypalobjects.com/webstatic/ru_RU/mktg/business/pages/logo-center/AM_mc_vs_dc_ae.jpg'; - break; - case 'NO': - $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/banner_pl_just_pp_319x110.jpg'; - break; - case 'CA': - $icon = 'https://www.paypalobjects.com/webstatic/en_CA/mktg/logo-image/AM_mc_vs_dc_ae.jpg'; - break; - case 'HK': - $icon = 'https://www.paypalobjects.com/webstatic/en_HK/mktg/logo/AM_mc_vs_dc_ae.jpg'; - break; - case 'SG': - $icon = 'https://www.paypalobjects.com/webstatic/en_SG/mktg/Logos/AM_mc_vs_dc_ae.jpg'; - break; - case 'TW': - $icon = 'https://www.paypalobjects.com/webstatic/en_TW/mktg/logos/AM_mc_vs_dc_ae.jpg'; - break; - case 'TH': - $icon = 'https://www.paypalobjects.com/webstatic/en_TH/mktg/Logos/AM_mc_vs_dc_ae.jpg'; - break; - case 'JP': - $icon = 'https://www.paypal.com/ja_JP/JP/i/bnr/horizontal_solution_4_jcb.gif'; - break; - case 'IN': - $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo/AM_mc_vs_dc_ae.jpg'; - break; - default: - $icon = WC_HTTPS::force_https_url( WC()->plugin_url() . '/includes/gateways/paypal/assets/images/paypal.png' ); - break; - } - return apply_filters( 'woocommerce_paypal_icon', $icon ); - } - - /** - * Check if this gateway is available in the user's country based on currency. - * - * @return bool - */ - public function is_valid_for_use() { - return in_array( - get_woocommerce_currency(), - apply_filters( - 'woocommerce_paypal_supported_currencies', - array( 'AUD', 'BRL', 'CAD', 'MXN', 'NZD', 'HKD', 'SGD', 'USD', 'EUR', 'JPY', 'TRY', 'NOK', 'CZK', 'DKK', 'HUF', 'ILS', 'MYR', 'PHP', 'PLN', 'SEK', 'CHF', 'TWD', 'THB', 'GBP', 'RMB', 'RUB', 'INR' ) - ), - true - ); - } - - /** - * Admin Panel Options. - * - Options for bits like 'title' and availability on a country-by-country basis. - * - * @since 1.0.0 - */ - public function admin_options() { - if ( $this->is_valid_for_use() ) { - parent::admin_options(); - } else { - ?> -
    -

    - : -

    -
    - form_fields = include __DIR__ . '/includes/settings-paypal.php'; - } - - /** - * Get the transaction URL. - * - * @param WC_Order $order Order object. - * @return string - */ - public function get_transaction_url( $order ) { - if ( $this->testmode ) { - $this->view_transaction_url = 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s'; - } else { - $this->view_transaction_url = 'https://www.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s'; - } - return parent::get_transaction_url( $order ); - } - - /** - * Process the payment and return the result. - * - * @param int $order_id Order ID. - * @return array - */ - public function process_payment( $order_id ) { - include_once dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-request.php'; - - $order = wc_get_order( $order_id ); - $paypal_request = new WC_Gateway_Paypal_Request( $this ); - - return array( - 'result' => 'success', - 'redirect' => $paypal_request->get_request_url( $order, $this->testmode ), - ); - } - - /** - * Can the order be refunded via PayPal? - * - * @param WC_Order $order Order object. - * @return bool - */ - public function can_refund_order( $order ) { - $has_api_creds = false; - - if ( $this->testmode ) { - $has_api_creds = $this->get_option( 'sandbox_api_username' ) && $this->get_option( 'sandbox_api_password' ) && $this->get_option( 'sandbox_api_signature' ); - } else { - $has_api_creds = $this->get_option( 'api_username' ) && $this->get_option( 'api_password' ) && $this->get_option( 'api_signature' ); - } - - return $order && $order->get_transaction_id() && $has_api_creds; - } - - /** - * Init the API class and set the username/password etc. - */ - protected function init_api() { - include_once dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-api-handler.php'; - - WC_Gateway_Paypal_API_Handler::$api_username = $this->testmode ? $this->get_option( 'sandbox_api_username' ) : $this->get_option( 'api_username' ); - WC_Gateway_Paypal_API_Handler::$api_password = $this->testmode ? $this->get_option( 'sandbox_api_password' ) : $this->get_option( 'api_password' ); - WC_Gateway_Paypal_API_Handler::$api_signature = $this->testmode ? $this->get_option( 'sandbox_api_signature' ) : $this->get_option( 'api_signature' ); - WC_Gateway_Paypal_API_Handler::$sandbox = $this->testmode; - } - - /** - * Process a refund if supported. - * - * @param int $order_id Order ID. - * @param float $amount Refund amount. - * @param string $reason Refund reason. - * @return bool|WP_Error - */ - public function process_refund( $order_id, $amount = null, $reason = '' ) { - $order = wc_get_order( $order_id ); - - if ( ! $this->can_refund_order( $order ) ) { - return new WP_Error( 'error', __( 'Refund failed.', 'woocommerce' ) ); - } - - $this->init_api(); - - $result = WC_Gateway_Paypal_API_Handler::refund_transaction( $order, $amount, $reason ); - - if ( is_wp_error( $result ) ) { - $this->log( 'Refund Failed: ' . $result->get_error_message(), 'error' ); - return new WP_Error( 'error', $result->get_error_message() ); - } - - $this->log( 'Refund Result: ' . wc_print_r( $result, true ) ); - - switch ( strtolower( $result->ACK ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - case 'success': - case 'successwithwarning': - $order->add_order_note( - /* translators: 1: Refund amount, 2: Refund ID */ - sprintf( __( 'Refunded %1$s - Refund ID: %2$s', 'woocommerce' ), $result->GROSSREFUNDAMT, $result->REFUNDTRANSACTIONID ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - ); - return true; - } - - return isset( $result->L_LONGMESSAGE0 ) ? new WP_Error( 'error', $result->L_LONGMESSAGE0 ) : false; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - } - - /** - * Capture payment when the order is changed from on-hold to complete or processing - * - * @param int $order_id Order ID. - */ - public function capture_payment( $order_id ) { - $order = wc_get_order( $order_id ); - - if ( 'paypal' === $order->get_payment_method() && 'pending' === $order->get_meta( '_paypal_status', true ) && $order->get_transaction_id() ) { - $this->init_api(); - $result = WC_Gateway_Paypal_API_Handler::do_capture( $order ); - - if ( is_wp_error( $result ) ) { - $this->log( 'Capture Failed: ' . $result->get_error_message(), 'error' ); - /* translators: %s: Paypal gateway error message */ - $order->add_order_note( sprintf( __( 'Payment could not be captured: %s', 'woocommerce' ), $result->get_error_message() ) ); - return; - } - - $this->log( 'Capture Result: ' . wc_print_r( $result, true ) ); - - // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - if ( ! empty( $result->PAYMENTSTATUS ) ) { - switch ( $result->PAYMENTSTATUS ) { - case 'Completed': - /* translators: 1: Amount, 2: Authorization ID, 3: Transaction ID */ - $order->add_order_note( sprintf( __( 'Payment of %1$s was captured - Auth ID: %2$s, Transaction ID: %3$s', 'woocommerce' ), $result->AMT, $result->AUTHORIZATIONID, $result->TRANSACTIONID ) ); - update_post_meta( $order->get_id(), '_paypal_status', $result->PAYMENTSTATUS ); - update_post_meta( $order->get_id(), '_transaction_id', $result->TRANSACTIONID ); - break; - default: - /* translators: 1: Authorization ID, 2: Payment status */ - $order->add_order_note( sprintf( __( 'Payment could not be captured - Auth ID: %1$s, Status: %2$s', 'woocommerce' ), $result->AUTHORIZATIONID, $result->PAYMENTSTATUS ) ); - break; - } - } - // phpcs:enable - } - } - - /** - * Load admin scripts. - * - * @since 3.3.0 - */ - public function admin_scripts() { - $screen = get_current_screen(); - $screen_id = $screen ? $screen->id : ''; - - if ( 'woocommerce_page_wc-settings' !== $screen_id ) { - return; - } - - $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; - $version = Constants::get_constant( 'WC_VERSION' ); - - wp_enqueue_script( 'woocommerce_paypal_admin', WC()->plugin_url() . '/includes/gateways/paypal/assets/js/paypal-admin' . $suffix . '.js', array(), $version, true ); - } - - /** - * Custom PayPal order received text. - * - * @since 3.9.0 - * @param string $text Default text. - * @param WC_Order $order Order data. - * @return string - */ - public function order_received_text( $text, $order ) { - if ( $order && $this->id === $order->get_payment_method() ) { - return esc_html__( 'Thank you for your payment. Your transaction has been completed, and a receipt for your purchase has been emailed to you. Log into your PayPal account to view transaction details.', 'woocommerce' ); - } - - return $text; - } -} diff --git a/includes/gateways/paypal/includes/class-wc-gateway-paypal-pdt-handler.php b/includes/gateways/paypal/includes/class-wc-gateway-paypal-pdt-handler.php deleted file mode 100644 index c37d4d1462e..00000000000 --- a/includes/gateways/paypal/includes/class-wc-gateway-paypal-pdt-handler.php +++ /dev/null @@ -1,138 +0,0 @@ -identity_token = $identity_token; - $this->sandbox = $sandbox; - } - - /** - * Validate a PDT transaction to ensure its authentic. - * - * @param string $transaction TX ID. - * @return bool|array False or result array if successful and valid. - */ - protected function validate_transaction( $transaction ) { - $pdt = array( - 'body' => array( - 'cmd' => '_notify-synch', - 'tx' => $transaction, - 'at' => $this->identity_token, - ), - 'timeout' => 60, - 'httpversion' => '1.1', - 'user-agent' => 'WooCommerce/' . Constants::get_constant( 'WC_VERSION' ), - ); - - // Post back to get a response. - $response = wp_safe_remote_post( $this->sandbox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr', $pdt ); - - if ( is_wp_error( $response ) || strpos( $response['body'], 'SUCCESS' ) !== 0 ) { - return false; - } - - // Parse transaction result data. - $transaction_result = array_map( 'wc_clean', array_map( 'urldecode', explode( "\n", $response['body'] ) ) ); - $transaction_results = array(); - - foreach ( $transaction_result as $line ) { - $line = explode( '=', $line ); - $transaction_results[ $line[0] ] = isset( $line[1] ) ? $line[1] : ''; - } - - if ( ! empty( $transaction_results['charset'] ) && function_exists( 'iconv' ) ) { - foreach ( $transaction_results as $key => $value ) { - $transaction_results[ $key ] = iconv( $transaction_results['charset'], 'utf-8', $value ); - } - } - - return $transaction_results; - } - - /** - * Check Response for PDT. - */ - public function check_response() { - if ( empty( $_REQUEST['cm'] ) || empty( $_REQUEST['tx'] ) || empty( $_REQUEST['st'] ) ) { // WPCS: Input var ok, CSRF ok, sanitization ok. - return; - } - - $order_id = wc_clean( wp_unslash( $_REQUEST['cm'] ) ); // WPCS: input var ok, CSRF ok, sanitization ok. - $status = wc_clean( strtolower( wp_unslash( $_REQUEST['st'] ) ) ); // WPCS: input var ok, CSRF ok, sanitization ok. - $amount = isset( $_REQUEST['amt'] ) ? wc_clean( wp_unslash( $_REQUEST['amt'] ) ) : 0; // WPCS: input var ok, CSRF ok, sanitization ok. - $transaction = wc_clean( wp_unslash( $_REQUEST['tx'] ) ); // WPCS: input var ok, CSRF ok, sanitization ok. - $order = $this->get_paypal_order( $order_id ); - - if ( ! $order || ! $order->needs_payment() ) { - return false; - } - - $transaction_result = $this->validate_transaction( $transaction ); - - if ( $transaction_result ) { - WC_Gateway_Paypal::log( 'PDT Transaction Status: ' . wc_print_r( $status, true ) ); - - $order->add_meta_data( '_paypal_status', $status ); - $order->set_transaction_id( $transaction ); - - if ( 'completed' === $status ) { - if ( number_format( $order->get_total(), 2, '.', '' ) !== number_format( $amount, 2, '.', '' ) ) { - WC_Gateway_Paypal::log( 'Payment error: Amounts do not match (amt ' . $amount . ')', 'error' ); - /* translators: 1: Payment amount */ - $this->payment_on_hold( $order, sprintf( __( 'Validation error: PayPal amounts do not match (amt %s).', 'woocommerce' ), $amount ) ); - } else { - // Log paypal transaction fee and payment type. - if ( ! empty( $transaction_result['mc_fee'] ) ) { - $order->add_meta_data( 'PayPal Transaction Fee', wc_clean( $transaction_result['mc_fee'] ) ); - } - if ( ! empty( $transaction_result['payment_type'] ) ) { - $order->add_meta_data( 'Payment type', wc_clean( $transaction_result['payment_type'] ) ); - } - - $this->payment_complete( $order, $transaction, __( 'PDT payment completed', 'woocommerce' ) ); - } - } else { - if ( 'authorization' === $transaction_result['pending_reason'] ) { - $this->payment_on_hold( $order, __( 'Payment authorized. Change payment status to processing or complete to capture funds.', 'woocommerce' ) ); - } else { - /* translators: 1: Pending reason */ - $this->payment_on_hold( $order, sprintf( __( 'Payment pending (%s).', 'woocommerce' ), $transaction_result['pending_reason'] ) ); - } - } - } else { - WC_Gateway_Paypal::log( 'Received invalid response from PayPal PDT' ); - } - } -} diff --git a/includes/gateways/paypal/includes/settings-paypal.php b/includes/gateways/paypal/includes/settings-paypal.php deleted file mode 100644 index d9d56ddaadf..00000000000 --- a/includes/gateways/paypal/includes/settings-paypal.php +++ /dev/null @@ -1,178 +0,0 @@ - array( - 'title' => __( 'Enable/Disable', 'woocommerce' ), - 'type' => 'checkbox', - 'label' => __( 'Enable PayPal Standard', 'woocommerce' ), - 'default' => 'no', - ), - 'title' => array( - 'title' => __( 'Title', 'woocommerce' ), - 'type' => 'text', - 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), - 'default' => __( 'PayPal', 'woocommerce' ), - 'desc_tip' => true, - ), - 'description' => array( - 'title' => __( 'Description', 'woocommerce' ), - 'type' => 'text', - 'desc_tip' => true, - 'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce' ), - 'default' => __( "Pay via PayPal; you can pay with your credit card if you don't have a PayPal account.", 'woocommerce' ), - ), - 'email' => array( - 'title' => __( 'PayPal email', 'woocommerce' ), - 'type' => 'email', - 'description' => __( 'Please enter your PayPal email address; this is needed in order to take payment.', 'woocommerce' ), - 'default' => get_option( 'admin_email' ), - 'desc_tip' => true, - 'placeholder' => 'you@youremail.com', - ), - 'advanced' => array( - 'title' => __( 'Advanced options', 'woocommerce' ), - 'type' => 'title', - 'description' => '', - ), - 'testmode' => array( - 'title' => __( 'PayPal sandbox', 'woocommerce' ), - 'type' => 'checkbox', - 'label' => __( 'Enable PayPal sandbox', 'woocommerce' ), - 'default' => 'no', - /* translators: %s: URL */ - 'description' => sprintf( __( 'PayPal sandbox can be used to test payments. Sign up for a developer account.', 'woocommerce' ), 'https://developer.paypal.com/' ), - ), - 'debug' => array( - 'title' => __( 'Debug log', 'woocommerce' ), - 'type' => 'checkbox', - 'label' => __( 'Enable logging', 'woocommerce' ), - 'default' => 'no', - /* translators: %s: URL */ - 'description' => sprintf( __( 'Log PayPal events, such as IPN requests, inside %s Note: this may log personal information. We recommend using this for debugging purposes only and deleting the logs when finished.', 'woocommerce' ), '' . WC_Log_Handler_File::get_log_file_path( 'paypal' ) . '' ), - ), - 'ipn_notification' => array( - 'title' => __( 'IPN Email Notifications', 'woocommerce' ), - 'type' => 'checkbox', - 'label' => __( 'Enable IPN email notifications', 'woocommerce' ), - 'default' => 'yes', - 'description' => __( 'Send notifications when an IPN is received from PayPal indicating refunds, chargebacks and cancellations.', 'woocommerce' ), - ), - 'receiver_email' => array( - 'title' => __( 'Receiver email', 'woocommerce' ), - 'type' => 'email', - 'description' => __( 'If your main PayPal email differs from the PayPal email entered above, input your main receiver email for your PayPal account here. This is used to validate IPN requests.', 'woocommerce' ), - 'default' => '', - 'desc_tip' => true, - 'placeholder' => 'you@youremail.com', - ), - 'identity_token' => array( - 'title' => __( 'PayPal identity token', 'woocommerce' ), - 'type' => 'text', - 'description' => __( 'Optionally enable "Payment Data Transfer" (Profile > Profile and Settings > My Selling Tools > Website Preferences) and then copy your identity token here. This will allow payments to be verified without the need for PayPal IPN.', 'woocommerce' ), - 'default' => '', - 'desc_tip' => true, - 'placeholder' => '', - ), - 'invoice_prefix' => array( - 'title' => __( 'Invoice prefix', 'woocommerce' ), - 'type' => 'text', - 'description' => __( 'Please enter a prefix for your invoice numbers. If you use your PayPal account for multiple stores ensure this prefix is unique as PayPal will not allow orders with the same invoice number.', 'woocommerce' ), - 'default' => 'WC-', - 'desc_tip' => true, - ), - 'send_shipping' => array( - 'title' => __( 'Shipping details', 'woocommerce' ), - 'type' => 'checkbox', - 'label' => __( 'Send shipping details to PayPal instead of billing.', 'woocommerce' ), - 'description' => __( 'PayPal allows us to send one address. If you are using PayPal for shipping labels you may prefer to send the shipping address rather than billing. Turning this option off may prevent PayPal Seller protection from applying.', 'woocommerce' ), - 'default' => 'yes', - ), - 'address_override' => array( - 'title' => __( 'Address override', 'woocommerce' ), - 'type' => 'checkbox', - 'label' => __( 'Enable "address_override" to prevent address information from being changed.', 'woocommerce' ), - 'description' => __( 'PayPal verifies addresses therefore this setting can cause errors (we recommend keeping it disabled).', 'woocommerce' ), - 'default' => 'no', - ), - 'paymentaction' => array( - 'title' => __( 'Payment action', 'woocommerce' ), - 'type' => 'select', - 'class' => 'wc-enhanced-select', - 'description' => __( 'Choose whether you wish to capture funds immediately or authorize payment only.', 'woocommerce' ), - 'default' => 'sale', - 'desc_tip' => true, - 'options' => array( - 'sale' => __( 'Capture', 'woocommerce' ), - 'authorization' => __( 'Authorize', 'woocommerce' ), - ), - ), - 'image_url' => array( - 'title' => __( 'Image url', 'woocommerce' ), - 'type' => 'text', - 'description' => __( 'Optionally enter the URL to a 150x50px image displayed as your logo in the upper left corner of the PayPal checkout pages.', 'woocommerce' ), - 'default' => '', - 'desc_tip' => true, - 'placeholder' => __( 'Optional', 'woocommerce' ), - ), - 'api_details' => array( - 'title' => __( 'API credentials', 'woocommerce' ), - 'type' => 'title', - /* translators: %s: URL */ - 'description' => sprintf( __( 'Enter your PayPal API credentials to process refunds via PayPal. Learn how to access your PayPal API Credentials.', 'woocommerce' ), 'https://developer.paypal.com/webapps/developer/docs/classic/api/apiCredentials/#create-an-api-signature' ), - ), - 'api_username' => array( - 'title' => __( 'Live API username', 'woocommerce' ), - 'type' => 'text', - 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), - 'default' => '', - 'desc_tip' => true, - 'placeholder' => __( 'Optional', 'woocommerce' ), - ), - 'api_password' => array( - 'title' => __( 'Live API password', 'woocommerce' ), - 'type' => 'password', - 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), - 'default' => '', - 'desc_tip' => true, - 'placeholder' => __( 'Optional', 'woocommerce' ), - ), - 'api_signature' => array( - 'title' => __( 'Live API signature', 'woocommerce' ), - 'type' => 'password', - 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), - 'default' => '', - 'desc_tip' => true, - 'placeholder' => __( 'Optional', 'woocommerce' ), - ), - 'sandbox_api_username' => array( - 'title' => __( 'Sandbox API username', 'woocommerce' ), - 'type' => 'text', - 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), - 'default' => '', - 'desc_tip' => true, - 'placeholder' => __( 'Optional', 'woocommerce' ), - ), - 'sandbox_api_password' => array( - 'title' => __( 'Sandbox API password', 'woocommerce' ), - 'type' => 'password', - 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), - 'default' => '', - 'desc_tip' => true, - 'placeholder' => __( 'Optional', 'woocommerce' ), - ), - 'sandbox_api_signature' => array( - 'title' => __( 'Sandbox API signature', 'woocommerce' ), - 'type' => 'password', - 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), - 'default' => '', - 'desc_tip' => true, - 'placeholder' => __( 'Optional', 'woocommerce' ), - ), -); diff --git a/includes/import/class-wc-product-csv-importer.php b/includes/import/class-wc-product-csv-importer.php deleted file mode 100644 index c9c11f44684..00000000000 --- a/includes/import/class-wc-product-csv-importer.php +++ /dev/null @@ -1,1128 +0,0 @@ - 0, // File pointer start. - 'end_pos' => -1, // File pointer end. - 'lines' => -1, // Max lines to read. - 'mapping' => array(), // Column mapping. csv_heading => schema_heading. - 'parse' => false, // Whether to sanitize and format data. - 'update_existing' => false, // Whether to update existing items. - 'delimiter' => ',', // CSV delimiter. - 'prevent_timeouts' => true, // Check memory and time usage and abort if reaching limit. - 'enclosure' => '"', // The character used to wrap text in the CSV. - 'escape' => "\0", // PHP uses '\' as the default escape character. This is not RFC-4180 compliant. This disables the escape character. - ); - - $this->params = wp_parse_args( $params, $default_args ); - $this->file = $file; - - if ( isset( $this->params['mapping']['from'], $this->params['mapping']['to'] ) ) { - $this->params['mapping'] = array_combine( $this->params['mapping']['from'], $this->params['mapping']['to'] ); - } - - // Import mappings for CSV data. - include_once dirname( dirname( __FILE__ ) ) . '/admin/importers/mappings/mappings.php'; - - $this->read_file(); - } - - /** - * Read file. - */ - protected function read_file() { - if ( ! WC_Product_CSV_Importer_Controller::is_file_valid_csv( $this->file ) ) { - wp_die( esc_html__( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); - } - - $handle = fopen( $this->file, 'r' ); // @codingStandardsIgnoreLine. - - if ( false !== $handle ) { - $this->raw_keys = version_compare( PHP_VERSION, '5.3', '>=' ) ? array_map( 'trim', fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'], $this->params['escape'] ) ) : array_map( 'trim', fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'] ) ); // @codingStandardsIgnoreLine - - // Remove BOM signature from the first item. - if ( isset( $this->raw_keys[0] ) ) { - $this->raw_keys[0] = $this->remove_utf8_bom( $this->raw_keys[0] ); - } - - if ( 0 !== $this->params['start_pos'] ) { - fseek( $handle, (int) $this->params['start_pos'] ); - } - - while ( 1 ) { - $row = version_compare( PHP_VERSION, '5.3', '>=' ) ? fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'], $this->params['escape'] ) : fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'] ); // @codingStandardsIgnoreLine - - if ( false !== $row ) { - $this->raw_data[] = $row; - $this->file_positions[ count( $this->raw_data ) ] = ftell( $handle ); - - if ( ( $this->params['end_pos'] > 0 && ftell( $handle ) >= $this->params['end_pos'] ) || 0 === --$this->params['lines'] ) { - break; - } - } else { - break; - } - } - - $this->file_position = ftell( $handle ); - } - - if ( ! empty( $this->params['mapping'] ) ) { - $this->set_mapped_keys(); - } - - if ( $this->params['parse'] ) { - $this->set_parsed_data(); - } - } - - /** - * Remove UTF-8 BOM signature. - * - * @param string $string String to handle. - * - * @return string - */ - protected function remove_utf8_bom( $string ) { - if ( 'efbbbf' === substr( bin2hex( $string ), 0, 6 ) ) { - $string = substr( $string, 3 ); - } - - return $string; - } - - /** - * Set file mapped keys. - */ - protected function set_mapped_keys() { - $mapping = $this->params['mapping']; - - foreach ( $this->raw_keys as $key ) { - $this->mapped_keys[] = isset( $mapping[ $key ] ) ? $mapping[ $key ] : $key; - } - } - - /** - * Parse relative field and return product ID. - * - * Handles `id:xx` and SKUs. - * - * If mapping to an id: and the product ID does not exist, this link is not - * valid. - * - * If mapping to a SKU and the product ID does not exist, a temporary object - * will be created so it can be updated later. - * - * @param string $value Field value. - * - * @return int|string - */ - public function parse_relative_field( $value ) { - global $wpdb; - - if ( empty( $value ) ) { - return ''; - } - - // IDs are prefixed with id:. - if ( preg_match( '/^id:(\d+)$/', $value, $matches ) ) { - $id = intval( $matches[1] ); - - // If original_id is found, use that instead of the given ID since a new placeholder must have been created already. - $original_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_original_id' AND meta_value = %s;", $id ) ); // WPCS: db call ok, cache ok. - - if ( $original_id ) { - return absint( $original_id ); - } - - // See if the given ID maps to a valid product allready. - $existing_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type IN ( 'product', 'product_variation' ) AND ID = %d;", $id ) ); // WPCS: db call ok, cache ok. - - if ( $existing_id ) { - return absint( $existing_id ); - } - - // If we're not updating existing posts, we may need a placeholder product to map to. - if ( ! $this->params['update_existing'] ) { - $product = wc_get_product_object( 'simple' ); - $product->set_name( 'Import placeholder for ' . $id ); - $product->set_status( 'importing' ); - $product->add_meta_data( '_original_id', $id, true ); - $id = $product->save(); - } - - return $id; - } - - $id = wc_get_product_id_by_sku( $value ); - - if ( $id ) { - return $id; - } - - try { - $product = wc_get_product_object( 'simple' ); - $product->set_name( 'Import placeholder for ' . $value ); - $product->set_status( 'importing' ); - $product->set_sku( $value ); - $id = $product->save(); - - if ( $id && ! is_wp_error( $id ) ) { - return $id; - } - } catch ( Exception $e ) { - return ''; - } - - return ''; - } - - /** - * Parse the ID field. - * - * If we're not doing an update, create a placeholder product so mapping works - * for rows following this one. - * - * @param string $value Field value. - * - * @return int - */ - public function parse_id_field( $value ) { - global $wpdb; - - $id = absint( $value ); - - if ( ! $id ) { - return 0; - } - - // See if this maps to an ID placeholder already. - $original_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_original_id' AND meta_value = %s;", $id ) ); // WPCS: db call ok, cache ok. - - if ( $original_id ) { - return absint( $original_id ); - } - - // Not updating? Make sure we have a new placeholder for this ID. - if ( ! $this->params['update_existing'] ) { - $mapped_keys = $this->get_mapped_keys(); - $sku_column_index = absint( array_search( 'sku', $mapped_keys, true ) ); - $row_sku = isset( $this->raw_data[ $this->parsing_raw_data_index ][ $sku_column_index ] ) ? $this->raw_data[ $this->parsing_raw_data_index ][ $sku_column_index ] : ''; - $id_from_sku = $row_sku ? wc_get_product_id_by_sku( $row_sku ) : ''; - - // If row has a SKU, make sure placeholder was not made already. - if ( $id_from_sku ) { - return $id_from_sku; - } - - $product = wc_get_product_object( 'simple' ); - $product->set_name( 'Import placeholder for ' . $id ); - $product->set_status( 'importing' ); - $product->add_meta_data( '_original_id', $id, true ); - - // If row has a SKU, make sure placeholder has it too. - if ( $row_sku ) { - $product->set_sku( $row_sku ); - } - $id = $product->save(); - } - - return $id && ! is_wp_error( $id ) ? $id : 0; - } - - /** - * Parse relative comma-delineated field and return product ID. - * - * @param string $value Field value. - * - * @return array - */ - public function parse_relative_comma_field( $value ) { - if ( empty( $value ) ) { - return array(); - } - - return array_filter( array_map( array( $this, 'parse_relative_field' ), $this->explode_values( $value ) ) ); - } - - /** - * Parse a comma-delineated field from a CSV. - * - * @param string $value Field value. - * - * @return array - */ - public function parse_comma_field( $value ) { - if ( empty( $value ) && '0' !== $value ) { - return array(); - } - - $value = $this->unescape_data( $value ); - return array_map( 'wc_clean', $this->explode_values( $value ) ); - } - - /** - * Parse a field that is generally '1' or '0' but can be something else. - * - * @param string $value Field value. - * - * @return bool|string - */ - public function parse_bool_field( $value ) { - if ( '0' === $value ) { - return false; - } - - if ( '1' === $value ) { - return true; - } - - // Don't return explicit true or false for empty fields or values like 'notify'. - return wc_clean( $value ); - } - - /** - * Parse a float value field. - * - * @param string $value Field value. - * - * @return float|string - */ - public function parse_float_field( $value ) { - if ( '' === $value ) { - return $value; - } - - // Remove the ' prepended to fields that start with - if needed. - $value = $this->unescape_data( $value ); - - return floatval( $value ); - } - - /** - * Parse the stock qty field. - * - * @param string $value Field value. - * - * @return float|string - */ - public function parse_stock_quantity_field( $value ) { - if ( '' === $value ) { - return $value; - } - - // Remove the ' prepended to fields that start with - if needed. - $value = $this->unescape_data( $value ); - - return wc_stock_amount( $value ); - } - - /** - * Parse the tax status field. - * - * @param string $value Field value. - * - * @return string - */ - public function parse_tax_status_field( $value ) { - if ( '' === $value ) { - return $value; - } - - // Remove the ' prepended to fields that start with - if needed. - $value = $this->unescape_data( $value ); - - if ( 'true' === strtolower( $value ) || 'false' === strtolower( $value ) ) { - $value = wc_string_to_bool( $value ) ? 'taxable' : 'none'; - } - - return wc_clean( $value ); - } - - /** - * Parse a category field from a CSV. - * Categories are separated by commas and subcategories are "parent > subcategory". - * - * @param string $value Field value. - * - * @return array of arrays with "parent" and "name" keys. - */ - public function parse_categories_field( $value ) { - if ( empty( $value ) ) { - return array(); - } - - $row_terms = $this->explode_values( $value ); - $categories = array(); - - foreach ( $row_terms as $row_term ) { - $parent = null; - $_terms = array_map( 'trim', explode( '>', $row_term ) ); - $total = count( $_terms ); - - foreach ( $_terms as $index => $_term ) { - // Don't allow users without capabilities to create new categories. - if ( ! current_user_can( 'manage_product_terms' ) ) { - break; - } - - $term = wp_insert_term( $_term, 'product_cat', array( 'parent' => intval( $parent ) ) ); - - if ( is_wp_error( $term ) ) { - if ( $term->get_error_code() === 'term_exists' ) { - // When term exists, error data should contain existing term id. - $term_id = $term->get_error_data(); - } else { - break; // We cannot continue on any other error. - } - } else { - // New term. - $term_id = $term['term_id']; - } - - // Only requires assign the last category. - if ( ( 1 + $index ) === $total ) { - $categories[] = $term_id; - } else { - // Store parent to be able to insert or query categories based in parent ID. - $parent = $term_id; - } - } - } - - return $categories; - } - - /** - * Parse a tag field from a CSV. - * - * @param string $value Field value. - * - * @return array - */ - public function parse_tags_field( $value ) { - if ( empty( $value ) ) { - return array(); - } - - $value = $this->unescape_data( $value ); - $names = $this->explode_values( $value ); - $tags = array(); - - foreach ( $names as $name ) { - $term = get_term_by( 'name', $name, 'product_tag' ); - - if ( ! $term || is_wp_error( $term ) ) { - $term = (object) wp_insert_term( $name, 'product_tag' ); - } - - if ( ! is_wp_error( $term ) ) { - $tags[] = $term->term_id; - } - } - - return $tags; - } - - /** - * Parse a tag field from a CSV with space separators. - * - * @param string $value Field value. - * - * @return array - */ - public function parse_tags_spaces_field( $value ) { - if ( empty( $value ) ) { - return array(); - } - - $value = $this->unescape_data( $value ); - $names = $this->explode_values( $value, ' ' ); - $tags = array(); - - foreach ( $names as $name ) { - $term = get_term_by( 'name', $name, 'product_tag' ); - - if ( ! $term || is_wp_error( $term ) ) { - $term = (object) wp_insert_term( $name, 'product_tag' ); - } - - if ( ! is_wp_error( $term ) ) { - $tags[] = $term->term_id; - } - } - - return $tags; - } - - /** - * Parse a shipping class field from a CSV. - * - * @param string $value Field value. - * - * @return int - */ - public function parse_shipping_class_field( $value ) { - if ( empty( $value ) ) { - return 0; - } - - $term = get_term_by( 'name', $value, 'product_shipping_class' ); - - if ( ! $term || is_wp_error( $term ) ) { - $term = (object) wp_insert_term( $value, 'product_shipping_class' ); - } - - if ( is_wp_error( $term ) ) { - return 0; - } - - return $term->term_id; - } - - /** - * Parse images list from a CSV. Images can be filenames or URLs. - * - * @param string $value Field value. - * - * @return array - */ - public function parse_images_field( $value ) { - if ( empty( $value ) ) { - return array(); - } - - $images = array(); - $separator = apply_filters( 'woocommerce_product_import_image_separator', ',' ); - - foreach ( $this->explode_values( $value, $separator ) as $image ) { - if ( stristr( $image, '://' ) ) { - $images[] = esc_url_raw( $image ); - } else { - $images[] = sanitize_file_name( $image ); - } - } - - return $images; - } - - /** - * Parse dates from a CSV. - * Dates requires the format YYYY-MM-DD and time is optional. - * - * @param string $value Field value. - * - * @return string|null - */ - public function parse_date_field( $value ) { - if ( empty( $value ) ) { - return null; - } - - if ( preg_match( '/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])([ 01-9:]*)$/', $value ) ) { - // Don't include the time if the field had time in it. - return current( explode( ' ', $value ) ); - } - - return null; - } - - /** - * Parse backorders from a CSV. - * - * @param string $value Field value. - * - * @return string - */ - public function parse_backorders_field( $value ) { - if ( empty( $value ) ) { - return 'no'; - } - - $value = $this->parse_bool_field( $value ); - - if ( 'notify' === $value ) { - return 'notify'; - } elseif ( is_bool( $value ) ) { - return $value ? 'yes' : 'no'; - } - - return 'no'; - } - - /** - * Just skip current field. - * - * By default is applied wc_clean() to all not listed fields - * in self::get_formatting_callback(), use this method to skip any formatting. - * - * @param string $value Field value. - * - * @return string - */ - public function parse_skip_field( $value ) { - return $value; - } - - /** - * Parse download file urls, we should allow shortcodes here. - * - * Allow shortcodes if present, othersiwe esc_url the value. - * - * @param string $value Field value. - * - * @return string - */ - public function parse_download_file_field( $value ) { - // Absolute file paths. - if ( 0 === strpos( $value, 'http' ) ) { - return esc_url_raw( $value ); - } - // Relative and shortcode paths. - return wc_clean( $value ); - } - - /** - * Parse an int value field - * - * @param int $value field value. - * - * @return int - */ - public function parse_int_field( $value ) { - // Remove the ' prepended to fields that start with - if needed. - $value = $this->unescape_data( $value ); - - return intval( $value ); - } - - /** - * Parse a description value field - * - * @param string $description field value. - * - * @return string - */ - public function parse_description_field( $description ) { - $parts = explode( "\\\\n", $description ); - foreach ( $parts as $key => $part ) { - $parts[ $key ] = str_replace( '\n', "\n", $part ); - } - - return implode( '\\\n', $parts ); - } - - /** - * Parse the published field. 1 is published, 0 is private, -1 is draft. - * Alternatively, 'true' can be used for published and 'false' for draft. - * - * @param string $value Field value. - * - * @return float|string - */ - public function parse_published_field( $value ) { - if ( '' === $value ) { - return $value; - } - - // Remove the ' prepended to fields that start with - if needed. - $value = $this->unescape_data( $value ); - - if ( 'true' === strtolower( $value ) || 'false' === strtolower( $value ) ) { - return wc_string_to_bool( $value ) ? 1 : -1; - } - - return floatval( $value ); - } - - /** - * Deprecated get formatting callback method. - * - * @deprecated 4.3.0 - * @return array - */ - protected function get_formating_callback() { - return $this->get_formatting_callback(); - } - - /** - * Get formatting callback. - * - * @since 4.3.0 - * @return array - */ - protected function get_formatting_callback() { - - /** - * Columns not mentioned here will get parsed with 'wc_clean'. - * column_name => callback. - */ - $data_formatting = array( - 'id' => array( $this, 'parse_id_field' ), - 'type' => array( $this, 'parse_comma_field' ), - 'published' => array( $this, 'parse_published_field' ), - 'featured' => array( $this, 'parse_bool_field' ), - 'date_on_sale_from' => array( $this, 'parse_date_field' ), - 'date_on_sale_to' => array( $this, 'parse_date_field' ), - 'name' => array( $this, 'parse_skip_field' ), - 'short_description' => array( $this, 'parse_description_field' ), - 'description' => array( $this, 'parse_description_field' ), - 'manage_stock' => array( $this, 'parse_bool_field' ), - 'low_stock_amount' => array( $this, 'parse_stock_quantity_field' ), - 'backorders' => array( $this, 'parse_backorders_field' ), - 'stock_status' => array( $this, 'parse_bool_field' ), - 'sold_individually' => array( $this, 'parse_bool_field' ), - 'width' => array( $this, 'parse_float_field' ), - 'length' => array( $this, 'parse_float_field' ), - 'height' => array( $this, 'parse_float_field' ), - 'weight' => array( $this, 'parse_float_field' ), - 'reviews_allowed' => array( $this, 'parse_bool_field' ), - 'purchase_note' => 'wp_filter_post_kses', - 'price' => 'wc_format_decimal', - 'regular_price' => 'wc_format_decimal', - 'stock_quantity' => array( $this, 'parse_stock_quantity_field' ), - 'category_ids' => array( $this, 'parse_categories_field' ), - 'tag_ids' => array( $this, 'parse_tags_field' ), - 'tag_ids_spaces' => array( $this, 'parse_tags_spaces_field' ), - 'shipping_class_id' => array( $this, 'parse_shipping_class_field' ), - 'images' => array( $this, 'parse_images_field' ), - 'parent_id' => array( $this, 'parse_relative_field' ), - 'grouped_products' => array( $this, 'parse_relative_comma_field' ), - 'upsell_ids' => array( $this, 'parse_relative_comma_field' ), - 'cross_sell_ids' => array( $this, 'parse_relative_comma_field' ), - 'download_limit' => array( $this, 'parse_int_field' ), - 'download_expiry' => array( $this, 'parse_int_field' ), - 'product_url' => 'esc_url_raw', - 'menu_order' => 'intval', - 'tax_status' => array( $this, 'parse_tax_status_field' ), - ); - - /** - * Match special column names. - */ - $regex_match_data_formatting = array( - '/attributes:value*/' => array( $this, 'parse_comma_field' ), - '/attributes:visible*/' => array( $this, 'parse_bool_field' ), - '/attributes:taxonomy*/' => array( $this, 'parse_bool_field' ), - '/downloads:url*/' => array( $this, 'parse_download_file_field' ), - '/meta:*/' => 'wp_kses_post', // Allow some HTML in meta fields. - ); - - $callbacks = array(); - - // Figure out the parse function for each column. - foreach ( $this->get_mapped_keys() as $index => $heading ) { - $callback = 'wc_clean'; - - if ( isset( $data_formatting[ $heading ] ) ) { - $callback = $data_formatting[ $heading ]; - } else { - foreach ( $regex_match_data_formatting as $regex => $callback ) { - if ( preg_match( $regex, $heading ) ) { - $callback = $callback; - break; - } - } - } - - $callbacks[] = $callback; - } - - return apply_filters( 'woocommerce_product_importer_formatting_callbacks', $callbacks, $this ); - } - - /** - * Check if strings starts with determined word. - * - * @param string $haystack Complete sentence. - * @param string $needle Excerpt. - * - * @return bool - */ - protected function starts_with( $haystack, $needle ) { - return substr( $haystack, 0, strlen( $needle ) ) === $needle; - } - - /** - * Expand special and internal data into the correct formats for the product CRUD. - * - * @param array $data Data to import. - * - * @return array - */ - protected function expand_data( $data ) { - $data = apply_filters( 'woocommerce_product_importer_pre_expand_data', $data ); - - // Images field maps to image and gallery id fields. - if ( isset( $data['images'] ) ) { - $images = $data['images']; - $data['raw_image_id'] = array_shift( $images ); - - if ( ! empty( $images ) ) { - $data['raw_gallery_image_ids'] = $images; - } - unset( $data['images'] ); - } - - // Type, virtual and downloadable are all stored in the same column. - if ( isset( $data['type'] ) ) { - $data['type'] = array_map( 'strtolower', $data['type'] ); - $data['virtual'] = in_array( 'virtual', $data['type'], true ); - $data['downloadable'] = in_array( 'downloadable', $data['type'], true ); - - // Convert type to string. - $data['type'] = current( array_diff( $data['type'], array( 'virtual', 'downloadable' ) ) ); - - if ( ! $data['type'] ) { - $data['type'] = 'simple'; - } - } - - // Status is mapped from a special published field. - if ( isset( $data['published'] ) ) { - $statuses = array( - -1 => 'draft', - 0 => 'private', - 1 => 'publish', - ); - $data['status'] = isset( $statuses[ $data['published'] ] ) ? $statuses[ $data['published'] ] : 'draft'; - - // Fix draft status of variations. - if ( isset( $data['type'] ) && 'variation' === $data['type'] && -1 === $data['published'] ) { - $data['status'] = 'publish'; - } - - unset( $data['published'] ); - } - - if ( isset( $data['stock_quantity'] ) ) { - if ( '' === $data['stock_quantity'] ) { - $data['manage_stock'] = false; - $data['stock_status'] = isset( $data['stock_status'] ) ? $data['stock_status'] : true; - } else { - $data['manage_stock'] = true; - } - } - - // Stock is bool or 'backorder'. - if ( isset( $data['stock_status'] ) ) { - if ( 'backorder' === $data['stock_status'] ) { - $data['stock_status'] = 'onbackorder'; - } else { - $data['stock_status'] = $data['stock_status'] ? 'instock' : 'outofstock'; - } - } - - // Prepare grouped products. - if ( isset( $data['grouped_products'] ) ) { - $data['children'] = $data['grouped_products']; - unset( $data['grouped_products'] ); - } - - // Tag ids. - if ( isset( $data['tag_ids_spaces'] ) ) { - $data['tag_ids'] = $data['tag_ids_spaces']; - unset( $data['tag_ids_spaces'] ); - } - - // Handle special column names which span multiple columns. - $attributes = array(); - $downloads = array(); - $meta_data = array(); - - foreach ( $data as $key => $value ) { - if ( $this->starts_with( $key, 'attributes:name' ) ) { - if ( ! empty( $value ) ) { - $attributes[ str_replace( 'attributes:name', '', $key ) ]['name'] = $value; - } - unset( $data[ $key ] ); - - } elseif ( $this->starts_with( $key, 'attributes:value' ) ) { - $attributes[ str_replace( 'attributes:value', '', $key ) ]['value'] = $value; - unset( $data[ $key ] ); - - } elseif ( $this->starts_with( $key, 'attributes:taxonomy' ) ) { - $attributes[ str_replace( 'attributes:taxonomy', '', $key ) ]['taxonomy'] = wc_string_to_bool( $value ); - unset( $data[ $key ] ); - - } elseif ( $this->starts_with( $key, 'attributes:visible' ) ) { - $attributes[ str_replace( 'attributes:visible', '', $key ) ]['visible'] = wc_string_to_bool( $value ); - unset( $data[ $key ] ); - - } elseif ( $this->starts_with( $key, 'attributes:default' ) ) { - if ( ! empty( $value ) ) { - $attributes[ str_replace( 'attributes:default', '', $key ) ]['default'] = $value; - } - unset( $data[ $key ] ); - - } elseif ( $this->starts_with( $key, 'downloads:name' ) ) { - if ( ! empty( $value ) ) { - $downloads[ str_replace( 'downloads:name', '', $key ) ]['name'] = $value; - } - unset( $data[ $key ] ); - - } elseif ( $this->starts_with( $key, 'downloads:url' ) ) { - if ( ! empty( $value ) ) { - $downloads[ str_replace( 'downloads:url', '', $key ) ]['url'] = $value; - } - unset( $data[ $key ] ); - - } elseif ( $this->starts_with( $key, 'meta:' ) ) { - $meta_data[] = array( - 'key' => str_replace( 'meta:', '', $key ), - 'value' => $value, - ); - unset( $data[ $key ] ); - } - } - - if ( ! empty( $attributes ) ) { - // Remove empty attributes and clear indexes. - foreach ( $attributes as $attribute ) { - if ( empty( $attribute['name'] ) ) { - continue; - } - - $data['raw_attributes'][] = $attribute; - } - } - - if ( ! empty( $downloads ) ) { - $data['downloads'] = array(); - - foreach ( $downloads as $key => $file ) { - if ( empty( $file['url'] ) ) { - continue; - } - - $data['downloads'][] = array( - 'name' => $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['url'] ), - 'file' => $file['url'], - ); - } - } - - if ( ! empty( $meta_data ) ) { - $data['meta_data'] = $meta_data; - } - - return $data; - } - - /** - * Map and format raw data to known fields. - */ - protected function set_parsed_data() { - $parse_functions = $this->get_formatting_callback(); - $mapped_keys = $this->get_mapped_keys(); - $use_mb = function_exists( 'mb_convert_encoding' ); - - // Parse the data. - foreach ( $this->raw_data as $row_index => $row ) { - // Skip empty rows. - if ( ! count( array_filter( $row ) ) ) { - continue; - } - - $this->parsing_raw_data_index = $row_index; - - $data = array(); - - do_action( 'woocommerce_product_importer_before_set_parsed_data', $row, $mapped_keys ); - - foreach ( $row as $id => $value ) { - // Skip ignored columns. - if ( empty( $mapped_keys[ $id ] ) ) { - continue; - } - - // Convert UTF8. - if ( $use_mb ) { - $encoding = mb_detect_encoding( $value, mb_detect_order(), true ); - if ( $encoding ) { - $value = mb_convert_encoding( $value, 'UTF-8', $encoding ); - } else { - $value = mb_convert_encoding( $value, 'UTF-8', 'UTF-8' ); - } - } else { - $value = wp_check_invalid_utf8( $value, true ); - } - - $data[ $mapped_keys[ $id ] ] = call_user_func( $parse_functions[ $id ], $value ); - } - - /** - * Filter product importer parsed data. - * - * @param array $parsed_data Parsed data. - * @param WC_Product_Importer $importer Importer instance. - */ - $this->parsed_data[] = apply_filters( 'woocommerce_product_importer_parsed_data', $this->expand_data( $data ), $this ); - } - } - - /** - * Get a string to identify the row from parsed data. - * - * @param array $parsed_data Parsed data. - * - * @return string - */ - protected function get_row_id( $parsed_data ) { - $id = isset( $parsed_data['id'] ) ? absint( $parsed_data['id'] ) : 0; - $sku = isset( $parsed_data['sku'] ) ? esc_attr( $parsed_data['sku'] ) : ''; - $name = isset( $parsed_data['name'] ) ? esc_attr( $parsed_data['name'] ) : ''; - $row_data = array(); - - if ( $name ) { - $row_data[] = $name; - } - if ( $id ) { - /* translators: %d: product ID */ - $row_data[] = sprintf( __( 'ID %d', 'woocommerce' ), $id ); - } - if ( $sku ) { - /* translators: %s: product SKU */ - $row_data[] = sprintf( __( 'SKU %s', 'woocommerce' ), $sku ); - } - - return implode( ', ', $row_data ); - } - - /** - * Process importer. - * - * Do not import products with IDs or SKUs that already exist if option - * update existing is false, and likewise, if updating products, do not - * process rows which do not exist if an ID/SKU is provided. - * - * @return array - */ - public function import() { - $this->start_time = time(); - $index = 0; - $update_existing = $this->params['update_existing']; - $data = array( - 'imported' => array(), - 'failed' => array(), - 'updated' => array(), - 'skipped' => array(), - ); - - foreach ( $this->parsed_data as $parsed_data_key => $parsed_data ) { - do_action( 'woocommerce_product_import_before_import', $parsed_data ); - - $id = isset( $parsed_data['id'] ) ? absint( $parsed_data['id'] ) : 0; - $sku = isset( $parsed_data['sku'] ) ? $parsed_data['sku'] : ''; - $id_exists = false; - $sku_exists = false; - - if ( $id ) { - $product = wc_get_product( $id ); - $id_exists = $product && 'importing' !== $product->get_status(); - } - - if ( $sku ) { - $id_from_sku = wc_get_product_id_by_sku( $sku ); - $product = $id_from_sku ? wc_get_product( $id_from_sku ) : false; - $sku_exists = $product && 'importing' !== $product->get_status(); - } - - if ( $id_exists && ! $update_existing ) { - $data['skipped'][] = new WP_Error( - 'woocommerce_product_importer_error', - esc_html__( 'A product with this ID already exists.', 'woocommerce' ), - array( - 'id' => $id, - 'row' => $this->get_row_id( $parsed_data ), - ) - ); - continue; - } - - if ( $sku_exists && ! $update_existing ) { - $data['skipped'][] = new WP_Error( - 'woocommerce_product_importer_error', - esc_html__( 'A product with this SKU already exists.', 'woocommerce' ), - array( - 'sku' => esc_attr( $sku ), - 'row' => $this->get_row_id( $parsed_data ), - ) - ); - continue; - } - - if ( $update_existing && ( isset( $parsed_data['id'] ) || isset( $parsed_data['sku'] ) ) && ! $id_exists && ! $sku_exists ) { - $data['skipped'][] = new WP_Error( - 'woocommerce_product_importer_error', - esc_html__( 'No matching product exists to update.', 'woocommerce' ), - array( - 'id' => $id, - 'sku' => esc_attr( $sku ), - 'row' => $this->get_row_id( $parsed_data ), - ) - ); - continue; - } - - $result = $this->process_item( $parsed_data ); - - if ( is_wp_error( $result ) ) { - $result->add_data( array( 'row' => $this->get_row_id( $parsed_data ) ) ); - $data['failed'][] = $result; - } elseif ( $result['updated'] ) { - $data['updated'][] = $result['id']; - } else { - $data['imported'][] = $result['id']; - } - - $index ++; - - if ( $this->params['prevent_timeouts'] && ( $this->time_exceeded() || $this->memory_exceeded() ) ) { - $this->file_position = $this->file_positions[ $index ]; - break; - } - } - - return $data; - } -} diff --git a/includes/interfaces/class-wc-abstract-order-data-store-interface.php b/includes/interfaces/class-wc-abstract-order-data-store-interface.php deleted file mode 100644 index c043c34e214..00000000000 --- a/includes/interfaces/class-wc-abstract-order-data-store-interface.php +++ /dev/null @@ -1,54 +0,0 @@ - [], 'failed' => []] - * - * @return array - */ - public function import(); - - /** - * Get file raw keys. - * - * CSV - Headers. - * XML - Element names. - * JSON - Keys - * - * @return array - */ - public function get_raw_keys(); - - /** - * Get file mapped headers. - * - * @return array - */ - public function get_mapped_keys(); - - /** - * Get raw data. - * - * @return array - */ - public function get_raw_data(); - - /** - * Get parsed data. - * - * @return array - */ - public function get_parsed_data(); - - /** - * Get file pointer position from the last read. - * - * @return int - */ - public function get_file_position(); - - /** - * Get file pointer position as a percentage of file size. - * - * @return int - */ - public function get_percent_complete(); -} diff --git a/includes/interfaces/class-wc-log-handler-interface.php b/includes/interfaces/class-wc-log-handler-interface.php deleted file mode 100644 index d84e39720c8..00000000000 --- a/includes/interfaces/class-wc-log-handler-interface.php +++ /dev/null @@ -1,33 +0,0 @@ -id). - * @return array - */ - public function delete_meta( &$data, $meta ); - - /** - * Add new piece of meta. - * - * @param WC_Data $data Data object. - * @param object $meta Meta object (containing ->key and ->value). - * @return int meta ID - */ - public function add_meta( &$data, $meta ); - - /** - * Update meta. - * - * @param WC_Data $data Data object. - * @param object $meta Meta object (containing ->id, ->key and ->value). - */ - public function update_meta( &$data, $meta ); -} diff --git a/includes/interfaces/class-wc-order-data-store-interface.php b/includes/interfaces/class-wc-order-data-store-interface.php deleted file mode 100644 index 6f00c8a3efe..00000000000 --- a/includes/interfaces/class-wc-order-data-store-interface.php +++ /dev/null @@ -1,141 +0,0 @@ -get_id() will be set. - * - * @param WC_Order_Item $item Item object. - */ - public function save_item_data( &$item ); -} diff --git a/includes/interfaces/class-wc-order-refund-data-store-interface.php b/includes/interfaces/class-wc-order-refund-data-store-interface.php deleted file mode 100644 index b97864d06e3..00000000000 --- a/includes/interfaces/class-wc-order-refund-data-store-interface.php +++ /dev/null @@ -1,21 +0,0 @@ -id, $return[0]->parent_id. - * - * @return array - */ - public function get_on_sale_products(); - - /** - * Returns a list of product IDs ( id as key => parent as value) that are - * featured. Uses get_posts instead of wc_get_products since we want - * some extra meta queries and ALL products (posts_per_page = -1). - * - * @return array - */ - public function get_featured_product_ids(); - - /** - * Check if product sku is found for any other product IDs. - * - * @param int $product_id Product ID. - * @param string $sku SKU. - * @return bool - */ - public function is_existing_sku( $product_id, $sku ); - - /** - * Return product ID based on SKU. - * - * @param string $sku SKU. - * @return int - */ - public function get_product_id_by_sku( $sku ); - - /** - * Returns an array of IDs of products that have sales starting soon. - * - * @return array - */ - public function get_starting_sales(); - - /** - * Returns an array of IDs of products that have sales which are due to end. - * - * @return array - */ - public function get_ending_sales(); - - /** - * Find a matching (enabled) variation within a variable product. - * - * @param WC_Product $product Variable product object. - * @param array $match_attributes Array of attributes we want to try to match. - * @return int Matching variation ID or 0. - */ - public function find_matching_product_variation( $product, $match_attributes = array() ); - - /** - * Make sure all variations have a sort order set so they can be reordered correctly. - * - * @param int $parent_id Parent ID. - */ - public function sort_all_product_variations( $parent_id ); - - /** - * Return a list of related products (using data like categories and IDs). - * - * @param array $cats_array List of categories IDs. - * @param array $tags_array List of tags IDs. - * @param array $exclude_ids Excluded IDs. - * @param int $limit Limit of results. - * @param int $product_id Product ID. - * @return array - */ - public function get_related_products( $cats_array, $tags_array, $exclude_ids, $limit, $product_id ); - - /** - * Update a product's stock amount directly. - * - * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues). - * - * @param int $product_id_with_stock Product ID. - * @param int|null $stock_quantity Stock quantity to update to. - * @param string $operation Either set, increase or decrease. - */ - public function update_product_stock( $product_id_with_stock, $stock_quantity = null, $operation = 'set' ); - - /** - * Update a product's sale count directly. - * - * Uses queries rather than update_post_meta so we can do this in one query for performance. - * - * @param int $product_id Product ID. - * @param int|null $quantity Stock quantity to use for update. - * @param string $operation Either set, increase or decrease. - */ - public function update_product_sales( $product_id, $quantity = null, $operation = 'set' ); - - /** - * Get shipping class ID by slug. - * - * @param string $slug Shipping class slug. - * @return int|false - */ - public function get_shipping_class_id_by_slug( $slug ); - - /** - * Returns an array of products. - * - * @param array $args @see wc_get_products. - * @return array - */ - public function get_products( $args = array() ); - - /** - * Get the product type based on product ID. - * - * @param int $product_id Product ID. - * @return bool|string - */ - public function get_product_type( $product_id ); -} diff --git a/includes/interfaces/class-wc-product-variable-data-store-interface.php b/includes/interfaces/class-wc-product-variable-data-store-interface.php deleted file mode 100644 index b22893dc3a6..00000000000 --- a/includes/interfaces/class-wc-product-variable-data-store-interface.php +++ /dev/null @@ -1,83 +0,0 @@ - '' - the name of the action that will be triggered. - * 'args' => null - the args array that will be passed with the action. - * 'date' => null - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. - * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '='. - * 'modified' => null - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. - * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '='. - * 'group' => '' - the group the action belongs to. - * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING. - * 'claimed' => null - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID. - * 'per_page' => 5 - Number of results to return. - * 'offset' => 0. - * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', or 'date'. - * 'order' => 'ASC'. - * @param string $return_format OBJECT, ARRAY_A, or ids. - * @return array - */ - public function search( $args = array(), $return_format = OBJECT ); -} diff --git a/includes/interfaces/class-wc-shipping-zone-data-store-interface.php b/includes/interfaces/class-wc-shipping-zone-data-store-interface.php deleted file mode 100644 index 7009c5b7209..00000000000 --- a/includes/interfaces/class-wc-shipping-zone-data-store-interface.php +++ /dev/null @@ -1,85 +0,0 @@ - - * GET /orders//notes - * - * @since 2.1 - * @param array $routes - * @return array - */ - public function register_routes( $routes ) { - - # GET|POST /orders - $routes[ $this->base ] = array( - array( array( $this, 'get_orders' ), WC_API_Server::READABLE ), - array( array( $this, 'create_order' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET /orders/count - $routes[ $this->base . '/count' ] = array( - array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ), - ); - - # GET /orders/statuses - $routes[ $this->base . '/statuses' ] = array( - array( array( $this, 'get_order_statuses' ), WC_API_Server::READABLE ), - ); - - # GET|PUT|DELETE /orders/ - $routes[ $this->base . '/(?P\d+)' ] = array( - array( array( $this, 'get_order' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_order' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_order' ), WC_API_Server::DELETABLE ), - ); - - # GET|POST /orders//notes - $routes[ $this->base . '/(?P\d+)/notes' ] = array( - array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ), - array( array( $this, 'create_order_note' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET|PUT|DELETE /orders//notes/ - $routes[ $this->base . '/(?P\d+)/notes/(?P\d+)' ] = array( - array( array( $this, 'get_order_note' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_order_note' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_order_note' ), WC_API_SERVER::DELETABLE ), - ); - - # GET|POST /orders//refunds - $routes[ $this->base . '/(?P\d+)/refunds' ] = array( - array( array( $this, 'get_order_refunds' ), WC_API_Server::READABLE ), - array( array( $this, 'create_order_refund' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET|PUT|DELETE /orders//refunds/ - $routes[ $this->base . '/(?P\d+)/refunds/(?P\d+)' ] = array( - array( array( $this, 'get_order_refund' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_order_refund' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_order_refund' ), WC_API_SERVER::DELETABLE ), - ); - - # POST|PUT /orders/bulk - $routes[ $this->base . '/bulk' ] = array( - array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - ); - - return $routes; - } - - /** - * Get all orders - * - * @since 2.1 - * @param string $fields - * @param array $filter - * @param string $status - * @param int $page - * @return array - */ - public function get_orders( $fields = null, $filter = array(), $status = null, $page = 1 ) { - - if ( ! empty( $status ) ) { - $filter['status'] = $status; - } - - $filter['page'] = $page; - - $query = $this->query_orders( $filter ); - - $orders = array(); - - foreach ( $query->posts as $order_id ) { - - if ( ! $this->is_readable( $order_id ) ) { - continue; - } - - $orders[] = current( $this->get_order( $order_id, $fields, $filter ) ); - } - - $this->server->add_pagination_headers( $query ); - - return array( 'orders' => $orders ); - } - - - /** - * Get the order for the given ID - * - * @since 2.1 - * @param int $id the order ID - * @param array $fields - * @param array $filter - * @return array|WP_Error - */ - public function get_order( $id, $fields = null, $filter = array() ) { - - // ensure order ID is valid & user has permission to read - $id = $this->validate_request( $id, $this->post_type, 'read' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - // Get the decimal precession - $dp = ( isset( $filter['dp'] ) ? intval( $filter['dp'] ) : 2 ); - $order = wc_get_order( $id ); - $order_data = array( - 'id' => $order->get_id(), - 'order_number' => $order->get_order_number(), - 'created_at' => $this->server->format_datetime( $order->get_date_created() ? $order->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times. - 'updated_at' => $this->server->format_datetime( $order->get_date_modified() ? $order->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times. - 'completed_at' => $this->server->format_datetime( $order->get_date_completed() ? $order->get_date_completed()->getTimestamp() : 0, false, false ), // API gives UTC times. - 'status' => $order->get_status(), - 'currency' => $order->get_currency(), - 'total' => wc_format_decimal( $order->get_total(), $dp ), - 'subtotal' => wc_format_decimal( $order->get_subtotal(), $dp ), - 'total_line_items_quantity' => $order->get_item_count(), - 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), - 'total_shipping' => wc_format_decimal( $order->get_shipping_total(), $dp ), - 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), - 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), - 'total_discount' => wc_format_decimal( $order->get_total_discount(), $dp ), - 'shipping_methods' => $order->get_shipping_method(), - 'payment_details' => array( - 'method_id' => $order->get_payment_method(), - 'method_title' => $order->get_payment_method_title(), - 'paid' => ! is_null( $order->get_date_paid() ), - ), - 'billing_address' => array( - 'first_name' => $order->get_billing_first_name(), - 'last_name' => $order->get_billing_last_name(), - 'company' => $order->get_billing_company(), - 'address_1' => $order->get_billing_address_1(), - 'address_2' => $order->get_billing_address_2(), - 'city' => $order->get_billing_city(), - 'state' => $order->get_billing_state(), - 'postcode' => $order->get_billing_postcode(), - 'country' => $order->get_billing_country(), - 'email' => $order->get_billing_email(), - 'phone' => $order->get_billing_phone(), - ), - 'shipping_address' => array( - 'first_name' => $order->get_shipping_first_name(), - 'last_name' => $order->get_shipping_last_name(), - 'company' => $order->get_shipping_company(), - 'address_1' => $order->get_shipping_address_1(), - 'address_2' => $order->get_shipping_address_2(), - 'city' => $order->get_shipping_city(), - 'state' => $order->get_shipping_state(), - 'postcode' => $order->get_shipping_postcode(), - 'country' => $order->get_shipping_country(), - ), - 'note' => $order->get_customer_note(), - 'customer_ip' => $order->get_customer_ip_address(), - 'customer_user_agent' => $order->get_customer_user_agent(), - 'customer_id' => $order->get_user_id(), - 'view_order_url' => $order->get_view_order_url(), - 'line_items' => array(), - 'shipping_lines' => array(), - 'tax_lines' => array(), - 'fee_lines' => array(), - 'coupon_lines' => array(), - ); - - // add line items - foreach ( $order->get_items() as $item_id => $item ) { - $product = $item->get_product(); - $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; - $item_meta = $item->get_formatted_meta_data( $hideprefix ); - - foreach ( $item_meta as $key => $values ) { - $item_meta[ $key ]->label = $values->display_key; - unset( $item_meta[ $key ]->display_key ); - unset( $item_meta[ $key ]->display_value ); - } - - $order_data['line_items'][] = array( - 'id' => $item_id, - 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), - 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), $dp ), - 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), - 'total_tax' => wc_format_decimal( $item->get_total_tax(), $dp ), - 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), - 'quantity' => $item->get_quantity(), - 'tax_class' => $item->get_tax_class(), - 'name' => $item->get_name(), - 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), - 'sku' => is_object( $product ) ? $product->get_sku() : null, - 'meta' => array_values( $item_meta ), - ); - } - - // add shipping - foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { - $order_data['shipping_lines'][] = array( - 'id' => $shipping_item_id, - 'method_id' => $shipping_item->get_method_id(), - 'method_title' => $shipping_item->get_name(), - 'total' => wc_format_decimal( $shipping_item->get_total(), $dp ), - ); - } - - // add taxes - foreach ( $order->get_tax_totals() as $tax_code => $tax ) { - $order_data['tax_lines'][] = array( - 'id' => $tax->id, - 'rate_id' => $tax->rate_id, - 'code' => $tax_code, - 'title' => $tax->label, - 'total' => wc_format_decimal( $tax->amount, $dp ), - 'compound' => (bool) $tax->is_compound, - ); - } - - // add fees - foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { - $order_data['fee_lines'][] = array( - 'id' => $fee_item_id, - 'title' => $fee_item->get_name(), - 'tax_class' => $fee_item->get_tax_class(), - 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ), - 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), - ); - } - - // add coupons - foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { - $order_data['coupon_lines'][] = array( - 'id' => $coupon_item_id, - 'code' => $coupon_item->get_code(), - 'amount' => wc_format_decimal( $coupon_item->get_discount(), $dp ), - ); - } - - return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) ); - } - - /** - * Get the total number of orders - * - * @since 2.4 - * - * @param string $status - * @param array $filter - * - * @return array|WP_Error - */ - public function get_orders_count( $status = null, $filter = array() ) { - - try { - if ( ! current_user_can( 'read_private_shop_orders' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), 401 ); - } - - if ( ! empty( $status ) ) { - - if ( 'any' === $status ) { - - $order_statuses = array(); - - foreach ( wc_get_order_statuses() as $slug => $name ) { - $filter['status'] = str_replace( 'wc-', '', $slug ); - $query = $this->query_orders( $filter ); - $order_statuses[ str_replace( 'wc-', '', $slug ) ] = (int) $query->found_posts; - } - - return array( 'count' => $order_statuses ); - - } else { - $filter['status'] = $status; - } - } - - $query = $this->query_orders( $filter ); - - return array( 'count' => (int) $query->found_posts ); - - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get a list of valid order statuses - * - * Note this requires no specific permissions other than being an authenticated - * API user. Order statuses (particularly custom statuses) could be considered - * private information which is why it's not in the API index. - * - * @since 2.1 - * @return array - */ - public function get_order_statuses() { - - $order_statuses = array(); - - foreach ( wc_get_order_statuses() as $slug => $name ) { - $order_statuses[ str_replace( 'wc-', '', $slug ) ] = $name; - } - - return array( 'order_statuses' => apply_filters( 'woocommerce_api_order_statuses_response', $order_statuses, $this ) ); - } - - /** - * Create an order - * - * @since 2.2 - * - * @param array $data raw order data - * - * @return array|WP_Error - */ - public function create_order( $data ) { - global $wpdb; - - try { - if ( ! isset( $data['order'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order' ), 400 ); - } - - $data = $data['order']; - - // permission check - if ( ! current_user_can( 'publish_shop_orders' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order', __( 'You do not have permission to create orders', 'woocommerce' ), 401 ); - } - - $data = apply_filters( 'woocommerce_api_create_order_data', $data, $this ); - - // default order args, note that status is checked for validity in wc_create_order() - $default_order_args = array( - 'status' => isset( $data['status'] ) ? $data['status'] : '', - 'customer_note' => isset( $data['note'] ) ? $data['note'] : null, - ); - - // if creating order for existing customer - if ( ! empty( $data['customer_id'] ) ) { - - // make sure customer exists - if ( false === get_user_by( 'id', $data['customer_id'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); - } - - $default_order_args['customer_id'] = $data['customer_id']; - } - - // create the pending order - $order = $this->create_base_order( $default_order_args, $data ); - - if ( is_wp_error( $order ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_order', sprintf( __( 'Cannot create order: %s', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 ); - } - - // billing/shipping addresses - $this->set_order_addresses( $order, $data ); - - $lines = array( - 'line_item' => 'line_items', - 'shipping' => 'shipping_lines', - 'fee' => 'fee_lines', - 'coupon' => 'coupon_lines', - ); - - foreach ( $lines as $line_type => $line ) { - - if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { - - $set_item = "set_{$line_type}"; - - foreach ( $data[ $line ] as $item ) { - - $this->$set_item( $order, $item, 'create' ); - } - } - } - - // calculate totals and set them - $order->calculate_totals(); - - // payment method (and payment_complete() if `paid` == true) - if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { - - // method ID & title are required - if ( empty( $data['payment_details']['method_id'] ) || empty( $data['payment_details']['method_title'] ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_payment_details', __( 'Payment method ID and title are required', 'woocommerce' ), 400 ); - } - - update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); - update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); - - // mark as paid if set - if ( isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { - $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); - } - } - - // set order currency - if ( isset( $data['currency'] ) ) { - - if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); - } - - update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); - } - - // set order meta - if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { - $this->set_order_meta( $order->get_id(), $data['order_meta'] ); - } - - // HTTP 201 Created - $this->server->send_status( 201 ); - - wc_delete_shop_order_transients( $order ); - - do_action( 'woocommerce_api_create_order', $order->get_id(), $data, $this ); - do_action( 'woocommerce_new_order', $order->get_id() ); - - return $this->get_order( $order->get_id() ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Creates new WC_Order. - * - * Requires a separate function for classes that extend WC_API_Orders. - * - * @since 2.3 - * - * @param $args array - * @param $data - * - * @return WC_Order - */ - protected function create_base_order( $args, $data ) { - return wc_create_order( $args ); - } - - /** - * Edit an order - * - * @since 2.2 - * - * @param int $id the order ID - * @param array $data - * - * @return array|WP_Error - */ - public function edit_order( $id, $data ) { - try { - if ( ! isset( $data['order'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order' ), 400 ); - } - - $data = $data['order']; - - $update_totals = false; - - $id = $this->validate_request( $id, $this->post_type, 'edit' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - $data = apply_filters( 'woocommerce_api_edit_order_data', $data, $id, $this ); - $order = wc_get_order( $id ); - - if ( empty( $order ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); - } - - $order_args = array( 'order_id' => $order->get_id() ); - - // Customer note. - if ( isset( $data['note'] ) ) { - $order_args['customer_note'] = $data['note']; - } - - // Customer ID. - if ( isset( $data['customer_id'] ) && $data['customer_id'] != $order->get_user_id() ) { - // Make sure customer exists. - if ( false === get_user_by( 'id', $data['customer_id'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); - } - - update_post_meta( $order->get_id(), '_customer_user', $data['customer_id'] ); - } - - // Billing/shipping address. - $this->set_order_addresses( $order, $data ); - - $lines = array( - 'line_item' => 'line_items', - 'shipping' => 'shipping_lines', - 'fee' => 'fee_lines', - 'coupon' => 'coupon_lines', - ); - - foreach ( $lines as $line_type => $line ) { - - if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { - - $update_totals = true; - - foreach ( $data[ $line ] as $item ) { - - // Item ID is always required. - if ( ! array_key_exists( 'id', $item ) ) { - $item['id'] = null; - } - - // Create item. - if ( is_null( $item['id'] ) ) { - $this->set_item( $order, $line_type, $item, 'create' ); - } elseif ( $this->item_is_null( $item ) ) { - // Delete item. - wc_delete_order_item( $item['id'] ); - } else { - // Update item. - $this->set_item( $order, $line_type, $item, 'update' ); - } - } - } - } - - // Payment method (and payment_complete() if `paid` == true and order needs payment). - if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { - - // Method ID. - if ( isset( $data['payment_details']['method_id'] ) ) { - update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); - } - - // Method title. - if ( isset( $data['payment_details']['method_title'] ) ) { - update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); - } - - // Mark as paid if set. - if ( $order->needs_payment() && isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { - $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); - } - } - - // Set order currency. - if ( isset( $data['currency'] ) ) { - if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); - } - - update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); - } - - // If items have changed, recalculate order totals. - if ( $update_totals ) { - $order->calculate_totals(); - } - - // Update order meta. - if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { - $this->set_order_meta( $order->get_id(), $data['order_meta'] ); - } - - // Update the order post to set customer note/modified date. - wc_update_order( $order_args ); - - // Order status. - if ( ! empty( $data['status'] ) ) { - // Refresh the order instance. - $order = wc_get_order( $order->get_id() ); - $order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true ); - } - - wc_delete_shop_order_transients( $order ); - - do_action( 'woocommerce_api_edit_order', $order->get_id(), $data, $this ); - do_action( 'woocommerce_update_order', $order->get_id() ); - - return $this->get_order( $id ); - - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete an order - * - * @param int $id the order ID - * @param bool $force true to permanently delete order, false to move to trash - * @return array|WP_Error - */ - public function delete_order( $id, $force = false ) { - - $id = $this->validate_request( $id, $this->post_type, 'delete' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - wc_delete_shop_order_transients( $id ); - - do_action( 'woocommerce_api_delete_order', $id, $this ); - - return $this->delete( $id, 'order', ( 'true' === $force ) ); - } - - /** - * Helper method to get order post objects - * - * @since 2.1 - * @param array $args request arguments for filtering query - * @return WP_Query - */ - protected function query_orders( $args ) { - - // set base query arguments - $query_args = array( - 'fields' => 'ids', - 'post_type' => $this->post_type, - 'post_status' => array_keys( wc_get_order_statuses() ), - ); - - // add status argument - if ( ! empty( $args['status'] ) ) { - - $statuses = 'wc-' . str_replace( ',', ',wc-', $args['status'] ); - $statuses = explode( ',', $statuses ); - $query_args['post_status'] = $statuses; - - unset( $args['status'] ); - - } - - $query_args = $this->merge_query_args( $query_args, $args ); - - return new WP_Query( $query_args ); - } - - /** - * Helper method to set/update the billing & shipping addresses for - * an order - * - * @since 2.1 - * @param \WC_Order $order - * @param array $data - */ - protected function set_order_addresses( $order, $data ) { - - $address_fields = array( - 'first_name', - 'last_name', - 'company', - 'email', - 'phone', - 'address_1', - 'address_2', - 'city', - 'state', - 'postcode', - 'country', - ); - - $billing_address = $shipping_address = array(); - - // billing address - if ( isset( $data['billing_address'] ) && is_array( $data['billing_address'] ) ) { - - foreach ( $address_fields as $field ) { - - if ( isset( $data['billing_address'][ $field ] ) ) { - $billing_address[ $field ] = wc_clean( $data['billing_address'][ $field ] ); - } - } - - unset( $address_fields['email'] ); - unset( $address_fields['phone'] ); - } - - // shipping address - if ( isset( $data['shipping_address'] ) && is_array( $data['shipping_address'] ) ) { - - foreach ( $address_fields as $field ) { - - if ( isset( $data['shipping_address'][ $field ] ) ) { - $shipping_address[ $field ] = wc_clean( $data['shipping_address'][ $field ] ); - } - } - } - - $this->update_address( $order, $billing_address, 'billing' ); - $this->update_address( $order, $shipping_address, 'shipping' ); - - // update user meta - if ( $order->get_user_id() ) { - foreach ( $billing_address as $key => $value ) { - update_user_meta( $order->get_user_id(), 'billing_' . $key, $value ); - } - foreach ( $shipping_address as $key => $value ) { - update_user_meta( $order->get_user_id(), 'shipping_' . $key, $value ); - } - } - } - - /** - * Update address. - * - * @param WC_Order $order - * @param array $posted - * @param string $type - */ - protected function update_address( $order, $posted, $type = 'billing' ) { - foreach ( $posted as $key => $value ) { - if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { - $order->{"set_{$type}_{$key}"}( $value ); - } - } - } - - /** - * Helper method to add/update order meta, with two restrictions: - * - * 1) Only non-protected meta (no leading underscore) can be set - * 2) Meta values must be scalar (int, string, bool) - * - * @since 2.2 - * @param int $order_id valid order ID - * @param array $order_meta order meta in array( 'meta_key' => 'meta_value' ) format - */ - protected function set_order_meta( $order_id, $order_meta ) { - - foreach ( $order_meta as $meta_key => $meta_value ) { - - if ( is_string( $meta_key ) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) { - update_post_meta( $order_id, $meta_key, $meta_value ); - } - } - } - - /** - * Helper method to check if the resource ID associated with the provided item is null - * - * Items can be deleted by setting the resource ID to null - * - * @since 2.2 - * @param array $item item provided in the request body - * @return bool true if the item resource ID is null, false otherwise - */ - protected function item_is_null( $item ) { - - $keys = array( 'product_id', 'method_id', 'title', 'code' ); - - foreach ( $keys as $key ) { - if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { - return true; - } - } - - return false; - } - - /** - * Wrapper method to create/update order items - * - * When updating, the item ID provided is checked to ensure it is associated - * with the order. - * - * @since 2.2 - * @param \WC_Order $order order - * @param string $item_type - * @param array $item item provided in the request body - * @param string $action either 'create' or 'update' - * @throws WC_API_Exception if item ID is not associated with order - */ - protected function set_item( $order, $item_type, $item, $action ) { - global $wpdb; - - $set_method = "set_{$item_type}"; - - // verify provided line item ID is associated with order - if ( 'update' === $action ) { - - $result = $wpdb->get_row( - $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", - absint( $item['id'] ), - absint( $order->get_id() ) - ) ); - - if ( is_null( $result ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); - } - } - - $this->$set_method( $order, $item, $action ); - } - - /** - * Create or update a line item - * - * @since 2.2 - * @param \WC_Order $order - * @param array $item line item data - * @param string $action 'create' to add line item or 'update' to update it - * @throws WC_API_Exception invalid data, server error - */ - protected function set_line_item( $order, $item, $action ) { - $creating = ( 'create' === $action ); - - // product is always required - if ( ! isset( $item['product_id'] ) && ! isset( $item['sku'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID or SKU is required', 'woocommerce' ), 400 ); - } - - // when updating, ensure product ID provided matches - if ( 'update' === $action ) { - - $item_product_id = wc_get_order_item_meta( $item['id'], '_product_id' ); - $item_variation_id = wc_get_order_item_meta( $item['id'], '_variation_id' ); - - if ( $item['product_id'] != $item_product_id && $item['product_id'] != $item_variation_id ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID provided does not match this line item', 'woocommerce' ), 400 ); - } - } - - if ( isset( $item['product_id'] ) ) { - $product_id = $item['product_id']; - } elseif ( isset( $item['sku'] ) ) { - $product_id = wc_get_product_id_by_sku( $item['sku'] ); - } - - // variations must each have a key & value - $variation_id = 0; - if ( isset( $item['variations'] ) && is_array( $item['variations'] ) ) { - foreach ( $item['variations'] as $key => $value ) { - if ( ! $key || ! $value ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_variation', __( 'The product variation is invalid', 'woocommerce' ), 400 ); - } - } - $variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item['variations'] ); - } - - $product = wc_get_product( $variation_id ? $variation_id : $product_id ); - - // must be a valid WC_Product - if ( ! is_object( $product ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 ); - } - - // quantity must be positive float - if ( isset( $item['quantity'] ) && floatval( $item['quantity'] ) <= 0 ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity must be a positive float.', 'woocommerce' ), 400 ); - } - - // quantity is required when creating - if ( $creating && ! isset( $item['quantity'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 ); - } - - if ( $creating ) { - $line_item = new WC_Order_Item_Product(); - } else { - $line_item = new WC_Order_Item_Product( $item['id'] ); - } - - $line_item->set_product( $product ); - $line_item->set_order_id( $order->get_id() ); - - if ( isset( $item['quantity'] ) ) { - $line_item->set_quantity( $item['quantity'] ); - } - if ( isset( $item['total'] ) ) { - $line_item->set_total( floatval( $item['total'] ) ); - } elseif ( $creating ) { - $total = wc_get_price_excluding_tax( $product, array( 'qty' => $line_item->get_quantity() ) ); - $line_item->set_total( $total ); - $line_item->set_subtotal( $total ); - } - if ( isset( $item['total_tax'] ) ) { - $line_item->set_total_tax( floatval( $item['total_tax'] ) ); - } - if ( isset( $item['subtotal'] ) ) { - $line_item->set_subtotal( floatval( $item['subtotal'] ) ); - } - if ( isset( $item['subtotal_tax'] ) ) { - $line_item->set_subtotal_tax( floatval( $item['subtotal_tax'] ) ); - } - if ( $variation_id ) { - $line_item->set_variation_id( $variation_id ); - $line_item->set_variation( $item['variations'] ); - } - - // Save or add to order. - if ( $creating ) { - $order->add_item( $line_item ); - } else { - $item_id = $line_item->save(); - - if ( ! $item_id ) { - throw new WC_API_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 ); - } - } - } - - /** - * Given a product ID & API provided variations, find the correct variation ID to use for calculation - * We can't just trust input from the API to pass a variation_id manually, otherwise you could pass - * the cheapest variation ID but provide other information so we have to look up the variation ID. - * - * @param WC_Product $product - * @param array $variations - * - * @return int returns an ID if a valid variation was found for this product - */ - function get_variation_id( $product, $variations = array() ) { - $variation_id = null; - $variations_normalized = array(); - - if ( $product->is_type( 'variable' ) && $product->has_child() ) { - if ( isset( $variations ) && is_array( $variations ) ) { - // start by normalizing the passed variations - foreach ( $variations as $key => $value ) { - $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); // from get_attributes in class-wc-api-products.php - $variations_normalized[ $key ] = strtolower( $value ); - } - // now search through each product child and see if our passed variations match anything - foreach ( $product->get_children() as $variation ) { - $meta = array(); - foreach ( get_post_meta( $variation ) as $key => $value ) { - $value = $value[0]; - $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); - $meta[ $key ] = strtolower( $value ); - } - // if the variation array is a part of the $meta array, we found our match - if ( $this->array_contains( $variations_normalized, $meta ) ) { - $variation_id = $variation; - break; - } - } - } - } - - return $variation_id; - } - - /** - * Utility function to see if the meta array contains data from variations - * - * @param array $needles - * @param array $haystack - * - * @return bool - */ - protected function array_contains( $needles, $haystack ) { - foreach ( $needles as $key => $value ) { - if ( $haystack[ $key ] !== $value ) { - return false; - } - } - return true; - } - - /** - * Create or update an order shipping method - * - * @since 2.2 - * @param \WC_Order $order - * @param array $shipping item data - * @param string $action 'create' to add shipping or 'update' to update it - * @throws WC_API_Exception invalid data, server error - */ - protected function set_shipping( $order, $shipping, $action ) { - - // total must be a positive float - if ( isset( $shipping['total'] ) && floatval( $shipping['total'] ) < 0 ) { - throw new WC_API_Exception( 'woocommerce_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 ); - } - - if ( 'create' === $action ) { - - // method ID is required - if ( ! isset( $shipping['method_id'] ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); - } - - $rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] ); - $item = new WC_Order_Item_Shipping(); - $item->set_order_id( $order->get_id() ); - $item->set_shipping_rate( $rate ); - $order->add_item( $item ); - } else { - - $item = new WC_Order_Item_Shipping( $shipping['id'] ); - - if ( isset( $shipping['method_id'] ) ) { - $item->set_method_id( $shipping['method_id'] ); - } - - if ( isset( $shipping['method_title'] ) ) { - $item->set_method_title( $shipping['method_title'] ); - } - - if ( isset( $shipping['total'] ) ) { - $item->set_total( floatval( $shipping['total'] ) ); - } - - $shipping_id = $item->save(); - - if ( ! $shipping_id ) { - throw new WC_API_Exception( 'woocommerce_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 ); - } - } - } - - /** - * Create or update an order fee - * - * @since 2.2 - * @param \WC_Order $order - * @param array $fee item data - * @param string $action 'create' to add fee or 'update' to update it - * @throws WC_API_Exception invalid data, server error - */ - protected function set_fee( $order, $fee, $action ) { - - if ( 'create' === $action ) { - - // fee title is required - if ( ! isset( $fee['title'] ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee title is required', 'woocommerce' ), 400 ); - } - - $item = new WC_Order_Item_Fee(); - $item->set_order_id( $order->get_id() ); - $item->set_name( wc_clean( $fee['title'] ) ); - $item->set_total( isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0 ); - - // if taxable, tax class and total are required - if ( ! empty( $fee['taxable'] ) ) { - if ( ! isset( $fee['tax_class'] ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 ); - } - - $item->set_tax_status( 'taxable' ); - $item->set_tax_class( $fee['tax_class'] ); - - if ( isset( $fee['total_tax'] ) ) { - $item->set_total_tax( isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0 ); - } - - if ( isset( $fee['tax_data'] ) ) { - $item->set_total_tax( wc_format_refund_total( array_sum( $fee['tax_data'] ) ) ); - $item->set_taxes( array_map( 'wc_format_refund_total', $fee['tax_data'] ) ); - } - } - - $order->add_item( $item ); - } else { - - $item = new WC_Order_Item_Fee( $fee['id'] ); - - if ( isset( $fee['title'] ) ) { - $item->set_name( wc_clean( $fee['title'] ) ); - } - - if ( isset( $fee['tax_class'] ) ) { - $item->set_tax_class( $fee['tax_class'] ); - } - - if ( isset( $fee['total'] ) ) { - $item->set_total( floatval( $fee['total'] ) ); - } - - if ( isset( $fee['total_tax'] ) ) { - $item->set_total_tax( floatval( $fee['total_tax'] ) ); - } - - $fee_id = $item->save(); - - if ( ! $fee_id ) { - throw new WC_API_Exception( 'woocommerce_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 ); - } - } - } - - /** - * Create or update an order coupon - * - * @since 2.2 - * @param \WC_Order $order - * @param array $coupon item data - * @param string $action 'create' to add coupon or 'update' to update it - * @throws WC_API_Exception invalid data, server error - */ - protected function set_coupon( $order, $coupon, $action ) { - - // coupon amount must be positive float - if ( isset( $coupon['amount'] ) && floatval( $coupon['amount'] ) < 0 ) { - throw new WC_API_Exception( 'woocommerce_invalid_coupon_total', __( 'Coupon discount total must be a positive amount.', 'woocommerce' ), 400 ); - } - - if ( 'create' === $action ) { - - // coupon code is required - if ( empty( $coupon['code'] ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); - } - - $item = new WC_Order_Item_Coupon(); - $item->set_props( array( - 'code' => $coupon['code'], - 'discount' => isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0, - 'discount_tax' => 0, - 'order_id' => $order->get_id(), - ) ); - $order->add_item( $item ); - } else { - - $item = new WC_Order_Item_Coupon( $coupon['id'] ); - - if ( isset( $coupon['code'] ) ) { - $item->set_code( $coupon['code'] ); - } - - if ( isset( $coupon['amount'] ) ) { - $item->set_discount( floatval( $coupon['amount'] ) ); - } - - $coupon_id = $item->save(); - - if ( ! $coupon_id ) { - throw new WC_API_Exception( 'woocommerce_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 ); - } - } - } - - /** - * Get the admin order notes for an order - * - * @since 2.1 - * @param string $order_id order ID - * @param string|null $fields fields to include in response - * @return array|WP_Error - */ - public function get_order_notes( $order_id, $fields = null ) { - - // ensure ID is valid order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $args = array( - 'post_id' => $order_id, - 'approve' => 'approve', - 'type' => 'order_note', - ); - - remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); - - $notes = get_comments( $args ); - - add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); - - $order_notes = array(); - - foreach ( $notes as $note ) { - - $order_notes[] = current( $this->get_order_note( $order_id, $note->comment_ID, $fields ) ); - } - - return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $order_id, $fields, $notes, $this->server ) ); - } - - /** - * Get an order note for the given order ID and ID - * - * @since 2.2 - * - * @param string $order_id order ID - * @param string $id order note ID - * @param string|null $fields fields to limit response to - * - * @return array|WP_Error - */ - public function get_order_note( $order_id, $id, $fields = null ) { - try { - // Validate order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); - } - - $note = get_comment( $id ); - - if ( is_null( $note ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $order_note = array( - 'id' => $note->comment_ID, - 'created_at' => $this->server->format_datetime( $note->comment_date_gmt ), - 'note' => $note->comment_content, - 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), - ); - - return array( 'order_note' => apply_filters( 'woocommerce_api_order_note_response', $order_note, $id, $fields, $note, $order_id, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Create a new order note for the given order - * - * @since 2.2 - * @param string $order_id order ID - * @param array $data raw request data - * @return WP_Error|array error or created note response data - */ - public function create_order_note( $order_id, $data ) { - try { - if ( ! isset( $data['order_note'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_note' ), 400 ); - } - - $data = $data['order_note']; - - // permission check - if ( ! current_user_can( 'publish_shop_orders' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_note', __( 'You do not have permission to create order notes', 'woocommerce' ), 401 ); - } - - $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $order = wc_get_order( $order_id ); - - $data = apply_filters( 'woocommerce_api_create_order_note_data', $data, $order_id, $this ); - - // note content is required - if ( ! isset( $data['note'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note', __( 'Order note is required', 'woocommerce' ), 400 ); - } - - $is_customer_note = ( isset( $data['customer_note'] ) && true === $data['customer_note'] ); - - // create the note - $note_id = $order->add_order_note( $data['note'], $is_customer_note ); - - if ( ! $note_id ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), 500 ); - } - - // HTTP 201 Created - $this->server->send_status( 201 ); - - do_action( 'woocommerce_api_create_order_note', $note_id, $order_id, $this ); - - return $this->get_order_note( $order->get_id(), $note_id ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit the order note - * - * @since 2.2 - * @param string $order_id order ID - * @param string $id note ID - * @param array $data parsed request data - * @return WP_Error|array error or edited note response data - */ - public function edit_order_note( $order_id, $id, $data ) { - try { - if ( ! isset( $data['order_note'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_note' ), 400 ); - } - - $data = $data['order_note']; - - // Validate order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $order = wc_get_order( $order_id ); - - // Validate note ID - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); - } - - // Ensure note ID is valid - $note = get_comment( $id ); - - if ( is_null( $note ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - // Ensure note ID is associated with given order - if ( $note->comment_post_ID != $order->get_id() ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); - } - - $data = apply_filters( 'woocommerce_api_edit_order_note_data', $data, $note->comment_ID, $order->get_id(), $this ); - - // Note content - if ( isset( $data['note'] ) ) { - - wp_update_comment( - array( - 'comment_ID' => $note->comment_ID, - 'comment_content' => $data['note'], - ) - ); - } - - // Customer note - if ( isset( $data['customer_note'] ) ) { - - update_comment_meta( $note->comment_ID, 'is_customer_note', true === $data['customer_note'] ? 1 : 0 ); - } - - do_action( 'woocommerce_api_edit_order_note', $note->comment_ID, $order->get_id(), $this ); - - return $this->get_order_note( $order->get_id(), $note->comment_ID ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete order note - * - * @since 2.2 - * @param string $order_id order ID - * @param string $id note ID - * @return WP_Error|array error or deleted message - */ - public function delete_order_note( $order_id, $id ) { - try { - $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - // Validate note ID - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); - } - - // Ensure note ID is valid - $note = get_comment( $id ); - - if ( is_null( $note ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - // Ensure note ID is associated with given order - if ( $note->comment_post_ID != $order_id ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); - } - - // Force delete since trashed order notes could not be managed through comments list table - $result = wc_delete_order_note( $note->comment_ID ); - - if ( ! $result ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_delete_order_note', __( 'This order note cannot be deleted', 'woocommerce' ), 500 ); - } - - do_action( 'woocommerce_api_delete_order_note', $note->comment_ID, $order_id, $this ); - - return array( 'message' => __( 'Permanently deleted order note', 'woocommerce' ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get the order refunds for an order - * - * @since 2.2 - * @param string $order_id order ID - * @param string|null $fields fields to include in response - * @return array|WP_Error - */ - public function get_order_refunds( $order_id, $fields = null ) { - - // Ensure ID is valid order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $refund_items = wc_get_orders( array( - 'type' => 'shop_order_refund', - 'parent' => $order_id, - 'limit' => -1, - 'return' => 'ids', - ) ); - $order_refunds = array(); - - foreach ( $refund_items as $refund_id ) { - $order_refunds[] = current( $this->get_order_refund( $order_id, $refund_id, $fields ) ); - } - - return array( 'order_refunds' => apply_filters( 'woocommerce_api_order_refunds_response', $order_refunds, $order_id, $fields, $refund_items, $this ) ); - } - - /** - * Get an order refund for the given order ID and ID - * - * @since 2.2 - * - * @param string $order_id order ID - * @param int $id - * @param string|null $fields fields to limit response to - * @param array $filter - * - * @return array|WP_Error - */ - public function get_order_refund( $order_id, $id, $fields = null, $filter = array() ) { - try { - // Validate order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); - } - - $order = wc_get_order( $order_id ); - $refund = wc_get_order( $id ); - - if ( ! $refund ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); - } - - $line_items = array(); - - // Add line items - foreach ( $refund->get_items( 'line_item' ) as $item_id => $item ) { - $product = $item->get_product(); - $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; - $item_meta = $item->get_formatted_meta_data( $hideprefix ); - - foreach ( $item_meta as $key => $values ) { - $item_meta[ $key ]->label = $values->display_key; - unset( $item_meta[ $key ]->display_key ); - unset( $item_meta[ $key ]->display_value ); - } - - $line_items[] = array( - 'id' => $item_id, - 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ), - 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), 2 ), - 'total' => wc_format_decimal( $order->get_line_total( $item ), 2 ), - 'total_tax' => wc_format_decimal( $order->get_line_tax( $item ), 2 ), - 'price' => wc_format_decimal( $order->get_item_total( $item ), 2 ), - 'quantity' => $item->get_quantity(), - 'tax_class' => $item->get_tax_class(), - 'name' => $item->get_name(), - 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), - 'sku' => is_object( $product ) ? $product->get_sku() : null, - 'meta' => array_values( $item_meta ), - 'refunded_item_id' => (int) $item->get_meta( 'refunded_item_id' ), - ); - } - - $order_refund = array( - 'id' => $refund->get_id(), - 'created_at' => $this->server->format_datetime( $refund->get_date_created() ? $refund->get_date_created()->getTimestamp() : 0, false, false ), - 'amount' => wc_format_decimal( $refund->get_amount(), 2 ), - 'reason' => $refund->get_reason(), - 'line_items' => $line_items, - ); - - return array( 'order_refund' => apply_filters( 'woocommerce_api_order_refund_response', $order_refund, $id, $fields, $refund, $order_id, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Create a new order refund for the given order - * - * @since 2.2 - * @param string $order_id order ID - * @param array $data raw request data - * @param bool $api_refund do refund using a payment gateway API - * @return WP_Error|array error or created refund response data - */ - public function create_order_refund( $order_id, $data, $api_refund = true ) { - try { - if ( ! isset( $data['order_refund'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_refund' ), 400 ); - } - - $data = $data['order_refund']; - - // Permission check - if ( ! current_user_can( 'publish_shop_orders' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_refund', __( 'You do not have permission to create order refunds', 'woocommerce' ), 401 ); - } - - $order_id = absint( $order_id ); - - if ( empty( $order_id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); - } - - $data = apply_filters( 'woocommerce_api_create_order_refund_data', $data, $order_id, $this ); - - // Refund amount is required - if ( ! isset( $data['amount'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount is required.', 'woocommerce' ), 400 ); - } elseif ( 0 > $data['amount'] ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount must be positive.', 'woocommerce' ), 400 ); - } - - $data['order_id'] = $order_id; - $data['refund_id'] = 0; - - // Create the refund - $refund = wc_create_refund( $data ); - - if ( ! $refund ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); - } - - // Refund via API - if ( $api_refund ) { - if ( WC()->payment_gateways() ) { - $payment_gateways = WC()->payment_gateways->payment_gateways(); - } - - $order = wc_get_order( $order_id ); - - if ( isset( $payment_gateways[ $order->get_payment_method() ] ) && $payment_gateways[ $order->get_payment_method() ]->supports( 'refunds' ) ) { - $result = $payment_gateways[ $order->get_payment_method() ]->process_refund( $order_id, $refund->get_amount(), $refund->get_reason() ); - - if ( is_wp_error( $result ) ) { - return $result; - } elseif ( ! $result ) { - throw new WC_API_Exception( 'woocommerce_api_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 ); - } - } - } - - // HTTP 201 Created - $this->server->send_status( 201 ); - - do_action( 'woocommerce_api_create_order_refund', $refund->get_id(), $order_id, $this ); - - return $this->get_order_refund( $order_id, $refund->get_id() ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit an order refund - * - * @since 2.2 - * @param string $order_id order ID - * @param string $id refund ID - * @param array $data parsed request data - * @return WP_Error|array error or edited refund response data - */ - public function edit_order_refund( $order_id, $id, $data ) { - try { - if ( ! isset( $data['order_refund'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_refund' ), 400 ); - } - - $data = $data['order_refund']; - - // Validate order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - // Validate refund ID - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); - } - - // Ensure order ID is valid - $refund = get_post( $id ); - - if ( ! $refund ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); - } - - // Ensure refund ID is associated with given order - if ( $refund->post_parent != $order_id ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); - } - - $data = apply_filters( 'woocommerce_api_edit_order_refund_data', $data, $refund->ID, $order_id, $this ); - - // Update reason - if ( isset( $data['reason'] ) ) { - $updated_refund = wp_update_post( array( 'ID' => $refund->ID, 'post_excerpt' => $data['reason'] ) ); - - if ( is_wp_error( $updated_refund ) ) { - return $updated_refund; - } - } - - // Update refund amount - if ( isset( $data['amount'] ) && 0 < $data['amount'] ) { - update_post_meta( $refund->ID, '_refund_amount', wc_format_decimal( $data['amount'] ) ); - } - - do_action( 'woocommerce_api_edit_order_refund', $refund->ID, $order_id, $this ); - - return $this->get_order_refund( $order_id, $refund->ID ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete order refund - * - * @since 2.2 - * @param string $order_id order ID - * @param string $id refund ID - * @return WP_Error|array error or deleted message - */ - public function delete_order_refund( $order_id, $id ) { - try { - $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - // Validate refund ID - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); - } - - // Ensure refund ID is valid - $refund = get_post( $id ); - - if ( ! $refund ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); - } - - // Ensure refund ID is associated with given order - if ( $refund->post_parent != $order_id ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); - } - - wc_delete_shop_order_transients( $order_id ); - - do_action( 'woocommerce_api_delete_order_refund', $refund->ID, $order_id, $this ); - - return $this->delete( $refund->ID, 'refund', true ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Bulk update or insert orders - * Accepts an array with orders in the formats supported by - * WC_API_Orders->create_order() and WC_API_Orders->edit_order() - * - * @since 2.4.0 - * - * @param array $data - * - * @return array|WP_Error - */ - public function bulk( $data ) { - - try { - if ( ! isset( $data['orders'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_orders_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'orders' ), 400 ); - } - - $data = $data['orders']; - $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'orders' ); - - // Limit bulk operation - if ( count( $data ) > $limit ) { - throw new WC_API_Exception( 'woocommerce_api_orders_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); - } - - $orders = array(); - - foreach ( $data as $_order ) { - $order_id = 0; - - // Try to get the order ID - if ( isset( $_order['id'] ) ) { - $order_id = intval( $_order['id'] ); - } - - // Order exists / edit order - if ( $order_id ) { - $edit = $this->edit_order( $order_id, array( 'order' => $_order ) ); - - if ( is_wp_error( $edit ) ) { - $orders[] = array( - 'id' => $order_id, - 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), - ); - } else { - $orders[] = $edit['order']; - } - } else { - // Order don't exists / create order - $new = $this->create_order( array( 'order' => $_order ) ); - - if ( is_wp_error( $new ) ) { - $orders[] = array( - 'id' => $order_id, - 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), - ); - } else { - $orders[] = $new['order']; - } - } - } - - return array( 'orders' => apply_filters( 'woocommerce_api_orders_bulk_response', $orders, $this ) ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } -} diff --git a/includes/legacy/api/v3/class-wc-api-orders.php b/includes/legacy/api/v3/class-wc-api-orders.php deleted file mode 100644 index 2fe2b6cdcf4..00000000000 --- a/includes/legacy/api/v3/class-wc-api-orders.php +++ /dev/null @@ -1,1877 +0,0 @@ - - * GET /orders//notes - * - * @since 2.1 - * @param array $routes - * @return array - */ - public function register_routes( $routes ) { - - # GET|POST /orders - $routes[ $this->base ] = array( - array( array( $this, 'get_orders' ), WC_API_Server::READABLE ), - array( array( $this, 'create_order' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET /orders/count - $routes[ $this->base . '/count' ] = array( - array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ), - ); - - # GET /orders/statuses - $routes[ $this->base . '/statuses' ] = array( - array( array( $this, 'get_order_statuses' ), WC_API_Server::READABLE ), - ); - - # GET|PUT|DELETE /orders/ - $routes[ $this->base . '/(?P\d+)' ] = array( - array( array( $this, 'get_order' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_order' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_order' ), WC_API_Server::DELETABLE ), - ); - - # GET|POST /orders//notes - $routes[ $this->base . '/(?P\d+)/notes' ] = array( - array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ), - array( array( $this, 'create_order_note' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET|PUT|DELETE /orders//notes/ - $routes[ $this->base . '/(?P\d+)/notes/(?P\d+)' ] = array( - array( array( $this, 'get_order_note' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_order_note' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_order_note' ), WC_API_SERVER::DELETABLE ), - ); - - # GET|POST /orders//refunds - $routes[ $this->base . '/(?P\d+)/refunds' ] = array( - array( array( $this, 'get_order_refunds' ), WC_API_Server::READABLE ), - array( array( $this, 'create_order_refund' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET|PUT|DELETE /orders//refunds/ - $routes[ $this->base . '/(?P\d+)/refunds/(?P\d+)' ] = array( - array( array( $this, 'get_order_refund' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_order_refund' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_order_refund' ), WC_API_SERVER::DELETABLE ), - ); - - # POST|PUT /orders/bulk - $routes[ $this->base . '/bulk' ] = array( - array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - ); - - return $routes; - } - - /** - * Get all orders - * - * @since 2.1 - * @param string $fields - * @param array $filter - * @param string $status - * @param int $page - * @return array - */ - public function get_orders( $fields = null, $filter = array(), $status = null, $page = 1 ) { - - if ( ! empty( $status ) ) { - $filter['status'] = $status; - } - - $filter['page'] = $page; - - $query = $this->query_orders( $filter ); - - $orders = array(); - - foreach ( $query->posts as $order_id ) { - - if ( ! $this->is_readable( $order_id ) ) { - continue; - } - - $orders[] = current( $this->get_order( $order_id, $fields, $filter ) ); - } - - $this->server->add_pagination_headers( $query ); - - return array( 'orders' => $orders ); - } - - - /** - * Get the order for the given ID. - * - * @since 2.1 - * @param int $id The order ID. - * @param array $fields Request fields. - * @param array $filter Request filters. - * @return array|WP_Error - */ - public function get_order( $id, $fields = null, $filter = array() ) { - - // Ensure order ID is valid & user has permission to read. - $id = $this->validate_request( $id, $this->post_type, 'read' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - // Get the decimal precession. - $dp = ( isset( $filter['dp'] ) ? intval( $filter['dp'] ) : 2 ); - $order = wc_get_order( $id ); - $expand = array(); - - if ( ! empty( $filter['expand'] ) ) { - $expand = explode( ',', $filter['expand'] ); - } - - $order_data = array( - 'id' => $order->get_id(), - 'order_number' => $order->get_order_number(), - 'order_key' => $order->get_order_key(), - 'created_at' => $this->server->format_datetime( $order->get_date_created() ? $order->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times. - 'updated_at' => $this->server->format_datetime( $order->get_date_modified() ? $order->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times. - 'completed_at' => $this->server->format_datetime( $order->get_date_completed() ? $order->get_date_completed()->getTimestamp() : 0, false, false ), // API gives UTC times. - 'status' => $order->get_status(), - 'currency' => $order->get_currency(), - 'total' => wc_format_decimal( $order->get_total(), $dp ), - 'subtotal' => wc_format_decimal( $order->get_subtotal(), $dp ), - 'total_line_items_quantity' => $order->get_item_count(), - 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), - 'total_shipping' => wc_format_decimal( $order->get_shipping_total(), $dp ), - 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), - 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), - 'total_discount' => wc_format_decimal( $order->get_total_discount(), $dp ), - 'shipping_methods' => $order->get_shipping_method(), - 'payment_details' => array( - 'method_id' => $order->get_payment_method(), - 'method_title' => $order->get_payment_method_title(), - 'paid' => ! is_null( $order->get_date_paid() ), - ), - 'billing_address' => array( - 'first_name' => $order->get_billing_first_name(), - 'last_name' => $order->get_billing_last_name(), - 'company' => $order->get_billing_company(), - 'address_1' => $order->get_billing_address_1(), - 'address_2' => $order->get_billing_address_2(), - 'city' => $order->get_billing_city(), - 'state' => $order->get_billing_state(), - 'postcode' => $order->get_billing_postcode(), - 'country' => $order->get_billing_country(), - 'email' => $order->get_billing_email(), - 'phone' => $order->get_billing_phone(), - ), - 'shipping_address' => array( - 'first_name' => $order->get_shipping_first_name(), - 'last_name' => $order->get_shipping_last_name(), - 'company' => $order->get_shipping_company(), - 'address_1' => $order->get_shipping_address_1(), - 'address_2' => $order->get_shipping_address_2(), - 'city' => $order->get_shipping_city(), - 'state' => $order->get_shipping_state(), - 'postcode' => $order->get_shipping_postcode(), - 'country' => $order->get_shipping_country(), - ), - 'note' => $order->get_customer_note(), - 'customer_ip' => $order->get_customer_ip_address(), - 'customer_user_agent' => $order->get_customer_user_agent(), - 'customer_id' => $order->get_user_id(), - 'view_order_url' => $order->get_view_order_url(), - 'line_items' => array(), - 'shipping_lines' => array(), - 'tax_lines' => array(), - 'fee_lines' => array(), - 'coupon_lines' => array(), - ); - - // Add line items. - foreach ( $order->get_items() as $item_id => $item ) { - $product = $item->get_product(); - $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; - $item_meta = $item->get_formatted_meta_data( $hideprefix ); - - foreach ( $item_meta as $key => $values ) { - $item_meta[ $key ]->label = $values->display_key; - unset( $item_meta[ $key ]->display_key ); - unset( $item_meta[ $key ]->display_value ); - } - - $line_item = array( - 'id' => $item_id, - 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), - 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), $dp ), - 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), - 'total_tax' => wc_format_decimal( $item->get_total_tax(), $dp ), - 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), - 'quantity' => $item->get_quantity(), - 'tax_class' => $item->get_tax_class(), - 'name' => $item->get_name(), - 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), - 'sku' => is_object( $product ) ? $product->get_sku() : null, - 'meta' => array_values( $item_meta ), - ); - - if ( in_array( 'products', $expand ) && is_object( $product ) ) { - $_product_data = WC()->api->WC_API_Products->get_product( $product->get_id() ); - - if ( isset( $_product_data['product'] ) ) { - $line_item['product_data'] = $_product_data['product']; - } - } - - $order_data['line_items'][] = $line_item; - } - - // Add shipping. - foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { - $order_data['shipping_lines'][] = array( - 'id' => $shipping_item_id, - 'method_id' => $shipping_item->get_method_id(), - 'method_title' => $shipping_item->get_name(), - 'total' => wc_format_decimal( $shipping_item->get_total(), $dp ), - ); - } - - // Add taxes. - foreach ( $order->get_tax_totals() as $tax_code => $tax ) { - $tax_line = array( - 'id' => $tax->id, - 'rate_id' => $tax->rate_id, - 'code' => $tax_code, - 'title' => $tax->label, - 'total' => wc_format_decimal( $tax->amount, $dp ), - 'compound' => (bool) $tax->is_compound, - ); - - if ( in_array( 'taxes', $expand ) ) { - $_rate_data = WC()->api->WC_API_Taxes->get_tax( $tax->rate_id ); - - if ( isset( $_rate_data['tax'] ) ) { - $tax_line['rate_data'] = $_rate_data['tax']; - } - } - - $order_data['tax_lines'][] = $tax_line; - } - - // Add fees. - foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { - $order_data['fee_lines'][] = array( - 'id' => $fee_item_id, - 'title' => $fee_item->get_name(), - 'tax_class' => $fee_item->get_tax_class(), - 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ), - 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), - ); - } - - // Add coupons. - foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { - $coupon_line = array( - 'id' => $coupon_item_id, - 'code' => $coupon_item->get_code(), - 'amount' => wc_format_decimal( $coupon_item->get_discount(), $dp ), - ); - - if ( in_array( 'coupons', $expand ) ) { - $_coupon_data = WC()->api->WC_API_Coupons->get_coupon_by_code( $coupon_item->get_code() ); - - if ( ! is_wp_error( $_coupon_data ) && isset( $_coupon_data['coupon'] ) ) { - $coupon_line['coupon_data'] = $_coupon_data['coupon']; - } - } - - $order_data['coupon_lines'][] = $coupon_line; - } - - return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) ); - } - - /** - * Get the total number of orders - * - * @since 2.4 - * - * @param string $status - * @param array $filter - * - * @return array|WP_Error - */ - public function get_orders_count( $status = null, $filter = array() ) { - - try { - if ( ! current_user_can( 'read_private_shop_orders' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), 401 ); - } - - if ( ! empty( $status ) ) { - - if ( 'any' === $status ) { - - $order_statuses = array(); - - foreach ( wc_get_order_statuses() as $slug => $name ) { - $filter['status'] = str_replace( 'wc-', '', $slug ); - $query = $this->query_orders( $filter ); - $order_statuses[ str_replace( 'wc-', '', $slug ) ] = (int) $query->found_posts; - } - - return array( 'count' => $order_statuses ); - - } else { - $filter['status'] = $status; - } - } - - $query = $this->query_orders( $filter ); - - return array( 'count' => (int) $query->found_posts ); - - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get a list of valid order statuses - * - * Note this requires no specific permissions other than being an authenticated - * API user. Order statuses (particularly custom statuses) could be considered - * private information which is why it's not in the API index. - * - * @since 2.1 - * @return array - */ - public function get_order_statuses() { - - $order_statuses = array(); - - foreach ( wc_get_order_statuses() as $slug => $name ) { - $order_statuses[ str_replace( 'wc-', '', $slug ) ] = $name; - } - - return array( 'order_statuses' => apply_filters( 'woocommerce_api_order_statuses_response', $order_statuses, $this ) ); - } - - /** - * Create an order - * - * @since 2.2 - * @param array $data raw order data - * @return array|WP_Error - */ - public function create_order( $data ) { - global $wpdb; - - try { - if ( ! isset( $data['order'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order' ), 400 ); - } - - $data = $data['order']; - - // permission check - if ( ! current_user_can( 'publish_shop_orders' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order', __( 'You do not have permission to create orders', 'woocommerce' ), 401 ); - } - - $data = apply_filters( 'woocommerce_api_create_order_data', $data, $this ); - - // default order args, note that status is checked for validity in wc_create_order() - $default_order_args = array( - 'status' => isset( $data['status'] ) ? $data['status'] : '', - 'customer_note' => isset( $data['note'] ) ? $data['note'] : null, - ); - - // if creating order for existing customer - if ( ! empty( $data['customer_id'] ) ) { - - // make sure customer exists - if ( false === get_user_by( 'id', $data['customer_id'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); - } - - $default_order_args['customer_id'] = $data['customer_id']; - } - - // create the pending order - $order = $this->create_base_order( $default_order_args, $data ); - - if ( is_wp_error( $order ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_order', sprintf( __( 'Cannot create order: %s', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 ); - } - - // billing/shipping addresses - $this->set_order_addresses( $order, $data ); - - $lines = array( - 'line_item' => 'line_items', - 'shipping' => 'shipping_lines', - 'fee' => 'fee_lines', - 'coupon' => 'coupon_lines', - ); - - foreach ( $lines as $line_type => $line ) { - - if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { - - $set_item = "set_{$line_type}"; - - foreach ( $data[ $line ] as $item ) { - - $this->$set_item( $order, $item, 'create' ); - } - } - } - - // set is vat exempt - if ( isset( $data['is_vat_exempt'] ) ) { - update_post_meta( $order->get_id(), '_is_vat_exempt', $data['is_vat_exempt'] ? 'yes' : 'no' ); - } - - // calculate totals and set them - $order->calculate_totals(); - - // payment method (and payment_complete() if `paid` == true) - if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { - - // method ID & title are required - if ( empty( $data['payment_details']['method_id'] ) || empty( $data['payment_details']['method_title'] ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_payment_details', __( 'Payment method ID and title are required', 'woocommerce' ), 400 ); - } - - update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); - update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); - - // mark as paid if set - if ( isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { - $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); - } - } - - // set order currency - if ( isset( $data['currency'] ) ) { - - if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); - } - - update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); - } - - // set order meta - if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { - $this->set_order_meta( $order->get_id(), $data['order_meta'] ); - } - - // HTTP 201 Created - $this->server->send_status( 201 ); - - wc_delete_shop_order_transients( $order ); - - do_action( 'woocommerce_api_create_order', $order->get_id(), $data, $this ); - do_action( 'woocommerce_new_order', $order->get_id() ); - - return $this->get_order( $order->get_id() ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Creates new WC_Order. - * - * Requires a separate function for classes that extend WC_API_Orders. - * - * @since 2.3 - * - * @param $args array - * @param $data - * - * @return WC_Order - */ - protected function create_base_order( $args, $data ) { - return wc_create_order( $args ); - } - - /** - * Edit an order - * - * @since 2.2 - * @param int $id the order ID - * @param array $data - * @return array|WP_Error - */ - public function edit_order( $id, $data ) { - try { - if ( ! isset( $data['order'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order' ), 400 ); - } - - $data = $data['order']; - - $update_totals = false; - - $id = $this->validate_request( $id, $this->post_type, 'edit' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - $data = apply_filters( 'woocommerce_api_edit_order_data', $data, $id, $this ); - $order = wc_get_order( $id ); - - if ( empty( $order ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); - } - - $order_args = array( 'order_id' => $order->get_id() ); - - // Customer note. - if ( isset( $data['note'] ) ) { - $order_args['customer_note'] = $data['note']; - } - - // Customer ID. - if ( isset( $data['customer_id'] ) && $data['customer_id'] != $order->get_user_id() ) { - // Make sure customer exists. - if ( false === get_user_by( 'id', $data['customer_id'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); - } - - update_post_meta( $order->get_id(), '_customer_user', $data['customer_id'] ); - } - - // Billing/shipping address. - $this->set_order_addresses( $order, $data ); - - $lines = array( - 'line_item' => 'line_items', - 'shipping' => 'shipping_lines', - 'fee' => 'fee_lines', - 'coupon' => 'coupon_lines', - ); - - foreach ( $lines as $line_type => $line ) { - - if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { - - $update_totals = true; - - foreach ( $data[ $line ] as $item ) { - // Item ID is always required. - if ( ! array_key_exists( 'id', $item ) ) { - $item['id'] = null; - } - - // Create item. - if ( is_null( $item['id'] ) ) { - $this->set_item( $order, $line_type, $item, 'create' ); - } elseif ( $this->item_is_null( $item ) ) { - // Delete item. - wc_delete_order_item( $item['id'] ); - } else { - // Update item. - $this->set_item( $order, $line_type, $item, 'update' ); - } - } - } - } - - // Payment method (and payment_complete() if `paid` == true and order needs payment). - if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { - - // Method ID. - if ( isset( $data['payment_details']['method_id'] ) ) { - update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); - } - - // Method title. - if ( isset( $data['payment_details']['method_title'] ) ) { - update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); - } - - // Mark as paid if set. - if ( $order->needs_payment() && isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { - $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); - } - } - - // Set order currency. - if ( isset( $data['currency'] ) ) { - if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); - } - - update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); - } - - // If items have changed, recalculate order totals. - if ( $update_totals ) { - $order->calculate_totals(); - } - - // Update order meta. - if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { - $this->set_order_meta( $order->get_id(), $data['order_meta'] ); - } - - // Update the order post to set customer note/modified date. - wc_update_order( $order_args ); - - // Order status. - if ( ! empty( $data['status'] ) ) { - // Refresh the order instance. - $order = wc_get_order( $order->get_id() ); - $order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true ); - } - - wc_delete_shop_order_transients( $order ); - - do_action( 'woocommerce_api_edit_order', $order->get_id(), $data, $this ); - do_action( 'woocommerce_update_order', $order->get_id() ); - - return $this->get_order( $id ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete an order - * - * @param int $id the order ID - * @param bool $force true to permanently delete order, false to move to trash - * @return array|WP_Error - */ - public function delete_order( $id, $force = false ) { - - $id = $this->validate_request( $id, $this->post_type, 'delete' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - wc_delete_shop_order_transients( $id ); - - do_action( 'woocommerce_api_delete_order', $id, $this ); - - return $this->delete( $id, 'order', ( 'true' === $force ) ); - } - - /** - * Helper method to get order post objects - * - * @since 2.1 - * @param array $args request arguments for filtering query - * @return WP_Query - */ - protected function query_orders( $args ) { - - // set base query arguments - $query_args = array( - 'fields' => 'ids', - 'post_type' => $this->post_type, - 'post_status' => array_keys( wc_get_order_statuses() ), - ); - - // add status argument - if ( ! empty( $args['status'] ) ) { - $statuses = 'wc-' . str_replace( ',', ',wc-', $args['status'] ); - $statuses = explode( ',', $statuses ); - $query_args['post_status'] = $statuses; - - unset( $args['status'] ); - } - - if ( ! empty( $args['customer_id'] ) ) { - $query_args['meta_query'] = array( - array( - 'key' => '_customer_user', - 'value' => absint( $args['customer_id'] ), - 'compare' => '=', - ), - ); - } - - $query_args = $this->merge_query_args( $query_args, $args ); - - return new WP_Query( $query_args ); - } - - /** - * Helper method to set/update the billing & shipping addresses for - * an order - * - * @since 2.1 - * @param \WC_Order $order - * @param array $data - */ - protected function set_order_addresses( $order, $data ) { - - $address_fields = array( - 'first_name', - 'last_name', - 'company', - 'email', - 'phone', - 'address_1', - 'address_2', - 'city', - 'state', - 'postcode', - 'country', - ); - - $billing_address = $shipping_address = array(); - - // billing address - if ( isset( $data['billing_address'] ) && is_array( $data['billing_address'] ) ) { - - foreach ( $address_fields as $field ) { - - if ( isset( $data['billing_address'][ $field ] ) ) { - $billing_address[ $field ] = wc_clean( $data['billing_address'][ $field ] ); - } - } - - unset( $address_fields['email'] ); - unset( $address_fields['phone'] ); - } - - // shipping address - if ( isset( $data['shipping_address'] ) && is_array( $data['shipping_address'] ) ) { - - foreach ( $address_fields as $field ) { - - if ( isset( $data['shipping_address'][ $field ] ) ) { - $shipping_address[ $field ] = wc_clean( $data['shipping_address'][ $field ] ); - } - } - } - - $this->update_address( $order, $billing_address, 'billing' ); - $this->update_address( $order, $shipping_address, 'shipping' ); - - // update user meta - if ( $order->get_user_id() ) { - foreach ( $billing_address as $key => $value ) { - update_user_meta( $order->get_user_id(), 'billing_' . $key, $value ); - } - foreach ( $shipping_address as $key => $value ) { - update_user_meta( $order->get_user_id(), 'shipping_' . $key, $value ); - } - } - } - - /** - * Update address. - * - * @param WC_Order $order - * @param array $posted - * @param string $type - */ - protected function update_address( $order, $posted, $type = 'billing' ) { - foreach ( $posted as $key => $value ) { - if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { - $order->{"set_{$type}_{$key}"}( $value ); - } - } - } - - /** - * Helper method to add/update order meta, with two restrictions: - * - * 1) Only non-protected meta (no leading underscore) can be set - * 2) Meta values must be scalar (int, string, bool) - * - * @since 2.2 - * @param int $order_id valid order ID - * @param array $order_meta order meta in array( 'meta_key' => 'meta_value' ) format - */ - protected function set_order_meta( $order_id, $order_meta ) { - - foreach ( $order_meta as $meta_key => $meta_value ) { - - if ( is_string( $meta_key ) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) { - update_post_meta( $order_id, $meta_key, $meta_value ); - } - } - } - - /** - * Helper method to check if the resource ID associated with the provided item is null - * - * Items can be deleted by setting the resource ID to null - * - * @since 2.2 - * @param array $item item provided in the request body - * @return bool true if the item resource ID is null, false otherwise - */ - protected function item_is_null( $item ) { - - $keys = array( 'product_id', 'method_id', 'title', 'code' ); - - foreach ( $keys as $key ) { - if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { - return true; - } - } - - return false; - } - - /** - * Wrapper method to create/update order items - * - * When updating, the item ID provided is checked to ensure it is associated - * with the order. - * - * @since 2.2 - * @param \WC_Order $order order - * @param string $item_type - * @param array $item item provided in the request body - * @param string $action either 'create' or 'update' - * @throws WC_API_Exception if item ID is not associated with order - */ - protected function set_item( $order, $item_type, $item, $action ) { - global $wpdb; - - $set_method = "set_{$item_type}"; - - // verify provided line item ID is associated with order - if ( 'update' === $action ) { - - $result = $wpdb->get_row( - $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", - absint( $item['id'] ), - absint( $order->get_id() ) - ) ); - - if ( is_null( $result ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); - } - } - - $this->$set_method( $order, $item, $action ); - } - - /** - * Create or update a line item - * - * @since 2.2 - * @param \WC_Order $order - * @param array $item line item data - * @param string $action 'create' to add line item or 'update' to update it - * @throws WC_API_Exception invalid data, server error - */ - protected function set_line_item( $order, $item, $action ) { - $creating = ( 'create' === $action ); - - // product is always required - if ( ! isset( $item['product_id'] ) && ! isset( $item['sku'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID or SKU is required', 'woocommerce' ), 400 ); - } - - // when updating, ensure product ID provided matches - if ( 'update' === $action ) { - - $item_product_id = wc_get_order_item_meta( $item['id'], '_product_id' ); - $item_variation_id = wc_get_order_item_meta( $item['id'], '_variation_id' ); - - if ( $item['product_id'] != $item_product_id && $item['product_id'] != $item_variation_id ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID provided does not match this line item', 'woocommerce' ), 400 ); - } - } - - if ( isset( $item['product_id'] ) ) { - $product_id = $item['product_id']; - } elseif ( isset( $item['sku'] ) ) { - $product_id = wc_get_product_id_by_sku( $item['sku'] ); - } - - // variations must each have a key & value - $variation_id = 0; - if ( isset( $item['variations'] ) && is_array( $item['variations'] ) ) { - foreach ( $item['variations'] as $key => $value ) { - if ( ! $key || ! $value ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_variation', __( 'The product variation is invalid', 'woocommerce' ), 400 ); - } - } - $variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item['variations'] ); - } - - $product = wc_get_product( $variation_id ? $variation_id : $product_id ); - - // must be a valid WC_Product - if ( ! is_object( $product ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 ); - } - - // quantity must be positive float - if ( isset( $item['quantity'] ) && floatval( $item['quantity'] ) <= 0 ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity must be a positive float.', 'woocommerce' ), 400 ); - } - - // quantity is required when creating - if ( $creating && ! isset( $item['quantity'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 ); - } - - // quantity - if ( $creating ) { - $line_item = new WC_Order_Item_Product(); - } else { - $line_item = new WC_Order_Item_Product( $item['id'] ); - } - - $line_item->set_product( $product ); - $line_item->set_order_id( $order->get_id() ); - - if ( isset( $item['quantity'] ) ) { - $line_item->set_quantity( $item['quantity'] ); - } - if ( isset( $item['total'] ) ) { - $line_item->set_total( floatval( $item['total'] ) ); - } elseif ( $creating ) { - $total = wc_get_price_excluding_tax( $product, array( 'qty' => $line_item->get_quantity() ) ); - $line_item->set_total( $total ); - $line_item->set_subtotal( $total ); - } - if ( isset( $item['total_tax'] ) ) { - $line_item->set_total_tax( floatval( $item['total_tax'] ) ); - } - if ( isset( $item['subtotal'] ) ) { - $line_item->set_subtotal( floatval( $item['subtotal'] ) ); - } - if ( isset( $item['subtotal_tax'] ) ) { - $line_item->set_subtotal_tax( floatval( $item['subtotal_tax'] ) ); - } - if ( $variation_id ) { - $line_item->set_variation_id( $variation_id ); - $line_item->set_variation( $item['variations'] ); - } - - // Save or add to order. - if ( $creating ) { - $order->add_item( $line_item ); - } else { - $item_id = $line_item->save(); - - if ( ! $item_id ) { - throw new WC_API_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 ); - } - } - } - - /** - * Given a product ID & API provided variations, find the correct variation ID to use for calculation - * We can't just trust input from the API to pass a variation_id manually, otherwise you could pass - * the cheapest variation ID but provide other information so we have to look up the variation ID. - * - * @param WC_Product $product Product instance - * @param array $variations - * - * @return int Returns an ID if a valid variation was found for this product - */ - public function get_variation_id( $product, $variations = array() ) { - $variation_id = null; - $variations_normalized = array(); - - if ( $product->is_type( 'variable' ) && $product->has_child() ) { - if ( isset( $variations ) && is_array( $variations ) ) { - // start by normalizing the passed variations - foreach ( $variations as $key => $value ) { - $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); // from get_attributes in class-wc-api-products.php - $variations_normalized[ $key ] = strtolower( $value ); - } - // now search through each product child and see if our passed variations match anything - foreach ( $product->get_children() as $variation ) { - $meta = array(); - foreach ( get_post_meta( $variation ) as $key => $value ) { - $value = $value[0]; - $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); - $meta[ $key ] = strtolower( $value ); - } - // if the variation array is a part of the $meta array, we found our match - if ( $this->array_contains( $variations_normalized, $meta ) ) { - $variation_id = $variation; - break; - } - } - } - } - - return $variation_id; - } - - /** - * Utility function to see if the meta array contains data from variations - * - * @param array $needles - * @param array $haystack - * - * @return bool - */ - protected function array_contains( $needles, $haystack ) { - foreach ( $needles as $key => $value ) { - if ( $haystack[ $key ] !== $value ) { - return false; - } - } - return true; - } - - /** - * Create or update an order shipping method - * - * @since 2.2 - * @param \WC_Order $order - * @param array $shipping item data - * @param string $action 'create' to add shipping or 'update' to update it - * @throws WC_API_Exception invalid data, server error - */ - protected function set_shipping( $order, $shipping, $action ) { - - // total must be a positive float - if ( isset( $shipping['total'] ) && floatval( $shipping['total'] ) < 0 ) { - throw new WC_API_Exception( 'woocommerce_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 ); - } - - if ( 'create' === $action ) { - - // method ID is required - if ( ! isset( $shipping['method_id'] ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); - } - - $rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] ); - $item = new WC_Order_Item_Shipping(); - $item->set_order_id( $order->get_id() ); - $item->set_shipping_rate( $rate ); - $order->add_item( $item ); - } else { - - $item = new WC_Order_Item_Shipping( $shipping['id'] ); - - if ( isset( $shipping['method_id'] ) ) { - $item->set_method_id( $shipping['method_id'] ); - } - - if ( isset( $shipping['method_title'] ) ) { - $item->set_method_title( $shipping['method_title'] ); - } - - if ( isset( $shipping['total'] ) ) { - $item->set_total( floatval( $shipping['total'] ) ); - } - - $shipping_id = $item->save(); - - if ( ! $shipping_id ) { - throw new WC_API_Exception( 'woocommerce_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 ); - } - } - } - - /** - * Create or update an order fee - * - * @since 2.2 - * @param \WC_Order $order - * @param array $fee item data - * @param string $action 'create' to add fee or 'update' to update it - * @throws WC_API_Exception invalid data, server error - */ - protected function set_fee( $order, $fee, $action ) { - - if ( 'create' === $action ) { - - // fee title is required - if ( ! isset( $fee['title'] ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee title is required', 'woocommerce' ), 400 ); - } - - $item = new WC_Order_Item_Fee(); - $item->set_order_id( $order->get_id() ); - $item->set_name( wc_clean( $fee['title'] ) ); - $item->set_total( isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0 ); - - // if taxable, tax class and total are required - if ( ! empty( $fee['taxable'] ) ) { - if ( ! isset( $fee['tax_class'] ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 ); - } - - $item->set_tax_status( 'taxable' ); - $item->set_tax_class( $fee['tax_class'] ); - - if ( isset( $fee['total_tax'] ) ) { - $item->set_total_tax( isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0 ); - } - - if ( isset( $fee['tax_data'] ) ) { - $item->set_total_tax( wc_format_refund_total( array_sum( $fee['tax_data'] ) ) ); - $item->set_taxes( array_map( 'wc_format_refund_total', $fee['tax_data'] ) ); - } - } - - $order->add_item( $item ); - } else { - - $item = new WC_Order_Item_Fee( $fee['id'] ); - - if ( isset( $fee['title'] ) ) { - $item->set_name( wc_clean( $fee['title'] ) ); - } - - if ( isset( $fee['tax_class'] ) ) { - $item->set_tax_class( $fee['tax_class'] ); - } - - if ( isset( $fee['total'] ) ) { - $item->set_total( floatval( $fee['total'] ) ); - } - - if ( isset( $fee['total_tax'] ) ) { - $item->set_total_tax( floatval( $fee['total_tax'] ) ); - } - - $fee_id = $item->save(); - - if ( ! $fee_id ) { - throw new WC_API_Exception( 'woocommerce_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 ); - } - } - } - - /** - * Create or update an order coupon - * - * @since 2.2 - * @param \WC_Order $order - * @param array $coupon item data - * @param string $action 'create' to add coupon or 'update' to update it - * @throws WC_API_Exception invalid data, server error - */ - protected function set_coupon( $order, $coupon, $action ) { - - // coupon amount must be positive float - if ( isset( $coupon['amount'] ) && floatval( $coupon['amount'] ) < 0 ) { - throw new WC_API_Exception( 'woocommerce_invalid_coupon_total', __( 'Coupon discount total must be a positive amount.', 'woocommerce' ), 400 ); - } - - if ( 'create' === $action ) { - - // coupon code is required - if ( empty( $coupon['code'] ) ) { - throw new WC_API_Exception( 'woocommerce_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); - } - - $item = new WC_Order_Item_Coupon(); - $item->set_props( array( - 'code' => $coupon['code'], - 'discount' => isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0, - 'discount_tax' => 0, - 'order_id' => $order->get_id(), - ) ); - $order->add_item( $item ); - } else { - - $item = new WC_Order_Item_Coupon( $coupon['id'] ); - - if ( isset( $coupon['code'] ) ) { - $item->set_code( $coupon['code'] ); - } - - if ( isset( $coupon['amount'] ) ) { - $item->set_discount( floatval( $coupon['amount'] ) ); - } - - $coupon_id = $item->save(); - - if ( ! $coupon_id ) { - throw new WC_API_Exception( 'woocommerce_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 ); - } - } - } - - /** - * Get the admin order notes for an order - * - * @since 2.1 - * @param string $order_id order ID - * @param string|null $fields fields to include in response - * @return array|WP_Error - */ - public function get_order_notes( $order_id, $fields = null ) { - - // ensure ID is valid order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $args = array( - 'post_id' => $order_id, - 'approve' => 'approve', - 'type' => 'order_note', - ); - - remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); - - $notes = get_comments( $args ); - - add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); - - $order_notes = array(); - - foreach ( $notes as $note ) { - - $order_notes[] = current( $this->get_order_note( $order_id, $note->comment_ID, $fields ) ); - } - - return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $order_id, $fields, $notes, $this->server ) ); - } - - /** - * Get an order note for the given order ID and ID - * - * @since 2.2 - * - * @param string $order_id order ID - * @param string $id order note ID - * @param string|null $fields fields to limit response to - * - * @return array|WP_Error - */ - public function get_order_note( $order_id, $id, $fields = null ) { - try { - // Validate order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); - } - - $note = get_comment( $id ); - - if ( is_null( $note ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $order_note = array( - 'id' => $note->comment_ID, - 'created_at' => $this->server->format_datetime( $note->comment_date_gmt ), - 'note' => $note->comment_content, - 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), - ); - - return array( 'order_note' => apply_filters( 'woocommerce_api_order_note_response', $order_note, $id, $fields, $note, $order_id, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Create a new order note for the given order - * - * @since 2.2 - * @param string $order_id order ID - * @param array $data raw request data - * @return WP_Error|array error or created note response data - */ - public function create_order_note( $order_id, $data ) { - try { - if ( ! isset( $data['order_note'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_note' ), 400 ); - } - - $data = $data['order_note']; - - // permission check - if ( ! current_user_can( 'publish_shop_orders' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_note', __( 'You do not have permission to create order notes', 'woocommerce' ), 401 ); - } - - $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $order = wc_get_order( $order_id ); - - $data = apply_filters( 'woocommerce_api_create_order_note_data', $data, $order_id, $this ); - - // note content is required - if ( ! isset( $data['note'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note', __( 'Order note is required', 'woocommerce' ), 400 ); - } - - $is_customer_note = ( isset( $data['customer_note'] ) && true === $data['customer_note'] ); - - // create the note - $note_id = $order->add_order_note( $data['note'], $is_customer_note ); - - if ( ! $note_id ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), 500 ); - } - - // HTTP 201 Created - $this->server->send_status( 201 ); - - do_action( 'woocommerce_api_create_order_note', $note_id, $order_id, $this ); - - return $this->get_order_note( $order->get_id(), $note_id ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit the order note - * - * @since 2.2 - * @param string $order_id order ID - * @param string $id note ID - * @param array $data parsed request data - * @return WP_Error|array error or edited note response data - */ - public function edit_order_note( $order_id, $id, $data ) { - try { - if ( ! isset( $data['order_note'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_note' ), 400 ); - } - - $data = $data['order_note']; - - // Validate order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $order = wc_get_order( $order_id ); - - // Validate note ID - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); - } - - // Ensure note ID is valid - $note = get_comment( $id ); - - if ( is_null( $note ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - // Ensure note ID is associated with given order - if ( $note->comment_post_ID != $order->get_id() ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); - } - - $data = apply_filters( 'woocommerce_api_edit_order_note_data', $data, $note->comment_ID, $order->get_id(), $this ); - - // Note content - if ( isset( $data['note'] ) ) { - - wp_update_comment( - array( - 'comment_ID' => $note->comment_ID, - 'comment_content' => $data['note'], - ) - ); - } - - // Customer note - if ( isset( $data['customer_note'] ) ) { - - update_comment_meta( $note->comment_ID, 'is_customer_note', true === $data['customer_note'] ? 1 : 0 ); - } - - do_action( 'woocommerce_api_edit_order_note', $note->comment_ID, $order->get_id(), $this ); - - return $this->get_order_note( $order->get_id(), $note->comment_ID ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete order note - * - * @since 2.2 - * @param string $order_id order ID - * @param string $id note ID - * @return WP_Error|array error or deleted message - */ - public function delete_order_note( $order_id, $id ) { - try { - $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - // Validate note ID - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); - } - - // Ensure note ID is valid - $note = get_comment( $id ); - - if ( is_null( $note ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - // Ensure note ID is associated with given order - if ( $note->comment_post_ID != $order_id ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); - } - - // Force delete since trashed order notes could not be managed through comments list table - $result = wc_delete_order_note( $note->comment_ID ); - - if ( ! $result ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_delete_order_note', __( 'This order note cannot be deleted', 'woocommerce' ), 500 ); - } - - do_action( 'woocommerce_api_delete_order_note', $note->comment_ID, $order_id, $this ); - - return array( 'message' => __( 'Permanently deleted order note', 'woocommerce' ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get the order refunds for an order - * - * @since 2.2 - * @param string $order_id order ID - * @param string|null $fields fields to include in response - * @return array|WP_Error - */ - public function get_order_refunds( $order_id, $fields = null ) { - - // Ensure ID is valid order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $refund_items = wc_get_orders( array( - 'type' => 'shop_order_refund', - 'parent' => $order_id, - 'limit' => -1, - 'return' => 'ids', - ) ); - $order_refunds = array(); - - foreach ( $refund_items as $refund_id ) { - $order_refunds[] = current( $this->get_order_refund( $order_id, $refund_id, $fields ) ); - } - - return array( 'order_refunds' => apply_filters( 'woocommerce_api_order_refunds_response', $order_refunds, $order_id, $fields, $refund_items, $this ) ); - } - - /** - * Get an order refund for the given order ID and ID - * - * @since 2.2 - * - * @param string $order_id order ID - * @param int $id - * @param string|null $fields fields to limit response to - * @param array $filter - * - * @return array|WP_Error - */ - public function get_order_refund( $order_id, $id, $fields = null, $filter = array() ) { - try { - // Validate order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); - } - - $order = wc_get_order( $order_id ); - $refund = wc_get_order( $id ); - - if ( ! $refund ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); - } - - $line_items = array(); - - // Add line items - foreach ( $refund->get_items( 'line_item' ) as $item_id => $item ) { - $product = $item->get_product(); - $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; - $item_meta = $item->get_formatted_meta_data( $hideprefix ); - - foreach ( $item_meta as $key => $values ) { - $item_meta[ $key ]->label = $values->display_key; - unset( $item_meta[ $key ]->display_key ); - unset( $item_meta[ $key ]->display_value ); - } - - $line_items[] = array( - 'id' => $item_id, - 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ), - 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), 2 ), - 'total' => wc_format_decimal( $order->get_line_total( $item ), 2 ), - 'total_tax' => wc_format_decimal( $order->get_line_tax( $item ), 2 ), - 'price' => wc_format_decimal( $order->get_item_total( $item ), 2 ), - 'quantity' => $item->get_quantity(), - 'tax_class' => $item->get_tax_class(), - 'name' => $item->get_name(), - 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), - 'sku' => is_object( $product ) ? $product->get_sku() : null, - 'meta' => array_values( $item_meta ), - 'refunded_item_id' => (int) $item->get_meta( 'refunded_item_id' ), - ); - } - - $order_refund = array( - 'id' => $refund->get_id(), - 'created_at' => $this->server->format_datetime( $refund->get_date_created() ? $refund->get_date_created()->getTimestamp() : 0, false, false ), - 'amount' => wc_format_decimal( $refund->get_amount(), 2 ), - 'reason' => $refund->get_reason(), - 'line_items' => $line_items, - ); - - return array( 'order_refund' => apply_filters( 'woocommerce_api_order_refund_response', $order_refund, $id, $fields, $refund, $order_id, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Create a new order refund for the given order - * - * @since 2.2 - * @param string $order_id order ID - * @param array $data raw request data - * @param bool $api_refund do refund using a payment gateway API - * @return WP_Error|array error or created refund response data - */ - public function create_order_refund( $order_id, $data, $api_refund = true ) { - try { - if ( ! isset( $data['order_refund'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_refund' ), 400 ); - } - - $data = $data['order_refund']; - - // Permission check - if ( ! current_user_can( 'publish_shop_orders' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_refund', __( 'You do not have permission to create order refunds', 'woocommerce' ), 401 ); - } - - $order_id = absint( $order_id ); - - if ( empty( $order_id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); - } - - $data = apply_filters( 'woocommerce_api_create_order_refund_data', $data, $order_id, $this ); - - // Refund amount is required - if ( ! isset( $data['amount'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount is required.', 'woocommerce' ), 400 ); - } elseif ( 0 > $data['amount'] ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount must be positive.', 'woocommerce' ), 400 ); - } - - $data['order_id'] = $order_id; - $data['refund_id'] = 0; - - // Create the refund - $refund = wc_create_refund( $data ); - - if ( ! $refund ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); - } - - // Refund via API - if ( $api_refund ) { - if ( WC()->payment_gateways() ) { - $payment_gateways = WC()->payment_gateways->payment_gateways(); - } - - $order = wc_get_order( $order_id ); - - if ( isset( $payment_gateways[ $order->get_payment_method() ] ) && $payment_gateways[ $order->get_payment_method() ]->supports( 'refunds' ) ) { - $result = $payment_gateways[ $order->get_payment_method() ]->process_refund( $order_id, $refund->get_amount(), $refund->get_reason() ); - - if ( is_wp_error( $result ) ) { - return $result; - } elseif ( ! $result ) { - throw new WC_API_Exception( 'woocommerce_api_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 ); - } - } - } - - // HTTP 201 Created - $this->server->send_status( 201 ); - - do_action( 'woocommerce_api_create_order_refund', $refund->get_id(), $order_id, $this ); - - return $this->get_order_refund( $order_id, $refund->get_id() ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit an order refund - * - * @since 2.2 - * @param string $order_id order ID - * @param string $id refund ID - * @param array $data parsed request data - * @return WP_Error|array error or edited refund response data - */ - public function edit_order_refund( $order_id, $id, $data ) { - try { - if ( ! isset( $data['order_refund'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_refund' ), 400 ); - } - - $data = $data['order_refund']; - - // Validate order ID - $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - // Validate refund ID - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); - } - - // Ensure order ID is valid - $refund = get_post( $id ); - - if ( ! $refund ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); - } - - // Ensure refund ID is associated with given order - if ( $refund->post_parent != $order_id ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); - } - - $data = apply_filters( 'woocommerce_api_edit_order_refund_data', $data, $refund->ID, $order_id, $this ); - - // Update reason - if ( isset( $data['reason'] ) ) { - $updated_refund = wp_update_post( array( 'ID' => $refund->ID, 'post_excerpt' => $data['reason'] ) ); - - if ( is_wp_error( $updated_refund ) ) { - return $updated_refund; - } - } - - // Update refund amount - if ( isset( $data['amount'] ) && 0 < $data['amount'] ) { - update_post_meta( $refund->ID, '_refund_amount', wc_format_decimal( $data['amount'] ) ); - } - - do_action( 'woocommerce_api_edit_order_refund', $refund->ID, $order_id, $this ); - - return $this->get_order_refund( $order_id, $refund->ID ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete order refund - * - * @since 2.2 - * @param string $order_id order ID - * @param string $id refund ID - * @return WP_Error|array error or deleted message - */ - public function delete_order_refund( $order_id, $id ) { - try { - $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); - - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - // Validate refund ID - $id = absint( $id ); - - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); - } - - // Ensure refund ID is valid - $refund = get_post( $id ); - - if ( ! $refund ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); - } - - // Ensure refund ID is associated with given order - if ( $refund->post_parent != $order_id ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); - } - - wc_delete_shop_order_transients( $order_id ); - - do_action( 'woocommerce_api_delete_order_refund', $refund->ID, $order_id, $this ); - - return $this->delete( $refund->ID, 'refund', true ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Bulk update or insert orders - * Accepts an array with orders in the formats supported by - * WC_API_Orders->create_order() and WC_API_Orders->edit_order() - * - * @since 2.4.0 - * - * @param array $data - * - * @return array|WP_Error - */ - public function bulk( $data ) { - - try { - if ( ! isset( $data['orders'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_orders_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'orders' ), 400 ); - } - - $data = $data['orders']; - $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'orders' ); - - // Limit bulk operation - if ( count( $data ) > $limit ) { - throw new WC_API_Exception( 'woocommerce_api_orders_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); - } - - $orders = array(); - - foreach ( $data as $_order ) { - $order_id = 0; - - // Try to get the order ID - if ( isset( $_order['id'] ) ) { - $order_id = intval( $_order['id'] ); - } - - if ( $order_id ) { - - // Order exists / edit order - $edit = $this->edit_order( $order_id, array( 'order' => $_order ) ); - - if ( is_wp_error( $edit ) ) { - $orders[] = array( - 'id' => $order_id, - 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), - ); - } else { - $orders[] = $edit['order']; - } - } else { - - // Order don't exists / create order - $new = $this->create_order( array( 'order' => $_order ) ); - - if ( is_wp_error( $new ) ) { - $orders[] = array( - 'id' => $order_id, - 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), - ); - } else { - $orders[] = $new['order']; - } - } - } - - return array( 'orders' => apply_filters( 'woocommerce_api_orders_bulk_response', $orders, $this ) ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } -} diff --git a/includes/legacy/api/v3/class-wc-api-products.php b/includes/legacy/api/v3/class-wc-api-products.php deleted file mode 100644 index cb224ffaee7..00000000000 --- a/includes/legacy/api/v3/class-wc-api-products.php +++ /dev/null @@ -1,3310 +0,0 @@ - - * GET /products//reviews - * - * @since 2.1 - * @param array $routes - * @return array - */ - public function register_routes( $routes ) { - - # GET/POST /products - $routes[ $this->base ] = array( - array( array( $this, 'get_products' ), WC_API_Server::READABLE ), - array( array( $this, 'create_product' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET /products/count - $routes[ $this->base . '/count' ] = array( - array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ), - ); - - # GET/PUT/DELETE /products/ - $routes[ $this->base . '/(?P\d+)' ] = array( - array( array( $this, 'get_product' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_product' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_product' ), WC_API_Server::DELETABLE ), - ); - - # GET /products//reviews - $routes[ $this->base . '/(?P\d+)/reviews' ] = array( - array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ), - ); - - # GET /products//orders - $routes[ $this->base . '/(?P\d+)/orders' ] = array( - array( array( $this, 'get_product_orders' ), WC_API_Server::READABLE ), - ); - - # GET/POST /products/categories - $routes[ $this->base . '/categories' ] = array( - array( array( $this, 'get_product_categories' ), WC_API_Server::READABLE ), - array( array( $this, 'create_product_category' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET/PUT/DELETE /products/categories/ - $routes[ $this->base . '/categories/(?P\d+)' ] = array( - array( array( $this, 'get_product_category' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_product_category' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_product_category' ), WC_API_Server::DELETABLE ), - ); - - # GET/POST /products/tags - $routes[ $this->base . '/tags' ] = array( - array( array( $this, 'get_product_tags' ), WC_API_Server::READABLE ), - array( array( $this, 'create_product_tag' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET/PUT/DELETE /products/tags/ - $routes[ $this->base . '/tags/(?P\d+)' ] = array( - array( array( $this, 'get_product_tag' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_product_tag' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_product_tag' ), WC_API_Server::DELETABLE ), - ); - - # GET/POST /products/shipping_classes - $routes[ $this->base . '/shipping_classes' ] = array( - array( array( $this, 'get_product_shipping_classes' ), WC_API_Server::READABLE ), - array( array( $this, 'create_product_shipping_class' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET/PUT/DELETE /products/shipping_classes/ - $routes[ $this->base . '/shipping_classes/(?P\d+)' ] = array( - array( array( $this, 'get_product_shipping_class' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_product_shipping_class' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_product_shipping_class' ), WC_API_Server::DELETABLE ), - ); - - # GET/POST /products/attributes - $routes[ $this->base . '/attributes' ] = array( - array( array( $this, 'get_product_attributes' ), WC_API_Server::READABLE ), - array( array( $this, 'create_product_attribute' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET/PUT/DELETE /products/attributes/ - $routes[ $this->base . '/attributes/(?P\d+)' ] = array( - array( array( $this, 'get_product_attribute' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_product_attribute' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_product_attribute' ), WC_API_Server::DELETABLE ), - ); - - # GET/POST /products/attributes//terms - $routes[ $this->base . '/attributes/(?P\d+)/terms' ] = array( - array( array( $this, 'get_product_attribute_terms' ), WC_API_Server::READABLE ), - array( array( $this, 'create_product_attribute_term' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET/PUT/DELETE /products/attributes//terms/ - $routes[ $this->base . '/attributes/(?P\d+)/terms/(?P\d+)' ] = array( - array( array( $this, 'get_product_attribute_term' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_product_attribute_term' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_product_attribute_term' ), WC_API_Server::DELETABLE ), - ); - - # POST|PUT /products/bulk - $routes[ $this->base . '/bulk' ] = array( - array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - ); - - return $routes; - } - - /** - * Get all products - * - * @since 2.1 - * @param string $fields - * @param string $type - * @param array $filter - * @param int $page - * @return array - */ - public function get_products( $fields = null, $type = null, $filter = array(), $page = 1 ) { - - if ( ! empty( $type ) ) { - $filter['type'] = $type; - } - - $filter['page'] = $page; - - $query = $this->query_products( $filter ); - - $products = array(); - - foreach ( $query->posts as $product_id ) { - - if ( ! $this->is_readable( $product_id ) ) { - continue; - } - - $products[] = current( $this->get_product( $product_id, $fields ) ); - } - - $this->server->add_pagination_headers( $query ); - - return array( 'products' => $products ); - } - - /** - * Get the product for the given ID - * - * @since 2.1 - * @param int $id the product ID - * @param string $fields - * @return array|WP_Error - */ - public function get_product( $id, $fields = null ) { - - $id = $this->validate_request( $id, 'product', 'read' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - $product = wc_get_product( $id ); - - // add data that applies to every product type - $product_data = $this->get_product_data( $product ); - - // add variations to variable products - if ( $product->is_type( 'variable' ) && $product->has_child() ) { - $product_data['variations'] = $this->get_variation_data( $product ); - } - - // add the parent product data to an individual variation - if ( $product->is_type( 'variation' ) && $product->get_parent_id() ) { - $product_data['parent'] = $this->get_product_data( $product->get_parent_id() ); - } - - // Add grouped products data - if ( $product->is_type( 'grouped' ) && $product->has_child() ) { - $product_data['grouped_products'] = $this->get_grouped_products_data( $product ); - } - - if ( $product->is_type( 'simple' ) ) { - $parent_id = $product->get_parent_id(); - if ( ! empty( $parent_id ) ) { - $_product = wc_get_product( $parent_id ); - $product_data['parent'] = $this->get_product_data( $_product ); - } - } - - return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) ); - } - - /** - * Get the total number of products - * - * @since 2.1 - * - * @param string $type - * @param array $filter - * - * @return array|WP_Error - */ - public function get_products_count( $type = null, $filter = array() ) { - try { - if ( ! current_user_can( 'read_private_products' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), 401 ); - } - - if ( ! empty( $type ) ) { - $filter['type'] = $type; - } - - $query = $this->query_products( $filter ); - - return array( 'count' => (int) $query->found_posts ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Create a new product. - * - * @since 2.2 - * - * @param array $data posted data - * - * @return array|WP_Error - */ - public function create_product( $data ) { - $id = 0; - - try { - if ( ! isset( $data['product'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product' ), 400 ); - } - - $data = $data['product']; - - // Check permissions. - if ( ! current_user_can( 'publish_products' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product', __( 'You do not have permission to create products', 'woocommerce' ), 401 ); - } - - $data = apply_filters( 'woocommerce_api_create_product_data', $data, $this ); - - // Check if product title is specified. - if ( ! isset( $data['title'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_title', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'title' ), 400 ); - } - - // Check product type. - if ( ! isset( $data['type'] ) ) { - $data['type'] = 'simple'; - } - - // Set visible visibility when not sent. - if ( ! isset( $data['catalog_visibility'] ) ) { - $data['catalog_visibility'] = 'visible'; - } - - // Validate the product type. - if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); - } - - // Enable description html tags. - $post_content = isset( $data['description'] ) ? wc_clean( $data['description'] ) : ''; - if ( $post_content && isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) { - - $post_content = wp_filter_post_kses( $data['description'] ); - } - - // Enable short description html tags. - $post_excerpt = isset( $data['short_description'] ) ? wc_clean( $data['short_description'] ) : ''; - if ( $post_excerpt && isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) { - $post_excerpt = wp_filter_post_kses( $data['short_description'] ); - } - - $classname = WC_Product_Factory::get_classname_from_product_type( $data['type'] ); - if ( ! class_exists( $classname ) ) { - $classname = 'WC_Product_Simple'; - } - $product = new $classname(); - - $product->set_name( wc_clean( $data['title'] ) ); - $product->set_status( isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish' ); - $product->set_short_description( isset( $data['short_description'] ) ? $post_excerpt : '' ); - $product->set_description( isset( $data['description'] ) ? $post_content : '' ); - $product->set_menu_order( isset( $data['menu_order'] ) ? intval( $data['menu_order'] ) : 0 ); - - if ( ! empty( $data['name'] ) ) { - $product->set_slug( sanitize_title( $data['name'] ) ); - } - - // Attempts to create the new product. - $product->save(); - $id = $product->get_id(); - - // Checks for an error in the product creation. - if ( 0 >= $id ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_product', $id->get_error_message(), 400 ); - } - - // Check for featured/gallery images, upload it and set it. - if ( isset( $data['images'] ) ) { - $product = $this->save_product_images( $product, $data['images'] ); - } - - // Save product meta fields. - $product = $this->save_product_meta( $product, $data ); - $product->save(); - - // Save variations. - if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) { - $this->save_variations( $product, $data ); - } - - do_action( 'woocommerce_api_create_product', $id, $data ); - - // Clear cache/transients. - wc_delete_product_transients( $id ); - - $this->server->send_status( 201 ); - - return $this->get_product( $id ); - } catch ( WC_Data_Exception $e ) { - $this->clear_product( $id ); - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } catch ( WC_API_Exception $e ) { - $this->clear_product( $id ); - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit a product - * - * @since 2.2 - * - * @param int $id the product ID - * @param array $data - * - * @return array|WP_Error - */ - public function edit_product( $id, $data ) { - try { - if ( ! isset( $data['product'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product' ), 400 ); - } - - $data = $data['product']; - - $id = $this->validate_request( $id, 'product', 'edit' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - $product = wc_get_product( $id ); - - $data = apply_filters( 'woocommerce_api_edit_product_data', $data, $this ); - - // Product title. - if ( isset( $data['title'] ) ) { - $product->set_name( wc_clean( $data['title'] ) ); - } - - // Product name (slug). - if ( isset( $data['name'] ) ) { - $product->set_slug( wc_clean( $data['name'] ) ); - } - - // Product status. - if ( isset( $data['status'] ) ) { - $product->set_status( wc_clean( $data['status'] ) ); - } - - // Product short description. - if ( isset( $data['short_description'] ) ) { - // Enable short description html tags. - $post_excerpt = ( isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) ? wp_filter_post_kses( $data['short_description'] ) : wc_clean( $data['short_description'] ); - $product->set_short_description( $post_excerpt ); - } - - // Product description. - if ( isset( $data['description'] ) ) { - // Enable description html tags. - $post_content = ( isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) ? wp_filter_post_kses( $data['description'] ) : wc_clean( $data['description'] ); - $product->set_description( $post_content ); - } - - // Validate the product type. - if ( isset( $data['type'] ) && ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); - } - - // Menu order. - if ( isset( $data['menu_order'] ) ) { - $product->set_menu_order( intval( $data['menu_order'] ) ); - } - - // Check for featured/gallery images, upload it and set it. - if ( isset( $data['images'] ) ) { - $product = $this->save_product_images( $product, $data['images'] ); - } - - // Save product meta fields. - $product = $this->save_product_meta( $product, $data ); - - // Save variations. - if ( $product->is_type( 'variable' ) ) { - if ( isset( $data['variations'] ) && is_array( $data['variations'] ) ) { - $this->save_variations( $product, $data ); - } else { - // Just sync variations. - $product = WC_Product_Variable::sync( $product, false ); - } - } - - $product->save(); - - do_action( 'woocommerce_api_edit_product', $id, $data ); - - // Clear cache/transients. - wc_delete_product_transients( $id ); - - return $this->get_product( $id ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete a product. - * - * @since 2.2 - * - * @param int $id the product ID. - * @param bool $force true to permanently delete order, false to move to trash. - * - * @return array|WP_Error - */ - public function delete_product( $id, $force = false ) { - - $id = $this->validate_request( $id, 'product', 'delete' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - $product = wc_get_product( $id ); - - do_action( 'woocommerce_api_delete_product', $id, $this ); - - // If we're forcing, then delete permanently. - if ( $force ) { - if ( $product->is_type( 'variable' ) ) { - foreach ( $product->get_children() as $child_id ) { - $child = wc_get_product( $child_id ); - if ( ! empty( $child ) ) { - $child->delete( true ); - } - } - } else { - // For other product types, if the product has children, remove the relationship. - foreach ( $product->get_children() as $child_id ) { - $child = wc_get_product( $child_id ); - if ( ! empty( $child ) ) { - $child->set_parent_id( 0 ); - $child->save(); - } - } - } - - $product->delete( true ); - $result = ! ( $product->get_id() > 0 ); - } else { - $product->delete(); - $result = 'trash' === $product->get_status(); - } - - if ( ! $result ) { - return new WP_Error( 'woocommerce_api_cannot_delete_product', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product' ), array( 'status' => 500 ) ); - } - - // Delete parent product transients. - if ( $parent_id = wp_get_post_parent_id( $id ) ) { - wc_delete_product_transients( $parent_id ); - } - - if ( $force ) { - return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), 'product' ) ); - } else { - $this->server->send_status( '202' ); - - return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product' ) ); - } - } - - /** - * Get the reviews for a product - * - * @since 2.1 - * @param int $id the product ID to get reviews for - * @param string $fields fields to include in response - * @return array|WP_Error - */ - public function get_product_reviews( $id, $fields = null ) { - - $id = $this->validate_request( $id, 'product', 'read' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - $comments = get_approved_comments( $id ); - $reviews = array(); - - foreach ( $comments as $comment ) { - - $reviews[] = array( - 'id' => intval( $comment->comment_ID ), - 'created_at' => $this->server->format_datetime( $comment->comment_date_gmt ), - 'review' => $comment->comment_content, - 'rating' => get_comment_meta( $comment->comment_ID, 'rating', true ), - 'reviewer_name' => $comment->comment_author, - 'reviewer_email' => $comment->comment_author_email, - 'verified' => wc_review_is_from_verified_owner( $comment->comment_ID ), - ); - } - - return array( 'product_reviews' => apply_filters( 'woocommerce_api_product_reviews_response', $reviews, $id, $fields, $comments, $this->server ) ); - } - - /** - * Get the orders for a product - * - * @since 2.4.0 - * @param int $id the product ID to get orders for - * @param string fields fields to retrieve - * @param array $filter filters to include in response - * @param string $status the order status to retrieve - * @param $page $page page to retrieve - * @return array|WP_Error - */ - public function get_product_orders( $id, $fields = null, $filter = array(), $status = null, $page = 1 ) { - global $wpdb; - - $id = $this->validate_request( $id, 'product', 'read' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - $order_ids = $wpdb->get_col( $wpdb->prepare( " - SELECT order_id - FROM {$wpdb->prefix}woocommerce_order_items - WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) - AND order_item_type = 'line_item' - ", $id ) ); - - if ( empty( $order_ids ) ) { - return array( 'orders' => array() ); - } - - $filter = array_merge( $filter, array( - 'in' => implode( ',', $order_ids ), - ) ); - - $orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, $status, $page ); - - return array( 'orders' => apply_filters( 'woocommerce_api_product_orders_response', $orders['orders'], $id, $filter, $fields, $this->server ) ); - } - - /** - * Get a listing of product categories - * - * @since 2.2 - * - * @param string|null $fields fields to limit response to - * - * @return array|WP_Error - */ - public function get_product_categories( $fields = null ) { - try { - // Permissions check - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); - } - - $product_categories = array(); - - $terms = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'ids' ) ); - - foreach ( $terms as $term_id ) { - $product_categories[] = current( $this->get_product_category( $term_id, $fields ) ); - } - - return array( 'product_categories' => apply_filters( 'woocommerce_api_product_categories_response', $product_categories, $terms, $fields, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get the product category for the given ID - * - * @since 2.2 - * - * @param string $id product category term ID - * @param string|null $fields fields to limit response to - * - * @return array|WP_Error - */ - public function get_product_category( $id, $fields = null ) { - try { - $id = absint( $id ); - - // Validate ID - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'Invalid product category ID', 'woocommerce' ), 400 ); - } - - // Permissions check - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); - } - - $term = get_term( $id, 'product_cat' ); - - if ( is_wp_error( $term ) || is_null( $term ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'A product category with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $term_id = intval( $term->term_id ); - - // Get category display type - $display_type = get_term_meta( $term_id, 'display_type', true ); - - // Get category image - $image = ''; - if ( $image_id = get_term_meta( $term_id, 'thumbnail_id', true ) ) { - $image = wp_get_attachment_url( $image_id ); - } - - $product_category = array( - 'id' => $term_id, - 'name' => $term->name, - 'slug' => $term->slug, - 'parent' => $term->parent, - 'description' => $term->description, - 'display' => $display_type ? $display_type : 'default', - 'image' => $image ? esc_url( $image ) : '', - 'count' => intval( $term->count ), - ); - - return array( 'product_category' => apply_filters( 'woocommerce_api_product_category_response', $product_category, $id, $fields, $term, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Create a new product category. - * - * @since 2.5.0 - * @param array $data Posted data - * @return array|WP_Error Product category if succeed, otherwise WP_Error - * will be returned - */ - public function create_product_category( $data ) { - global $wpdb; - - try { - if ( ! isset( $data['product_category'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_category_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_category' ), 400 ); - } - - // Check permissions - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_category', __( 'You do not have permission to create product categories', 'woocommerce' ), 401 ); - } - - $defaults = array( - 'name' => '', - 'slug' => '', - 'description' => '', - 'parent' => 0, - 'display' => 'default', - 'image' => '', - ); - - $data = wp_parse_args( $data['product_category'], $defaults ); - $data = apply_filters( 'woocommerce_api_create_product_category_data', $data, $this ); - - // Check parent. - $data['parent'] = absint( $data['parent'] ); - if ( $data['parent'] ) { - $parent = get_term_by( 'id', $data['parent'], 'product_cat' ); - if ( ! $parent ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_parent', __( 'Product category parent is invalid', 'woocommerce' ), 400 ); - } - } - - // If value of image is numeric, assume value as image_id. - $image = $data['image']; - $image_id = 0; - if ( is_numeric( $image ) ) { - $image_id = absint( $image ); - } elseif ( ! empty( $image ) ) { - $upload = $this->upload_product_category_image( esc_url_raw( $image ) ); - $image_id = $this->set_product_category_image_as_attachment( $upload ); - } - - $insert = wp_insert_term( $data['name'], 'product_cat', $data ); - if ( is_wp_error( $insert ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_category', $insert->get_error_message(), 400 ); - } - - $id = $insert['term_id']; - - update_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) ); - - // Check if image_id is a valid image attachment before updating the term meta. - if ( $image_id && wp_attachment_is_image( $image_id ) ) { - update_term_meta( $id, 'thumbnail_id', $image_id ); - } - - do_action( 'woocommerce_api_create_product_category', $id, $data ); - - $this->server->send_status( 201 ); - - return $this->get_product_category( $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit a product category. - * - * @since 2.5.0 - * @param int $id Product category term ID - * @param array $data Posted data - * @return array|WP_Error Product category if succeed, otherwise WP_Error - * will be returned - */ - public function edit_product_category( $id, $data ) { - global $wpdb; - - try { - if ( ! isset( $data['product_category'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_category', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_category' ), 400 ); - } - - $id = absint( $id ); - $data = $data['product_category']; - - // Check permissions. - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_category', __( 'You do not have permission to edit product categories', 'woocommerce' ), 401 ); - } - - $data = apply_filters( 'woocommerce_api_edit_product_category_data', $data, $this ); - $category = $this->get_product_category( $id ); - - if ( is_wp_error( $category ) ) { - return $category; - } - - if ( isset( $data['image'] ) ) { - $image_id = 0; - - // If value of image is numeric, assume value as image_id. - $image = $data['image']; - if ( is_numeric( $image ) ) { - $image_id = absint( $image ); - } elseif ( ! empty( $image ) ) { - $upload = $this->upload_product_category_image( esc_url_raw( $image ) ); - $image_id = $this->set_product_category_image_as_attachment( $upload ); - } - - // In case client supplies invalid image or wants to unset category image. - if ( ! wp_attachment_is_image( $image_id ) ) { - $image_id = ''; - } - } - - $update = wp_update_term( $id, 'product_cat', $data ); - if ( is_wp_error( $update ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_catgory', __( 'Could not edit the category', 'woocommerce' ), 400 ); - } - - if ( ! empty( $data['display'] ) ) { - update_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) ); - } - - if ( isset( $image_id ) ) { - update_term_meta( $id, 'thumbnail_id', $image_id ); - } - - do_action( 'woocommerce_api_edit_product_category', $id, $data ); - - return $this->get_product_category( $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete a product category. - * - * @since 2.5.0 - * @param int $id Product category term ID - * @return array|WP_Error Success message if succeed, otherwise WP_Error - * will be returned - */ - public function delete_product_category( $id ) { - global $wpdb; - - try { - // Check permissions - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_category', __( 'You do not have permission to delete product category', 'woocommerce' ), 401 ); - } - - $id = absint( $id ); - $deleted = wp_delete_term( $id, 'product_cat' ); - if ( ! $deleted || is_wp_error( $deleted ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_category', __( 'Could not delete the category', 'woocommerce' ), 401 ); - } - - do_action( 'woocommerce_api_delete_product_category', $id, $this ); - - return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_category' ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get a listing of product tags. - * - * @since 2.5.0 - * - * @param string|null $fields Fields to limit response to - * - * @return array|WP_Error - */ - public function get_product_tags( $fields = null ) { - try { - // Permissions check - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_tags', __( 'You do not have permission to read product tags', 'woocommerce' ), 401 ); - } - - $product_tags = array(); - - $terms = get_terms( 'product_tag', array( 'hide_empty' => false, 'fields' => 'ids' ) ); - - foreach ( $terms as $term_id ) { - $product_tags[] = current( $this->get_product_tag( $term_id, $fields ) ); - } - - return array( 'product_tags' => apply_filters( 'woocommerce_api_product_tags_response', $product_tags, $terms, $fields, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get the product tag for the given ID. - * - * @since 2.5.0 - * - * @param string $id Product tag term ID - * @param string|null $fields Fields to limit response to - * - * @return array|WP_Error - */ - public function get_product_tag( $id, $fields = null ) { - try { - $id = absint( $id ); - - // Validate ID - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_tag_id', __( 'Invalid product tag ID', 'woocommerce' ), 400 ); - } - - // Permissions check - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_tags', __( 'You do not have permission to read product tags', 'woocommerce' ), 401 ); - } - - $term = get_term( $id, 'product_tag' ); - - if ( is_wp_error( $term ) || is_null( $term ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_tag_id', __( 'A product tag with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $term_id = intval( $term->term_id ); - - $tag = array( - 'id' => $term_id, - 'name' => $term->name, - 'slug' => $term->slug, - 'description' => $term->description, - 'count' => intval( $term->count ), - ); - - return array( 'product_tag' => apply_filters( 'woocommerce_api_product_tag_response', $tag, $id, $fields, $term, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Create a new product tag. - * - * @since 2.5.0 - * @param array $data Posted data - * @return array|WP_Error Product tag if succeed, otherwise WP_Error - * will be returned - */ - public function create_product_tag( $data ) { - try { - if ( ! isset( $data['product_tag'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_tag_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_tag' ), 400 ); - } - - // Check permissions - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_tag', __( 'You do not have permission to create product tags', 'woocommerce' ), 401 ); - } - - $defaults = array( - 'name' => '', - 'slug' => '', - 'description' => '', - ); - - $data = wp_parse_args( $data['product_tag'], $defaults ); - $data = apply_filters( 'woocommerce_api_create_product_tag_data', $data, $this ); - - $insert = wp_insert_term( $data['name'], 'product_tag', $data ); - if ( is_wp_error( $insert ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_tag', $insert->get_error_message(), 400 ); - } - $id = $insert['term_id']; - - do_action( 'woocommerce_api_create_product_tag', $id, $data ); - - $this->server->send_status( 201 ); - - return $this->get_product_tag( $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit a product tag. - * - * @since 2.5.0 - * @param int $id Product tag term ID - * @param array $data Posted data - * @return array|WP_Error Product tag if succeed, otherwise WP_Error - * will be returned - */ - public function edit_product_tag( $id, $data ) { - try { - if ( ! isset( $data['product_tag'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_tag', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_tag' ), 400 ); - } - - $id = absint( $id ); - $data = $data['product_tag']; - - // Check permissions. - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_tag', __( 'You do not have permission to edit product tags', 'woocommerce' ), 401 ); - } - - $data = apply_filters( 'woocommerce_api_edit_product_tag_data', $data, $this ); - $tag = $this->get_product_tag( $id ); - - if ( is_wp_error( $tag ) ) { - return $tag; - } - - $update = wp_update_term( $id, 'product_tag', $data ); - if ( is_wp_error( $update ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_tag', __( 'Could not edit the tag', 'woocommerce' ), 400 ); - } - - do_action( 'woocommerce_api_edit_product_tag', $id, $data ); - - return $this->get_product_tag( $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete a product tag. - * - * @since 2.5.0 - * @param int $id Product tag term ID - * @return array|WP_Error Success message if succeed, otherwise WP_Error - * will be returned - */ - public function delete_product_tag( $id ) { - try { - // Check permissions - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_tag', __( 'You do not have permission to delete product tag', 'woocommerce' ), 401 ); - } - - $id = absint( $id ); - $deleted = wp_delete_term( $id, 'product_tag' ); - if ( ! $deleted || is_wp_error( $deleted ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_tag', __( 'Could not delete the tag', 'woocommerce' ), 401 ); - } - - do_action( 'woocommerce_api_delete_product_tag', $id, $this ); - - return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_tag' ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Helper method to get product post objects - * - * @since 2.1 - * @param array $args request arguments for filtering query - * @return WP_Query - */ - private function query_products( $args ) { - - // Set base query arguments - $query_args = array( - 'fields' => 'ids', - 'post_type' => 'product', - 'post_status' => 'publish', - 'meta_query' => array(), - ); - - // Taxonomy query to filter products by type, category, tag, shipping class, and - // attribute. - $tax_query = array(); - - // Map between taxonomy name and arg's key. - $taxonomies_arg_map = array( - 'product_type' => 'type', - 'product_cat' => 'category', - 'product_tag' => 'tag', - 'product_shipping_class' => 'shipping_class', - ); - - // Add attribute taxonomy names into the map. - foreach ( wc_get_attribute_taxonomy_names() as $attribute_name ) { - $taxonomies_arg_map[ $attribute_name ] = $attribute_name; - } - - // Set tax_query for each passed arg. - foreach ( $taxonomies_arg_map as $tax_name => $arg ) { - if ( ! empty( $args[ $arg ] ) ) { - $terms = explode( ',', $args[ $arg ] ); - - $tax_query[] = array( - 'taxonomy' => $tax_name, - 'field' => 'slug', - 'terms' => $terms, - ); - - unset( $args[ $arg ] ); - } - } - - if ( ! empty( $tax_query ) ) { - $query_args['tax_query'] = $tax_query; - } - - // Filter by specific sku - if ( ! empty( $args['sku'] ) ) { - if ( ! is_array( $query_args['meta_query'] ) ) { - $query_args['meta_query'] = array(); - } - - $query_args['meta_query'][] = array( - 'key' => '_sku', - 'value' => $args['sku'], - 'compare' => '=', - ); - - $query_args['post_type'] = array( 'product', 'product_variation' ); - } - - $query_args = $this->merge_query_args( $query_args, $args ); - - return new WP_Query( $query_args ); - } - - /** - * Get standard product data that applies to every product type - * - * @since 2.1 - * @param WC_Product|int $product - * - * @return array - */ - private function get_product_data( $product ) { - if ( is_numeric( $product ) ) { - $product = wc_get_product( $product ); - } - - if ( ! is_a( $product, 'WC_Product' ) ) { - return array(); - } - - return array( - 'title' => $product->get_name(), - 'id' => $product->get_id(), - 'created_at' => $this->server->format_datetime( $product->get_date_created(), false, true ), - 'updated_at' => $this->server->format_datetime( $product->get_date_modified(), false, true ), - 'type' => $product->get_type(), - 'status' => $product->get_status(), - 'downloadable' => $product->is_downloadable(), - 'virtual' => $product->is_virtual(), - 'permalink' => $product->get_permalink(), - 'sku' => $product->get_sku(), - 'price' => $product->get_price(), - 'regular_price' => $product->get_regular_price(), - 'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : null, - 'price_html' => $product->get_price_html(), - 'taxable' => $product->is_taxable(), - 'tax_status' => $product->get_tax_status(), - 'tax_class' => $product->get_tax_class(), - 'managing_stock' => $product->managing_stock(), - 'stock_quantity' => $product->get_stock_quantity(), - 'in_stock' => $product->is_in_stock(), - 'backorders_allowed' => $product->backorders_allowed(), - 'backordered' => $product->is_on_backorder(), - 'sold_individually' => $product->is_sold_individually(), - 'purchaseable' => $product->is_purchasable(), - 'featured' => $product->is_featured(), - 'visible' => $product->is_visible(), - 'catalog_visibility' => $product->get_catalog_visibility(), - 'on_sale' => $product->is_on_sale(), - 'product_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', - 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', - 'weight' => $product->get_weight() ? $product->get_weight() : null, - 'dimensions' => array( - 'length' => $product->get_length(), - 'width' => $product->get_width(), - 'height' => $product->get_height(), - 'unit' => get_option( 'woocommerce_dimension_unit' ), - ), - 'shipping_required' => $product->needs_shipping(), - 'shipping_taxable' => $product->is_shipping_taxable(), - 'shipping_class' => $product->get_shipping_class(), - 'shipping_class_id' => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null, - 'description' => wpautop( do_shortcode( $product->get_description() ) ), - 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_short_description() ), - 'reviews_allowed' => $product->get_reviews_allowed(), - 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), - 'rating_count' => $product->get_rating_count(), - 'related_ids' => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ), - 'upsell_ids' => array_map( 'absint', $product->get_upsell_ids() ), - 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sell_ids() ), - 'parent_id' => $product->get_parent_id(), - 'categories' => wc_get_object_terms( $product->get_id(), 'product_cat', 'name' ), - 'tags' => wc_get_object_terms( $product->get_id(), 'product_tag', 'name' ), - 'images' => $this->get_images( $product ), - 'featured_src' => wp_get_attachment_url( get_post_thumbnail_id( $product->get_id() ) ), - 'attributes' => $this->get_attributes( $product ), - 'downloads' => $this->get_downloads( $product ), - 'download_limit' => $product->get_download_limit(), - 'download_expiry' => $product->get_download_expiry(), - 'download_type' => 'standard', - 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ), - 'total_sales' => $product->get_total_sales(), - 'variations' => array(), - 'parent' => array(), - 'grouped_products' => array(), - 'menu_order' => $this->get_product_menu_order( $product ), - ); - } - - /** - * Get product menu order. - * - * @since 2.5.3 - * @param WC_Product $product - * @return int - */ - private function get_product_menu_order( $product ) { - $menu_order = $product->get_menu_order(); - - return apply_filters( 'woocommerce_api_product_menu_order', $menu_order, $product ); - } - - /** - * Get an individual variation's data. - * - * @since 2.1 - * @param WC_Product $product - * @return array - */ - private function get_variation_data( $product ) { - $variations = array(); - - foreach ( $product->get_children() as $child_id ) { - $variation = wc_get_product( $child_id ); - - if ( ! $variation || ! $variation->exists() ) { - continue; - } - - $variations[] = array( - 'id' => $variation->get_id(), - 'created_at' => $this->server->format_datetime( $variation->get_date_created(), false, true ), - 'updated_at' => $this->server->format_datetime( $variation->get_date_modified(), false, true ), - 'downloadable' => $variation->is_downloadable(), - 'virtual' => $variation->is_virtual(), - 'permalink' => $variation->get_permalink(), - 'sku' => $variation->get_sku(), - 'price' => $variation->get_price(), - 'regular_price' => $variation->get_regular_price(), - 'sale_price' => $variation->get_sale_price() ? $variation->get_sale_price() : null, - 'taxable' => $variation->is_taxable(), - 'tax_status' => $variation->get_tax_status(), - 'tax_class' => $variation->get_tax_class(), - 'managing_stock' => $variation->managing_stock(), - 'stock_quantity' => $variation->get_stock_quantity(), - 'in_stock' => $variation->is_in_stock(), - 'backorders_allowed' => $variation->backorders_allowed(), - 'backordered' => $variation->is_on_backorder(), - 'purchaseable' => $variation->is_purchasable(), - 'visible' => $variation->variation_is_visible(), - 'on_sale' => $variation->is_on_sale(), - 'weight' => $variation->get_weight() ? $variation->get_weight() : null, - 'dimensions' => array( - 'length' => $variation->get_length(), - 'width' => $variation->get_width(), - 'height' => $variation->get_height(), - 'unit' => get_option( 'woocommerce_dimension_unit' ), - ), - 'shipping_class' => $variation->get_shipping_class(), - 'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null, - 'image' => $this->get_images( $variation ), - 'attributes' => $this->get_attributes( $variation ), - 'downloads' => $this->get_downloads( $variation ), - 'download_limit' => (int) $product->get_download_limit(), - 'download_expiry' => (int) $product->get_download_expiry(), - ); - } - - return $variations; - } - - /** - * Get grouped products data - * - * @since 2.5.0 - * @param WC_Product $product - * - * @return array - */ - private function get_grouped_products_data( $product ) { - $products = array(); - - foreach ( $product->get_children() as $child_id ) { - $_product = wc_get_product( $child_id ); - - if ( ! $_product || ! $_product->exists() ) { - continue; - } - - $products[] = $this->get_product_data( $_product ); - - } - - return $products; - } - - /** - * Save default attributes. - * - * @since 3.0.0 - * - * @param WC_Product $product - * @param WP_REST_Request $request - * @return WC_Product - */ - protected function save_default_attributes( $product, $request ) { - // Update default attributes options setting. - if ( isset( $request['default_attribute'] ) ) { - $request['default_attributes'] = $request['default_attribute']; - } - - if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { - $attributes = $product->get_attributes(); - $default_attributes = array(); - - foreach ( $request['default_attributes'] as $default_attr_key => $default_attr ) { - if ( ! isset( $default_attr['name'] ) ) { - continue; - } - - $taxonomy = sanitize_title( $default_attr['name'] ); - - if ( isset( $default_attr['slug'] ) ) { - $taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] ); - } - - if ( isset( $attributes[ $taxonomy ] ) ) { - $_attribute = $attributes[ $taxonomy ]; - - if ( $_attribute['is_variation'] ) { - $value = ''; - - if ( isset( $default_attr['option'] ) ) { - if ( $_attribute['is_taxonomy'] ) { - // Don't use wc_clean as it destroys sanitized characters - $value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) ); - } else { - $value = wc_clean( trim( stripslashes( $default_attr['option'] ) ) ); - } - } - - if ( $value ) { - $default_attributes[ $taxonomy ] = $value; - } - } - } - } - - $product->set_default_attributes( $default_attributes ); - } - - return $product; - } - - /** - * Save product meta. - * - * @since 2.2 - * @param WC_Product $product - * @param array $data - * @return WC_Product - * @throws WC_API_Exception - */ - protected function save_product_meta( $product, $data ) { - global $wpdb; - - // Virtual. - if ( isset( $data['virtual'] ) ) { - $product->set_virtual( $data['virtual'] ); - } - - // Tax status. - if ( isset( $data['tax_status'] ) ) { - $product->set_tax_status( wc_clean( $data['tax_status'] ) ); - } - - // Tax Class. - if ( isset( $data['tax_class'] ) ) { - $product->set_tax_class( wc_clean( $data['tax_class'] ) ); - } - - // Catalog Visibility. - if ( isset( $data['catalog_visibility'] ) ) { - $product->set_catalog_visibility( wc_clean( $data['catalog_visibility'] ) ); - } - - // Purchase Note. - if ( isset( $data['purchase_note'] ) ) { - $product->set_purchase_note( wc_clean( $data['purchase_note'] ) ); - } - - // Featured Product. - if ( isset( $data['featured'] ) ) { - $product->set_featured( $data['featured'] ); - } - - // Shipping data. - $product = $this->save_product_shipping_data( $product, $data ); - - // SKU. - if ( isset( $data['sku'] ) ) { - $sku = $product->get_sku(); - $new_sku = wc_clean( $data['sku'] ); - - if ( '' == $new_sku ) { - $product->set_sku( '' ); - } elseif ( $new_sku !== $sku ) { - if ( ! empty( $new_sku ) ) { - $unique_sku = wc_product_has_unique_sku( $product->get_id(), $new_sku ); - if ( ! $unique_sku ) { - throw new WC_API_Exception( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 ); - } else { - $product->set_sku( $new_sku ); - } - } else { - $product->set_sku( '' ); - } - } - } - - // Attributes. - if ( isset( $data['attributes'] ) ) { - $attributes = array(); - - foreach ( $data['attributes'] as $attribute ) { - $is_taxonomy = 0; - $taxonomy = 0; - - if ( ! isset( $attribute['name'] ) ) { - continue; - } - - $attribute_slug = sanitize_title( $attribute['name'] ); - - if ( isset( $attribute['slug'] ) ) { - $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); - $attribute_slug = sanitize_title( $attribute['slug'] ); - } - - if ( $taxonomy ) { - $is_taxonomy = 1; - } - - if ( $is_taxonomy ) { - - $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute['name'] ); - - if ( isset( $attribute['options'] ) ) { - $options = $attribute['options']; - - if ( ! is_array( $attribute['options'] ) ) { - // Text based attributes - Posted values are term names. - $options = explode( WC_DELIMITER, $options ); - } - - $values = array_map( 'wc_sanitize_term_text_based', $options ); - $values = array_filter( $values, 'strlen' ); - } else { - $values = array(); - } - - // Update post terms - if ( taxonomy_exists( $taxonomy ) ) { - wp_set_object_terms( $product->get_id(), $values, $taxonomy ); - } - - if ( ! empty( $values ) ) { - // Add attribute to array, but don't set values. - $attribute_object = new WC_Product_Attribute(); - $attribute_object->set_id( $attribute_id ); - $attribute_object->set_name( $taxonomy ); - $attribute_object->set_options( $values ); - $attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 ); - $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); - $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); - $attributes[] = $attribute_object; - } - } elseif ( isset( $attribute['options'] ) ) { - // Array based. - if ( is_array( $attribute['options'] ) ) { - $values = $attribute['options']; - - // Text based, separate by pipe. - } else { - $values = array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ); - } - - // Custom attribute - Add attribute to array and set the values. - $attribute_object = new WC_Product_Attribute(); - $attribute_object->set_name( $attribute['name'] ); - $attribute_object->set_options( $values ); - $attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 ); - $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); - $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); - $attributes[] = $attribute_object; - } - } - - uasort( $attributes, 'wc_product_attribute_uasort_comparison' ); - - $product->set_attributes( $attributes ); - } - - // Sales and prices. - if ( in_array( $product->get_type(), array( 'variable', 'grouped' ) ) ) { - - // Variable and grouped products have no prices. - $product->set_regular_price( '' ); - $product->set_sale_price( '' ); - $product->set_date_on_sale_to( '' ); - $product->set_date_on_sale_from( '' ); - $product->set_price( '' ); - - } else { - - // Regular Price. - if ( isset( $data['regular_price'] ) ) { - $regular_price = ( '' === $data['regular_price'] ) ? '' : $data['regular_price']; - $product->set_regular_price( $regular_price ); - } - - // Sale Price. - if ( isset( $data['sale_price'] ) ) { - $sale_price = ( '' === $data['sale_price'] ) ? '' : $data['sale_price']; - $product->set_sale_price( $sale_price ); - } - - if ( isset( $data['sale_price_dates_from'] ) ) { - $date_from = $data['sale_price_dates_from']; - } else { - $date_from = $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : ''; - } - - if ( isset( $data['sale_price_dates_to'] ) ) { - $date_to = $data['sale_price_dates_to']; - } else { - $date_to = $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : ''; - } - - if ( $date_to && ! $date_from ) { - $date_from = strtotime( 'NOW', current_time( 'timestamp', true ) ); - } - - $product->set_date_on_sale_to( $date_to ); - $product->set_date_on_sale_from( $date_from ); - - if ( $product->is_on_sale( 'edit' ) ) { - $product->set_price( $product->get_sale_price( 'edit' ) ); - } else { - $product->set_price( $product->get_regular_price( 'edit' ) ); - } - } - - // Product parent ID for groups. - if ( isset( $data['parent_id'] ) ) { - $product->set_parent_id( absint( $data['parent_id'] ) ); - } - - // Sold Individually. - if ( isset( $data['sold_individually'] ) ) { - $product->set_sold_individually( true === $data['sold_individually'] ? 'yes' : '' ); - } - - // Stock status. - if ( isset( $data['in_stock'] ) ) { - $stock_status = ( true === $data['in_stock'] ) ? 'instock' : 'outofstock'; - } else { - $stock_status = $product->get_stock_status(); - - if ( '' === $stock_status ) { - $stock_status = 'instock'; - } - } - - // Stock Data. - if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) { - // Manage stock. - if ( isset( $data['managing_stock'] ) ) { - $managing_stock = ( true === $data['managing_stock'] ) ? 'yes' : 'no'; - $product->set_manage_stock( $managing_stock ); - } else { - $managing_stock = $product->get_manage_stock() ? 'yes' : 'no'; - } - - // Backorders. - if ( isset( $data['backorders'] ) ) { - if ( 'notify' === $data['backorders'] ) { - $backorders = 'notify'; - } else { - $backorders = ( true === $data['backorders'] ) ? 'yes' : 'no'; - } - - $product->set_backorders( $backorders ); - } else { - $backorders = $product->get_backorders(); - } - - if ( $product->is_type( 'grouped' ) ) { - $product->set_manage_stock( 'no' ); - $product->set_backorders( 'no' ); - $product->set_stock_quantity( '' ); - $product->set_stock_status( $stock_status ); - } elseif ( $product->is_type( 'external' ) ) { - $product->set_manage_stock( 'no' ); - $product->set_backorders( 'no' ); - $product->set_stock_quantity( '' ); - $product->set_stock_status( 'instock' ); - } elseif ( 'yes' == $managing_stock ) { - $product->set_backorders( $backorders ); - - // Stock status is always determined by children so sync later. - if ( ! $product->is_type( 'variable' ) ) { - $product->set_stock_status( $stock_status ); - } - - // Stock quantity. - if ( isset( $data['stock_quantity'] ) ) { - $product->set_stock_quantity( wc_stock_amount( $data['stock_quantity'] ) ); - } elseif ( isset( $data['inventory_delta'] ) ) { - $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); - $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); - $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); - } - } else { - // Don't manage stock. - $product->set_manage_stock( 'no' ); - $product->set_backorders( $backorders ); - $product->set_stock_quantity( '' ); - $product->set_stock_status( $stock_status ); - } - } elseif ( ! $product->is_type( 'variable' ) ) { - $product->set_stock_status( $stock_status ); - } - - // Upsells. - if ( isset( $data['upsell_ids'] ) ) { - $upsells = array(); - $ids = $data['upsell_ids']; - - if ( ! empty( $ids ) ) { - foreach ( $ids as $id ) { - if ( $id && $id > 0 ) { - $upsells[] = $id; - } - } - - $product->set_upsell_ids( $upsells ); - } else { - $product->set_upsell_ids( array() ); - } - } - - // Cross sells. - if ( isset( $data['cross_sell_ids'] ) ) { - $crosssells = array(); - $ids = $data['cross_sell_ids']; - - if ( ! empty( $ids ) ) { - foreach ( $ids as $id ) { - if ( $id && $id > 0 ) { - $crosssells[] = $id; - } - } - - $product->set_cross_sell_ids( $crosssells ); - } else { - $product->set_cross_sell_ids( array() ); - } - } - - // Product categories. - if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) { - $product->set_category_ids( $data['categories'] ); - } - - // Product tags. - if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) { - $product->set_tag_ids( $data['tags'] ); - } - - // Downloadable. - if ( isset( $data['downloadable'] ) ) { - $is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no'; - $product->set_downloadable( $is_downloadable ); - } else { - $is_downloadable = $product->get_downloadable() ? 'yes' : 'no'; - } - - // Downloadable options. - if ( 'yes' == $is_downloadable ) { - - // Downloadable files. - if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { - $product = $this->save_downloadable_files( $product, $data['downloads'] ); - } - - // Download limit. - if ( isset( $data['download_limit'] ) ) { - $product->set_download_limit( $data['download_limit'] ); - } - - // Download expiry. - if ( isset( $data['download_expiry'] ) ) { - $product->set_download_expiry( $data['download_expiry'] ); - } - } - - // Product url. - if ( $product->is_type( 'external' ) ) { - if ( isset( $data['product_url'] ) ) { - $product->set_product_url( $data['product_url'] ); - } - - if ( isset( $data['button_text'] ) ) { - $product->set_button_text( $data['button_text'] ); - } - } - - // Reviews allowed. - if ( isset( $data['reviews_allowed'] ) ) { - $product->set_reviews_allowed( $data['reviews_allowed'] ); - } - - // Save default attributes for variable products. - if ( $product->is_type( 'variable' ) ) { - $product = $this->save_default_attributes( $product, $data ); - } - - // Do action for product type - do_action( 'woocommerce_api_process_product_meta_' . $product->get_type(), $product->get_id(), $data ); - - return $product; - } - - /** - * Save variations. - * - * @since 2.2 - * - * @param WC_Product $product - * @param array $request - * - * @return bool - * @throws WC_API_Exception - */ - protected function save_variations( $product, $request ) { - global $wpdb; - - $id = $product->get_id(); - $variations = $request['variations']; - $attributes = $product->get_attributes(); - - foreach ( $variations as $menu_order => $data ) { - $variation_id = isset( $data['id'] ) ? absint( $data['id'] ) : 0; - $variation = new WC_Product_Variation( $variation_id ); - - // Create initial name and status. - if ( ! $variation->get_slug() ) { - /* translators: 1: variation id 2: product name */ - $variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) ); - $variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' ); - } - - // Parent ID. - $variation->set_parent_id( $product->get_id() ); - - // Menu order. - $variation->set_menu_order( $menu_order ); - - // Status. - if ( isset( $data['visible'] ) ) { - $variation->set_status( false === $data['visible'] ? 'private' : 'publish' ); - } - - // SKU. - if ( isset( $data['sku'] ) ) { - $variation->set_sku( wc_clean( $data['sku'] ) ); - } - - // Thumbnail. - if ( isset( $data['image'] ) && is_array( $data['image'] ) ) { - $image = current( $data['image'] ); - if ( is_array( $image ) ) { - $image['position'] = 0; - } - - $variation = $this->save_product_images( $variation, array( $image ) ); - } - - // Virtual variation. - if ( isset( $data['virtual'] ) ) { - $variation->set_virtual( $data['virtual'] ); - } - - // Downloadable variation. - if ( isset( $data['downloadable'] ) ) { - $is_downloadable = $data['downloadable']; - $variation->set_downloadable( $is_downloadable ); - } else { - $is_downloadable = $variation->get_downloadable(); - } - - // Downloads. - if ( $is_downloadable ) { - // Downloadable files. - if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { - $variation = $this->save_downloadable_files( $variation, $data['downloads'] ); - } - - // Download limit. - if ( isset( $data['download_limit'] ) ) { - $variation->set_download_limit( $data['download_limit'] ); - } - - // Download expiry. - if ( isset( $data['download_expiry'] ) ) { - $variation->set_download_expiry( $data['download_expiry'] ); - } - } - - // Shipping data. - $variation = $this->save_product_shipping_data( $variation, $data ); - - // Stock handling. - $manage_stock = (bool) $variation->get_manage_stock(); - if ( isset( $data['managing_stock'] ) ) { - $manage_stock = $data['managing_stock']; - } - $variation->set_manage_stock( $manage_stock ); - - $stock_status = $variation->get_stock_status(); - if ( isset( $data['in_stock'] ) ) { - $stock_status = true === $data['in_stock'] ? 'instock' : 'outofstock'; - } - $variation->set_stock_status( $stock_status ); - - $backorders = $variation->get_backorders(); - if ( isset( $data['backorders'] ) ) { - $backorders = $data['backorders']; - } - $variation->set_backorders( $backorders ); - - if ( $manage_stock ) { - if ( isset( $data['stock_quantity'] ) ) { - $variation->set_stock_quantity( $data['stock_quantity'] ); - } elseif ( isset( $data['inventory_delta'] ) ) { - $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); - $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); - $variation->set_stock_quantity( $stock_quantity ); - } - } else { - $variation->set_backorders( 'no' ); - $variation->set_stock_quantity( '' ); - } - - // Regular Price. - if ( isset( $data['regular_price'] ) ) { - $variation->set_regular_price( $data['regular_price'] ); - } - - // Sale Price. - if ( isset( $data['sale_price'] ) ) { - $variation->set_sale_price( $data['sale_price'] ); - } - - if ( isset( $data['sale_price_dates_from'] ) ) { - $variation->set_date_on_sale_from( $data['sale_price_dates_from'] ); - } - - if ( isset( $data['sale_price_dates_to'] ) ) { - $variation->set_date_on_sale_to( $data['sale_price_dates_to'] ); - } - - // Tax class. - if ( isset( $data['tax_class'] ) ) { - $variation->set_tax_class( $data['tax_class'] ); - } - - // Description. - if ( isset( $data['description'] ) ) { - $variation->set_description( wp_kses_post( $data['description'] ) ); - } - - // Update taxonomies. - if ( isset( $data['attributes'] ) ) { - $_attributes = array(); - - foreach ( $data['attributes'] as $attribute_key => $attribute ) { - if ( ! isset( $attribute['name'] ) ) { - continue; - } - - $taxonomy = 0; - $_attribute = array(); - - if ( isset( $attribute['slug'] ) ) { - $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); - } - - if ( ! $taxonomy ) { - $taxonomy = sanitize_title( $attribute['name'] ); - } - - if ( isset( $attributes[ $taxonomy ] ) ) { - $_attribute = $attributes[ $taxonomy ]; - } - - if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) { - $_attribute_key = sanitize_title( $_attribute['name'] ); - - if ( isset( $_attribute['is_taxonomy'] ) && $_attribute['is_taxonomy'] ) { - // Don't use wc_clean as it destroys sanitized characters. - $_attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : ''; - } else { - $_attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; - } - - $_attributes[ $_attribute_key ] = $_attribute_value; - } - } - - $variation->set_attributes( $_attributes ); - } - - $variation->save(); - - do_action( 'woocommerce_api_save_product_variation', $variation_id, $menu_order, $variation ); - } - - return true; - } - - /** - * Save product shipping data - * - * @since 2.2 - * @param WC_Product $product - * @param array $data - * @return WC_Product - */ - private function save_product_shipping_data( $product, $data ) { - if ( isset( $data['weight'] ) ) { - $product->set_weight( '' === $data['weight'] ? '' : wc_format_decimal( $data['weight'] ) ); - } - - // Product dimensions - if ( isset( $data['dimensions'] ) ) { - // Height - if ( isset( $data['dimensions']['height'] ) ) { - $product->set_height( '' === $data['dimensions']['height'] ? '' : wc_format_decimal( $data['dimensions']['height'] ) ); - } - - // Width - if ( isset( $data['dimensions']['width'] ) ) { - $product->set_width( '' === $data['dimensions']['width'] ? '' : wc_format_decimal( $data['dimensions']['width'] ) ); - } - - // Length - if ( isset( $data['dimensions']['length'] ) ) { - $product->set_length( '' === $data['dimensions']['length'] ? '' : wc_format_decimal( $data['dimensions']['length'] ) ); - } - } - - // Virtual - if ( isset( $data['virtual'] ) ) { - $virtual = ( true === $data['virtual'] ) ? 'yes' : 'no'; - - if ( 'yes' == $virtual ) { - $product->set_weight( '' ); - $product->set_height( '' ); - $product->set_length( '' ); - $product->set_width( '' ); - } - } - - // Shipping class - if ( isset( $data['shipping_class'] ) ) { - $data_store = $product->get_data_store(); - $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); - $product->set_shipping_class_id( $shipping_class_id ); - } - - return $product; - } - - /** - * Save downloadable files - * - * @since 2.2 - * @param WC_Product $product - * @param array $downloads - * @param int $deprecated Deprecated since 3.0. - * @return WC_Product - */ - private function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { - if ( $deprecated ) { - wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() does not require a variation_id anymore.' ); - } - - $files = array(); - foreach ( $downloads as $key => $file ) { - if ( isset( $file['url'] ) ) { - $file['file'] = $file['url']; - } - - if ( empty( $file['file'] ) ) { - continue; - } - - $download = new WC_Product_Download(); - $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); - $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); - $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); - $files[] = $download; - } - $product->set_downloads( $files ); - - return $product; - } - - /** - * Get attribute taxonomy by slug. - * - * @since 2.2 - * @param string $slug - * @return string|null - */ - private function get_attribute_taxonomy_by_slug( $slug ) { - $taxonomy = null; - $attribute_taxonomies = wc_get_attribute_taxonomies(); - - foreach ( $attribute_taxonomies as $key => $tax ) { - if ( $slug == $tax->attribute_name ) { - $taxonomy = 'pa_' . $tax->attribute_name; - - break; - } - } - - return $taxonomy; - } - - /** - * Get the images for a product or product variation - * - * @since 2.1 - * @param WC_Product|WC_Product_Variation $product - * @return array - */ - private function get_images( $product ) { - $images = $attachment_ids = array(); - $product_image = $product->get_image_id(); - - // Add featured image. - if ( ! empty( $product_image ) ) { - $attachment_ids[] = $product_image; - } - - // Add gallery images. - $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); - - // Build image data. - foreach ( $attachment_ids as $position => $attachment_id ) { - - $attachment_post = get_post( $attachment_id ); - - if ( is_null( $attachment_post ) ) { - continue; - } - - $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); - - if ( ! is_array( $attachment ) ) { - continue; - } - - $images[] = array( - 'id' => (int) $attachment_id, - 'created_at' => $this->server->format_datetime( $attachment_post->post_date_gmt ), - 'updated_at' => $this->server->format_datetime( $attachment_post->post_modified_gmt ), - 'src' => current( $attachment ), - 'title' => get_the_title( $attachment_id ), - 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), - 'position' => (int) $position, - ); - } - - // Set a placeholder image if the product has no images set. - if ( empty( $images ) ) { - - $images[] = array( - 'id' => 0, - 'created_at' => $this->server->format_datetime( time() ), // Default to now. - 'updated_at' => $this->server->format_datetime( time() ), - 'src' => wc_placeholder_img_src(), - 'title' => __( 'Placeholder', 'woocommerce' ), - 'alt' => __( 'Placeholder', 'woocommerce' ), - 'position' => 0, - ); - } - - return $images; - } - - /** - * Save product images. - * - * @since 2.2 - * @param WC_Product $product - * @param array $images - * @throws WC_API_Exception - * @return WC_Product - */ - protected function save_product_images( $product, $images ) { - if ( is_array( $images ) ) { - $gallery = array(); - - foreach ( $images as $image ) { - if ( isset( $image['position'] ) && 0 == $image['position'] ) { - $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; - - if ( 0 === $attachment_id && isset( $image['src'] ) ) { - $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); - - if ( is_wp_error( $upload ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); - } - - $attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() ); - } - - $product->set_image_id( $attachment_id ); - } else { - $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; - - if ( 0 === $attachment_id && isset( $image['src'] ) ) { - $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); - - if ( is_wp_error( $upload ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); - } - - $attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() ); - } - - $gallery[] = $attachment_id; - } - - // Set the image alt if present. - if ( ! empty( $image['alt'] ) && $attachment_id ) { - update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); - } - - // Set the image title if present. - if ( ! empty( $image['title'] ) && $attachment_id ) { - wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['title'] ) ); - } - } - - if ( ! empty( $gallery ) ) { - $product->set_gallery_image_ids( $gallery ); - } - } else { - $product->set_image_id( '' ); - $product->set_gallery_image_ids( array() ); - } - - return $product; - } - - /** - * Upload image from URL - * - * @since 2.2 - * @param string $image_url - * @return int|WP_Error attachment id - */ - public function upload_product_image( $image_url ) { - return $this->upload_image_from_url( $image_url, 'product_image' ); - } - - /** - * Upload product category image from URL. - * - * @since 2.5.0 - * @param string $image_url - * @return int|WP_Error attachment id - */ - public function upload_product_category_image( $image_url ) { - return $this->upload_image_from_url( $image_url, 'product_category_image' ); - } - - /** - * Upload image from URL. - * - * @throws WC_API_Exception - * - * @since 2.5.0 - * @param string $image_url - * @param string $upload_for - * @return array - */ - protected function upload_image_from_url( $image_url, $upload_for = 'product_image' ) { - $upload = wc_rest_upload_image_from_url( $image_url ); - if ( is_wp_error( $upload ) ) { - throw new WC_API_Exception( 'woocommerce_api_' . $upload_for . '_upload_error', $upload->get_error_message(), 400 ); - } - - do_action( 'woocommerce_api_uploaded_image_from_url', $upload, $image_url, $upload_for ); - - return $upload; - } - - /** - * Sets product image as attachment and returns the attachment ID. - * - * @since 2.2 - * @param array $upload - * @param int $id - * @return int - */ - protected function set_product_image_as_attachment( $upload, $id ) { - return $this->set_uploaded_image_as_attachment( $upload, $id ); - } - - /** - * Sets uploaded category image as attachment and returns the attachment ID. - * - * @since 2.5.0 - * @param integer $upload Upload information from wp_upload_bits - * @return int Attachment ID - */ - protected function set_product_category_image_as_attachment( $upload ) { - return $this->set_uploaded_image_as_attachment( $upload ); - } - - /** - * Set uploaded image as attachment. - * - * @since 2.5.0 - * @param array $upload Upload information from wp_upload_bits - * @param int $id Post ID. Default to 0. - * @return int Attachment ID - */ - protected function set_uploaded_image_as_attachment( $upload, $id = 0 ) { - $info = wp_check_filetype( $upload['file'] ); - $title = ''; - $content = ''; - - if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) { - if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { - $title = wc_clean( $image_meta['title'] ); - } - if ( trim( $image_meta['caption'] ) ) { - $content = wc_clean( $image_meta['caption'] ); - } - } - - $attachment = array( - 'post_mime_type' => $info['type'], - 'guid' => $upload['url'], - 'post_parent' => $id, - 'post_title' => $title, - 'post_content' => $content, - ); - - $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id ); - if ( ! is_wp_error( $attachment_id ) ) { - wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); - } - - return $attachment_id; - } - - /** - * Get attribute options. - * - * @param int $product_id - * @param array $attribute - * @return array - */ - protected function get_attribute_options( $product_id, $attribute ) { - if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { - return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) ); - } elseif ( isset( $attribute['value'] ) ) { - return array_map( 'trim', explode( '|', $attribute['value'] ) ); - } - - return array(); - } - - /** - * Get the attributes for a product or product variation - * - * @since 2.1 - * @param WC_Product|WC_Product_Variation $product - * @return array - */ - private function get_attributes( $product ) { - - $attributes = array(); - - if ( $product->is_type( 'variation' ) ) { - - // variation attributes - foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { - - // taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_` - $attributes[] = array( - 'name' => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ), $product ), - 'slug' => str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $attribute_name ) ), - 'option' => $attribute, - ); - } - } else { - - foreach ( $product->get_attributes() as $attribute ) { - $attributes[] = array( - 'name' => wc_attribute_label( $attribute['name'], $product ), - 'slug' => wc_attribute_taxonomy_slug( $attribute['name'] ), - 'position' => (int) $attribute['position'], - 'visible' => (bool) $attribute['is_visible'], - 'variation' => (bool) $attribute['is_variation'], - 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), - ); - } - } - - return $attributes; - } - - /** - * Get the downloads for a product or product variation - * - * @since 2.1 - * @param WC_Product|WC_Product_Variation $product - * @return array - */ - private function get_downloads( $product ) { - - $downloads = array(); - - if ( $product->is_downloadable() ) { - - foreach ( $product->get_downloads() as $file_id => $file ) { - - $downloads[] = array( - 'id' => $file_id, // do not cast as int as this is a hash - 'name' => $file['name'], - 'file' => $file['file'], - ); - } - } - - return $downloads; - } - - /** - * Get a listing of product attributes - * - * @since 2.5.0 - * - * @param string|null $fields fields to limit response to - * - * @return array|WP_Error - */ - public function get_product_attributes( $fields = null ) { - try { - // Permissions check. - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); - } - - $product_attributes = array(); - $attribute_taxonomies = wc_get_attribute_taxonomies(); - - foreach ( $attribute_taxonomies as $attribute ) { - $product_attributes[] = array( - 'id' => intval( $attribute->attribute_id ), - 'name' => $attribute->attribute_label, - 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), - 'type' => $attribute->attribute_type, - 'order_by' => $attribute->attribute_orderby, - 'has_archives' => (bool) $attribute->attribute_public, - ); - } - - return array( 'product_attributes' => apply_filters( 'woocommerce_api_product_attributes_response', $product_attributes, $attribute_taxonomies, $fields, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get the product attribute for the given ID - * - * @since 2.5.0 - * - * @param string $id product attribute term ID - * @param string|null $fields fields to limit response to - * - * @return array|WP_Error - */ - public function get_product_attribute( $id, $fields = null ) { - global $wpdb; - - try { - $id = absint( $id ); - - // Validate ID - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 ); - } - - // Permissions check - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); - } - - $attribute = $wpdb->get_row( $wpdb->prepare( " - SELECT * - FROM {$wpdb->prefix}woocommerce_attribute_taxonomies - WHERE attribute_id = %d - ", $id ) ); - - if ( is_wp_error( $attribute ) || is_null( $attribute ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $product_attribute = array( - 'id' => intval( $attribute->attribute_id ), - 'name' => $attribute->attribute_label, - 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), - 'type' => $attribute->attribute_type, - 'order_by' => $attribute->attribute_orderby, - 'has_archives' => (bool) $attribute->attribute_public, - ); - - return array( 'product_attribute' => apply_filters( 'woocommerce_api_product_attribute_response', $product_attribute, $id, $fields, $attribute, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Validate attribute data. - * - * @since 2.5.0 - * @param string $name - * @param string $slug - * @param string $type - * @param string $order_by - * @param bool $new_data - * @return bool - * @throws WC_API_Exception - */ - protected function validate_attribute_data( $name, $slug, $type, $order_by, $new_data = true ) { - if ( empty( $name ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); - } - - if ( strlen( $slug ) >= 28 ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), 400 ); - } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), 400 ); - } elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), 400 ); - } - - // Validate the attribute type - if ( ! in_array( wc_clean( $type ), array_keys( wc_get_attribute_types() ) ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_type', sprintf( __( 'Invalid product attribute type - the product attribute type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_attribute_types() ) ) ), 400 ); - } - - // Validate the attribute order by - if ( ! in_array( wc_clean( $order_by ), array( 'menu_order', 'name', 'name_num', 'id' ) ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_order_by', sprintf( __( 'Invalid product attribute order_by type - the product attribute order_by type must be any of these: %s', 'woocommerce' ), implode( ', ', array( 'menu_order', 'name', 'name_num', 'id' ) ) ), 400 ); - } - - return true; - } - - /** - * Create a new product attribute. - * - * @since 2.5.0 - * - * @param array $data Posted data. - * - * @return array|WP_Error - */ - public function create_product_attribute( $data ) { - global $wpdb; - - try { - if ( ! isset( $data['product_attribute'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); - } - - $data = $data['product_attribute']; - - // Check permissions. - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 ); - } - - $data = apply_filters( 'woocommerce_api_create_product_attribute_data', $data, $this ); - - if ( ! isset( $data['name'] ) ) { - $data['name'] = ''; - } - - // Set the attribute slug. - if ( ! isset( $data['slug'] ) ) { - $data['slug'] = wc_sanitize_taxonomy_name( stripslashes( $data['name'] ) ); - } else { - $data['slug'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ) ); - } - - // Set attribute type when not sent. - if ( ! isset( $data['type'] ) ) { - $data['type'] = 'select'; - } - - // Set order by when not sent. - if ( ! isset( $data['order_by'] ) ) { - $data['order_by'] = 'menu_order'; - } - - // Validate the attribute data. - $this->validate_attribute_data( $data['name'], $data['slug'], $data['type'], $data['order_by'], true ); - - $insert = $wpdb->insert( - $wpdb->prefix . 'woocommerce_attribute_taxonomies', - array( - 'attribute_label' => $data['name'], - 'attribute_name' => $data['slug'], - 'attribute_type' => $data['type'], - 'attribute_orderby' => $data['order_by'], - 'attribute_public' => isset( $data['has_archives'] ) && true === $data['has_archives'] ? 1 : 0, - ), - array( '%s', '%s', '%s', '%s', '%d' ) - ); - - // Checks for an error in the product creation. - if ( is_wp_error( $insert ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $insert->get_error_message(), 400 ); - } - - $id = $wpdb->insert_id; - - do_action( 'woocommerce_api_create_product_attribute', $id, $data ); - - // Clear transients. - wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); - delete_transient( 'wc_attribute_taxonomies' ); - WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); - - $this->server->send_status( 201 ); - - return $this->get_product_attribute( $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit a product attribute. - * - * @since 2.5.0 - * - * @param int $id the attribute ID. - * @param array $data - * - * @return array|WP_Error - */ - public function edit_product_attribute( $id, $data ) { - global $wpdb; - - try { - if ( ! isset( $data['product_attribute'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); - } - - $id = absint( $id ); - $data = $data['product_attribute']; - - // Check permissions. - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 ); - } - - $data = apply_filters( 'woocommerce_api_edit_product_attribute_data', $data, $this ); - $attribute = $this->get_product_attribute( $id ); - - if ( is_wp_error( $attribute ) ) { - return $attribute; - } - - $attribute_name = isset( $data['name'] ) ? $data['name'] : $attribute['product_attribute']['name']; - $attribute_type = isset( $data['type'] ) ? $data['type'] : $attribute['product_attribute']['type']; - $attribute_order_by = isset( $data['order_by'] ) ? $data['order_by'] : $attribute['product_attribute']['order_by']; - - if ( isset( $data['slug'] ) ) { - $attribute_slug = wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ); - } else { - $attribute_slug = $attribute['product_attribute']['slug']; - } - $attribute_slug = preg_replace( '/^pa\_/', '', $attribute_slug ); - - if ( isset( $data['has_archives'] ) ) { - $attribute_public = true === $data['has_archives'] ? 1 : 0; - } else { - $attribute_public = $attribute['product_attribute']['has_archives']; - } - - // Validate the attribute data. - $this->validate_attribute_data( $attribute_name, $attribute_slug, $attribute_type, $attribute_order_by, false ); - - $update = $wpdb->update( - $wpdb->prefix . 'woocommerce_attribute_taxonomies', - array( - 'attribute_label' => $attribute_name, - 'attribute_name' => $attribute_slug, - 'attribute_type' => $attribute_type, - 'attribute_orderby' => $attribute_order_by, - 'attribute_public' => $attribute_public, - ), - array( 'attribute_id' => $id ), - array( '%s', '%s', '%s', '%s', '%d' ), - array( '%d' ) - ); - - // Checks for an error in the product creation. - if ( false === $update ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute', __( 'Could not edit the attribute', 'woocommerce' ), 400 ); - } - - do_action( 'woocommerce_api_edit_product_attribute', $id, $data ); - - // Clear transients. - wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); - delete_transient( 'wc_attribute_taxonomies' ); - WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); - - return $this->get_product_attribute( $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete a product attribute. - * - * @since 2.5.0 - * - * @param int $id the product attribute ID. - * - * @return array|WP_Error - */ - public function delete_product_attribute( $id ) { - global $wpdb; - - try { - // Check permissions. - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute', __( 'You do not have permission to delete product attributes', 'woocommerce' ), 401 ); - } - - $id = absint( $id ); - - $attribute_name = $wpdb->get_var( $wpdb->prepare( " - SELECT attribute_name - FROM {$wpdb->prefix}woocommerce_attribute_taxonomies - WHERE attribute_id = %d - ", $id ) ); - - if ( is_null( $attribute_name ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $deleted = $wpdb->delete( - $wpdb->prefix . 'woocommerce_attribute_taxonomies', - array( 'attribute_id' => $id ), - array( '%d' ) - ); - - if ( false === $deleted ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute', __( 'Could not delete the attribute', 'woocommerce' ), 401 ); - } - - $taxonomy = wc_attribute_taxonomy_name( $attribute_name ); - - if ( taxonomy_exists( $taxonomy ) ) { - $terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' ); - foreach ( $terms as $term ) { - wp_delete_term( $term->term_id, $taxonomy ); - } - } - - do_action( 'woocommerce_attribute_deleted', $id, $attribute_name, $taxonomy ); - do_action( 'woocommerce_api_delete_product_attribute', $id, $this ); - - // Clear transients. - wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); - delete_transient( 'wc_attribute_taxonomies' ); - WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); - - return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get a listing of product attribute terms. - * - * @since 2.5.0 - * - * @param int $attribute_id Attribute ID. - * @param string|null $fields Fields to limit response to. - * - * @return array|WP_Error - */ - public function get_product_attribute_terms( $attribute_id, $fields = null ) { - try { - // Permissions check. - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attribute_terms', __( 'You do not have permission to read product attribute terms', 'woocommerce' ), 401 ); - } - - $attribute_id = absint( $attribute_id ); - $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); - - if ( ! $taxonomy ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $terms = get_terms( $taxonomy, array( 'hide_empty' => false ) ); - $attribute_terms = array(); - - foreach ( $terms as $term ) { - $attribute_terms[] = array( - 'id' => $term->term_id, - 'slug' => $term->slug, - 'name' => $term->name, - 'count' => $term->count, - ); - } - - return array( 'product_attribute_terms' => apply_filters( 'woocommerce_api_product_attribute_terms_response', $attribute_terms, $terms, $fields, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get the product attribute term for the given ID. - * - * @since 2.5.0 - * - * @param int $attribute_id Attribute ID. - * @param string $id Product attribute term ID. - * @param string|null $fields Fields to limit response to. - * - * @return array|WP_Error - */ - public function get_product_attribute_term( $attribute_id, $id, $fields = null ) { - global $wpdb; - - try { - $id = absint( $id ); - $attribute_id = absint( $attribute_id ); - - // Validate ID - if ( empty( $id ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_term_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 ); - } - - // Permissions check - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attribute_terms', __( 'You do not have permission to read product attribute terms', 'woocommerce' ), 401 ); - } - - $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); - - if ( ! $taxonomy ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $term = get_term( $id, $taxonomy ); - - if ( is_wp_error( $term ) || is_null( $term ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_term_id', __( 'A product attribute term with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $attribute_term = array( - 'id' => $term->term_id, - 'name' => $term->name, - 'slug' => $term->slug, - 'count' => $term->count, - ); - - return array( 'product_attribute_term' => apply_filters( 'woocommerce_api_product_attribute_response', $attribute_term, $id, $fields, $term, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Create a new product attribute term. - * - * @since 2.5.0 - * - * @param int $attribute_id Attribute ID. - * @param array $data Posted data. - * - * @return array|WP_Error - */ - public function create_product_attribute_term( $attribute_id, $data ) { - global $wpdb; - - try { - if ( ! isset( $data['product_attribute_term'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute_term' ), 400 ); - } - - $data = $data['product_attribute_term']; - - // Check permissions. - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 ); - } - - $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); - - if ( ! $taxonomy ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $data = apply_filters( 'woocommerce_api_create_product_attribute_term_data', $data, $this ); - - // Check if attribute term name is specified. - if ( ! isset( $data['name'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); - } - - $args = array(); - - // Set the attribute term slug. - if ( isset( $data['slug'] ) ) { - $args['slug'] = sanitize_title( wp_unslash( $data['slug'] ) ); - } - - $term = wp_insert_term( $data['name'], $taxonomy, $args ); - - // Checks for an error in the term creation. - if ( is_wp_error( $term ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $term->get_error_message(), 400 ); - } - - $id = $term['term_id']; - - do_action( 'woocommerce_api_create_product_attribute_term', $id, $data ); - - $this->server->send_status( 201 ); - - return $this->get_product_attribute_term( $attribute_id, $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit a product attribute term. - * - * @since 2.5.0 - * - * @param int $attribute_id Attribute ID. - * @param int $id the attribute ID. - * @param array $data - * - * @return array|WP_Error - */ - public function edit_product_attribute_term( $attribute_id, $id, $data ) { - global $wpdb; - - try { - if ( ! isset( $data['product_attribute_term'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute_term' ), 400 ); - } - - $id = absint( $id ); - $data = $data['product_attribute_term']; - - // Check permissions. - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 ); - } - - $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); - - if ( ! $taxonomy ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $data = apply_filters( 'woocommerce_api_edit_product_attribute_term_data', $data, $this ); - - $args = array(); - - // Update name. - if ( isset( $data['name'] ) ) { - $args['name'] = wc_clean( wp_unslash( $data['name'] ) ); - } - - // Update slug. - if ( isset( $data['slug'] ) ) { - $args['slug'] = sanitize_title( wp_unslash( $data['slug'] ) ); - } - - $term = wp_update_term( $id, $taxonomy, $args ); - - if ( is_wp_error( $term ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute_term', $term->get_error_message(), 400 ); - } - - do_action( 'woocommerce_api_edit_product_attribute_term', $id, $data ); - - return $this->get_product_attribute_term( $attribute_id, $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete a product attribute term. - * - * @since 2.5.0 - * - * @param int $attribute_id Attribute ID. - * @param int $id the product attribute ID. - * - * @return array|WP_Error - */ - public function delete_product_attribute_term( $attribute_id, $id ) { - global $wpdb; - - try { - // Check permissions. - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute_term', __( 'You do not have permission to delete product attribute terms', 'woocommerce' ), 401 ); - } - - $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); - - if ( ! $taxonomy ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $id = absint( $id ); - $term = wp_delete_term( $id, $taxonomy ); - - if ( ! $term ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute_term', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product_attribute_term' ), 500 ); - } elseif ( is_wp_error( $term ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute_term', $term->get_error_message(), 400 ); - } - - do_action( 'woocommerce_api_delete_product_attribute_term', $id, $this ); - - return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Clear product - * - * @param int $product_id - */ - protected function clear_product( $product_id ) { - if ( ! is_numeric( $product_id ) || 0 >= $product_id ) { - return; - } - - // Delete product attachments - $attachments = get_children( array( - 'post_parent' => $product_id, - 'post_status' => 'any', - 'post_type' => 'attachment', - ) ); - - foreach ( (array) $attachments as $attachment ) { - wp_delete_attachment( $attachment->ID, true ); - } - - // Delete product - $product = wc_get_product( $product_id ); - $product->delete( true ); - } - - /** - * Bulk update or insert products - * Accepts an array with products in the formats supported by - * WC_API_Products->create_product() and WC_API_Products->edit_product() - * - * @since 2.4.0 - * - * @param array $data - * - * @return array|WP_Error - */ - public function bulk( $data ) { - - try { - if ( ! isset( $data['products'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_products_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'products' ), 400 ); - } - - $data = $data['products']; - $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'products' ); - - // Limit bulk operation - if ( count( $data ) > $limit ) { - throw new WC_API_Exception( 'woocommerce_api_products_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); - } - - $products = array(); - - foreach ( $data as $_product ) { - $product_id = 0; - $product_sku = ''; - - // Try to get the product ID - if ( isset( $_product['id'] ) ) { - $product_id = intval( $_product['id'] ); - } - - if ( ! $product_id && isset( $_product['sku'] ) ) { - $product_sku = wc_clean( $_product['sku'] ); - $product_id = wc_get_product_id_by_sku( $product_sku ); - } - - if ( $product_id ) { - - // Product exists / edit product - $edit = $this->edit_product( $product_id, array( 'product' => $_product ) ); - - if ( is_wp_error( $edit ) ) { - $products[] = array( - 'id' => $product_id, - 'sku' => $product_sku, - 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), - ); - } else { - $products[] = $edit['product']; - } - } else { - - // Product don't exists / create product - $new = $this->create_product( array( 'product' => $_product ) ); - - if ( is_wp_error( $new ) ) { - $products[] = array( - 'id' => $product_id, - 'sku' => $product_sku, - 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), - ); - } else { - $products[] = $new['product']; - } - } - } - - return array( 'products' => apply_filters( 'woocommerce_api_products_bulk_response', $products, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get a listing of product shipping classes. - * - * @since 2.5.0 - * @param string|null $fields Fields to limit response to - * @return array|WP_Error List of product shipping classes if succeed, - * otherwise WP_Error will be returned - */ - public function get_product_shipping_classes( $fields = null ) { - try { - // Permissions check - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_shipping_classes', __( 'You do not have permission to read product shipping classes', 'woocommerce' ), 401 ); - } - - $product_shipping_classes = array(); - - $terms = get_terms( 'product_shipping_class', array( 'hide_empty' => false, 'fields' => 'ids' ) ); - - foreach ( $terms as $term_id ) { - $product_shipping_classes[] = current( $this->get_product_shipping_class( $term_id, $fields ) ); - } - - return array( 'product_shipping_classes' => apply_filters( 'woocommerce_api_product_shipping_classes_response', $product_shipping_classes, $terms, $fields, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get the product shipping class for the given ID. - * - * @since 2.5.0 - * @param string $id Product shipping class term ID - * @param string|null $fields Fields to limit response to - * @return array|WP_Error Product shipping class if succeed, otherwise - * WP_Error will be returned - */ - public function get_product_shipping_class( $id, $fields = null ) { - try { - $id = absint( $id ); - if ( ! $id ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_id', __( 'Invalid product shipping class ID', 'woocommerce' ), 400 ); - } - - // Permissions check - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_shipping_classes', __( 'You do not have permission to read product shipping classes', 'woocommerce' ), 401 ); - } - - $term = get_term( $id, 'product_shipping_class' ); - - if ( is_wp_error( $term ) || is_null( $term ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_id', __( 'A product shipping class with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $term_id = intval( $term->term_id ); - - $product_shipping_class = array( - 'id' => $term_id, - 'name' => $term->name, - 'slug' => $term->slug, - 'parent' => $term->parent, - 'description' => $term->description, - 'count' => intval( $term->count ), - ); - - return array( 'product_shipping_class' => apply_filters( 'woocommerce_api_product_shipping_class_response', $product_shipping_class, $id, $fields, $term, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Create a new product shipping class. - * - * @since 2.5.0 - * @param array $data Posted data - * @return array|WP_Error Product shipping class if succeed, otherwise - * WP_Error will be returned - */ - public function create_product_shipping_class( $data ) { - global $wpdb; - - try { - if ( ! isset( $data['product_shipping_class'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_shipping_class_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_shipping_class' ), 400 ); - } - - // Check permissions - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_shipping_class', __( 'You do not have permission to create product shipping classes', 'woocommerce' ), 401 ); - } - - $defaults = array( - 'name' => '', - 'slug' => '', - 'description' => '', - 'parent' => 0, - ); - - $data = wp_parse_args( $data['product_shipping_class'], $defaults ); - $data = apply_filters( 'woocommerce_api_create_product_shipping_class_data', $data, $this ); - - // Check parent. - $data['parent'] = absint( $data['parent'] ); - if ( $data['parent'] ) { - $parent = get_term_by( 'id', $data['parent'], 'product_shipping_class' ); - if ( ! $parent ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_parent', __( 'Product shipping class parent is invalid', 'woocommerce' ), 400 ); - } - } - - $insert = wp_insert_term( $data['name'], 'product_shipping_class', $data ); - if ( is_wp_error( $insert ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_shipping_class', $insert->get_error_message(), 400 ); - } - - $id = $insert['term_id']; - - do_action( 'woocommerce_api_create_product_shipping_class', $id, $data ); - - $this->server->send_status( 201 ); - - return $this->get_product_shipping_class( $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit a product shipping class. - * - * @since 2.5.0 - * @param int $id Product shipping class term ID - * @param array $data Posted data - * @return array|WP_Error Product shipping class if succeed, otherwise - * WP_Error will be returned - */ - public function edit_product_shipping_class( $id, $data ) { - global $wpdb; - - try { - if ( ! isset( $data['product_shipping_class'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_product_shipping_class', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_shipping_class' ), 400 ); - } - - $id = absint( $id ); - $data = $data['product_shipping_class']; - - // Check permissions - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_shipping_class', __( 'You do not have permission to edit product shipping classes', 'woocommerce' ), 401 ); - } - - $data = apply_filters( 'woocommerce_api_edit_product_shipping_class_data', $data, $this ); - $shipping_class = $this->get_product_shipping_class( $id ); - - if ( is_wp_error( $shipping_class ) ) { - return $shipping_class; - } - - $update = wp_update_term( $id, 'product_shipping_class', $data ); - if ( is_wp_error( $update ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_shipping_class', __( 'Could not edit the shipping class', 'woocommerce' ), 400 ); - } - - do_action( 'woocommerce_api_edit_product_shipping_class', $id, $data ); - - return $this->get_product_shipping_class( $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete a product shipping class. - * - * @since 2.5.0 - * @param int $id Product shipping class term ID - * @return array|WP_Error Success message if succeed, otherwise WP_Error - * will be returned - */ - public function delete_product_shipping_class( $id ) { - global $wpdb; - - try { - // Check permissions - if ( ! current_user_can( 'manage_product_terms' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_shipping_class', __( 'You do not have permission to delete product shipping classes', 'woocommerce' ), 401 ); - } - - $id = absint( $id ); - $deleted = wp_delete_term( $id, 'product_shipping_class' ); - if ( ! $deleted || is_wp_error( $deleted ) ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_shipping_class', __( 'Could not delete the shipping class', 'woocommerce' ), 401 ); - } - - do_action( 'woocommerce_api_delete_product_shipping_class', $id, $this ); - - return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_shipping_class' ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } -} diff --git a/includes/legacy/api/v3/class-wc-api-resource.php b/includes/legacy/api/v3/class-wc-api-resource.php deleted file mode 100644 index f321b9cb877..00000000000 --- a/includes/legacy/api/v3/class-wc-api-resource.php +++ /dev/null @@ -1,471 +0,0 @@ -server = $server; - - // automatically register routes for sub-classes - add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) ); - - // maybe add meta to top-level resource responses - foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) { - add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 ); - } - - $response_names = array( - 'order', - 'coupon', - 'customer', - 'product', - 'report', - 'customer_orders', - 'customer_downloads', - 'order_note', - 'order_refund', - 'product_reviews', - 'product_category', - 'tax', - 'tax_class', - ); - - foreach ( $response_names as $name ) { - - /** - * Remove fields from responses when requests specify certain fields - * note these are hooked at a later priority so data added via - * filters (e.g. customer data to the order response) still has the - * fields filtered properly - */ - add_filter( "woocommerce_api_{$name}_response", array( $this, 'filter_response_fields' ), 20, 3 ); - } - } - - /** - * Validate the request by checking: - * - * 1) the ID is a valid integer - * 2) the ID returns a valid post object and matches the provided post type - * 3) the current user has the proper permissions to read/edit/delete the post - * - * @since 2.1 - * @param string|int $id the post ID - * @param string $type the post type, either `shop_order`, `shop_coupon`, or `product` - * @param string $context the context of the request, either `read`, `edit` or `delete` - * @return int|WP_Error valid post ID or WP_Error if any of the checks fails - */ - protected function validate_request( $id, $type, $context ) { - - if ( 'shop_order' === $type || 'shop_coupon' === $type || 'shop_webhook' === $type ) { - $resource_name = str_replace( 'shop_', '', $type ); - } else { - $resource_name = $type; - } - - $id = absint( $id ); - - // Validate ID - if ( empty( $id ) ) { - return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) ); - } - - // Only custom post types have per-post type/permission checks - if ( 'customer' !== $type ) { - - $post = get_post( $id ); - - if ( null === $post ) { - return new WP_Error( "woocommerce_api_no_{$resource_name}_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), $resource_name, $id ), array( 'status' => 404 ) ); - } - - // For checking permissions, product variations are the same as the product post type - $post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type; - - // Validate post type - if ( $type !== $post_type ) { - return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) ); - } - - // Validate permissions - switch ( $context ) { - - case 'read': - if ( ! $this->is_readable( $post ) ) { - return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); - } - break; - - case 'edit': - if ( ! $this->is_editable( $post ) ) { - return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); - } - break; - - case 'delete': - if ( ! $this->is_deletable( $post ) ) { - return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); - } - break; - } - } - - return $id; - } - - /** - * Add common request arguments to argument list before WP_Query is run - * - * @since 2.1 - * @param array $base_args required arguments for the query (e.g. `post_type`, etc) - * @param array $request_args arguments provided in the request - * @return array - */ - protected function merge_query_args( $base_args, $request_args ) { - - $args = array(); - - // date - if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) { - - $args['date_query'] = array(); - - // resources created after specified date - if ( ! empty( $request_args['created_at_min'] ) ) { - $args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true ); - } - - // resources created before specified date - if ( ! empty( $request_args['created_at_max'] ) ) { - $args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true ); - } - - // resources updated after specified date - if ( ! empty( $request_args['updated_at_min'] ) ) { - $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true ); - } - - // resources updated before specified date - if ( ! empty( $request_args['updated_at_max'] ) ) { - $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true ); - } - } - - // search - if ( ! empty( $request_args['q'] ) ) { - $args['s'] = $request_args['q']; - } - - // resources per response - if ( ! empty( $request_args['limit'] ) ) { - $args['posts_per_page'] = $request_args['limit']; - } - - // resource offset - if ( ! empty( $request_args['offset'] ) ) { - $args['offset'] = $request_args['offset']; - } - - // order (ASC or DESC, ASC by default) - if ( ! empty( $request_args['order'] ) ) { - $args['order'] = $request_args['order']; - } - - // orderby - if ( ! empty( $request_args['orderby'] ) ) { - $args['orderby'] = $request_args['orderby']; - - // allow sorting by meta value - if ( ! empty( $request_args['orderby_meta_key'] ) ) { - $args['meta_key'] = $request_args['orderby_meta_key']; - } - } - - // allow post status change - if ( ! empty( $request_args['post_status'] ) ) { - $args['post_status'] = $request_args['post_status']; - unset( $request_args['post_status'] ); - } - - // filter by a list of post id - if ( ! empty( $request_args['in'] ) ) { - $args['post__in'] = explode( ',', $request_args['in'] ); - unset( $request_args['in'] ); - } - - // exclude by a list of post id - if ( ! empty( $request_args['not_in'] ) ) { - $args['post__not_in'] = explode( ',', $request_args['not_in'] ); - unset( $request_args['not_in'] ); - } - - // resource page - $args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1; - - $args = apply_filters( 'woocommerce_api_query_args', $args, $request_args ); - - return array_merge( $base_args, $args ); - } - - /** - * Add meta to resources when requested by the client. Meta is added as a top-level - * `_meta` attribute (e.g. `order_meta`) as a list of key/value pairs - * - * @since 2.1 - * @param array $data the resource data - * @param object $resource the resource object (e.g WC_Order) - * @return mixed - */ - public function maybe_add_meta( $data, $resource ) { - - if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) { - - // don't attempt to add meta more than once - if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) { - return $data; - } - - // define the top-level property name for the meta - switch ( get_class( $resource ) ) { - - case 'WC_Order': - $meta_name = 'order_meta'; - break; - - case 'WC_Coupon': - $meta_name = 'coupon_meta'; - break; - - case 'WP_User': - $meta_name = 'customer_meta'; - break; - - default: - $meta_name = 'product_meta'; - break; - } - - if ( is_a( $resource, 'WP_User' ) ) { - - // customer meta - $meta = (array) get_user_meta( $resource->ID ); - - } else { - - // coupon/order/product meta - $meta = (array) get_post_meta( $resource->get_id() ); - } - - foreach ( $meta as $meta_key => $meta_value ) { - - // don't add hidden meta by default - if ( ! is_protected_meta( $meta_key ) ) { - $data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] ); - } - } - } - - return $data; - } - - /** - * Restrict the fields included in the response if the request specified certain only certain fields should be returned - * - * @since 2.1 - * @param array $data the response data - * @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order - * @param array|string the requested list of fields to include in the response - * @return array response data - */ - public function filter_response_fields( $data, $resource, $fields ) { - - if ( ! is_array( $data ) || empty( $fields ) ) { - return $data; - } - - $fields = explode( ',', $fields ); - $sub_fields = array(); - - // get sub fields - foreach ( $fields as $field ) { - - if ( false !== strpos( $field, '.' ) ) { - - list( $name, $value ) = explode( '.', $field ); - - $sub_fields[ $name ] = $value; - } - } - - // iterate through top-level fields - foreach ( $data as $data_field => $data_value ) { - - // if a field has sub-fields and the top-level field has sub-fields to filter - if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) { - - // iterate through each sub-field - foreach ( $data_value as $sub_field => $sub_field_value ) { - - // remove non-matching sub-fields - if ( ! in_array( $sub_field, $sub_fields ) ) { - unset( $data[ $data_field ][ $sub_field ] ); - } - } - } else { - - // remove non-matching top-level fields - if ( ! in_array( $data_field, $fields ) ) { - unset( $data[ $data_field ] ); - } - } - } - - return $data; - } - - /** - * Delete a given resource - * - * @since 2.1 - * @param int $id the resource ID - * @param string $type the resource post type, or `customer` - * @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`) - * @return array|WP_Error - */ - protected function delete( $id, $type, $force = false ) { - - if ( 'shop_order' === $type || 'shop_coupon' === $type ) { - $resource_name = str_replace( 'shop_', '', $type ); - } else { - $resource_name = $type; - } - - if ( 'customer' === $type ) { - - $result = wp_delete_user( $id ); - - if ( $result ) { - return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) ); - } else { - return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) ); - } - } else { - - // delete order/coupon/product/webhook - $result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id ); - - if ( ! $result ) { - return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) ); - } - - if ( $force ) { - return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) ); - - } else { - - $this->server->send_status( '202' ); - - return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) ); - } - } - } - - - /** - * Checks if the given post is readable by the current user - * - * @since 2.1 - * @see WC_API_Resource::check_permission() - * @param WP_Post|int $post - * @return bool - */ - protected function is_readable( $post ) { - - return $this->check_permission( $post, 'read' ); - } - - /** - * Checks if the given post is editable by the current user - * - * @since 2.1 - * @see WC_API_Resource::check_permission() - * @param WP_Post|int $post - * @return bool - */ - protected function is_editable( $post ) { - - return $this->check_permission( $post, 'edit' ); - - } - - /** - * Checks if the given post is deletable by the current user - * - * @since 2.1 - * @see WC_API_Resource::check_permission() - * @param WP_Post|int $post - * @return bool - */ - protected function is_deletable( $post ) { - - return $this->check_permission( $post, 'delete' ); - } - - /** - * Checks the permissions for the current user given a post and context - * - * @since 2.1 - * @param WP_Post|int $post - * @param string $context the type of permission to check, either `read`, `write`, or `delete` - * @return bool true if the current user has the permissions to perform the context on the post - */ - private function check_permission( $post, $context ) { - $permission = false; - - if ( ! is_a( $post, 'WP_Post' ) ) { - $post = get_post( $post ); - } - - if ( is_null( $post ) ) { - return $permission; - } - - $post_type = get_post_type_object( $post->post_type ); - - if ( 'read' === $context ) { - $permission = 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID ); - } elseif ( 'edit' === $context ) { - $permission = current_user_can( $post_type->cap->edit_post, $post->ID ); - } elseif ( 'delete' === $context ) { - $permission = current_user_can( $post_type->cap->delete_post, $post->ID ); - } - - return apply_filters( 'woocommerce_api_check_permission', $permission, $context, $post, $post_type ); - } -} diff --git a/includes/legacy/api/v3/class-wc-api-taxes.php b/includes/legacy/api/v3/class-wc-api-taxes.php deleted file mode 100644 index 887a793a1dc..00000000000 --- a/includes/legacy/api/v3/class-wc-api-taxes.php +++ /dev/null @@ -1,649 +0,0 @@ - - * - * @since 2.1 - * @param array $routes - * @return array - */ - public function register_routes( $routes ) { - - # GET/POST /taxes - $routes[ $this->base ] = array( - array( array( $this, 'get_taxes' ), WC_API_Server::READABLE ), - array( array( $this, 'create_tax' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET /taxes/count - $routes[ $this->base . '/count' ] = array( - array( array( $this, 'get_taxes_count' ), WC_API_Server::READABLE ), - ); - - # GET/PUT/DELETE /taxes/ - $routes[ $this->base . '/(?P\d+)' ] = array( - array( array( $this, 'get_tax' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_tax' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ), - array( array( $this, 'delete_tax' ), WC_API_SERVER::DELETABLE ), - ); - - # GET/POST /taxes/classes - $routes[ $this->base . '/classes' ] = array( - array( array( $this, 'get_tax_classes' ), WC_API_Server::READABLE ), - array( array( $this, 'create_tax_class' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET /taxes/classes/count - $routes[ $this->base . '/classes/count' ] = array( - array( array( $this, 'get_tax_classes_count' ), WC_API_Server::READABLE ), - ); - - # GET /taxes/classes/ - $routes[ $this->base . '/classes/(?P\w[\w\s\-]*)' ] = array( - array( array( $this, 'delete_tax_class' ), WC_API_SERVER::DELETABLE ), - ); - - # POST|PUT /taxes/bulk - $routes[ $this->base . '/bulk' ] = array( - array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - ); - - return $routes; - } - - /** - * Get all taxes - * - * @since 2.5.0 - * - * @param string $fields - * @param array $filter - * @param string $class - * @param int $page - * - * @return array - */ - public function get_taxes( $fields = null, $filter = array(), $class = null, $page = 1 ) { - if ( ! empty( $class ) ) { - $filter['tax_rate_class'] = $class; - } - - $filter['page'] = $page; - - $query = $this->query_tax_rates( $filter ); - - $taxes = array(); - - foreach ( $query['results'] as $tax ) { - $taxes[] = current( $this->get_tax( $tax->tax_rate_id, $fields ) ); - } - - // Set pagination headers - $this->server->add_pagination_headers( $query['headers'] ); - - return array( 'taxes' => $taxes ); - } - - /** - * Get the tax for the given ID - * - * @since 2.5.0 - * - * @param int $id The tax ID - * @param string $fields fields to include in response - * - * @return array|WP_Error - */ - public function get_tax( $id, $fields = null ) { - global $wpdb; - - try { - $id = absint( $id ); - - // Permissions check - if ( ! current_user_can( 'manage_woocommerce' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax', __( 'You do not have permission to read tax rate', 'woocommerce' ), 401 ); - } - - // Get tax rate details - $tax = WC_Tax::_get_tax_rate( $id ); - - if ( is_wp_error( $tax ) || empty( $tax ) ) { - throw new WC_API_Exception( 'woocommerce_api_invalid_tax_id', __( 'A tax rate with the provided ID could not be found', 'woocommerce' ), 404 ); - } - - $tax_data = array( - 'id' => (int) $tax['tax_rate_id'], - 'country' => $tax['tax_rate_country'], - 'state' => $tax['tax_rate_state'], - 'postcode' => '', - 'city' => '', - 'rate' => $tax['tax_rate'], - 'name' => $tax['tax_rate_name'], - 'priority' => (int) $tax['tax_rate_priority'], - 'compound' => (bool) $tax['tax_rate_compound'], - 'shipping' => (bool) $tax['tax_rate_shipping'], - 'order' => (int) $tax['tax_rate_order'], - 'class' => $tax['tax_rate_class'] ? $tax['tax_rate_class'] : 'standard', - ); - - // Get locales from a tax rate - $locales = $wpdb->get_results( $wpdb->prepare( " - SELECT location_code, location_type - FROM {$wpdb->prefix}woocommerce_tax_rate_locations - WHERE tax_rate_id = %d - ", $id ) ); - - if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) { - foreach ( $locales as $locale ) { - $tax_data[ $locale->location_type ] = $locale->location_code; - } - } - - return array( 'tax' => apply_filters( 'woocommerce_api_tax_response', $tax_data, $tax, $fields, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Create a tax - * - * @since 2.5.0 - * - * @param array $data - * - * @return array|WP_Error - */ - public function create_tax( $data ) { - try { - if ( ! isset( $data['tax'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_tax_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'tax' ), 400 ); - } - - // Check permissions - if ( ! current_user_can( 'manage_woocommerce' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_tax', __( 'You do not have permission to create tax rates', 'woocommerce' ), 401 ); - } - - $data = apply_filters( 'woocommerce_api_create_tax_data', $data['tax'], $this ); - - $tax_data = array( - 'tax_rate_country' => '', - 'tax_rate_state' => '', - 'tax_rate' => '', - 'tax_rate_name' => '', - 'tax_rate_priority' => 1, - 'tax_rate_compound' => 0, - 'tax_rate_shipping' => 1, - 'tax_rate_order' => 0, - 'tax_rate_class' => '', - ); - - foreach ( $tax_data as $key => $value ) { - $new_key = str_replace( 'tax_rate_', '', $key ); - $new_key = 'tax_rate' === $new_key ? 'rate' : $new_key; - - if ( isset( $data[ $new_key ] ) ) { - if ( in_array( $new_key, array( 'compound', 'shipping' ) ) ) { - $tax_data[ $key ] = $data[ $new_key ] ? 1 : 0; - } else { - $tax_data[ $key ] = $data[ $new_key ]; - } - } - } - - // Create tax rate - $id = WC_Tax::_insert_tax_rate( $tax_data ); - - // Add locales - if ( ! empty( $data['postcode'] ) ) { - WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $data['postcode'] ) ); - } - - if ( ! empty( $data['city'] ) ) { - WC_Tax::_update_tax_rate_cities( $id, wc_clean( $data['city'] ) ); - } - - do_action( 'woocommerce_api_create_tax', $id, $data ); - - $this->server->send_status( 201 ); - - return $this->get_tax( $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Edit a tax - * - * @since 2.5.0 - * - * @param int $id The tax ID - * @param array $data - * - * @return array|WP_Error - */ - public function edit_tax( $id, $data ) { - try { - if ( ! isset( $data['tax'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_tax_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'tax' ), 400 ); - } - - // Check permissions - if ( ! current_user_can( 'manage_woocommerce' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_tax', __( 'You do not have permission to edit tax rates', 'woocommerce' ), 401 ); - } - - $data = $data['tax']; - - // Get current tax rate data - $tax = $this->get_tax( $id ); - - if ( is_wp_error( $tax ) ) { - $error_data = $tax->get_error_data(); - throw new WC_API_Exception( $tax->get_error_code(), $tax->get_error_message(), $error_data['status'] ); - } - - $current_data = $tax['tax']; - $data = apply_filters( 'woocommerce_api_edit_tax_data', $data, $this ); - $tax_data = array(); - $default_fields = array( - 'tax_rate_country', - 'tax_rate_state', - 'tax_rate', - 'tax_rate_name', - 'tax_rate_priority', - 'tax_rate_compound', - 'tax_rate_shipping', - 'tax_rate_order', - 'tax_rate_class', - ); - - foreach ( $data as $key => $value ) { - $new_key = 'rate' === $key ? 'tax_rate' : 'tax_rate_' . $key; - - // Check if the key is valid - if ( ! in_array( $new_key, $default_fields ) ) { - continue; - } - - // Test new data against current data - if ( $value === $current_data[ $key ] ) { - continue; - } - - // Fix compound and shipping values - if ( in_array( $key, array( 'compound', 'shipping' ) ) ) { - $value = $value ? 1 : 0; - } - - $tax_data[ $new_key ] = $value; - } - - // Update tax rate - WC_Tax::_update_tax_rate( $id, $tax_data ); - - // Update locales - if ( ! empty( $data['postcode'] ) && $current_data['postcode'] != $data['postcode'] ) { - WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $data['postcode'] ) ); - } - - if ( ! empty( $data['city'] ) && $current_data['city'] != $data['city'] ) { - WC_Tax::_update_tax_rate_cities( $id, wc_clean( $data['city'] ) ); - } - - do_action( 'woocommerce_api_edit_tax_rate', $id, $data ); - - return $this->get_tax( $id ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete a tax - * - * @since 2.5.0 - * - * @param int $id The tax ID - * - * @return array|WP_Error - */ - public function delete_tax( $id ) { - global $wpdb; - - try { - // Check permissions - if ( ! current_user_can( 'manage_woocommerce' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_tax', __( 'You do not have permission to delete tax rates', 'woocommerce' ), 401 ); - } - - $id = absint( $id ); - - WC_Tax::_delete_tax_rate( $id ); - - if ( 0 === $wpdb->rows_affected ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_delete_tax', __( 'Could not delete the tax rate', 'woocommerce' ), 401 ); - } - - return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'tax' ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get the total number of taxes - * - * @since 2.5.0 - * - * @param string $class - * @param array $filter - * - * @return array|WP_Error - */ - public function get_taxes_count( $class = null, $filter = array() ) { - try { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_taxes_count', __( 'You do not have permission to read the taxes count', 'woocommerce' ), 401 ); - } - - if ( ! empty( $class ) ) { - $filter['tax_rate_class'] = $class; - } - - $query = $this->query_tax_rates( $filter, true ); - - return array( 'count' => (int) $query['headers']->total ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Helper method to get tax rates objects - * - * @since 2.5.0 - * - * @param array $args - * @param bool $count_only - * - * @return array - */ - protected function query_tax_rates( $args, $count_only = false ) { - global $wpdb; - - $results = ''; - - // Set args - $args = $this->merge_query_args( $args, array() ); - - $query = " - SELECT tax_rate_id - FROM {$wpdb->prefix}woocommerce_tax_rates - WHERE 1 = 1 - "; - - // Filter by tax class - if ( ! empty( $args['tax_rate_class'] ) ) { - $tax_rate_class = 'standard' !== $args['tax_rate_class'] ? sanitize_title( $args['tax_rate_class'] ) : ''; - $query .= " AND tax_rate_class = '$tax_rate_class'"; - } - - // Order tax rates - $order_by = ' ORDER BY tax_rate_order'; - - // Pagination - $per_page = isset( $args['posts_per_page'] ) ? $args['posts_per_page'] : get_option( 'posts_per_page' ); - $offset = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $per_page : 0; - $pagination = sprintf( ' LIMIT %d, %d', $offset, $per_page ); - - if ( ! $count_only ) { - $results = $wpdb->get_results( $query . $order_by . $pagination ); - } - - $wpdb->get_results( $query ); - $headers = new stdClass; - $headers->page = $args['paged']; - $headers->total = (int) $wpdb->num_rows; - $headers->is_single = $per_page > $headers->total; - $headers->total_pages = ceil( $headers->total / $per_page ); - - return array( - 'results' => $results, - 'headers' => $headers, - ); - } - - /** - * Bulk update or insert taxes - * Accepts an array with taxes in the formats supported by - * WC_API_Taxes->create_tax() and WC_API_Taxes->edit_tax() - * - * @since 2.5.0 - * - * @param array $data - * - * @return array|WP_Error - */ - public function bulk( $data ) { - try { - if ( ! isset( $data['taxes'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_taxes_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'taxes' ), 400 ); - } - - $data = $data['taxes']; - $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'taxes' ); - - // Limit bulk operation - if ( count( $data ) > $limit ) { - throw new WC_API_Exception( 'woocommerce_api_taxes_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); - } - - $taxes = array(); - - foreach ( $data as $_tax ) { - $tax_id = 0; - - // Try to get the tax rate ID - if ( isset( $_tax['id'] ) ) { - $tax_id = intval( $_tax['id'] ); - } - - if ( $tax_id ) { - - // Tax rate exists / edit tax rate - $edit = $this->edit_tax( $tax_id, array( 'tax' => $_tax ) ); - - if ( is_wp_error( $edit ) ) { - $taxes[] = array( - 'id' => $tax_id, - 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), - ); - } else { - $taxes[] = $edit['tax']; - } - } else { - - // Tax rate don't exists / create tax rate - $new = $this->create_tax( array( 'tax' => $_tax ) ); - - if ( is_wp_error( $new ) ) { - $taxes[] = array( - 'id' => $tax_id, - 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), - ); - } else { - $taxes[] = $new['tax']; - } - } - } - - return array( 'taxes' => apply_filters( 'woocommerce_api_taxes_bulk_response', $taxes, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get all tax classes - * - * @since 2.5.0 - * - * @param string $fields - * - * @return array|WP_Error - */ - public function get_tax_classes( $fields = null ) { - try { - // Permissions check - if ( ! current_user_can( 'manage_woocommerce' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax_classes', __( 'You do not have permission to read tax classes', 'woocommerce' ), 401 ); - } - - $tax_classes = array(); - - // Add standard class - $tax_classes[] = array( - 'slug' => 'standard', - 'name' => __( 'Standard rate', 'woocommerce' ), - ); - - $classes = WC_Tax::get_tax_classes(); - - foreach ( $classes as $class ) { - $tax_classes[] = apply_filters( 'woocommerce_api_tax_class_response', array( - 'slug' => sanitize_title( $class ), - 'name' => $class, - ), $class, $fields, $this ); - } - - return array( 'tax_classes' => apply_filters( 'woocommerce_api_tax_classes_response', $tax_classes, $classes, $fields, $this ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Create a tax class. - * - * @since 2.5.0 - * - * @param array $data - * - * @return array|WP_Error - */ - public function create_tax_class( $data ) { - try { - if ( ! isset( $data['tax_class'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_tax_class_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'tax_class' ), 400 ); - } - - // Check permissions - if ( ! current_user_can( 'manage_woocommerce' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_tax_class', __( 'You do not have permission to create tax classes', 'woocommerce' ), 401 ); - } - - $data = $data['tax_class']; - - if ( empty( $data['name'] ) ) { - throw new WC_API_Exception( 'woocommerce_api_missing_tax_class_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); - } - - $name = sanitize_text_field( $data['name'] ); - $tax_class = WC_Tax::create_tax_class( $name ); - - if ( is_wp_error( $tax_class ) ) { - return new WP_Error( 'woocommerce_api_' . $tax_class->get_error_code(), $tax_class->get_error_message(), 401 ); - } - - do_action( 'woocommerce_api_create_tax_class', $tax_class['slug'], $data ); - - $this->server->send_status( 201 ); - - return array( - 'tax_class' => $tax_class, - ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete a tax class - * - * @since 2.5.0 - * - * @param int $slug The tax class slug - * - * @return array|WP_Error - */ - public function delete_tax_class( $slug ) { - global $wpdb; - - try { - // Check permissions - if ( ! current_user_can( 'manage_woocommerce' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_tax_class', __( 'You do not have permission to delete tax classes', 'woocommerce' ), 401 ); - } - - $slug = sanitize_title( $slug ); - $tax_class = WC_Tax::get_tax_class_by( 'slug', $slug ); - $deleted = WC_Tax::delete_tax_class_by( 'slug', $slug ); - - if ( is_wp_error( $deleted ) || ! $deleted ) { - throw new WC_API_Exception( 'woocommerce_api_cannot_delete_tax_class', __( 'Could not delete the tax class', 'woocommerce' ), 401 ); - } - - return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'tax_class' ) ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get the total number of tax classes - * - * @since 2.5.0 - * - * @return array|WP_Error - */ - public function get_tax_classes_count() { - try { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax_classes_count', __( 'You do not have permission to read the tax classes count', 'woocommerce' ), 401 ); - } - - $total = count( WC_Tax::get_tax_classes() ) + 1; // +1 for Standard Rate - - return array( 'count' => $total ); - } catch ( WC_API_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } -} diff --git a/includes/legacy/class-wc-legacy-api.php b/includes/legacy/class-wc-legacy-api.php deleted file mode 100644 index 5c86c6b9800..00000000000 --- a/includes/legacy/class-wc-legacy-api.php +++ /dev/null @@ -1,298 +0,0 @@ -query_vars['wc-api-version'] = $_GET['wc-api-version']; - } - - if ( ! empty( $_GET['wc-api-route'] ) ) { - $wp->query_vars['wc-api-route'] = $_GET['wc-api-route']; - } - - // REST API request. - if ( ! empty( $wp->query_vars['wc-api-version'] ) && ! empty( $wp->query_vars['wc-api-route'] ) ) { - - wc_maybe_define_constant( 'WC_API_REQUEST', true ); - wc_maybe_define_constant( 'WC_API_REQUEST_VERSION', absint( $wp->query_vars['wc-api-version'] ) ); - - // Legacy v1 API request. - if ( 1 === WC_API_REQUEST_VERSION ) { - $this->handle_v1_rest_api_request(); - } elseif ( 2 === WC_API_REQUEST_VERSION ) { - $this->handle_v2_rest_api_request(); - } else { - $this->includes(); - - $this->server = new WC_API_Server( $wp->query_vars['wc-api-route'] ); - - // load API resource classes. - $this->register_resources( $this->server ); - - // Fire off the request. - $this->server->serve_request(); - } - - exit; - } - } - - /** - * Include required files for REST API request. - * - * @since 2.1 - * @deprecated 2.6.0 - */ - public function includes() { - - // API server / response handlers. - include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-exception.php' ); - include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-server.php' ); - include_once( dirname( __FILE__ ) . '/api/v3/interface-wc-api-handler.php' ); - include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-json-handler.php' ); - - // Authentication. - include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-authentication.php' ); - $this->authentication = new WC_API_Authentication(); - - include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-resource.php' ); - include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-coupons.php' ); - include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-customers.php' ); - include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-orders.php' ); - include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-products.php' ); - include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-reports.php' ); - include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-taxes.php' ); - include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-webhooks.php' ); - - // Allow plugins to load other response handlers or resource classes. - do_action( 'woocommerce_api_loaded' ); - } - - /** - * Register available API resources. - * - * @since 2.1 - * @deprecated 2.6.0 - * @param WC_API_Server $server the REST server. - */ - public function register_resources( $server ) { - - $api_classes = apply_filters( 'woocommerce_api_classes', - array( - 'WC_API_Coupons', - 'WC_API_Customers', - 'WC_API_Orders', - 'WC_API_Products', - 'WC_API_Reports', - 'WC_API_Taxes', - 'WC_API_Webhooks', - ) - ); - - foreach ( $api_classes as $api_class ) { - $this->$api_class = new $api_class( $server ); - } - } - - - /** - * Handle legacy v1 REST API requests. - * - * @since 2.2 - * @deprecated 2.6.0 - */ - private function handle_v1_rest_api_request() { - - // Include legacy required files for v1 REST API request. - include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-server.php' ); - include_once( dirname( __FILE__ ) . '/api/v1/interface-wc-api-handler.php' ); - include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-json-handler.php' ); - include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-xml-handler.php' ); - - include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-authentication.php' ); - $this->authentication = new WC_API_Authentication(); - - include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-resource.php' ); - include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-coupons.php' ); - include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-customers.php' ); - include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-orders.php' ); - include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-products.php' ); - include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-reports.php' ); - - // Allow plugins to load other response handlers or resource classes. - do_action( 'woocommerce_api_loaded' ); - - $this->server = new WC_API_Server( $GLOBALS['wp']->query_vars['wc-api-route'] ); - - // Register available resources for legacy v1 REST API request. - $api_classes = apply_filters( 'woocommerce_api_classes', - array( - 'WC_API_Customers', - 'WC_API_Orders', - 'WC_API_Products', - 'WC_API_Coupons', - 'WC_API_Reports', - ) - ); - - foreach ( $api_classes as $api_class ) { - $this->$api_class = new $api_class( $this->server ); - } - - // Fire off the request. - $this->server->serve_request(); - } - - /** - * Handle legacy v2 REST API requests. - * - * @since 2.4 - * @deprecated 2.6.0 - */ - private function handle_v2_rest_api_request() { - include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-exception.php' ); - include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-server.php' ); - include_once( dirname( __FILE__ ) . '/api/v2/interface-wc-api-handler.php' ); - include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-json-handler.php' ); - - include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-authentication.php' ); - $this->authentication = new WC_API_Authentication(); - - include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-resource.php' ); - include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-coupons.php' ); - include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-customers.php' ); - include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-orders.php' ); - include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-products.php' ); - include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-reports.php' ); - include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-webhooks.php' ); - - // allow plugins to load other response handlers or resource classes. - do_action( 'woocommerce_api_loaded' ); - - $this->server = new WC_API_Server( $GLOBALS['wp']->query_vars['wc-api-route'] ); - - // Register available resources for legacy v2 REST API request. - $api_classes = apply_filters( 'woocommerce_api_classes', - array( - 'WC_API_Customers', - 'WC_API_Orders', - 'WC_API_Products', - 'WC_API_Coupons', - 'WC_API_Reports', - 'WC_API_Webhooks', - ) - ); - - foreach ( $api_classes as $api_class ) { - $this->$api_class = new $api_class( $this->server ); - } - - // Fire off the request. - $this->server->serve_request(); - } - - /** - * Rest API Init. - * - * @deprecated 3.7.0 - REST API clases autoload. - */ - public function rest_api_init() {} - - /** - * Include REST API classes. - * - * @deprecated 3.7.0 - REST API clases autoload. - */ - public function rest_api_includes() { - $this->rest_api_init(); - } - /** - * Register REST API routes. - * - * @deprecated 3.7.0 - */ - public function register_rest_routes() { - wc_deprecated_function( 'WC_Legacy_API::register_rest_routes', '3.7.0', '' ); - $this->register_wp_admin_settings(); - } -} diff --git a/includes/log-handlers/class-wc-log-handler-file.php b/includes/log-handlers/class-wc-log-handler-file.php deleted file mode 100644 index 0fc59a36a2d..00000000000 --- a/includes/log-handlers/class-wc-log-handler-file.php +++ /dev/null @@ -1,444 +0,0 @@ -log_size_limit = apply_filters( 'woocommerce_log_file_size_limit', $log_size_limit ); - - add_action( 'plugins_loaded', array( $this, 'write_cached_logs' ) ); - } - - /** - * Destructor. - * - * Cleans up open file handles. - */ - public function __destruct() { - foreach ( $this->handles as $handle ) { - if ( is_resource( $handle ) ) { - fclose( $handle ); // @codingStandardsIgnoreLine. - } - } - } - - /** - * Handle a log entry. - * - * @param int $timestamp Log timestamp. - * @param string $level emergency|alert|critical|error|warning|notice|info|debug. - * @param string $message Log message. - * @param array $context { - * Additional information for log handlers. - * - * @type string $source Optional. Determines log file to write to. Default 'log'. - * @type bool $_legacy Optional. Default false. True to use outdated log format - * originally used in deprecated WC_Logger::add calls. - * } - * - * @return bool False if value was not handled and true if value was handled. - */ - public function handle( $timestamp, $level, $message, $context ) { - - if ( isset( $context['source'] ) && $context['source'] ) { - $handle = $context['source']; - } else { - $handle = 'log'; - } - - $entry = self::format_entry( $timestamp, $level, $message, $context ); - - return $this->add( $entry, $handle ); - } - - /** - * Builds a log entry text from timestamp, level and message. - * - * @param int $timestamp Log timestamp. - * @param string $level emergency|alert|critical|error|warning|notice|info|debug. - * @param string $message Log message. - * @param array $context Additional information for log handlers. - * - * @return string Formatted log entry. - */ - protected static function format_entry( $timestamp, $level, $message, $context ) { - - if ( isset( $context['_legacy'] ) && true === $context['_legacy'] ) { - if ( isset( $context['source'] ) && $context['source'] ) { - $handle = $context['source']; - } else { - $handle = 'log'; - } - $message = apply_filters( 'woocommerce_logger_add_message', $message, $handle ); - $time = date_i18n( 'm-d-Y @ H:i:s' ); - $entry = "{$time} - {$message}"; - } else { - $entry = parent::format_entry( $timestamp, $level, $message, $context ); - } - - return $entry; - } - - /** - * Open log file for writing. - * - * @param string $handle Log handle. - * @param string $mode Optional. File mode. Default 'a'. - * @return bool Success. - */ - protected function open( $handle, $mode = 'a' ) { - if ( $this->is_open( $handle ) ) { - return true; - } - - $file = self::get_log_file_path( $handle ); - - if ( $file ) { - if ( ! file_exists( $file ) ) { - $temphandle = @fopen( $file, 'w+' ); // @codingStandardsIgnoreLine. - @fclose( $temphandle ); // @codingStandardsIgnoreLine. - - if ( Constants::is_defined( 'FS_CHMOD_FILE' ) ) { - @chmod( $file, FS_CHMOD_FILE ); // @codingStandardsIgnoreLine. - } - } - - $resource = @fopen( $file, $mode ); // @codingStandardsIgnoreLine. - - if ( $resource ) { - $this->handles[ $handle ] = $resource; - return true; - } - } - - return false; - } - - /** - * Check if a handle is open. - * - * @param string $handle Log handle. - * @return bool True if $handle is open. - */ - protected function is_open( $handle ) { - return array_key_exists( $handle, $this->handles ) && is_resource( $this->handles[ $handle ] ); - } - - /** - * Close a handle. - * - * @param string $handle Log handle. - * @return bool success - */ - protected function close( $handle ) { - $result = false; - - if ( $this->is_open( $handle ) ) { - $result = fclose( $this->handles[ $handle ] ); // @codingStandardsIgnoreLine. - unset( $this->handles[ $handle ] ); - } - - return $result; - } - - /** - * Add a log entry to chosen file. - * - * @param string $entry Log entry text. - * @param string $handle Log entry handle. - * - * @return bool True if write was successful. - */ - protected function add( $entry, $handle ) { - $result = false; - - if ( $this->should_rotate( $handle ) ) { - $this->log_rotate( $handle ); - } - - if ( $this->open( $handle ) && is_resource( $this->handles[ $handle ] ) ) { - $result = fwrite( $this->handles[ $handle ], $entry . PHP_EOL ); // @codingStandardsIgnoreLine. - } else { - $this->cache_log( $entry, $handle ); - } - - return false !== $result; - } - - /** - * Clear entries from chosen file. - * - * @param string $handle Log handle. - * - * @return bool - */ - public function clear( $handle ) { - $result = false; - - // Close the file if it's already open. - $this->close( $handle ); - - /** - * $this->open( $handle, 'w' ) == Open the file for writing only. Place the file pointer at - * the beginning of the file, and truncate the file to zero length. - */ - if ( $this->open( $handle, 'w' ) && is_resource( $this->handles[ $handle ] ) ) { - $result = true; - } - - do_action( 'woocommerce_log_clear', $handle ); - - return $result; - } - - /** - * Remove/delete the chosen file. - * - * @param string $handle Log handle. - * - * @return bool - */ - public function remove( $handle ) { - $removed = false; - $logs = $this->get_log_files(); - $handle = sanitize_title( $handle ); - - if ( isset( $logs[ $handle ] ) && $logs[ $handle ] ) { - $file = realpath( trailingslashit( WC_LOG_DIR ) . $logs[ $handle ] ); - if ( 0 === stripos( $file, realpath( trailingslashit( WC_LOG_DIR ) ) ) && is_file( $file ) && is_writable( $file ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable - $this->close( $file ); // Close first to be certain no processes keep it alive after it is unlinked. - $removed = unlink( $file ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink - } - do_action( 'woocommerce_log_remove', $handle, $removed ); - } - return $removed; - } - - /** - * Check if log file should be rotated. - * - * Compares the size of the log file to determine whether it is over the size limit. - * - * @param string $handle Log handle. - * @return bool True if if should be rotated. - */ - protected function should_rotate( $handle ) { - $file = self::get_log_file_path( $handle ); - if ( $file ) { - if ( $this->is_open( $handle ) ) { - $file_stat = fstat( $this->handles[ $handle ] ); - return $file_stat['size'] > $this->log_size_limit; - } elseif ( file_exists( $file ) ) { - return filesize( $file ) > $this->log_size_limit; - } else { - return false; - } - } else { - return false; - } - } - - /** - * Rotate log files. - * - * Logs are rotated by prepending '.x' to the '.log' suffix. - * The current log plus 10 historical logs are maintained. - * For example: - * base.9.log -> [ REMOVED ] - * base.8.log -> base.9.log - * ... - * base.0.log -> base.1.log - * base.log -> base.0.log - * - * @param string $handle Log handle. - */ - protected function log_rotate( $handle ) { - for ( $i = 8; $i >= 0; $i-- ) { - $this->increment_log_infix( $handle, $i ); - } - $this->increment_log_infix( $handle ); - } - - /** - * Increment a log file suffix. - * - * @param string $handle Log handle. - * @param null|int $number Optional. Default null. Log suffix number to be incremented. - * @return bool True if increment was successful, otherwise false. - */ - protected function increment_log_infix( $handle, $number = null ) { - if ( null === $number ) { - $suffix = ''; - $next_suffix = '.0'; - } else { - $suffix = '.' . $number; - $next_suffix = '.' . ( $number + 1 ); - } - - $rename_from = self::get_log_file_path( "{$handle}{$suffix}" ); - $rename_to = self::get_log_file_path( "{$handle}{$next_suffix}" ); - - if ( $this->is_open( $rename_from ) ) { - $this->close( $rename_from ); - } - - if ( is_writable( $rename_from ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable - return rename( $rename_from, $rename_to ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_rename - } else { - return false; - } - - } - - /** - * Get a log file path. - * - * @param string $handle Log name. - * @return bool|string The log file path or false if path cannot be determined. - */ - public static function get_log_file_path( $handle ) { - if ( function_exists( 'wp_hash' ) ) { - return trailingslashit( WC_LOG_DIR ) . self::get_log_file_name( $handle ); - } else { - wc_doing_it_wrong( __METHOD__, __( 'This method should not be called before plugins_loaded.', 'woocommerce' ), '3.0' ); - return false; - } - } - - /** - * Get a log file name. - * - * File names consist of the handle, followed by the date, followed by a hash, .log. - * - * @since 3.3 - * @param string $handle Log name. - * @return bool|string The log file name or false if cannot be determined. - */ - public static function get_log_file_name( $handle ) { - if ( function_exists( 'wp_hash' ) ) { - $date_suffix = date( 'Y-m-d', time() ); - $hash_suffix = wp_hash( $handle ); - return sanitize_file_name( implode( '-', array( $handle, $date_suffix, $hash_suffix ) ) . '.log' ); - } else { - wc_doing_it_wrong( __METHOD__, __( 'This method should not be called before plugins_loaded.', 'woocommerce' ), '3.3' ); - return false; - } - } - - /** - * Cache log to write later. - * - * @param string $entry Log entry text. - * @param string $handle Log entry handle. - */ - protected function cache_log( $entry, $handle ) { - $this->cached_logs[] = array( - 'entry' => $entry, - 'handle' => $handle, - ); - } - - /** - * Write cached logs. - */ - public function write_cached_logs() { - foreach ( $this->cached_logs as $log ) { - $this->add( $log['entry'], $log['handle'] ); - } - } - - /** - * Delete all logs older than a defined timestamp. - * - * @since 3.4.0 - * @param integer $timestamp Timestamp to delete logs before. - */ - public static function delete_logs_before_timestamp( $timestamp = 0 ) { - if ( ! $timestamp ) { - return; - } - - $log_files = self::get_log_files(); - - foreach ( $log_files as $log_file ) { - $last_modified = filemtime( trailingslashit( WC_LOG_DIR ) . $log_file ); - - if ( $last_modified < $timestamp ) { - @unlink( trailingslashit( WC_LOG_DIR ) . $log_file ); // @codingStandardsIgnoreLine. - } - } - } - - /** - * Get all log files in the log directory. - * - * @since 3.4.0 - * @return array - */ - public static function get_log_files() { - $files = @scandir( WC_LOG_DIR ); // @codingStandardsIgnoreLine. - $result = array(); - - if ( ! empty( $files ) ) { - foreach ( $files as $key => $value ) { - if ( ! in_array( $value, array( '.', '..' ), true ) ) { - if ( ! is_dir( $value ) && strstr( $value, '.log' ) ) { - $result[ sanitize_title( $value ) ] = $value; - } - } - } - } - - return $result; - } -} diff --git a/includes/queue/class-wc-action-queue.php b/includes/queue/class-wc-action-queue.php deleted file mode 100644 index 88bccaeceef..00000000000 --- a/includes/queue/class-wc-action-queue.php +++ /dev/null @@ -1,160 +0,0 @@ -schedule_single( time(), $hook, $args, $group ); - } - - /** - * Schedule an action to run once at some time in the future - * - * @param int $timestamp When the job will run. - * @param string $hook The hook to trigger. - * @param array $args Arguments to pass when the hook triggers. - * @param string $group The group to assign this job to. - * @return string The action ID. - */ - public function schedule_single( $timestamp, $hook, $args = array(), $group = '' ) { - return as_schedule_single_action( $timestamp, $hook, $args, $group ); - } - - /** - * Schedule a recurring action - * - * @param int $timestamp When the first instance of the job will run. - * @param int $interval_in_seconds How long to wait between runs. - * @param string $hook The hook to trigger. - * @param array $args Arguments to pass when the hook triggers. - * @param string $group The group to assign this job to. - * @return string The action ID. - */ - public function schedule_recurring( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '' ) { - return as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args, $group ); - } - - /** - * Schedule an action that recurs on a cron-like schedule. - * - * @param int $timestamp The schedule will start on or after this time. - * @param string $cron_schedule A cron-link schedule string. - * @see http://en.wikipedia.org/wiki/Cron - * * * * * * * - * ┬ ┬ ┬ ┬ ┬ ┬ - * | | | | | | - * | | | | | + year [optional] - * | | | | +----- day of week (0 - 7) (Sunday=0 or 7) - * | | | +---------- month (1 - 12) - * | | +--------------- day of month (1 - 31) - * | +-------------------- hour (0 - 23) - * +------------------------- min (0 - 59) - * @param string $hook The hook to trigger. - * @param array $args Arguments to pass when the hook triggers. - * @param string $group The group to assign this job to. - * @return string The action ID - */ - public function schedule_cron( $timestamp, $cron_schedule, $hook, $args = array(), $group = '' ) { - return as_schedule_cron_action( $timestamp, $cron_schedule, $hook, $args, $group ); - } - - /** - * Dequeue the next scheduled instance of an action with a matching hook (and optionally matching args and group). - * - * Any recurring actions with a matching hook should also be cancelled, not just the next scheduled action. - * - * While technically only the next instance of a recurring or cron action is unscheduled by this method, that will also - * prevent all future instances of that recurring or cron action from being run. Recurring and cron actions are scheduled - * in a sequence instead of all being scheduled at once. Each successive occurrence of a recurring action is scheduled - * only after the former action is run. As the next instance is never run, because it's unscheduled by this function, - * then the following instance will never be scheduled (or exist), which is effectively the same as being unscheduled - * by this method also. - * - * @param string $hook The hook that the job will trigger. - * @param array $args Args that would have been passed to the job. - * @param string $group The group the job is assigned to (if any). - */ - public function cancel( $hook, $args = array(), $group = '' ) { - as_unschedule_action( $hook, $args, $group ); - } - - /** - * Dequeue all actions with a matching hook (and optionally matching args and group) so no matching actions are ever run. - * - * @param string $hook The hook that the job will trigger. - * @param array $args Args that would have been passed to the job. - * @param string $group The group the job is assigned to (if any). - */ - public function cancel_all( $hook, $args = array(), $group = '' ) { - as_unschedule_all_actions( $hook, $args, $group ); - } - - /** - * Get the date and time for the next scheduled occurence of an action with a given hook - * (an optionally that matches certain args and group), if any. - * - * @param string $hook The hook that the job will trigger. - * @param array $args Filter to a hook with matching args that will be passed to the job when it runs. - * @param string $group Filter to only actions assigned to a specific group. - * @return WC_DateTime|null The date and time for the next occurrence, or null if there is no pending, scheduled action for the given hook. - */ - public function get_next( $hook, $args = null, $group = '' ) { - - $next_timestamp = as_next_scheduled_action( $hook, $args, $group ); - - if ( is_numeric( $next_timestamp ) ) { - return new WC_DateTime( "@{$next_timestamp}", new DateTimeZone( 'UTC' ) ); - } - - return null; - } - - /** - * Find scheduled actions - * - * @param array $args Possible arguments, with their default values: - * 'hook' => '' - the name of the action that will be triggered - * 'args' => null - the args array that will be passed with the action - * 'date' => null - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. - * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '=' - * 'modified' => null - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. - * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '=' - * 'group' => '' - the group the action belongs to - * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING - * 'claimed' => null - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID - * 'per_page' => 5 - Number of results to return - * 'offset' => 0 - * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', or 'date' - * 'order' => 'ASC'. - * - * @param string $return_format OBJECT, ARRAY_A, or ids. - * @return array - */ - public function search( $args = array(), $return_format = OBJECT ) { - return as_get_scheduled_actions( $args, $return_format ); - } -} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php deleted file mode 100644 index 8a07bb36705..00000000000 --- a/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php +++ /dev/null @@ -1,924 +0,0 @@ -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' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( - 'email' => array( - 'required' => true, - 'type' => 'string', - 'description' => __( 'New user email address.', 'woocommerce' ), - ), - 'username' => array( - 'required' => 'no' === get_option( 'woocommerce_registration_generate_username', 'yes' ), - 'description' => __( 'New user username.', 'woocommerce' ), - 'type' => 'string', - ), - 'password' => array( - 'required' => 'no' === get_option( 'woocommerce_registration_generate_password', 'no' ), - 'description' => __( 'New user password.', 'woocommerce' ), - 'type' => 'string', - ), - ) ), - ), - '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.', 'woocommerce' ), - 'type' => 'integer', - ), - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), - ), - ), - 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 ), - ), - array( - 'methods' => WP_REST_Server::DELETABLE, - 'callback' => array( $this, 'delete_item' ), - 'permission_callback' => array( $this, 'delete_item_permissions_check' ), - 'args' => array( - 'force' => array( - 'default' => false, - 'type' => 'boolean', - 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), - ), - 'reassign' => array( - 'default' => 0, - 'type' => 'integer', - 'description' => __( 'ID to reassign posts to.', 'woocommerce' ), - ), - ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) ); - - register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'batch_items' ), - 'permission_callback' => array( $this, 'batch_items_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), - ), - 'schema' => array( $this, 'get_public_batch_schema' ), - ) ); - } - - /** - * Check whether a given request has permission to read customers. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_items_permissions_check( $request ) { - if ( ! wc_rest_check_user_permissions( 'read' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access create customers. - * - * @param WP_REST_Request $request Full details about the request. - * - * @return bool|WP_Error - */ - public function create_item_permissions_check( $request ) { - if ( ! wc_rest_check_user_permissions( 'create' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access to read a customer. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_item_permissions_check( $request ) { - $id = (int) $request['id']; - - if ( ! wc_rest_check_user_permissions( 'read', $id ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access update a customer. - * - * @param WP_REST_Request $request Full details about the request. - * - * @return bool|WP_Error - */ - public function update_item_permissions_check( $request ) { - $id = (int) $request['id']; - - if ( ! wc_rest_check_user_permissions( 'edit', $id ) ) { - return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access delete a customer. - * - * @param WP_REST_Request $request Full details about the request. - * - * @return bool|WP_Error - */ - public function delete_item_permissions_check( $request ) { - $id = (int) $request['id']; - - if ( ! wc_rest_check_user_permissions( 'delete', $id ) ) { - return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access batch create, update and delete items. - * - * @param WP_REST_Request $request Full details about the request. - * - * @return bool|WP_Error - */ - public function batch_items_permissions_check( $request ) { - if ( ! wc_rest_check_user_permissions( 'batch' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Get all customers. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function get_items( $request ) { - $prepared_args = array(); - $prepared_args['exclude'] = $request['exclude']; - $prepared_args['include'] = $request['include']; - $prepared_args['order'] = $request['order']; - $prepared_args['number'] = $request['per_page']; - if ( ! empty( $request['offset'] ) ) { - $prepared_args['offset'] = $request['offset']; - } else { - $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; - } - $orderby_possibles = array( - 'id' => 'ID', - 'include' => 'include', - 'name' => 'display_name', - 'registered_date' => 'registered', - ); - $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; - $prepared_args['search'] = $request['search']; - - if ( '' !== $prepared_args['search'] ) { - $prepared_args['search'] = '*' . $prepared_args['search'] . '*'; - } - - // Filter by email. - if ( ! empty( $request['email'] ) ) { - $prepared_args['search'] = $request['email']; - $prepared_args['search_columns'] = array( 'user_email' ); - } - - // Filter by role. - if ( 'all' !== $request['role'] ) { - $prepared_args['role'] = $request['role']; - } - - /** - * Filter arguments, before passing to WP_User_Query, when querying users via the REST API. - * - * @see https://developer.wordpress.org/reference/classes/wp_user_query/ - * - * @param array $prepared_args Array of arguments for WP_User_Query. - * @param WP_REST_Request $request The current request. - */ - $prepared_args = apply_filters( 'woocommerce_rest_customer_query', $prepared_args, $request ); - - $query = new WP_User_Query( $prepared_args ); - - $users = array(); - foreach ( $query->results as $user ) { - $data = $this->prepare_item_for_response( $user, $request ); - $users[] = $this->prepare_response_for_collection( $data ); - } - - $response = rest_ensure_response( $users ); - - // Store pagination values for headers then unset for count query. - $per_page = (int) $prepared_args['number']; - $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); - - $prepared_args['fields'] = 'ID'; - - $total_users = $query->get_total(); - if ( $total_users < 1 ) { - // Out-of-bounds, run the query again without LIMIT for total count. - unset( $prepared_args['number'] ); - unset( $prepared_args['offset'] ); - $count_query = new WP_User_Query( $prepared_args ); - $total_users = $count_query->get_total(); - } - $response->header( 'X-WP-Total', (int) $total_users ); - $max_pages = ceil( $total_users / $per_page ); - $response->header( 'X-WP-TotalPages', (int) $max_pages ); - - $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); - if ( $page > 1 ) { - $prev_page = $page - 1; - if ( $prev_page > $max_pages ) { - $prev_page = $max_pages; - } - $prev_link = add_query_arg( 'page', $prev_page, $base ); - $response->link_header( 'prev', $prev_link ); - } - if ( $max_pages > $page ) { - $next_page = $page + 1; - $next_link = add_query_arg( 'page', $next_page, $base ); - $response->link_header( 'next', $next_link ); - } - - return $response; - } - - /** - * Create a single customer. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function create_item( $request ) { - try { - if ( ! empty( $request['id'] ) ) { - throw new WC_REST_Exception( 'woocommerce_rest_customer_exists', __( 'Cannot create existing resource.', 'woocommerce' ), 400 ); - } - - // Sets the username. - $request['username'] = ! empty( $request['username'] ) ? $request['username'] : ''; - - // Sets the password. - $request['password'] = ! empty( $request['password'] ) ? $request['password'] : ''; - - // Create customer. - $customer = new WC_Customer; - $customer->set_username( $request['username'] ); - $customer->set_password( $request['password'] ); - $customer->set_email( $request['email'] ); - $this->update_customer_meta_fields( $customer, $request ); - $customer->save(); - - if ( ! $customer->get_id() ) { - throw new WC_REST_Exception( 'woocommerce_rest_cannot_create', __( 'This resource cannot be created.', 'woocommerce' ), 400 ); - } - - $user_data = get_userdata( $customer->get_id() ); - $this->update_additional_fields_for_object( $user_data, $request ); - - /** - * Fires after a customer is created or updated via the REST API. - * - * @param WP_User $user_data Data used to create the customer. - * @param WP_REST_Request $request Request object. - * @param boolean $creating True when creating customer, false when updating customer. - */ - do_action( 'woocommerce_rest_insert_customer', $user_data, $request, true ); - - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( $user_data, $request ); - $response = rest_ensure_response( $response ); - $response->set_status( 201 ); - $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer->get_id() ) ) ); - - return $response; - } catch ( Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get a single customer. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function get_item( $request ) { - $id = (int) $request['id']; - $user_data = get_userdata( $id ); - - if ( empty( $id ) || empty( $user_data->ID ) ) { - return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - $customer = $this->prepare_item_for_response( $user_data, $request ); - $response = rest_ensure_response( $customer ); - - return $response; - } - - /** - * Update a single user. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function update_item( $request ) { - try { - $id = (int) $request['id']; - $customer = new WC_Customer( $id ); - - if ( ! $customer->get_id() ) { - throw new WC_REST_Exception( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), 400 ); - } - - if ( ! empty( $request['email'] ) && email_exists( $request['email'] ) && $request['email'] !== $customer->get_email() ) { - throw new WC_REST_Exception( 'woocommerce_rest_customer_invalid_email', __( 'Email address is invalid.', 'woocommerce' ), 400 ); - } - - if ( ! empty( $request['username'] ) && $request['username'] !== $customer->get_username() ) { - throw new WC_REST_Exception( 'woocommerce_rest_customer_invalid_argument', __( "Username isn't editable.", 'woocommerce' ), 400 ); - } - - // Customer email. - if ( isset( $request['email'] ) ) { - $customer->set_email( sanitize_email( $request['email'] ) ); - } - - // Customer password. - if ( isset( $request['password'] ) ) { - $customer->set_password( $request['password'] ); - } - - $this->update_customer_meta_fields( $customer, $request ); - $customer->save(); - - $user_data = get_userdata( $customer->get_id() ); - $this->update_additional_fields_for_object( $user_data, $request ); - - if ( ! is_user_member_of_blog( $user_data->ID ) ) { - $user_data->add_role( 'customer' ); - } - - /** - * Fires after a customer is created or updated via the REST API. - * - * @param WP_User $customer Data used to create the customer. - * @param WP_REST_Request $request Request object. - * @param boolean $creating True when creating customer, false when updating customer. - */ - do_action( 'woocommerce_rest_insert_customer', $user_data, $request, false ); - - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( $user_data, $request ); - $response = rest_ensure_response( $response ); - return $response; - } catch ( Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Delete a single customer. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function delete_item( $request ) { - $id = (int) $request['id']; - $reassign = isset( $request['reassign'] ) ? absint( $request['reassign'] ) : null; - $force = isset( $request['force'] ) ? (bool) $request['force'] : false; - - // We don't support trashing for this type, error out. - if ( ! $force ) { - return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Customers do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); - } - - $user_data = get_userdata( $id ); - if ( ! $user_data ) { - return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); - } - - if ( ! empty( $reassign ) ) { - if ( $reassign === $id || ! get_userdata( $reassign ) ) { - return new WP_Error( 'woocommerce_rest_customer_invalid_reassign', __( 'Invalid resource id for reassignment.', 'woocommerce' ), array( 'status' => 400 ) ); - } - } - - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( $user_data, $request ); - - /** Include admin customer functions to get access to wp_delete_user() */ - require_once ABSPATH . 'wp-admin/includes/user.php'; - - $customer = new WC_Customer( $id ); - - if ( ! is_null( $reassign ) ) { - $result = $customer->delete_and_reassign( $reassign ); - } else { - $result = $customer->delete(); - } - - if ( ! $result ) { - return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); - } - - /** - * Fires after a customer is deleted via the REST API. - * - * @param WP_User $user_data User data. - * @param WP_REST_Response $response The response returned from the API. - * @param WP_REST_Request $request The request sent to the API. - */ - do_action( 'woocommerce_rest_delete_customer', $user_data, $response, $request ); - - return $response; - } - - /** - * Prepare a single customer output for response. - * - * @param WP_User $user_data User object. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response $response Response data. - */ - public function prepare_item_for_response( $user_data, $request ) { - $customer = new WC_Customer( $user_data->ID ); - $_data = $customer->get_data(); - $last_order = wc_get_customer_last_order( $customer->get_id() ); - $format_date = array( 'date_created', 'date_modified' ); - - // Format date values. - foreach ( $format_date as $key ) { - $_data[ $key ] = $_data[ $key ] ? wc_rest_prepare_date_response( $_data[ $key ] ) : null; // v1 API used UTC. - } - - $data = array( - 'id' => $_data['id'], - 'date_created' => $_data['date_created'], - 'date_modified' => $_data['date_modified'], - 'email' => $_data['email'], - 'first_name' => $_data['first_name'], - 'last_name' => $_data['last_name'], - 'username' => $_data['username'], - 'last_order' => array( - 'id' => is_object( $last_order ) ? $last_order->get_id() : null, - 'date' => is_object( $last_order ) ? wc_rest_prepare_date_response( $last_order->get_date_created() ) : null, // v1 API used UTC. - ), - 'orders_count' => $customer->get_order_count(), - 'total_spent' => $customer->get_total_spent(), - 'avatar_url' => $customer->get_avatar_url(), - 'billing' => $_data['billing'], - 'shipping' => $_data['shipping'], - ); - - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); - $response = rest_ensure_response( $data ); - $response->add_links( $this->prepare_links( $user_data ) ); - - /** - * Filter customer data returned from the REST API. - * - * @param WP_REST_Response $response The response object. - * @param WP_User $user_data User object used to create response. - * @param WP_REST_Request $request Request object. - */ - return apply_filters( 'woocommerce_rest_prepare_customer', $response, $user_data, $request ); - } - - /** - * Update customer meta fields. - * - * @param WC_Customer $customer - * @param WP_REST_Request $request - */ - protected function update_customer_meta_fields( $customer, $request ) { - $schema = $this->get_item_schema(); - - // Customer first name. - if ( isset( $request['first_name'] ) ) { - $customer->set_first_name( wc_clean( $request['first_name'] ) ); - } - - // Customer last name. - if ( isset( $request['last_name'] ) ) { - $customer->set_last_name( wc_clean( $request['last_name'] ) ); - } - - // Customer billing address. - if ( isset( $request['billing'] ) ) { - foreach ( array_keys( $schema['properties']['billing']['properties'] ) as $field ) { - if ( isset( $request['billing'][ $field ] ) && is_callable( array( $customer, "set_billing_{$field}" ) ) ) { - $customer->{"set_billing_{$field}"}( $request['billing'][ $field ] ); - } - } - } - - // Customer shipping address. - if ( isset( $request['shipping'] ) ) { - foreach ( array_keys( $schema['properties']['shipping']['properties'] ) as $field ) { - if ( isset( $request['shipping'][ $field ] ) && is_callable( array( $customer, "set_shipping_{$field}" ) ) ) { - $customer->{"set_shipping_{$field}"}( $request['shipping'][ $field ] ); - } - } - } - } - - /** - * Prepare links for the request. - * - * @param WP_User $customer Customer object. - * @return array Links for the given customer. - */ - protected function prepare_links( $customer ) { - $links = array( - 'self' => array( - 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer->ID ) ), - ), - 'collection' => array( - 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), - ), - ); - - return $links; - } - - /** - * Get the Customer's schema, conforming to JSON Schema. - * - * @return array - */ - public function get_item_schema() { - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'customer', - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_created' => array( - 'description' => __( 'The date the customer was created, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_modified' => array( - 'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'email' => array( - 'description' => __( 'The email address for the customer.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'email', - 'context' => array( 'view', 'edit' ), - ), - 'first_name' => array( - 'description' => __( 'Customer first name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'arg_options' => array( - 'sanitize_callback' => 'sanitize_text_field', - ), - ), - 'last_name' => array( - 'description' => __( 'Customer last name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'arg_options' => array( - 'sanitize_callback' => 'sanitize_text_field', - ), - ), - 'username' => array( - 'description' => __( 'Customer login name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'arg_options' => array( - 'sanitize_callback' => 'sanitize_user', - ), - ), - 'password' => array( - 'description' => __( 'Customer password.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'edit' ), - ), - 'last_order' => array( - 'description' => __( 'Last order data.', 'woocommerce' ), - 'type' => 'object', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - 'properties' => array( - 'id' => array( - 'description' => __( 'Last order ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date' => array( - 'description' => __( 'The date of the customer last order, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ), - 'orders_count' => array( - 'description' => __( 'Quantity of orders made by the customer.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'total_spent' => array( - 'description' => __( 'Total amount spent.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'avatar_url' => array( - 'description' => __( 'Avatar URL.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'billing' => array( - 'description' => __( 'List of billing address data.', 'woocommerce' ), - 'type' => 'object', - 'context' => array( 'view', 'edit' ), - 'properties' => array( - 'first_name' => array( - 'description' => __( 'First name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'last_name' => array( - 'description' => __( 'Last name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'company' => array( - 'description' => __( 'Company name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'address_1' => array( - 'description' => __( 'Address line 1.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'address_2' => array( - 'description' => __( 'Address line 2.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'city' => array( - 'description' => __( 'City name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'state' => array( - 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'postcode' => array( - 'description' => __( 'Postal code.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'country' => array( - 'description' => __( 'ISO code of the country.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'email' => array( - 'description' => __( 'Email address.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'email', - 'context' => array( 'view', 'edit' ), - ), - 'phone' => array( - 'description' => __( 'Phone number.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - 'shipping' => array( - 'description' => __( 'List of shipping address data.', 'woocommerce' ), - 'type' => 'object', - 'context' => array( 'view', 'edit' ), - 'properties' => array( - 'first_name' => array( - 'description' => __( 'First name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'last_name' => array( - 'description' => __( 'Last name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'company' => array( - 'description' => __( 'Company name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'address_1' => array( - 'description' => __( 'Address line 1.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'address_2' => array( - 'description' => __( 'Address line 2.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'city' => array( - 'description' => __( 'City name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'state' => array( - 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'postcode' => array( - 'description' => __( 'Postal code.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'country' => array( - 'description' => __( 'ISO code of the country.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - ); - - return $this->add_additional_fields_schema( $schema ); - } - - /** - * Get role names. - * - * @return array - */ - protected function get_role_names() { - global $wp_roles; - - return array_keys( $wp_roles->role_names ); - } - - /** - * Get the query params for collections. - * - * @return array - */ - public function get_collection_params() { - $params = parent::get_collection_params(); - - $params['context']['default'] = 'view'; - - $params['exclude'] = array( - 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'default' => array(), - 'sanitize_callback' => 'wp_parse_id_list', - ); - $params['include'] = array( - 'description' => __( 'Limit result set to specific IDs.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'default' => array(), - 'sanitize_callback' => 'wp_parse_id_list', - ); - $params['offset'] = array( - 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), - 'type' => 'integer', - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['order'] = array( - 'default' => 'asc', - 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), - 'enum' => array( 'asc', 'desc' ), - 'sanitize_callback' => 'sanitize_key', - 'type' => 'string', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['orderby'] = array( - 'default' => 'name', - 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), - 'enum' => array( - 'id', - 'include', - 'name', - 'registered_date', - ), - 'sanitize_callback' => 'sanitize_key', - 'type' => 'string', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['email'] = array( - 'description' => __( 'Limit result set to resources with a specific email.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'email', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['role'] = array( - 'description' => __( 'Limit result set to resources with a specific role.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'customer', - 'enum' => array_merge( array( 'all' ), $this->get_role_names() ), - 'validate_callback' => 'rest_validate_request_arg', - ); - return $params; - } -} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php deleted file mode 100644 index 550aef9107f..00000000000 --- a/includes/rest-api/Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php +++ /dev/null @@ -1,530 +0,0 @@ -/refunds endpoint. - * - * @author WooThemes - * @category API - * @package WooCommerce\RestApi - * @since 2.6.0 - */ - -if ( ! defined( 'ABSPATH' ) ) { - exit; -} - -/** - * REST API Order Refunds controller class. - * - * @package WooCommerce\RestApi - * @extends WC_REST_Orders_V1_Controller - */ -class WC_REST_Order_Refunds_V1_Controller extends WC_REST_Orders_V1_Controller { - - /** - * Endpoint namespace. - * - * @var string - */ - protected $namespace = 'wc/v1'; - - /** - * Route base. - * - * @var string - */ - protected $rest_base = 'orders/(?P[\d]+)/refunds'; - - /** - * Post type. - * - * @var string - */ - protected $post_type = 'shop_order_refund'; - - /** - * Order refunds actions. - */ - public function __construct() { - add_filter( "woocommerce_rest_{$this->post_type}_trashable", '__return_false' ); - add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); - } - - /** - * Register the routes for order refunds. - */ - public function register_routes() { - register_rest_route( $this->namespace, '/' . $this->rest_base, array( - 'args' => array( - 'order_id' => array( - 'description' => __( 'The order ID.', 'woocommerce' ), - 'type' => 'integer', - ), - ), - 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( - 'order_id' => array( - 'description' => __( 'The order ID.', 'woocommerce' ), - 'type' => 'integer', - ), - 'id' => array( - 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), - 'type' => 'integer', - ), - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), - ), - ), - array( - 'methods' => WP_REST_Server::DELETABLE, - 'callback' => array( $this, 'delete_item' ), - 'permission_callback' => array( $this, 'delete_item_permissions_check' ), - 'args' => array( - 'force' => array( - 'default' => true, - 'type' => 'boolean', - 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), - ), - ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) ); - } - - /** - * Prepare a single order refund output for response. - * - * @param WP_Post $post Post object. - * @param WP_REST_Request $request Request object. - * - * @return WP_Error|WP_REST_Response - */ - public function prepare_item_for_response( $post, $request ) { - $order = wc_get_order( (int) $request['order_id'] ); - - if ( ! $order ) { - return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); - } - - $refund = wc_get_order( $post ); - - if ( ! $refund || $refund->get_parent_id() !== $order->get_id() ) { - return new WP_Error( 'woocommerce_rest_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 404 ); - } - - $dp = is_null( $request['dp'] ) ? wc_get_price_decimals() : absint( $request['dp'] ); - - $data = array( - 'id' => $refund->get_id(), - 'date_created' => wc_rest_prepare_date_response( $refund->get_date_created() ), - 'amount' => wc_format_decimal( $refund->get_amount(), $dp ), - 'reason' => $refund->get_reason(), - 'line_items' => array(), - ); - - // Add line items. - foreach ( $refund->get_items() as $item_id => $item ) { - $product = $item->get_product(); - $product_id = 0; - $variation_id = 0; - $product_sku = null; - - // Check if the product exists. - if ( is_object( $product ) ) { - $product_id = $item->get_product_id(); - $variation_id = $item->get_variation_id(); - $product_sku = $product->get_sku(); - } - - $item_meta = array(); - - $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; - - foreach ( $item->get_formatted_meta_data( $hideprefix, true ) as $meta_key => $formatted_meta ) { - $item_meta[] = array( - 'key' => $formatted_meta->key, - 'label' => $formatted_meta->display_key, - 'value' => wc_clean( $formatted_meta->display_value ), - ); - } - - $line_item = array( - 'id' => $item_id, - 'name' => $item['name'], - 'sku' => $product_sku, - 'product_id' => (int) $product_id, - 'variation_id' => (int) $variation_id, - 'quantity' => wc_stock_amount( $item['qty'] ), - 'tax_class' => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '', - 'price' => wc_format_decimal( $refund->get_item_total( $item, false, false ), $dp ), - 'subtotal' => wc_format_decimal( $refund->get_line_subtotal( $item, false, false ), $dp ), - 'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ), - 'total' => wc_format_decimal( $refund->get_line_total( $item, false, false ), $dp ), - 'total_tax' => wc_format_decimal( $item['line_tax'], $dp ), - 'taxes' => array(), - 'meta' => $item_meta, - ); - - $item_line_taxes = maybe_unserialize( $item['line_tax_data'] ); - if ( isset( $item_line_taxes['total'] ) ) { - $line_tax = array(); - - foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) { - $line_tax[ $tax_rate_id ] = array( - 'id' => $tax_rate_id, - 'total' => $tax, - 'subtotal' => '', - ); - } - - foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) { - $line_tax[ $tax_rate_id ]['subtotal'] = $tax; - } - - $line_item['taxes'] = array_values( $line_tax ); - } - - $data['line_items'][] = $line_item; - } - - $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 ); - - $response->add_links( $this->prepare_links( $refund, $request ) ); - - /** - * Filter the data for a response. - * - * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being - * prepared for the response. - * - * @param WP_REST_Response $response The response object. - * @param WP_Post $post Post object. - * @param WP_REST_Request $request Request object. - */ - return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); - } - - /** - * Prepare links for the request. - * - * @param WC_Order_Refund $refund Comment object. - * @param WP_REST_Request $request Request object. - * @return array Links for the given order refund. - */ - protected function prepare_links( $refund, $request ) { - $order_id = $refund->get_parent_id(); - $base = str_replace( '(?P[\d]+)', $order_id, $this->rest_base ); - $links = array( - 'self' => array( - 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $refund->get_id() ) ), - ), - 'collection' => array( - 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), - ), - 'up' => array( - 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order_id ) ), - ), - ); - - return $links; - } - - /** - * Query args. - * - * @param array $args Request args. - * @param WP_REST_Request $request Request object. - * @return array - */ - public function query_args( $args, $request ) { - $args['post_status'] = array_keys( wc_get_order_statuses() ); - $args['post_parent__in'] = array( absint( $request['order_id'] ) ); - - return $args; - } - - /** - * Create a single item. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function create_item( $request ) { - if ( ! empty( $request['id'] ) ) { - /* translators: %s: post type */ - return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); - } - - $order_data = get_post( (int) $request['order_id'] ); - - if ( empty( $order_data ) ) { - return new WP_Error( 'woocommerce_rest_invalid_order', __( 'Order is invalid', 'woocommerce' ), 400 ); - } - - if ( 0 > $request['amount'] ) { - return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 ); - } - - // Create the refund. - $refund = wc_create_refund( array( - 'order_id' => $order_data->ID, - 'amount' => $request['amount'], - 'reason' => empty( $request['reason'] ) ? null : $request['reason'], - 'refund_payment' => is_bool( $request['api_refund'] ) ? $request['api_refund'] : true, - 'restock_items' => true, - ) ); - - if ( is_wp_error( $refund ) ) { - return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 ); - } - - if ( ! $refund ) { - return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); - } - - $post = get_post( $refund->get_id() ); - $this->update_additional_fields_for_object( $post, $request ); - - /** - * Fires after a single item is created or updated via the REST API. - * - * @param WP_Post $post Post object. - * @param WP_REST_Request $request Request object. - * @param boolean $creating True when creating item, false when updating. - */ - do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); - - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( $post, $request ); - $response = rest_ensure_response( $response ); - $response->set_status( 201 ); - $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); - - return $response; - } - - /** - * Get the Order's schema, conforming to JSON Schema. - * - * @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 for the resource.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_created' => array( - 'description' => __( "The date the order refund was created, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'amount' => array( - 'description' => __( 'Refund amount.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'reason' => array( - 'description' => __( 'Reason for refund.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'line_items' => array( - 'description' => __( 'Line items data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Item ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'name' => array( - 'description' => __( 'Product name.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'sku' => array( - 'description' => __( 'Product SKU.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'product_id' => array( - 'description' => __( 'Product ID.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'variation_id' => array( - 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'quantity' => array( - 'description' => __( 'Quantity ordered.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'tax_class' => array( - 'description' => __( 'Tax class of product.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'price' => array( - 'description' => __( 'Product price.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'subtotal' => array( - 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'subtotal_tax' => array( - 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'total' => array( - 'description' => __( 'Line total (after discounts).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'total_tax' => array( - 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'taxes' => array( - 'description' => __( 'Line taxes.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Tax rate ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'total' => array( - 'description' => __( 'Tax total.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'subtotal' => array( - 'description' => __( 'Tax subtotal.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ), - ), - 'meta' => array( - 'description' => __( 'Line item meta data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'key' => array( - 'description' => __( 'Meta key.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'label' => array( - 'description' => __( 'Meta label.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'value' => array( - 'description' => __( 'Meta value.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ), - ), - ), - ), - ), - ), - ); - - return $this->add_additional_fields_schema( $schema ); - } - - /** - * Get the query params for collections. - * - * @return array - */ - public function get_collection_params() { - $params = parent::get_collection_params(); - - $params['dp'] = array( - 'default' => wc_get_price_decimals(), - 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), - 'type' => 'integer', - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - ); - - return $params; - } -} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php deleted file mode 100644 index 51fffe19e4a..00000000000 --- a/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php +++ /dev/null @@ -1,1631 +0,0 @@ -post_type}_query", array( $this, 'query_args' ), 10, 2 ); - } - - /** - * Register the routes for orders. - */ - 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.', 'woocommerce' ), - 'type' => 'integer', - ), - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), - ), - ), - 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 ), - ), - array( - 'methods' => WP_REST_Server::DELETABLE, - 'callback' => array( $this, 'delete_item' ), - 'permission_callback' => array( $this, 'delete_item_permissions_check' ), - 'args' => array( - 'force' => array( - 'default' => false, - 'type' => 'boolean', - 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), - ), - ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) ); - - register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'batch_items' ), - 'permission_callback' => array( $this, 'batch_items_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), - ), - 'schema' => array( $this, 'get_public_batch_schema' ), - ) ); - } - - /** - * Prepare a single order output for response. - * - * @param WP_Post $post Post object. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response $data - */ - public function prepare_item_for_response( $post, $request ) { - $order = wc_get_order( $post ); - $dp = is_null( $request['dp'] ) ? wc_get_price_decimals() : absint( $request['dp'] ); - - $data = array( - 'id' => $order->get_id(), - 'parent_id' => $order->get_parent_id(), - 'status' => $order->get_status(), - 'order_key' => $order->get_order_key(), - 'number' => $order->get_order_number(), - 'currency' => $order->get_currency(), - 'version' => $order->get_version(), - 'prices_include_tax' => $order->get_prices_include_tax(), - 'date_created' => wc_rest_prepare_date_response( $order->get_date_created() ), // v1 API used UTC. - 'date_modified' => wc_rest_prepare_date_response( $order->get_date_modified() ), // v1 API used UTC. - 'customer_id' => $order->get_customer_id(), - 'discount_total' => wc_format_decimal( $order->get_total_discount(), $dp ), - 'discount_tax' => wc_format_decimal( $order->get_discount_tax(), $dp ), - 'shipping_total' => wc_format_decimal( $order->get_shipping_total(), $dp ), - 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), - 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), - 'total' => wc_format_decimal( $order->get_total(), $dp ), - 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), - 'billing' => array(), - 'shipping' => array(), - 'payment_method' => $order->get_payment_method(), - 'payment_method_title' => $order->get_payment_method_title(), - 'transaction_id' => $order->get_transaction_id(), - 'customer_ip_address' => $order->get_customer_ip_address(), - 'customer_user_agent' => $order->get_customer_user_agent(), - 'created_via' => $order->get_created_via(), - 'customer_note' => $order->get_customer_note(), - 'date_completed' => wc_rest_prepare_date_response( $order->get_date_completed(), false ), // v1 API used local time. - 'date_paid' => wc_rest_prepare_date_response( $order->get_date_paid(), false ), // v1 API used local time. - 'cart_hash' => $order->get_cart_hash(), - 'line_items' => array(), - 'tax_lines' => array(), - 'shipping_lines' => array(), - 'fee_lines' => array(), - 'coupon_lines' => array(), - 'refunds' => array(), - ); - - // Add addresses. - $data['billing'] = $order->get_address( 'billing' ); - $data['shipping'] = $order->get_address( 'shipping' ); - - // Add line items. - foreach ( $order->get_items() as $item_id => $item ) { - $product = $item->get_product(); - $product_id = 0; - $variation_id = 0; - $product_sku = null; - - // Check if the product exists. - if ( is_object( $product ) ) { - $product_id = $item->get_product_id(); - $variation_id = $item->get_variation_id(); - $product_sku = $product->get_sku(); - } - - $item_meta = array(); - - $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; - - foreach ( $item->get_formatted_meta_data( $hideprefix, true ) as $meta_key => $formatted_meta ) { - $item_meta[] = array( - 'key' => $formatted_meta->key, - 'label' => $formatted_meta->display_key, - 'value' => wc_clean( $formatted_meta->display_value ), - ); - } - - $line_item = array( - 'id' => $item_id, - 'name' => $item['name'], - 'sku' => $product_sku, - 'product_id' => (int) $product_id, - 'variation_id' => (int) $variation_id, - 'quantity' => wc_stock_amount( $item['qty'] ), - 'tax_class' => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '', - 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), - 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), - 'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ), - 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), - 'total_tax' => wc_format_decimal( $item['line_tax'], $dp ), - 'taxes' => array(), - 'meta' => $item_meta, - ); - - $item_line_taxes = maybe_unserialize( $item['line_tax_data'] ); - if ( isset( $item_line_taxes['total'] ) ) { - $line_tax = array(); - - foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) { - $line_tax[ $tax_rate_id ] = array( - 'id' => $tax_rate_id, - 'total' => $tax, - 'subtotal' => '', - ); - } - - foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) { - $line_tax[ $tax_rate_id ]['subtotal'] = $tax; - } - - $line_item['taxes'] = array_values( $line_tax ); - } - - $data['line_items'][] = $line_item; - } - - // Add taxes. - foreach ( $order->get_items( 'tax' ) as $key => $tax ) { - $tax_line = array( - 'id' => $key, - 'rate_code' => $tax['name'], - 'rate_id' => $tax['rate_id'], - 'label' => isset( $tax['label'] ) ? $tax['label'] : $tax['name'], - 'compound' => (bool) $tax['compound'], - 'tax_total' => wc_format_decimal( $tax['tax_amount'], $dp ), - 'shipping_tax_total' => wc_format_decimal( $tax['shipping_tax_amount'], $dp ), - ); - - $data['tax_lines'][] = $tax_line; - } - - // Add shipping. - foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { - $shipping_line = array( - 'id' => $shipping_item_id, - 'method_title' => $shipping_item['name'], - 'method_id' => $shipping_item['method_id'], - 'total' => wc_format_decimal( $shipping_item['cost'], $dp ), - 'total_tax' => wc_format_decimal( '', $dp ), - 'taxes' => array(), - ); - - $shipping_taxes = $shipping_item->get_taxes(); - - if ( ! empty( $shipping_taxes['total'] ) ) { - $shipping_line['total_tax'] = wc_format_decimal( array_sum( $shipping_taxes['total'] ), $dp ); - - foreach ( $shipping_taxes['total'] as $tax_rate_id => $tax ) { - $shipping_line['taxes'][] = array( - 'id' => $tax_rate_id, - 'total' => $tax, - ); - } - } - - $data['shipping_lines'][] = $shipping_line; - } - - // Add fees. - foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { - $fee_line = array( - 'id' => $fee_item_id, - 'name' => $fee_item['name'], - 'tax_class' => ! empty( $fee_item['tax_class'] ) ? $fee_item['tax_class'] : '', - 'tax_status' => 'taxable', - 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ), - 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), - 'taxes' => array(), - ); - - $fee_line_taxes = maybe_unserialize( $fee_item['line_tax_data'] ); - if ( isset( $fee_line_taxes['total'] ) ) { - $fee_tax = array(); - - foreach ( $fee_line_taxes['total'] as $tax_rate_id => $tax ) { - $fee_tax[ $tax_rate_id ] = array( - 'id' => $tax_rate_id, - 'total' => $tax, - 'subtotal' => '', - ); - } - - if ( isset( $fee_line_taxes['subtotal'] ) ) { - foreach ( $fee_line_taxes['subtotal'] as $tax_rate_id => $tax ) { - $fee_tax[ $tax_rate_id ]['subtotal'] = $tax; - } - } - - $fee_line['taxes'] = array_values( $fee_tax ); - } - - $data['fee_lines'][] = $fee_line; - } - - // Add coupons. - foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { - $coupon_line = array( - 'id' => $coupon_item_id, - 'code' => $coupon_item['name'], - 'discount' => wc_format_decimal( $coupon_item['discount_amount'], $dp ), - 'discount_tax' => wc_format_decimal( $coupon_item['discount_amount_tax'], $dp ), - ); - - $data['coupon_lines'][] = $coupon_line; - } - - // Add refunds. - foreach ( $order->get_refunds() as $refund ) { - $data['refunds'][] = array( - 'id' => $refund->get_id(), - 'refund' => $refund->get_reason() ? $refund->get_reason() : '', - 'total' => '-' . wc_format_decimal( $refund->get_amount(), $dp ), - ); - } - - $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 ); - - $response->add_links( $this->prepare_links( $order, $request ) ); - - /** - * Filter the data for a response. - * - * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being - * prepared for the response. - * - * @param WP_REST_Response $response The response object. - * @param WP_Post $post Post object. - * @param WP_REST_Request $request Request object. - */ - return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); - } - - /** - * Prepare links for the request. - * - * @param WC_Order $order Order object. - * @param WP_REST_Request $request Request object. - * @return array Links for the given order. - */ - protected function prepare_links( $order, $request ) { - $links = array( - 'self' => array( - 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $order->get_id() ) ), - ), - 'collection' => array( - 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), - ), - ); - if ( 0 !== (int) $order->get_user_id() ) { - $links['customer'] = array( - 'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $order->get_user_id() ) ), - ); - } - if ( 0 !== (int) $order->get_parent_id() ) { - $links['up'] = array( - 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order->get_parent_id() ) ), - ); - } - return $links; - } - - /** - * Query args. - * - * @param array $args - * @param WP_REST_Request $request - * @return array - */ - public function query_args( $args, $request ) { - global $wpdb; - - // Set post_status. - if ( 'any' !== $request['status'] ) { - $args['post_status'] = 'wc-' . $request['status']; - } else { - $args['post_status'] = 'any'; - } - - if ( isset( $request['customer'] ) ) { - if ( ! empty( $args['meta_query'] ) ) { - $args['meta_query'] = array(); - } - - $args['meta_query'][] = array( - 'key' => '_customer_user', - 'value' => $request['customer'], - 'type' => 'NUMERIC', - ); - } - - // Search by product. - if ( ! empty( $request['product'] ) ) { - $order_ids = $wpdb->get_col( $wpdb->prepare( " - SELECT order_id - FROM {$wpdb->prefix}woocommerce_order_items - WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) - AND order_item_type = 'line_item' - ", $request['product'] ) ); - - // Force WP_Query return empty if don't found any order. - $order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 ); - - $args['post__in'] = $order_ids; - } - - // Search. - if ( ! empty( $args['s'] ) ) { - $order_ids = wc_order_search( $args['s'] ); - - if ( ! empty( $order_ids ) ) { - unset( $args['s'] ); - $args['post__in'] = array_merge( $order_ids, array( 0 ) ); - } - } - - return $args; - } - - /** - * Prepare a single order for create. - * - * @param WP_REST_Request $request Request object. - * @return WP_Error|WC_Order $data Object. - */ - protected function prepare_item_for_database( $request ) { - $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; - $order = new WC_Order( $id ); - $schema = $this->get_item_schema(); - $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); - - // Handle all writable props - foreach ( $data_keys as $key ) { - $value = $request[ $key ]; - - if ( ! is_null( $value ) ) { - switch ( $key ) { - case 'billing' : - case 'shipping' : - $this->update_address( $order, $value, $key ); - break; - case 'line_items' : - case 'shipping_lines' : - case 'fee_lines' : - case 'coupon_lines' : - if ( is_array( $value ) ) { - foreach ( $value as $item ) { - if ( is_array( $item ) ) { - if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) { - $order->remove_item( $item['id'] ); - } else { - $this->set_item( $order, $key, $item ); - } - } - } - } - break; - default : - if ( is_callable( array( $order, "set_{$key}" ) ) ) { - $order->{"set_{$key}"}( $value ); - } - break; - } - } - } - - /** - * Filter the data for the insert. - * - * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being - * prepared for the response. - * - * @param WC_Order $order The order object. - * @param WP_REST_Request $request Request object. - */ - return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $order, $request ); - } - - /** - * Create base WC Order object. - * @deprecated 3.0.0 - * @param array $data - * @return WC_Order - */ - protected function create_base_order( $data ) { - return wc_create_order( $data ); - } - - /** - * Only return writable props from schema. - * @param array $schema - * @return bool - */ - protected function filter_writable_props( $schema ) { - return empty( $schema['readonly'] ); - } - - /** - * Create order. - * - * @param WP_REST_Request $request Full details about the request. - * @return int|WP_Error - */ - protected function create_order( $request ) { - try { - // Make sure customer exists. - if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) { - throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id',__( 'Customer ID is invalid.', 'woocommerce' ), 400 ); - } - - // Make sure customer is part of blog. - if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) { - add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' ); - } - - $order = $this->prepare_item_for_database( $request ); - $order->set_created_via( 'rest-api' ); - $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); - $order->calculate_totals(); - $order->save(); - - // Handle set paid. - if ( true === $request['set_paid'] ) { - $order->payment_complete( $request['transaction_id'] ); - } - - return $order->get_id(); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); - } catch ( WC_REST_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Update order. - * - * @param WP_REST_Request $request Full details about the request. - * @return int|WP_Error - */ - protected function update_order( $request ) { - try { - $order = $this->prepare_item_for_database( $request ); - $order->save(); - - // Handle set paid. - if ( $order->needs_payment() && true === $request['set_paid'] ) { - $order->payment_complete( $request['transaction_id'] ); - } - - // If items have changed, recalculate order totals. - if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) { - $order->calculate_totals( true ); - } - - return $order->get_id(); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); - } catch ( WC_REST_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Update address. - * - * @param WC_Order $order - * @param array $posted - * @param string $type - */ - protected function update_address( $order, $posted, $type = 'billing' ) { - foreach ( $posted as $key => $value ) { - if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { - $order->{"set_{$type}_{$key}"}( $value ); - } - } - } - - /** - * Gets the product ID from the SKU or posted ID. - * - * @throws WC_REST_Exception When SKU or ID is not valid. - * @param array $posted Request data. - * @param string $action 'create' to add line item or 'update' to update it. - * @return int - */ - protected function get_product_id( $posted, $action = 'create' ) { - if ( ! empty( $posted['sku'] ) ) { - $product_id = (int) wc_get_product_id_by_sku( $posted['sku'] ); - } elseif ( ! empty( $posted['product_id'] ) && empty( $posted['variation_id'] ) ) { - $product_id = (int) $posted['product_id']; - } elseif ( ! empty( $posted['variation_id'] ) ) { - $product_id = (int) $posted['variation_id']; - } elseif ( 'update' === $action ) { - $product_id = 0; - } else { - throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 ); - } - return $product_id; - } - - /** - * Maybe set an item prop if the value was posted. - * @param WC_Order_Item $item - * @param string $prop - * @param array $posted Request data. - */ - protected function maybe_set_item_prop( $item, $prop, $posted ) { - if ( isset( $posted[ $prop ] ) ) { - $item->{"set_$prop"}( $posted[ $prop ] ); - } - } - - /** - * Maybe set item props if the values were posted. - * @param WC_Order_Item $item - * @param string[] $props - * @param array $posted Request data. - */ - protected function maybe_set_item_props( $item, $props, $posted ) { - foreach ( $props as $prop ) { - $this->maybe_set_item_prop( $item, $prop, $posted ); - } - } - - /** - * Create or update a line item. - * - * @param array $posted Line item data. - * @param string $action 'create' to add line item or 'update' to update it. - * - * @return WC_Order_Item_Product - * @throws WC_REST_Exception Invalid data, server error. - */ - protected function prepare_line_items( $posted, $action = 'create' ) { - $item = new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' ); - $product = wc_get_product( $this->get_product_id( $posted, $action ) ); - - if ( $product && $product !== $item->get_product() ) { - $item->set_product( $product ); - - if ( 'create' === $action ) { - $quantity = isset( $posted['quantity'] ) ? $posted['quantity'] : 1; - $total = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) ); - $item->set_total( $total ); - $item->set_subtotal( $total ); - } - } - - $this->maybe_set_item_props( $item, array( 'name', 'quantity', 'total', 'subtotal', 'tax_class' ), $posted ); - - return $item; - } - - /** - * Create or update an order shipping method. - * - * @param $posted $shipping Item data. - * @param string $action 'create' to add shipping or 'update' to update it. - * - * @return WC_Order_Item_Shipping - * @throws WC_REST_Exception Invalid data, server error. - */ - protected function prepare_shipping_lines( $posted, $action ) { - $item = new WC_Order_Item_Shipping( ! empty( $posted['id'] ) ? $posted['id'] : '' ); - - if ( 'create' === $action ) { - if ( empty( $posted['method_id'] ) ) { - throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); - } - } - - $this->maybe_set_item_props( $item, array( 'method_id', 'method_title', 'total' ), $posted ); - - return $item; - } - - /** - * Create or update an order fee. - * - * @param array $posted Item data. - * @param string $action 'create' to add fee or 'update' to update it. - * - * @return WC_Order_Item_Fee - * @throws WC_REST_Exception Invalid data, server error. - */ - protected function prepare_fee_lines( $posted, $action ) { - $item = new WC_Order_Item_Fee( ! empty( $posted['id'] ) ? $posted['id'] : '' ); - - if ( 'create' === $action ) { - if ( empty( $posted['name'] ) ) { - throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 ); - } - } - - $this->maybe_set_item_props( $item, array( 'name', 'tax_class', 'tax_status', 'total' ), $posted ); - - return $item; - } - - /** - * Create or update an order coupon. - * - * @param array $posted Item data. - * @param string $action 'create' to add coupon or 'update' to update it. - * - * @return WC_Order_Item_Coupon - * @throws WC_REST_Exception Invalid data, server error. - */ - protected function prepare_coupon_lines( $posted, $action ) { - $item = new WC_Order_Item_Coupon( ! empty( $posted['id'] ) ? $posted['id'] : '' ); - - if ( 'create' === $action ) { - if ( empty( $posted['code'] ) ) { - throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); - } - } - - $this->maybe_set_item_props( $item, array( 'code', 'discount' ), $posted ); - - return $item; - } - - /** - * Wrapper method to create/update order items. - * When updating, the item ID provided is checked to ensure it is associated - * with the order. - * - * @param WC_Order $order order - * @param string $item_type - * @param array $posted item provided in the request body - * @throws WC_REST_Exception If item ID is not associated with order - */ - protected function set_item( $order, $item_type, $posted ) { - global $wpdb; - - if ( ! empty( $posted['id'] ) ) { - $action = 'update'; - } else { - $action = 'create'; - } - - $method = 'prepare_' . $item_type; - - // Verify provided line item ID is associated with order. - if ( 'update' === $action ) { - $result = $wpdb->get_row( - $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", - absint( $posted['id'] ), - absint( $order->get_id() ) - ) ); - if ( is_null( $result ) ) { - throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); - } - } - - // Prepare item data - $item = $this->$method( $posted, $action ); - - /** - * Action hook to adjust item before save. - * @since 3.0.0 - */ - do_action( 'woocommerce_rest_set_order_item', $item, $posted ); - - // Save or add to order - if ( 'create' === $action ) { - $order->add_item( $item ); - } else { - $item->save(); - } - } - - /** - * Helper method to check if the resource ID associated with the provided item is null. - * Items can be deleted by setting the resource ID to null. - * - * @param array $item Item provided in the request body. - * @return bool True if the item resource ID is null, false otherwise. - */ - protected function item_is_null( $item ) { - $keys = array( 'product_id', 'method_id', 'method_title', 'name', 'code' ); - - foreach ( $keys as $key ) { - if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { - return true; - } - } - - return false; - } - - /** - * Create a single item. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function create_item( $request ) { - if ( ! empty( $request['id'] ) ) { - /* translators: %s: post type */ - return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); - } - - $order_id = $this->create_order( $request ); - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $post = get_post( $order_id ); - $this->update_additional_fields_for_object( $post, $request ); - - /** - * Fires after a single item is created or updated via the REST API. - * - * @param WP_Post $post Post object. - * @param WP_REST_Request $request Request object. - * @param boolean $creating True when creating item, false when updating. - */ - do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( $post, $request ); - $response = rest_ensure_response( $response ); - $response->set_status( 201 ); - $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); - - return $response; - } - - /** - * Update a single order. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function update_item( $request ) { - try { - $post_id = (int) $request['id']; - - if ( empty( $post_id ) || get_post_type( $post_id ) !== $this->post_type ) { - return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); - } - - $order_id = $this->update_order( $request ); - if ( is_wp_error( $order_id ) ) { - return $order_id; - } - - $post = get_post( $order_id ); - $this->update_additional_fields_for_object( $post, $request ); - - /** - * Fires after a single item is created or updated via the REST API. - * - * @param WP_Post $post Post object. - * @param WP_REST_Request $request Request object. - * @param boolean $creating True when creating item, false when updating. - */ - do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false ); - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( $post, $request ); - return rest_ensure_response( $response ); - - } catch ( Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Get order statuses without prefixes. - * @return array - */ - protected function get_order_statuses() { - $order_statuses = array(); - - foreach ( array_keys( wc_get_order_statuses() ) as $status ) { - $order_statuses[] = str_replace( 'wc-', '', $status ); - } - - return $order_statuses; - } - - /** - * Get the Order's schema, conforming to JSON Schema. - * - * @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 for the resource.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'parent_id' => array( - 'description' => __( 'Parent order ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'status' => array( - 'description' => __( 'Order status.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'pending', - 'enum' => $this->get_order_statuses(), - 'context' => array( 'view', 'edit' ), - ), - 'order_key' => array( - 'description' => __( 'Order key.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'number' => array( - 'description' => __( 'Order number.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'currency' => array( - 'description' => __( 'Currency the order was created with, in ISO format.', 'woocommerce' ), - 'type' => 'string', - 'default' => get_woocommerce_currency(), - 'enum' => array_keys( get_woocommerce_currencies() ), - 'context' => array( 'view', 'edit' ), - ), - 'version' => array( - 'description' => __( 'Version of WooCommerce which last updated the order.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'prices_include_tax' => array( - 'description' => __( 'True the prices included tax during checkout.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_created' => array( - 'description' => __( "The date the order was created, as GMT.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_modified' => array( - 'description' => __( "The date the order was last modified, as GMT.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'customer_id' => array( - 'description' => __( 'User ID who owns the order. 0 for guests.', 'woocommerce' ), - 'type' => 'integer', - 'default' => 0, - 'context' => array( 'view', 'edit' ), - ), - 'discount_total' => array( - 'description' => __( 'Total discount amount for the order.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'discount_tax' => array( - 'description' => __( 'Total discount tax amount for the order.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'shipping_total' => array( - 'description' => __( 'Total shipping amount for the order.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'shipping_tax' => array( - 'description' => __( 'Total shipping tax amount for the order.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'cart_tax' => array( - 'description' => __( 'Sum of line item taxes only.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'total' => array( - 'description' => __( 'Grand total.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'total_tax' => array( - 'description' => __( 'Sum of all taxes.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'billing' => array( - 'description' => __( 'Billing address.', 'woocommerce' ), - 'type' => 'object', - 'context' => array( 'view', 'edit' ), - 'properties' => array( - 'first_name' => array( - 'description' => __( 'First name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'last_name' => array( - 'description' => __( 'Last name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'company' => array( - 'description' => __( 'Company name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'address_1' => array( - 'description' => __( 'Address line 1.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'address_2' => array( - 'description' => __( 'Address line 2.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'city' => array( - 'description' => __( 'City name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'state' => array( - 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'postcode' => array( - 'description' => __( 'Postal code.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'country' => array( - 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'email' => array( - 'description' => __( 'Email address.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'email', - 'context' => array( 'view', 'edit' ), - ), - 'phone' => array( - 'description' => __( 'Phone number.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - 'shipping' => array( - 'description' => __( 'Shipping address.', 'woocommerce' ), - 'type' => 'object', - 'context' => array( 'view', 'edit' ), - 'properties' => array( - 'first_name' => array( - 'description' => __( 'First name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'last_name' => array( - 'description' => __( 'Last name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'company' => array( - 'description' => __( 'Company name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'address_1' => array( - 'description' => __( 'Address line 1.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'address_2' => array( - 'description' => __( 'Address line 2.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'city' => array( - 'description' => __( 'City name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'state' => array( - 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'postcode' => array( - 'description' => __( 'Postal code.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'country' => array( - 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - 'payment_method' => array( - 'description' => __( 'Payment method ID.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'payment_method_title' => array( - 'description' => __( 'Payment method title.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'arg_options' => array( - 'sanitize_callback' => 'sanitize_text_field', - ), - ), - 'set_paid' => array( - 'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'edit' ), - ), - 'transaction_id' => array( - 'description' => __( 'Unique transaction ID.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'customer_ip_address' => array( - 'description' => __( "Customer's IP address.", 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'customer_user_agent' => array( - 'description' => __( 'User agent of the customer.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'created_via' => array( - 'description' => __( 'Shows where the order was created.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'customer_note' => array( - 'description' => __( 'Note left by customer during checkout.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'date_completed' => array( - 'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_paid' => array( - 'description' => __( "The date the order was paid, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'cart_hash' => array( - 'description' => __( 'MD5 hash of cart items to ensure orders are not modified.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'line_items' => array( - 'description' => __( 'Line items data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Item ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'name' => array( - 'description' => __( 'Product name.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'sku' => array( - 'description' => __( 'Product SKU.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'product_id' => array( - 'description' => __( 'Product ID.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - 'variation_id' => array( - 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'quantity' => array( - 'description' => __( 'Quantity ordered.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'tax_class' => array( - 'description' => __( 'Tax class of product.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'price' => array( - 'description' => __( 'Product price.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'subtotal' => array( - 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'subtotal_tax' => array( - 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'total' => array( - 'description' => __( 'Line total (after discounts).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'total_tax' => array( - 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'taxes' => array( - 'description' => __( 'Line taxes.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Tax rate ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'total' => array( - 'description' => __( 'Tax total.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'subtotal' => array( - 'description' => __( 'Tax subtotal.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ), - ), - 'meta' => array( - 'description' => __( 'Line item meta data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'key' => array( - 'description' => __( 'Meta key.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'label' => array( - 'description' => __( 'Meta label.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'value' => array( - 'description' => __( 'Meta value.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ), - ), - ), - ), - ), - 'tax_lines' => array( - 'description' => __( 'Tax lines data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Item ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'rate_code' => array( - 'description' => __( 'Tax rate code.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'rate_id' => array( - 'description' => __( 'Tax rate ID.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'label' => array( - 'description' => __( 'Tax rate label.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'compound' => array( - 'description' => __( 'Show if is a compound tax rate.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'tax_total' => array( - 'description' => __( 'Tax total (not including shipping taxes).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'shipping_tax_total' => array( - 'description' => __( 'Shipping tax total.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ), - ), - 'shipping_lines' => array( - 'description' => __( 'Shipping lines data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Item ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'method_title' => array( - 'description' => __( 'Shipping method name.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - 'method_id' => array( - 'description' => __( 'Shipping method ID.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - 'total' => array( - 'description' => __( 'Line total (after discounts).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'total_tax' => array( - 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'taxes' => array( - 'description' => __( 'Line taxes.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Tax rate ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'total' => array( - 'description' => __( 'Tax total.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ), - ), - ), - ), - ), - 'fee_lines' => array( - 'description' => __( 'Fee lines data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Item ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'name' => array( - 'description' => __( 'Fee name.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - 'tax_class' => array( - 'description' => __( 'Tax class of fee.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'tax_status' => array( - 'description' => __( 'Tax status of fee.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'enum' => array( 'taxable', 'none' ), - ), - 'total' => array( - 'description' => __( 'Line total (after discounts).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'total_tax' => array( - 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'taxes' => array( - 'description' => __( 'Line taxes.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Tax rate ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'total' => array( - 'description' => __( 'Tax total.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'subtotal' => array( - 'description' => __( 'Tax subtotal.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ), - ), - ), - ), - ), - 'coupon_lines' => array( - 'description' => __( 'Coupons line data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Item ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'code' => array( - 'description' => __( 'Coupon code.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - 'discount' => array( - 'description' => __( 'Discount total.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'discount_tax' => array( - 'description' => __( 'Discount total tax.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ), - ), - 'refunds' => array( - 'description' => __( 'List of refunds.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Refund ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'reason' => array( - 'description' => __( 'Refund reason.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'total' => array( - 'description' => __( 'Refund total.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ), - ), - ), - ); - - return $this->add_additional_fields_schema( $schema ); - } - - /** - * Get the query params for collections. - * - * @return array - */ - public function get_collection_params() { - $params = parent::get_collection_params(); - - $params['status'] = array( - 'default' => 'any', - 'description' => __( 'Limit result set to orders assigned a specific status.', 'woocommerce' ), - 'type' => 'string', - 'enum' => array_merge( array( 'any' ), $this->get_order_statuses() ), - 'sanitize_callback' => 'sanitize_key', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['customer'] = array( - 'description' => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ), - 'type' => 'integer', - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['product'] = array( - 'description' => __( 'Limit result set to orders assigned a specific product.', 'woocommerce' ), - 'type' => 'integer', - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['dp'] = array( - 'default' => wc_get_price_decimals(), - 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), - 'type' => 'integer', - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - ); - - return $params; - } -} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php deleted file mode 100644 index d8b0f77b278..00000000000 --- a/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php +++ /dev/null @@ -1,578 +0,0 @@ -/reviews. - * - * @author WooThemes - * @category API - * @package WooCommerce\RestApi - * @since 3.0.0 - */ - -if ( ! defined( 'ABSPATH' ) ) { - exit; -} - -/** - * REST API Product Reviews Controller Class. - * - * @package WooCommerce\RestApi - * @extends WC_REST_Controller - */ -class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller { - - /** - * Endpoint namespace. - * - * @var string - */ - protected $namespace = 'wc/v1'; - - /** - * Route base. - * - * @var string - */ - protected $rest_base = 'products/(?P[\d]+)/reviews'; - - /** - * Register the routes for product reviews. - */ - public function register_routes() { - register_rest_route( $this->namespace, '/' . $this->rest_base, array( - 'args' => array( - 'product_id' => array( - 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), - 'type' => 'integer', - ), - 'id' => array( - 'description' => __( 'Unique identifier for the variation.', 'woocommerce' ), - 'type' => 'integer', - ), - ), - 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' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( - 'review' => array( - 'required' => true, - 'type' => 'string', - 'description' => __( 'Review content.', 'woocommerce' ), - ), - 'name' => array( - 'required' => true, - 'type' => 'string', - 'description' => __( 'Name of the reviewer.', 'woocommerce' ), - ), - 'email' => array( - 'required' => true, - 'type' => 'string', - 'description' => __( 'Email of the reviewer.', 'woocommerce' ), - ), - ) ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) ); - - register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( - 'args' => array( - 'product_id' => array( - 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), - 'type' => 'integer', - ), - 'id' => array( - 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), - 'type' => 'integer', - ), - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), - ), - ), - 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 ), - ), - array( - 'methods' => WP_REST_Server::DELETABLE, - 'callback' => array( $this, 'delete_item' ), - 'permission_callback' => array( $this, 'delete_item_permissions_check' ), - 'args' => array( - 'force' => array( - 'default' => false, - 'type' => 'boolean', - 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), - ), - ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) ); - } - - /** - * Check whether a given request has permission to read webhook deliveries. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_items_permissions_check( $request ) { - if ( ! wc_rest_check_post_permissions( 'product', 'read' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access to read a product review. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_item_permissions_check( $request ) { - $post = get_post( (int) $request['product_id'] ); - - if ( $post && ! wc_rest_check_post_permissions( 'product', 'read', $post->ID ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access to create a new product review. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function create_item_permissions_check( $request ) { - $post = get_post( (int) $request['product_id'] ); - if ( $post && ! wc_rest_check_post_permissions( 'product', 'create', $post->ID ) ) { - return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - return true; - } - - /** - * Check if a given request has access to update a product review. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function update_item_permissions_check( $request ) { - $post = get_post( (int) $request['product_id'] ); - if ( $post && ! wc_rest_check_post_permissions( 'product', 'edit', $post->ID ) ) { - return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - return true; - } - - /** - * Check if a given request has access to delete a product review. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function delete_item_permissions_check( $request ) { - $post = get_post( (int) $request['product_id'] ); - if ( $post && ! wc_rest_check_post_permissions( 'product', 'delete', $post->ID ) ) { - return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - return true; - } - - /** - * Get all reviews from a product. - * - * @param WP_REST_Request $request - * - * @return array|WP_Error - */ - public function get_items( $request ) { - $product_id = (int) $request['product_id']; - - if ( 'product' !== get_post_type( $product_id ) ) { - return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - $reviews = get_approved_comments( $product_id ); - $data = array(); - foreach ( $reviews as $review_data ) { - $review = $this->prepare_item_for_response( $review_data, $request ); - $review = $this->prepare_response_for_collection( $review ); - $data[] = $review; - } - - return rest_ensure_response( $data ); - } - - /** - * Get a single product review. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function get_item( $request ) { - $id = (int) $request['id']; - $product_id = (int) $request['product_id']; - - if ( 'product' !== get_post_type( $product_id ) ) { - return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - $review = get_comment( $id ); - - if ( empty( $id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) { - return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - $delivery = $this->prepare_item_for_response( $review, $request ); - $response = rest_ensure_response( $delivery ); - - return $response; - } - - - /** - * Create a product review. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function create_item( $request ) { - $product_id = (int) $request['product_id']; - - if ( 'product' !== get_post_type( $product_id ) ) { - return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - $prepared_review = $this->prepare_item_for_database( $request ); - - /** - * Filter a product review (comment) before it is inserted via the REST API. - * - * Allows modification of the comment right before it is inserted via `wp_insert_comment`. - * - * @param array $prepared_review The prepared comment data for `wp_insert_comment`. - * @param WP_REST_Request $request Request used to insert the comment. - */ - $prepared_review = apply_filters( 'rest_pre_insert_product_review', $prepared_review, $request ); - - $product_review_id = wp_insert_comment( $prepared_review ); - if ( ! $product_review_id ) { - return new WP_Error( 'rest_product_review_failed_create', __( 'Creating product review failed.', 'woocommerce' ), array( 'status' => 500 ) ); - } - - update_comment_meta( $product_review_id, 'rating', ( ! empty( $request['rating'] ) ? $request['rating'] : '0' ) ); - - $product_review = get_comment( $product_review_id ); - $this->update_additional_fields_for_object( $product_review, $request ); - - /** - * Fires after a single item is created or updated via the REST API. - * - * @param WP_Comment $product_review Inserted object. - * @param WP_REST_Request $request Request object. - * @param boolean $creating True when creating item, false when updating. - */ - do_action( "woocommerce_rest_insert_product_review", $product_review, $request, true ); - - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( $product_review, $request ); - $response = rest_ensure_response( $response ); - $response->set_status( 201 ); - $base = str_replace( '(?P[\d]+)', $product_id, $this->rest_base ); - $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $product_review_id ) ) ); - - return $response; - } - - /** - * Update a single product review. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function update_item( $request ) { - $product_review_id = (int) $request['id']; - $product_id = (int) $request['product_id']; - - if ( 'product' !== get_post_type( $product_id ) ) { - return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - $review = get_comment( $product_review_id ); - - if ( empty( $product_review_id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) { - return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - $prepared_review = $this->prepare_item_for_database( $request ); - - $updated = wp_update_comment( $prepared_review ); - if ( 0 === $updated ) { - return new WP_Error( 'rest_product_review_failed_edit', __( 'Updating product review failed.', 'woocommerce' ), array( 'status' => 500 ) ); - } - - if ( ! empty( $request['rating'] ) ) { - update_comment_meta( $product_review_id, 'rating', $request['rating'] ); - } - - $product_review = get_comment( $product_review_id ); - $this->update_additional_fields_for_object( $product_review, $request ); - - /** - * Fires after a single item is created or updated via the REST API. - * - * @param WP_Comment $comment Inserted object. - * @param WP_REST_Request $request Request object. - * @param boolean $creating True when creating item, false when updating. - */ - do_action( "woocommerce_rest_insert_product_review", $product_review, $request, true ); - - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( $product_review, $request ); - - return rest_ensure_response( $response ); - } - - /** - * Delete a product review. - * - * @param WP_REST_Request $request Full details about the request - * - * @return bool|WP_Error|WP_REST_Response - */ - public function delete_item( $request ) { - $product_review_id = absint( is_array( $request['id'] ) ? $request['id']['id'] : $request['id'] ); - $force = isset( $request['force'] ) ? (bool) $request['force'] : false; - - $product_review = get_comment( $product_review_id ); - if ( empty( $product_review_id ) || empty( $product_review->comment_ID ) || empty( $product_review->comment_post_ID ) ) { - return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid product review ID.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - /** - * Filter whether a product review is trashable. - * - * Return false to disable trash support for the product review. - * - * @param boolean $supports_trash Whether the object supports trashing. - * @param WP_Post $product_review The object being considered for trashing support. - */ - $supports_trash = apply_filters( 'rest_product_review_trashable', ( EMPTY_TRASH_DAYS > 0 ), $product_review ); - - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( $product_review, $request ); - - if ( $force ) { - $result = wp_delete_comment( $product_review_id, true ); - } else { - if ( ! $supports_trash ) { - return new WP_Error( 'rest_trash_not_supported', __( 'The product review does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); - } - - if ( 'trash' === $product_review->comment_approved ) { - return new WP_Error( 'rest_already_trashed', __( 'The comment has already been trashed.', 'woocommerce' ), array( 'status' => 410 ) ); - } - - $result = wp_trash_comment( $product_review->comment_ID ); - } - - if ( ! $result ) { - return new WP_Error( 'rest_cannot_delete', __( 'The product review cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); - } - - /** - * Fires after a product review is deleted via the REST API. - * - * @param object $product_review The deleted item. - * @param WP_REST_Response $response The response data. - * @param WP_REST_Request $request The request sent to the API. - */ - do_action( 'rest_delete_product_review', $product_review, $response, $request ); - - return $response; - } - - /** - * Prepare a single product review output for response. - * - * @param WP_Comment $review Product review object. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response $response Response data. - */ - public function prepare_item_for_response( $review, $request ) { - $data = array( - 'id' => (int) $review->comment_ID, - 'date_created' => wc_rest_prepare_date_response( $review->comment_date_gmt ), - 'review' => $review->comment_content, - 'rating' => (int) get_comment_meta( $review->comment_ID, 'rating', true ), - 'name' => $review->comment_author, - 'email' => $review->comment_author_email, - 'verified' => wc_review_is_from_verified_owner( $review->comment_ID ), - ); - - $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 ); - - $response->add_links( $this->prepare_links( $review, $request ) ); - - /** - * Filter product reviews object returned from the REST API. - * - * @param WP_REST_Response $response The response object. - * @param WP_Comment $review Product review object used to create response. - * @param WP_REST_Request $request Request object. - */ - return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request ); - } - - /** - * Prepare a single product review to be inserted into the database. - * - * @param WP_REST_Request $request Request object. - * @return array|WP_Error $prepared_review - */ - protected function prepare_item_for_database( $request ) { - $prepared_review = array( 'comment_approved' => 1, 'comment_type' => 'review' ); - - if ( isset( $request['id'] ) ) { - $prepared_review['comment_ID'] = (int) $request['id']; - } - - if ( isset( $request['review'] ) ) { - $prepared_review['comment_content'] = $request['review']; - } - - if ( isset( $request['product_id'] ) ) { - $prepared_review['comment_post_ID'] = (int) $request['product_id']; - } - - if ( isset( $request['name'] ) ) { - $prepared_review['comment_author'] = $request['name']; - } - - if ( isset( $request['email'] ) ) { - $prepared_review['comment_author_email'] = $request['email']; - } - - if ( isset( $request['date_created'] ) ) { - $prepared_review['comment_date'] = $request['date_created']; - } - - if ( isset( $request['date_created_gmt'] ) ) { - $prepared_review['comment_date_gmt'] = $request['date_created_gmt']; - } - - return apply_filters( 'rest_preprocess_product_review', $prepared_review, $request ); - } - - /** - * Prepare links for the request. - * - * @param WP_Comment $review Product review object. - * @param WP_REST_Request $request Request object. - * @return array Links for the given product review. - */ - protected function prepare_links( $review, $request ) { - $product_id = (int) $request['product_id']; - $base = str_replace( '(?P[\d]+)', $product_id, $this->rest_base ); - $links = array( - 'self' => array( - 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $review->comment_ID ) ), - ), - 'collection' => array( - 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), - ), - 'up' => array( - 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product_id ) ), - ), - ); - - return $links; - } - - /** - * Get the Product Review's schema, conforming to JSON Schema. - * - * @return array - */ - public function get_item_schema() { - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'product_review', - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'review' => array( - 'description' => __( 'The content of the review.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'date_created' => array( - 'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'rating' => array( - 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'Reviewer name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'email' => array( - 'description' => __( 'Reviewer email.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'verified' => array( - 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ); - - return $this->add_additional_fields_schema( $schema ); - } - - /** - * Get the query params for collections. - * - * @return array - */ - public function get_collection_params() { - return array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), - ); - } -} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php deleted file mode 100644 index add78763cf2..00000000000 --- a/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php +++ /dev/null @@ -1,709 +0,0 @@ -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.', 'woocommerce' ), - 'type' => 'integer', - ), - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), - ), - ), - 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 ), - ), - array( - 'methods' => WP_REST_Server::DELETABLE, - 'callback' => array( $this, 'delete_item' ), - 'permission_callback' => array( $this, 'delete_item_permissions_check' ), - 'args' => array( - 'force' => array( - 'default' => false, - 'type' => 'boolean', - 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), - ), - ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) ); - - register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'batch_items' ), - 'permission_callback' => array( $this, 'batch_items_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), - ), - 'schema' => array( $this, 'get_public_batch_schema' ), - ) ); - } - - /** - * Check whether a given request has permission to read taxes. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_items_permissions_check( $request ) { - if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access create taxes. - * - * @param WP_REST_Request $request Full details about the request. - * - * @return bool|WP_Error - */ - public function create_item_permissions_check( $request ) { - if ( ! wc_rest_check_manager_permissions( 'settings', 'create' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access to read a tax. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_item_permissions_check( $request ) { - if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access update a tax. - * - * @param WP_REST_Request $request Full details about the request. - * - * @return bool|WP_Error - */ - public function update_item_permissions_check( $request ) { - if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access delete a tax. - * - * @param WP_REST_Request $request Full details about the request. - * - * @return bool|WP_Error - */ - public function delete_item_permissions_check( $request ) { - if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access batch create, update and delete items. - * - * @param WP_REST_Request $request Full details about the request. - * - * @return bool|WP_Error - */ - public function batch_items_permissions_check( $request ) { - if ( ! wc_rest_check_manager_permissions( 'settings', 'batch' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Get all taxes. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function get_items( $request ) { - global $wpdb; - - $prepared_args = array(); - $prepared_args['order'] = $request['order']; - $prepared_args['number'] = $request['per_page']; - if ( ! empty( $request['offset'] ) ) { - $prepared_args['offset'] = $request['offset']; - } else { - $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; - } - $orderby_possibles = array( - 'id' => 'tax_rate_id', - 'order' => 'tax_rate_order', - ); - $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; - $prepared_args['class'] = $request['class']; - - /** - * Filter arguments, before passing to $wpdb->get_results(), when querying taxes via the REST API. - * - * @param array $prepared_args Array of arguments for $wpdb->get_results(). - * @param WP_REST_Request $request The current request. - */ - $prepared_args = apply_filters( 'woocommerce_rest_tax_query', $prepared_args, $request ); - - $query = " - SELECT * - FROM {$wpdb->prefix}woocommerce_tax_rates - WHERE 1 = 1 - "; - - // Filter by tax class. - if ( ! empty( $prepared_args['class'] ) ) { - $class = 'standard' !== $prepared_args['class'] ? sanitize_title( $prepared_args['class'] ) : ''; - $query .= " AND tax_rate_class = '$class'"; - } - - // Order tax rates. - $order_by = sprintf( ' ORDER BY %s', sanitize_key( $prepared_args['orderby'] ) ); - - // Pagination. - $pagination = sprintf( ' LIMIT %d, %d', $prepared_args['offset'], $prepared_args['number'] ); - - // Query taxes. - $results = $wpdb->get_results( $query . $order_by . $pagination ); - - $taxes = array(); - foreach ( $results as $tax ) { - $data = $this->prepare_item_for_response( $tax, $request ); - $taxes[] = $this->prepare_response_for_collection( $data ); - } - - $response = rest_ensure_response( $taxes ); - - // Store pagination values for headers then unset for count query. - $per_page = (int) $prepared_args['number']; - $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); - - // Query only for ids. - $wpdb->get_results( str_replace( 'SELECT *', 'SELECT tax_rate_id', $query ) ); - - // Calculate totals. - $total_taxes = (int) $wpdb->num_rows; - $response->header( 'X-WP-Total', (int) $total_taxes ); - $max_pages = ceil( $total_taxes / $per_page ); - $response->header( 'X-WP-TotalPages', (int) $max_pages ); - - $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); - if ( $page > 1 ) { - $prev_page = $page - 1; - if ( $prev_page > $max_pages ) { - $prev_page = $max_pages; - } - $prev_link = add_query_arg( 'page', $prev_page, $base ); - $response->link_header( 'prev', $prev_link ); - } - if ( $max_pages > $page ) { - $next_page = $page + 1; - $next_link = add_query_arg( 'page', $next_page, $base ); - $response->link_header( 'next', $next_link ); - } - - return $response; - } - - /** - * Take tax data from the request and return the updated or newly created rate. - * - * @param WP_REST_Request $request Full details about the request. - * @param stdClass|null $current Existing tax object. - * @return object - */ - protected function create_or_update_tax( $request, $current = null ) { - $id = absint( isset( $request['id'] ) ? $request['id'] : 0 ); - $data = array(); - $fields = array( - 'tax_rate_country', - 'tax_rate_state', - 'tax_rate', - 'tax_rate_name', - 'tax_rate_priority', - 'tax_rate_compound', - 'tax_rate_shipping', - 'tax_rate_order', - 'tax_rate_class', - ); - - foreach ( $fields as $field ) { - // Keys via API differ from the stored names returned by _get_tax_rate. - $key = 'tax_rate' === $field ? 'rate' : str_replace( 'tax_rate_', '', $field ); - - // Remove data that was not posted. - if ( ! isset( $request[ $key ] ) ) { - continue; - } - - // Test new data against current data. - if ( $current && $current->$field === $request[ $key ] ) { - continue; - } - - // Add to data array. - switch ( $key ) { - case 'tax_rate_priority' : - case 'tax_rate_compound' : - case 'tax_rate_shipping' : - case 'tax_rate_order' : - $data[ $field ] = absint( $request[ $key ] ); - break; - case 'tax_rate_class' : - $data[ $field ] = 'standard' !== $request['tax_rate_class'] ? $request['tax_rate_class'] : ''; - break; - default : - $data[ $field ] = wc_clean( $request[ $key ] ); - break; - } - } - - if ( $id ) { - WC_Tax::_update_tax_rate( $id, $data ); - } else { - $id = WC_Tax::_insert_tax_rate( $data ); - } - - // Add locales. - if ( ! empty( $request['postcode'] ) ) { - WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $request['postcode'] ) ); - } - if ( ! empty( $request['city'] ) ) { - WC_Tax::_update_tax_rate_cities( $id, wc_clean( $request['city'] ) ); - } - - return WC_Tax::_get_tax_rate( $id, OBJECT ); - } - - /** - * Create a single tax. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function create_item( $request ) { - if ( ! empty( $request['id'] ) ) { - return new WP_Error( 'woocommerce_rest_tax_exists', __( 'Cannot create existing resource.', 'woocommerce' ), array( 'status' => 400 ) ); - } - - $tax = $this->create_or_update_tax( $request ); - - $this->update_additional_fields_for_object( $tax, $request ); - - /** - * Fires after a tax is created or updated via the REST API. - * - * @param stdClass $tax Data used to create the tax. - * @param WP_REST_Request $request Request object. - * @param boolean $creating True when creating tax, false when updating tax. - */ - do_action( 'woocommerce_rest_insert_tax', $tax, $request, true ); - - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( $tax, $request ); - $response = rest_ensure_response( $response ); - $response->set_status( 201 ); - $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ) ); - - return $response; - } - - /** - * Get a single tax. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function get_item( $request ) { - $id = (int) $request['id']; - $tax_obj = WC_Tax::_get_tax_rate( $id, OBJECT ); - - if ( empty( $id ) || empty( $tax_obj ) ) { - return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - $tax = $this->prepare_item_for_response( $tax_obj, $request ); - $response = rest_ensure_response( $tax ); - - return $response; - } - - /** - * Update a single tax. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function update_item( $request ) { - $id = (int) $request['id']; - $tax_obj = WC_Tax::_get_tax_rate( $id, OBJECT ); - - if ( empty( $id ) || empty( $tax_obj ) ) { - return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - $tax = $this->create_or_update_tax( $request, $tax_obj ); - - $this->update_additional_fields_for_object( $tax, $request ); - - /** - * Fires after a tax is created or updated via the REST API. - * - * @param stdClass $tax Data used to create the tax. - * @param WP_REST_Request $request Request object. - * @param boolean $creating True when creating tax, false when updating tax. - */ - do_action( 'woocommerce_rest_insert_tax', $tax, $request, false ); - - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( $tax, $request ); - $response = rest_ensure_response( $response ); - - return $response; - } - - /** - * Delete a single tax. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function delete_item( $request ) { - global $wpdb; - - $id = (int) $request['id']; - $force = isset( $request['force'] ) ? (bool) $request['force'] : false; - - // We don't support trashing for this type, error out. - if ( ! $force ) { - return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Taxes do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); - } - - $tax = WC_Tax::_get_tax_rate( $id, OBJECT ); - - if ( empty( $id ) || empty( $tax ) ) { - return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 400 ) ); - } - - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( $tax, $request ); - - WC_Tax::_delete_tax_rate( $id ); - - if ( 0 === $wpdb->rows_affected ) { - return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); - } - - /** - * Fires after a tax is deleted via the REST API. - * - * @param stdClass $tax The tax data. - * @param WP_REST_Response $response The response returned from the API. - * @param WP_REST_Request $request The request sent to the API. - */ - do_action( 'woocommerce_rest_delete_tax', $tax, $response, $request ); - - return $response; - } - - /** - * Prepare a single tax output for response. - * - * @param stdClass $tax Tax object. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response $response Response data. - */ - public function prepare_item_for_response( $tax, $request ) { - global $wpdb; - - $id = (int) $tax->tax_rate_id; - $data = array( - 'id' => $id, - 'country' => $tax->tax_rate_country, - 'state' => $tax->tax_rate_state, - 'postcode' => '', - 'city' => '', - 'rate' => $tax->tax_rate, - 'name' => $tax->tax_rate_name, - 'priority' => (int) $tax->tax_rate_priority, - 'compound' => (bool) $tax->tax_rate_compound, - 'shipping' => (bool) $tax->tax_rate_shipping, - 'order' => (int) $tax->tax_rate_order, - 'class' => $tax->tax_rate_class ? $tax->tax_rate_class : 'standard', - ); - - // Get locales from a tax rate. - $locales = $wpdb->get_results( $wpdb->prepare( " - SELECT location_code, location_type - FROM {$wpdb->prefix}woocommerce_tax_rate_locations - WHERE tax_rate_id = %d - ", $id ) ); - - if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) { - foreach ( $locales as $locale ) { - $data[ $locale->location_type ] = $locale->location_code; - } - } - - $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 ); - - $response->add_links( $this->prepare_links( $tax ) ); - - /** - * Filter tax object returned from the REST API. - * - * @param WP_REST_Response $response The response object. - * @param stdClass $tax Tax object used to create response. - * @param WP_REST_Request $request Request object. - */ - return apply_filters( 'woocommerce_rest_prepare_tax', $response, $tax, $request ); - } - - /** - * Prepare links for the request. - * - * @param stdClass $tax Tax object. - * @return array Links for the given tax. - */ - protected function prepare_links( $tax ) { - $links = array( - 'self' => array( - 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ), - ), - 'collection' => array( - 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), - ), - ); - - return $links; - } - - /** - * Get the Taxes schema, conforming to JSON Schema. - * - * @return array - */ - public function get_item_schema() { - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'tax', - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'country' => array( - 'description' => __( 'Country ISO 3166 code.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'state' => array( - 'description' => __( 'State code.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'postcode' => array( - 'description' => __( 'Postcode / ZIP.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'city' => array( - 'description' => __( 'City name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'rate' => array( - 'description' => __( 'Tax rate.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'Tax rate name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'priority' => array( - 'description' => __( 'Tax priority.', 'woocommerce' ), - 'type' => 'integer', - 'default' => 1, - 'context' => array( 'view', 'edit' ), - ), - 'compound' => array( - 'description' => __( 'Whether or not this is a compound rate.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'shipping' => array( - 'description' => __( 'Whether or not this tax rate also gets applied to shipping.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => true, - 'context' => array( 'view', 'edit' ), - ), - 'order' => array( - 'description' => __( 'Indicates the order that will appear in queries.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'class' => array( - 'description' => __( 'Tax class.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'standard', - 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), - 'context' => array( 'view', 'edit' ), - ), - ), - ); - - return $this->add_additional_fields_schema( $schema ); - } - - /** - * Get the query params for collections. - * - * @return array - */ - public function get_collection_params() { - $params = array(); - $params['context'] = $this->get_context_param(); - $params['context']['default'] = 'view'; - - $params['page'] = array( - 'description' => __( 'Current page of the collection.', 'woocommerce' ), - 'type' => 'integer', - 'default' => 1, - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - 'minimum' => 1, - ); - $params['per_page'] = array( - 'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ), - 'type' => 'integer', - 'default' => 10, - 'minimum' => 1, - 'maximum' => 100, - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['offset'] = array( - 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), - 'type' => 'integer', - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['order'] = array( - 'default' => 'asc', - 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), - 'enum' => array( 'asc', 'desc' ), - 'sanitize_callback' => 'sanitize_key', - 'type' => 'string', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['orderby'] = array( - 'default' => 'order', - 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), - 'enum' => array( - 'id', - 'order', - ), - 'sanitize_callback' => 'sanitize_key', - 'type' => 'string', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['class'] = array( - 'description' => __( 'Sort by tax class.', 'woocommerce' ), - 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), - 'sanitize_callback' => 'sanitize_title', - 'type' => 'string', - 'validate_callback' => 'rest_validate_request_arg', - ); - - return $params; - } -} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php deleted file mode 100644 index f8598fcd019..00000000000 --- a/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php +++ /dev/null @@ -1,542 +0,0 @@ -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' => array_merge( - $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( - 'code' => array( - 'description' => __( 'Coupon code.', 'woocommerce' ), - 'required' => true, - 'type' => 'string', - ), - ) - ), - ), - '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.', 'woocommerce' ), - 'type' => 'integer', - ), - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), - ), - ), - 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 ), - ), - array( - 'methods' => WP_REST_Server::DELETABLE, - 'callback' => array( $this, 'delete_item' ), - 'permission_callback' => array( $this, 'delete_item_permissions_check' ), - 'args' => array( - 'force' => array( - 'default' => false, - 'type' => 'boolean', - 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), - ), - ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - - register_rest_route( - $this->namespace, '/' . $this->rest_base . '/batch', array( - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'batch_items' ), - 'permission_callback' => array( $this, 'batch_items_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), - ), - 'schema' => array( $this, 'get_public_batch_schema' ), - ) - ); - } - - /** - * Get object. - * - * @since 3.0.0 - * @param int $id Object ID. - * @return WC_Data - */ - protected function get_object( $id ) { - return new WC_Coupon( $id ); - } - - /** - * Get formatted item data. - * - * @since 3.0.0 - * @param WC_Data $object WC_Data instance. - * @return array - */ - protected function get_formatted_item_data( $object ) { - $data = $object->get_data(); - - $format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' ); - $format_date = array( 'date_created', 'date_modified', 'date_expires' ); - $format_null = array( 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items' ); - - // Format decimal values. - foreach ( $format_decimal as $key ) { - $data[ $key ] = wc_format_decimal( $data[ $key ], 2 ); - } - - // Format date values. - foreach ( $format_date as $key ) { - $datetime = $data[ $key ]; - $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); - $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); - } - - // Format null values. - foreach ( $format_null as $key ) { - $data[ $key ] = $data[ $key ] ? $data[ $key ] : null; - } - - return array( - 'id' => $object->get_id(), - 'code' => $data['code'], - 'amount' => $data['amount'], - 'date_created' => $data['date_created'], - 'date_created_gmt' => $data['date_created_gmt'], - 'date_modified' => $data['date_modified'], - 'date_modified_gmt' => $data['date_modified_gmt'], - 'discount_type' => $data['discount_type'], - 'description' => $data['description'], - 'date_expires' => $data['date_expires'], - 'date_expires_gmt' => $data['date_expires_gmt'], - 'usage_count' => $data['usage_count'], - 'individual_use' => $data['individual_use'], - 'product_ids' => $data['product_ids'], - 'excluded_product_ids' => $data['excluded_product_ids'], - 'usage_limit' => $data['usage_limit'], - 'usage_limit_per_user' => $data['usage_limit_per_user'], - 'limit_usage_to_x_items' => $data['limit_usage_to_x_items'], - 'free_shipping' => $data['free_shipping'], - 'product_categories' => $data['product_categories'], - 'excluded_product_categories' => $data['excluded_product_categories'], - 'exclude_sale_items' => $data['exclude_sale_items'], - 'minimum_amount' => $data['minimum_amount'], - 'maximum_amount' => $data['maximum_amount'], - 'email_restrictions' => $data['email_restrictions'], - 'used_by' => $data['used_by'], - 'meta_data' => $data['meta_data'], - ); - } - - /** - * Prepare a single coupon output for response. - * - * @since 3.0.0 - * @param WC_Data $object Object data. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response - */ - public function prepare_object_for_response( $object, $request ) { - $data = $this->get_formatted_item_data( $object ); - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); - $response = rest_ensure_response( $data ); - $response->add_links( $this->prepare_links( $object, $request ) ); - - /** - * Filter the data for a response. - * - * The dynamic portion of the hook name, $this->post_type, - * refers to object type being prepared for the response. - * - * @param WP_REST_Response $response The response object. - * @param WC_Data $object Object data. - * @param WP_REST_Request $request Request object. - */ - return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); - } - - /** - * Prepare objects query. - * - * @since 3.0.0 - * @param WP_REST_Request $request Full details about the request. - * @return array - */ - protected function prepare_objects_query( $request ) { - $args = parent::prepare_objects_query( $request ); - - if ( ! empty( $request['code'] ) ) { - $id = wc_get_coupon_id_by_code( $request['code'] ); - $args['post__in'] = array( $id ); - } - - // Get only ids. - $args['fields'] = 'ids'; - - return $args; - } - - /** - * Only return writable props from schema. - * - * @param array $schema Schema. - * @return bool - */ - protected function filter_writable_props( $schema ) { - return empty( $schema['readonly'] ); - } - - /** - * Prepare a single coupon for create or update. - * - * @param WP_REST_Request $request Request object. - * @param bool $creating If is creating a new object. - * @return WP_Error|WC_Data - */ - protected function prepare_object_for_database( $request, $creating = false ) { - $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; - $coupon = new WC_Coupon( $id ); - $schema = $this->get_item_schema(); - $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); - - // Validate required POST fields. - if ( $creating && empty( $request['code'] ) ) { - return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) ); - } - - // Handle all writable props. - foreach ( $data_keys as $key ) { - $value = $request[ $key ]; - - if ( ! is_null( $value ) ) { - switch ( $key ) { - case 'code': - $coupon_code = wc_format_coupon_code( $value ); - $id = $coupon->get_id() ? $coupon->get_id() : 0; - $id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id ); - - if ( $id_from_code ) { - return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) ); - } - - $coupon->set_code( $coupon_code ); - break; - case 'meta_data': - if ( is_array( $value ) ) { - foreach ( $value as $meta ) { - $coupon->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); - } - } - break; - case 'description': - $coupon->set_description( wp_filter_post_kses( $value ) ); - break; - default: - if ( is_callable( array( $coupon, "set_{$key}" ) ) ) { - $coupon->{"set_{$key}"}( $value ); - } - break; - } - } - } - - /** - * Filters an object before it is inserted via the REST API. - * - * The dynamic portion of the hook name, `$this->post_type`, - * refers to the object type slug. - * - * @param WC_Data $coupon Object object. - * @param WP_REST_Request $request Request object. - * @param bool $creating If is creating a new object. - */ - return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $coupon, $request, $creating ); - } - - /** - * Get the Coupon's schema, conforming to JSON Schema. - * - * @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 for the object.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'code' => array( - 'description' => __( 'Coupon code.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'amount' => array( - 'description' => __( 'The amount of discount. Should always be numeric, even if setting a percentage.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'date_created' => array( - 'description' => __( "The date the coupon was created, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_created_gmt' => array( - 'description' => __( 'The date the coupon was created, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_modified' => array( - 'description' => __( "The date the coupon was last modified, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_modified_gmt' => array( - 'description' => __( 'The date the coupon was last modified, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'discount_type' => array( - 'description' => __( 'Determines the type of discount that will be applied.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'fixed_cart', - 'enum' => array_keys( wc_get_coupon_types() ), - 'context' => array( 'view', 'edit' ), - ), - 'description' => array( - 'description' => __( 'Coupon description.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'date_expires' => array( - 'description' => __( "The date the coupon expires, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'date_expires_gmt' => array( - 'description' => __( 'The date the coupon expires, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'usage_count' => array( - 'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'individual_use' => array( - 'description' => __( 'If true, the coupon can only be used individually. Other applied coupons will be removed from the cart.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'product_ids' => array( - 'description' => __( 'List of product IDs the coupon can be used on.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'context' => array( 'view', 'edit' ), - ), - 'excluded_product_ids' => array( - 'description' => __( 'List of product IDs the coupon cannot be used on.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'context' => array( 'view', 'edit' ), - ), - 'usage_limit' => array( - 'description' => __( 'How many times the coupon can be used in total.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'usage_limit_per_user' => array( - 'description' => __( 'How many times the coupon can be used per customer.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'limit_usage_to_x_items' => array( - 'description' => __( 'Max number of items in the cart the coupon can be applied to.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'free_shipping' => array( - 'description' => __( 'If true and if the free shipping method requires a coupon, this coupon will enable free shipping.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'product_categories' => array( - 'description' => __( 'List of category IDs the coupon applies to.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'context' => array( 'view', 'edit' ), - ), - 'excluded_product_categories' => array( - 'description' => __( 'List of category IDs the coupon does not apply to.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'context' => array( 'view', 'edit' ), - ), - 'exclude_sale_items' => array( - 'description' => __( 'If true, this coupon will not be applied to items that have sale prices.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'minimum_amount' => array( - 'description' => __( 'Minimum order amount that needs to be in the cart before coupon applies.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'maximum_amount' => array( - 'description' => __( 'Maximum order amount allowed when using the coupon.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'email_restrictions' => array( - 'description' => __( 'List of email addresses that can use this coupon.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'string', - ), - 'context' => array( 'view', 'edit' ), - ), - 'used_by' => array( - 'description' => __( 'List of user IDs (or guest email addresses) that have used the coupon.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'meta_data' => array( - 'description' => __( 'Meta data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Meta ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'key' => array( - 'description' => __( 'Meta key.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'value' => array( - 'description' => __( 'Meta value.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - ), - ); - return $this->add_additional_fields_schema( $schema ); - } - - /** - * Get the query params for collections of attachments. - * - * @return array - */ - public function get_collection_params() { - $params = parent::get_collection_params(); - - $params['code'] = array( - 'description' => __( 'Limit result set to resources with a specific code.', 'woocommerce' ), - 'type' => 'string', - 'sanitize_callback' => 'sanitize_text_field', - 'validate_callback' => 'rest_validate_request_arg', - ); - - return $params; - } -} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php deleted file mode 100644 index 9d909c27679..00000000000 --- a/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php +++ /dev/null @@ -1,1827 +0,0 @@ -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.', 'woocommerce' ), - 'type' => 'integer', - ), - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), - ), - ), - 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 ), - ), - array( - 'methods' => WP_REST_Server::DELETABLE, - 'callback' => array( $this, 'delete_item' ), - 'permission_callback' => array( $this, 'delete_item_permissions_check' ), - 'args' => array( - 'force' => array( - 'default' => false, - 'type' => 'boolean', - 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), - ), - ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/batch', - array( - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'batch_items' ), - 'permission_callback' => array( $this, 'batch_items_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), - ), - 'schema' => array( $this, 'get_public_batch_schema' ), - ) - ); - } - - /** - * Get object. Return false if object is not of required type. - * - * @since 3.0.0 - * @param int $id Object ID. - * @return WC_Data|bool - */ - protected function get_object( $id ) { - $order = wc_get_order( $id ); - // In case id is a refund's id (or it's not an order at all), don't expose it via /orders/ path. - if ( ! $order || 'shop_order_refund' === $order->get_type() ) { - return false; - } - - return $order; - } - - /** - * Expands an order item to get its data. - * - * @param WC_Order_item $item Order item data. - * @return array - */ - protected function get_order_item_data( $item ) { - $data = $item->get_data(); - $format_decimal = array( 'subtotal', 'subtotal_tax', 'total', 'total_tax', 'tax_total', 'shipping_tax_total' ); - - // Format decimal values. - foreach ( $format_decimal as $key ) { - if ( isset( $data[ $key ] ) ) { - $data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] ); - } - } - - // Add SKU and PRICE to products. - if ( is_callable( array( $item, 'get_product' ) ) ) { - $data['sku'] = $item->get_product() ? $item->get_product()->get_sku() : null; - $data['price'] = $item->get_quantity() ? $item->get_total() / $item->get_quantity() : 0; - } - - // Add parent_name if the product is a variation. - if ( is_callable( array( $item, 'get_product' ) ) ) { - $product = $item->get_product(); - - if ( is_callable( array( $product, 'get_parent_data' ) ) ) { - $data['parent_name'] = $product->get_title(); - } else { - $data['parent_name'] = null; - } - } - - // Format taxes. - if ( ! empty( $data['taxes']['total'] ) ) { - $taxes = array(); - - foreach ( $data['taxes']['total'] as $tax_rate_id => $tax ) { - $taxes[] = array( - 'id' => $tax_rate_id, - 'total' => $tax, - 'subtotal' => isset( $data['taxes']['subtotal'][ $tax_rate_id ] ) ? $data['taxes']['subtotal'][ $tax_rate_id ] : '', - ); - } - $data['taxes'] = $taxes; - } elseif ( isset( $data['taxes'] ) ) { - $data['taxes'] = array(); - } - - // Remove names for coupons, taxes and shipping. - if ( isset( $data['code'] ) || isset( $data['rate_code'] ) || isset( $data['method_title'] ) ) { - unset( $data['name'] ); - } - - // Remove props we don't want to expose. - unset( $data['order_id'] ); - unset( $data['type'] ); - - // Expand meta_data to include user-friendly values. - $formatted_meta_data = $item->get_formatted_meta_data( null, true ); - $data['meta_data'] = array_map( - array( $this, 'merge_meta_item_with_formatted_meta_display_attributes' ), - $data['meta_data'], - array_fill( 0, count( $data['meta_data'] ), $formatted_meta_data ) - ); - - return $data; - } - - /** - * Merge the `$formatted_meta_data` `display_key` and `display_value` attribute values into the corresponding - * {@link WC_Meta_Data}. Returns the merged array. - * - * @param WC_Meta_Data $meta_item An object from {@link WC_Order_Item::get_meta_data()}. - * @param array $formatted_meta_data An object result from {@link WC_Order_Item::get_formatted_meta_data}. - * The keys are the IDs of {@link WC_Meta_Data}. - * - * @return array - */ - private function merge_meta_item_with_formatted_meta_display_attributes( $meta_item, $formatted_meta_data ) { - $result = array( - 'id' => $meta_item->id, - 'key' => $meta_item->key, - 'value' => $meta_item->value, - 'display_key' => $meta_item->key, // Default to original key, in case a formatted key is not available. - 'display_value' => $meta_item->value, // Default to original value, in case a formatted value is not available. - ); - - if ( array_key_exists( $meta_item->id, $formatted_meta_data ) ) { - $formatted_meta_item = $formatted_meta_data[ $meta_item->id ]; - - $result['display_key'] = wc_clean( $formatted_meta_item->display_key ); - $result['display_value'] = wc_clean( $formatted_meta_item->display_value ); - } - - return $result; - } - - /** - * Get formatted item data. - * - * @since 3.0.0 - * @param WC_Order $order WC_Data instance. - * - * @return array - */ - protected function get_formatted_item_data( $order ) { - $extra_fields = array( 'meta_data', 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines', 'refunds' ); - $format_decimal = array( 'discount_total', 'discount_tax', 'shipping_total', 'shipping_tax', 'shipping_total', 'shipping_tax', 'cart_tax', 'total', 'total_tax' ); - $format_date = array( 'date_created', 'date_modified', 'date_completed', 'date_paid' ); - // These fields are dependent on other fields. - $dependent_fields = array( - 'date_created_gmt' => 'date_created', - 'date_modified_gmt' => 'date_modified', - 'date_completed_gmt' => 'date_completed', - 'date_paid_gmt' => 'date_paid', - ); - - $format_line_items = array( 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines' ); - - // Only fetch fields that we need. - $fields = $this->get_fields_for_response( $this->request ); - foreach ( $dependent_fields as $field_key => $dependency ) { - if ( in_array( $field_key, $fields ) && ! in_array( $dependency, $fields ) ) { - $fields[] = $dependency; - } - } - - $extra_fields = array_intersect( $extra_fields, $fields ); - $format_decimal = array_intersect( $format_decimal, $fields ); - $format_date = array_intersect( $format_date, $fields ); - - $format_line_items = array_intersect( $format_line_items, $fields ); - - $data = $order->get_base_data(); - - // Add extra data as necessary. - foreach ( $extra_fields as $field ) { - switch ( $field ) { - case 'meta_data': - $data['meta_data'] = $order->get_meta_data(); - break; - case 'line_items': - $data['line_items'] = $order->get_items( 'line_item' ); - break; - case 'tax_lines': - $data['tax_lines'] = $order->get_items( 'tax' ); - break; - case 'shipping_lines': - $data['shipping_lines'] = $order->get_items( 'shipping' ); - break; - case 'fee_lines': - $data['fee_lines'] = $order->get_items( 'fee' ); - break; - case 'coupon_lines': - $data['coupon_lines'] = $order->get_items( 'coupon' ); - break; - case 'refunds': - $data['refunds'] = array(); - foreach ( $order->get_refunds() as $refund ) { - $data['refunds'][] = array( - 'id' => $refund->get_id(), - 'reason' => $refund->get_reason() ? $refund->get_reason() : '', - 'total' => '-' . wc_format_decimal( $refund->get_amount(), $this->request['dp'] ), - ); - } - break; - } - } - - // Format decimal values. - foreach ( $format_decimal as $key ) { - $data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] ); - } - - // Format date values. - foreach ( $format_date as $key ) { - $datetime = $data[ $key ]; - $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); - $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); - } - - // Format the order status. - $data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status']; - - // Format line items. - foreach ( $format_line_items as $key ) { - $data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) ); - } - - $allowed_fields = array( - 'id', - 'parent_id', - 'number', - 'order_key', - 'created_via', - 'version', - 'status', - 'currency', - 'date_created', - 'date_created_gmt', - 'date_modified', - 'date_modified_gmt', - 'discount_total', - 'discount_tax', - 'shipping_total', - 'shipping_tax', - 'cart_tax', - 'total', - 'total_tax', - 'prices_include_tax', - 'customer_id', - 'customer_ip_address', - 'customer_user_agent', - 'customer_note', - 'billing', - 'shipping', - 'payment_method', - 'payment_method_title', - 'transaction_id', - 'date_paid', - 'date_paid_gmt', - 'date_completed', - 'date_completed_gmt', - 'cart_hash', - 'meta_data', - 'line_items', - 'tax_lines', - 'shipping_lines', - 'fee_lines', - 'coupon_lines', - 'refunds', - ); - - $data = array_intersect_key( $data, array_flip( $allowed_fields ) ); - - return $data; - } - - /** - * Prepare a single order output for response. - * - * @since 3.0.0 - * @param WC_Data $object Object data. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response - */ - public function prepare_object_for_response( $object, $request ) { - $this->request = $request; - $this->request['dp'] = is_null( $this->request['dp'] ) ? wc_get_price_decimals() : absint( $this->request['dp'] ); - $request['context'] = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->get_formatted_item_data( $object ); - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $request['context'] ); - $response = rest_ensure_response( $data ); - $response->add_links( $this->prepare_links( $object, $request ) ); - - /** - * Filter the data for a response. - * - * The dynamic portion of the hook name, $this->post_type, - * refers to object type being prepared for the response. - * - * @param WP_REST_Response $response The response object. - * @param WC_Data $object Object data. - * @param WP_REST_Request $request Request object. - */ - return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); - } - - /** - * Prepare links for the request. - * - * @param WC_Data $object Object data. - * @param WP_REST_Request $request Request object. - * @return array Links for the given post. - */ - protected function prepare_links( $object, $request ) { - $links = array( - 'self' => array( - 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), - ), - 'collection' => array( - 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), - ), - ); - - if ( 0 !== (int) $object->get_customer_id() ) { - $links['customer'] = array( - 'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $object->get_customer_id() ) ), - ); - } - - if ( 0 !== (int) $object->get_parent_id() ) { - $links['up'] = array( - 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $object->get_parent_id() ) ), - ); - } - - return $links; - } - - /** - * Prepare objects query. - * - * @since 3.0.0 - * @param WP_REST_Request $request Full details about the request. - * @return array - */ - protected function prepare_objects_query( $request ) { - global $wpdb; - - $args = parent::prepare_objects_query( $request ); - - // Set post_status. - if ( in_array( $request['status'], $this->get_order_statuses(), true ) ) { - $args['post_status'] = 'wc-' . $request['status']; - } elseif ( 'any' === $request['status'] ) { - $args['post_status'] = 'any'; - } else { - $args['post_status'] = $request['status']; - } - - if ( isset( $request['customer'] ) ) { - if ( ! empty( $args['meta_query'] ) ) { - $args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - } - - $args['meta_query'][] = array( - 'key' => '_customer_user', - 'value' => $request['customer'], - 'type' => 'NUMERIC', - ); - } - - // Search by product. - if ( ! empty( $request['product'] ) ) { - $order_ids = $wpdb->get_col( - $wpdb->prepare( - "SELECT order_id - FROM {$wpdb->prefix}woocommerce_order_items - WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) - AND order_item_type = 'line_item'", - $request['product'] - ) - ); - - // Force WP_Query return empty if don't found any order. - $order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 ); - - $args['post__in'] = $order_ids; - } - - // Search. - if ( ! empty( $args['s'] ) ) { - $order_ids = wc_order_search( $args['s'] ); - - if ( ! empty( $order_ids ) ) { - unset( $args['s'] ); - $args['post__in'] = array_merge( $order_ids, array( 0 ) ); - } - } - - /** - * Filter the query arguments for a request. - * - * Enables adding extra arguments or setting defaults for an order collection request. - * - * @param array $args Key value array of query var to query value. - * @param WP_REST_Request $request The request used. - */ - $args = apply_filters( 'woocommerce_rest_orders_prepare_object_query', $args, $request ); - - return $args; - } - - /** - * Only return writable props from schema. - * - * @param array $schema Schema. - * @return bool - */ - protected function filter_writable_props( $schema ) { - return empty( $schema['readonly'] ); - } - - /** - * Prepare a single order for create or update. - * - * @param WP_REST_Request $request Request object. - * @param bool $creating If is creating a new object. - * @return WP_Error|WC_Data - */ - protected function prepare_object_for_database( $request, $creating = false ) { - $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; - $order = new WC_Order( $id ); - $schema = $this->get_item_schema(); - $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); - - // Handle all writable props. - foreach ( $data_keys as $key ) { - $value = $request[ $key ]; - - if ( ! is_null( $value ) ) { - switch ( $key ) { - case 'status': - // Status change should be done later so transitions have new data. - break; - case 'billing': - case 'shipping': - $this->update_address( $order, $value, $key ); - break; - case 'line_items': - case 'shipping_lines': - case 'fee_lines': - case 'coupon_lines': - if ( is_array( $value ) ) { - foreach ( $value as $item ) { - if ( is_array( $item ) ) { - if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) { - $order->remove_item( $item['id'] ); - } else { - $this->set_item( $order, $key, $item ); - } - } - } - } - break; - case 'meta_data': - if ( is_array( $value ) ) { - foreach ( $value as $meta ) { - $order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); - } - } - break; - default: - if ( is_callable( array( $order, "set_{$key}" ) ) ) { - $order->{"set_{$key}"}( $value ); - } - break; - } - } - } - - /** - * Filters an object before it is inserted via the REST API. - * - * The dynamic portion of the hook name, `$this->post_type`, - * refers to the object type slug. - * - * @param WC_Data $order Object object. - * @param WP_REST_Request $request Request object. - * @param bool $creating If is creating a new object. - */ - return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $order, $request, $creating ); - } - - /** - * Save an object data. - * - * @since 3.0.0 - * @throws WC_REST_Exception But all errors are validated before returning any data. - * @param WP_REST_Request $request Full details about the request. - * @param bool $creating If is creating a new object. - * @return WC_Data|WP_Error - */ - protected function save_object( $request, $creating = false ) { - try { - $object = $this->prepare_object_for_database( $request, $creating ); - - if ( is_wp_error( $object ) ) { - return $object; - } - - // Make sure gateways are loaded so hooks from gateways fire on save/create. - WC()->payment_gateways(); - - if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] ) { - // Make sure customer exists. - if ( false === get_user_by( 'id', $request['customer_id'] ) ) { - throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); - } - - // Make sure customer is part of blog. - if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) { - add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' ); - } - } - - if ( $creating ) { - $object->set_created_via( 'rest-api' ); - $object->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); - $object->calculate_totals(); - } else { - // If items have changed, recalculate order totals. - if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) { - $object->calculate_totals( true ); - } - } - - // Set status. - if ( ! empty( $request['status'] ) ) { - $object->set_status( $request['status'] ); - } - - $object->save(); - - // Actions for after the order is saved. - if ( true === $request['set_paid'] ) { - if ( $creating || $object->needs_payment() ) { - $object->payment_complete( $request['transaction_id'] ); - } - } - - return $this->get_object( $object->get_id() ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); - } catch ( WC_REST_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Update address. - * - * @param WC_Order $order Order data. - * @param array $posted Posted data. - * @param string $type Address type. - */ - protected function update_address( $order, $posted, $type = 'billing' ) { - foreach ( $posted as $key => $value ) { - if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { - $order->{"set_{$type}_{$key}"}( $value ); - } - } - } - - /** - * Gets the product ID from the SKU or posted ID. - * - * @throws WC_REST_Exception When SKU or ID is not valid. - * @param array $posted Request data. - * @param string $action 'create' to add line item or 'update' to update it. - * @return int - */ - protected function get_product_id( $posted, $action = 'create' ) { - if ( ! empty( $posted['sku'] ) ) { - $product_id = (int) wc_get_product_id_by_sku( $posted['sku'] ); - } elseif ( ! empty( $posted['product_id'] ) && empty( $posted['variation_id'] ) ) { - $product_id = (int) $posted['product_id']; - } elseif ( ! empty( $posted['variation_id'] ) ) { - $product_id = (int) $posted['variation_id']; - } elseif ( 'update' === $action ) { - $product_id = 0; - } else { - throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 ); - } - return $product_id; - } - - /** - * Maybe set an item prop if the value was posted. - * - * @param WC_Order_Item $item Order item. - * @param string $prop Order property. - * @param array $posted Request data. - */ - protected function maybe_set_item_prop( $item, $prop, $posted ) { - if ( isset( $posted[ $prop ] ) ) { - $item->{"set_$prop"}( $posted[ $prop ] ); - } - } - - /** - * Maybe set item props if the values were posted. - * - * @param WC_Order_Item $item Order item data. - * @param string[] $props Properties. - * @param array $posted Request data. - */ - protected function maybe_set_item_props( $item, $props, $posted ) { - foreach ( $props as $prop ) { - $this->maybe_set_item_prop( $item, $prop, $posted ); - } - } - - /** - * Maybe set item meta if posted. - * - * @param WC_Order_Item $item Order item data. - * @param array $posted Request data. - */ - protected function maybe_set_item_meta_data( $item, $posted ) { - if ( ! empty( $posted['meta_data'] ) && is_array( $posted['meta_data'] ) ) { - foreach ( $posted['meta_data'] as $meta ) { - if ( isset( $meta['key'] ) ) { - $value = isset( $meta['value'] ) ? $meta['value'] : null; - $item->update_meta_data( $meta['key'], $value, isset( $meta['id'] ) ? $meta['id'] : '' ); - } - } - } - } - - /** - * Create or update a line item. - * - * @param array $posted Line item data. - * @param string $action 'create' to add line item or 'update' to update it. - * @param object $item Passed when updating an item. Null during creation. - * @return WC_Order_Item_Product - * @throws WC_REST_Exception Invalid data, server error. - */ - protected function prepare_line_items( $posted, $action = 'create', $item = null ) { - $item = is_null( $item ) ? new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item; - $product = wc_get_product( $this->get_product_id( $posted, $action ) ); - - if ( $product && $product !== $item->get_product() ) { - $item->set_product( $product ); - - if ( 'create' === $action ) { - $quantity = isset( $posted['quantity'] ) ? $posted['quantity'] : 1; - $total = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) ); - $item->set_total( $total ); - $item->set_subtotal( $total ); - } - } - - $this->maybe_set_item_props( $item, array( 'name', 'quantity', 'total', 'subtotal', 'tax_class' ), $posted ); - $this->maybe_set_item_meta_data( $item, $posted ); - - return $item; - } - - /** - * Create or update an order shipping method. - * - * @param array $posted $shipping Item data. - * @param string $action 'create' to add shipping or 'update' to update it. - * @param object $item Passed when updating an item. Null during creation. - * @return WC_Order_Item_Shipping - * @throws WC_REST_Exception Invalid data, server error. - */ - protected function prepare_shipping_lines( $posted, $action = 'create', $item = null ) { - $item = is_null( $item ) ? new WC_Order_Item_Shipping( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item; - - if ( 'create' === $action ) { - if ( empty( $posted['method_id'] ) ) { - throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); - } - } - - $this->maybe_set_item_props( $item, array( 'method_id', 'method_title', 'total', 'instance_id' ), $posted ); - $this->maybe_set_item_meta_data( $item, $posted ); - - return $item; - } - - /** - * Create or update an order fee. - * - * @param array $posted Item data. - * @param string $action 'create' to add fee or 'update' to update it. - * @param object $item Passed when updating an item. Null during creation. - * @return WC_Order_Item_Fee - * @throws WC_REST_Exception Invalid data, server error. - */ - protected function prepare_fee_lines( $posted, $action = 'create', $item = null ) { - $item = is_null( $item ) ? new WC_Order_Item_Fee( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item; - - if ( 'create' === $action ) { - if ( empty( $posted['name'] ) ) { - throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 ); - } - } - - $this->maybe_set_item_props( $item, array( 'name', 'tax_class', 'tax_status', 'total' ), $posted ); - $this->maybe_set_item_meta_data( $item, $posted ); - - return $item; - } - - /** - * Create or update an order coupon. - * - * @param array $posted Item data. - * @param string $action 'create' to add coupon or 'update' to update it. - * @param object $item Passed when updating an item. Null during creation. - * @return WC_Order_Item_Coupon - * @throws WC_REST_Exception Invalid data, server error. - */ - protected function prepare_coupon_lines( $posted, $action = 'create', $item = null ) { - $item = is_null( $item ) ? new WC_Order_Item_Coupon( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item; - - if ( 'create' === $action ) { - if ( empty( $posted['code'] ) ) { - throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); - } - } - - $this->maybe_set_item_props( $item, array( 'code', 'discount' ), $posted ); - $this->maybe_set_item_meta_data( $item, $posted ); - - return $item; - } - - /** - * Wrapper method to create/update order items. - * When updating, the item ID provided is checked to ensure it is associated - * with the order. - * - * @param WC_Order $order order object. - * @param string $item_type The item type. - * @param array $posted item provided in the request body. - * @throws WC_REST_Exception If item ID is not associated with order. - */ - protected function set_item( $order, $item_type, $posted ) { - global $wpdb; - - if ( ! empty( $posted['id'] ) ) { - $action = 'update'; - } else { - $action = 'create'; - } - - $method = 'prepare_' . $item_type; - $item = null; - - // Verify provided line item ID is associated with order. - if ( 'update' === $action ) { - $item = $order->get_item( absint( $posted['id'] ), false ); - - if ( ! $item ) { - throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); - } - } - - // Prepare item data. - $item = $this->$method( $posted, $action, $item ); - - do_action( 'woocommerce_rest_set_order_item', $item, $posted ); - - // If creating the order, add the item to it. - if ( 'create' === $action ) { - $order->add_item( $item ); - } else { - $item->save(); - } - } - - /** - * Helper method to check if the resource ID associated with the provided item is null. - * Items can be deleted by setting the resource ID to null. - * - * @param array $item Item provided in the request body. - * @return bool True if the item resource ID is null, false otherwise. - */ - protected function item_is_null( $item ) { - $keys = array( 'product_id', 'method_id', 'method_title', 'name', 'code' ); - - foreach ( $keys as $key ) { - if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { - return true; - } - } - - return false; - } - - /** - * Get order statuses without prefixes. - * - * @return array - */ - protected function get_order_statuses() { - $order_statuses = array(); - - foreach ( array_keys( wc_get_order_statuses() ) as $status ) { - $order_statuses[] = str_replace( 'wc-', '', $status ); - } - - return $order_statuses; - } - - /** - * Get the Order's schema, conforming to JSON Schema. - * - * @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 for the resource.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'parent_id' => array( - 'description' => __( 'Parent order ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'number' => array( - 'description' => __( 'Order number.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'order_key' => array( - 'description' => __( 'Order key.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'created_via' => array( - 'description' => __( 'Shows where the order was created.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'version' => array( - 'description' => __( 'Version of WooCommerce which last updated the order.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'status' => array( - 'description' => __( 'Order status.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'pending', - 'enum' => $this->get_order_statuses(), - 'context' => array( 'view', 'edit' ), - ), - 'currency' => array( - 'description' => __( 'Currency the order was created with, in ISO format.', 'woocommerce' ), - 'type' => 'string', - 'default' => get_woocommerce_currency(), - 'enum' => array_keys( get_woocommerce_currencies() ), - 'context' => array( 'view', 'edit' ), - ), - 'date_created' => array( - 'description' => __( "The date the order was created, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_created_gmt' => array( - 'description' => __( 'The date the order was created, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_modified' => array( - 'description' => __( "The date the order was last modified, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_modified_gmt' => array( - 'description' => __( 'The date the order was last modified, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'discount_total' => array( - 'description' => __( 'Total discount amount for the order.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'discount_tax' => array( - 'description' => __( 'Total discount tax amount for the order.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'shipping_total' => array( - 'description' => __( 'Total shipping amount for the order.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'shipping_tax' => array( - 'description' => __( 'Total shipping tax amount for the order.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'cart_tax' => array( - 'description' => __( 'Sum of line item taxes only.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'total' => array( - 'description' => __( 'Grand total.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'total_tax' => array( - 'description' => __( 'Sum of all taxes.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'prices_include_tax' => array( - 'description' => __( 'True the prices included tax during checkout.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'customer_id' => array( - 'description' => __( 'User ID who owns the order. 0 for guests.', 'woocommerce' ), - 'type' => 'integer', - 'default' => 0, - 'context' => array( 'view', 'edit' ), - ), - 'customer_ip_address' => array( - 'description' => __( "Customer's IP address.", 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'customer_user_agent' => array( - 'description' => __( 'User agent of the customer.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'customer_note' => array( - 'description' => __( 'Note left by customer during checkout.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'billing' => array( - 'description' => __( 'Billing address.', 'woocommerce' ), - 'type' => 'object', - 'context' => array( 'view', 'edit' ), - 'properties' => array( - 'first_name' => array( - 'description' => __( 'First name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'last_name' => array( - 'description' => __( 'Last name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'company' => array( - 'description' => __( 'Company name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'address_1' => array( - 'description' => __( 'Address line 1', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'address_2' => array( - 'description' => __( 'Address line 2', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'city' => array( - 'description' => __( 'City name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'state' => array( - 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'postcode' => array( - 'description' => __( 'Postal code.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'country' => array( - 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'email' => array( - 'description' => __( 'Email address.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'email', - 'context' => array( 'view', 'edit' ), - ), - 'phone' => array( - 'description' => __( 'Phone number.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - 'shipping' => array( - 'description' => __( 'Shipping address.', 'woocommerce' ), - 'type' => 'object', - 'context' => array( 'view', 'edit' ), - 'properties' => array( - 'first_name' => array( - 'description' => __( 'First name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'last_name' => array( - 'description' => __( 'Last name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'company' => array( - 'description' => __( 'Company name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'address_1' => array( - 'description' => __( 'Address line 1', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'address_2' => array( - 'description' => __( 'Address line 2', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'city' => array( - 'description' => __( 'City name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'state' => array( - 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'postcode' => array( - 'description' => __( 'Postal code.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'country' => array( - 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - 'payment_method' => array( - 'description' => __( 'Payment method ID.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'payment_method_title' => array( - 'description' => __( 'Payment method title.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'arg_options' => array( - 'sanitize_callback' => 'sanitize_text_field', - ), - ), - 'transaction_id' => array( - 'description' => __( 'Unique transaction ID.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'date_paid' => array( - 'description' => __( "The date the order was paid, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_paid_gmt' => array( - 'description' => __( 'The date the order was paid, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_completed' => array( - 'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_completed_gmt' => array( - 'description' => __( 'The date the order was completed, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'cart_hash' => array( - 'description' => __( 'MD5 hash of cart items to ensure orders are not modified.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'meta_data' => array( - 'description' => __( 'Meta data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Meta ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'key' => array( - 'description' => __( 'Meta key.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'value' => array( - 'description' => __( 'Meta value.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - 'line_items' => array( - 'description' => __( 'Line items data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Item ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'name' => array( - 'description' => __( 'Product name.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - 'parent_name' => array( - 'description' => __( 'Parent product name if the product is a variation.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'product_id' => array( - 'description' => __( 'Product ID.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - 'variation_id' => array( - 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'quantity' => array( - 'description' => __( 'Quantity ordered.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'tax_class' => array( - 'description' => __( 'Tax class of product.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'subtotal' => array( - 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'subtotal_tax' => array( - 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'total' => array( - 'description' => __( 'Line total (after discounts).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'total_tax' => array( - 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'taxes' => array( - 'description' => __( 'Line taxes.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Tax rate ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'total' => array( - 'description' => __( 'Tax total.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'subtotal' => array( - 'description' => __( 'Tax subtotal.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - 'meta_data' => array( - 'description' => __( 'Meta data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Meta ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'key' => array( - 'description' => __( 'Meta key.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'value' => array( - 'description' => __( 'Meta value.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - 'display_key' => array( - 'description' => __( 'Meta key for UI display.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'display_value' => array( - 'description' => __( 'Meta value for UI display.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - 'sku' => array( - 'description' => __( 'Product SKU.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'price' => array( - 'description' => __( 'Product price.', 'woocommerce' ), - 'type' => 'number', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ), - ), - 'tax_lines' => array( - 'description' => __( 'Tax lines data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Item ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'rate_code' => array( - 'description' => __( 'Tax rate code.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'rate_id' => array( - 'description' => __( 'Tax rate ID.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'label' => array( - 'description' => __( 'Tax rate label.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'compound' => array( - 'description' => __( 'Show if is a compound tax rate.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'tax_total' => array( - 'description' => __( 'Tax total (not including shipping taxes).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'shipping_tax_total' => array( - 'description' => __( 'Shipping tax total.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'meta_data' => array( - 'description' => __( 'Meta data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Meta ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'key' => array( - 'description' => __( 'Meta key.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'value' => array( - 'description' => __( 'Meta value.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - ), - ), - ), - 'shipping_lines' => array( - 'description' => __( 'Shipping lines data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Item ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'method_title' => array( - 'description' => __( 'Shipping method name.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - 'method_id' => array( - 'description' => __( 'Shipping method ID.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - 'instance_id' => array( - 'description' => __( 'Shipping instance ID.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'total' => array( - 'description' => __( 'Line total (after discounts).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'total_tax' => array( - 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'taxes' => array( - 'description' => __( 'Line taxes.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Tax rate ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'total' => array( - 'description' => __( 'Tax total.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ), - ), - 'meta_data' => array( - 'description' => __( 'Meta data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Meta ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'key' => array( - 'description' => __( 'Meta key.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'value' => array( - 'description' => __( 'Meta value.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - ), - ), - ), - 'fee_lines' => array( - 'description' => __( 'Fee lines data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Item ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'name' => array( - 'description' => __( 'Fee name.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - 'tax_class' => array( - 'description' => __( 'Tax class of fee.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'tax_status' => array( - 'description' => __( 'Tax status of fee.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'enum' => array( 'taxable', 'none' ), - ), - 'total' => array( - 'description' => __( 'Line total (after discounts).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'total_tax' => array( - 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'taxes' => array( - 'description' => __( 'Line taxes.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Tax rate ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'total' => array( - 'description' => __( 'Tax total.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'subtotal' => array( - 'description' => __( 'Tax subtotal.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ), - ), - 'meta_data' => array( - 'description' => __( 'Meta data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Meta ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'key' => array( - 'description' => __( 'Meta key.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'value' => array( - 'description' => __( 'Meta value.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - ), - ), - ), - 'coupon_lines' => array( - 'description' => __( 'Coupons line data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Item ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'code' => array( - 'description' => __( 'Coupon code.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - 'discount' => array( - 'description' => __( 'Discount total.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'discount_tax' => array( - 'description' => __( 'Discount total tax.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'meta_data' => array( - 'description' => __( 'Meta data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Meta ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'key' => array( - 'description' => __( 'Meta key.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'value' => array( - 'description' => __( 'Meta value.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - ), - ), - ), - 'refunds' => array( - 'description' => __( 'List of refunds.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Refund ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'reason' => array( - 'description' => __( 'Refund reason.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'total' => array( - 'description' => __( 'Refund total.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ), - ), - 'set_paid' => array( - 'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'edit' ), - ), - ), - ); - - return $this->add_additional_fields_schema( $schema ); - } - - /** - * Get the query params for collections. - * - * @return array - */ - public function get_collection_params() { - $params = parent::get_collection_params(); - - $params['status'] = array( - 'default' => 'any', - 'description' => __( 'Limit result set to orders assigned a specific status.', 'woocommerce' ), - 'type' => 'string', - 'enum' => array_merge( array( 'any', 'trash' ), $this->get_order_statuses() ), - 'sanitize_callback' => 'sanitize_key', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['customer'] = array( - 'description' => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ), - 'type' => 'integer', - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['product'] = array( - 'description' => __( 'Limit result set to orders assigned a specific product.', 'woocommerce' ), - 'type' => 'integer', - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['dp'] = array( - 'default' => wc_get_price_decimals(), - 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), - 'type' => 'integer', - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - ); - - return $params; - } -} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php deleted file mode 100644 index cc3480936e3..00000000000 --- a/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php +++ /dev/null @@ -1,199 +0,0 @@ -/reviews. - * - * @package WooCommerce\RestApi - * @since 2.6.0 - */ - -defined( 'ABSPATH' ) || exit; - -/** - * REST API Product Reviews Controller Class. - * - * @package WooCommerce\RestApi - * @extends WC_REST_Product_Reviews_V1_Controller - */ -class WC_REST_Product_Reviews_V2_Controller extends WC_REST_Product_Reviews_V1_Controller { - - /** - * Endpoint namespace. - * - * @var string - */ - protected $namespace = 'wc/v2'; - - /** - * Route base. - * - * @var string - */ - protected $rest_base = 'products/(?P[\d]+)/reviews'; - - /** - * Register the routes for product reviews. - */ - public function register_routes() { - parent::register_routes(); - - register_rest_route( - $this->namespace, '/' . $this->rest_base . '/batch', array( - 'args' => array( - 'product_id' => array( - 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), - 'type' => 'integer', - ), - ), - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'batch_items' ), - 'permission_callback' => array( $this, 'batch_items_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), - ), - 'schema' => array( $this, 'get_public_batch_schema' ), - ) - ); - } - - /** - * Check if a given request has access to batch manage product reviews. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function batch_items_permissions_check( $request ) { - if ( ! wc_rest_check_post_permissions( 'product', 'batch' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - return true; - } - - /** - * Prepare a single product review output for response. - * - * @param WP_Comment $review Product review object. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response $response Response data. - */ - public function prepare_item_for_response( $review, $request ) { - $data = array( - 'id' => (int) $review->comment_ID, - 'date_created' => wc_rest_prepare_date_response( $review->comment_date ), - 'date_created_gmt' => wc_rest_prepare_date_response( $review->comment_date_gmt ), - 'review' => $review->comment_content, - 'rating' => (int) get_comment_meta( $review->comment_ID, 'rating', true ), - 'name' => $review->comment_author, - 'email' => $review->comment_author_email, - 'verified' => wc_review_is_from_verified_owner( $review->comment_ID ), - ); - - $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 ); - - $response->add_links( $this->prepare_links( $review, $request ) ); - - /** - * Filter product reviews object returned from the REST API. - * - * @param WP_REST_Response $response The response object. - * @param WP_Comment $review Product review object used to create response. - * @param WP_REST_Request $request Request object. - */ - return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request ); - } - - - /** - * Bulk create, update and delete items. - * - * @since 3.0.0 - * @param WP_REST_Request $request Full details about the request. - * @return array Of WP_Error or WP_REST_Response. - */ - public function batch_items( $request ) { - $items = array_filter( $request->get_params() ); - $params = $request->get_url_params(); - $product_id = $params['product_id']; - $body_params = array(); - - foreach ( array( 'update', 'create', 'delete' ) as $batch_type ) { - if ( ! empty( $items[ $batch_type ] ) ) { - $injected_items = array(); - foreach ( $items[ $batch_type ] as $item ) { - $injected_items[] = is_array( $item ) ? array_merge( array( 'product_id' => $product_id ), $item ) : $item; - } - $body_params[ $batch_type ] = $injected_items; - } - } - - $request = new WP_REST_Request( $request->get_method() ); - $request->set_body_params( $body_params ); - - return parent::batch_items( $request ); - } - - /** - * Get the Product Review's schema, conforming to JSON Schema. - * - * @return array - */ - public function get_item_schema() { - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'product_review', - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'review' => array( - 'description' => __( 'The content of the review.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'date_created' => array( - 'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'date_created_gmt' => array( - 'description' => __( 'The date the review was created, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'rating' => array( - 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'Reviewer name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'email' => array( - 'description' => __( 'Reviewer email.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'verified' => array( - 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ); - - return $this->add_additional_fields_schema( $schema ); - } -} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php deleted file mode 100644 index 7d06b2573d2..00000000000 --- a/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php +++ /dev/null @@ -1,2382 +0,0 @@ -post_type}_object", array( $this, 'clear_transients' ) ); - } - - /** - * Register the routes for products. - */ - 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.', 'woocommerce' ), - 'type' => 'integer', - ), - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => array( - 'context' => $this->get_context_param( - array( - 'default' => 'view', - ) - ), - ), - ), - 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 ), - ), - array( - 'methods' => WP_REST_Server::DELETABLE, - 'callback' => array( $this, 'delete_item' ), - 'permission_callback' => array( $this, 'delete_item_permissions_check' ), - 'args' => array( - 'force' => array( - 'default' => false, - 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), - 'type' => 'boolean', - ), - ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/batch', - array( - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'batch_items' ), - 'permission_callback' => array( $this, 'batch_items_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), - ), - 'schema' => array( $this, 'get_public_batch_schema' ), - ) - ); - } - - /** - * Get object. - * - * @param int $id Object ID. - * - * @since 3.0.0 - * @return WC_Data - */ - protected function get_object( $id ) { - return wc_get_product( $id ); - } - - /** - * Prepare a single product output for response. - * - * @param WC_Data $object Object data. - * @param WP_REST_Request $request Request object. - * - * @since 3.0.0 - * @return WP_REST_Response - */ - public function prepare_object_for_response( $object, $request ) { - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $this->request = $request; - $data = $this->get_product_data( $object, $context, $request ); - - // Add variations to variable products. - if ( $object->is_type( 'variable' ) && $object->has_child() ) { - $data['variations'] = $object->get_children(); - } - - // Add grouped products data. - if ( $object->is_type( 'grouped' ) && $object->has_child() ) { - $data['grouped_products'] = $object->get_children(); - } - - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); - $response = rest_ensure_response( $data ); - $response->add_links( $this->prepare_links( $object, $request ) ); - - /** - * Filter the data for a response. - * - * The dynamic portion of the hook name, $this->post_type, - * refers to object type being prepared for the response. - * - * @param WP_REST_Response $response The response object. - * @param WC_Data $object Object data. - * @param WP_REST_Request $request Request object. - */ - return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); - } - - /** - * Prepare objects query. - * - * @param WP_REST_Request $request Full details about the request. - * - * @since 3.0.0 - * @return array - */ - protected function prepare_objects_query( $request ) { - $args = parent::prepare_objects_query( $request ); - - // Set post_status. - $args['post_status'] = $request['status']; - - // Taxonomy query to filter products by type, category, - // tag, shipping class, and attribute. - $tax_query = array(); - - // Map between taxonomy name and arg's key. - $taxonomies = array( - 'product_cat' => 'category', - 'product_tag' => 'tag', - 'product_shipping_class' => 'shipping_class', - ); - - // Set tax_query for each passed arg. - foreach ( $taxonomies as $taxonomy => $key ) { - if ( ! empty( $request[ $key ] ) ) { - $tax_query[] = array( - 'taxonomy' => $taxonomy, - 'field' => 'term_id', - 'terms' => $request[ $key ], - ); - } - } - - // Filter product type by slug. - if ( ! empty( $request['type'] ) ) { - $tax_query[] = array( - 'taxonomy' => 'product_type', - 'field' => 'slug', - 'terms' => $request['type'], - ); - } - - // Filter by attribute and term. - if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { - if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { - $tax_query[] = array( - 'taxonomy' => $request['attribute'], - 'field' => 'term_id', - 'terms' => $request['attribute_term'], - ); - } - } - - if ( ! empty( $tax_query ) ) { - $args['tax_query'] = $tax_query; // WPCS: slow query ok. - } - - // Filter featured. - if ( is_bool( $request['featured'] ) ) { - $args['tax_query'][] = array( - 'taxonomy' => 'product_visibility', - 'field' => 'name', - 'terms' => 'featured', - 'operator' => true === $request['featured'] ? 'IN' : 'NOT IN', - ); - } - - // Filter by sku. - if ( ! empty( $request['sku'] ) ) { - $skus = explode( ',', $request['sku'] ); - // Include the current string as a SKU too. - if ( 1 < count( $skus ) ) { - $skus[] = $request['sku']; - } - - $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. - $args, - array( - 'key' => '_sku', - 'value' => $skus, - 'compare' => 'IN', - ) - ); - } - - // Filter by tax class. - if ( ! empty( $request['tax_class'] ) ) { - $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. - $args, - array( - 'key' => '_tax_class', - 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', - ) - ); - } - - // Price filter. - if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { - $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); // WPCS: slow query ok. - } - - // Filter product in stock or out of stock. - if ( is_bool( $request['in_stock'] ) ) { - $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. - $args, - array( - 'key' => '_stock_status', - 'value' => true === $request['in_stock'] ? 'instock' : 'outofstock', - ) - ); - } - - // Filter by on sale products. - if ( is_bool( $request['on_sale'] ) ) { - $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; - $on_sale_ids = wc_get_product_ids_on_sale(); - - // Use 0 when there's no on sale products to avoid return all products. - $on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids; - - $args[ $on_sale_key ] += $on_sale_ids; - } - - // Force the post_type argument, since it's not a user input variable. - if ( ! empty( $request['sku'] ) ) { - $args['post_type'] = array( 'product', 'product_variation' ); - } else { - $args['post_type'] = $this->post_type; - } - - return $args; - } - - /** - * Get the downloads for a product or product variation. - * - * @param WC_Product|WC_Product_Variation $product Product instance. - * - * @return array - */ - protected function get_downloads( $product ) { - $downloads = array(); - - if ( $product->is_downloadable() ) { - foreach ( $product->get_downloads() as $file_id => $file ) { - $downloads[] = array( - 'id' => $file_id, // MD5 hash. - 'name' => $file['name'], - 'file' => $file['file'], - ); - } - } - - return $downloads; - } - - /** - * Get taxonomy terms. - * - * @param WC_Product $product Product instance. - * @param string $taxonomy Taxonomy slug. - * - * @return array - */ - protected function get_taxonomy_terms( $product, $taxonomy = 'cat' ) { - $terms = array(); - - foreach ( wc_get_object_terms( $product->get_id(), 'product_' . $taxonomy ) as $term ) { - $terms[] = array( - 'id' => $term->term_id, - 'name' => $term->name, - 'slug' => $term->slug, - ); - } - - return $terms; - } - - /** - * Get the images for a product or product variation. - * - * @param WC_Product|WC_Product_Variation $product Product instance. - * - * @return array - */ - protected function get_images( $product ) { - $images = array(); - $attachment_ids = array(); - - // Add featured image. - if ( $product->get_image_id() ) { - $attachment_ids[] = $product->get_image_id(); - } - - // Add gallery images. - $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); - - // Build image data. - foreach ( $attachment_ids as $position => $attachment_id ) { - $attachment_post = get_post( $attachment_id ); - if ( is_null( $attachment_post ) ) { - continue; - } - - $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); - if ( ! is_array( $attachment ) ) { - continue; - } - - $images[] = array( - 'id' => (int) $attachment_id, - 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ), - 'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ), - 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ), - 'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ), - 'src' => current( $attachment ), - 'name' => get_the_title( $attachment_id ), - 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), - 'position' => (int) $position, - ); - } - - // Set a placeholder image if the product has no images set. - if ( empty( $images ) ) { - $images[] = array( - 'id' => 0, - 'date_created' => wc_rest_prepare_date_response( current_time( 'mysql' ), false ), // Default to now. - 'date_created_gmt' => wc_rest_prepare_date_response( time() ), // Default to now. - 'date_modified' => wc_rest_prepare_date_response( current_time( 'mysql' ), false ), - 'date_modified_gmt' => wc_rest_prepare_date_response( time() ), - 'src' => wc_placeholder_img_src(), - 'name' => __( 'Placeholder', 'woocommerce' ), - 'alt' => __( 'Placeholder', 'woocommerce' ), - 'position' => 0, - ); - } - - return $images; - } - - /** - * Get attribute taxonomy label. - * - * @param string $name Taxonomy name. - * - * @deprecated 3.0.0 - * @return string - */ - protected function get_attribute_taxonomy_label( $name ) { - $tax = get_taxonomy( $name ); - $labels = get_taxonomy_labels( $tax ); - - return $labels->singular_name; - } - - /** - * Get product attribute taxonomy name. - * - * @param string $slug Taxonomy name. - * @param WC_Product $product Product data. - * - * @since 3.0.0 - * @return string - */ - protected function get_attribute_taxonomy_name( $slug, $product ) { - // Format slug so it matches attributes of the product. - $slug = wc_attribute_taxonomy_slug( $slug ); - $attributes = $product->get_attributes(); - $attribute = false; - - // pa_ attributes. - if ( isset( $attributes[ wc_attribute_taxonomy_name( $slug ) ] ) ) { - $attribute = $attributes[ wc_attribute_taxonomy_name( $slug ) ]; - } elseif ( isset( $attributes[ $slug ] ) ) { - $attribute = $attributes[ $slug ]; - } - - if ( ! $attribute ) { - return $slug; - } - - // Taxonomy attribute name. - if ( $attribute->is_taxonomy() ) { - $taxonomy = $attribute->get_taxonomy_object(); - return $taxonomy->attribute_label; - } - - // Custom product attribute name. - return $attribute->get_name(); - } - - /** - * Get default attributes. - * - * @param WC_Product $product Product instance. - * - * @return array - */ - protected function get_default_attributes( $product ) { - $default = array(); - - if ( $product->is_type( 'variable' ) ) { - foreach ( array_filter( (array) $product->get_default_attributes(), 'strlen' ) as $key => $value ) { - if ( 0 === strpos( $key, 'pa_' ) ) { - $default[] = array( - 'id' => wc_attribute_taxonomy_id_by_name( $key ), - 'name' => $this->get_attribute_taxonomy_name( $key, $product ), - 'option' => $value, - ); - } else { - $default[] = array( - 'id' => 0, - 'name' => $this->get_attribute_taxonomy_name( $key, $product ), - 'option' => $value, - ); - } - } - } - - return $default; - } - - /** - * Get attribute options. - * - * @param int $product_id Product ID. - * @param array $attribute Attribute data. - * - * @return array - */ - protected function get_attribute_options( $product_id, $attribute ) { - if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { - return wc_get_product_terms( - $product_id, - $attribute['name'], - array( - 'fields' => 'names', - ) - ); - } elseif ( isset( $attribute['value'] ) ) { - return array_map( 'trim', explode( '|', $attribute['value'] ) ); - } - - return array(); - } - - /** - * Get the attributes for a product or product variation. - * - * @param WC_Product|WC_Product_Variation $product Product instance. - * - * @return array - */ - protected function get_attributes( $product ) { - $attributes = array(); - - if ( $product->is_type( 'variation' ) ) { - $_product = wc_get_product( $product->get_parent_id() ); - foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { - $name = str_replace( 'attribute_', '', $attribute_name ); - - if ( empty( $attribute ) && '0' !== $attribute ) { - continue; - } - - // Taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`. - if ( 0 === strpos( $attribute_name, 'attribute_pa_' ) ) { - $option_term = get_term_by( 'slug', $attribute, $name ); - $attributes[] = array( - 'id' => wc_attribute_taxonomy_id_by_name( $name ), - 'name' => $this->get_attribute_taxonomy_name( $name, $_product ), - 'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute, - ); - } else { - $attributes[] = array( - 'id' => 0, - 'name' => $this->get_attribute_taxonomy_name( $name, $_product ), - 'option' => $attribute, - ); - } - } - } else { - foreach ( $product->get_attributes() as $attribute ) { - $attributes[] = array( - 'id' => $attribute['is_taxonomy'] ? wc_attribute_taxonomy_id_by_name( $attribute['name'] ) : 0, - 'name' => $this->get_attribute_taxonomy_name( $attribute['name'], $product ), - 'position' => (int) $attribute['position'], - 'visible' => (bool) $attribute['is_visible'], - 'variation' => (bool) $attribute['is_variation'], - 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), - ); - } - } - - return $attributes; - } - - /** - * Fetch price HTML. - * - * @param WC_Product $product Product object. - * @param string $context Context of request, can be `view` or `edit`. - * - * @return string - */ - protected function api_get_price_html( $product, $context ) { - return $product->get_price_html(); - } - - /** - * Fetch related IDs. - * - * @param WC_Product $product Product object. - * @param string $context Context of request, can be `view` or `edit`. - * - * @return array - */ - protected function api_get_related_ids( $product, $context ) { - return array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ); - } - - /** - * Fetch meta data. - * - * @param WC_Product $product Product object. - * @param string $context Context of request, can be `view` or `edit`. - * - * @return array - */ - protected function api_get_meta_data( $product, $context ) { - return $product->get_meta_data(); - } - - /** - * Get product data. - * - * @param WC_Product $product Product instance. - * @param string $context Request context. Options: 'view' and 'edit'. - * - * @return array - */ - protected function get_product_data( $product, $context = 'view' ) { - /* - * @param WP_REST_Request $request Current request object. For backward compatibility, we pass this argument silently. - * - * TODO: Refactor to fix this behavior when DI gets included to make it obvious and clean. - */ - $request = func_num_args() >= 2 ? func_get_arg( 2 ) : new WP_REST_Request( '', '', array( 'context' => $context ) ); - $fields = $this->get_fields_for_response( $request ); - - $base_data = array(); - foreach ( $fields as $field ) { - switch ( $field ) { - case 'id': - $base_data['id'] = $product->get_id(); - break; - case 'name': - $base_data['name'] = $product->get_name( $context ); - break; - case 'slug': - $base_data['slug'] = $product->get_slug( $context ); - break; - case 'permalink': - $base_data['permalink'] = $product->get_permalink(); - break; - case 'date_created': - $base_data['date_created'] = wc_rest_prepare_date_response( $product->get_date_created( $context ), false ); - break; - case 'date_created_gmt': - $base_data['date_created_gmt'] = wc_rest_prepare_date_response( $product->get_date_created( $context ) ); - break; - case 'date_modified': - $base_data['date_modified'] = wc_rest_prepare_date_response( $product->get_date_modified( $context ), false ); - break; - case 'date_modified_gmt': - $base_data['date_modified_gmt'] = wc_rest_prepare_date_response( $product->get_date_modified( $context ) ); - break; - case 'type': - $base_data['type'] = $product->get_type(); - break; - case 'status': - $base_data['status'] = $product->get_status( $context ); - break; - case 'featured': - $base_data['featured'] = $product->is_featured(); - break; - case 'catalog_visibility': - $base_data['catalog_visibility'] = $product->get_catalog_visibility( $context ); - break; - case 'description': - $base_data['description'] = 'view' === $context ? wpautop( do_shortcode( $product->get_description() ) ) : $product->get_description( $context ); - break; - case 'short_description': - $base_data['short_description'] = 'view' === $context ? apply_filters( 'woocommerce_short_description', $product->get_short_description() ) : $product->get_short_description( $context ); - break; - case 'sku': - $base_data['sku'] = $product->get_sku( $context ); - break; - case 'price': - $base_data['price'] = $product->get_price( $context ); - break; - case 'regular_price': - $base_data['regular_price'] = $product->get_regular_price( $context ); - break; - case 'sale_price': - $base_data['sale_price'] = $product->get_sale_price( $context ) ? $product->get_sale_price( $context ) : ''; - break; - case 'date_on_sale_from': - $base_data['date_on_sale_from'] = wc_rest_prepare_date_response( $product->get_date_on_sale_from( $context ), false ); - break; - case 'date_on_sale_from_gmt': - $base_data['date_on_sale_from_gmt'] = wc_rest_prepare_date_response( $product->get_date_on_sale_from( $context ) ); - break; - case 'date_on_sale_to': - $base_data['date_on_sale_to'] = wc_rest_prepare_date_response( $product->get_date_on_sale_to( $context ), false ); - break; - case 'date_on_sale_to_gmt': - $base_data['date_on_sale_to_gmt'] = wc_rest_prepare_date_response( $product->get_date_on_sale_to( $context ) ); - break; - case 'on_sale': - $base_data['on_sale'] = $product->is_on_sale( $context ); - break; - case 'purchasable': - $base_data['purchasable'] = $product->is_purchasable(); - break; - case 'total_sales': - $base_data['total_sales'] = $product->get_total_sales( $context ); - break; - case 'virtual': - $base_data['virtual'] = $product->is_virtual(); - break; - case 'downloadable': - $base_data['downloadable'] = $product->is_downloadable(); - break; - case 'downloads': - $base_data['downloads'] = $this->get_downloads( $product ); - break; - case 'download_limit': - $base_data['download_limit'] = $product->get_download_limit( $context ); - break; - case 'download_expiry': - $base_data['download_expiry'] = $product->get_download_expiry( $context ); - break; - case 'external_url': - $base_data['external_url'] = $product->is_type( 'external' ) ? $product->get_product_url( $context ) : ''; - break; - case 'button_text': - $base_data['button_text'] = $product->is_type( 'external' ) ? $product->get_button_text( $context ) : ''; - break; - case 'tax_status': - $base_data['tax_status'] = $product->get_tax_status( $context ); - break; - case 'tax_class': - $base_data['tax_class'] = $product->get_tax_class( $context ); - break; - case 'manage_stock': - $base_data['manage_stock'] = $product->managing_stock(); - break; - case 'stock_quantity': - $base_data['stock_quantity'] = $product->get_stock_quantity( $context ); - break; - case 'in_stock': - $base_data['in_stock'] = $product->is_in_stock(); - break; - case 'backorders': - $base_data['backorders'] = $product->get_backorders( $context ); - break; - case 'backorders_allowed': - $base_data['backorders_allowed'] = $product->backorders_allowed(); - break; - case 'backordered': - $base_data['backordered'] = $product->is_on_backorder(); - break; - case 'sold_individually': - $base_data['sold_individually'] = $product->is_sold_individually(); - break; - case 'weight': - $base_data['weight'] = $product->get_weight( $context ); - break; - case 'dimensions': - $base_data['dimensions'] = array( - 'length' => $product->get_length( $context ), - 'width' => $product->get_width( $context ), - 'height' => $product->get_height( $context ), - ); - break; - case 'shipping_required': - $base_data['shipping_required'] = $product->needs_shipping(); - break; - case 'shipping_taxable': - $base_data['shipping_taxable'] = $product->is_shipping_taxable(); - break; - case 'shipping_class': - $base_data['shipping_class'] = $product->get_shipping_class(); - break; - case 'shipping_class_id': - $base_data['shipping_class_id'] = $product->get_shipping_class_id( $context ); - break; - case 'reviews_allowed': - $base_data['reviews_allowed'] = $product->get_reviews_allowed( $context ); - break; - case 'average_rating': - $base_data['average_rating'] = 'view' === $context ? wc_format_decimal( $product->get_average_rating(), 2 ) : $product->get_average_rating( $context ); - break; - case 'rating_count': - $base_data['rating_count'] = $product->get_rating_count(); - break; - case 'upsell_ids': - $base_data['upsell_ids'] = array_map( 'absint', $product->get_upsell_ids( $context ) ); - break; - case 'cross_sell_ids': - $base_data['cross_sell_ids'] = array_map( 'absint', $product->get_cross_sell_ids( $context ) ); - break; - case 'parent_id': - $base_data['parent_id'] = $product->get_parent_id( $context ); - break; - case 'purchase_note': - $base_data['purchase_note'] = 'view' === $context ? wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ) : $product->get_purchase_note( $context ); - break; - case 'categories': - $base_data['categories'] = $this->get_taxonomy_terms( $product ); - break; - case 'tags': - $base_data['tags'] = $this->get_taxonomy_terms( $product, 'tag' ); - break; - case 'images': - $base_data['images'] = $this->get_images( $product ); - break; - case 'attributes': - $base_data['attributes'] = $this->get_attributes( $product ); - break; - case 'default_attributes': - $base_data['default_attributes'] = $this->get_default_attributes( $product ); - break; - case 'variations': - $base_data['variations'] = array(); - break; - case 'grouped_products': - $base_data['grouped_products'] = array(); - break; - case 'menu_order': - $base_data['menu_order'] = $product->get_menu_order( $context ); - break; - } - } - - $data = array_merge( - $base_data, - $this->fetch_fields_using_getters( $product, $context, $fields ) - ); - - return $data; - } - - /** - * Prepare links for the request. - * - * @param WC_Data $object Object data. - * @param WP_REST_Request $request Request object. - * - * @return array Links for the given post. - */ - protected function prepare_links( $object, $request ) { - $links = array( - 'self' => array( - 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), // @codingStandardsIgnoreLine. - ), - 'collection' => array( - 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), // @codingStandardsIgnoreLine. - ), - ); - - if ( $object->get_parent_id() ) { - $links['up'] = array( - 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $object->get_parent_id() ) ), // @codingStandardsIgnoreLine. - ); - } - - return $links; - } - - /** - * Prepare a single product for create or update. - * - * @param WP_REST_Request $request Request object. - * @param bool $creating If is creating a new object. - * - * @return WP_Error|WC_Data - */ - protected function prepare_object_for_database( $request, $creating = false ) { - $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; - - // Type is the most important part here because we need to be using the correct class and methods. - if ( isset( $request['type'] ) ) { - $classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] ); - - if ( ! class_exists( $classname ) ) { - $classname = 'WC_Product_Simple'; - } - - $product = new $classname( $id ); - } elseif ( isset( $request['id'] ) ) { - $product = wc_get_product( $id ); - } else { - $product = new WC_Product_Simple(); - } - - if ( 'variation' === $product->get_type() ) { - return new WP_Error( - "woocommerce_rest_invalid_{$this->post_type}_id", - __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), - array( - 'status' => 404, - ) - ); - } - - // Post title. - if ( isset( $request['name'] ) ) { - $product->set_name( wp_filter_post_kses( $request['name'] ) ); - } - - // Post content. - if ( isset( $request['description'] ) ) { - $product->set_description( wp_filter_post_kses( $request['description'] ) ); - } - - // Post excerpt. - if ( isset( $request['short_description'] ) ) { - $product->set_short_description( wp_filter_post_kses( $request['short_description'] ) ); - } - - // Post status. - if ( isset( $request['status'] ) ) { - $product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); - } - - // Post slug. - if ( isset( $request['slug'] ) ) { - $product->set_slug( $request['slug'] ); - } - - // Menu order. - if ( isset( $request['menu_order'] ) ) { - $product->set_menu_order( $request['menu_order'] ); - } - - // Comment status. - if ( isset( $request['reviews_allowed'] ) ) { - $product->set_reviews_allowed( $request['reviews_allowed'] ); - } - - // Virtual. - if ( isset( $request['virtual'] ) ) { - $product->set_virtual( $request['virtual'] ); - } - - // Tax status. - if ( isset( $request['tax_status'] ) ) { - $product->set_tax_status( $request['tax_status'] ); - } - - // Tax Class. - if ( isset( $request['tax_class'] ) ) { - $product->set_tax_class( $request['tax_class'] ); - } - - // Catalog Visibility. - if ( isset( $request['catalog_visibility'] ) ) { - $product->set_catalog_visibility( $request['catalog_visibility'] ); - } - - // Purchase Note. - if ( isset( $request['purchase_note'] ) ) { - $product->set_purchase_note( wp_kses_post( wp_unslash( $request['purchase_note'] ) ) ); - } - - // Featured Product. - if ( isset( $request['featured'] ) ) { - $product->set_featured( $request['featured'] ); - } - - // Shipping data. - $product = $this->save_product_shipping_data( $product, $request ); - - // SKU. - if ( isset( $request['sku'] ) ) { - $product->set_sku( wc_clean( $request['sku'] ) ); - } - - // Attributes. - if ( isset( $request['attributes'] ) ) { - $attributes = array(); - - foreach ( $request['attributes'] as $attribute ) { - $attribute_id = 0; - $attribute_name = ''; - - // Check ID for global attributes or name for product attributes. - if ( ! empty( $attribute['id'] ) ) { - $attribute_id = absint( $attribute['id'] ); - $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); - } elseif ( ! empty( $attribute['name'] ) ) { - $attribute_name = wc_clean( $attribute['name'] ); - } - - if ( ! $attribute_id && ! $attribute_name ) { - continue; - } - - if ( $attribute_id ) { - - if ( isset( $attribute['options'] ) ) { - $options = $attribute['options']; - - if ( ! is_array( $attribute['options'] ) ) { - // Text based attributes - Posted values are term names. - $options = explode( WC_DELIMITER, $options ); - } - - $values = array_map( 'wc_sanitize_term_text_based', $options ); - $values = array_filter( $values, 'strlen' ); - } else { - $values = array(); - } - - if ( ! empty( $values ) ) { - // Add attribute to array, but don't set values. - $attribute_object = new WC_Product_Attribute(); - $attribute_object->set_id( $attribute_id ); - $attribute_object->set_name( $attribute_name ); - $attribute_object->set_options( $values ); - $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); - $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); - $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); - $attributes[] = $attribute_object; - } - } elseif ( isset( $attribute['options'] ) ) { - // Custom attribute - Add attribute to array and set the values. - if ( is_array( $attribute['options'] ) ) { - $values = $attribute['options']; - } else { - $values = explode( WC_DELIMITER, $attribute['options'] ); - } - $attribute_object = new WC_Product_Attribute(); - $attribute_object->set_name( $attribute_name ); - $attribute_object->set_options( $values ); - $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); - $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); - $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); - $attributes[] = $attribute_object; - } - } - $product->set_attributes( $attributes ); - } - - // Sales and prices. - if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) { - $product->set_regular_price( '' ); - $product->set_sale_price( '' ); - $product->set_date_on_sale_to( '' ); - $product->set_date_on_sale_from( '' ); - $product->set_price( '' ); - } else { - // Regular Price. - if ( isset( $request['regular_price'] ) ) { - $product->set_regular_price( $request['regular_price'] ); - } - - // Sale Price. - if ( isset( $request['sale_price'] ) ) { - $product->set_sale_price( $request['sale_price'] ); - } - - if ( isset( $request['date_on_sale_from'] ) ) { - $product->set_date_on_sale_from( $request['date_on_sale_from'] ); - } - - if ( isset( $request['date_on_sale_from_gmt'] ) ) { - $product->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null ); - } - - if ( isset( $request['date_on_sale_to'] ) ) { - $product->set_date_on_sale_to( $request['date_on_sale_to'] ); - } - - if ( isset( $request['date_on_sale_to_gmt'] ) ) { - $product->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null ); - } - } - - // Product parent ID. - if ( isset( $request['parent_id'] ) ) { - $product->set_parent_id( $request['parent_id'] ); - } - - // Sold individually. - if ( isset( $request['sold_individually'] ) ) { - $product->set_sold_individually( $request['sold_individually'] ); - } - - // Stock status. - if ( isset( $request['in_stock'] ) ) { - $stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock'; - } else { - $stock_status = $product->get_stock_status(); - } - - // Stock data. - if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { - // Manage stock. - if ( isset( $request['manage_stock'] ) ) { - $product->set_manage_stock( $request['manage_stock'] ); - } - - // Backorders. - if ( isset( $request['backorders'] ) ) { - $product->set_backorders( $request['backorders'] ); - } - - if ( $product->is_type( 'grouped' ) ) { - $product->set_manage_stock( 'no' ); - $product->set_backorders( 'no' ); - $product->set_stock_quantity( '' ); - $product->set_stock_status( $stock_status ); - } elseif ( $product->is_type( 'external' ) ) { - $product->set_manage_stock( 'no' ); - $product->set_backorders( 'no' ); - $product->set_stock_quantity( '' ); - $product->set_stock_status( 'instock' ); - } elseif ( $product->get_manage_stock() ) { - // Stock status is always determined by children so sync later. - if ( ! $product->is_type( 'variable' ) ) { - $product->set_stock_status( $stock_status ); - } - - // Stock quantity. - if ( isset( $request['stock_quantity'] ) ) { - $product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) ); - } elseif ( isset( $request['inventory_delta'] ) ) { - $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); - $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); - $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); - } - } else { - // Don't manage stock. - $product->set_manage_stock( 'no' ); - $product->set_stock_quantity( '' ); - $product->set_stock_status( $stock_status ); - } - } elseif ( ! $product->is_type( 'variable' ) ) { - $product->set_stock_status( $stock_status ); - } - - // Upsells. - if ( isset( $request['upsell_ids'] ) ) { - $upsells = array(); - $ids = $request['upsell_ids']; - - if ( ! empty( $ids ) ) { - foreach ( $ids as $id ) { - if ( $id && $id > 0 ) { - $upsells[] = $id; - } - } - } - - $product->set_upsell_ids( $upsells ); - } - - // Cross sells. - if ( isset( $request['cross_sell_ids'] ) ) { - $crosssells = array(); - $ids = $request['cross_sell_ids']; - - if ( ! empty( $ids ) ) { - foreach ( $ids as $id ) { - if ( $id && $id > 0 ) { - $crosssells[] = $id; - } - } - } - - $product->set_cross_sell_ids( $crosssells ); - } - - // Product categories. - if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { - $product = $this->save_taxonomy_terms( $product, $request['categories'] ); - } - - // Product tags. - if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { - $product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' ); - } - - // Downloadable. - if ( isset( $request['downloadable'] ) ) { - $product->set_downloadable( $request['downloadable'] ); - } - - // Downloadable options. - if ( $product->get_downloadable() ) { - - // Downloadable files. - if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { - $product = $this->save_downloadable_files( $product, $request['downloads'] ); - } - - // Download limit. - if ( isset( $request['download_limit'] ) ) { - $product->set_download_limit( $request['download_limit'] ); - } - - // Download expiry. - if ( isset( $request['download_expiry'] ) ) { - $product->set_download_expiry( $request['download_expiry'] ); - } - } - - // Product url and button text for external products. - if ( $product->is_type( 'external' ) ) { - if ( isset( $request['external_url'] ) ) { - $product->set_product_url( $request['external_url'] ); - } - - if ( isset( $request['button_text'] ) ) { - $product->set_button_text( $request['button_text'] ); - } - } - - // Save default attributes for variable products. - if ( $product->is_type( 'variable' ) ) { - $product = $this->save_default_attributes( $product, $request ); - } - - // Set children for a grouped product. - if ( $product->is_type( 'grouped' ) && isset( $request['grouped_products'] ) ) { - $product->set_children( $request['grouped_products'] ); - } - - // Check for featured/gallery images, upload it and set it. - if ( isset( $request['images'] ) ) { - $product = $this->set_product_images( $product, $request['images'] ); - } - - // Allow set meta_data. - if ( is_array( $request['meta_data'] ) ) { - foreach ( $request['meta_data'] as $meta ) { - $product->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); - } - } - - /** - * Filters an object before it is inserted via the REST API. - * - * The dynamic portion of the hook name, `$this->post_type`, - * refers to the object type slug. - * - * @param WC_Data $product Object object. - * @param WP_REST_Request $request Request object. - * @param bool $creating If is creating a new object. - */ - return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $product, $request, $creating ); - } - - /** - * Set product images. - * - * @param WC_Product $product Product instance. - * @param array $images Images data. - * - * @throws WC_REST_Exception REST API exceptions. - * @return WC_Product - */ - protected function set_product_images( $product, $images ) { - $images = is_array( $images ) ? array_filter( $images ) : array(); - - if ( ! empty( $images ) ) { - $gallery_positions = array(); - - foreach ( $images as $index => $image ) { - $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; - - if ( 0 === $attachment_id && isset( $image['src'] ) ) { - $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); - - if ( is_wp_error( $upload ) ) { - if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { - throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); - } else { - continue; - } - } - - $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); - } - - if ( ! wp_attachment_is_image( $attachment_id ) ) { - /* translators: %s: attachment id */ - throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); - } - - $gallery_positions[ $attachment_id ] = absint( isset( $image['position'] ) ? $image['position'] : $index ); - - // Set the image alt if present. - if ( ! empty( $image['alt'] ) ) { - update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); - } - - // Set the image name if present. - if ( ! empty( $image['name'] ) ) { - wp_update_post( - array( - 'ID' => $attachment_id, - 'post_title' => $image['name'], - ) - ); - } - - // Set the image source if present, for future reference. - if ( ! empty( $image['src'] ) ) { - update_post_meta( $attachment_id, '_wc_attachment_source', esc_url_raw( $image['src'] ) ); - } - } - - // Sort images and get IDs in correct order. - asort( $gallery_positions ); - - // Get gallery in correct order. - $gallery = array_keys( $gallery_positions ); - - // Featured image is in position 0. - $image_id = array_shift( $gallery ); - - // Set images. - $product->set_image_id( $image_id ); - $product->set_gallery_image_ids( $gallery ); - } else { - $product->set_image_id( '' ); - $product->set_gallery_image_ids( array() ); - } - - return $product; - } - - /** - * Save product shipping data. - * - * @param WC_Product $product Product instance. - * @param array $data Shipping data. - * - * @return WC_Product - */ - protected function save_product_shipping_data( $product, $data ) { - // Virtual. - if ( isset( $data['virtual'] ) && true === $data['virtual'] ) { - $product->set_weight( '' ); - $product->set_height( '' ); - $product->set_length( '' ); - $product->set_width( '' ); - } else { - if ( isset( $data['weight'] ) ) { - $product->set_weight( $data['weight'] ); - } - - // Height. - if ( isset( $data['dimensions']['height'] ) ) { - $product->set_height( $data['dimensions']['height'] ); - } - - // Width. - if ( isset( $data['dimensions']['width'] ) ) { - $product->set_width( $data['dimensions']['width'] ); - } - - // Length. - if ( isset( $data['dimensions']['length'] ) ) { - $product->set_length( $data['dimensions']['length'] ); - } - } - - // Shipping class. - if ( isset( $data['shipping_class'] ) ) { - $data_store = $product->get_data_store(); - $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); - $product->set_shipping_class_id( $shipping_class_id ); - } - - return $product; - } - - /** - * Save downloadable files. - * - * @param WC_Product $product Product instance. - * @param array $downloads Downloads data. - * @param int $deprecated Deprecated since 3.0. - * - * @return WC_Product - */ - protected function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { - if ( $deprecated ) { - wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() not requires a variation_id anymore.' ); - } - - $files = array(); - foreach ( $downloads as $key => $file ) { - if ( empty( $file['file'] ) ) { - continue; - } - - $download = new WC_Product_Download(); - $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); - $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); - $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); - $files[] = $download; - } - $product->set_downloads( $files ); - - return $product; - } - - /** - * Save taxonomy terms. - * - * @param WC_Product $product Product instance. - * @param array $terms Terms data. - * @param string $taxonomy Taxonomy name. - * - * @return WC_Product - */ - protected function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) { - $term_ids = wp_list_pluck( $terms, 'id' ); - - if ( 'cat' === $taxonomy ) { - $product->set_category_ids( $term_ids ); - } elseif ( 'tag' === $taxonomy ) { - $product->set_tag_ids( $term_ids ); - } - - return $product; - } - - /** - * Save default attributes. - * - * @param WC_Product $product Product instance. - * @param WP_REST_Request $request Request data. - * - * @since 3.0.0 - * @return WC_Product - */ - protected function save_default_attributes( $product, $request ) { - if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { - - $attributes = $product->get_attributes(); - $default_attributes = array(); - - foreach ( $request['default_attributes'] as $attribute ) { - $attribute_id = 0; - $attribute_name = ''; - - // Check ID for global attributes or name for product attributes. - if ( ! empty( $attribute['id'] ) ) { - $attribute_id = absint( $attribute['id'] ); - $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); - } elseif ( ! empty( $attribute['name'] ) ) { - $attribute_name = sanitize_title( $attribute['name'] ); - } - - if ( ! $attribute_id && ! $attribute_name ) { - continue; - } - - if ( isset( $attributes[ $attribute_name ] ) ) { - $_attribute = $attributes[ $attribute_name ]; - - if ( $_attribute['is_variation'] ) { - $value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; - - if ( ! empty( $_attribute['is_taxonomy'] ) ) { - // If dealing with a taxonomy, we need to get the slug from the name posted to the API. - $term = get_term_by( 'name', $value, $attribute_name ); - - if ( $term && ! is_wp_error( $term ) ) { - $value = $term->slug; - } else { - $value = sanitize_title( $value ); - } - } - - if ( $value ) { - $default_attributes[ $attribute_name ] = $value; - } - } - } - } - - $product->set_default_attributes( $default_attributes ); - } - - return $product; - } - - /** - * Clear caches here so in sync with any new variations/children. - * - * @param WC_Data $object Object data. - */ - public function clear_transients( $object ) { - wc_delete_product_transients( $object->get_id() ); - wp_cache_delete( 'product-' . $object->get_id(), 'products' ); - } - - /** - * Delete a single item. - * - * @param WP_REST_Request $request Full details about the request. - * - * @return WP_REST_Response|WP_Error - */ - public function delete_item( $request ) { - $id = (int) $request['id']; - $force = (bool) $request['force']; - $object = $this->get_object( (int) $request['id'] ); - $result = false; - - if ( ! $object || 0 === $object->get_id() ) { - return new WP_Error( - "woocommerce_rest_{$this->post_type}_invalid_id", - __( 'Invalid ID.', 'woocommerce' ), - array( - 'status' => 404, - ) - ); - } - - if ( 'variation' === $object->get_type() ) { - return new WP_Error( - "woocommerce_rest_invalid_{$this->post_type}_id", - __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), - array( - 'status' => 404, - ) - ); - } - - $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) ); - - /** - * Filter whether an object is trashable. - * - * Return false to disable trash support for the object. - * - * @param boolean $supports_trash Whether the object type support trashing. - * @param WC_Data $object The object being considered for trashing support. - */ - $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object ); - - if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { - return new WP_Error( - "woocommerce_rest_user_cannot_delete_{$this->post_type}", - /* translators: %s: post type */ - sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), - array( - 'status' => rest_authorization_required_code(), - ) - ); - } - - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_object_for_response( $object, $request ); - - // If we're forcing, then delete permanently. - if ( $force ) { - if ( $object->is_type( 'variable' ) ) { - foreach ( $object->get_children() as $child_id ) { - $child = wc_get_product( $child_id ); - if ( ! empty( $child ) ) { - $child->delete( true ); - } - } - } else { - // For other product types, if the product has children, remove the relationship. - foreach ( $object->get_children() as $child_id ) { - $child = wc_get_product( $child_id ); - if ( ! empty( $child ) ) { - $child->set_parent_id( 0 ); - $child->save(); - } - } - } - - $object->delete( true ); - $result = 0 === $object->get_id(); - } else { - // If we don't support trashing for this type, error out. - if ( ! $supports_trash ) { - return new WP_Error( - 'woocommerce_rest_trash_not_supported', - /* translators: %s: post type */ - sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), - array( - 'status' => 501, - ) - ); - } - - // Otherwise, only trash if we haven't already. - if ( is_callable( array( $object, 'get_status' ) ) ) { - if ( 'trash' === $object->get_status() ) { - return new WP_Error( - 'woocommerce_rest_already_trashed', - /* translators: %s: post type */ - sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), - array( - 'status' => 410, - ) - ); - } - - $object->delete(); - $result = 'trash' === $object->get_status(); - } - } - - if ( ! $result ) { - return new WP_Error( - 'woocommerce_rest_cannot_delete', - /* translators: %s: post type */ - sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), - array( - 'status' => 500, - ) - ); - } - - // Delete parent product transients. - if ( 0 !== $object->get_parent_id() ) { - wc_delete_product_transients( $object->get_parent_id() ); - } - - /** - * Fires after a single object is deleted or trashed via the REST API. - * - * @param WC_Data $object The deleted or trashed object. - * @param WP_REST_Response $response The response data. - * @param WP_REST_Request $request The request sent to the API. - */ - do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request ); - - return $response; - } - - /** - * Get the Product's schema, conforming to JSON Schema. - * - * @return array - */ - public function get_item_schema() { - $weight_unit = get_option( 'woocommerce_weight_unit' ); - $dimension_unit = get_option( 'woocommerce_dimension_unit' ); - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => $this->post_type, - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'name' => array( - 'description' => __( 'Product name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'slug' => array( - 'description' => __( 'Product slug.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'permalink' => array( - 'description' => __( 'Product URL.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'uri', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_created' => array( - 'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_created_gmt' => array( - 'description' => __( 'The date the product was created, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_modified' => array( - 'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_modified_gmt' => array( - 'description' => __( 'The date the product was last modified, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'type' => array( - 'description' => __( 'Product type.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'simple', - 'enum' => array_keys( wc_get_product_types() ), - 'context' => array( 'view', 'edit' ), - ), - 'status' => array( - 'description' => __( 'Product status (post status).', 'woocommerce' ), - 'type' => 'string', - 'default' => 'publish', - 'enum' => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ), - 'context' => array( 'view', 'edit' ), - ), - 'featured' => array( - 'description' => __( 'Featured product.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'catalog_visibility' => array( - 'description' => __( 'Catalog visibility.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'visible', - 'enum' => array( 'visible', 'catalog', 'search', 'hidden' ), - 'context' => array( 'view', 'edit' ), - ), - 'description' => array( - 'description' => __( 'Product description.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'short_description' => array( - 'description' => __( 'Product short description.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'sku' => array( - 'description' => __( 'Unique identifier.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'price' => array( - 'description' => __( 'Current product price.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'regular_price' => array( - 'description' => __( 'Product regular price.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'sale_price' => array( - 'description' => __( 'Product sale price.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'date_on_sale_from' => array( - 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'date_on_sale_from_gmt' => array( - 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'date_on_sale_to' => array( - 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'date_on_sale_to_gmt' => array( - 'description' => __( 'End date of sale price, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'price_html' => array( - 'description' => __( 'Price formatted in HTML.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'on_sale' => array( - 'description' => __( 'Shows if the product is on sale.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'purchasable' => array( - 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'total_sales' => array( - 'description' => __( 'Amount of sales.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'virtual' => array( - 'description' => __( 'If the product is virtual.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'downloadable' => array( - 'description' => __( 'If the product is downloadable.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'downloads' => array( - 'description' => __( 'List of downloadable files.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'File ID.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'File name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'file' => array( - 'description' => __( 'File URL.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - 'download_limit' => array( - 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), - 'type' => 'integer', - 'default' => -1, - 'context' => array( 'view', 'edit' ), - ), - 'download_expiry' => array( - 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), - 'type' => 'integer', - 'default' => -1, - 'context' => array( 'view', 'edit' ), - ), - 'external_url' => array( - 'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'uri', - 'context' => array( 'view', 'edit' ), - ), - 'button_text' => array( - 'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'tax_status' => array( - 'description' => __( 'Tax status.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'taxable', - 'enum' => array( 'taxable', 'shipping', 'none' ), - 'context' => array( 'view', 'edit' ), - ), - 'tax_class' => array( - 'description' => __( 'Tax class.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'manage_stock' => array( - 'description' => __( 'Stock management at product level.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'stock_quantity' => array( - 'description' => __( 'Stock quantity.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'in_stock' => array( - 'description' => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => true, - 'context' => array( 'view', 'edit' ), - ), - 'backorders' => array( - 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'no', - 'enum' => array( 'no', 'notify', 'yes' ), - 'context' => array( 'view', 'edit' ), - ), - 'backorders_allowed' => array( - 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'backordered' => array( - 'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'sold_individually' => array( - 'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'weight' => array( - /* translators: %s: weight unit */ - 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'dimensions' => array( - 'description' => __( 'Product dimensions.', 'woocommerce' ), - 'type' => 'object', - 'context' => array( 'view', 'edit' ), - 'properties' => array( - 'length' => array( - /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'width' => array( - /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'height' => array( - /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - 'shipping_required' => array( - 'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'shipping_taxable' => array( - 'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'shipping_class' => array( - 'description' => __( 'Shipping class slug.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'shipping_class_id' => array( - 'description' => __( 'Shipping class ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'reviews_allowed' => array( - 'description' => __( 'Allow reviews.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => true, - 'context' => array( 'view', 'edit' ), - ), - 'average_rating' => array( - 'description' => __( 'Reviews average rating.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'rating_count' => array( - 'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'related_ids' => array( - 'description' => __( 'List of related products IDs.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'upsell_ids' => array( - 'description' => __( 'List of up-sell products IDs.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'context' => array( 'view', 'edit' ), - ), - 'cross_sell_ids' => array( - 'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'context' => array( 'view', 'edit' ), - ), - 'parent_id' => array( - 'description' => __( 'Product parent ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'purchase_note' => array( - 'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'categories' => array( - 'description' => __( 'List of categories.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Category ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'Category name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'slug' => array( - 'description' => __( 'Category slug.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ), - ), - 'tags' => array( - 'description' => __( 'List of tags.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Tag ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'Tag name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'slug' => array( - 'description' => __( 'Tag slug.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ), - ), - 'images' => array( - 'description' => __( 'List of images.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Image ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'date_created' => array( - 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_created_gmt' => array( - 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_modified' => array( - 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_modified_gmt' => array( - 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'src' => array( - 'description' => __( 'Image URL.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'uri', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'Image name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'alt' => array( - 'description' => __( 'Image alternative text.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'position' => array( - 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - 'attributes' => array( - 'description' => __( 'List of attributes.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Attribute ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'Attribute name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'position' => array( - 'description' => __( 'Attribute position.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'visible' => array( - 'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'variation' => array( - 'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'options' => array( - 'description' => __( 'List of available term names of the attribute.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'string', - ), - ), - ), - ), - ), - 'default_attributes' => array( - 'description' => __( 'Defaults variation attributes.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Attribute ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'Attribute name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'option' => array( - 'description' => __( 'Selected attribute term name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - 'variations' => array( - 'description' => __( 'List of variations IDs.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'integer', - ), - 'readonly' => true, - ), - 'grouped_products' => array( - 'description' => __( 'List of grouped products ID.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'context' => array( 'view', 'edit' ), - ), - 'menu_order' => array( - 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'meta_data' => array( - 'description' => __( 'Meta data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Meta ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'key' => array( - 'description' => __( 'Meta key.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'value' => array( - 'description' => __( 'Meta value.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - ), - ); - - return $this->add_additional_fields_schema( $schema ); - } - - /** - * Get the query params for collections of attachments. - * - * @return array - */ - public function get_collection_params() { - $params = parent::get_collection_params(); - - $params['orderby']['enum'] = array_merge( $params['orderby']['enum'], array( 'menu_order' ) ); - - $params['slug'] = array( - 'description' => __( 'Limit result set to products with a specific slug.', 'woocommerce' ), - 'type' => 'string', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['status'] = array( - 'default' => 'any', - 'description' => __( 'Limit result set to products assigned a specific status.', 'woocommerce' ), - 'type' => 'string', - 'enum' => array_merge( array( 'any', 'future', 'trash' ), array_keys( get_post_statuses() ) ), - 'sanitize_callback' => 'sanitize_key', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['type'] = array( - 'description' => __( 'Limit result set to products assigned a specific type.', 'woocommerce' ), - 'type' => 'string', - 'enum' => array_keys( wc_get_product_types() ), - 'sanitize_callback' => 'sanitize_key', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['sku'] = array( - 'description' => __( 'Limit result set to products with specific SKU(s). Use commas to separate.', 'woocommerce' ), - 'type' => 'string', - 'sanitize_callback' => 'sanitize_text_field', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['featured'] = array( - 'description' => __( 'Limit result set to featured products.', 'woocommerce' ), - 'type' => 'boolean', - 'sanitize_callback' => 'wc_string_to_bool', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['category'] = array( - 'description' => __( 'Limit result set to products assigned a specific category ID.', 'woocommerce' ), - 'type' => 'string', - 'sanitize_callback' => 'wp_parse_id_list', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['tag'] = array( - 'description' => __( 'Limit result set to products assigned a specific tag ID.', 'woocommerce' ), - 'type' => 'string', - 'sanitize_callback' => 'wp_parse_id_list', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['shipping_class'] = array( - 'description' => __( 'Limit result set to products assigned a specific shipping class ID.', 'woocommerce' ), - 'type' => 'string', - 'sanitize_callback' => 'wp_parse_id_list', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['attribute'] = array( - 'description' => __( 'Limit result set to products with a specific attribute. Use the taxonomy name/attribute slug.', 'woocommerce' ), - 'type' => 'string', - 'sanitize_callback' => 'sanitize_text_field', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['attribute_term'] = array( - 'description' => __( 'Limit result set to products with a specific attribute term ID (required an assigned attribute).', 'woocommerce' ), - 'type' => 'string', - 'sanitize_callback' => 'wp_parse_id_list', - 'validate_callback' => 'rest_validate_request_arg', - ); - - if ( wc_tax_enabled() ) { - $params['tax_class'] = array( - 'description' => __( 'Limit result set to products with a specific tax class.', 'woocommerce' ), - 'type' => 'string', - 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), - 'sanitize_callback' => 'sanitize_text_field', - 'validate_callback' => 'rest_validate_request_arg', - ); - } - - $params['in_stock'] = array( - 'description' => __( 'Limit result set to products in stock or out of stock.', 'woocommerce' ), - 'type' => 'boolean', - 'sanitize_callback' => 'wc_string_to_bool', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['on_sale'] = array( - 'description' => __( 'Limit result set to products on sale.', 'woocommerce' ), - 'type' => 'boolean', - 'sanitize_callback' => 'wc_string_to_bool', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['min_price'] = array( - 'description' => __( 'Limit result set to products based on a minimum price.', 'woocommerce' ), - 'type' => 'string', - 'sanitize_callback' => 'sanitize_text_field', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['max_price'] = array( - 'description' => __( 'Limit result set to products based on a maximum price.', 'woocommerce' ), - 'type' => 'string', - 'sanitize_callback' => 'sanitize_text_field', - 'validate_callback' => 'rest_validate_request_arg', - ); - - return $params; - } -} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php deleted file mode 100644 index 06838eb5469..00000000000 --- a/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php +++ /dev/null @@ -1,618 +0,0 @@ -namespace, - '/' . $this->rest_base, - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_items' ), - 'permission_callback' => array( $this, 'get_items_permissions_check' ), - 'args' => $this->get_collection_params(), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/(?P[\w-]+)', - array( - 'args' => array( - 'id' => array( - 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), - 'type' => 'string', - ), - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - ), - 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 ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - } - - /** - * Check whether a given request has permission to view system status tools. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_items_permissions_check( $request ) { - if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - return true; - } - - /** - * Check whether a given request has permission to view a specific system status tool. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_item_permissions_check( $request ) { - if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - return true; - } - - /** - * Check whether a given request has permission to execute a specific system status tool. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function update_item_permissions_check( $request ) { - if ( ! wc_rest_check_manager_permissions( 'system_status', 'edit' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - return true; - } - - /** - * A list of available tools for use in the system status section. - * 'button' becomes 'action' in the API. - * - * @return array - */ - public function get_tools() { - $tools = array( - 'clear_transients' => array( - 'name' => __( 'WooCommerce transients', 'woocommerce' ), - 'button' => __( 'Clear transients', 'woocommerce' ), - 'desc' => __( 'This tool will clear the product/shop transients cache.', 'woocommerce' ), - ), - 'clear_expired_transients' => array( - 'name' => __( 'Expired transients', 'woocommerce' ), - 'button' => __( 'Clear transients', 'woocommerce' ), - 'desc' => __( 'This tool will clear ALL expired transients from WordPress.', 'woocommerce' ), - ), - 'delete_orphaned_variations' => array( - 'name' => __( 'Orphaned variations', 'woocommerce' ), - 'button' => __( 'Delete orphaned variations', 'woocommerce' ), - 'desc' => __( 'This tool will delete all variations which have no parent.', 'woocommerce' ), - ), - 'clear_expired_download_permissions' => array( - 'name' => __( 'Used-up download permissions', 'woocommerce' ), - 'button' => __( 'Clean up download permissions', 'woocommerce' ), - 'desc' => __( 'This tool will delete expired download permissions and permissions with 0 remaining downloads.', 'woocommerce' ), - ), - 'regenerate_product_lookup_tables' => array( - 'name' => __( 'Product lookup tables', 'woocommerce' ), - 'button' => __( 'Regenerate', 'woocommerce' ), - 'desc' => __( 'This tool will regenerate product lookup table data. This process may take a while.', 'woocommerce' ), - ), - 'recount_terms' => array( - 'name' => __( 'Term counts', 'woocommerce' ), - 'button' => __( 'Recount terms', 'woocommerce' ), - 'desc' => __( 'This tool will recount product terms - useful when changing your settings in a way which hides products from the catalog.', 'woocommerce' ), - ), - 'reset_roles' => array( - 'name' => __( 'Capabilities', 'woocommerce' ), - 'button' => __( 'Reset capabilities', 'woocommerce' ), - 'desc' => __( 'This tool will reset the admin, customer and shop_manager roles to default. Use this if your users cannot access all of the WooCommerce admin pages.', 'woocommerce' ), - ), - 'clear_sessions' => array( - 'name' => __( 'Clear customer sessions', 'woocommerce' ), - 'button' => __( 'Clear', 'woocommerce' ), - 'desc' => sprintf( - '%1$s %2$s', - __( 'Note:', 'woocommerce' ), - __( 'This tool will delete all customer session data from the database, including current carts and saved carts in the database.', 'woocommerce' ) - ), - ), - 'clear_template_cache' => array( - 'name' => __( 'Clear template cache', 'woocommerce' ), - 'button' => __( 'Clear', 'woocommerce' ), - 'desc' => sprintf( - '%1$s %2$s', - __( 'Note:', 'woocommerce' ), - __( 'This tool will empty the template cache.', 'woocommerce' ) - ), - ), - 'install_pages' => array( - 'name' => __( 'Create default WooCommerce pages', 'woocommerce' ), - 'button' => __( 'Create pages', 'woocommerce' ), - 'desc' => sprintf( - '%1$s %2$s', - __( 'Note:', 'woocommerce' ), - __( 'This tool will install all the missing WooCommerce pages. Pages already defined and set up will not be replaced.', 'woocommerce' ) - ), - ), - 'delete_taxes' => array( - 'name' => __( 'Delete WooCommerce tax rates', 'woocommerce' ), - 'button' => __( 'Delete tax rates', 'woocommerce' ), - 'desc' => sprintf( - '%1$s %2$s', - __( 'Note:', 'woocommerce' ), - __( 'This option will delete ALL of your tax rates, use with caution. This action cannot be reversed.', 'woocommerce' ) - ), - ), - 'regenerate_thumbnails' => array( - 'name' => __( 'Regenerate shop thumbnails', 'woocommerce' ), - 'button' => __( 'Regenerate', 'woocommerce' ), - 'desc' => __( 'This will regenerate all shop thumbnails to match your theme and/or image settings.', 'woocommerce' ), - ), - 'db_update_routine' => array( - 'name' => __( 'Update database', 'woocommerce' ), - 'button' => __( 'Update database', 'woocommerce' ), - 'desc' => sprintf( - '%1$s %2$s', - __( 'Note:', 'woocommerce' ), - __( 'This tool will update your WooCommerce database to the latest version. Please ensure you make sufficient backups before proceeding.', 'woocommerce' ) - ), - ), - ); - if ( method_exists( 'WC_Install', 'verify_base_tables' ) ) { - $tools['verify_db_tables'] = array( - 'name' => __( 'Verify base database tables', 'woocommerce' ), - 'button' => __( 'Verify database', 'woocommerce' ), - 'desc' => sprintf( - __( 'Verify if all base database tables are present.', 'woocommerce' ) - ), - ); - } - - // Jetpack does the image resizing heavy lifting so you don't have to. - if ( ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'photon' ) ) || ! apply_filters( 'woocommerce_background_image_regeneration', true ) ) { - unset( $tools['regenerate_thumbnails'] ); - } - - if ( ! function_exists( 'wc_clear_template_cache' ) ) { - unset( $tools['clear_template_cache'] ); - } - - return apply_filters( 'woocommerce_debug_tools', $tools ); - } - - /** - * Get a list of system status tools. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function get_items( $request ) { - $tools = array(); - foreach ( $this->get_tools() as $id => $tool ) { - $tools[] = $this->prepare_response_for_collection( - $this->prepare_item_for_response( - array( - 'id' => $id, - 'name' => $tool['name'], - 'action' => $tool['button'], - 'description' => $tool['desc'], - ), - $request - ) - ); - } - - $response = rest_ensure_response( $tools ); - return $response; - } - - /** - * Return a single tool. - * - * @param WP_REST_Request $request Request data. - * @return WP_Error|WP_REST_Response - */ - public function get_item( $request ) { - $tools = $this->get_tools(); - if ( empty( $tools[ $request['id'] ] ) ) { - return new WP_Error( 'woocommerce_rest_system_status_tool_invalid_id', __( 'Invalid tool ID.', 'woocommerce' ), array( 'status' => 404 ) ); - } - $tool = $tools[ $request['id'] ]; - return rest_ensure_response( - $this->prepare_item_for_response( - array( - 'id' => $request['id'], - 'name' => $tool['name'], - 'action' => $tool['button'], - 'description' => $tool['desc'], - ), - $request - ) - ); - } - - /** - * Update (execute) a tool. - * - * @param WP_REST_Request $request Request data. - * @return WP_Error|WP_REST_Response - */ - public function update_item( $request ) { - $tools = $this->get_tools(); - if ( empty( $tools[ $request['id'] ] ) ) { - return new WP_Error( 'woocommerce_rest_system_status_tool_invalid_id', __( 'Invalid tool ID.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - $tool = $tools[ $request['id'] ]; - $tool = array( - 'id' => $request['id'], - 'name' => $tool['name'], - 'action' => $tool['button'], - 'description' => $tool['desc'], - ); - - $execute_return = $this->execute_tool( $request['id'] ); - $tool = array_merge( $tool, $execute_return ); - - /** - * Fires after a WooCommerce REST system status tool has been executed. - * - * @param array $tool Details about the tool that has been executed. - * @param WP_REST_Request $request The current WP_REST_Request object. - */ - do_action( 'woocommerce_rest_insert_system_status_tool', $tool, $request ); - - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( $tool, $request ); - return rest_ensure_response( $response ); - } - - /** - * Prepare a tool item for serialization. - * - * @param array $item Object. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response $response Response data. - */ - public function prepare_item_for_response( $item, $request ) { - $context = empty( $request['context'] ) ? 'view' : $request['context']; - $data = $this->add_additional_fields_to_object( $item, $request ); - $data = $this->filter_response_by_context( $data, $context ); - - $response = rest_ensure_response( $data ); - - $response->add_links( $this->prepare_links( $item['id'] ) ); - - return $response; - } - - /** - * Get the system status tools schema, conforming to JSON Schema. - * - * @return array - */ - public function get_item_schema() { - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'system_status_tool', - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'A unique identifier for the tool.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'arg_options' => array( - 'sanitize_callback' => 'sanitize_title', - ), - ), - 'name' => array( - 'description' => __( 'Tool name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'arg_options' => array( - 'sanitize_callback' => 'sanitize_text_field', - ), - ), - 'action' => array( - 'description' => __( 'What running the tool will do.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'arg_options' => array( - 'sanitize_callback' => 'sanitize_text_field', - ), - ), - 'description' => array( - 'description' => __( 'Tool description.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'arg_options' => array( - 'sanitize_callback' => 'sanitize_text_field', - ), - ), - 'success' => array( - 'description' => __( 'Did the tool run successfully?', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'edit' ), - ), - 'message' => array( - 'description' => __( 'Tool return message.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'edit' ), - 'arg_options' => array( - 'sanitize_callback' => 'sanitize_text_field', - ), - ), - ), - ); - - return $this->add_additional_fields_schema( $schema ); - } - - /** - * Prepare links for the request. - * - * @param string $id ID. - * @return array - */ - protected function prepare_links( $id ) { - $base = '/' . $this->namespace . '/' . $this->rest_base; - $links = array( - 'item' => array( - 'href' => rest_url( trailingslashit( $base ) . $id ), - 'embeddable' => true, - ), - ); - - return $links; - } - - /** - * Get any query params needed. - * - * @return array - */ - public function get_collection_params() { - return array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), - ); - } - - /** - * Actually executes a tool. - * - * @param string $tool Tool. - * @return array - */ - public function execute_tool( $tool ) { - global $wpdb; - $ran = true; - switch ( $tool ) { - case 'clear_transients': - wc_delete_product_transients(); - wc_delete_shop_order_transients(); - delete_transient( 'wc_count_comments' ); - delete_transient( 'as_comment_count' ); - - $attribute_taxonomies = wc_get_attribute_taxonomies(); - - if ( $attribute_taxonomies ) { - foreach ( $attribute_taxonomies as $attribute ) { - delete_transient( 'wc_layered_nav_counts_pa_' . $attribute->attribute_name ); - } - } - - WC_Cache_Helper::get_transient_version( 'shipping', true ); - $message = __( 'Product transients cleared', 'woocommerce' ); - break; - - case 'clear_expired_transients': - /* translators: %d: amount of expired transients */ - $message = sprintf( __( '%d transients rows cleared', 'woocommerce' ), wc_delete_expired_transients() ); - break; - - case 'delete_orphaned_variations': - // Delete orphans. - $result = absint( - $wpdb->query( - "DELETE products - FROM {$wpdb->posts} products - LEFT JOIN {$wpdb->posts} wp ON wp.ID = products.post_parent - WHERE wp.ID IS NULL AND products.post_type = 'product_variation';" - ) - ); - /* translators: %d: amount of orphaned variations */ - $message = sprintf( __( '%d orphaned variations deleted', 'woocommerce' ), $result ); - break; - - case 'clear_expired_download_permissions': - // Delete expired download permissions and ones with 0 downloads remaining. - $result = absint( - $wpdb->query( - $wpdb->prepare( - "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions - WHERE ( downloads_remaining != '' AND downloads_remaining = 0 ) OR ( access_expires IS NOT NULL AND access_expires < %s )", - gmdate( 'Y-m-d', current_time( 'timestamp' ) ) - ) - ) - ); - /* translators: %d: amount of permissions */ - $message = sprintf( __( '%d permissions deleted', 'woocommerce' ), $result ); - break; - - case 'regenerate_product_lookup_tables': - if ( ! wc_update_product_lookup_tables_is_running() ) { - wc_update_product_lookup_tables(); - } - $message = __( 'Lookup tables are regenerating', 'woocommerce' ); - break; - case 'reset_roles': - // Remove then re-add caps and roles. - WC_Install::remove_roles(); - WC_Install::create_roles(); - $message = __( 'Roles successfully reset', 'woocommerce' ); - break; - - case 'recount_terms': - $product_cats = get_terms( - 'product_cat', - array( - 'hide_empty' => false, - 'fields' => 'id=>parent', - ) - ); - _wc_term_recount( $product_cats, get_taxonomy( 'product_cat' ), true, false ); - $product_tags = get_terms( - 'product_tag', - array( - 'hide_empty' => false, - 'fields' => 'id=>parent', - ) - ); - _wc_term_recount( $product_tags, get_taxonomy( 'product_tag' ), true, false ); - $message = __( 'Terms successfully recounted', 'woocommerce' ); - break; - - case 'clear_sessions': - $wpdb->query( "TRUNCATE {$wpdb->prefix}woocommerce_sessions" ); - $result = absint( $wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key='_woocommerce_persistent_cart_" . get_current_blog_id() . "';" ) ); // WPCS: unprepared SQL ok. - wp_cache_flush(); - /* translators: %d: amount of sessions */ - $message = sprintf( __( 'Deleted all active sessions, and %d saved carts.', 'woocommerce' ), absint( $result ) ); - break; - - case 'install_pages': - WC_Install::create_pages(); - $message = __( 'All missing WooCommerce pages successfully installed', 'woocommerce' ); - break; - - case 'delete_taxes': - $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_tax_rates;" ); - $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_tax_rate_locations;" ); - - if ( method_exists( 'WC_Cache_Helper', 'invalidate_cache_group' ) ) { - WC_Cache_Helper::invalidate_cache_group( 'taxes' ); - } else { - WC_Cache_Helper::incr_cache_prefix( 'taxes' ); - } - $message = __( 'Tax rates successfully deleted', 'woocommerce' ); - break; - - case 'regenerate_thumbnails': - WC_Regenerate_Images::queue_image_regeneration(); - $message = __( 'Thumbnail regeneration has been scheduled to run in the background.', 'woocommerce' ); - break; - - case 'db_update_routine': - $blog_id = get_current_blog_id(); - // Used to fire an action added in WP_Background_Process::_construct() that calls WP_Background_Process::handle_cron_healthcheck(). - // This method will make sure the database updates are executed even if cron is disabled. Nothing will happen if the updates are already running. - do_action( 'wp_' . $blog_id . '_wc_updater_cron' ); - $message = __( 'Database upgrade routine has been scheduled to run in the background.', 'woocommerce' ); - break; - - case 'clear_template_cache': - if ( function_exists( 'wc_clear_template_cache' ) ) { - wc_clear_template_cache(); - $message = __( 'Template cache cleared.', 'woocommerce' ); - } else { - $message = __( 'The active version of WooCommerce does not support template cache clearing.', 'woocommerce' ); - $ran = false; - } - break; - - case 'verify_db_tables': - if ( ! method_exists( 'WC_Install', 'verify_base_tables' ) ) { - $message = __( 'You need WooCommerce 4.2 or newer to run this tool.', 'woocommerce' ); - $ran = false; - break; - } - // Try to manually create table again. - $missing_tables = WC_Install::verify_base_tables( true, true ); - if ( 0 === count( $missing_tables ) ) { - $message = __( 'Database verified successfully.', 'woocommerce' ); - } else { - $message = __( 'Verifying database... One or more tables are still missing: ', 'woocommerce' ); - $message .= implode( ', ', $missing_tables ); - $ran = false; - } - break; - - default: - $tools = $this->get_tools(); - if ( isset( $tools[ $tool ]['callback'] ) ) { - $callback = $tools[ $tool ]['callback']; - $return = call_user_func( $callback ); - if ( is_string( $return ) ) { - $message = $return; - } elseif ( false === $return ) { - $callback_string = is_array( $callback ) ? get_class( $callback[0] ) . '::' . $callback[1] : $callback; - $ran = false; - /* translators: %s: callback string */ - $message = sprintf( __( 'There was an error calling %s', 'woocommerce' ), $callback_string ); - } else { - $message = __( 'Tool ran.', 'woocommerce' ); - } - } else { - $ran = false; - $message = __( 'There was an error calling this tool. There is no callback present.', 'woocommerce' ); - } - break; - } - - return array( - 'success' => $ran, - 'message' => $message, - ); - } -} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller.php deleted file mode 100644 index b975964674d..00000000000 --- a/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller.php +++ /dev/null @@ -1,27 +0,0 @@ - 405 ) ); - } - - /** - * Check if a given request has access to read an item. - * - * @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 ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access to update an item. - * - * @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 ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $object->get_id() ) ) { - return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access to delete an item. - * - * @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 ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { - return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Get object permalink. - * - * @param object $object Object. - * @return string - */ - protected function get_permalink( $object ) { - return ''; - } - - /** - * Prepares the object for the REST response. - * - * @since 3.0.0 - * @param WC_Data $object Object data. - * @param WP_REST_Request $request Request object. - * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. - */ - protected function prepare_object_for_response( $object, $request ) { - // translators: %s: Class method name. - return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) ); - } - - /** - * Prepares one object for create or update operation. - * - * @since 3.0.0 - * @param WP_REST_Request $request Request object. - * @param bool $creating If is creating a new object. - * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure. - */ - protected function prepare_object_for_database( $request, $creating = false ) { - // translators: %s: Class method name. - return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) ); - } - - /** - * Get a single item. - * - * @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 ( ! $object || 0 === $object->get_id() ) { - return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - $data = $this->prepare_object_for_response( $object, $request ); - $response = rest_ensure_response( $data ); - - if ( $this->public ) { - $response->link_header( 'alternate', $this->get_permalink( $object ), array( 'type' => 'text/html' ) ); - } - - return $response; - } - - /** - * Save an object data. - * - * @since 3.0.0 - * @param WP_REST_Request $request Full details about the request. - * @param bool $creating If is creating a new object. - * @return WC_Data|WP_Error - */ - protected function save_object( $request, $creating = false ) { - try { - $object = $this->prepare_object_for_database( $request, $creating ); - - if ( is_wp_error( $object ) ) { - return $object; - } - - $object->save(); - - return $this->get_object( $object->get_id() ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); - } catch ( WC_REST_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Create a single item. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function create_item( $request ) { - if ( ! empty( $request['id'] ) ) { - /* translators: %s: post type */ - return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); - } - - $object = $this->save_object( $request, true ); - - if ( is_wp_error( $object ) ) { - return $object; - } - - try { - $this->update_additional_fields_for_object( $object, $request ); - - /** - * Fires after a single object is created or updated via the REST API. - * - * @param WC_Data $object Inserted object. - * @param WP_REST_Request $request Request object. - * @param boolean $creating True when creating object, false when updating. - */ - do_action( "woocommerce_rest_insert_{$this->post_type}_object", $object, $request, true ); - } catch ( WC_Data_Exception $e ) { - $object->delete(); - return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); - } catch ( WC_REST_Exception $e ) { - $object->delete(); - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_object_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, $object->get_id() ) ) ); - - return $response; - } - - /** - * Update a single post. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function update_item( $request ) { - $object = $this->get_object( (int) $request['id'] ); - - if ( ! $object || 0 === $object->get_id() ) { - return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 400 ) ); - } - - $object = $this->save_object( $request, false ); - - if ( is_wp_error( $object ) ) { - return $object; - } - - try { - $this->update_additional_fields_for_object( $object, $request ); - - /** - * Fires after a single object is created or updated via the REST API. - * - * @param WC_Data $object Inserted object. - * @param WP_REST_Request $request Request object. - * @param boolean $creating True when creating object, false when updating. - */ - do_action( "woocommerce_rest_insert_{$this->post_type}_object", $object, $request, false ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); - } catch ( WC_REST_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_object_for_response( $object, $request ); - return rest_ensure_response( $response ); - } - - /** - * Prepare objects query. - * - * @since 3.0.0 - * @param WP_REST_Request $request Full details about the request. - * @return array - */ - protected function prepare_objects_query( $request ) { - $args = array(); - $args['offset'] = $request['offset']; - $args['order'] = $request['order']; - $args['orderby'] = $request['orderby']; - $args['paged'] = $request['page']; - $args['post__in'] = $request['include']; - $args['post__not_in'] = $request['exclude']; - $args['posts_per_page'] = $request['per_page']; - $args['name'] = $request['slug']; - $args['post_parent__in'] = $request['parent']; - $args['post_parent__not_in'] = $request['parent_exclude']; - $args['s'] = $request['search']; - $args['fields'] = $this->get_fields_for_response( $request ); - - if ( 'date' === $args['orderby'] ) { - $args['orderby'] = 'date ID'; - } - - $args['date_query'] = array(); - // Set before into date query. Date query must be specified as an array of an array. - if ( isset( $request['before'] ) ) { - $args['date_query'][0]['before'] = $request['before']; - } - - // Set after into date query. Date query must be specified as an array of an array. - if ( isset( $request['after'] ) ) { - $args['date_query'][0]['after'] = $request['after']; - } - - // Force the post_type argument, since it's not a user input variable. - $args['post_type'] = $this->post_type; - - /** - * Filter the query arguments for a request. - * - * Enables adding extra arguments or setting defaults for a post - * collection request. - * - * @param array $args Key value array of query var to query value. - * @param WP_REST_Request $request The request used. - */ - $args = apply_filters( "woocommerce_rest_{$this->post_type}_object_query", $args, $request ); - - return $this->prepare_items_query( $args, $request ); - } - - /** - * Get objects. - * - * @since 3.0.0 - * @param array $query_args Query args. - * @return array - */ - protected function get_objects( $query_args ) { - $query = new WP_Query(); - $result = $query->query( $query_args ); - - $total_posts = $query->found_posts; - if ( $total_posts < 1 ) { - // Out-of-bounds, run the query again without LIMIT for total count. - unset( $query_args['paged'] ); - $count_query = new WP_Query(); - $count_query->query( $query_args ); - $total_posts = $count_query->found_posts; - } - - return array( - 'objects' => array_filter( array_map( array( $this, 'get_object' ), $result ) ), - 'total' => (int) $total_posts, - 'pages' => (int) ceil( $total_posts / (int) $query->query_vars['posts_per_page'] ), - ); - } - - /** - * Get a collection of posts. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function get_items( $request ) { - $query_args = $this->prepare_objects_query( $request ); - $query_results = $this->get_objects( $query_args ); - - $objects = array(); - foreach ( $query_results['objects'] as $object ) { - if ( ! wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) { - continue; - } - - $data = $this->prepare_object_for_response( $object, $request ); - $objects[] = $this->prepare_response_for_collection( $data ); - } - - $page = (int) $query_args['paged']; - $max_pages = $query_results['pages']; - - $response = rest_ensure_response( $objects ); - $response->header( 'X-WP-Total', $query_results['total'] ); - $response->header( 'X-WP-TotalPages', (int) $max_pages ); - - $base = $this->rest_base; - $attrib_prefix = '(?P<'; - if ( strpos( $base, $attrib_prefix ) !== false ) { - $attrib_names = array(); - preg_match( '/\(\?P<[^>]+>.*\)/', $base, $attrib_names, PREG_OFFSET_CAPTURE ); - foreach ( $attrib_names as $attrib_name_match ) { - $beginning_offset = strlen( $attrib_prefix ); - $attrib_name_end = strpos( $attrib_name_match[0], '>', $attrib_name_match[1] ); - $attrib_name = substr( $attrib_name_match[0], $beginning_offset, $attrib_name_end - $beginning_offset ); - if ( isset( $request[ $attrib_name ] ) ) { - $base = str_replace( "(?P<$attrib_name>[\d]+)", $request[ $attrib_name ], $base ); - } - } - } - $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ) ); - - if ( $page > 1 ) { - $prev_page = $page - 1; - if ( $prev_page > $max_pages ) { - $prev_page = $max_pages; - } - $prev_link = add_query_arg( 'page', $prev_page, $base ); - $response->link_header( 'prev', $prev_link ); - } - if ( $max_pages > $page ) { - $next_page = $page + 1; - $next_link = add_query_arg( 'page', $next_page, $base ); - $response->link_header( 'next', $next_link ); - } - - return $response; - } - - /** - * Delete a single item. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Response|WP_Error - */ - public function delete_item( $request ) { - $force = (bool) $request['force']; - $object = $this->get_object( (int) $request['id'] ); - $result = false; - - if ( ! $object || 0 === $object->get_id() ) { - return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) ); - - /** - * Filter whether an object is trashable. - * - * Return false to disable trash support for the object. - * - * @param boolean $supports_trash Whether the object type support trashing. - * @param WC_Data $object The object being considered for trashing support. - */ - $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object ); - - if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { - /* translators: %s: post type */ - return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); - } - - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_object_for_response( $object, $request ); - - // If we're forcing, then delete permanently. - if ( $force ) { - $object->delete( true ); - $result = 0 === $object->get_id(); - } else { - // If we don't support trashing for this type, error out. - if ( ! $supports_trash ) { - /* translators: %s: post type */ - return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) ); - } - - // Otherwise, only trash if we haven't already. - if ( is_callable( array( $object, 'get_status' ) ) ) { - if ( 'trash' === $object->get_status() ) { - /* translators: %s: post type */ - return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); - } - - $object->delete(); - $result = 'trash' === $object->get_status(); - } - } - - if ( ! $result ) { - /* translators: %s: post type */ - return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); - } - - /** - * Fires after a single object is deleted or trashed via the REST API. - * - * @param WC_Data $object The deleted or trashed object. - * @param WP_REST_Response $response The response data. - * @param WP_REST_Request $request The request sent to the API. - */ - do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request ); - - return $response; - } - - /** - * Get fields for an object if getter is defined. - * - * @param object $object Object we are fetching response for. - * @param string $context Context of the request. Can be `view` or `edit`. - * @param array $fields List of fields to fetch. - * @return array Data fetched from getters. - */ - public function fetch_fields_using_getters( $object, $context, $fields ) { - $data = array(); - foreach ( $fields as $field ) { - if ( method_exists( $this, "api_get_$field" ) ) { - $data[ $field ] = $this->{"api_get_$field"}( $object, $context ); - } - } - return $data; - } - - /** - * Prepare links for the request. - * - * @param WC_Data $object Object data. - * @param WP_REST_Request $request Request object. - * @return array Links for the given post. - */ - protected function prepare_links( $object, $request ) { - $links = array( - 'self' => array( - 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), - ), - 'collection' => array( - 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), - ), - ); - - return $links; - } - - /** - * Get the query params for collections of attachments. - * - * @return array - */ - public function get_collection_params() { - $params = array(); - $params['context'] = $this->get_context_param(); - $params['context']['default'] = 'view'; - - $params['page'] = array( - 'description' => __( 'Current page of the collection.', 'woocommerce' ), - 'type' => 'integer', - 'default' => 1, - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - 'minimum' => 1, - ); - $params['per_page'] = array( - 'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ), - 'type' => 'integer', - 'default' => 10, - 'minimum' => 1, - 'maximum' => 100, - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['search'] = array( - 'description' => __( 'Limit results to those matching a string.', 'woocommerce' ), - 'type' => 'string', - 'sanitize_callback' => 'sanitize_text_field', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['after'] = array( - 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'date-time', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['before'] = array( - 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'date-time', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['exclude'] = array( - 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'default' => array(), - 'sanitize_callback' => 'wp_parse_id_list', - ); - $params['include'] = array( - 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'default' => array(), - 'sanitize_callback' => 'wp_parse_id_list', - ); - $params['offset'] = array( - 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), - 'type' => 'integer', - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['order'] = array( - 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'desc', - 'enum' => array( 'asc', 'desc' ), - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['orderby'] = array( - 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'date', - 'enum' => array( - 'date', - 'id', - 'include', - 'title', - 'slug', - 'modified', - ), - 'validate_callback' => 'rest_validate_request_arg', - ); - - if ( $this->hierarchical ) { - $params['parent'] = array( - 'description' => __( 'Limit result set to those of particular parent IDs.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'sanitize_callback' => 'wp_parse_id_list', - 'default' => array(), - ); - $params['parent_exclude'] = array( - 'description' => __( 'Limit result set to all items except those of a particular parent ID.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'sanitize_callback' => 'wp_parse_id_list', - 'default' => array(), - ); - } - - /** - * Filter collection parameters for the posts controller. - * - * The dynamic part of the filter `$this->post_type` refers to the post - * type slug for the controller. - * - * This filter registers the collection parameter, but does not map the - * collection parameter to an internal WP_Query parameter. Use the - * `rest_{$this->post_type}_query` filter to set WP_Query parameters. - * - * @param array $query_params JSON Schema-formatted collection parameters. - * @param WP_Post_Type $post_type Post type object. - */ - return apply_filters( "rest_{$this->post_type}_collection_params", $params, $this->post_type ); - } -} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-customers-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-customers-controller.php deleted file mode 100644 index 473094b6a2b..00000000000 --- a/includes/rest-api/Controllers/Version3/class-wc-rest-customers-controller.php +++ /dev/null @@ -1,307 +0,0 @@ -get_data(); - $format_date = array( 'date_created', 'date_modified' ); - - // Format date values. - foreach ( $format_date as $key ) { - // Date created is stored UTC, date modified is stored WP local time. - $datetime = 'date_created' === $key ? get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $data[ $key ]->getTimestamp() ) ) : $data[ $key ]; - $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); - $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); - } - - return array( - 'id' => $object->get_id(), - 'date_created' => $data['date_created'], - 'date_created_gmt' => $data['date_created_gmt'], - 'date_modified' => $data['date_modified'], - 'date_modified_gmt' => $data['date_modified_gmt'], - 'email' => $data['email'], - 'first_name' => $data['first_name'], - 'last_name' => $data['last_name'], - 'role' => $data['role'], - 'username' => $data['username'], - 'billing' => $data['billing'], - 'shipping' => $data['shipping'], - 'is_paying_customer' => $data['is_paying_customer'], - 'avatar_url' => $object->get_avatar_url(), - 'meta_data' => $data['meta_data'], - ); - } - - /** - * Get the Customer's schema, conforming to JSON Schema. - * - * @return array - */ - public function get_item_schema() { - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'customer', - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_created' => array( - 'description' => __( "The date the customer was created, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_created_gmt' => array( - 'description' => __( 'The date the customer was created, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_modified' => array( - 'description' => __( "The date the customer was last modified, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_modified_gmt' => array( - 'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'email' => array( - 'description' => __( 'The email address for the customer.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'email', - 'context' => array( 'view', 'edit' ), - ), - 'first_name' => array( - 'description' => __( 'Customer first name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'arg_options' => array( - 'sanitize_callback' => 'sanitize_text_field', - ), - ), - 'last_name' => array( - 'description' => __( 'Customer last name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'arg_options' => array( - 'sanitize_callback' => 'sanitize_text_field', - ), - ), - 'role' => array( - 'description' => __( 'Customer role.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'username' => array( - 'description' => __( 'Customer login name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'arg_options' => array( - 'sanitize_callback' => 'sanitize_user', - ), - ), - 'password' => array( - 'description' => __( 'Customer password.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'edit' ), - ), - 'billing' => array( - 'description' => __( 'List of billing address data.', 'woocommerce' ), - 'type' => 'object', - 'context' => array( 'view', 'edit' ), - 'properties' => array( - 'first_name' => array( - 'description' => __( 'First name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'last_name' => array( - 'description' => __( 'Last name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'company' => array( - 'description' => __( 'Company name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'address_1' => array( - 'description' => __( 'Address line 1', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'address_2' => array( - 'description' => __( 'Address line 2', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'city' => array( - 'description' => __( 'City name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'state' => array( - 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'postcode' => array( - 'description' => __( 'Postal code.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'country' => array( - 'description' => __( 'ISO code of the country.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'email' => array( - 'description' => __( 'Email address.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'email', - 'context' => array( 'view', 'edit' ), - ), - 'phone' => array( - 'description' => __( 'Phone number.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - 'shipping' => array( - 'description' => __( 'List of shipping address data.', 'woocommerce' ), - 'type' => 'object', - 'context' => array( 'view', 'edit' ), - 'properties' => array( - 'first_name' => array( - 'description' => __( 'First name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'last_name' => array( - 'description' => __( 'Last name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'company' => array( - 'description' => __( 'Company name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'address_1' => array( - 'description' => __( 'Address line 1', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'address_2' => array( - 'description' => __( 'Address line 2', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'city' => array( - 'description' => __( 'City name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'state' => array( - 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'postcode' => array( - 'description' => __( 'Postal code.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'country' => array( - 'description' => __( 'ISO code of the country.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - 'is_paying_customer' => array( - 'description' => __( 'Is the customer a paying customer?', 'woocommerce' ), - 'type' => 'bool', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'avatar_url' => array( - 'description' => __( 'Avatar URL.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'meta_data' => array( - 'description' => __( 'Meta data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Meta ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'key' => array( - 'description' => __( 'Meta key.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'value' => array( - 'description' => __( 'Meta value.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - ), - ); - - return $this->add_additional_fields_schema( $schema ); - } -} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-data-continents-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-data-continents-controller.php deleted file mode 100644 index f998271969e..00000000000 --- a/includes/rest-api/Controllers/Version3/class-wc-rest-data-continents-controller.php +++ /dev/null @@ -1,362 +0,0 @@ -namespace, - '/' . $this->rest_base, - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_items' ), - 'permission_callback' => array( $this, 'get_items_permissions_check' ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/(?P[\w-]+)', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_items_permissions_check' ), - 'args' => array( - 'continent' => array( - 'description' => __( '2 character continent code.', 'woocommerce' ), - 'type' => 'string', - ), - ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - } - - /** - * Return the list of countries and states for a given continent. - * - * @since 3.5.0 - * @param string $continent_code Continent code. - * @param WP_REST_Request $request Request data. - * @return array|mixed Response data, ready for insertion into collection data. - */ - public function get_continent( $continent_code, $request ) { - $continents = WC()->countries->get_continents(); - $countries = WC()->countries->get_countries(); - $states = WC()->countries->get_states(); - $locale_info = include WC()->plugin_path() . '/i18n/locale-info.php'; - $data = array(); - - if ( ! array_key_exists( $continent_code, $continents ) ) { - return false; - } - - $continent_list = $continents[ $continent_code ]; - - $continent = array( - 'code' => $continent_code, - 'name' => $continent_list['name'], - ); - - $local_countries = array(); - foreach ( $continent_list['countries'] as $country_code ) { - if ( isset( $countries[ $country_code ] ) ) { - $country = array( - 'code' => $country_code, - 'name' => $countries[ $country_code ], - ); - - // If we have detailed locale information include that in the response. - if ( array_key_exists( $country_code, $locale_info ) ) { - // Defensive programming against unexpected changes in locale-info.php. - $country_data = wp_parse_args( - $locale_info[ $country_code ], - array( - 'currency_code' => 'USD', - 'currency_pos' => 'left', - 'decimal_sep' => '.', - 'dimension_unit' => 'in', - 'num_decimals' => 2, - 'thousand_sep' => ',', - 'weight_unit' => 'lbs', - ) - ); - - $country = array_merge( $country, $country_data ); - } - - $local_states = array(); - if ( isset( $states[ $country_code ] ) ) { - foreach ( $states[ $country_code ] as $state_code => $state_name ) { - $local_states[] = array( - 'code' => $state_code, - 'name' => $state_name, - ); - } - } - $country['states'] = $local_states; - - // Allow only desired keys (e.g. filter out tax rates). - $allowed = array( - 'code', - 'currency_code', - 'currency_pos', - 'decimal_sep', - 'dimension_unit', - 'name', - 'num_decimals', - 'states', - 'thousand_sep', - 'weight_unit', - ); - $country = array_intersect_key( $country, array_flip( $allowed ) ); - - $local_countries[] = $country; - } - } - - $continent['countries'] = $local_countries; - return $continent; - } - - /** - * Return the list of states for all continents. - * - * @since 3.5.0 - * @param WP_REST_Request $request Request data. - * @return WP_Error|WP_REST_Response - */ - public function get_items( $request ) { - $continents = WC()->countries->get_continents(); - $data = array(); - - foreach ( array_keys( $continents ) as $continent_code ) { - $continent = $this->get_continent( $continent_code, $request ); - $response = $this->prepare_item_for_response( $continent, $request ); - $data[] = $this->prepare_response_for_collection( $response ); - } - - return rest_ensure_response( $data ); - } - - /** - * Return the list of locations for a given continent. - * - * @since 3.5.0 - * @param WP_REST_Request $request Request data. - * @return WP_Error|WP_REST_Response - */ - public function get_item( $request ) { - $data = $this->get_continent( strtoupper( $request['location'] ), $request ); - if ( empty( $data ) ) { - return new WP_Error( 'woocommerce_rest_data_invalid_location', __( 'There are no locations matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) ); - } - return $this->prepare_item_for_response( $data, $request ); - } - - /** - * Prepare the data object for response. - * - * @since 3.5.0 - * @param object $item Data object. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response $response Response data. - */ - public function prepare_item_for_response( $item, $request ) { - $data = $this->add_additional_fields_to_object( $item, $request ); - $data = $this->filter_response_by_context( $data, 'view' ); - $response = rest_ensure_response( $data ); - - $response->add_links( $this->prepare_links( $item ) ); - - /** - * Filter the location list returned from the API. - * - * Allows modification of the loction data right before it is returned. - * - * @param WP_REST_Response $response The response object. - * @param array $item The original list of continent(s), countries, and states. - * @param WP_REST_Request $request Request used to generate the response. - */ - return apply_filters( 'woocommerce_rest_prepare_data_continent', $response, $item, $request ); - } - - /** - * Prepare links for the request. - * - * @param object $item Data object. - * @return array Links for the given continent. - */ - protected function prepare_links( $item ) { - $continent_code = strtolower( $item['code'] ); - $links = array( - 'self' => array( - 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $continent_code ) ), - ), - 'collection' => array( - 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), - ), - ); - return $links; - } - - /** - * Get the location schema, conforming to JSON Schema. - * - * @since 3.5.0 - * @return array - */ - public function get_item_schema() { - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'data_continents', - 'type' => 'object', - 'properties' => array( - 'code' => array( - 'type' => 'string', - 'description' => __( '2 character continent code.', 'woocommerce' ), - 'context' => array( 'view' ), - 'readonly' => true, - ), - 'name' => array( - 'type' => 'string', - 'description' => __( 'Full name of continent.', 'woocommerce' ), - 'context' => array( 'view' ), - 'readonly' => true, - ), - 'countries' => array( - 'type' => 'array', - 'description' => __( 'List of countries on this continent.', 'woocommerce' ), - 'context' => array( 'view' ), - 'readonly' => true, - 'items' => array( - 'type' => 'object', - 'context' => array( 'view' ), - 'readonly' => true, - 'properties' => array( - 'code' => array( - 'type' => 'string', - 'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ), - 'context' => array( 'view' ), - 'readonly' => true, - ), - 'currency_code' => array( - 'type' => 'string', - 'description' => __( 'Default ISO4127 alpha-3 currency code for the country.', 'woocommerce' ), - 'context' => array( 'view' ), - 'readonly' => true, - ), - 'currency_pos' => array( - 'type' => 'string', - 'description' => __( 'Currency symbol position for this country.', 'woocommerce' ), - 'context' => array( 'view' ), - 'readonly' => true, - ), - 'decimal_sep' => array( - 'type' => 'string', - 'description' => __( 'Decimal separator for displayed prices for this country.', 'woocommerce' ), - 'context' => array( 'view' ), - 'readonly' => true, - ), - 'dimension_unit' => array( - 'type' => 'string', - 'description' => __( 'The unit lengths are defined in for this country.', 'woocommerce' ), - 'context' => array( 'view' ), - 'readonly' => true, - ), - 'name' => array( - 'type' => 'string', - 'description' => __( 'Full name of country.', 'woocommerce' ), - 'context' => array( 'view' ), - 'readonly' => true, - ), - 'num_decimals' => array( - 'type' => 'integer', - 'description' => __( 'Number of decimal points shown in displayed prices for this country.', 'woocommerce' ), - 'context' => array( 'view' ), - 'readonly' => true, - ), - 'states' => array( - 'type' => 'array', - 'description' => __( 'List of states in this country.', 'woocommerce' ), - 'context' => array( 'view' ), - 'readonly' => true, - 'items' => array( - 'type' => 'object', - 'context' => array( 'view' ), - 'readonly' => true, - 'properties' => array( - 'code' => array( - 'type' => 'string', - 'description' => __( 'State code.', 'woocommerce' ), - 'context' => array( 'view' ), - 'readonly' => true, - ), - 'name' => array( - 'type' => 'string', - 'description' => __( 'Full name of state.', 'woocommerce' ), - 'context' => array( 'view' ), - 'readonly' => true, - ), - ), - ), - ), - 'thousand_sep' => array( - 'type' => 'string', - 'description' => __( 'Thousands separator for displayed prices in this country.', 'woocommerce' ), - 'context' => array( 'view' ), - 'readonly' => true, - ), - 'weight_unit' => array( - 'type' => 'string', - 'description' => __( 'The unit weights are defined in for this country.', 'woocommerce' ), - 'context' => array( 'view' ), - 'readonly' => true, - ), - ), - ), - ), - ), - ); - - return $this->add_additional_fields_schema( $schema ); - } -} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-data-countries-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-data-countries-controller.php deleted file mode 100644 index 7144e5f79a9..00000000000 --- a/includes/rest-api/Controllers/Version3/class-wc-rest-data-countries-controller.php +++ /dev/null @@ -1,244 +0,0 @@ -namespace, - '/' . $this->rest_base, - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_items' ), - 'permission_callback' => array( $this, 'get_items_permissions_check' ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/(?P[\w-]+)', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_items_permissions_check' ), - 'args' => array( - 'location' => array( - 'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ), - 'type' => 'string', - ), - ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - } - - /** - * Get a list of countries and states. - * - * @param string $country_code Country code. - * @param WP_REST_Request $request Request data. - * @return array|mixed Response data, ready for insertion into collection data. - */ - public function get_country( $country_code, $request ) { - $countries = WC()->countries->get_countries(); - $states = WC()->countries->get_states(); - $data = array(); - - if ( ! array_key_exists( $country_code, $countries ) ) { - return false; - } - - $country = array( - 'code' => $country_code, - 'name' => $countries[ $country_code ], - ); - - $local_states = array(); - if ( isset( $states[ $country_code ] ) ) { - foreach ( $states[ $country_code ] as $state_code => $state_name ) { - $local_states[] = array( - 'code' => $state_code, - 'name' => $state_name, - ); - } - } - $country['states'] = $local_states; - return $country; - } - - /** - * Return the list of states for all countries. - * - * @since 3.5.0 - * @param WP_REST_Request $request Request data. - * @return WP_Error|WP_REST_Response - */ - public function get_items( $request ) { - $countries = WC()->countries->get_countries(); - $data = array(); - - foreach ( array_keys( $countries ) as $country_code ) { - $country = $this->get_country( $country_code, $request ); - $response = $this->prepare_item_for_response( $country, $request ); - $data[] = $this->prepare_response_for_collection( $response ); - } - - return rest_ensure_response( $data ); - } - - /** - * Return the list of states for a given country. - * - * @since 3.5.0 - * @param WP_REST_Request $request Request data. - * @return WP_Error|WP_REST_Response - */ - public function get_item( $request ) { - $data = $this->get_country( strtoupper( $request['location'] ), $request ); - if ( empty( $data ) ) { - return new WP_Error( 'woocommerce_rest_data_invalid_location', __( 'There are no locations matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) ); - } - return $this->prepare_item_for_response( $data, $request ); - } - - /** - * Prepare the data object for response. - * - * @since 3.5.0 - * @param object $item Data object. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response $response Response data. - */ - public function prepare_item_for_response( $item, $request ) { - $data = $this->add_additional_fields_to_object( $item, $request ); - $data = $this->filter_response_by_context( $data, 'view' ); - $response = rest_ensure_response( $data ); - - $response->add_links( $this->prepare_links( $item ) ); - - /** - * Filter the states list for a country returned from the API. - * - * Allows modification of the loction data right before it is returned. - * - * @param WP_REST_Response $response The response object. - * @param array $data The original country's states list. - * @param WP_REST_Request $request Request used to generate the response. - */ - return apply_filters( 'woocommerce_rest_prepare_data_country', $response, $item, $request ); - } - - /** - * Prepare links for the request. - * - * @param object $item Data object. - * @return array Links for the given country. - */ - protected function prepare_links( $item ) { - $country_code = strtolower( $item['code'] ); - $links = array( - 'self' => array( - 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $country_code ) ), - ), - 'collection' => array( - 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), - ), - ); - - return $links; - } - - - /** - * Get the location schema, conforming to JSON Schema. - * - * @since 3.5.0 - * @return array - */ - public function get_item_schema() { - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'data_countries', - 'type' => 'object', - 'properties' => array( - 'code' => array( - 'type' => 'string', - 'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ), - 'context' => array( 'view' ), - 'readonly' => true, - ), - 'name' => array( - 'type' => 'string', - 'description' => __( 'Full name of country.', 'woocommerce' ), - 'context' => array( 'view' ), - 'readonly' => true, - ), - 'states' => array( - 'type' => 'array', - 'description' => __( 'List of states in this country.', 'woocommerce' ), - 'context' => array( 'view' ), - 'readonly' => true, - 'items' => array( - 'type' => 'object', - 'context' => array( 'view' ), - 'readonly' => true, - 'properties' => array( - 'code' => array( - 'type' => 'string', - 'description' => __( 'State code.', 'woocommerce' ), - 'context' => array( 'view' ), - 'readonly' => true, - ), - 'name' => array( - 'type' => 'string', - 'description' => __( 'Full name of state.', 'woocommerce' ), - 'context' => array( 'view' ), - 'readonly' => true, - ), - ), - ), - ), - ), - ); - - return $this->add_additional_fields_schema( $schema ); - } -} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller.php deleted file mode 100644 index 6cbc0ef64f1..00000000000 --- a/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller.php +++ /dev/null @@ -1,115 +0,0 @@ -/refunds endpoint. - * - * @package WooCommerce\RestApi - * @since 2.6.0 - */ - -defined( 'ABSPATH' ) || exit; - -use Automattic\WooCommerce\Internal\RestApiUtil; - -/** - * REST API Order Refunds controller class. - * - * @package WooCommerce\RestApi - * @extends WC_REST_Order_Refunds_V2_Controller - */ -class WC_REST_Order_Refunds_Controller extends WC_REST_Order_Refunds_V2_Controller { - - /** - * Endpoint namespace. - * - * @var string - */ - protected $namespace = 'wc/v3'; - - /** - * Prepares one object for create or update operation. - * - * @since 3.0.0 - * @param WP_REST_Request $request Request object. - * @param bool $creating If is creating a new object. - * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure. - */ - protected function prepare_object_for_database( $request, $creating = false ) { - RestApiUtil::adjust_create_refund_request_parameters( $request ); - - $order = wc_get_order( (int) $request['order_id'] ); - - if ( ! $order ) { - return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); - } - - if ( 0 > $request['amount'] ) { - return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 ); - } - - // Create the refund. - $refund = wc_create_refund( - array( - 'order_id' => $order->get_id(), - 'amount' => $request['amount'], - 'reason' => $request['reason'], - 'line_items' => $request['line_items'], - 'refund_payment' => $request['api_refund'], - 'restock_items' => true, - ) - ); - - if ( is_wp_error( $refund ) ) { - return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 ); - } - - if ( ! $refund ) { - return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); - } - - if ( ! empty( $request['meta_data'] ) && is_array( $request['meta_data'] ) ) { - foreach ( $request['meta_data'] as $meta ) { - $refund->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); - } - $refund->save_meta_data(); - } - - /** - * Filters an object before it is inserted via the REST API. - * - * The dynamic portion of the hook name, `$this->post_type`, - * refers to the object type slug. - * - * @param WC_Data $coupon Object object. - * @param WP_REST_Request $request Request object. - * @param bool $creating If is creating a new object. - */ - return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $refund, $request, $creating ); - } - - /** - * Get the refund schema, conforming to JSON Schema. - * - * @return array - */ - public function get_item_schema() { - $schema = parent::get_item_schema(); - - $schema['properties']['line_items']['items']['properties']['refund_total'] = array( - 'description' => __( 'Amount that will be refunded for this line item (excluding taxes).', 'woocommerce' ), - 'type' => 'number', - 'context' => array( 'edit' ), - 'readonly' => true, - ); - - $schema['properties']['line_items']['items']['properties']['taxes']['items']['properties']['refund_total'] = array( - 'description' => __( 'Amount that will be refunded for this tax.', 'woocommerce' ), - 'type' => 'number', - 'context' => array( 'edit' ), - 'readonly' => true, - ); - - return $schema; - } -} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php deleted file mode 100644 index 0517a6a18ab..00000000000 --- a/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php +++ /dev/null @@ -1,273 +0,0 @@ -get_items( 'coupon' ) as $coupon ) { - $order->remove_coupon( $coupon->get_code() ); - } - - foreach ( $request['coupon_lines'] as $item ) { - if ( is_array( $item ) ) { - if ( ! empty( $item['id'] ) ) { - throw new WC_REST_Exception( 'woocommerce_rest_coupon_item_id_readonly', __( 'Coupon item ID is readonly.', 'woocommerce' ), 400 ); - } - - if ( empty( $item['code'] ) ) { - throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); - } - - $results = $order->apply_coupon( wc_clean( $item['code'] ) ); - - if ( is_wp_error( $results ) ) { - throw new WC_REST_Exception( 'woocommerce_rest_' . $results->get_error_code(), $results->get_error_message(), 400 ); - } - } - } - - return true; - } - - /** - * Prepare a single order for create or update. - * - * @throws WC_REST_Exception When fails to set any item. - * @param WP_REST_Request $request Request object. - * @param bool $creating If is creating a new object. - * @return WP_Error|WC_Data - */ - protected function prepare_object_for_database( $request, $creating = false ) { - $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; - $order = new WC_Order( $id ); - $schema = $this->get_item_schema(); - $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); - - // Handle all writable props. - foreach ( $data_keys as $key ) { - $value = $request[ $key ]; - - if ( ! is_null( $value ) ) { - switch ( $key ) { - case 'coupon_lines': - case 'status': - // Change should be done later so transitions have new data. - break; - case 'billing': - case 'shipping': - $this->update_address( $order, $value, $key ); - break; - case 'line_items': - case 'shipping_lines': - case 'fee_lines': - if ( is_array( $value ) ) { - foreach ( $value as $item ) { - if ( is_array( $item ) ) { - if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) { - $order->remove_item( $item['id'] ); - } else { - $this->set_item( $order, $key, $item ); - } - } - } - } - break; - case 'meta_data': - if ( is_array( $value ) ) { - foreach ( $value as $meta ) { - $order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); - } - } - break; - default: - if ( is_callable( array( $order, "set_{$key}" ) ) ) { - $order->{"set_{$key}"}( $value ); - } - break; - } - } - } - - /** - * Filters an object before it is inserted via the REST API. - * - * The dynamic portion of the hook name, `$this->post_type`, - * refers to the object type slug. - * - * @param WC_Data $order Object object. - * @param WP_REST_Request $request Request object. - * @param bool $creating If is creating a new object. - */ - return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $order, $request, $creating ); - } - - /** - * Save an object data. - * - * @since 3.0.0 - * @throws WC_REST_Exception But all errors are validated before returning any data. - * @param WP_REST_Request $request Full details about the request. - * @param bool $creating If is creating a new object. - * @return WC_Data|WP_Error - */ - protected function save_object( $request, $creating = false ) { - try { - $object = $this->prepare_object_for_database( $request, $creating ); - - if ( is_wp_error( $object ) ) { - return $object; - } - - // Make sure gateways are loaded so hooks from gateways fire on save/create. - WC()->payment_gateways(); - - if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] ) { - // Make sure customer exists. - if ( false === get_user_by( 'id', $request['customer_id'] ) ) { - throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); - } - - // Make sure customer is part of blog. - if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) { - add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' ); - } - } - - if ( $creating ) { - $object->set_created_via( 'rest-api' ); - $object->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); - $object->calculate_totals(); - } else { - // If items have changed, recalculate order totals. - if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) { - $object->calculate_totals( true ); - } - } - - // Set coupons. - $this->calculate_coupons( $request, $object ); - - // Set status. - if ( ! empty( $request['status'] ) ) { - $object->set_status( $request['status'] ); - } - - $object->save(); - - // Actions for after the order is saved. - if ( true === $request['set_paid'] ) { - if ( $creating || $object->needs_payment() ) { - $object->payment_complete( $request['transaction_id'] ); - } - } - - return $this->get_object( $object->get_id() ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); - } catch ( WC_REST_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Prepare objects query. - * - * @since 3.0.0 - * @param WP_REST_Request $request Full details about the request. - * @return array - */ - protected function prepare_objects_query( $request ) { - // This is needed to get around an array to string notice in WC_REST_Orders_V2_Controller::prepare_objects_query. - $statuses = $request['status']; - unset( $request['status'] ); - $args = parent::prepare_objects_query( $request ); - - $args['post_status'] = array(); - foreach ( $statuses as $status ) { - if ( in_array( $status, $this->get_order_statuses(), true ) ) { - $args['post_status'][] = 'wc-' . $status; - } elseif ( 'any' === $status ) { - // Set status to "any" and short-circuit out. - $args['post_status'] = 'any'; - break; - } else { - $args['post_status'][] = $status; - } - } - - // Put the statuses back for further processing (next/prev links, etc). - $request['status'] = $statuses; - - return $args; - } - - /** - * Get the Order's schema, conforming to JSON Schema. - * - * @return array - */ - public function get_item_schema() { - $schema = parent::get_item_schema(); - - $schema['properties']['coupon_lines']['items']['properties']['discount']['readonly'] = true; - - return $schema; - } - - /** - * Get the query params for collections. - * - * @return array - */ - public function get_collection_params() { - $params = parent::get_collection_params(); - - $params['status'] = array( - 'default' => 'any', - 'description' => __( 'Limit result set to orders which have specific statuses.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'string', - 'enum' => array_merge( array( 'any', 'trash' ), $this->get_order_statuses() ), - ), - 'validate_callback' => 'rest_validate_request_arg', - ); - - return $params; - } -} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php deleted file mode 100644 index 40aea51aaac..00000000000 --- a/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php +++ /dev/null @@ -1,1164 +0,0 @@ -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' => array_merge( - $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( - 'product_id' => array( - 'required' => true, - 'description' => __( 'Unique identifier for the product.', 'woocommerce' ), - 'type' => 'integer', - ), - 'review' => array( - 'required' => true, - 'type' => 'string', - 'description' => __( 'Review content.', 'woocommerce' ), - ), - 'reviewer' => array( - 'required' => true, - 'type' => 'string', - 'description' => __( 'Name of the reviewer.', 'woocommerce' ), - ), - 'reviewer_email' => array( - 'required' => true, - 'type' => 'string', - 'description' => __( 'Email of the reviewer.', 'woocommerce' ), - ), - ) - ), - ), - '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.', 'woocommerce' ), - 'type' => 'integer', - ), - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), - ), - ), - 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 ), - ), - array( - 'methods' => WP_REST_Server::DELETABLE, - 'callback' => array( $this, 'delete_item' ), - 'permission_callback' => array( $this, 'delete_item_permissions_check' ), - 'args' => array( - 'force' => array( - 'default' => false, - 'type' => 'boolean', - 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), - ), - ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - - register_rest_route( - $this->namespace, '/' . $this->rest_base . '/batch', array( - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'batch_items' ), - 'permission_callback' => array( $this, 'batch_items_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), - ), - 'schema' => array( $this, 'get_public_batch_schema' ), - ) - ); - } - - /** - * Check whether a given request has permission to read webhook deliveries. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_items_permissions_check( $request ) { - if ( ! wc_rest_check_product_reviews_permissions( 'read' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access to read a product review. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_item_permissions_check( $request ) { - $id = (int) $request['id']; - $review = get_comment( $id ); - - if ( $review && ! wc_rest_check_product_reviews_permissions( 'read', $review->comment_ID ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access to create a new product review. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function create_item_permissions_check( $request ) { - if ( ! wc_rest_check_product_reviews_permissions( 'create' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access to update a product review. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function update_item_permissions_check( $request ) { - $id = (int) $request['id']; - $review = get_comment( $id ); - - if ( $review && ! wc_rest_check_product_reviews_permissions( 'edit', $review->comment_ID ) ) { - return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access to delete a product review. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function delete_item_permissions_check( $request ) { - $id = (int) $request['id']; - $review = get_comment( $id ); - - if ( $review && ! wc_rest_check_product_reviews_permissions( 'delete', $review->comment_ID ) ) { - return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access batch create, update and delete items. - * - * @param WP_REST_Request $request Full details about the request. - * @return boolean|WP_Error - */ - public function batch_items_permissions_check( $request ) { - if ( ! wc_rest_check_product_reviews_permissions( 'create' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Get all reviews. - * - * @param WP_REST_Request $request Full details about the request. - * @return array|WP_Error - */ - public function get_items( $request ) { - // Retrieve the list of registered collection query parameters. - $registered = $this->get_collection_params(); - - /* - * 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( - 'reviewer' => 'author__in', - 'reviewer_email' => 'author_email', - 'reviewer_exclude' => 'author__not_in', - 'exclude' => 'comment__not_in', - 'include' => 'comment__in', - 'offset' => 'offset', - 'order' => 'order', - 'per_page' => 'number', - 'product' => 'post__in', - 'search' => 'search', - 'status' => 'status', - ); - - $prepared_args = array(); - - /* - * For each known parameter which is both registered and present in the request, - * set the parameter's value on the query $prepared_args. - */ - foreach ( $parameter_mappings as $api_param => $wp_param ) { - if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) { - $prepared_args[ $wp_param ] = $request[ $api_param ]; - } - } - - // Ensure certain parameter values default to empty strings. - foreach ( array( 'author_email', 'search' ) as $param ) { - if ( ! isset( $prepared_args[ $param ] ) ) { - $prepared_args[ $param ] = ''; - } - } - - if ( isset( $registered['orderby'] ) ) { - $prepared_args['orderby'] = $this->normalize_query_param( $request['orderby'] ); - } - - if ( isset( $prepared_args['status'] ) ) { - $prepared_args['status'] = 'approved' === $prepared_args['status'] ? 'approve' : $prepared_args['status']; - } - - $prepared_args['no_found_rows'] = false; - $prepared_args['date_query'] = array(); - - // Set before into date query. Date query must be specified as an array of an array. - if ( isset( $registered['before'], $request['before'] ) ) { - $prepared_args['date_query'][0]['before'] = $request['before']; - } - - // Set after into date query. Date query must be specified as an array of an array. - if ( isset( $registered['after'], $request['after'] ) ) { - $prepared_args['date_query'][0]['after'] = $request['after']; - } - - if ( isset( $registered['page'] ) && empty( $request['offset'] ) ) { - $prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 ); - } - - /** - * Filters arguments, before passing to WP_Comment_Query, when querying reviews via the REST API. - * - * @since 3.5.0 - * @link https://developer.wordpress.org/reference/classes/wp_comment_query/ - * @param array $prepared_args Array of arguments for WP_Comment_Query. - * @param WP_REST_Request $request The current request. - */ - $prepared_args = apply_filters( 'woocommerce_rest_product_review_query', $prepared_args, $request ); - - // Make sure that returns only reviews. - $prepared_args['type'] = 'review'; - - // Query reviews. - $query = new WP_Comment_Query(); - $query_result = $query->query( $prepared_args ); - $reviews = array(); - - foreach ( $query_result as $review ) { - if ( ! wc_rest_check_product_reviews_permissions( 'read', $review->comment_ID ) ) { - continue; - } - - $data = $this->prepare_item_for_response( $review, $request ); - $reviews[] = $this->prepare_response_for_collection( $data ); - } - - $total_reviews = (int) $query->found_comments; - $max_pages = (int) $query->max_num_pages; - - if ( $total_reviews < 1 ) { - // Out-of-bounds, run the query again without LIMIT for total count. - unset( $prepared_args['number'], $prepared_args['offset'] ); - - $query = new WP_Comment_Query(); - $prepared_args['count'] = true; - - $total_reviews = $query->query( $prepared_args ); - $max_pages = ceil( $total_reviews / $request['per_page'] ); - } - - $response = rest_ensure_response( $reviews ); - $response->header( 'X-WP-Total', $total_reviews ); - $response->header( 'X-WP-TotalPages', $max_pages ); - - $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); - - if ( $request['page'] > 1 ) { - $prev_page = $request['page'] - 1; - - if ( $prev_page > $max_pages ) { - $prev_page = $max_pages; - } - - $prev_link = add_query_arg( 'page', $prev_page, $base ); - $response->link_header( 'prev', $prev_link ); - } - - if ( $max_pages > $request['page'] ) { - $next_page = $request['page'] + 1; - $next_link = add_query_arg( 'page', $next_page, $base ); - - $response->link_header( 'next', $next_link ); - } - - return $response; - } - - /** - * Create a single review. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function create_item( $request ) { - if ( ! empty( $request['id'] ) ) { - return new WP_Error( 'woocommerce_rest_review_exists', __( 'Cannot create existing product review.', 'woocommerce' ), array( 'status' => 400 ) ); - } - - $product_id = (int) $request['product_id']; - - if ( 'product' !== get_post_type( $product_id ) ) { - return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - $prepared_review = $this->prepare_item_for_database( $request ); - if ( is_wp_error( $prepared_review ) ) { - return $prepared_review; - } - - $prepared_review['comment_type'] = 'review'; - - /* - * Do not allow a comment to be created with missing or empty comment_content. See wp_handle_comment_submission(). - */ - if ( empty( $prepared_review['comment_content'] ) ) { - return new WP_Error( 'woocommerce_rest_review_content_invalid', __( 'Invalid review content.', 'woocommerce' ), array( 'status' => 400 ) ); - } - - // Setting remaining values before wp_insert_comment so we can use wp_allow_comment(). - if ( ! isset( $prepared_review['comment_date_gmt'] ) ) { - $prepared_review['comment_date_gmt'] = current_time( 'mysql', true ); - } - - if ( ! empty( $_SERVER['REMOTE_ADDR'] ) && rest_is_ip_address( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ) { // WPCS: input var ok, sanitization ok. - $prepared_review['comment_author_IP'] = wc_clean( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); // WPCS: input var ok. - } else { - $prepared_review['comment_author_IP'] = '127.0.0.1'; - } - - if ( ! empty( $request['author_user_agent'] ) ) { - $prepared_review['comment_agent'] = $request['author_user_agent']; - } elseif ( $request->get_header( 'user_agent' ) ) { - $prepared_review['comment_agent'] = $request->get_header( 'user_agent' ); - } else { - $prepared_review['comment_agent'] = ''; - } - - $check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_review ); - if ( is_wp_error( $check_comment_lengths ) ) { - $error_code = str_replace( array( 'comment_author', 'comment_content' ), array( 'reviewer', 'review_content' ), $check_comment_lengths->get_error_code() ); - return new WP_Error( 'woocommerce_rest_' . $error_code, __( 'Product review field exceeds maximum length allowed.', 'woocommerce' ), array( 'status' => 400 ) ); - } - - $prepared_review['comment_parent'] = 0; - $prepared_review['comment_author_url'] = ''; - $prepared_review['comment_approved'] = wp_allow_comment( $prepared_review, true ); - - if ( is_wp_error( $prepared_review['comment_approved'] ) ) { - $error_code = $prepared_review['comment_approved']->get_error_code(); - $error_message = $prepared_review['comment_approved']->get_error_message(); - - if ( 'comment_duplicate' === $error_code ) { - return new WP_Error( 'woocommerce_rest_' . $error_code, $error_message, array( 'status' => 409 ) ); - } - - if ( 'comment_flood' === $error_code ) { - return new WP_Error( 'woocommerce_rest_' . $error_code, $error_message, array( 'status' => 400 ) ); - } - - return $prepared_review['comment_approved']; - } - - /** - * Filters a review before it is inserted via the REST API. - * - * Allows modification of the review right before it is inserted via wp_insert_comment(). - * Returning a WP_Error value from the filter will shortcircuit insertion and allow - * skipping further processing. - * - * @since 3.5.0 - * @param array|WP_Error $prepared_review The prepared review data for wp_insert_comment(). - * @param WP_REST_Request $request Request used to insert the review. - */ - $prepared_review = apply_filters( 'woocommerce_rest_pre_insert_product_review', $prepared_review, $request ); - if ( is_wp_error( $prepared_review ) ) { - return $prepared_review; - } - - $review_id = wp_insert_comment( wp_filter_comment( wp_slash( (array) $prepared_review ) ) ); - - if ( ! $review_id ) { - return new WP_Error( 'woocommerce_rest_review_failed_create', __( 'Creating product review failed.', 'woocommerce' ), array( 'status' => 500 ) ); - } - - if ( isset( $request['status'] ) ) { - $this->handle_status_param( $request['status'], $review_id ); - } - - update_comment_meta( $review_id, 'rating', ! empty( $request['rating'] ) ? $request['rating'] : '0' ); - - $review = get_comment( $review_id ); - - /** - * Fires after a comment is created or updated via the REST API. - * - * @param WP_Comment $review Inserted or updated comment object. - * @param WP_REST_Request $request Request object. - * @param bool $creating True when creating a comment, false when updating. - */ - do_action( 'woocommerce_rest_insert_product_review', $review, $request, true ); - - $fields_update = $this->update_additional_fields_for_object( $review, $request ); - if ( is_wp_error( $fields_update ) ) { - return $fields_update; - } - - $context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view'; - $request->set_param( 'context', $context ); - - $response = $this->prepare_item_for_response( $review, $request ); - $response = rest_ensure_response( $response ); - - $response->set_status( 201 ); - $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $review_id ) ) ); - - return $response; - } - - /** - * Get a single product review. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function get_item( $request ) { - $review = $this->get_review( $request['id'] ); - if ( is_wp_error( $review ) ) { - return $review; - } - - $data = $this->prepare_item_for_response( $review, $request ); - $response = rest_ensure_response( $data ); - - return $response; - } - - /** - * Updates a review. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response Response object on success, or error object on failure. - */ - public function update_item( $request ) { - $review = $this->get_review( $request['id'] ); - if ( is_wp_error( $review ) ) { - return $review; - } - - $id = (int) $review->comment_ID; - - if ( isset( $request['type'] ) && 'review' !== get_comment_type( $id ) ) { - return new WP_Error( 'woocommerce_rest_review_invalid_type', __( 'Sorry, you are not allowed to change the comment type.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - $prepared_args = $this->prepare_item_for_database( $request ); - if ( is_wp_error( $prepared_args ) ) { - return $prepared_args; - } - - if ( ! empty( $prepared_args['comment_post_ID'] ) ) { - if ( 'product' !== get_post_type( (int) $prepared_args['comment_post_ID'] ) ) { - return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); - } - } - - if ( empty( $prepared_args ) && isset( $request['status'] ) ) { - // Only the comment status is being changed. - $change = $this->handle_status_param( $request['status'], $id ); - - if ( ! $change ) { - return new WP_Error( 'woocommerce_rest_review_failed_edit', __( 'Updating review status failed.', 'woocommerce' ), array( 'status' => 500 ) ); - } - } elseif ( ! empty( $prepared_args ) ) { - if ( is_wp_error( $prepared_args ) ) { - return $prepared_args; - } - - if ( isset( $prepared_args['comment_content'] ) && empty( $prepared_args['comment_content'] ) ) { - return new WP_Error( 'woocommerce_rest_review_content_invalid', __( 'Invalid review content.', 'woocommerce' ), array( 'status' => 400 ) ); - } - - $prepared_args['comment_ID'] = $id; - - $check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_args ); - if ( is_wp_error( $check_comment_lengths ) ) { - $error_code = str_replace( array( 'comment_author', 'comment_content' ), array( 'reviewer', 'review_content' ), $check_comment_lengths->get_error_code() ); - return new WP_Error( 'woocommerce_rest_' . $error_code, __( 'Product review field exceeds maximum length allowed.', 'woocommerce' ), array( 'status' => 400 ) ); - } - - $updated = wp_update_comment( wp_slash( (array) $prepared_args ) ); - - if ( false === $updated ) { - return new WP_Error( 'woocommerce_rest_comment_failed_edit', __( 'Updating review failed.', 'woocommerce' ), array( 'status' => 500 ) ); - } - - if ( isset( $request['status'] ) ) { - $this->handle_status_param( $request['status'], $id ); - } - } - - if ( ! empty( $request['rating'] ) ) { - update_comment_meta( $id, 'rating', $request['rating'] ); - } - - $review = get_comment( $id ); - - /** This action is documented in includes/api/class-wc-rest-product-reviews-controller.php */ - do_action( 'woocommerce_rest_insert_product_review', $review, $request, false ); - - $fields_update = $this->update_additional_fields_for_object( $review, $request ); - - if ( is_wp_error( $fields_update ) ) { - return $fields_update; - } - - $request->set_param( 'context', 'edit' ); - - $response = $this->prepare_item_for_response( $review, $request ); - - return rest_ensure_response( $response ); - } - - /** - * Deletes a review. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response Response object on success, or error object on failure. - */ - public function delete_item( $request ) { - $review = $this->get_review( $request['id'] ); - if ( is_wp_error( $review ) ) { - return $review; - } - - $force = isset( $request['force'] ) ? (bool) $request['force'] : false; - - /** - * Filters whether a review can be trashed. - * - * Return false to disable trash support for the post. - * - * @since 3.5.0 - * @param bool $supports_trash Whether the post type support trashing. - * @param WP_Comment $review The review object being considered for trashing support. - */ - $supports_trash = apply_filters( 'woocommerce_rest_product_review_trashable', ( EMPTY_TRASH_DAYS > 0 ), $review ); - - $request->set_param( 'context', 'edit' ); - - if ( $force ) { - $previous = $this->prepare_item_for_response( $review, $request ); - $result = wp_delete_comment( $review->comment_ID, true ); - $response = new WP_REST_Response(); - $response->set_data( - array( - 'deleted' => true, - 'previous' => $previous->get_data(), - ) - ); - } else { - // If this type doesn't support trashing, error out. - if ( ! $supports_trash ) { - /* translators: %s: force=true */ - return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( "The object does not support trashing. Set '%s' to delete.", 'woocommerce' ), 'force=true' ), array( 'status' => 501 ) ); - } - - if ( 'trash' === $review->comment_approved ) { - return new WP_Error( 'woocommerce_rest_already_trashed', __( 'The object has already been trashed.', 'woocommerce' ), array( 'status' => 410 ) ); - } - - $result = wp_trash_comment( $review->comment_ID ); - $review = get_comment( $review->comment_ID ); - $response = $this->prepare_item_for_response( $review, $request ); - } - - if ( ! $result ) { - return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The object cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); - } - - /** - * Fires after a review is deleted via the REST API. - * - * @param WP_Comment $review The deleted review data. - * @param WP_REST_Response $response The response returned from the API. - * @param WP_REST_Request $request The request sent to the API. - */ - do_action( 'woocommerce_rest_delete_review', $review, $response, $request ); - - return $response; - } - - /** - * Prepare a single product review output for response. - * - * @param WP_Comment $review Product review object. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response $response Response data. - */ - public function prepare_item_for_response( $review, $request ) { - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $fields = $this->get_fields_for_response( $request ); - $data = array(); - - if ( in_array( 'id', $fields, true ) ) { - $data['id'] = (int) $review->comment_ID; - } - if ( in_array( 'date_created', $fields, true ) ) { - $data['date_created'] = wc_rest_prepare_date_response( $review->comment_date ); - } - if ( in_array( 'date_created_gmt', $fields, true ) ) { - $data['date_created_gmt'] = wc_rest_prepare_date_response( $review->comment_date_gmt ); - } - if ( in_array( 'product_id', $fields, true ) ) { - $data['product_id'] = (int) $review->comment_post_ID; - } - if ( in_array( 'status', $fields, true ) ) { - $data['status'] = $this->prepare_status_response( (string) $review->comment_approved ); - } - if ( in_array( 'reviewer', $fields, true ) ) { - $data['reviewer'] = $review->comment_author; - } - if ( in_array( 'reviewer_email', $fields, true ) ) { - $data['reviewer_email'] = $review->comment_author_email; - } - if ( in_array( 'review', $fields, true ) ) { - $data['review'] = 'view' === $context ? wpautop( $review->comment_content ) : $review->comment_content; - } - if ( in_array( 'rating', $fields, true ) ) { - $data['rating'] = (int) get_comment_meta( $review->comment_ID, 'rating', true ); - } - if ( in_array( 'verified', $fields, true ) ) { - $data['verified'] = wc_review_is_from_verified_owner( $review->comment_ID ); - } - if ( in_array( 'reviewer_avatar_urls', $fields, true ) ) { - $data['reviewer_avatar_urls'] = rest_get_avatar_urls( $review->comment_author_email ); - } - - $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 ); - - $response->add_links( $this->prepare_links( $review ) ); - - /** - * Filter product reviews object returned from the REST API. - * - * @param WP_REST_Response $response The response object. - * @param WP_Comment $review Product review object used to create response. - * @param WP_REST_Request $request Request object. - */ - return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request ); - } - - /** - * Prepare a single product review to be inserted into the database. - * - * @param WP_REST_Request $request Request object. - * @return array|WP_Error $prepared_review - */ - protected function prepare_item_for_database( $request ) { - if ( isset( $request['id'] ) ) { - $prepared_review['comment_ID'] = (int) $request['id']; - } - - if ( isset( $request['review'] ) ) { - $prepared_review['comment_content'] = $request['review']; - } - - if ( isset( $request['product_id'] ) ) { - $prepared_review['comment_post_ID'] = (int) $request['product_id']; - } - - if ( isset( $request['reviewer'] ) ) { - $prepared_review['comment_author'] = $request['reviewer']; - } - - if ( isset( $request['reviewer_email'] ) ) { - $prepared_review['comment_author_email'] = $request['reviewer_email']; - } - - if ( ! empty( $request['date_created'] ) ) { - $date_data = rest_get_date_with_gmt( $request['date_created'] ); - - if ( ! empty( $date_data ) ) { - list( $prepared_review['comment_date'], $prepared_review['comment_date_gmt'] ) = $date_data; - } - } elseif ( ! empty( $request['date_created_gmt'] ) ) { - $date_data = rest_get_date_with_gmt( $request['date_created_gmt'], true ); - - if ( ! empty( $date_data ) ) { - list( $prepared_review['comment_date'], $prepared_review['comment_date_gmt'] ) = $date_data; - } - } - - /** - * Filters a review after it is prepared for the database. - * - * Allows modification of the review right after it is prepared for the database. - * - * @since 3.5.0 - * @param array $prepared_review The prepared review data for `wp_insert_comment`. - * @param WP_REST_Request $request The current request. - */ - return apply_filters( 'woocommerce_rest_preprocess_product_review', $prepared_review, $request ); - } - - /** - * Prepare links for the request. - * - * @param WP_Comment $review Product review object. - * @return array Links for the given product review. - */ - protected function prepare_links( $review ) { - $links = array( - 'self' => array( - 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $review->comment_ID ) ), - ), - 'collection' => array( - 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), - ), - ); - - if ( 0 !== (int) $review->comment_post_ID ) { - $links['up'] = array( - 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $review->comment_post_ID ) ), - ); - } - - if ( 0 !== (int) $review->user_id ) { - $links['reviewer'] = array( - 'href' => rest_url( 'wp/v2/users/' . $review->user_id ), - 'embeddable' => true, - ); - } - - return $links; - } - - /** - * Get the Product Review's schema, conforming to JSON Schema. - * - * @return array - */ - public function get_item_schema() { - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'product_review', - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_created' => array( - 'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_created_gmt' => array( - 'description' => __( 'The date the review was created, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'product_id' => array( - 'description' => __( 'Unique identifier for the product that the review belongs to.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'status' => array( - 'description' => __( 'Status of the review.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'approved', - 'enum' => array( 'approved', 'hold', 'spam', 'unspam', 'trash', 'untrash' ), - 'context' => array( 'view', 'edit' ), - ), - 'reviewer' => array( - 'description' => __( 'Reviewer name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'reviewer_email' => array( - 'description' => __( 'Reviewer email.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'email', - 'context' => array( 'view', 'edit' ), - ), - 'review' => array( - 'description' => __( 'The content of the review.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'arg_options' => array( - 'sanitize_callback' => 'wp_filter_post_kses', - ), - ), - 'rating' => array( - 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'verified' => array( - 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ); - - if ( get_option( 'show_avatars' ) ) { - $avatar_properties = array(); - $avatar_sizes = rest_get_avatar_sizes(); - - foreach ( $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.', 'woocommerce' ), $size ), - 'type' => 'string', - 'format' => 'uri', - 'context' => array( 'embed', 'view', 'edit' ), - ); - } - $schema['properties']['reviewer_avatar_urls'] = array( - 'description' => __( 'Avatar URLs for the object reviewer.', 'woocommerce' ), - 'type' => 'object', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - 'properties' => $avatar_properties, - ); - } - - return $this->add_additional_fields_schema( $schema ); - } - - /** - * Get the query params for collections. - * - * @return array - */ - public function get_collection_params() { - $params = parent::get_collection_params(); - - $params['context']['default'] = 'view'; - - $params['after'] = array( - 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'date-time', - ); - $params['before'] = array( - 'description' => __( 'Limit response to reviews published before a given ISO8601 compliant date.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'date-time', - ); - $params['exclude'] = array( - 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'default' => array(), - ); - $params['include'] = array( - 'description' => __( 'Limit result set to specific IDs.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'default' => array(), - ); - $params['offset'] = array( - 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), - 'type' => 'integer', - ); - $params['order'] = array( - 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'desc', - 'enum' => array( - 'asc', - 'desc', - ), - ); - $params['orderby'] = array( - 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'date_gmt', - 'enum' => array( - 'date', - 'date_gmt', - 'id', - 'include', - 'product', - ), - ); - $params['reviewer'] = array( - 'description' => __( 'Limit result set to reviews assigned to specific user IDs.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - ); - $params['reviewer_exclude'] = array( - 'description' => __( 'Ensure result set excludes reviews assigned to specific user IDs.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - ); - $params['reviewer_email'] = array( - 'default' => null, - 'description' => __( 'Limit result set to that from a specific author email.', 'woocommerce' ), - 'format' => 'email', - 'type' => 'string', - ); - $params['product'] = array( - 'default' => array(), - 'description' => __( 'Limit result set to reviews assigned to specific product IDs.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - ); - $params['status'] = array( - 'default' => 'approved', - 'description' => __( 'Limit result set to reviews assigned a specific status.', 'woocommerce' ), - 'sanitize_callback' => 'sanitize_key', - 'type' => 'string', - 'enum' => array( - 'all', - 'hold', - 'approved', - 'spam', - 'trash', - ), - ); - - /** - * Filter collection parameters for the reviews controller. - * - * This filter registers the collection parameter, but does not map the - * collection parameter to an internal WP_Comment_Query parameter. Use the - * `wc_rest_review_query` filter to set WP_Comment_Query parameters. - * - * @since 3.5.0 - * @param array $params JSON Schema-formatted collection parameters. - */ - return apply_filters( 'woocommerce_rest_product_review_collection_params', $params ); - } - - /** - * Get the reivew, if the ID is valid. - * - * @since 3.5.0 - * @param int $id Supplied ID. - * @return WP_Comment|WP_Error Comment object if ID is valid, WP_Error otherwise. - */ - protected function get_review( $id ) { - $id = (int) $id; - $error = new WP_Error( 'woocommerce_rest_review_invalid_id', __( 'Invalid review ID.', 'woocommerce' ), array( 'status' => 404 ) ); - - if ( 0 >= $id ) { - return $error; - } - - $review = get_comment( $id ); - if ( empty( $review ) ) { - return $error; - } - - if ( ! empty( $review->comment_post_ID ) ) { - $post = get_post( (int) $review->comment_post_ID ); - - if ( 'product' !== get_post_type( (int) $review->comment_post_ID ) ) { - return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); - } - } - - return $review; - } - - /** - * Prepends internal property prefix to query parameters to match our response fields. - * - * @since 3.5.0 - * @param string $query_param Query parameter. - * @return string - */ - protected function normalize_query_param( $query_param ) { - $prefix = 'comment_'; - - switch ( $query_param ) { - case 'id': - $normalized = $prefix . 'ID'; - break; - case 'product': - $normalized = $prefix . 'post_ID'; - break; - case 'include': - $normalized = 'comment__in'; - break; - default: - $normalized = $prefix . $query_param; - break; - } - - return $normalized; - } - - /** - * Checks comment_approved to set comment status for single comment output. - * - * @since 3.5.0 - * @param string|int $comment_approved comment status. - * @return string Comment status. - */ - protected function prepare_status_response( $comment_approved ) { - switch ( $comment_approved ) { - case 'hold': - case '0': - $status = 'hold'; - break; - case 'approve': - case '1': - $status = 'approved'; - break; - case 'spam': - case 'trash': - default: - $status = $comment_approved; - break; - } - - return $status; - } - - /** - * Sets the comment_status of a given review object when creating or updating a review. - * - * @since 3.5.0 - * @param string|int $new_status New review status. - * @param int $id Review ID. - * @return bool Whether the status was changed. - */ - protected function handle_status_param( $new_status, $id ) { - $old_status = wp_get_comment_status( $id ); - - if ( $new_status === $old_status ) { - return false; - } - - switch ( $new_status ) { - case 'approved': - case 'approve': - case '1': - $changed = wp_set_comment_status( $id, 'approve' ); - break; - case 'hold': - case '0': - $changed = wp_set_comment_status( $id, 'hold' ); - break; - case 'spam': - $changed = wp_spam_comment( $id ); - break; - case 'unspam': - $changed = wp_unspam_comment( $id ); - break; - case 'trash': - $changed = wp_trash_comment( $id ); - break; - case 'untrash': - $changed = wp_untrash_comment( $id ); - break; - default: - $changed = false; - break; - } - - return $changed; - } -} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php deleted file mode 100644 index 417804ff383..00000000000 --- a/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php +++ /dev/null @@ -1,865 +0,0 @@ -/variations endpoints. - * - * @package WooCommerce\RestApi - * @since 3.0.0 - */ - -defined( 'ABSPATH' ) || exit; - -/** - * REST API variations controller class. - * - * @package WooCommerce\RestApi - * @extends WC_REST_Product_Variations_V2_Controller - */ -class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V2_Controller { - - /** - * Endpoint namespace. - * - * @var string - */ - protected $namespace = 'wc/v3'; - - /** - * Prepare a single variation output for response. - * - * @param WC_Data $object Object data. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response - */ - public function prepare_object_for_response( $object, $request ) { - $data = array( - 'id' => $object->get_id(), - 'date_created' => wc_rest_prepare_date_response( $object->get_date_created(), false ), - 'date_created_gmt' => wc_rest_prepare_date_response( $object->get_date_created() ), - 'date_modified' => wc_rest_prepare_date_response( $object->get_date_modified(), false ), - 'date_modified_gmt' => wc_rest_prepare_date_response( $object->get_date_modified() ), - 'description' => wc_format_content( $object->get_description() ), - 'permalink' => $object->get_permalink(), - 'sku' => $object->get_sku(), - 'price' => $object->get_price(), - 'regular_price' => $object->get_regular_price(), - 'sale_price' => $object->get_sale_price(), - 'date_on_sale_from' => wc_rest_prepare_date_response( $object->get_date_on_sale_from(), false ), - 'date_on_sale_from_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_from() ), - 'date_on_sale_to' => wc_rest_prepare_date_response( $object->get_date_on_sale_to(), false ), - 'date_on_sale_to_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_to() ), - 'on_sale' => $object->is_on_sale(), - 'status' => $object->get_status(), - 'purchasable' => $object->is_purchasable(), - 'virtual' => $object->is_virtual(), - 'downloadable' => $object->is_downloadable(), - 'downloads' => $this->get_downloads( $object ), - 'download_limit' => '' !== $object->get_download_limit() ? (int) $object->get_download_limit() : -1, - 'download_expiry' => '' !== $object->get_download_expiry() ? (int) $object->get_download_expiry() : -1, - 'tax_status' => $object->get_tax_status(), - 'tax_class' => $object->get_tax_class(), - 'manage_stock' => $object->managing_stock(), - 'stock_quantity' => $object->get_stock_quantity(), - 'stock_status' => $object->get_stock_status(), - 'backorders' => $object->get_backorders(), - 'backorders_allowed' => $object->backorders_allowed(), - 'backordered' => $object->is_on_backorder(), - 'weight' => $object->get_weight(), - 'dimensions' => array( - 'length' => $object->get_length(), - 'width' => $object->get_width(), - 'height' => $object->get_height(), - ), - 'shipping_class' => $object->get_shipping_class(), - 'shipping_class_id' => $object->get_shipping_class_id(), - 'image' => $this->get_image( $object ), - 'attributes' => $this->get_attributes( $object ), - 'menu_order' => $object->get_menu_order(), - 'meta_data' => $object->get_meta_data(), - ); - - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); - $response = rest_ensure_response( $data ); - $response->add_links( $this->prepare_links( $object, $request ) ); - - /** - * Filter the data for a response. - * - * The dynamic portion of the hook name, $this->post_type, - * refers to object type being prepared for the response. - * - * @param WP_REST_Response $response The response object. - * @param WC_Data $object Object data. - * @param WP_REST_Request $request Request object. - */ - return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); - } - - /** - * Prepare a single variation for create or update. - * - * @param WP_REST_Request $request Request object. - * @param bool $creating If is creating a new object. - * @return WP_Error|WC_Data - */ - protected function prepare_object_for_database( $request, $creating = false ) { - if ( isset( $request['id'] ) ) { - $variation = wc_get_product( absint( $request['id'] ) ); - } else { - $variation = new WC_Product_Variation(); - } - - $variation->set_parent_id( absint( $request['product_id'] ) ); - - // Status. - if ( isset( $request['status'] ) ) { - $variation->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); - } - - // SKU. - if ( isset( $request['sku'] ) ) { - $variation->set_sku( wc_clean( $request['sku'] ) ); - } - - // Thumbnail. - if ( isset( $request['image'] ) ) { - if ( is_array( $request['image'] ) ) { - $variation = $this->set_variation_image( $variation, $request['image'] ); - } else { - $variation->set_image_id( '' ); - } - } - - // Virtual variation. - if ( isset( $request['virtual'] ) ) { - $variation->set_virtual( $request['virtual'] ); - } - - // Downloadable variation. - if ( isset( $request['downloadable'] ) ) { - $variation->set_downloadable( $request['downloadable'] ); - } - - // Downloads. - if ( $variation->get_downloadable() ) { - // Downloadable files. - if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { - $variation = $this->save_downloadable_files( $variation, $request['downloads'] ); - } - - // Download limit. - if ( isset( $request['download_limit'] ) ) { - $variation->set_download_limit( $request['download_limit'] ); - } - - // Download expiry. - if ( isset( $request['download_expiry'] ) ) { - $variation->set_download_expiry( $request['download_expiry'] ); - } - } - - // Shipping data. - $variation = $this->save_product_shipping_data( $variation, $request ); - - // Stock handling. - if ( isset( $request['manage_stock'] ) ) { - $variation->set_manage_stock( $request['manage_stock'] ); - } - - if ( isset( $request['stock_status'] ) ) { - $variation->set_stock_status( $request['stock_status'] ); - } - - if ( isset( $request['backorders'] ) ) { - $variation->set_backorders( $request['backorders'] ); - } - - if ( $variation->get_manage_stock() ) { - if ( isset( $request['stock_quantity'] ) ) { - $variation->set_stock_quantity( $request['stock_quantity'] ); - } elseif ( isset( $request['inventory_delta'] ) ) { - $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); - $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); - $variation->set_stock_quantity( $stock_quantity ); - } - } else { - $variation->set_backorders( 'no' ); - $variation->set_stock_quantity( '' ); - } - - // Regular Price. - if ( isset( $request['regular_price'] ) ) { - $variation->set_regular_price( $request['regular_price'] ); - } - - // Sale Price. - if ( isset( $request['sale_price'] ) ) { - $variation->set_sale_price( $request['sale_price'] ); - } - - if ( isset( $request['date_on_sale_from'] ) ) { - $variation->set_date_on_sale_from( $request['date_on_sale_from'] ); - } - - if ( isset( $request['date_on_sale_from_gmt'] ) ) { - $variation->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null ); - } - - if ( isset( $request['date_on_sale_to'] ) ) { - $variation->set_date_on_sale_to( $request['date_on_sale_to'] ); - } - - if ( isset( $request['date_on_sale_to_gmt'] ) ) { - $variation->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null ); - } - - // Tax class. - if ( isset( $request['tax_class'] ) ) { - $variation->set_tax_class( $request['tax_class'] ); - } - - // Description. - if ( isset( $request['description'] ) ) { - $variation->set_description( wp_kses_post( $request['description'] ) ); - } - - // Update taxonomies. - if ( isset( $request['attributes'] ) ) { - $attributes = array(); - $parent = wc_get_product( $variation->get_parent_id() ); - - if ( ! $parent ) { - return new WP_Error( - // Translators: %d parent ID. - "woocommerce_rest_{$this->post_type}_invalid_parent", - __( 'Cannot set attributes due to invalid parent product.', 'woocommerce' ), - array( 'status' => 404 ) - ); - } - - $parent_attributes = $parent->get_attributes(); - - foreach ( $request['attributes'] as $attribute ) { - $attribute_id = 0; - $attribute_name = ''; - - // Check ID for global attributes or name for product attributes. - if ( ! empty( $attribute['id'] ) ) { - $attribute_id = absint( $attribute['id'] ); - $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); - } elseif ( ! empty( $attribute['name'] ) ) { - $attribute_name = sanitize_title( $attribute['name'] ); - } - - if ( ! $attribute_id && ! $attribute_name ) { - continue; - } - - if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { - continue; - } - - $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); - $attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; - - if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { - // If dealing with a taxonomy, we need to get the slug from the name posted to the API. - $term = get_term_by( 'name', $attribute_value, $attribute_name ); - - if ( $term && ! is_wp_error( $term ) ) { - $attribute_value = $term->slug; - } else { - $attribute_value = sanitize_title( $attribute_value ); - } - } - - $attributes[ $attribute_key ] = $attribute_value; - } - - $variation->set_attributes( $attributes ); - } - - // Menu order. - if ( $request['menu_order'] ) { - $variation->set_menu_order( $request['menu_order'] ); - } - - // Meta data. - if ( is_array( $request['meta_data'] ) ) { - foreach ( $request['meta_data'] as $meta ) { - $variation->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); - } - } - - /** - * Filters an object before it is inserted via the REST API. - * - * The dynamic portion of the hook name, `$this->post_type`, - * refers to the object type slug. - * - * @param WC_Data $variation Object object. - * @param WP_REST_Request $request Request object. - * @param bool $creating If is creating a new object. - */ - return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $variation, $request, $creating ); - } - - /** - * Get the image for a product variation. - * - * @param WC_Product_Variation $variation Variation data. - * @return array - */ - protected function get_image( $variation ) { - if ( ! $variation->get_image_id() ) { - return; - } - - $attachment_id = $variation->get_image_id(); - $attachment_post = get_post( $attachment_id ); - if ( is_null( $attachment_post ) ) { - return; - } - - $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); - if ( ! is_array( $attachment ) ) { - return; - } - - if ( ! isset( $image ) ) { - return array( - 'id' => (int) $attachment_id, - 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ), - 'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ), - 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ), - 'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ), - 'src' => current( $attachment ), - 'name' => get_the_title( $attachment_id ), - 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), - ); - } - } - - /** - * Set variation image. - * - * @throws WC_REST_Exception REST API exceptions. - * @param WC_Product_Variation $variation Variation instance. - * @param array $image Image data. - * @return WC_Product_Variation - */ - protected function set_variation_image( $variation, $image ) { - $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; - - if ( 0 === $attachment_id ) { - if ( isset( $image['src'] ) ) { - $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); - - if ( is_wp_error( $upload ) ) { - if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $variation->get_id(), array( $image ) ) ) { - throw new WC_REST_Exception( 'woocommerce_variation_image_upload_error', $upload->get_error_message(), 400 ); - } - } - - $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $variation->get_id() ); - } else { - $variation->set_image_id( '' ); - return $variation; - } - } - - if ( ! wp_attachment_is_image( $attachment_id ) ) { - /* translators: %s: attachment ID */ - throw new WC_REST_Exception( 'woocommerce_variation_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); - } - - $variation->set_image_id( $attachment_id ); - - // Set the image alt if present. - if ( ! empty( $image['alt'] ) ) { - update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); - } - - // Set the image name if present. - if ( ! empty( $image['name'] ) ) { - wp_update_post( - array( - 'ID' => $attachment_id, - 'post_title' => $image['name'], - ) - ); - } - - return $variation; - } - - /** - * Get the Variation's schema, conforming to JSON Schema. - * - * @return array - */ - public function get_item_schema() { - $weight_unit = get_option( 'woocommerce_weight_unit' ); - $dimension_unit = get_option( 'woocommerce_dimension_unit' ); - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => $this->post_type, - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_created' => array( - 'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_modified' => array( - 'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'description' => array( - 'description' => __( 'Variation description.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'permalink' => array( - 'description' => __( 'Variation URL.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'uri', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'sku' => array( - 'description' => __( 'Unique identifier.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'price' => array( - 'description' => __( 'Current variation price.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'regular_price' => array( - 'description' => __( 'Variation regular price.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'sale_price' => array( - 'description' => __( 'Variation sale price.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'date_on_sale_from' => array( - 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'date_on_sale_from_gmt' => array( - 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'date_on_sale_to' => array( - 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'date_on_sale_to_gmt' => array( - 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'on_sale' => array( - 'description' => __( 'Shows if the variation is on sale.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'status' => array( - 'description' => __( 'Variation status.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'publish', - 'enum' => array_keys( get_post_statuses() ), - 'context' => array( 'view', 'edit' ), - ), - 'purchasable' => array( - 'description' => __( 'Shows if the variation can be bought.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'virtual' => array( - 'description' => __( 'If the variation is virtual.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'downloadable' => array( - 'description' => __( 'If the variation is downloadable.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'downloads' => array( - 'description' => __( 'List of downloadable files.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'File ID.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'File name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'file' => array( - 'description' => __( 'File URL.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - 'download_limit' => array( - 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), - 'type' => 'integer', - 'default' => -1, - 'context' => array( 'view', 'edit' ), - ), - 'download_expiry' => array( - 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), - 'type' => 'integer', - 'default' => -1, - 'context' => array( 'view', 'edit' ), - ), - 'tax_status' => array( - 'description' => __( 'Tax status.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'taxable', - 'enum' => array( 'taxable', 'shipping', 'none' ), - 'context' => array( 'view', 'edit' ), - ), - 'tax_class' => array( - 'description' => __( 'Tax class.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'manage_stock' => array( - 'description' => __( 'Stock management at variation level.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'stock_quantity' => array( - 'description' => __( 'Stock quantity.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'stock_status' => array( - 'description' => __( 'Controls the stock status of the product.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'instock', - 'enum' => array_keys( wc_get_product_stock_status_options() ), - 'context' => array( 'view', 'edit' ), - ), - 'backorders' => array( - 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'no', - 'enum' => array( 'no', 'notify', 'yes' ), - 'context' => array( 'view', 'edit' ), - ), - 'backorders_allowed' => array( - 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'backordered' => array( - 'description' => __( 'Shows if the variation is on backordered.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'weight' => array( - /* translators: %s: weight unit */ - 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'dimensions' => array( - 'description' => __( 'Variation dimensions.', 'woocommerce' ), - 'type' => 'object', - 'context' => array( 'view', 'edit' ), - 'properties' => array( - 'length' => array( - /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'width' => array( - /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'height' => array( - /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - 'shipping_class' => array( - 'description' => __( 'Shipping class slug.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'shipping_class_id' => array( - 'description' => __( 'Shipping class ID.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'image' => array( - 'description' => __( 'Variation image data.', 'woocommerce' ), - 'type' => 'object', - 'context' => array( 'view', 'edit' ), - 'properties' => array( - 'id' => array( - 'description' => __( 'Image ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'date_created' => array( - 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_created_gmt' => array( - 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_modified' => array( - 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_modified_gmt' => array( - 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'src' => array( - 'description' => __( 'Image URL.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'uri', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'Image name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'alt' => array( - 'description' => __( 'Image alternative text.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - 'attributes' => array( - 'description' => __( 'List of attributes.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Attribute ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'Attribute name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'option' => array( - 'description' => __( 'Selected attribute term name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - 'menu_order' => array( - 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'meta_data' => array( - 'description' => __( 'Meta data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Meta ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'key' => array( - 'description' => __( 'Meta key.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'value' => array( - 'description' => __( 'Meta value.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - ), - ); - return $this->add_additional_fields_schema( $schema ); - } - - /** - * Prepare objects query. - * - * @since 3.0.0 - * @param WP_REST_Request $request Full details about the request. - * @return array - */ - protected function prepare_objects_query( $request ) { - $args = WC_REST_CRUD_Controller::prepare_objects_query( $request ); - - // Set post_status. - $args['post_status'] = $request['status']; - - // Filter by sku. - if ( ! empty( $request['sku'] ) ) { - $skus = explode( ',', $request['sku'] ); - // Include the current string as a SKU too. - if ( 1 < count( $skus ) ) { - $skus[] = $request['sku']; - } - - $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. - $args, - array( - 'key' => '_sku', - 'value' => $skus, - 'compare' => 'IN', - ) - ); - } - - // Filter by tax class. - if ( ! empty( $request['tax_class'] ) ) { - $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. - $args, - array( - 'key' => '_tax_class', - 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', - ) - ); - } - - // Price filter. - if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { - $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); // WPCS: slow query ok. - } - - // Filter product based on stock_status. - if ( ! empty( $request['stock_status'] ) ) { - $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. - $args, - array( - 'key' => '_stock_status', - 'value' => $request['stock_status'], - ) - ); - } - - // Filter by on sale products. - if ( is_bool( $request['on_sale'] ) ) { - $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; - $on_sale_ids = wc_get_product_ids_on_sale(); - - // Use 0 when there's no on sale products to avoid return all products. - $on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids; - - $args[ $on_sale_key ] += $on_sale_ids; - } - - // Force the post_type argument, since it's not a user input variable. - if ( ! empty( $request['sku'] ) ) { - $args['post_type'] = array( 'product', 'product_variation' ); - } else { - $args['post_type'] = $this->post_type; - } - - $args['post_parent'] = $request['product_id']; - - return $args; - } - - /** - * Get the query params for collections of attachments. - * - * @return array - */ - public function get_collection_params() { - $params = parent::get_collection_params(); - - unset( - $params['in_stock'], - $params['type'], - $params['featured'], - $params['category'], - $params['tag'], - $params['shipping_class'], - $params['attribute'], - $params['attribute_term'] - ); - - $params['stock_status'] = array( - 'description' => __( 'Limit result set to products with specified stock status.', 'woocommerce' ), - 'type' => 'string', - 'enum' => array_keys( wc_get_product_stock_status_options() ), - 'sanitize_callback' => 'sanitize_text_field', - 'validate_callback' => 'rest_validate_request_arg', - ); - - return $params; - } -} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php deleted file mode 100644 index bad0b9bca14..00000000000 --- a/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php +++ /dev/null @@ -1,1374 +0,0 @@ -get_image_id() ) { - $attachment_ids[] = $product->get_image_id(); - } - - // Add gallery images. - $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); - - // Build image data. - foreach ( $attachment_ids as $attachment_id ) { - $attachment_post = get_post( $attachment_id ); - if ( is_null( $attachment_post ) ) { - continue; - } - - $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); - if ( ! is_array( $attachment ) ) { - continue; - } - - $images[] = array( - 'id' => (int) $attachment_id, - 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ), - 'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ), - 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ), - 'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ), - 'src' => current( $attachment ), - 'name' => get_the_title( $attachment_id ), - 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), - ); - } - - return $images; - } - - /** - * Make extra product orderby features supported by WooCommerce available to the WC API. - * This includes 'price', 'popularity', and 'rating'. - * - * @param WP_REST_Request $request Request data. - * @return array - */ - protected function prepare_objects_query( $request ) { - $args = WC_REST_CRUD_Controller::prepare_objects_query( $request ); - - // Set post_status. - $args['post_status'] = $request['status']; - - // Taxonomy query to filter products by type, category, - // tag, shipping class, and attribute. - $tax_query = array(); - - // Map between taxonomy name and arg's key. - $taxonomies = array( - 'product_cat' => 'category', - 'product_tag' => 'tag', - 'product_shipping_class' => 'shipping_class', - ); - - // Set tax_query for each passed arg. - foreach ( $taxonomies as $taxonomy => $key ) { - if ( ! empty( $request[ $key ] ) ) { - $tax_query[] = array( - 'taxonomy' => $taxonomy, - 'field' => 'term_id', - 'terms' => $request[ $key ], - ); - } - } - - // Filter product type by slug. - if ( ! empty( $request['type'] ) ) { - $tax_query[] = array( - 'taxonomy' => 'product_type', - 'field' => 'slug', - 'terms' => $request['type'], - ); - } - - // Filter by attribute and term. - if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { - if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { - $tax_query[] = array( - 'taxonomy' => $request['attribute'], - 'field' => 'term_id', - 'terms' => $request['attribute_term'], - ); - } - } - - // Build tax_query if taxonomies are set. - if ( ! empty( $tax_query ) ) { - if ( ! empty( $args['tax_query'] ) ) { - $args['tax_query'] = array_merge( $tax_query, $args['tax_query'] ); // WPCS: slow query ok. - } else { - $args['tax_query'] = $tax_query; // WPCS: slow query ok. - } - } - - // Filter featured. - if ( is_bool( $request['featured'] ) ) { - $args['tax_query'][] = array( - 'taxonomy' => 'product_visibility', - 'field' => 'name', - 'terms' => 'featured', - 'operator' => true === $request['featured'] ? 'IN' : 'NOT IN', - ); - } - - // Filter by sku. - if ( ! empty( $request['sku'] ) ) { - $skus = explode( ',', $request['sku'] ); - // Include the current string as a SKU too. - if ( 1 < count( $skus ) ) { - $skus[] = $request['sku']; - } - - $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. - $args, - array( - 'key' => '_sku', - 'value' => $skus, - 'compare' => 'IN', - ) - ); - } - - // Filter by tax class. - if ( ! empty( $request['tax_class'] ) ) { - $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. - $args, - array( - 'key' => '_tax_class', - 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', - ) - ); - } - - // Price filter. - if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { - $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); // WPCS: slow query ok. - } - - // Filter product by stock_status. - if ( ! empty( $request['stock_status'] ) ) { - $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. - $args, - array( - 'key' => '_stock_status', - 'value' => $request['stock_status'], - ) - ); - } - - // Filter by on sale products. - if ( is_bool( $request['on_sale'] ) ) { - $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; - $on_sale_ids = wc_get_product_ids_on_sale(); - - // Use 0 when there's no on sale products to avoid return all products. - $on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids; - - $args[ $on_sale_key ] += $on_sale_ids; - } - - // Force the post_type argument, since it's not a user input variable. - if ( ! empty( $request['sku'] ) ) { - $args['post_type'] = array( 'product', 'product_variation' ); - } else { - $args['post_type'] = $this->post_type; - } - - $orderby = $request->get_param( 'orderby' ); - $order = $request->get_param( 'order' ); - - $ordering_args = WC()->query->get_catalog_ordering_args( $orderby, $order ); - $args['orderby'] = $ordering_args['orderby']; - $args['order'] = $ordering_args['order']; - if ( $ordering_args['meta_key'] ) { - $args['meta_key'] = $ordering_args['meta_key']; // WPCS: slow query ok. - } - - return $args; - } - - /** - * Set product images. - * - * @throws WC_REST_Exception REST API exceptions. - * @param WC_Product $product Product instance. - * @param array $images Images data. - * @return WC_Product - */ - protected function set_product_images( $product, $images ) { - $images = is_array( $images ) ? array_filter( $images ) : array(); - - if ( ! empty( $images ) ) { - $gallery = array(); - - foreach ( $images as $index => $image ) { - $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; - - if ( 0 === $attachment_id && isset( $image['src'] ) ) { - $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); - - if ( is_wp_error( $upload ) ) { - if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { - throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); - } else { - continue; - } - } - - $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); - } - - if ( ! wp_attachment_is_image( $attachment_id ) ) { - /* translators: %s: image ID */ - throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); - } - - $featured_image = $product->get_image_id(); - - if ( 0 === $index ) { - $product->set_image_id( $attachment_id ); - } else { - $gallery[] = $attachment_id; - } - - // Set the image alt if present. - if ( ! empty( $image['alt'] ) ) { - update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); - } - - // Set the image name if present. - if ( ! empty( $image['name'] ) ) { - wp_update_post( - array( - 'ID' => $attachment_id, - 'post_title' => $image['name'], - ) - ); - } - } - - $product->set_gallery_image_ids( $gallery ); - } else { - $product->set_image_id( '' ); - $product->set_gallery_image_ids( array() ); - } - - return $product; - } - - /** - * Prepare a single product for create or update. - * - * @param WP_REST_Request $request Request object. - * @param bool $creating If is creating a new object. - * @return WP_Error|WC_Data - */ - protected function prepare_object_for_database( $request, $creating = false ) { - $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; - - // Type is the most important part here because we need to be using the correct class and methods. - if ( isset( $request['type'] ) ) { - $classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] ); - - if ( ! class_exists( $classname ) ) { - $classname = 'WC_Product_Simple'; - } - - $product = new $classname( $id ); - } elseif ( isset( $request['id'] ) ) { - $product = wc_get_product( $id ); - } else { - $product = new WC_Product_Simple(); - } - - if ( 'variation' === $product->get_type() ) { - return new WP_Error( - "woocommerce_rest_invalid_{$this->post_type}_id", - __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), - array( - 'status' => 404, - ) - ); - } - - // Post title. - if ( isset( $request['name'] ) ) { - $product->set_name( wp_filter_post_kses( $request['name'] ) ); - } - - // Post content. - if ( isset( $request['description'] ) ) { - $product->set_description( wp_filter_post_kses( $request['description'] ) ); - } - - // Post excerpt. - if ( isset( $request['short_description'] ) ) { - $product->set_short_description( wp_filter_post_kses( $request['short_description'] ) ); - } - - // Post status. - if ( isset( $request['status'] ) ) { - $product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); - } - - // Post slug. - if ( isset( $request['slug'] ) ) { - $product->set_slug( $request['slug'] ); - } - - // Menu order. - if ( isset( $request['menu_order'] ) ) { - $product->set_menu_order( $request['menu_order'] ); - } - - // Comment status. - if ( isset( $request['reviews_allowed'] ) ) { - $product->set_reviews_allowed( $request['reviews_allowed'] ); - } - - // Virtual. - if ( isset( $request['virtual'] ) ) { - $product->set_virtual( $request['virtual'] ); - } - - // Tax status. - if ( isset( $request['tax_status'] ) ) { - $product->set_tax_status( $request['tax_status'] ); - } - - // Tax Class. - if ( isset( $request['tax_class'] ) ) { - $product->set_tax_class( $request['tax_class'] ); - } - - // Catalog Visibility. - if ( isset( $request['catalog_visibility'] ) ) { - $product->set_catalog_visibility( $request['catalog_visibility'] ); - } - - // Purchase Note. - if ( isset( $request['purchase_note'] ) ) { - $product->set_purchase_note( wp_kses_post( wp_unslash( $request['purchase_note'] ) ) ); - } - - // Featured Product. - if ( isset( $request['featured'] ) ) { - $product->set_featured( $request['featured'] ); - } - - // Shipping data. - $product = $this->save_product_shipping_data( $product, $request ); - - // SKU. - if ( isset( $request['sku'] ) ) { - $product->set_sku( wc_clean( $request['sku'] ) ); - } - - // Attributes. - if ( isset( $request['attributes'] ) ) { - $attributes = array(); - - foreach ( $request['attributes'] as $attribute ) { - $attribute_id = 0; - $attribute_name = ''; - - // Check ID for global attributes or name for product attributes. - if ( ! empty( $attribute['id'] ) ) { - $attribute_id = absint( $attribute['id'] ); - $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); - } elseif ( ! empty( $attribute['name'] ) ) { - $attribute_name = wc_clean( $attribute['name'] ); - } - - if ( ! $attribute_id && ! $attribute_name ) { - continue; - } - - if ( $attribute_id ) { - - if ( isset( $attribute['options'] ) ) { - $options = $attribute['options']; - - if ( ! is_array( $attribute['options'] ) ) { - // Text based attributes - Posted values are term names. - $options = explode( WC_DELIMITER, $options ); - } - - $values = array_map( 'wc_sanitize_term_text_based', $options ); - $values = array_filter( $values, 'strlen' ); - } else { - $values = array(); - } - - if ( ! empty( $values ) ) { - // Add attribute to array, but don't set values. - $attribute_object = new WC_Product_Attribute(); - $attribute_object->set_id( $attribute_id ); - $attribute_object->set_name( $attribute_name ); - $attribute_object->set_options( $values ); - $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); - $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); - $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); - $attributes[] = $attribute_object; - } - } elseif ( isset( $attribute['options'] ) ) { - // Custom attribute - Add attribute to array and set the values. - if ( is_array( $attribute['options'] ) ) { - $values = $attribute['options']; - } else { - $values = explode( WC_DELIMITER, $attribute['options'] ); - } - $attribute_object = new WC_Product_Attribute(); - $attribute_object->set_name( $attribute_name ); - $attribute_object->set_options( $values ); - $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); - $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); - $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); - $attributes[] = $attribute_object; - } - } - $product->set_attributes( $attributes ); - } - - // Sales and prices. - if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) { - $product->set_regular_price( '' ); - $product->set_sale_price( '' ); - $product->set_date_on_sale_to( '' ); - $product->set_date_on_sale_from( '' ); - $product->set_price( '' ); - } else { - // Regular Price. - if ( isset( $request['regular_price'] ) ) { - $product->set_regular_price( $request['regular_price'] ); - } - - // Sale Price. - if ( isset( $request['sale_price'] ) ) { - $product->set_sale_price( $request['sale_price'] ); - } - - if ( isset( $request['date_on_sale_from'] ) ) { - $product->set_date_on_sale_from( $request['date_on_sale_from'] ); - } - - if ( isset( $request['date_on_sale_from_gmt'] ) ) { - $product->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null ); - } - - if ( isset( $request['date_on_sale_to'] ) ) { - $product->set_date_on_sale_to( $request['date_on_sale_to'] ); - } - - if ( isset( $request['date_on_sale_to_gmt'] ) ) { - $product->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null ); - } - } - - // Product parent ID. - if ( isset( $request['parent_id'] ) ) { - $product->set_parent_id( $request['parent_id'] ); - } - - // Sold individually. - if ( isset( $request['sold_individually'] ) ) { - $product->set_sold_individually( $request['sold_individually'] ); - } - - // Stock status; stock_status has priority over in_stock. - if ( isset( $request['stock_status'] ) ) { - $stock_status = $request['stock_status']; - } else { - $stock_status = $product->get_stock_status(); - } - - // Stock data. - if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { - // Manage stock. - if ( isset( $request['manage_stock'] ) ) { - $product->set_manage_stock( $request['manage_stock'] ); - } - - // Backorders. - if ( isset( $request['backorders'] ) ) { - $product->set_backorders( $request['backorders'] ); - } - - if ( $product->is_type( 'grouped' ) ) { - $product->set_manage_stock( 'no' ); - $product->set_backorders( 'no' ); - $product->set_stock_quantity( '' ); - $product->set_stock_status( $stock_status ); - } elseif ( $product->is_type( 'external' ) ) { - $product->set_manage_stock( 'no' ); - $product->set_backorders( 'no' ); - $product->set_stock_quantity( '' ); - $product->set_stock_status( 'instock' ); - } elseif ( $product->get_manage_stock() ) { - // Stock status is always determined by children so sync later. - if ( ! $product->is_type( 'variable' ) ) { - $product->set_stock_status( $stock_status ); - } - - // Stock quantity. - if ( isset( $request['stock_quantity'] ) ) { - $product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) ); - } elseif ( isset( $request['inventory_delta'] ) ) { - $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); - $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); - $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); - } - } else { - // Don't manage stock. - $product->set_manage_stock( 'no' ); - $product->set_stock_quantity( '' ); - $product->set_stock_status( $stock_status ); - } - } elseif ( ! $product->is_type( 'variable' ) ) { - $product->set_stock_status( $stock_status ); - } - - // Upsells. - if ( isset( $request['upsell_ids'] ) ) { - $upsells = array(); - $ids = $request['upsell_ids']; - - if ( ! empty( $ids ) ) { - foreach ( $ids as $id ) { - if ( $id && $id > 0 ) { - $upsells[] = $id; - } - } - } - - $product->set_upsell_ids( $upsells ); - } - - // Cross sells. - if ( isset( $request['cross_sell_ids'] ) ) { - $crosssells = array(); - $ids = $request['cross_sell_ids']; - - if ( ! empty( $ids ) ) { - foreach ( $ids as $id ) { - if ( $id && $id > 0 ) { - $crosssells[] = $id; - } - } - } - - $product->set_cross_sell_ids( $crosssells ); - } - - // Product categories. - if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { - $product = $this->save_taxonomy_terms( $product, $request['categories'] ); - } - - // Product tags. - if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { - $new_tags = array(); - - foreach ( $request['tags'] as $tag ) { - if ( ! isset( $tag['name'] ) ) { - $new_tags[] = $tag; - continue; - } - - if ( ! term_exists( $tag['name'], 'product_tag' ) ) { - // Create the tag if it doesn't exist. - $term = wp_insert_term( $tag['name'], 'product_tag' ); - - if ( ! is_wp_error( $term ) ) { - $new_tags[] = array( - 'id' => $term['term_id'], - ); - - continue; - } - } else { - // Tag exists, assume user wants to set the product with this tag. - $new_tags[] = array( - 'id' => get_term_by( 'name', $tag['name'], 'product_tag' )->term_id, - ); - } - } - - $product = $this->save_taxonomy_terms( $product, $new_tags, 'tag' ); - } - - // Downloadable. - if ( isset( $request['downloadable'] ) ) { - $product->set_downloadable( $request['downloadable'] ); - } - - // Downloadable options. - if ( $product->get_downloadable() ) { - - // Downloadable files. - if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { - $product = $this->save_downloadable_files( $product, $request['downloads'] ); - } - - // Download limit. - if ( isset( $request['download_limit'] ) ) { - $product->set_download_limit( $request['download_limit'] ); - } - - // Download expiry. - if ( isset( $request['download_expiry'] ) ) { - $product->set_download_expiry( $request['download_expiry'] ); - } - } - - // Product url and button text for external products. - if ( $product->is_type( 'external' ) ) { - if ( isset( $request['external_url'] ) ) { - $product->set_product_url( $request['external_url'] ); - } - - if ( isset( $request['button_text'] ) ) { - $product->set_button_text( $request['button_text'] ); - } - } - - // Save default attributes for variable products. - if ( $product->is_type( 'variable' ) ) { - $product = $this->save_default_attributes( $product, $request ); - } - - // Set children for a grouped product. - if ( $product->is_type( 'grouped' ) && isset( $request['grouped_products'] ) ) { - $product->set_children( $request['grouped_products'] ); - } - - // Check for featured/gallery images, upload it and set it. - if ( isset( $request['images'] ) ) { - $product = $this->set_product_images( $product, $request['images'] ); - } - - // Allow set meta_data. - if ( is_array( $request['meta_data'] ) ) { - foreach ( $request['meta_data'] as $meta ) { - $product->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); - } - } - - if ( ! empty( $request['date_created'] ) ) { - $date = rest_parse_date( $request['date_created'] ); - - if ( $date ) { - $product->set_date_created( $date ); - } - } - - if ( ! empty( $request['date_created_gmt'] ) ) { - $date = rest_parse_date( $request['date_created_gmt'], true ); - - if ( $date ) { - $product->set_date_created( $date ); - } - } - - /** - * Filters an object before it is inserted via the REST API. - * - * The dynamic portion of the hook name, `$this->post_type`, - * refers to the object type slug. - * - * @param WC_Data $product Object object. - * @param WP_REST_Request $request Request object. - * @param bool $creating If is creating a new object. - */ - return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $product, $request, $creating ); - } - - /** - * Get the Product's schema, conforming to JSON Schema. - * - * @return array - */ - public function get_item_schema() { - $weight_unit = get_option( 'woocommerce_weight_unit' ); - $dimension_unit = get_option( 'woocommerce_dimension_unit' ); - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => $this->post_type, - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'name' => array( - 'description' => __( 'Product name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'slug' => array( - 'description' => __( 'Product slug.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'permalink' => array( - 'description' => __( 'Product URL.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'uri', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_created' => array( - 'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'date_created_gmt' => array( - 'description' => __( 'The date the product was created, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'date_modified' => array( - 'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_modified_gmt' => array( - 'description' => __( 'The date the product was last modified, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'type' => array( - 'description' => __( 'Product type.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'simple', - 'enum' => array_keys( wc_get_product_types() ), - 'context' => array( 'view', 'edit' ), - ), - 'status' => array( - 'description' => __( 'Product status (post status).', 'woocommerce' ), - 'type' => 'string', - 'default' => 'publish', - 'enum' => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ), - 'context' => array( 'view', 'edit' ), - ), - 'featured' => array( - 'description' => __( 'Featured product.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'catalog_visibility' => array( - 'description' => __( 'Catalog visibility.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'visible', - 'enum' => array( 'visible', 'catalog', 'search', 'hidden' ), - 'context' => array( 'view', 'edit' ), - ), - 'description' => array( - 'description' => __( 'Product description.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'short_description' => array( - 'description' => __( 'Product short description.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'sku' => array( - 'description' => __( 'Unique identifier.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'price' => array( - 'description' => __( 'Current product price.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'regular_price' => array( - 'description' => __( 'Product regular price.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'sale_price' => array( - 'description' => __( 'Product sale price.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'date_on_sale_from' => array( - 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'date_on_sale_from_gmt' => array( - 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'date_on_sale_to' => array( - 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'date_on_sale_to_gmt' => array( - 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'price_html' => array( - 'description' => __( 'Price formatted in HTML.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'on_sale' => array( - 'description' => __( 'Shows if the product is on sale.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'purchasable' => array( - 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'total_sales' => array( - 'description' => __( 'Amount of sales.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'virtual' => array( - 'description' => __( 'If the product is virtual.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'downloadable' => array( - 'description' => __( 'If the product is downloadable.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'downloads' => array( - 'description' => __( 'List of downloadable files.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'File ID.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'File name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'file' => array( - 'description' => __( 'File URL.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - 'download_limit' => array( - 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), - 'type' => 'integer', - 'default' => -1, - 'context' => array( 'view', 'edit' ), - ), - 'download_expiry' => array( - 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), - 'type' => 'integer', - 'default' => -1, - 'context' => array( 'view', 'edit' ), - ), - 'external_url' => array( - 'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'uri', - 'context' => array( 'view', 'edit' ), - ), - 'button_text' => array( - 'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'tax_status' => array( - 'description' => __( 'Tax status.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'taxable', - 'enum' => array( 'taxable', 'shipping', 'none' ), - 'context' => array( 'view', 'edit' ), - ), - 'tax_class' => array( - 'description' => __( 'Tax class.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'manage_stock' => array( - 'description' => __( 'Stock management at product level.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'stock_quantity' => array( - 'description' => __( 'Stock quantity.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'stock_status' => array( - 'description' => __( 'Controls the stock status of the product.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'instock', - 'enum' => array_keys( wc_get_product_stock_status_options() ), - 'context' => array( 'view', 'edit' ), - ), - 'backorders' => array( - 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'no', - 'enum' => array( 'no', 'notify', 'yes' ), - 'context' => array( 'view', 'edit' ), - ), - 'backorders_allowed' => array( - 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'backordered' => array( - 'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'sold_individually' => array( - 'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'weight' => array( - /* translators: %s: weight unit */ - 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'dimensions' => array( - 'description' => __( 'Product dimensions.', 'woocommerce' ), - 'type' => 'object', - 'context' => array( 'view', 'edit' ), - 'properties' => array( - 'length' => array( - /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'width' => array( - /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'height' => array( - /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - 'shipping_required' => array( - 'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'shipping_taxable' => array( - 'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'shipping_class' => array( - 'description' => __( 'Shipping class slug.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'shipping_class_id' => array( - 'description' => __( 'Shipping class ID.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'reviews_allowed' => array( - 'description' => __( 'Allow reviews.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => true, - 'context' => array( 'view', 'edit' ), - ), - 'average_rating' => array( - 'description' => __( 'Reviews average rating.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'rating_count' => array( - 'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'related_ids' => array( - 'description' => __( 'List of related products IDs.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'upsell_ids' => array( - 'description' => __( 'List of up-sell products IDs.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'context' => array( 'view', 'edit' ), - ), - 'cross_sell_ids' => array( - 'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'context' => array( 'view', 'edit' ), - ), - 'parent_id' => array( - 'description' => __( 'Product parent ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'purchase_note' => array( - 'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'categories' => array( - 'description' => __( 'List of categories.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Category ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'Category name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'slug' => array( - 'description' => __( 'Category slug.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ), - ), - 'tags' => array( - 'description' => __( 'List of tags.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Tag ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'Tag name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'slug' => array( - 'description' => __( 'Tag slug.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ), - ), - 'images' => array( - 'description' => __( 'List of images.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Image ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'date_created' => array( - 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_created_gmt' => array( - 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_modified' => array( - 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_modified_gmt' => array( - 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'src' => array( - 'description' => __( 'Image URL.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'uri', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'Image name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'alt' => array( - 'description' => __( 'Image alternative text.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - 'attributes' => array( - 'description' => __( 'List of attributes.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Attribute ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'Attribute name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'position' => array( - 'description' => __( 'Attribute position.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'visible' => array( - 'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'variation' => array( - 'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'options' => array( - 'description' => __( 'List of available term names of the attribute.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'string', - ), - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - 'default_attributes' => array( - 'description' => __( 'Defaults variation attributes.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Attribute ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'Attribute name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'option' => array( - 'description' => __( 'Selected attribute term name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - 'variations' => array( - 'description' => __( 'List of variations IDs.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'integer', - ), - 'readonly' => true, - ), - 'grouped_products' => array( - 'description' => __( 'List of grouped products ID.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'menu_order' => array( - 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'meta_data' => array( - 'description' => __( 'Meta data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Meta ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'key' => array( - 'description' => __( 'Meta key.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'value' => array( - 'description' => __( 'Meta value.', 'woocommerce' ), - 'type' => 'mixed', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - ), - ); - return $this->add_additional_fields_schema( $schema ); - } - - /** - * Add new options for 'orderby' to the collection params. - * - * @return array - */ - public function get_collection_params() { - $params = parent::get_collection_params(); - $params['orderby']['enum'] = array_merge( $params['orderby']['enum'], array( 'price', 'popularity', 'rating' ) ); - - unset( $params['in_stock'] ); - $params['stock_status'] = array( - 'description' => __( 'Limit result set to products with specified stock status.', 'woocommerce' ), - 'type' => 'string', - 'enum' => array_keys( wc_get_product_stock_status_options() ), - 'sanitize_callback' => 'sanitize_text_field', - 'validate_callback' => 'rest_validate_request_arg', - ); - - return $params; - } - - /** - * Get product data. - * - * @param WC_Product $product Product instance. - * @param string $context Request context. Options: 'view' and 'edit'. - * - * @return array - */ - protected function get_product_data( $product, $context = 'view' ) { - $data = parent::get_product_data( ...func_get_args() ); - // Add stock_status if needed. - if ( isset( $this->request ) ) { - $fields = $this->get_fields_for_response( $this->request ); - if ( in_array( 'stock_status', $fields ) ) { - $data['stock_status'] = $product->get_stock_status( $context ); - } - } - return $data; - } -} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php deleted file mode 100644 index 32b59d0771f..00000000000 --- a/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php +++ /dev/null @@ -1,27 +0,0 @@ -/methods endpoint. - * - * @package WooCommerce\RestApi - * @since 3.0.0 - */ - -defined( 'ABSPATH' ) || exit; - -/** - * REST API Shipping Zone Methods class. - * - * @package WooCommerce\RestApi - * @extends WC_REST_Shipping_Zone_Methods_V2_Controller - */ -class WC_REST_Shipping_Zone_Methods_Controller extends WC_REST_Shipping_Zone_Methods_V2_Controller { - - /** - * Endpoint namespace. - * - * @var string - */ - protected $namespace = 'wc/v3'; -} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php deleted file mode 100644 index 516aee8dfb7..00000000000 --- a/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php +++ /dev/null @@ -1,27 +0,0 @@ -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' => array_merge( - $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), - array( - 'name' => array( - 'type' => 'string', - 'description' => __( 'Name for the resource.', 'woocommerce' ), - 'required' => true, - ), - ) - ), - ), - '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.', 'woocommerce' ), - 'type' => 'integer', - ), - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), - ), - ), - 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 ), - ), - array( - 'methods' => WP_REST_Server::DELETABLE, - 'callback' => array( $this, 'delete_item' ), - 'permission_callback' => array( $this, 'delete_item_permissions_check' ), - 'args' => array( - 'force' => array( - 'default' => false, - 'type' => 'boolean', - 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), - ), - ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/batch', - array( - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'batch_items' ), - 'permission_callback' => array( $this, 'batch_items_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), - ), - 'schema' => array( $this, 'get_public_batch_schema' ), - ) - ); - } - - /** - * Check if a given request has access to read the terms. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_items_permissions_check( $request ) { - $permissions = $this->check_permissions( $request, 'read' ); - if ( is_wp_error( $permissions ) ) { - return $permissions; - } - - if ( ! $permissions ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access to create a term. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function create_item_permissions_check( $request ) { - $permissions = $this->check_permissions( $request, 'create' ); - if ( is_wp_error( $permissions ) ) { - return $permissions; - } - - if ( ! $permissions ) { - return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access to read a term. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_item_permissions_check( $request ) { - $permissions = $this->check_permissions( $request, 'read' ); - if ( is_wp_error( $permissions ) ) { - return $permissions; - } - - if ( ! $permissions ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access to update a term. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function update_item_permissions_check( $request ) { - $permissions = $this->check_permissions( $request, 'edit' ); - if ( is_wp_error( $permissions ) ) { - return $permissions; - } - - if ( ! $permissions ) { - return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access to delete a term. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function delete_item_permissions_check( $request ) { - $permissions = $this->check_permissions( $request, 'delete' ); - if ( is_wp_error( $permissions ) ) { - return $permissions; - } - - if ( ! $permissions ) { - return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access batch create, update and delete items. - * - * @param WP_REST_Request $request Full details about the request. - * @return boolean|WP_Error - */ - public function batch_items_permissions_check( $request ) { - $permissions = $this->check_permissions( $request, 'batch' ); - if ( is_wp_error( $permissions ) ) { - return $permissions; - } - - if ( ! $permissions ) { - return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check permissions. - * - * @param WP_REST_Request $request Full details about the request. - * @param string $context Request context. - * @return bool|WP_Error - */ - protected function check_permissions( $request, $context = 'read' ) { - // Get taxonomy. - $taxonomy = $this->get_taxonomy( $request ); - if ( ! $taxonomy || ! taxonomy_exists( $taxonomy ) ) { - return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Taxonomy does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - // Check permissions for a single term. - $id = intval( $request['id'] ); - if ( $id ) { - $term = get_term( $id, $taxonomy ); - - if ( is_wp_error( $term ) || ! $term || $term->taxonomy !== $taxonomy ) { - return new WP_Error( 'woocommerce_rest_term_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - return wc_rest_check_product_term_permissions( $taxonomy, $context, $term->term_id ); - } - - return wc_rest_check_product_term_permissions( $taxonomy, $context ); - } - - /** - * Get terms associated with a taxonomy. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Response|WP_Error - */ - public function get_items( $request ) { - $taxonomy = $this->get_taxonomy( $request ); - $prepared_args = array( - 'exclude' => $request['exclude'], - 'include' => $request['include'], - 'order' => $request['order'], - 'orderby' => $request['orderby'], - 'product' => $request['product'], - 'hide_empty' => $request['hide_empty'], - 'number' => $request['per_page'], - 'search' => $request['search'], - 'slug' => $request['slug'], - ); - - if ( ! empty( $request['offset'] ) ) { - $prepared_args['offset'] = $request['offset']; - } else { - $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; - } - - $taxonomy_obj = get_taxonomy( $taxonomy ); - - if ( $taxonomy_obj->hierarchical && isset( $request['parent'] ) ) { - if ( 0 === $request['parent'] ) { - // Only query top-level terms. - $prepared_args['parent'] = 0; - } else { - if ( $request['parent'] ) { - $prepared_args['parent'] = $request['parent']; - } - } - } - - /** - * Filter the query arguments, before passing them to `get_terms()`. - * - * Enables adding extra arguments or setting defaults for a terms - * collection request. - * - * @see https://developer.wordpress.org/reference/functions/get_terms/ - * - * @param array $prepared_args Array of arguments to be - * passed to get_terms. - * @param WP_REST_Request $request The current request. - */ - $prepared_args = apply_filters( "woocommerce_rest_{$taxonomy}_query", $prepared_args, $request ); - - if ( ! empty( $prepared_args['product'] ) ) { - $query_result = $this->get_terms_for_product( $prepared_args, $request ); - $total_terms = $this->total_terms; - } else { - $query_result = get_terms( $taxonomy, $prepared_args ); - - $count_args = $prepared_args; - unset( $count_args['number'] ); - unset( $count_args['offset'] ); - $total_terms = wp_count_terms( $taxonomy, $count_args ); - - // Ensure we don't return results when offset is out of bounds. - // See https://core.trac.wordpress.org/ticket/35935. - if ( $prepared_args['offset'] && $prepared_args['offset'] >= $total_terms ) { - $query_result = array(); - } - - // wp_count_terms can return a falsy value when the term has no children. - if ( ! $total_terms ) { - $total_terms = 0; - } - } - $response = array(); - foreach ( $query_result as $term ) { - $data = $this->prepare_item_for_response( $term, $request ); - $response[] = $this->prepare_response_for_collection( $data ); - } - - $response = rest_ensure_response( $response ); - - // Store pagination values for headers then unset for count query. - $per_page = (int) $prepared_args['number']; - $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); - - $response->header( 'X-WP-Total', (int) $total_terms ); - $max_pages = ceil( $total_terms / $per_page ); - $response->header( 'X-WP-TotalPages', (int) $max_pages ); - - $base = str_replace( '(?P[\d]+)', $request['attribute_id'], $this->rest_base ); - $base = add_query_arg( $request->get_query_params(), rest_url( '/' . $this->namespace . '/' . $base ) ); - if ( $page > 1 ) { - $prev_page = $page - 1; - if ( $prev_page > $max_pages ) { - $prev_page = $max_pages; - } - $prev_link = add_query_arg( 'page', $prev_page, $base ); - $response->link_header( 'prev', $prev_link ); - } - if ( $max_pages > $page ) { - $next_page = $page + 1; - $next_link = add_query_arg( 'page', $next_page, $base ); - $response->link_header( 'next', $next_link ); - } - - return $response; - } - - /** - * Create a single term for a taxonomy. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Request|WP_Error - */ - public function create_item( $request ) { - $taxonomy = $this->get_taxonomy( $request ); - $name = $request['name']; - $args = array(); - $schema = $this->get_item_schema(); - - if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) { - $args['description'] = $request['description']; - } - if ( isset( $request['slug'] ) ) { - $args['slug'] = $request['slug']; - } - if ( isset( $request['parent'] ) ) { - if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { - return new WP_Error( 'woocommerce_rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.', 'woocommerce' ), array( 'status' => 400 ) ); - } - $args['parent'] = $request['parent']; - } - - $term = wp_insert_term( $name, $taxonomy, $args ); - if ( is_wp_error( $term ) ) { - $error_data = array( 'status' => 400 ); - - // If we're going to inform the client that the term exists, - // give them the identifier they can actually use. - $term_id = $term->get_error_data( 'term_exists' ); - if ( $term_id ) { - $error_data['resource_id'] = $term_id; - } - - return new WP_Error( $term->get_error_code(), $term->get_error_message(), $error_data ); - } - - $term = get_term( $term['term_id'], $taxonomy ); - - $this->update_additional_fields_for_object( $term, $request ); - - // Add term data. - $meta_fields = $this->update_term_meta_fields( $term, $request ); - if ( is_wp_error( $meta_fields ) ) { - wp_delete_term( $term->term_id, $taxonomy ); - - return $meta_fields; - } - - /** - * Fires after a single term is created or updated via the REST API. - * - * @param WP_Term $term Inserted Term object. - * @param WP_REST_Request $request Request object. - * @param boolean $creating True when creating term, false when updating. - */ - do_action( "woocommerce_rest_insert_{$taxonomy}", $term, $request, true ); - - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( $term, $request ); - $response = rest_ensure_response( $response ); - $response->set_status( 201 ); - - $base = '/' . $this->namespace . '/' . $this->rest_base; - if ( ! empty( $request['attribute_id'] ) ) { - $base = str_replace( '(?P[\d]+)', (int) $request['attribute_id'], $base ); - } - - $response->header( 'Location', rest_url( $base . '/' . $term->term_id ) ); - - return $response; - } - - /** - * Get a single term from a taxonomy. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Request|WP_Error - */ - public function get_item( $request ) { - $taxonomy = $this->get_taxonomy( $request ); - $term = get_term( (int) $request['id'], $taxonomy ); - - if ( is_wp_error( $term ) ) { - return $term; - } - - $response = $this->prepare_item_for_response( $term, $request ); - - return rest_ensure_response( $response ); - } - - /** - * Update a single term from a taxonomy. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Request|WP_Error - */ - public function update_item( $request ) { - $taxonomy = $this->get_taxonomy( $request ); - $term = get_term( (int) $request['id'], $taxonomy ); - $schema = $this->get_item_schema(); - $prepared_args = array(); - - if ( isset( $request['name'] ) ) { - $prepared_args['name'] = $request['name']; - } - if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) { - $prepared_args['description'] = $request['description']; - } - if ( isset( $request['slug'] ) ) { - $prepared_args['slug'] = $request['slug']; - } - if ( isset( $request['parent'] ) ) { - if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { - return new WP_Error( 'woocommerce_rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.', 'woocommerce' ), array( 'status' => 400 ) ); - } - $prepared_args['parent'] = $request['parent']; - } - - // Only update the term if we haz something to update. - if ( ! empty( $prepared_args ) ) { - $update = wp_update_term( $term->term_id, $term->taxonomy, $prepared_args ); - if ( is_wp_error( $update ) ) { - return $update; - } - } - - $term = get_term( (int) $request['id'], $taxonomy ); - - $this->update_additional_fields_for_object( $term, $request ); - - // Update term data. - $meta_fields = $this->update_term_meta_fields( $term, $request ); - if ( is_wp_error( $meta_fields ) ) { - return $meta_fields; - } - - /** - * Fires after a single term is created or updated via the REST API. - * - * @param WP_Term $term Inserted Term object. - * @param WP_REST_Request $request Request object. - * @param boolean $creating True when creating term, false when updating. - */ - do_action( "woocommerce_rest_insert_{$taxonomy}", $term, $request, false ); - - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( $term, $request ); - return rest_ensure_response( $response ); - } - - /** - * Delete a single term from a taxonomy. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Response|WP_Error - */ - public function delete_item( $request ) { - $taxonomy = $this->get_taxonomy( $request ); - $force = isset( $request['force'] ) ? (bool) $request['force'] : false; - - // We don't support trashing for this type, error out. - if ( ! $force ) { - return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); - } - - $term = get_term( (int) $request['id'], $taxonomy ); - // Get default category id. - $default_category_id = absint( get_option( 'default_product_cat', 0 ) ); - - // Prevent deleting the default product category. - if ( $default_category_id === (int) $request['id'] ) { - return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Default product category cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); - } - - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( $term, $request ); - - $retval = wp_delete_term( $term->term_id, $term->taxonomy ); - if ( ! $retval ) { - return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); - } - - /** - * Fires after a single term is deleted via the REST API. - * - * @param WP_Term $term The deleted term. - * @param WP_REST_Response $response The response data. - * @param WP_REST_Request $request The request sent to the API. - */ - do_action( "woocommerce_rest_delete_{$taxonomy}", $term, $response, $request ); - - return $response; - } - - /** - * Prepare links for the request. - * - * @param object $term Term object. - * @param WP_REST_Request $request Full details about the request. - * @return array Links for the given term. - */ - protected function prepare_links( $term, $request ) { - $base = '/' . $this->namespace . '/' . $this->rest_base; - - if ( ! empty( $request['attribute_id'] ) ) { - $base = str_replace( '(?P[\d]+)', (int) $request['attribute_id'], $base ); - } - - $links = array( - 'self' => array( - 'href' => rest_url( trailingslashit( $base ) . $term->term_id ), - ), - 'collection' => array( - 'href' => rest_url( $base ), - ), - ); - - if ( $term->parent ) { - $parent_term = get_term( (int) $term->parent, $term->taxonomy ); - if ( $parent_term ) { - $links['up'] = array( - 'href' => rest_url( trailingslashit( $base ) . $parent_term->term_id ), - ); - } - } - - return $links; - } - - /** - * Update term meta fields. - * - * @param WP_Term $term Term object. - * @param WP_REST_Request $request Full details about the request. - * @return bool|WP_Error - */ - protected function update_term_meta_fields( $term, $request ) { - return true; - } - - /** - * Get the terms attached to a product. - * - * This is an alternative to `get_terms()` that uses `get_the_terms()` - * instead, which hits the object cache. There are a few things not - * supported, notably `include`, `exclude`. In `self::get_items()` these - * are instead treated as a full query. - * - * @param array $prepared_args Arguments for `get_terms()`. - * @param WP_REST_Request $request Full details about the request. - * @return array List of term objects. (Total count in `$this->total_terms`). - */ - protected function get_terms_for_product( $prepared_args, $request ) { - $taxonomy = $this->get_taxonomy( $request ); - - $query_result = get_the_terms( $prepared_args['product'], $taxonomy ); - if ( empty( $query_result ) ) { - $this->total_terms = 0; - return array(); - } - - // get_items() verifies that we don't have `include` set, and default. - // ordering is by `name`. - if ( ! in_array( $prepared_args['orderby'], array( 'name', 'none', 'include' ), true ) ) { - switch ( $prepared_args['orderby'] ) { - case 'id': - $this->sort_column = 'term_id'; - break; - case 'slug': - case 'term_group': - case 'description': - case 'count': - $this->sort_column = $prepared_args['orderby']; - break; - } - usort( $query_result, array( $this, 'compare_terms' ) ); - } - if ( strtolower( $prepared_args['order'] ) !== 'asc' ) { - $query_result = array_reverse( $query_result ); - } - - // Pagination. - $this->total_terms = count( $query_result ); - $query_result = array_slice( $query_result, $prepared_args['offset'], $prepared_args['number'] ); - - return $query_result; - } - - /** - * Comparison function for sorting terms by a column. - * - * Uses `$this->sort_column` to determine field to sort by. - * - * @param stdClass $left Term object. - * @param stdClass $right Term object. - * @return int <0 if left is higher "priority" than right, 0 if equal, >0 if right is higher "priority" than left. - */ - protected function compare_terms( $left, $right ) { - $col = $this->sort_column; - $left_val = $left->$col; - $right_val = $right->$col; - - if ( is_int( $left_val ) && is_int( $right_val ) ) { - return $left_val - $right_val; - } - - return strcmp( $left_val, $right_val ); - } - - /** - * Get the query params for collections - * - * @return array - */ - public function get_collection_params() { - $params = parent::get_collection_params(); - - $params['context']['default'] = 'view'; - - $params['exclude'] = array( - 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'default' => array(), - 'sanitize_callback' => 'wp_parse_id_list', - ); - $params['include'] = array( - 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'default' => array(), - 'sanitize_callback' => 'wp_parse_id_list', - ); - $params['offset'] = array( - 'description' => __( 'Offset the result set by a specific number of items. Applies to hierarchical taxonomies only.', 'woocommerce' ), - 'type' => 'integer', - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['order'] = array( - 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), - 'type' => 'string', - 'sanitize_callback' => 'sanitize_key', - 'default' => 'asc', - 'enum' => array( - 'asc', - 'desc', - ), - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['orderby'] = array( - 'description' => __( 'Sort collection by resource attribute.', 'woocommerce' ), - 'type' => 'string', - 'sanitize_callback' => 'sanitize_key', - 'default' => 'name', - 'enum' => array( - 'id', - 'include', - 'name', - 'slug', - 'term_group', - 'description', - 'count', - ), - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['hide_empty'] = array( - 'description' => __( 'Whether to hide resources not assigned to any products.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['parent'] = array( - 'description' => __( 'Limit result set to resources assigned to a specific parent. Applies to hierarchical taxonomies only.', 'woocommerce' ), - 'type' => 'integer', - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['product'] = array( - 'description' => __( 'Limit result set to resources assigned to a specific product.', 'woocommerce' ), - 'type' => 'integer', - 'default' => null, - 'validate_callback' => 'rest_validate_request_arg', - ); - $params['slug'] = array( - 'description' => __( 'Limit result set to resources with a specific slug.', 'woocommerce' ), - 'type' => 'string', - 'validate_callback' => 'rest_validate_request_arg', - ); - - return $params; - } - - /** - * Get taxonomy. - * - * @param WP_REST_Request $request Full details about the request. - * @return int|WP_Error - */ - protected function get_taxonomy( $request ) { - $attribute_id = $request['attribute_id']; - - if ( empty( $attribute_id ) ) { - return $this->taxonomy; - } - - if ( isset( $this->taxonomies_by_id[ $attribute_id ] ) ) { - return $this->taxonomies_by_id[ $attribute_id ]; - } - - $taxonomy = WC()->call_function( 'wc_attribute_taxonomy_name_by_id', (int) $request['attribute_id'] ); - if ( ! empty( $taxonomy ) ) { - $this->taxonomy = $taxonomy; - $this->taxonomies_by_id[ $attribute_id ] = $taxonomy; - } - - return $taxonomy; - } -} diff --git a/includes/rest-api/Package.php b/includes/rest-api/Package.php deleted file mode 100644 index 4d67524e719..00000000000 --- a/includes/rest-api/Package.php +++ /dev/null @@ -1,60 +0,0 @@ -init() - */ - public static function init() { - wc_deprecated_function( 'Automattic\WooCommerce\RestApi\Server::instance()->init()', '4.5.0' ); - \Automattic\WooCommerce\RestApi\Server::instance()->init(); - } - - /** - * Return the version of the package. - * - * @deprecated since 4.5.0. This tracks WooCommerce version now. - * @return string - */ - public static function get_version() { - wc_deprecated_function( 'WC()->version', '4.5.0' ); - return WC()->version; - } - - /** - * Return the path to the package. - * - * @deprecated since 4.5.0. Directly call Automattic\WooCommerce\RestApi\Server::get_path() - * @return string - */ - public static function get_path() { - wc_deprecated_function( 'Automattic\WooCommerce\RestApi\Server::get_path()', '4.5.0' ); - return \Automattic\WooCommerce\RestApi\Server::get_path(); - } -} diff --git a/includes/rest-api/Server.php b/includes/rest-api/Server.php deleted file mode 100644 index ab69619abfd..00000000000 --- a/includes/rest-api/Server.php +++ /dev/null @@ -1,190 +0,0 @@ -get_rest_namespaces() as $namespace => $controllers ) { - foreach ( $controllers as $controller_name => $controller_class ) { - $this->controllers[ $namespace ][ $controller_name ] = new $controller_class(); - $this->controllers[ $namespace ][ $controller_name ]->register_routes(); - } - } - } - - /** - * Get API namespaces - new namespaces should be registered here. - * - * @return array List of Namespaces and Main controller classes. - */ - protected function get_rest_namespaces() { - return apply_filters( - 'woocommerce_rest_api_get_rest_namespaces', - array( - 'wc/v1' => $this->get_v1_controllers(), - 'wc/v2' => $this->get_v2_controllers(), - 'wc/v3' => $this->get_v3_controllers(), - ) - ); - } - - /** - * List of controllers in the wc/v1 namespace. - * - * @return array - */ - protected function get_v1_controllers() { - return array( - 'coupons' => 'WC_REST_Coupons_V1_Controller', - 'customer-downloads' => 'WC_REST_Customer_Downloads_V1_Controller', - 'customers' => 'WC_REST_Customers_V1_Controller', - 'order-notes' => 'WC_REST_Order_Notes_V1_Controller', - 'order-refunds' => 'WC_REST_Order_Refunds_V1_Controller', - 'orders' => 'WC_REST_Orders_V1_Controller', - 'product-attribute-terms' => 'WC_REST_Product_Attribute_Terms_V1_Controller', - 'product-attributes' => 'WC_REST_Product_Attributes_V1_Controller', - 'product-categories' => 'WC_REST_Product_Categories_V1_Controller', - 'product-reviews' => 'WC_REST_Product_Reviews_V1_Controller', - 'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_V1_Controller', - 'product-tags' => 'WC_REST_Product_Tags_V1_Controller', - 'products' => 'WC_REST_Products_V1_Controller', - 'reports-sales' => 'WC_REST_Report_Sales_V1_Controller', - 'reports-top-sellers' => 'WC_REST_Report_Top_Sellers_V1_Controller', - 'reports' => 'WC_REST_Reports_V1_Controller', - 'tax-classes' => 'WC_REST_Tax_Classes_V1_Controller', - 'taxes' => 'WC_REST_Taxes_V1_Controller', - 'webhooks' => 'WC_REST_Webhooks_V1_Controller', - 'webhook-deliveries' => 'WC_REST_Webhook_Deliveries_V1_Controller', - ); - } - - /** - * List of controllers in the wc/v2 namespace. - * - * @return array - */ - protected function get_v2_controllers() { - return array( - 'coupons' => 'WC_REST_Coupons_V2_Controller', - 'customer-downloads' => 'WC_REST_Customer_Downloads_V2_Controller', - 'customers' => 'WC_REST_Customers_V2_Controller', - 'network-orders' => 'WC_REST_Network_Orders_V2_Controller', - 'order-notes' => 'WC_REST_Order_Notes_V2_Controller', - 'order-refunds' => 'WC_REST_Order_Refunds_V2_Controller', - 'orders' => 'WC_REST_Orders_V2_Controller', - 'product-attribute-terms' => 'WC_REST_Product_Attribute_Terms_V2_Controller', - 'product-attributes' => 'WC_REST_Product_Attributes_V2_Controller', - 'product-categories' => 'WC_REST_Product_Categories_V2_Controller', - 'product-reviews' => 'WC_REST_Product_Reviews_V2_Controller', - 'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_V2_Controller', - 'product-tags' => 'WC_REST_Product_Tags_V2_Controller', - 'products' => 'WC_REST_Products_V2_Controller', - 'product-variations' => 'WC_REST_Product_Variations_V2_Controller', - 'reports-sales' => 'WC_REST_Report_Sales_V2_Controller', - 'reports-top-sellers' => 'WC_REST_Report_Top_Sellers_V2_Controller', - 'reports' => 'WC_REST_Reports_V2_Controller', - 'settings' => 'WC_REST_Settings_V2_Controller', - 'settings-options' => 'WC_REST_Setting_Options_V2_Controller', - 'shipping-zones' => 'WC_REST_Shipping_Zones_V2_Controller', - 'shipping-zone-locations' => 'WC_REST_Shipping_Zone_Locations_V2_Controller', - 'shipping-zone-methods' => 'WC_REST_Shipping_Zone_Methods_V2_Controller', - 'tax-classes' => 'WC_REST_Tax_Classes_V2_Controller', - 'taxes' => 'WC_REST_Taxes_V2_Controller', - 'webhooks' => 'WC_REST_Webhooks_V2_Controller', - 'webhook-deliveries' => 'WC_REST_Webhook_Deliveries_V2_Controller', - 'system-status' => 'WC_REST_System_Status_V2_Controller', - 'system-status-tools' => 'WC_REST_System_Status_Tools_V2_Controller', - 'shipping-methods' => 'WC_REST_Shipping_Methods_V2_Controller', - 'payment-gateways' => 'WC_REST_Payment_Gateways_V2_Controller', - ); - } - - /** - * List of controllers in the wc/v3 namespace. - * - * @return array - */ - protected function get_v3_controllers() { - return array( - 'coupons' => 'WC_REST_Coupons_Controller', - 'customer-downloads' => 'WC_REST_Customer_Downloads_Controller', - 'customers' => 'WC_REST_Customers_Controller', - 'network-orders' => 'WC_REST_Network_Orders_Controller', - 'order-notes' => 'WC_REST_Order_Notes_Controller', - 'order-refunds' => 'WC_REST_Order_Refunds_Controller', - 'orders' => 'WC_REST_Orders_Controller', - 'product-attribute-terms' => 'WC_REST_Product_Attribute_Terms_Controller', - 'product-attributes' => 'WC_REST_Product_Attributes_Controller', - 'product-categories' => 'WC_REST_Product_Categories_Controller', - 'product-reviews' => 'WC_REST_Product_Reviews_Controller', - 'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_Controller', - 'product-tags' => 'WC_REST_Product_Tags_Controller', - 'products' => 'WC_REST_Products_Controller', - 'product-variations' => 'WC_REST_Product_Variations_Controller', - 'reports-sales' => 'WC_REST_Report_Sales_Controller', - 'reports-top-sellers' => 'WC_REST_Report_Top_Sellers_Controller', - 'reports-orders-totals' => 'WC_REST_Report_Orders_Totals_Controller', - 'reports-products-totals' => 'WC_REST_Report_Products_Totals_Controller', - 'reports-customers-totals' => 'WC_REST_Report_Customers_Totals_Controller', - 'reports-coupons-totals' => 'WC_REST_Report_Coupons_Totals_Controller', - 'reports-reviews-totals' => 'WC_REST_Report_Reviews_Totals_Controller', - 'reports' => 'WC_REST_Reports_Controller', - 'settings' => 'WC_REST_Settings_Controller', - 'settings-options' => 'WC_REST_Setting_Options_Controller', - 'shipping-zones' => 'WC_REST_Shipping_Zones_Controller', - 'shipping-zone-locations' => 'WC_REST_Shipping_Zone_Locations_Controller', - 'shipping-zone-methods' => 'WC_REST_Shipping_Zone_Methods_Controller', - 'tax-classes' => 'WC_REST_Tax_Classes_Controller', - 'taxes' => 'WC_REST_Taxes_Controller', - 'webhooks' => 'WC_REST_Webhooks_Controller', - 'system-status' => 'WC_REST_System_Status_Controller', - 'system-status-tools' => 'WC_REST_System_Status_Tools_Controller', - 'shipping-methods' => 'WC_REST_Shipping_Methods_Controller', - 'payment-gateways' => 'WC_REST_Payment_Gateways_Controller', - 'data' => 'WC_REST_Data_Controller', - 'data-continents' => 'WC_REST_Data_Continents_Controller', - 'data-countries' => 'WC_REST_Data_Countries_Controller', - 'data-currencies' => 'WC_REST_Data_Currencies_Controller', - ); - } - - /** - * Return the path to the package. - * - * @return string - */ - public static function get_path() { - return dirname( __DIR__ ); - } -} diff --git a/includes/shipping/free-shipping/class-wc-shipping-free-shipping.php b/includes/shipping/free-shipping/class-wc-shipping-free-shipping.php deleted file mode 100644 index fc630e67a61..00000000000 --- a/includes/shipping/free-shipping/class-wc-shipping-free-shipping.php +++ /dev/null @@ -1,245 +0,0 @@ -id = 'free_shipping'; - $this->instance_id = absint( $instance_id ); - $this->method_title = __( 'Free shipping', 'woocommerce' ); - $this->method_description = __( 'Free shipping is a special method which can be triggered with coupons and minimum spends.', 'woocommerce' ); - $this->supports = array( - 'shipping-zones', - 'instance-settings', - 'instance-settings-modal', - ); - - $this->init(); - } - - /** - * Initialize free shipping. - */ - public function init() { - // Load the settings. - $this->init_form_fields(); - $this->init_settings(); - - // Define user set variables. - $this->title = $this->get_option( 'title' ); - $this->min_amount = $this->get_option( 'min_amount', 0 ); - $this->requires = $this->get_option( 'requires' ); - $this->ignore_discounts = $this->get_option( 'ignore_discounts' ); - - // Actions. - add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); - add_action( 'admin_footer', array( 'WC_Shipping_Free_Shipping', 'enqueue_admin_js' ), 10 ); // Priority needs to be higher than wc_print_js (25). - } - - /** - * Init form fields. - */ - public function init_form_fields() { - $this->instance_form_fields = array( - 'title' => array( - 'title' => __( 'Title', 'woocommerce' ), - 'type' => 'text', - 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), - 'default' => $this->method_title, - 'desc_tip' => true, - ), - 'requires' => array( - 'title' => __( 'Free shipping requires...', 'woocommerce' ), - 'type' => 'select', - 'class' => 'wc-enhanced-select', - 'default' => '', - 'options' => array( - '' => __( 'N/A', 'woocommerce' ), - 'coupon' => __( 'A valid free shipping coupon', 'woocommerce' ), - 'min_amount' => __( 'A minimum order amount', 'woocommerce' ), - 'either' => __( 'A minimum order amount OR a coupon', 'woocommerce' ), - 'both' => __( 'A minimum order amount AND a coupon', 'woocommerce' ), - ), - ), - 'min_amount' => array( - 'title' => __( 'Minimum order amount', 'woocommerce' ), - 'type' => 'price', - 'placeholder' => wc_format_localized_price( 0 ), - 'description' => __( 'Users will need to spend this amount to get free shipping (if enabled above).', 'woocommerce' ), - 'default' => '0', - 'desc_tip' => true, - ), - 'ignore_discounts' => array( - 'title' => __( 'Coupons discounts', 'woocommerce' ), - 'label' => __( 'Apply minimum order rule before coupon discount', 'woocommerce' ), - 'type' => 'checkbox', - 'description' => __( 'If checked, free shipping would be available based on pre-discount order amount.', 'woocommerce' ), - 'default' => 'no', - 'desc_tip' => true, - ), - ); - } - - /** - * Get setting form fields for instances of this shipping method within zones. - * - * @return array - */ - public function get_instance_form_fields() { - return parent::get_instance_form_fields(); - } - - /** - * See if free shipping is available based on the package and cart. - * - * @param array $package Shipping package. - * @return bool - */ - public function is_available( $package ) { - $has_coupon = false; - $has_met_min_amount = false; - - if ( in_array( $this->requires, array( 'coupon', 'either', 'both' ), true ) ) { - $coupons = WC()->cart->get_coupons(); - - if ( $coupons ) { - foreach ( $coupons as $code => $coupon ) { - if ( $coupon->is_valid() && $coupon->get_free_shipping() ) { - $has_coupon = true; - break; - } - } - } - } - - if ( in_array( $this->requires, array( 'min_amount', 'either', 'both' ), true ) ) { - $total = WC()->cart->get_displayed_subtotal(); - - if ( WC()->cart->display_prices_including_tax() ) { - $total = $total - WC()->cart->get_discount_tax(); - } - - if ( 'no' === $this->ignore_discounts ) { - $total = $total - WC()->cart->get_discount_total(); - } - - $total = NumberUtil::round( $total, wc_get_price_decimals() ); - - if ( $total >= $this->min_amount ) { - $has_met_min_amount = true; - } - } - - switch ( $this->requires ) { - case 'min_amount': - $is_available = $has_met_min_amount; - break; - case 'coupon': - $is_available = $has_coupon; - break; - case 'both': - $is_available = $has_met_min_amount && $has_coupon; - break; - case 'either': - $is_available = $has_met_min_amount || $has_coupon; - break; - default: - $is_available = true; - break; - } - - return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', $is_available, $package, $this ); - } - - /** - * Called to calculate shipping rates for this method. Rates can be added using the add_rate() method. - * - * @uses WC_Shipping_Method::add_rate() - * - * @param array $package Shipping package. - */ - public function calculate_shipping( $package = array() ) { - $this->add_rate( - array( - 'label' => $this->title, - 'cost' => 0, - 'taxes' => false, - 'package' => $package, - ) - ); - } - - /** - * Enqueue JS to handle free shipping options. - * - * Static so that's enqueued only once. - */ - public static function enqueue_admin_js() { - wc_enqueue_js( - "jQuery( function( $ ) { - function wcFreeShippingShowHideMinAmountField( el ) { - var form = $( el ).closest( 'form' ); - var minAmountField = $( '#woocommerce_free_shipping_min_amount', form ).closest( 'tr' ); - var ignoreDiscountField = $( '#woocommerce_free_shipping_ignore_discounts', form ).closest( 'tr' ); - if ( 'coupon' === $( el ).val() || '' === $( el ).val() ) { - minAmountField.hide(); - ignoreDiscountField.hide(); - } else { - minAmountField.show(); - ignoreDiscountField.show(); - } - } - - $( document.body ).on( 'change', '#woocommerce_free_shipping_requires', function() { - wcFreeShippingShowHideMinAmountField( this ); - }); - - // Change while load. - $( '#woocommerce_free_shipping_requires' ).change(); - $( document.body ).on( 'wc_backbone_modal_loaded', function( evt, target ) { - if ( 'wc-modal-shipping-method-settings' === target ) { - wcFreeShippingShowHideMinAmountField( $( '#wc-backbone-modal-dialog #woocommerce_free_shipping_requires', evt.currentTarget ) ); - } - } ); - });" - ); - } -} diff --git a/includes/shipping/legacy-flat-rate/class-wc-shipping-legacy-flat-rate.php b/includes/shipping/legacy-flat-rate/class-wc-shipping-legacy-flat-rate.php deleted file mode 100644 index 1f579c49613..00000000000 --- a/includes/shipping/legacy-flat-rate/class-wc-shipping-legacy-flat-rate.php +++ /dev/null @@ -1,411 +0,0 @@ -id = 'legacy_flat_rate'; - $this->method_title = __( 'Flat rate (legacy)', 'woocommerce' ); - /* translators: %s: Admin shipping settings URL */ - $this->method_description = '' . sprintf( __( 'This method is deprecated in 2.6.0 and will be removed in future versions - we recommend disabling it and instead setting up a new rate within your Shipping zones.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ) . ''; - $this->init(); - - add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); - add_action( 'woocommerce_flat_rate_shipping_add_rate', array( $this, 'calculate_extra_shipping' ), 10, 2 ); - } - - /** - * Process and redirect if disabled. - */ - public function process_admin_options() { - parent::process_admin_options(); - - if ( 'no' === $this->settings['enabled'] ) { - wp_redirect( admin_url( 'admin.php?page=wc-settings&tab=shipping§ion=options' ) ); - exit; - } - } - - /** - * Return the name of the option in the WP DB. - * - * @since 2.6.0 - * @return string - */ - public function get_option_key() { - return $this->plugin_id . 'flat_rate_settings'; - } - - /** - * Init function. - */ - public function init() { - // Load the settings. - $this->init_form_fields(); - $this->init_settings(); - - // Define user set variables. - $this->title = $this->get_option( 'title' ); - $this->availability = $this->get_option( 'availability' ); - $this->countries = $this->get_option( 'countries' ); - $this->tax_status = $this->get_option( 'tax_status' ); - $this->cost = $this->get_option( 'cost' ); - $this->type = $this->get_option( 'type', 'class' ); - $this->options = $this->get_option( 'options', false ); // @deprecated 2.4.0 - } - - /** - * Initialise Settings Form Fields. - */ - public function init_form_fields() { - $this->form_fields = include __DIR__ . '/includes/settings-flat-rate.php'; - } - - /** - * Evaluate a cost from a sum/string. - * - * @param string $sum Sum to evaluate. - * @param array $args Arguments. - * @return string - */ - protected function evaluate_cost( $sum, $args = array() ) { - include_once WC()->plugin_path() . '/includes/libraries/class-wc-eval-math.php'; - - $locale = localeconv(); - $decimals = array( wc_get_price_decimal_separator(), $locale['decimal_point'], $locale['mon_decimal_point'] ); - - $this->fee_cost = $args['cost']; - - // Expand shortcodes. - add_shortcode( 'fee', array( $this, 'fee' ) ); - - $sum = do_shortcode( - str_replace( - array( - '[qty]', - '[cost]', - ), - array( - $args['qty'], - $args['cost'], - ), - $sum - ) - ); - - remove_shortcode( 'fee', array( $this, 'fee' ) ); - - // Remove whitespace from string. - $sum = preg_replace( '/\s+/', '', $sum ); - - // Remove locale from string. - $sum = str_replace( $decimals, '.', $sum ); - - // Trim invalid start/end characters. - $sum = rtrim( ltrim( $sum, "\t\n\r\0\x0B+*/" ), "\t\n\r\0\x0B+-*/" ); - - // Do the math. - return $sum ? WC_Eval_Math::evaluate( $sum ) : 0; - } - - /** - * Work out fee (shortcode). - * - * @param array $atts Shortcode attributes. - * @return string - */ - public function fee( $atts ) { - $atts = shortcode_atts( - array( - 'percent' => '', - 'min_fee' => '', - ), - $atts, - 'fee' - ); - - $calculated_fee = 0; - - if ( $atts['percent'] ) { - $calculated_fee = $this->fee_cost * ( floatval( $atts['percent'] ) / 100 ); - } - - if ( $atts['min_fee'] && $calculated_fee < $atts['min_fee'] ) { - $calculated_fee = $atts['min_fee']; - } - - return $calculated_fee; - } - - /** - * Calculate shipping. - * - * @param array $package (default: array()). - */ - public function calculate_shipping( $package = array() ) { - $rate = array( - 'id' => $this->id, - 'label' => $this->title, - 'cost' => 0, - 'package' => $package, - ); - - // Calculate the costs. - $has_costs = false; // True when a cost is set. False if all costs are blank strings. - $cost = $this->get_option( 'cost' ); - - if ( '' !== $cost ) { - $has_costs = true; - $rate['cost'] = $this->evaluate_cost( - $cost, - array( - 'qty' => $this->get_package_item_qty( $package ), - 'cost' => $package['contents_cost'], - ) - ); - } - - // Add shipping class costs. - $found_shipping_classes = $this->find_shipping_classes( $package ); - $highest_class_cost = 0; - - foreach ( $found_shipping_classes as $shipping_class => $products ) { - // Also handles BW compatibility when slugs were used instead of ids. - $shipping_class_term = get_term_by( 'slug', $shipping_class, 'product_shipping_class' ); - $class_cost_string = $shipping_class_term && $shipping_class_term->term_id ? $this->get_option( 'class_cost_' . $shipping_class_term->term_id, $this->get_option( 'class_cost_' . $shipping_class, '' ) ) : $this->get_option( 'no_class_cost', '' ); - - if ( '' === $class_cost_string ) { - continue; - } - - $has_costs = true; - $class_cost = $this->evaluate_cost( - $class_cost_string, - array( - 'qty' => array_sum( wp_list_pluck( $products, 'quantity' ) ), - 'cost' => array_sum( wp_list_pluck( $products, 'line_total' ) ), - ) - ); - - if ( 'class' === $this->type ) { - $rate['cost'] += $class_cost; - } else { - $highest_class_cost = $class_cost > $highest_class_cost ? $class_cost : $highest_class_cost; - } - } - - if ( 'order' === $this->type && $highest_class_cost ) { - $rate['cost'] += $highest_class_cost; - } - - $rate['package'] = $package; - - // Add the rate. - if ( $has_costs ) { - $this->add_rate( $rate ); - } - - /** - * Developers can add additional flat rates based on this one via this action since @version 2.4. - * - * Previously there were (overly complex) options to add additional rates however this was not user. - * friendly and goes against what Flat Rate Shipping was originally intended for. - * - * This example shows how you can add an extra rate based on this flat rate via custom function: - * - * add_action( 'woocommerce_flat_rate_shipping_add_rate', 'add_another_custom_flat_rate', 10, 2 ); - * - * function add_another_custom_flat_rate( $method, $rate ) { - * $new_rate = $rate; - * $new_rate['id'] .= ':' . 'custom_rate_name'; // Append a custom ID. - * $new_rate['label'] = 'Rushed Shipping'; // Rename to 'Rushed Shipping'. - * $new_rate['cost'] += 2; // Add $2 to the cost. - * - * // Add it to WC. - * $method->add_rate( $new_rate ); - * }. - */ - do_action( 'woocommerce_flat_rate_shipping_add_rate', $this, $rate ); - } - - /** - * Get items in package. - * - * @param array $package Package information. - * @return int - */ - public function get_package_item_qty( $package ) { - $total_quantity = 0; - foreach ( $package['contents'] as $item_id => $values ) { - if ( $values['quantity'] > 0 && $values['data']->needs_shipping() ) { - $total_quantity += $values['quantity']; - } - } - return $total_quantity; - } - - /** - * Finds and returns shipping classes and the products with said class. - * - * @param mixed $package Package information. - * @return array - */ - public function find_shipping_classes( $package ) { - $found_shipping_classes = array(); - - foreach ( $package['contents'] as $item_id => $values ) { - if ( $values['data']->needs_shipping() ) { - $found_class = $values['data']->get_shipping_class(); - - if ( ! isset( $found_shipping_classes[ $found_class ] ) ) { - $found_shipping_classes[ $found_class ] = array(); - } - - $found_shipping_classes[ $found_class ][ $item_id ] = $values; - } - } - - return $found_shipping_classes; - } - - /** - * Adds extra calculated flat rates. - * - * @deprecated 2.4.0 - * - * Additional rates defined like this: - * Option Name | Additional Cost [+- Percents%] | Per Cost Type (order, class, or item). - * - * @param null $method Deprecated. - * @param array $rate Rate information. - */ - public function calculate_extra_shipping( $method, $rate ) { - if ( $this->options ) { - $options = array_filter( (array) explode( "\n", $this->options ) ); - - foreach ( $options as $option ) { - $this_option = array_map( 'trim', explode( WC_DELIMITER, $option ) ); - if ( count( $this_option ) !== 3 ) { - continue; - } - $extra_rate = $rate; - $extra_rate['id'] = $this->id . ':' . urldecode( sanitize_title( $this_option[0] ) ); - $extra_rate['label'] = $this_option[0]; - $extra_cost = $this->get_extra_cost( $this_option[1], $this_option[2], $rate['package'] ); - if ( is_array( $extra_rate['cost'] ) ) { - $extra_rate['cost']['order'] = $extra_rate['cost']['order'] + $extra_cost; - } else { - $extra_rate['cost'] += $extra_cost; - } - - $this->add_rate( $extra_rate ); - } - } - } - - /** - * Calculate the percentage adjustment for each shipping rate. - * - * @deprecated 2.4.0 - * @param float $cost Cost. - * @param float $percent_adjustment Percent adjusment. - * @param string $percent_operator Percent operator. - * @param float $base_price Base price. - * @return float - */ - public function calc_percentage_adjustment( $cost, $percent_adjustment, $percent_operator, $base_price ) { - if ( '+' === $percent_operator ) { - $cost += $percent_adjustment * $base_price; - } else { - $cost -= $percent_adjustment * $base_price; - } - return $cost; - } - - /** - * Get extra cost. - * - * @deprecated 2.4.0 - * @param string $cost_string Cost string. - * @param string $type Type. - * @param array $package Package information. - * @return float - */ - public function get_extra_cost( $cost_string, $type, $package ) { - $cost = $cost_string; - $cost_percent = false; - // @codingStandardsIgnoreStart - $pattern = - '/' . // Start regex. - '(\d+\.?\d*)' . // Capture digits, optionally capture a `.` and more digits. - '\s*' . // Match whitespace. - '(\+|-)' . // Capture the operand. - '\s*' . // Match whitespace. - '(\d+\.?\d*)' . // Capture digits, optionally capture a `.` and more digits. - '\%/'; // Match the percent sign & end regex. - // @codingStandardsIgnoreEnd - if ( preg_match( $pattern, $cost_string, $this_cost_matches ) ) { - $cost_operator = $this_cost_matches[2]; - $cost_percent = $this_cost_matches[3] / 100; - $cost = $this_cost_matches[1]; - } - switch ( $type ) { - case 'class': - $cost = $cost * count( $this->find_shipping_classes( $package ) ); - break; - case 'item': - $cost = $cost * $this->get_package_item_qty( $package ); - break; - } - if ( $cost_percent ) { - switch ( $type ) { - case 'class': - $shipping_classes = $this->find_shipping_classes( $package ); - foreach ( $shipping_classes as $shipping_class => $items ) { - foreach ( $items as $item_id => $values ) { - $cost = $this->calc_percentage_adjustment( $cost, $cost_percent, $cost_operator, $values['line_total'] ); - } - } - break; - case 'item': - foreach ( $package['contents'] as $item_id => $values ) { - if ( $values['data']->needs_shipping() ) { - $cost = $this->calc_percentage_adjustment( $cost, $cost_percent, $cost_operator, $values['line_total'] ); - } - } - break; - case 'order': - $cost = $this->calc_percentage_adjustment( $cost, $cost_percent, $cost_operator, $package['contents_cost'] ); - break; - } - } - return $cost; - } -} diff --git a/includes/shortcodes/class-wc-shortcode-products.php b/includes/shortcodes/class-wc-shortcode-products.php deleted file mode 100644 index d0d089f905a..00000000000 --- a/includes/shortcodes/class-wc-shortcode-products.php +++ /dev/null @@ -1,703 +0,0 @@ -type = $type; - $this->attributes = $this->parse_attributes( $attributes ); - $this->query_args = $this->parse_query_args(); - } - - /** - * Get shortcode attributes. - * - * @since 3.2.0 - * @return array - */ - public function get_attributes() { - return $this->attributes; - } - - /** - * Get query args. - * - * @since 3.2.0 - * @return array - */ - public function get_query_args() { - return $this->query_args; - } - - /** - * Get shortcode type. - * - * @since 3.2.0 - * @return array - */ - public function get_type() { - return $this->type; - } - - /** - * Get shortcode content. - * - * @since 3.2.0 - * @return string - */ - public function get_content() { - return $this->product_loop(); - } - - /** - * Parse attributes. - * - * @since 3.2.0 - * @param array $attributes Shortcode attributes. - * @return array - */ - protected function parse_attributes( $attributes ) { - $attributes = $this->parse_legacy_attributes( $attributes ); - - $attributes = shortcode_atts( - array( - 'limit' => '-1', // Results limit. - 'columns' => '', // Number of columns. - 'rows' => '', // Number of rows. If defined, limit will be ignored. - 'orderby' => '', // menu_order, title, date, rand, price, popularity, rating, or id. - 'order' => '', // ASC or DESC. - 'ids' => '', // Comma separated IDs. - 'skus' => '', // Comma separated SKUs. - 'category' => '', // Comma separated category slugs or ids. - 'cat_operator' => 'IN', // Operator to compare categories. Possible values are 'IN', 'NOT IN', 'AND'. - 'attribute' => '', // Single attribute slug. - 'terms' => '', // Comma separated term slugs or ids. - 'terms_operator' => 'IN', // Operator to compare terms. Possible values are 'IN', 'NOT IN', 'AND'. - 'tag' => '', // Comma separated tag slugs. - 'tag_operator' => 'IN', // Operator to compare tags. Possible values are 'IN', 'NOT IN', 'AND'. - 'visibility' => 'visible', // Product visibility setting. Possible values are 'visible', 'catalog', 'search', 'hidden'. - 'class' => '', // HTML class. - 'page' => 1, // Page for pagination. - 'paginate' => false, // Should results be paginated. - 'cache' => true, // Should shortcode output be cached. - ), - $attributes, - $this->type - ); - - if ( ! absint( $attributes['columns'] ) ) { - $attributes['columns'] = wc_get_default_products_per_row(); - } - - return $attributes; - } - - /** - * Parse legacy attributes. - * - * @since 3.2.0 - * @param array $attributes Attributes. - * @return array - */ - protected function parse_legacy_attributes( $attributes ) { - $mapping = array( - 'per_page' => 'limit', - 'operator' => 'cat_operator', - 'filter' => 'terms', - ); - - foreach ( $mapping as $old => $new ) { - if ( isset( $attributes[ $old ] ) ) { - $attributes[ $new ] = $attributes[ $old ]; - unset( $attributes[ $old ] ); - } - } - - return $attributes; - } - - /** - * Parse query args. - * - * @since 3.2.0 - * @return array - */ - protected function parse_query_args() { - $query_args = array( - 'post_type' => 'product', - 'post_status' => 'publish', - 'ignore_sticky_posts' => true, - 'no_found_rows' => false === wc_string_to_bool( $this->attributes['paginate'] ), - 'orderby' => empty( $_GET['orderby'] ) ? $this->attributes['orderby'] : wc_clean( wp_unslash( $_GET['orderby'] ) ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended - ); - - $orderby_value = explode( '-', $query_args['orderby'] ); - $orderby = esc_attr( $orderby_value[0] ); - $order = ! empty( $orderby_value[1] ) ? $orderby_value[1] : strtoupper( $this->attributes['order'] ); - $query_args['orderby'] = $orderby; - $query_args['order'] = $order; - - if ( wc_string_to_bool( $this->attributes['paginate'] ) ) { - $this->attributes['page'] = absint( empty( $_GET['product-page'] ) ? 1 : $_GET['product-page'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - } - - if ( ! empty( $this->attributes['rows'] ) ) { - $this->attributes['limit'] = $this->attributes['columns'] * $this->attributes['rows']; - } - - $ordering_args = WC()->query->get_catalog_ordering_args( $query_args['orderby'], $query_args['order'] ); - $query_args['orderby'] = $ordering_args['orderby']; - $query_args['order'] = $ordering_args['order']; - if ( $ordering_args['meta_key'] ) { - $query_args['meta_key'] = $ordering_args['meta_key']; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key - } - $query_args['posts_per_page'] = intval( $this->attributes['limit'] ); - if ( 1 < $this->attributes['page'] ) { - $query_args['paged'] = absint( $this->attributes['page'] ); - } - $query_args['meta_query'] = WC()->query->get_meta_query(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - $query_args['tax_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query - - // Visibility. - $this->set_visibility_query_args( $query_args ); - - // SKUs. - $this->set_skus_query_args( $query_args ); - - // IDs. - $this->set_ids_query_args( $query_args ); - - // Set specific types query args. - if ( method_exists( $this, "set_{$this->type}_query_args" ) ) { - $this->{"set_{$this->type}_query_args"}( $query_args ); - } - - // Attributes. - $this->set_attributes_query_args( $query_args ); - - // Categories. - $this->set_categories_query_args( $query_args ); - - // Tags. - $this->set_tags_query_args( $query_args ); - - $query_args = apply_filters( 'woocommerce_shortcode_products_query', $query_args, $this->attributes, $this->type ); - - // Always query only IDs. - $query_args['fields'] = 'ids'; - - return $query_args; - } - - /** - * Set skus query args. - * - * @since 3.2.0 - * @param array $query_args Query args. - */ - protected function set_skus_query_args( &$query_args ) { - if ( ! empty( $this->attributes['skus'] ) ) { - $skus = array_map( 'trim', explode( ',', $this->attributes['skus'] ) ); - $query_args['meta_query'][] = array( - 'key' => '_sku', - 'value' => 1 === count( $skus ) ? $skus[0] : $skus, - 'compare' => 1 === count( $skus ) ? '=' : 'IN', - ); - } - } - - /** - * Set ids query args. - * - * @since 3.2.0 - * @param array $query_args Query args. - */ - protected function set_ids_query_args( &$query_args ) { - if ( ! empty( $this->attributes['ids'] ) ) { - $ids = array_map( 'trim', explode( ',', $this->attributes['ids'] ) ); - - if ( 1 === count( $ids ) ) { - $query_args['p'] = $ids[0]; - } else { - $query_args['post__in'] = $ids; - } - } - } - - /** - * Set attributes query args. - * - * @since 3.2.0 - * @param array $query_args Query args. - */ - protected function set_attributes_query_args( &$query_args ) { - if ( ! empty( $this->attributes['attribute'] ) || ! empty( $this->attributes['terms'] ) ) { - $taxonomy = strstr( $this->attributes['attribute'], 'pa_' ) ? sanitize_title( $this->attributes['attribute'] ) : 'pa_' . sanitize_title( $this->attributes['attribute'] ); - $terms = $this->attributes['terms'] ? array_map( 'sanitize_title', explode( ',', $this->attributes['terms'] ) ) : array(); - $field = 'slug'; - - if ( $terms && is_numeric( $terms[0] ) ) { - $field = 'term_id'; - $terms = array_map( 'absint', $terms ); - // Check numeric slugs. - foreach ( $terms as $term ) { - $the_term = get_term_by( 'slug', $term, $taxonomy ); - if ( false !== $the_term ) { - $terms[] = $the_term->term_id; - } - } - } - - // If no terms were specified get all products that are in the attribute taxonomy. - if ( ! $terms ) { - $terms = get_terms( - array( - 'taxonomy' => $taxonomy, - 'fields' => 'ids', - ) - ); - $field = 'term_id'; - } - - // We always need to search based on the slug as well, this is to accommodate numeric slugs. - $query_args['tax_query'][] = array( - 'taxonomy' => $taxonomy, - 'terms' => $terms, - 'field' => $field, - 'operator' => $this->attributes['terms_operator'], - ); - } - } - - /** - * Set categories query args. - * - * @since 3.2.0 - * @param array $query_args Query args. - */ - protected function set_categories_query_args( &$query_args ) { - if ( ! empty( $this->attributes['category'] ) ) { - $categories = array_map( 'sanitize_title', explode( ',', $this->attributes['category'] ) ); - $field = 'slug'; - - if ( is_numeric( $categories[0] ) ) { - $field = 'term_id'; - $categories = array_map( 'absint', $categories ); - // Check numeric slugs. - foreach ( $categories as $cat ) { - $the_cat = get_term_by( 'slug', $cat, 'product_cat' ); - if ( false !== $the_cat ) { - $categories[] = $the_cat->term_id; - } - } - } - - $query_args['tax_query'][] = array( - 'taxonomy' => 'product_cat', - 'terms' => $categories, - 'field' => $field, - 'operator' => $this->attributes['cat_operator'], - - /* - * When cat_operator is AND, the children categories should be excluded, - * as only products belonging to all the children categories would be selected. - */ - 'include_children' => 'AND' === $this->attributes['cat_operator'] ? false : true, - ); - } - } - - /** - * Set tags query args. - * - * @since 3.3.0 - * @param array $query_args Query args. - */ - protected function set_tags_query_args( &$query_args ) { - if ( ! empty( $this->attributes['tag'] ) ) { - $query_args['tax_query'][] = array( - 'taxonomy' => 'product_tag', - 'terms' => array_map( 'sanitize_title', explode( ',', $this->attributes['tag'] ) ), - 'field' => 'slug', - 'operator' => $this->attributes['tag_operator'], - ); - } - } - - /** - * Set sale products query args. - * - * @since 3.2.0 - * @param array $query_args Query args. - */ - protected function set_sale_products_query_args( &$query_args ) { - $query_args['post__in'] = array_merge( array( 0 ), wc_get_product_ids_on_sale() ); - } - - /** - * Set best selling products query args. - * - * @since 3.2.0 - * @param array $query_args Query args. - */ - protected function set_best_selling_products_query_args( &$query_args ) { - $query_args['meta_key'] = 'total_sales'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key - $query_args['order'] = 'DESC'; - $query_args['orderby'] = 'meta_value_num'; - } - - /** - * Set top rated products query args. - * - * @since 3.6.5 - * @param array $query_args Query args. - */ - protected function set_top_rated_products_query_args( &$query_args ) { - $query_args['meta_key'] = '_wc_average_rating'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key - $query_args['order'] = 'DESC'; - $query_args['orderby'] = 'meta_value_num'; - } - - /** - * Set visibility as hidden. - * - * @since 3.2.0 - * @param array $query_args Query args. - */ - protected function set_visibility_hidden_query_args( &$query_args ) { - $this->custom_visibility = true; - $query_args['tax_query'][] = array( - 'taxonomy' => 'product_visibility', - 'terms' => array( 'exclude-from-catalog', 'exclude-from-search' ), - 'field' => 'name', - 'operator' => 'AND', - 'include_children' => false, - ); - } - - /** - * Set visibility as catalog. - * - * @since 3.2.0 - * @param array $query_args Query args. - */ - protected function set_visibility_catalog_query_args( &$query_args ) { - $this->custom_visibility = true; - $query_args['tax_query'][] = array( - 'taxonomy' => 'product_visibility', - 'terms' => 'exclude-from-search', - 'field' => 'name', - 'operator' => 'IN', - 'include_children' => false, - ); - $query_args['tax_query'][] = array( - 'taxonomy' => 'product_visibility', - 'terms' => 'exclude-from-catalog', - 'field' => 'name', - 'operator' => 'NOT IN', - 'include_children' => false, - ); - } - - /** - * Set visibility as search. - * - * @since 3.2.0 - * @param array $query_args Query args. - */ - protected function set_visibility_search_query_args( &$query_args ) { - $this->custom_visibility = true; - $query_args['tax_query'][] = array( - 'taxonomy' => 'product_visibility', - 'terms' => 'exclude-from-catalog', - 'field' => 'name', - 'operator' => 'IN', - 'include_children' => false, - ); - $query_args['tax_query'][] = array( - 'taxonomy' => 'product_visibility', - 'terms' => 'exclude-from-search', - 'field' => 'name', - 'operator' => 'NOT IN', - 'include_children' => false, - ); - } - - /** - * Set visibility as featured. - * - * @since 3.2.0 - * @param array $query_args Query args. - */ - protected function set_visibility_featured_query_args( &$query_args ) { - $query_args['tax_query'] = array_merge( $query_args['tax_query'], WC()->query->get_tax_query() ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query - - $query_args['tax_query'][] = array( - 'taxonomy' => 'product_visibility', - 'terms' => 'featured', - 'field' => 'name', - 'operator' => 'IN', - 'include_children' => false, - ); - } - - /** - * Set visibility query args. - * - * @since 3.2.0 - * @param array $query_args Query args. - */ - protected function set_visibility_query_args( &$query_args ) { - if ( method_exists( $this, 'set_visibility_' . $this->attributes['visibility'] . '_query_args' ) ) { - $this->{'set_visibility_' . $this->attributes['visibility'] . '_query_args'}( $query_args ); - } else { - $query_args['tax_query'] = array_merge( $query_args['tax_query'], WC()->query->get_tax_query() ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query - } - } - - /** - * Set product as visible when querying for hidden products. - * - * @since 3.2.0 - * @param bool $visibility Product visibility. - * @return bool - */ - public function set_product_as_visible( $visibility ) { - return $this->custom_visibility ? true : $visibility; - } - - /** - * Get wrapper classes. - * - * @since 3.2.0 - * @param int $columns Number of columns. - * @return array - */ - protected function get_wrapper_classes( $columns ) { - $classes = array( 'woocommerce' ); - - if ( 'product' !== $this->type ) { - $classes[] = 'columns-' . $columns; - } - - $classes[] = $this->attributes['class']; - - return $classes; - } - - /** - * Generate and return the transient name for this shortcode based on the query args. - * - * @since 3.3.0 - * @return string - */ - protected function get_transient_name() { - $transient_name = 'wc_product_loop_' . md5( wp_json_encode( $this->query_args ) . $this->type ); - - if ( 'rand' === $this->query_args['orderby'] ) { - // When using rand, we'll cache a number of random queries and pull those to avoid querying rand on each page load. - $rand_index = wp_rand( 0, max( 1, absint( apply_filters( 'woocommerce_product_query_max_rand_cache_count', 5 ) ) ) ); - $transient_name .= $rand_index; - } - - return $transient_name; - } - - /** - * Run the query and return an array of data, including queried ids and pagination information. - * - * @since 3.3.0 - * @return object Object with the following props; ids, per_page, found_posts, max_num_pages, current_page - */ - protected function get_query_results() { - $transient_name = $this->get_transient_name(); - $transient_version = WC_Cache_Helper::get_transient_version( 'product_query' ); - $cache = wc_string_to_bool( $this->attributes['cache'] ) === true; - $transient_value = $cache ? get_transient( $transient_name ) : false; - - if ( isset( $transient_value['value'], $transient_value['version'] ) && $transient_value['version'] === $transient_version ) { - $results = $transient_value['value']; - } else { - $query = new WP_Query( $this->query_args ); - - $paginated = ! $query->get( 'no_found_rows' ); - - $results = (object) array( - 'ids' => wp_parse_id_list( $query->posts ), - 'total' => $paginated ? (int) $query->found_posts : count( $query->posts ), - 'total_pages' => $paginated ? (int) $query->max_num_pages : 1, - 'per_page' => (int) $query->get( 'posts_per_page' ), - 'current_page' => $paginated ? (int) max( 1, $query->get( 'paged', 1 ) ) : 1, - ); - - if ( $cache ) { - $transient_value = array( - 'version' => $transient_version, - 'value' => $results, - ); - set_transient( $transient_name, $transient_value, DAY_IN_SECONDS * 30 ); - } - } - - // Remove ordering query arguments which may have been added by get_catalog_ordering_args. - WC()->query->remove_ordering_args(); - - /** - * Filter shortcode products query results. - * - * @since 4.0.0 - * @param stdClass $results Query results. - * @param WC_Shortcode_Products $this WC_Shortcode_Products instance. - */ - return apply_filters( 'woocommerce_shortcode_products_query_results', $results, $this ); - } - - /** - * Loop over found products. - * - * @since 3.2.0 - * @return string - */ - protected function product_loop() { - $columns = absint( $this->attributes['columns'] ); - $classes = $this->get_wrapper_classes( $columns ); - $products = $this->get_query_results(); - - ob_start(); - - if ( $products && $products->ids ) { - // Prime caches to reduce future queries. - if ( is_callable( '_prime_post_caches' ) ) { - _prime_post_caches( $products->ids ); - } - - // Setup the loop. - wc_setup_loop( - array( - 'columns' => $columns, - 'name' => $this->type, - 'is_shortcode' => true, - 'is_search' => false, - 'is_paginated' => wc_string_to_bool( $this->attributes['paginate'] ), - 'total' => $products->total, - 'total_pages' => $products->total_pages, - 'per_page' => $products->per_page, - 'current_page' => $products->current_page, - ) - ); - - $original_post = $GLOBALS['post']; - - do_action( "woocommerce_shortcode_before_{$this->type}_loop", $this->attributes ); - - // Fire standard shop loop hooks when paginating results so we can show result counts and so on. - if ( wc_string_to_bool( $this->attributes['paginate'] ) ) { - do_action( 'woocommerce_before_shop_loop' ); - } - - woocommerce_product_loop_start(); - - if ( wc_get_loop_prop( 'total' ) ) { - foreach ( $products->ids as $product_id ) { - $GLOBALS['post'] = get_post( $product_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - setup_postdata( $GLOBALS['post'] ); - - // Set custom product visibility when quering hidden products. - add_action( 'woocommerce_product_is_visible', array( $this, 'set_product_as_visible' ) ); - - // Render product template. - wc_get_template_part( 'content', 'product' ); - - // Restore product visibility. - remove_action( 'woocommerce_product_is_visible', array( $this, 'set_product_as_visible' ) ); - } - } - - $GLOBALS['post'] = $original_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - woocommerce_product_loop_end(); - - // Fire standard shop loop hooks when paginating results so we can show result counts and so on. - if ( wc_string_to_bool( $this->attributes['paginate'] ) ) { - do_action( 'woocommerce_after_shop_loop' ); - } - - do_action( "woocommerce_shortcode_after_{$this->type}_loop", $this->attributes ); - - wp_reset_postdata(); - wc_reset_loop(); - } else { - do_action( "woocommerce_shortcode_{$this->type}_loop_no_results", $this->attributes ); - } - - return '
    ' . ob_get_clean() . '
    '; - } - - /** - * Order by rating. - * - * @since 3.2.0 - * @param array $args Query args. - * @return array - */ - public static function order_by_rating_post_clauses( $args ) { - global $wpdb; - - $args['where'] .= " AND $wpdb->commentmeta.meta_key = 'rating' "; - $args['join'] .= "LEFT JOIN $wpdb->comments ON($wpdb->posts.ID = $wpdb->comments.comment_post_ID) LEFT JOIN $wpdb->commentmeta ON($wpdb->comments.comment_ID = $wpdb->commentmeta.comment_id)"; - $args['orderby'] = "$wpdb->commentmeta.meta_value DESC"; - $args['groupby'] = "$wpdb->posts.ID"; - - return $args; - } -} diff --git a/includes/theme-support/class-wc-twenty-nineteen.php b/includes/theme-support/class-wc-twenty-nineteen.php deleted file mode 100644 index 47c1aff660c..00000000000 --- a/includes/theme-support/class-wc-twenty-nineteen.php +++ /dev/null @@ -1,132 +0,0 @@ - 300, - 'single_image_width' => 450, - ) - ); - - // Tweak Twenty Nineteen features. - add_action( 'wp', array( __CLASS__, 'tweak_theme_features' ) ); - - // Color scheme CSS - add_filter( 'twentynineteen_custom_colors_css', array( __CLASS__, 'custom_colors_css' ), 10, 3 ); - } - - /** - * Open the Twenty Nineteen wrapper. - */ - public static function output_content_wrapper() { - echo '
    '; - echo '
    '; - } - - /** - * Close the Twenty Nineteen wrapper. - */ - public static function output_content_wrapper_end() { - echo '
    '; - echo '
    '; - } - - /** - * Enqueue CSS for this theme. - * - * @param array $styles Array of registered styles. - * @return array - */ - public static function enqueue_styles( $styles ) { - unset( $styles['woocommerce-general'] ); - - $styles['woocommerce-general'] = array( - 'src' => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-nineteen.css', - 'deps' => '', - 'version' => Constants::get_constant( 'WC_VERSION' ), - 'media' => 'all', - 'has_rtl' => true, - ); - - return apply_filters( 'woocommerce_twenty_nineteen_styles', $styles ); - } - - /** - * Tweak Twenty Nineteen features. - */ - public static function tweak_theme_features() { - if ( is_woocommerce() ) { - add_filter( 'twentynineteen_can_show_post_thumbnail', '__return_false' ); - } - } - - /** - * Filters Twenty Nineteen custom colors CSS. - * - * @param string $css Base theme colors CSS. - * @param int $primary_color The user's selected color hue. - * @param string $saturation Filtered theme color saturation level. - */ - public static function custom_colors_css( $css, $primary_color, $saturation ) { - if ( function_exists( 'register_block_type' ) && is_admin() ) { - return $css; - } - - $lightness = absint( apply_filters( 'twentynineteen_custom_colors_lightness', 33 ) ); - $lightness = $lightness . '%'; - - $css .= ' - .onsale, - .woocommerce-info, - .woocommerce-store-notice { - background-color: hsl( ' . $primary_color . ', ' . $saturation . ', ' . $lightness . ' ); - } - - .woocommerce-tabs ul li.active a { - color: hsl( ' . $primary_color . ', ' . $saturation . ', ' . $lightness . ' ); - box-shadow: 0 2px 0 hsl( ' . $primary_color . ', ' . $saturation . ', ' . $lightness . ' ); - } - '; - - return $css; - } -} - -WC_Twenty_Nineteen::init(); diff --git a/includes/theme-support/class-wc-twenty-twenty-one.php b/includes/theme-support/class-wc-twenty-twenty-one.php deleted file mode 100644 index b7d36f646b7..00000000000 --- a/includes/theme-support/class-wc-twenty-twenty-one.php +++ /dev/null @@ -1,105 +0,0 @@ - 450, - 'single_image_width' => 600, - ) - ); - - } - - /** - * Open the Twenty Twenty One wrapper. - */ - public static function output_content_wrapper() { - echo '
    '; - echo '
    '; - } - - /** - * Close the Twenty Twenty One wrapper. - */ - public static function output_content_wrapper_end() { - echo '
    '; - echo '
    '; - } - - /** - * Enqueue CSS for this theme. - * - * @param array $styles Array of registered styles. - * @return array - */ - public static function enqueue_styles( $styles ) { - unset( $styles['woocommerce-general'] ); - - $styles['woocommerce-general'] = array( - 'src' => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-twenty-one.css', - 'deps' => '', - 'version' => Constants::get_constant( 'WC_VERSION' ), - 'media' => 'all', - 'has_rtl' => true, - ); - - return apply_filters( 'woocommerce_twenty_twenty_one_styles', $styles ); - } - - /** - * Enqueue the wp-admin CSS overrides for this theme. - */ - public static function enqueue_admin_styles() { - wp_enqueue_style( - 'woocommerce-twenty-twenty-one-admin', - str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-twenty-one-admin.css', - '', - Constants::get_constant( 'WC_VERSION' ), - 'all' - ); - } - - -} - -WC_Twenty_Twenty_One::init(); diff --git a/includes/tracks/class-wc-site-tracking.php b/includes/tracks/class-wc-site-tracking.php deleted file mode 100644 index 74e06215985..00000000000 --- a/includes/tracks/class-wc-site-tracking.php +++ /dev/null @@ -1,186 +0,0 @@ - - - - registered['woo-tracks'] ) ) { - return; - } - - $woo_tracks_script = $wp_scripts->registered['woo-tracks']->src; - - ?> - - cap_key ) { - return false; - } - $user_id = $user->ID; - $anon_id = get_user_meta( $user_id, '_woocommerce_tracks_anon_id', true ); - - // If an id is still not found, create one and save it. - if ( ! $anon_id ) { - $anon_id = self::get_anon_id(); - update_user_meta( $user_id, '_woocommerce_tracks_anon_id', $anon_id ); - } - - // Don't set cookie on API requests. - if ( ! Constants::is_true( 'REST_REQUEST' ) && ! Constants::is_true( 'XMLRPC_REQUEST' ) ) { - wc_setcookie( 'tk_ai', $anon_id ); - } - } - - /** - * Record a Tracks event - * - * @param array $event Array of event properties. - * @return bool|WP_Error True on success, WP_Error on failure. - */ - public static function record_event( $event ) { - if ( ! $event instanceof WC_Tracks_Event ) { - $event = new WC_Tracks_Event( $event ); - } - - if ( is_wp_error( $event ) ) { - return $event; - } - - $pixel = $event->build_pixel_url( $event ); - - if ( ! $pixel ) { - return new WP_Error( 'invalid_pixel', 'cannot generate tracks pixel for given input', 400 ); - } - - return self::record_pixel( $pixel ); - } - - /** - * Synchronously request the pixel. - * - * @param string $pixel pixel url and query string. - * @return bool Always returns true. - */ - public static function record_pixel( $pixel ) { - // Add the Request Timestamp and URL terminator just before the HTTP request. - $pixel .= '&_rt=' . self::build_timestamp() . '&_=_'; - - wp_safe_remote_get( - $pixel, - array( - 'blocking' => false, - 'redirection' => 2, - 'httpversion' => '1.1', - 'timeout' => 1, - ) - ); - - return true; - } - - /** - * Create a timestap representing milliseconds since 1970-01-01 - * - * @return string A string representing a timestamp. - */ - public static function build_timestamp() { - $ts = NumberUtil::round( microtime( true ) * 1000 ); - - return number_format( $ts, 0, '', '' ); - } - - /** - * Get a user's identity to send to Tracks. If Jetpack exists, default to its implementation. - * - * @param int $user_id User id. - * @return array Identity properties. - */ - public static function get_identity( $user_id ) { - $jetpack_lib = '/tracks/client.php'; - - if ( class_exists( 'Jetpack' ) && Constants::is_defined( 'JETPACK__VERSION' ) ) { - if ( version_compare( Constants::get_constant( 'JETPACK__VERSION' ), '7.5', '<' ) ) { - if ( file_exists( jetpack_require_lib_dir() . $jetpack_lib ) ) { - include_once jetpack_require_lib_dir() . $jetpack_lib; - if ( function_exists( 'jetpack_tracks_get_identity' ) ) { - return jetpack_tracks_get_identity( $user_id ); - } - } - } else { - $tracking = new Automattic\Jetpack\Tracking(); - return $tracking->tracks_get_identity( $user_id ); - } - } - - // Start with a previously set cookie. - $anon_id = isset( $_COOKIE['tk_ai'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ) : false; - - // If there is no cookie, apply a saved id. - if ( ! $anon_id ) { - $anon_id = get_user_meta( $user_id, '_woocommerce_tracks_anon_id', true ); - } - - // If an id is still not found, create one and save it. - if ( ! $anon_id ) { - $anon_id = self::get_anon_id(); - - update_user_meta( $user_id, '_woocommerce_tracks_anon_id', $anon_id ); - } - - return array( - '_ut' => 'anon', - '_ui' => $anon_id, - ); - } - - /** - * Grabs the user's anon id from cookies, or generates and sets a new one - * - * @return string An anon id for the user - */ - public static function get_anon_id() { - static $anon_id = null; - - if ( ! isset( $anon_id ) ) { - - // Did the browser send us a cookie? - if ( isset( $_COOKIE['tk_ai'] ) ) { - $anon_id = sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ); - } else { - - $binary = ''; - - // Generate a new anonId and try to save it in the browser's cookies. - // Note that base64-encoding an 18 character string generates a 24-character anon id. - for ( $i = 0; $i < 18; ++$i ) { - $binary .= chr( wp_rand( 0, 255 ) ); - } - - // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode - $anon_id = 'woo:' . base64_encode( $binary ); - } - } - - return $anon_id; - } -} - -WC_Tracks_Client::init(); diff --git a/includes/tracks/class-wc-tracks.php b/includes/tracks/class-wc-tracks.php deleted file mode 100644 index 7528906ef45..00000000000 --- a/includes/tracks/class-wc-tracks.php +++ /dev/null @@ -1,116 +0,0 @@ - home_url(), - 'blog_lang' => get_user_locale( $user_id ), - 'blog_id' => class_exists( 'Jetpack_Options' ) ? Jetpack_Options::get_option( 'id' ) : null, - 'products_count' => self::get_products_count(), - 'wc_version' => WC()->version, - ); - set_transient( 'wc_tracks_blog_details', $blog_details, DAY_IN_SECONDS ); - } - return $blog_details; - } - - /** - * Gather details from the request to the server. - * - * @return array Server details. - */ - public static function get_server_details() { - $data = array(); - - $data['_via_ua'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; - $data['_via_ip'] = isset( $_SERVER['REMOTE_ADDR'] ) ? wc_clean( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; - $data['_lg'] = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : ''; - $data['_dr'] = isset( $_SERVER['HTTP_REFERER'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : ''; - - $uri = isset( $_SERVER['REQUEST_URI'] ) ? wc_clean( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; - $host = isset( $_SERVER['HTTP_HOST'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; - $data['_dl'] = isset( $_SERVER['REQUEST_SCHEME'] ) ? wc_clean( wp_unslash( $_SERVER['REQUEST_SCHEME'] ) ) . '://' . $host . $uri : ''; - - return $data; - } - - /** - * Record an event in Tracks - this is the preferred way to record events from PHP. - * - * @param string $event_name The name of the event. - * @param array $properties Custom properties to send with the event. - * @return bool|WP_Error True for success or WP_Error if the event pixel could not be fired. - */ - public static function record_event( $event_name, $properties = array() ) { - /** - * Don't track users who don't have tracking enabled. - */ - if ( ! WC_Site_Tracking::is_tracking_enabled() ) { - return false; - } - - $user = wp_get_current_user(); - - // We don't want to track user events during unit tests/CI runs. - if ( $user instanceof WP_User && 'wptests_capabilities' === $user->cap_key ) { - return false; - } - $prefixed_event_name = self::PREFIX . $event_name; - - $data = array( - '_en' => $prefixed_event_name, - '_ts' => WC_Tracks_Client::build_timestamp(), - ); - - $server_details = self::get_server_details(); - $identity = WC_Tracks_Client::get_identity( $user->ID ); - $blog_details = self::get_blog_details( $user->ID ); - - // Allow event props to be filtered to enable adding site-wide props. - $filtered_properties = apply_filters( 'woocommerce_tracks_event_properties', $properties, $prefixed_event_name ); - - // Delete _ui and _ut protected properties. - unset( $filtered_properties['_ui'] ); - unset( $filtered_properties['_ut'] ); - - $event_obj = new WC_Tracks_Event( array_merge( $data, $server_details, $identity, $blog_details, $filtered_properties ) ); - - if ( is_wp_error( $event_obj->error ) ) { - return $event_obj->error; - } - - return $event_obj->record(); - } -} diff --git a/includes/tracks/events/class-wc-extensions-tracking.php b/includes/tracks/events/class-wc-extensions-tracking.php deleted file mode 100644 index 3944ef96f1a..00000000000 --- a/includes/tracks/events/class-wc-extensions-tracking.php +++ /dev/null @@ -1,97 +0,0 @@ - empty( $_REQUEST['section'] ) ? '_featured' : wc_clean( wp_unslash( $_REQUEST['section'] ) ), - ); - - if ( ! empty( $_REQUEST['search'] ) ) { - $event = 'extensions_view_search'; - $properties['search_term'] = wc_clean( wp_unslash( $_REQUEST['search'] ) ); - } - // phpcs:enable - - WC_Tracks::record_event( $event, $properties ); - } - - /** - * Send a Tracks even when a Helper connection process is initiated. - */ - public function track_helper_connection_start() { - WC_Tracks::record_event( 'extensions_subscriptions_connect' ); - } - - /** - * Send a Tracks even when a Helper connection process is cancelled. - */ - public function track_helper_connection_cancelled() { - WC_Tracks::record_event( 'extensions_subscriptions_cancelled' ); - } - - /** - * Send a Tracks even when a Helper connection process completed successfully. - */ - public function track_helper_connection_complete() { - WC_Tracks::record_event( 'extensions_subscriptions_connected' ); - } - - /** - * Send a Tracks even when a Helper has been disconnected. - */ - public function track_helper_disconnected() { - WC_Tracks::record_event( 'extensions_subscriptions_disconnect' ); - } - - /** - * Send a Tracks even when Helper subscriptions are refreshed. - */ - public function track_helper_subscriptions_refresh() { - WC_Tracks::record_event( 'extensions_subscriptions_update' ); - } - - /** - * Send a Tracks event when addon is installed via the Extensions page. - * - * @param string $addon_id Addon slug. - * @param string $section Extensions tab. - */ - public function track_addon_install( $addon_id, $section ) { - $properties = array( - 'context' => 'extensions', - 'section' => $section, - ); - - if ( 'woocommerce-payments' === $addon_id ) { - WC_Tracks::record_event( 'woocommerce_payments_install', $properties ); - } - } -} diff --git a/includes/tracks/events/class-wc-products-tracking.php b/includes/tracks/events/class-wc-products-tracking.php deleted file mode 100644 index ec68dfa5736..00000000000 --- a/includes/tracks/events/class-wc-products-tracking.php +++ /dev/null @@ -1,211 +0,0 @@ -post_type ) { - return; - } - - $properties = array( - 'product_id' => $product_id, - ); - - WC_Tracks::record_event( 'product_edit', $properties ); - } - - /** - * Track the Update button being clicked on the client side. - * This is needed because `track_product_updated` (using the `edit_post` - * hook) is called in response to a number of other triggers. - * - * @param WP_Post $post The post, not used. - */ - public function track_product_updated_client_side( $post ) { - wc_enqueue_js( - " - if ( $( 'h1.wp-heading-inline' ).text().trim() === '" . __( 'Edit product', 'woocommerce' ) . "') { - var initialStockValue = $( '#_stock' ).val(); - var hasRecordedEvent = false; - - $( '#publish' ).on( 'click', function() { - if ( hasRecordedEvent ) { - return; - } - - var currentStockValue = $( '#_stock' ).val(); - var properties = { - product_type: $( '#product-type' ).val(), - is_virtual: $( '#_virtual' ).is( ':checked' ) ? 'Y' : 'N', - is_downloadable: $( '#_downloadable' ).is( ':checked' ) ? 'Y' : 'N', - manage_stock: $( '#_manage_stock' ).is( ':checked' ) ? 'Y' : 'N', - stock_quantity_update: ( initialStockValue != currentStockValue ) ? 'Y' : 'N', - }; - - window.wcTracks.recordEvent( 'product_update', properties ); - hasRecordedEvent = true; - } ); - } - " - ); - } - - /** - * Send a Tracks event when a product is published. - * - * @param string $new_status New post_status. - * @param string $old_status Previous post_status. - * @param object $post WordPress post. - */ - public function track_product_published( $new_status, $old_status, $post ) { - if ( - 'product' !== $post->post_type || - 'publish' !== $new_status || - 'publish' === $old_status - ) { - return; - } - - $properties = array( - 'product_id' => $post->ID, - ); - - WC_Tracks::record_event( 'product_add_publish', $properties ); - } - - /** - * Send a Tracks event when a product category is created. - * - * @param int $category_id Category ID. - */ - public function track_product_category_created( $category_id ) { - // phpcs:disable WordPress.Security.NonceVerification.Missing - // Only track category creation from the edit product screen or the - // category management screen (which both occur via AJAX). - if ( - ! Constants::is_defined( 'DOING_AJAX' ) || - empty( $_POST['action'] ) || - ( - // Product Categories screen. - 'add-tag' !== $_POST['action'] && - // Edit Product screen. - 'add-product_cat' !== $_POST['action'] - ) - ) { - return; - } - - $category = get_term( $category_id, 'product_cat' ); - $properties = array( - 'category_id' => $category_id, - 'parent_id' => $category->parent, - 'page' => ( 'add-tag' === $_POST['action'] ) ? 'categories' : 'product', - ); - // phpcs:enable - - WC_Tracks::record_event( 'product_category_add', $properties ); - } -} diff --git a/includes/tracks/events/class-wc-settings-tracking.php b/includes/tracks/events/class-wc-settings-tracking.php deleted file mode 100644 index c713db576f0..00000000000 --- a/includes/tracks/events/class-wc-settings-tracking.php +++ /dev/null @@ -1,118 +0,0 @@ -allowed_options[] = $option['id']; - - // Delay attaching this action since it could get fired a lot. - if ( false === has_action( 'update_option', array( $this, 'track_setting_change' ) ) ) { - add_action( 'update_option', array( $this, 'track_setting_change' ), 10, 3 ); - } - } - - /** - * Add WooCommerce option to a list of updated options. - * - * @param string $option_name Option being updated. - * @param mixed $old_value Old value of option. - * @param mixed $new_value New value of option. - */ - public function track_setting_change( $option_name, $old_value, $new_value ) { - // Make sure this is a WooCommerce option. - if ( ! in_array( $option_name, $this->allowed_options, true ) ) { - return; - } - - // Check to make sure the new value is truly different. - // `woocommerce_price_num_decimals` tends to trigger this - // because form values aren't coerced (e.g. '2' vs. 2). - if ( - is_scalar( $old_value ) && - is_scalar( $new_value ) && - (string) $old_value === (string) $new_value - ) { - return; - } - - $this->updated_options[] = $option_name; - } - - /** - * Send a Tracks event for WooCommerce options that changed values. - */ - public function send_settings_change_event() { - global $current_tab; - - if ( empty( $this->updated_options ) ) { - return; - } - - $properties = array( - 'settings' => implode( ',', $this->updated_options ), - ); - - if ( isset( $current_tab ) ) { - $properties['tab'] = $current_tab; - } - - WC_Tracks::record_event( 'settings_change', $properties ); - } - - /** - * Send a Tracks event for WooCommerce settings page views. - */ - public function track_settings_page_view() { - global $current_tab, $current_section; - - $properties = array( - 'tab' => $current_tab, - 'section' => empty( $current_section ) ? null : $current_section, - ); - - WC_Tracks::record_event( 'settings_view', $properties ); - } -} diff --git a/includes/wc-attribute-functions.php b/includes/wc-attribute-functions.php deleted file mode 100644 index 535a719f1b8..00000000000 --- a/includes/wc-attribute-functions.php +++ /dev/null @@ -1,734 +0,0 @@ -get_results( "SELECT * FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_name != '' ORDER BY attribute_name ASC;" ); - - set_transient( 'wc_attribute_taxonomies', $raw_attribute_taxonomies ); - } - - /** - * Filter attribute taxonomies. - * - * @param array $attribute_taxonomies Results of the DB query. Each taxonomy is an object. - */ - $raw_attribute_taxonomies = (array) array_filter( apply_filters( 'woocommerce_attribute_taxonomies', $raw_attribute_taxonomies ) ); - - // Index by ID for easer lookups. - $attribute_taxonomies = array(); - - foreach ( $raw_attribute_taxonomies as $result ) { - $attribute_taxonomies[ 'id:' . $result->attribute_id ] = $result; - } - - wp_cache_set( $cache_key, $attribute_taxonomies, 'woocommerce-attributes' ); - - return $attribute_taxonomies; -} - -/** - * Get (cached) attribute taxonomy ID and name pairs. - * - * @since 3.6.0 - * @return array - */ -function wc_get_attribute_taxonomy_ids() { - $prefix = WC_Cache_Helper::get_cache_prefix( 'woocommerce-attributes' ); - $cache_key = $prefix . 'ids'; - $cache_value = wp_cache_get( $cache_key, 'woocommerce-attributes' ); - - if ( $cache_value ) { - return $cache_value; - } - - $taxonomy_ids = array_map( 'absint', wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_id', 'attribute_name' ) ); - - wp_cache_set( $cache_key, $taxonomy_ids, 'woocommerce-attributes' ); - - return $taxonomy_ids; -} - -/** - * Get (cached) attribute taxonomy label and name pairs. - * - * @since 3.6.0 - * @return array - */ -function wc_get_attribute_taxonomy_labels() { - $prefix = WC_Cache_Helper::get_cache_prefix( 'woocommerce-attributes' ); - $cache_key = $prefix . 'labels'; - $cache_value = wp_cache_get( $cache_key, 'woocommerce-attributes' ); - - if ( $cache_value ) { - return $cache_value; - } - - $taxonomy_labels = wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_label', 'attribute_name' ); - - wp_cache_set( $cache_key, $taxonomy_labels, 'woocommerce-attributes' ); - - return $taxonomy_labels; -} - -/** - * Get a product attribute name. - * - * @param string $attribute_name Attribute name. - * @return string - */ -function wc_attribute_taxonomy_name( $attribute_name ) { - return $attribute_name ? 'pa_' . wc_sanitize_taxonomy_name( $attribute_name ) : ''; -} - -/** - * Get the attribute name used when storing values in post meta. - * - * @since 2.6.0 - * @param string $attribute_name Attribute name. - * @return string - */ -function wc_variation_attribute_name( $attribute_name ) { - return 'attribute_' . sanitize_title( $attribute_name ); -} - -/** - * Get a product attribute name by ID. - * - * @since 2.4.0 - * @param int $attribute_id Attribute ID. - * @return string Return an empty string if attribute doesn't exist. - */ -function wc_attribute_taxonomy_name_by_id( $attribute_id ) { - $taxonomy_ids = wc_get_attribute_taxonomy_ids(); - $attribute_name = (string) array_search( $attribute_id, $taxonomy_ids, true ); - return wc_attribute_taxonomy_name( $attribute_name ); -} - -/** - * Get a product attribute ID by name. - * - * @since 2.6.0 - * @param string $name Attribute name. - * @return int - */ -function wc_attribute_taxonomy_id_by_name( $name ) { - $name = wc_attribute_taxonomy_slug( $name ); - $taxonomy_ids = wc_get_attribute_taxonomy_ids(); - - return isset( $taxonomy_ids[ $name ] ) ? $taxonomy_ids[ $name ] : 0; -} - -/** - * Get a product attributes label. - * - * @param string $name Attribute name. - * @param WC_Product $product Product data. - * @return string - */ -function wc_attribute_label( $name, $product = '' ) { - if ( taxonomy_is_product_attribute( $name ) ) { - $slug = wc_attribute_taxonomy_slug( $name ); - $all_labels = wc_get_attribute_taxonomy_labels(); - $label = isset( $all_labels[ $slug ] ) ? $all_labels[ $slug ] : $slug; - } elseif ( $product ) { - if ( $product->is_type( 'variation' ) ) { - $product = wc_get_product( $product->get_parent_id() ); - } - $attributes = array(); - - if ( false !== $product ) { - $attributes = $product->get_attributes(); - } - - // Attempt to get label from product, as entered by the user. - if ( $attributes && isset( $attributes[ sanitize_title( $name ) ] ) ) { - $label = $attributes[ sanitize_title( $name ) ]->get_name(); - } else { - $label = $name; - } - } else { - $label = $name; - } - - return apply_filters( 'woocommerce_attribute_label', $label, $name, $product ); -} - -/** - * Get a product attributes orderby setting. - * - * @param string $name Attribute name. - * @return string - */ -function wc_attribute_orderby( $name ) { - $name = wc_attribute_taxonomy_slug( $name ); - $id = wc_attribute_taxonomy_id_by_name( $name ); - $taxonomies = wc_get_attribute_taxonomies(); - - return apply_filters( 'woocommerce_attribute_orderby', isset( $taxonomies[ 'id:' . $id ] ) ? $taxonomies[ 'id:' . $id ]->attribute_orderby : 'menu_order', $name ); -} - -/** - * Get an array of product attribute taxonomies. - * - * @return array - */ -function wc_get_attribute_taxonomy_names() { - $taxonomy_names = array(); - $attribute_taxonomies = wc_get_attribute_taxonomies(); - if ( ! empty( $attribute_taxonomies ) ) { - foreach ( $attribute_taxonomies as $tax ) { - $taxonomy_names[] = wc_attribute_taxonomy_name( $tax->attribute_name ); - } - } - return $taxonomy_names; -} - -/** - * Get attribute types. - * - * @since 2.4.0 - * @return array - */ -function wc_get_attribute_types() { - return (array) apply_filters( - 'product_attributes_type_selector', - array( - 'select' => __( 'Select', 'woocommerce' ), - ) - ); -} - -/** - * Check if there are custom attribute types. - * - * @since 3.3.2 - * @return bool True if there are custom types, otherwise false. - */ -function wc_has_custom_attribute_types() { - $types = wc_get_attribute_types(); - - return 1 < count( $types ) || ! array_key_exists( 'select', $types ); -} - -/** - * Get attribute type label. - * - * @since 3.0.0 - * @param string $type Attribute type slug. - * @return string - */ -function wc_get_attribute_type_label( $type ) { - $types = wc_get_attribute_types(); - - return isset( $types[ $type ] ) ? $types[ $type ] : __( 'Select', 'woocommerce' ); -} - -/** - * Check if attribute name is reserved. - * https://codex.wordpress.org/Function_Reference/register_taxonomy#Reserved_Terms. - * - * @since 2.4.0 - * @param string $attribute_name Attribute name. - * @return bool - */ -function wc_check_if_attribute_name_is_reserved( $attribute_name ) { - // Forbidden attribute names. - $reserved_terms = array( - 'attachment', - 'attachment_id', - 'author', - 'author_name', - 'calendar', - 'cat', - 'category', - 'category__and', - 'category__in', - 'category__not_in', - 'category_name', - 'comments_per_page', - 'comments_popup', - 'cpage', - 'day', - 'debug', - 'error', - 'exact', - 'feed', - 'hour', - 'link_category', - 'm', - 'minute', - 'monthnum', - 'more', - 'name', - 'nav_menu', - 'nopaging', - 'offset', - 'order', - 'orderby', - 'p', - 'page', - 'page_id', - 'paged', - 'pagename', - 'pb', - 'perm', - 'post', - 'post__in', - 'post__not_in', - 'post_format', - 'post_mime_type', - 'post_status', - 'post_tag', - 'post_type', - 'posts', - 'posts_per_archive_page', - 'posts_per_page', - 'preview', - 'robots', - 's', - 'search', - 'second', - 'sentence', - 'showposts', - 'static', - 'subpost', - 'subpost_id', - 'tag', - 'tag__and', - 'tag__in', - 'tag__not_in', - 'tag_id', - 'tag_slug__and', - 'tag_slug__in', - 'taxonomy', - 'tb', - 'term', - 'type', - 'w', - 'withcomments', - 'withoutcomments', - 'year', - ); - - return in_array( $attribute_name, $reserved_terms, true ); -} - -/** - * Callback for array filter to get visible only. - * - * @since 3.0.0 - * @param WC_Product_Attribute $attribute Attribute data. - * @return bool - */ -function wc_attributes_array_filter_visible( $attribute ) { - return $attribute && is_a( $attribute, 'WC_Product_Attribute' ) && $attribute->get_visible() && ( ! $attribute->is_taxonomy() || taxonomy_exists( $attribute->get_name() ) ); -} - -/** - * Callback for array filter to get variation attributes only. - * - * @since 3.0.0 - * @param WC_Product_Attribute $attribute Attribute data. - * @return bool - */ -function wc_attributes_array_filter_variation( $attribute ) { - return $attribute && is_a( $attribute, 'WC_Product_Attribute' ) && $attribute->get_variation(); -} - -/** - * Check if an attribute is included in the attributes area of a variation name. - * - * @since 3.0.2 - * @param string $attribute Attribute value to check for. - * @param string $name Product name to check in. - * @return bool - */ -function wc_is_attribute_in_product_name( $attribute, $name ) { - $is_in_name = stristr( $name, ' ' . $attribute . ',' ) || 0 === stripos( strrev( $name ), strrev( ' ' . $attribute ) ); - return apply_filters( 'woocommerce_is_attribute_in_product_name', $is_in_name, $attribute, $name ); -} - -/** - * Callback for array filter to get default attributes. Will allow for '0' string values, but regard all other - * class PHP FALSE equivalents normally. - * - * @since 3.1.0 - * @param mixed $attribute Attribute being considered for exclusion from parent array. - * @return bool - */ -function wc_array_filter_default_attributes( $attribute ) { - return is_scalar( $attribute ) && ( ! empty( $attribute ) || '0' === $attribute ); -} - -/** - * Get attribute data by ID. - * - * @since 3.2.0 - * @param int $id Attribute ID. - * @return stdClass|null - */ -function wc_get_attribute( $id ) { - $attributes = wc_get_attribute_taxonomies(); - - if ( ! isset( $attributes[ 'id:' . $id ] ) ) { - return null; - } - - $data = $attributes[ 'id:' . $id ]; - $attribute = new stdClass(); - $attribute->id = (int) $data->attribute_id; - $attribute->name = $data->attribute_label; - $attribute->slug = wc_attribute_taxonomy_name( $data->attribute_name ); - $attribute->type = $data->attribute_type; - $attribute->order_by = $data->attribute_orderby; - $attribute->has_archives = (bool) $data->attribute_public; - return $attribute; -} - -/** - * Create attribute. - * - * @since 3.2.0 - * @param array $args Attribute arguments { - * Array of attribute parameters. - * - * @type int $id Unique identifier, used to update an attribute. - * @type string $name Attribute name. Always required. - * @type string $slug Attribute alphanumeric identifier. - * @type string $type Type of attribute. - * Core by default accepts: 'select' and 'text'. - * Default to 'select'. - * @type string $order_by Sort order. - * Accepts: 'menu_order', 'name', 'name_num' and 'id'. - * Default to 'menu_order'. - * @type bool $has_archives Enable or disable attribute archives. False by default. - * } - * @return int|WP_Error - */ -function wc_create_attribute( $args ) { - global $wpdb; - - $args = wp_unslash( $args ); - $id = ! empty( $args['id'] ) ? intval( $args['id'] ) : 0; - $format = array( '%s', '%s', '%s', '%s', '%d' ); - - // Name is required. - if ( empty( $args['name'] ) ) { - return new WP_Error( 'missing_attribute_name', __( 'Please, provide an attribute name.', 'woocommerce' ), array( 'status' => 400 ) ); - } - - // Set the attribute slug. - if ( empty( $args['slug'] ) ) { - $slug = wc_sanitize_taxonomy_name( $args['name'] ); - } else { - $slug = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( $args['slug'] ) ); - } - - // Validate slug. - if ( strlen( $slug ) >= 28 ) { - /* translators: %s: attribute slug */ - return new WP_Error( 'invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); - } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { - /* translators: %s: attribute slug */ - return new WP_Error( 'invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); - } elseif ( ( 0 === $id && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) || ( isset( $args['old_slug'] ) && $args['old_slug'] !== $slug && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) ) { - /* translators: %s: attribute slug */ - return new WP_Error( 'invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); - } - - // Validate type. - if ( empty( $args['type'] ) || ! array_key_exists( $args['type'], wc_get_attribute_types() ) ) { - $args['type'] = 'select'; - } - - // Validate order by. - if ( empty( $args['order_by'] ) || ! in_array( $args['order_by'], array( 'menu_order', 'name', 'name_num', 'id' ), true ) ) { - $args['order_by'] = 'menu_order'; - } - - $data = array( - 'attribute_label' => $args['name'], - 'attribute_name' => $slug, - 'attribute_type' => $args['type'], - 'attribute_orderby' => $args['order_by'], - 'attribute_public' => isset( $args['has_archives'] ) ? (int) $args['has_archives'] : 0, - ); - - // Create or update. - if ( 0 === $id ) { - $results = $wpdb->insert( - $wpdb->prefix . 'woocommerce_attribute_taxonomies', - $data, - $format - ); - - if ( is_wp_error( $results ) ) { - return new WP_Error( 'cannot_create_attribute', $results->get_error_message(), array( 'status' => 400 ) ); - } - - $id = $wpdb->insert_id; - - /** - * Attribute added. - * - * @param int $id Added attribute ID. - * @param array $data Attribute data. - */ - do_action( 'woocommerce_attribute_added', $id, $data ); - } else { - $results = $wpdb->update( - $wpdb->prefix . 'woocommerce_attribute_taxonomies', - $data, - array( 'attribute_id' => $id ), - $format, - array( '%d' ) - ); - - if ( false === $results ) { - return new WP_Error( 'cannot_update_attribute', __( 'Could not update the attribute.', 'woocommerce' ), array( 'status' => 400 ) ); - } - - // Set old slug to check for database changes. - $old_slug = ! empty( $args['old_slug'] ) ? wc_sanitize_taxonomy_name( $args['old_slug'] ) : $slug; - - /** - * Attribute updated. - * - * @param int $id Added attribute ID. - * @param array $data Attribute data. - * @param string $old_slug Attribute old name. - */ - do_action( 'woocommerce_attribute_updated', $id, $data, $old_slug ); - - if ( $old_slug !== $slug ) { - // Update taxonomies in the wp term taxonomy table. - $wpdb->update( - $wpdb->term_taxonomy, - array( 'taxonomy' => wc_attribute_taxonomy_name( $data['attribute_name'] ) ), - array( 'taxonomy' => 'pa_' . $old_slug ) - ); - - // Update taxonomy ordering term meta. - $wpdb->update( - $wpdb->termmeta, - array( 'meta_key' => 'order_pa_' . sanitize_title( $data['attribute_name'] ) ), // WPCS: slow query ok. - array( 'meta_key' => 'order_pa_' . sanitize_title( $old_slug ) ) // WPCS: slow query ok. - ); - - // Update product attributes which use this taxonomy. - $old_taxonomy_name = 'pa_' . $old_slug; - $new_taxonomy_name = 'pa_' . $data['attribute_name']; - $old_attribute_key = sanitize_title( $old_taxonomy_name ); // @see WC_Product::set_attributes(). - $new_attribute_key = sanitize_title( $new_taxonomy_name ); // @see WC_Product::set_attributes(). - $metadatas = $wpdb->get_results( - $wpdb->prepare( - "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_product_attributes' AND meta_value LIKE %s", - '%' . $wpdb->esc_like( $old_taxonomy_name ) . '%' - ), - ARRAY_A - ); - foreach ( $metadatas as $metadata ) { - $product_id = $metadata['post_id']; - $unserialized_data = maybe_unserialize( $metadata['meta_value'] ); - - if ( ! $unserialized_data || ! is_array( $unserialized_data ) || ! isset( $unserialized_data[ $old_attribute_key ] ) ) { - continue; - } - - $unserialized_data[ $new_attribute_key ] = $unserialized_data[ $old_attribute_key ]; - unset( $unserialized_data[ $old_attribute_key ] ); - $unserialized_data[ $new_attribute_key ]['name'] = $new_taxonomy_name; - update_post_meta( $product_id, '_product_attributes', wp_slash( $unserialized_data ) ); - } - - // Update variations which use this taxonomy. - $wpdb->update( - $wpdb->postmeta, - array( 'meta_key' => 'attribute_pa_' . sanitize_title( $data['attribute_name'] ) ), // WPCS: slow query ok. - array( 'meta_key' => 'attribute_pa_' . sanitize_title( $old_slug ) ) // WPCS: slow query ok. - ); - } - } - - // Clear cache and flush rewrite rules. - wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); - delete_transient( 'wc_attribute_taxonomies' ); - WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); - - return $id; -} - -/** - * Update an attribute. - * - * For available args see wc_create_attribute(). - * - * @since 3.2.0 - * @param int $id Attribute ID. - * @param array $args Attribute arguments. - * @return int|WP_Error - */ -function wc_update_attribute( $id, $args ) { - global $wpdb; - - $attribute = wc_get_attribute( $id ); - - $args['id'] = $attribute ? $attribute->id : 0; - - if ( $args['id'] && empty( $args['name'] ) ) { - $args['name'] = $attribute->name; - } - - $args['old_slug'] = $wpdb->get_var( - $wpdb->prepare( - " - SELECT attribute_name - FROM {$wpdb->prefix}woocommerce_attribute_taxonomies - WHERE attribute_id = %d - ", - $args['id'] - ) - ); - - return wc_create_attribute( $args ); -} - -/** - * Delete attribute by ID. - * - * @since 3.2.0 - * @param int $id Attribute ID. - * @return bool - */ -function wc_delete_attribute( $id ) { - global $wpdb; - - $name = $wpdb->get_var( - $wpdb->prepare( - " - SELECT attribute_name - FROM {$wpdb->prefix}woocommerce_attribute_taxonomies - WHERE attribute_id = %d - ", - $id - ) - ); - - $taxonomy = wc_attribute_taxonomy_name( $name ); - - /** - * Before deleting an attribute. - * - * @param int $id Attribute ID. - * @param string $name Attribute name. - * @param string $taxonomy Attribute taxonomy name. - */ - do_action( 'woocommerce_before_attribute_delete', $id, $name, $taxonomy ); - - if ( $name && $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_id = %d", $id ) ) ) { - if ( taxonomy_exists( $taxonomy ) ) { - $terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' ); - foreach ( $terms as $term ) { - wp_delete_term( $term->term_id, $taxonomy ); - } - } - - /** - * After deleting an attribute. - * - * @param int $id Attribute ID. - * @param string $name Attribute name. - * @param string $taxonomy Attribute taxonomy name. - */ - do_action( 'woocommerce_attribute_deleted', $id, $name, $taxonomy ); - wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); - delete_transient( 'wc_attribute_taxonomies' ); - WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); - - return true; - } - - return false; -} - -/** - * Get an unprefixed product attribute name. - * - * @since 3.6.0 - * - * @param string $attribute_name Attribute name. - * @return string - */ -function wc_attribute_taxonomy_slug( $attribute_name ) { - $prefix = WC_Cache_Helper::get_cache_prefix( 'woocommerce-attributes' ); - $cache_key = $prefix . 'slug-' . $attribute_name; - $cache_value = wp_cache_get( $cache_key, 'woocommerce-attributes' ); - - if ( $cache_value ) { - return $cache_value; - } - - $attribute_name = wc_sanitize_taxonomy_name( $attribute_name ); - $attribute_slug = 0 === strpos( $attribute_name, 'pa_' ) ? substr( $attribute_name, 3 ) : $attribute_name; - wp_cache_set( $cache_key, $attribute_slug, 'woocommerce-attributes' ); - - return $attribute_slug; -} diff --git a/includes/wc-cart-functions.php b/includes/wc-cart-functions.php deleted file mode 100644 index 1d159c7d55a..00000000000 --- a/includes/wc-cart-functions.php +++ /dev/null @@ -1,505 +0,0 @@ -cart ) || '' === WC()->cart ) { - WC()->cart = new WC_Cart(); - } - WC()->cart->empty_cart( false ); -} - -/** - * Load the persistent cart. - * - * @param string $user_login User login. - * @param WP_User $user User data. - * @deprecated 2.3 - */ -function wc_load_persistent_cart( $user_login, $user ) { - if ( ! $user || ! apply_filters( 'woocommerce_persistent_cart_enabled', true ) ) { - return; - } - - $saved_cart = get_user_meta( $user->ID, '_woocommerce_persistent_cart_' . get_current_blog_id(), true ); - - if ( ! $saved_cart ) { - return; - } - - $cart = WC()->session->cart; - - if ( empty( $cart ) || ! is_array( $cart ) || 0 === count( $cart ) ) { - WC()->session->cart = $saved_cart['cart']; - } -} - -/** - * Retrieves unvalidated referer from '_wp_http_referer' or HTTP referer. - * - * Do not use for redirects, use {@see wp_get_referer()} instead. - * - * @since 2.6.1 - * @return string|false Referer URL on success, false on failure. - */ -function wc_get_raw_referer() { - if ( function_exists( 'wp_get_raw_referer' ) ) { - return wp_get_raw_referer(); - } - - if ( ! empty( $_REQUEST['_wp_http_referer'] ) ) { // WPCS: input var ok, CSRF ok. - return wp_unslash( $_REQUEST['_wp_http_referer'] ); // WPCS: input var ok, CSRF ok, sanitization ok. - } elseif ( ! empty( $_SERVER['HTTP_REFERER'] ) ) { // WPCS: input var ok, CSRF ok. - return wp_unslash( $_SERVER['HTTP_REFERER'] ); // WPCS: input var ok, CSRF ok, sanitization ok. - } - - return false; -} - -/** - * Add to cart messages. - * - * @param int|array $products Product ID list or single product ID. - * @param bool $show_qty Should qty's be shown? Added in 2.6.0. - * @param bool $return Return message rather than add it. - * - * @return mixed - */ -function wc_add_to_cart_message( $products, $show_qty = false, $return = false ) { - $titles = array(); - $count = 0; - - if ( ! is_array( $products ) ) { - $products = array( $products => 1 ); - $show_qty = false; - } - - if ( ! $show_qty ) { - $products = array_fill_keys( array_keys( $products ), 1 ); - } - - foreach ( $products as $product_id => $qty ) { - /* translators: %s: product name */ - $titles[] = apply_filters( 'woocommerce_add_to_cart_qty_html', ( $qty > 1 ? absint( $qty ) . ' × ' : '' ), $product_id ) . apply_filters( 'woocommerce_add_to_cart_item_name_in_quotes', sprintf( _x( '“%s”', 'Item name in quotes', 'woocommerce' ), strip_tags( get_the_title( $product_id ) ) ), $product_id ); - $count += $qty; - } - - $titles = array_filter( $titles ); - /* translators: %s: product name */ - $added_text = sprintf( _n( '%s has been added to your cart.', '%s have been added to your cart.', $count, 'woocommerce' ), wc_format_list_of_items( $titles ) ); - - // Output success messages. - if ( 'yes' === get_option( 'woocommerce_cart_redirect_after_add' ) ) { - $return_to = apply_filters( 'woocommerce_continue_shopping_redirect', wc_get_raw_referer() ? wp_validate_redirect( wc_get_raw_referer(), false ) : wc_get_page_permalink( 'shop' ) ); - $message = sprintf( '%s %s', esc_url( $return_to ), esc_html__( 'Continue shopping', 'woocommerce' ), esc_html( $added_text ) ); - } else { - $message = sprintf( '%s %s', esc_url( wc_get_cart_url() ), esc_html__( 'View cart', 'woocommerce' ), esc_html( $added_text ) ); - } - - if ( has_filter( 'wc_add_to_cart_message' ) ) { - wc_deprecated_function( 'The wc_add_to_cart_message filter', '3.0', 'wc_add_to_cart_message_html' ); - $message = apply_filters( 'wc_add_to_cart_message', $message, $product_id ); - } - - $message = apply_filters( 'wc_add_to_cart_message_html', $message, $products, $show_qty ); - - if ( $return ) { - return $message; - } else { - wc_add_notice( $message, apply_filters( 'woocommerce_add_to_cart_notice_type', 'success' ) ); - } -} - -/** - * Comma separate a list of item names, and replace final comma with 'and'. - * - * @param array $items Cart items. - * @return string - */ -function wc_format_list_of_items( $items ) { - $item_string = ''; - - foreach ( $items as $key => $item ) { - $item_string .= $item; - - if ( count( $items ) === $key + 2 ) { - $item_string .= ' ' . __( 'and', 'woocommerce' ) . ' '; - } elseif ( count( $items ) !== $key + 1 ) { - $item_string .= ', '; - } - } - - return $item_string; -} - -/** - * Clear cart after payment. - */ -function wc_clear_cart_after_payment() { - global $wp; - - if ( ! empty( $wp->query_vars['order-received'] ) ) { - - $order_id = absint( $wp->query_vars['order-received'] ); - $order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // WPCS: input var ok, CSRF ok. - - if ( $order_id > 0 ) { - $order = wc_get_order( $order_id ); - - if ( $order && hash_equals( $order->get_order_key(), $order_key ) ) { - WC()->cart->empty_cart(); - } - } - } - - if ( WC()->session->order_awaiting_payment > 0 ) { - $order = wc_get_order( WC()->session->order_awaiting_payment ); - - if ( $order && $order->get_id() > 0 ) { - // If the order has not failed, or is not pending, the order must have gone through. - if ( ! $order->has_status( array( 'failed', 'pending', 'cancelled' ) ) ) { - WC()->cart->empty_cart(); - } - } - } -} -add_action( 'get_header', 'wc_clear_cart_after_payment' ); - -/** - * Get the subtotal. - */ -function wc_cart_totals_subtotal_html() { - echo WC()->cart->get_cart_subtotal(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -} - -/** - * Get shipping methods. - */ -function wc_cart_totals_shipping_html() { - $packages = WC()->shipping()->get_packages(); - $first = true; - - foreach ( $packages as $i => $package ) { - $chosen_method = isset( WC()->session->chosen_shipping_methods[ $i ] ) ? WC()->session->chosen_shipping_methods[ $i ] : ''; - $product_names = array(); - - if ( count( $packages ) > 1 ) { - foreach ( $package['contents'] as $item_id => $values ) { - $product_names[ $item_id ] = $values['data']->get_name() . ' ×' . $values['quantity']; - } - $product_names = apply_filters( 'woocommerce_shipping_package_details_array', $product_names, $package ); - } - - wc_get_template( - 'cart/cart-shipping.php', - array( - 'package' => $package, - 'available_methods' => $package['rates'], - 'show_package_details' => count( $packages ) > 1, - 'show_shipping_calculator' => is_cart() && apply_filters( 'woocommerce_shipping_show_shipping_calculator', $first, $i, $package ), - 'package_details' => implode( ', ', $product_names ), - /* translators: %d: shipping package number */ - 'package_name' => apply_filters( 'woocommerce_shipping_package_name', ( ( $i + 1 ) > 1 ) ? sprintf( _x( 'Shipping %d', 'shipping packages', 'woocommerce' ), ( $i + 1 ) ) : _x( 'Shipping', 'shipping packages', 'woocommerce' ), $i, $package ), - 'index' => $i, - 'chosen_method' => $chosen_method, - 'formatted_destination' => WC()->countries->get_formatted_address( $package['destination'], ', ' ), - 'has_calculated_shipping' => WC()->customer->has_calculated_shipping(), - ) - ); - - $first = false; - } -} - -/** - * Get taxes total. - */ -function wc_cart_totals_taxes_total_html() { - echo apply_filters( 'woocommerce_cart_totals_taxes_total_html', wc_price( WC()->cart->get_taxes_total() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -} - -/** - * Get a coupon label. - * - * @param string|WC_Coupon $coupon Coupon data or code. - * @param bool $echo Echo or return. - * - * @return string - */ -function wc_cart_totals_coupon_label( $coupon, $echo = true ) { - if ( is_string( $coupon ) ) { - $coupon = new WC_Coupon( $coupon ); - } - - /* translators: %s: coupon code */ - $label = apply_filters( 'woocommerce_cart_totals_coupon_label', sprintf( esc_html__( 'Coupon: %s', 'woocommerce' ), $coupon->get_code() ), $coupon ); - - if ( $echo ) { - echo $label; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } else { - return $label; - } -} - -/** - * Get coupon display HTML. - * - * @param string|WC_Coupon $coupon Coupon data or code. - */ -function wc_cart_totals_coupon_html( $coupon ) { - if ( is_string( $coupon ) ) { - $coupon = new WC_Coupon( $coupon ); - } - - $discount_amount_html = ''; - - $amount = WC()->cart->get_coupon_discount_amount( $coupon->get_code(), WC()->cart->display_cart_ex_tax ); - $discount_amount_html = '-' . wc_price( $amount ); - - if ( $coupon->get_free_shipping() && empty( $amount ) ) { - $discount_amount_html = __( 'Free shipping coupon', 'woocommerce' ); - } - - $discount_amount_html = apply_filters( 'woocommerce_coupon_discount_amount_html', $discount_amount_html, $coupon ); - $coupon_html = $discount_amount_html . ' ' . __( '[Remove]', 'woocommerce' ) . ''; - - echo wp_kses( apply_filters( 'woocommerce_cart_totals_coupon_html', $coupon_html, $coupon, $discount_amount_html ), array_replace_recursive( wp_kses_allowed_html( 'post' ), array( 'a' => array( 'data-coupon' => true ) ) ) ); // phpcs:ignore PHPCompatibility.PHP.NewFunctions.array_replace_recursiveFound -} - -/** - * Get order total html including inc tax if needed. - */ -function wc_cart_totals_order_total_html() { - $value = '' . WC()->cart->get_total() . ' '; - - // If prices are tax inclusive, show taxes here. - if ( wc_tax_enabled() && WC()->cart->display_prices_including_tax() ) { - $tax_string_array = array(); - $cart_tax_totals = WC()->cart->get_tax_totals(); - - if ( get_option( 'woocommerce_tax_total_display' ) === 'itemized' ) { - foreach ( $cart_tax_totals as $code => $tax ) { - $tax_string_array[] = sprintf( '%s %s', $tax->formatted_amount, $tax->label ); - } - } elseif ( ! empty( $cart_tax_totals ) ) { - $tax_string_array[] = sprintf( '%s %s', wc_price( WC()->cart->get_taxes_total( true, true ) ), WC()->countries->tax_or_vat() ); - } - - if ( ! empty( $tax_string_array ) ) { - $taxable_address = WC()->customer->get_taxable_address(); - if ( WC()->customer->is_customer_outside_base() && ! WC()->customer->has_calculated_shipping() ) { - $country = WC()->countries->estimated_for_prefix( $taxable_address[0] ) . WC()->countries->countries[ $taxable_address[0] ]; - /* translators: 1: tax amount 2: country name */ - $tax_text = wp_kses_post( sprintf( __( '(includes %1$s estimated for %2$s)', 'woocommerce' ), implode( ', ', $tax_string_array ), $country ) ); - } else { - /* translators: %s: tax amount */ - $tax_text = wp_kses_post( sprintf( __( '(includes %s)', 'woocommerce' ), implode( ', ', $tax_string_array ) ) ); - } - - $value .= '' . $tax_text . ''; - } - } - - echo apply_filters( 'woocommerce_cart_totals_order_total_html', $value ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -} - -/** - * Get the fee value. - * - * @param object $fee Fee data. - */ -function wc_cart_totals_fee_html( $fee ) { - $cart_totals_fee_html = WC()->cart->display_prices_including_tax() ? wc_price( $fee->total + $fee->tax ) : wc_price( $fee->total ); - - echo apply_filters( 'woocommerce_cart_totals_fee_html', $cart_totals_fee_html, $fee ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -} - -/** - * Get a shipping methods full label including price. - * - * @param WC_Shipping_Rate $method Shipping method rate data. - * @return string - */ -function wc_cart_totals_shipping_method_label( $method ) { - $label = $method->get_label(); - $has_cost = 0 < $method->cost; - $hide_cost = ! $has_cost && in_array( $method->get_method_id(), array( 'free_shipping', 'local_pickup' ), true ); - - if ( $has_cost && ! $hide_cost ) { - if ( WC()->cart->display_prices_including_tax() ) { - $label .= ': ' . wc_price( $method->cost + $method->get_shipping_tax() ); - if ( $method->get_shipping_tax() > 0 && ! wc_prices_include_tax() ) { - $label .= ' ' . WC()->countries->inc_tax_or_vat() . ''; - } - } else { - $label .= ': ' . wc_price( $method->cost ); - if ( $method->get_shipping_tax() > 0 && wc_prices_include_tax() ) { - $label .= ' ' . WC()->countries->ex_tax_or_vat() . ''; - } - } - } - - return apply_filters( 'woocommerce_cart_shipping_method_full_label', $label, $method ); -} - -/** - * Round discount. - * - * @param double $value Amount to round. - * @param int $precision DP to round. - * @return float - */ -function wc_cart_round_discount( $value, $precision ) { - return wc_round_discount( $value, $precision ); -} - -/** - * Gets chosen shipping method IDs from chosen_shipping_methods session, without instance IDs. - * - * @since 2.6.2 - * @return string[] - */ -function wc_get_chosen_shipping_method_ids() { - $method_ids = array(); - $chosen_methods = WC()->session->get( 'chosen_shipping_methods', array() ); - foreach ( $chosen_methods as $chosen_method ) { - $chosen_method = explode( ':', $chosen_method ); - $method_ids[] = current( $chosen_method ); - } - return $method_ids; -} - -/** - * Get chosen method for package from session. - * - * @since 3.2.0 - * @param int $key Key of package. - * @param array $package Package data array. - * @return string|bool - */ -function wc_get_chosen_shipping_method_for_package( $key, $package ) { - $chosen_methods = WC()->session->get( 'chosen_shipping_methods' ); - $chosen_method = isset( $chosen_methods[ $key ] ) ? $chosen_methods[ $key ] : false; - $changed = wc_shipping_methods_have_changed( $key, $package ); - - // This is deprecated but here for BW compat. TODO: Remove in 4.0.0. - $method_counts = WC()->session->get( 'shipping_method_counts' ); - - if ( ! empty( $method_counts[ $key ] ) ) { - $method_count = absint( $method_counts[ $key ] ); - } else { - $method_count = 0; - } - - // If not set, not available, or available methods have changed, set to the DEFAULT option. - if ( ! $chosen_method || $changed || ! isset( $package['rates'][ $chosen_method ] ) || count( $package['rates'] ) !== $method_count ) { - $chosen_method = wc_get_default_shipping_method_for_package( $key, $package, $chosen_method ); - $chosen_methods[ $key ] = $chosen_method; - $method_counts[ $key ] = count( $package['rates'] ); - - WC()->session->set( 'chosen_shipping_methods', $chosen_methods ); - WC()->session->set( 'shipping_method_counts', $method_counts ); - - do_action( 'woocommerce_shipping_method_chosen', $chosen_method ); - } - return $chosen_method; -} - -/** - * Choose the default method for a package. - * - * @since 3.2.0 - * @param int $key Key of package. - * @param array $package Package data array. - * @param string $chosen_method Chosen method id. - * @return string - */ -function wc_get_default_shipping_method_for_package( $key, $package, $chosen_method ) { - $rate_keys = array_keys( $package['rates'] ); - $default = current( $rate_keys ); - $coupons = WC()->cart->get_coupons(); - foreach ( $coupons as $coupon ) { - if ( $coupon->get_free_shipping() ) { - foreach ( $rate_keys as $rate_key ) { - if ( 0 === stripos( $rate_key, 'free_shipping' ) ) { - $default = $rate_key; - break; - } - } - break; - } - } - return apply_filters( 'woocommerce_shipping_chosen_method', $default, $package['rates'], $chosen_method ); -} - -/** - * See if the methods have changed since the last request. - * - * @since 3.2.0 - * @param int $key Key of package. - * @param array $package Package data array. - * @return bool - */ -function wc_shipping_methods_have_changed( $key, $package ) { - // Lookup previous methods from session. - $previous_shipping_methods = WC()->session->get( 'previous_shipping_methods' ); - // Get new and old rates. - $new_rates = array_keys( $package['rates'] ); - $prev_rates = isset( $previous_shipping_methods[ $key ] ) ? $previous_shipping_methods[ $key ] : false; - // Update session. - $previous_shipping_methods[ $key ] = $new_rates; - WC()->session->set( 'previous_shipping_methods', $previous_shipping_methods ); - return $new_rates !== $prev_rates; -} - -/** - * Gets a hash of important product data that when changed should cause cart items to be invalidated. - * - * The woocommerce_cart_item_data_to_validate filter can be used to add custom properties. - * - * @param WC_Product $product Product object. - * @return string - */ -function wc_get_cart_item_data_hash( $product ) { - return md5( - wp_json_encode( - apply_filters( - 'woocommerce_cart_item_data_to_validate', - array( - 'type' => $product->get_type(), - 'attributes' => 'variation' === $product->get_type() ? $product->get_variation_attributes() : '', - ), - $product - ) - ) - ); -} diff --git a/includes/wc-conditional-functions.php b/includes/wc-conditional-functions.php deleted file mode 100644 index 641066cb657..00000000000 --- a/includes/wc-conditional-functions.php +++ /dev/null @@ -1,496 +0,0 @@ -query_vars['order-pay'] ); - } -} - -if ( ! function_exists( 'is_wc_endpoint_url' ) ) { - - /** - * Is_wc_endpoint_url - Check if an endpoint is showing. - * - * @param string|false $endpoint Whether endpoint. - * @return bool - */ - function is_wc_endpoint_url( $endpoint = false ) { - global $wp; - - $wc_endpoints = WC()->query->get_query_vars(); - - if ( false !== $endpoint ) { - if ( ! isset( $wc_endpoints[ $endpoint ] ) ) { - return false; - } else { - $endpoint_var = $wc_endpoints[ $endpoint ]; - } - - return isset( $wp->query_vars[ $endpoint_var ] ); - } else { - foreach ( $wc_endpoints as $key => $value ) { - if ( isset( $wp->query_vars[ $key ] ) ) { - return true; - } - } - - return false; - } - } -} - -if ( ! function_exists( 'is_account_page' ) ) { - - /** - * Is_account_page - Returns true when viewing an account page. - * - * @return bool - */ - function is_account_page() { - $page_id = wc_get_page_id( 'myaccount' ); - - return ( $page_id && is_page( $page_id ) ) || wc_post_content_has_shortcode( 'woocommerce_my_account' ) || apply_filters( 'woocommerce_is_account_page', false ); - } -} - -if ( ! function_exists( 'is_view_order_page' ) ) { - - /** - * Is_view_order_page - Returns true when on the view order page. - * - * @return bool - */ - function is_view_order_page() { - global $wp; - - $page_id = wc_get_page_id( 'myaccount' ); - - return ( $page_id && is_page( $page_id ) && isset( $wp->query_vars['view-order'] ) ); - } -} - -if ( ! function_exists( 'is_edit_account_page' ) ) { - - /** - * Check for edit account page. - * Returns true when viewing the edit account page. - * - * @since 2.5.1 - * @return bool - */ - function is_edit_account_page() { - global $wp; - - $page_id = wc_get_page_id( 'myaccount' ); - - return ( $page_id && is_page( $page_id ) && isset( $wp->query_vars['edit-account'] ) ); - } -} - -if ( ! function_exists( 'is_order_received_page' ) ) { - - /** - * Is_order_received_page - Returns true when viewing the order received page. - * - * @return bool - */ - function is_order_received_page() { - global $wp; - - $page_id = wc_get_page_id( 'checkout' ); - - return apply_filters( 'woocommerce_is_order_received_page', ( $page_id && is_page( $page_id ) && isset( $wp->query_vars['order-received'] ) ) ); - } -} - -if ( ! function_exists( 'is_add_payment_method_page' ) ) { - - /** - * Is_add_payment_method_page - Returns true when viewing the add payment method page. - * - * @return bool - */ - function is_add_payment_method_page() { - global $wp; - - $page_id = wc_get_page_id( 'myaccount' ); - - return ( $page_id && is_page( $page_id ) && ( isset( $wp->query_vars['payment-methods'] ) || isset( $wp->query_vars['add-payment-method'] ) ) ); - } -} - -if ( ! function_exists( 'is_lost_password_page' ) ) { - - /** - * Is_lost_password_page - Returns true when viewing the lost password page. - * - * @return bool - */ - function is_lost_password_page() { - global $wp; - - $page_id = wc_get_page_id( 'myaccount' ); - - return ( $page_id && is_page( $page_id ) && isset( $wp->query_vars['lost-password'] ) ); - } -} - -if ( ! function_exists( 'is_ajax' ) ) { - - /** - * Is_ajax - Returns true when the page is loaded via ajax. - * - * @return bool - */ - function is_ajax() { - return function_exists( 'wp_doing_ajax' ) ? wp_doing_ajax() : Constants::is_defined( 'DOING_AJAX' ); - } -} - -if ( ! function_exists( 'is_store_notice_showing' ) ) { - - /** - * Is_store_notice_showing - Returns true when store notice is active. - * - * @return bool - */ - function is_store_notice_showing() { - return 'no' !== get_option( 'woocommerce_demo_store', 'no' ); - } -} - -if ( ! function_exists( 'is_filtered' ) ) { - - /** - * Is_filtered - Returns true when filtering products using layered nav or price sliders. - * - * @return bool - */ - function is_filtered() { - return apply_filters( 'woocommerce_is_filtered', ( count( WC_Query::get_layered_nav_chosen_attributes() ) > 0 || isset( $_GET['max_price'] ) || isset( $_GET['min_price'] ) || isset( $_GET['rating_filter'] ) ) ); // WPCS: CSRF ok. - } -} - -if ( ! function_exists( 'taxonomy_is_product_attribute' ) ) { - - /** - * Returns true when the passed taxonomy name is a product attribute. - * - * @uses $wc_product_attributes global which stores taxonomy names upon registration - * @param string $name of the attribute. - * @return bool - */ - function taxonomy_is_product_attribute( $name ) { - global $wc_product_attributes; - - return taxonomy_exists( $name ) && array_key_exists( $name, (array) $wc_product_attributes ); - } -} - -if ( ! function_exists( 'meta_is_product_attribute' ) ) { - - /** - * Returns true when the passed meta name is a product attribute. - * - * @param string $name of the attribute. - * @param string $value of the attribute. - * @param int $product_id to check for attribute. - * @return bool - */ - function meta_is_product_attribute( $name, $value, $product_id ) { - $product = wc_get_product( $product_id ); - - if ( $product && method_exists( $product, 'get_variation_attributes' ) ) { - $variation_attributes = $product->get_variation_attributes(); - $attributes = $product->get_attributes(); - return ( in_array( $name, array_keys( $attributes ), true ) && in_array( $value, $variation_attributes[ $attributes[ $name ]['name'] ], true ) ); - } else { - return false; - } - } -} - -if ( ! function_exists( 'wc_tax_enabled' ) ) { - - /** - * Are store-wide taxes enabled? - * - * @return bool - */ - function wc_tax_enabled() { - return apply_filters( 'wc_tax_enabled', get_option( 'woocommerce_calc_taxes' ) === 'yes' ); - } -} - -if ( ! function_exists( 'wc_shipping_enabled' ) ) { - - /** - * Is shipping enabled? - * - * @return bool - */ - function wc_shipping_enabled() { - return apply_filters( 'wc_shipping_enabled', get_option( 'woocommerce_ship_to_countries' ) !== 'disabled' ); - } -} - -if ( ! function_exists( 'wc_prices_include_tax' ) ) { - - /** - * Are prices inclusive of tax? - * - * @return bool - */ - function wc_prices_include_tax() { - return wc_tax_enabled() && apply_filters( 'woocommerce_prices_include_tax', get_option( 'woocommerce_prices_include_tax' ) === 'yes' ); - } -} - -/** - * Simple check for validating a URL, it must start with http:// or https://. - * and pass FILTER_VALIDATE_URL validation. - * - * @param string $url to check. - * @return bool - */ -function wc_is_valid_url( $url ) { - - // Must start with http:// or https://. - if ( 0 !== strpos( $url, 'http://' ) && 0 !== strpos( $url, 'https://' ) ) { - return false; - } - - // Must pass validation. - if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) { - return false; - } - - return true; -} - -/** - * Check if the home URL is https. If it is, we don't need to do things such as 'force ssl'. - * - * @since 2.4.13 - * @return bool - */ -function wc_site_is_https() { - return false !== strstr( get_option( 'home' ), 'https:' ); -} - -/** - * Check if the checkout is configured for https. Look at options, WP HTTPS plugin, or the permalink itself. - * - * @since 2.5.0 - * @return bool - */ -function wc_checkout_is_https() { - return wc_site_is_https() || 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) || class_exists( 'WordPressHTTPS' ) || strstr( wc_get_page_permalink( 'checkout' ), 'https:' ); -} - -/** - * Checks whether the content passed contains a specific short code. - * - * @param string $tag Shortcode tag to check. - * @return bool - */ -function wc_post_content_has_shortcode( $tag = '' ) { - global $post; - - return is_singular() && is_a( $post, 'WP_Post' ) && has_shortcode( $post->post_content, $tag ); -} - -/** - * Check if reviews are enabled. - * - * @since 3.6.0 - * @return bool - */ -function wc_reviews_enabled() { - return 'yes' === get_option( 'woocommerce_enable_reviews' ); -} - -/** - * Check if reviews ratings are enabled. - * - * @since 3.6.0 - * @return bool - */ -function wc_review_ratings_enabled() { - return wc_reviews_enabled() && 'yes' === get_option( 'woocommerce_enable_review_rating' ); -} - -/** - * Check if review ratings are required. - * - * @since 3.6.0 - * @return bool - */ -function wc_review_ratings_required() { - return 'yes' === get_option( 'woocommerce_review_rating_required' ); -} - -/** - * Check if a CSV file is valid. - * - * @since 3.6.5 - * @param string $file File name. - * @param bool $check_path If should check for the path. - * @return bool - */ -function wc_is_file_valid_csv( $file, $check_path = true ) { - /** - * Filter check for CSV file path. - * - * @since 3.6.4 - * @param bool $check_import_file_path If requires file path check. Defaults to true. - */ - $check_import_file_path = apply_filters( 'woocommerce_csv_importer_check_import_file_path', true ); - - if ( $check_path && $check_import_file_path && false !== stripos( $file, '://' ) ) { - return false; - } - - /** - * Filter CSV valid file types. - * - * @since 3.6.5 - * @param array $valid_filetypes List of valid file types. - */ - $valid_filetypes = apply_filters( - 'woocommerce_csv_import_valid_filetypes', - array( - 'csv' => 'text/csv', - 'txt' => 'text/plain', - ) - ); - - $filetype = wp_check_filetype( $file, $valid_filetypes ); - - if ( in_array( $filetype['type'], $valid_filetypes, true ) ) { - return true; - } - - return false; -} diff --git a/includes/wc-core-functions.php b/includes/wc-core-functions.php deleted file mode 100644 index 47d0167b703..00000000000 --- a/includes/wc-core-functions.php +++ /dev/null @@ -1,2530 +0,0 @@ - null, - 'customer_id' => null, - 'customer_note' => null, - 'parent' => null, - 'created_via' => null, - 'cart_hash' => null, - 'order_id' => 0, - ); - - try { - $args = wp_parse_args( $args, $default_args ); - $order = new WC_Order( $args['order_id'] ); - - // Update props that were set (not null). - if ( ! is_null( $args['parent'] ) ) { - $order->set_parent_id( absint( $args['parent'] ) ); - } - - if ( ! is_null( $args['status'] ) ) { - $order->set_status( $args['status'] ); - } - - if ( ! is_null( $args['customer_note'] ) ) { - $order->set_customer_note( $args['customer_note'] ); - } - - if ( ! is_null( $args['customer_id'] ) ) { - $order->set_customer_id( is_numeric( $args['customer_id'] ) ? absint( $args['customer_id'] ) : 0 ); - } - - if ( ! is_null( $args['created_via'] ) ) { - $order->set_created_via( sanitize_text_field( $args['created_via'] ) ); - } - - if ( ! is_null( $args['cart_hash'] ) ) { - $order->set_cart_hash( sanitize_text_field( $args['cart_hash'] ) ); - } - - // Set these fields when creating a new order but not when updating an existing order. - if ( ! $args['order_id'] ) { - $order->set_currency( get_woocommerce_currency() ); - $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); - $order->set_customer_ip_address( WC_Geolocation::get_ip_address() ); - $order->set_customer_user_agent( wc_get_user_agent() ); - } - - // Update other order props set automatically. - $order->save(); - } catch ( Exception $e ) { - return new WP_Error( 'error', $e->getMessage() ); - } - - return $order; -} - -/** - * Update an order. Uses wc_create_order. - * - * @param array $args Order arguments. - * @return WC_Order|WP_Error - */ -function wc_update_order( $args ) { - if ( empty( $args['order_id'] ) ) { - return new WP_Error( __( 'Invalid order ID.', 'woocommerce' ) ); - } - return wc_create_order( $args ); -} - -/** - * Given a path, this will convert any of the subpaths into their corresponding tokens. - * - * @since 4.3.0 - * @param string $path The absolute path to tokenize. - * @param array $path_tokens An array keyed with the token, containing paths that should be replaced. - * @return string The tokenized path. - */ -function wc_tokenize_path( $path, $path_tokens ) { - // Order most to least specific so that the token can encompass as much of the path as possible. - uasort( - $path_tokens, - function ( $a, $b ) { - $a = strlen( $a ); - $b = strlen( $b ); - - if ( $a > $b ) { - return -1; - } - - if ( $b > $a ) { - return 1; - } - - return 0; - } - ); - - foreach ( $path_tokens as $token => $token_path ) { - if ( 0 !== strpos( $path, $token_path ) ) { - continue; - } - - $path = str_replace( $token_path, '{{' . $token . '}}', $path ); - } - - return $path; -} - -/** - * Given a tokenized path, this will expand the tokens to their full path. - * - * @since 4.3.0 - * @param string $path The absolute path to expand. - * @param array $path_tokens An array keyed with the token, containing paths that should be expanded. - * @return string The absolute path. - */ -function wc_untokenize_path( $path, $path_tokens ) { - foreach ( $path_tokens as $token => $token_path ) { - $path = str_replace( '{{' . $token . '}}', $token_path, $path ); - } - - return $path; -} - -/** - * Fetches an array containing all of the configurable path constants to be used in tokenization. - * - * @return array The key is the define and the path is the constant. - */ -function wc_get_path_define_tokens() { - $defines = array( - 'ABSPATH', - 'WP_CONTENT_DIR', - 'WP_PLUGIN_DIR', - 'WPMU_PLUGIN_DIR', - 'PLUGINDIR', - 'WP_THEME_DIR', - ); - - $path_tokens = array(); - foreach ( $defines as $define ) { - if ( defined( $define ) ) { - $path_tokens[ $define ] = constant( $define ); - } - } - - return apply_filters( 'woocommerce_get_path_define_tokens', $path_tokens ); -} - -/** - * Get template part (for templates like the shop-loop). - * - * WC_TEMPLATE_DEBUG_MODE will prevent overrides in themes from taking priority. - * - * @param mixed $slug Template slug. - * @param string $name Template name (default: ''). - */ -function wc_get_template_part( $slug, $name = '' ) { - $cache_key = sanitize_key( implode( '-', array( 'template-part', $slug, $name, Constants::get_constant( 'WC_VERSION' ) ) ) ); - $template = (string) wp_cache_get( $cache_key, 'woocommerce' ); - - if ( ! $template ) { - if ( $name ) { - $template = WC_TEMPLATE_DEBUG_MODE ? '' : locate_template( - array( - "{$slug}-{$name}.php", - WC()->template_path() . "{$slug}-{$name}.php", - ) - ); - - if ( ! $template ) { - $fallback = WC()->plugin_path() . "/templates/{$slug}-{$name}.php"; - $template = file_exists( $fallback ) ? $fallback : ''; - } - } - - if ( ! $template ) { - // If template file doesn't exist, look in yourtheme/slug.php and yourtheme/woocommerce/slug.php. - $template = WC_TEMPLATE_DEBUG_MODE ? '' : locate_template( - array( - "{$slug}.php", - WC()->template_path() . "{$slug}.php", - ) - ); - } - - // Don't cache the absolute path so that it can be shared between web servers with different paths. - $cache_path = wc_tokenize_path( $template, wc_get_path_define_tokens() ); - - wc_set_template_cache( $cache_key, $cache_path ); - } else { - // Make sure that the absolute path to the template is resolved. - $template = wc_untokenize_path( $template, wc_get_path_define_tokens() ); - } - - // Allow 3rd party plugins to filter template file from their plugin. - $template = apply_filters( 'wc_get_template_part', $template, $slug, $name ); - - if ( $template ) { - load_template( $template, false ); - } -} - -/** - * Get other templates (e.g. product attributes) passing attributes and including the file. - * - * @param string $template_name Template name. - * @param array $args Arguments. (default: array). - * @param string $template_path Template path. (default: ''). - * @param string $default_path Default path. (default: ''). - */ -function wc_get_template( $template_name, $args = array(), $template_path = '', $default_path = '' ) { - $cache_key = sanitize_key( implode( '-', array( 'template', $template_name, $template_path, $default_path, Constants::get_constant( 'WC_VERSION' ) ) ) ); - $template = (string) wp_cache_get( $cache_key, 'woocommerce' ); - - if ( ! $template ) { - $template = wc_locate_template( $template_name, $template_path, $default_path ); - - // Don't cache the absolute path so that it can be shared between web servers with different paths. - $cache_path = wc_tokenize_path( $template, wc_get_path_define_tokens() ); - - wc_set_template_cache( $cache_key, $cache_path ); - } else { - // Make sure that the absolute path to the template is resolved. - $template = wc_untokenize_path( $template, wc_get_path_define_tokens() ); - } - - // Allow 3rd party plugin filter template file from their plugin. - $filter_template = apply_filters( 'wc_get_template', $template, $template_name, $args, $template_path, $default_path ); - - if ( $filter_template !== $template ) { - if ( ! file_exists( $filter_template ) ) { - /* translators: %s template */ - wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%s does not exist.', 'woocommerce' ), '' . $filter_template . '' ), '2.1' ); - return; - } - $template = $filter_template; - } - - $action_args = array( - 'template_name' => $template_name, - 'template_path' => $template_path, - 'located' => $template, - 'args' => $args, - ); - - if ( ! empty( $args ) && is_array( $args ) ) { - if ( isset( $args['action_args'] ) ) { - wc_doing_it_wrong( - __FUNCTION__, - __( 'action_args should not be overwritten when calling wc_get_template.', 'woocommerce' ), - '3.6.0' - ); - unset( $args['action_args'] ); - } - extract( $args ); // @codingStandardsIgnoreLine - } - - do_action( 'woocommerce_before_template_part', $action_args['template_name'], $action_args['template_path'], $action_args['located'], $action_args['args'] ); - - include $action_args['located']; - - do_action( 'woocommerce_after_template_part', $action_args['template_name'], $action_args['template_path'], $action_args['located'], $action_args['args'] ); -} - -/** - * Like wc_get_template, but returns the HTML instead of outputting. - * - * @see wc_get_template - * @since 2.5.0 - * @param string $template_name Template name. - * @param array $args Arguments. (default: array). - * @param string $template_path Template path. (default: ''). - * @param string $default_path Default path. (default: ''). - * - * @return string - */ -function wc_get_template_html( $template_name, $args = array(), $template_path = '', $default_path = '' ) { - ob_start(); - wc_get_template( $template_name, $args, $template_path, $default_path ); - return ob_get_clean(); -} -/** - * Locate a template and return the path for inclusion. - * - * This is the load order: - * - * yourtheme/$template_path/$template_name - * yourtheme/$template_name - * $default_path/$template_name - * - * @param string $template_name Template name. - * @param string $template_path Template path. (default: ''). - * @param string $default_path Default path. (default: ''). - * @return string - */ -function wc_locate_template( $template_name, $template_path = '', $default_path = '' ) { - if ( ! $template_path ) { - $template_path = WC()->template_path(); - } - - if ( ! $default_path ) { - $default_path = WC()->plugin_path() . '/templates/'; - } - - // Look within passed path within the theme - this is priority. - if ( false !== strpos( $template_name, 'product_cat' ) || false !== strpos( $template_name, 'product_tag' ) ) { - $cs_template = str_replace( '_', '-', $template_name ); - $template = locate_template( - array( - trailingslashit( $template_path ) . $cs_template, - $cs_template, - ) - ); - } - - if ( empty( $template ) ) { - $template = locate_template( - array( - trailingslashit( $template_path ) . $template_name, - $template_name, - ) - ); - } - - // Get default template/. - if ( ! $template || WC_TEMPLATE_DEBUG_MODE ) { - if ( empty( $cs_template ) ) { - $template = $default_path . $template_name; - } else { - $template = $default_path . $cs_template; - } - } - - // Return what we found. - return apply_filters( 'woocommerce_locate_template', $template, $template_name, $template_path ); -} - -/** - * Add a template to the template cache. - * - * @since 4.3.0 - * @param string $cache_key Object cache key. - * @param string $template Located template. - */ -function wc_set_template_cache( $cache_key, $template ) { - wp_cache_set( $cache_key, $template, 'woocommerce' ); - - $cached_templates = wp_cache_get( 'cached_templates', 'woocommerce' ); - if ( is_array( $cached_templates ) ) { - $cached_templates[] = $cache_key; - } else { - $cached_templates = array( $cache_key ); - } - - wp_cache_set( 'cached_templates', $cached_templates, 'woocommerce' ); -} - -/** - * Clear the template cache. - * - * @since 4.3.0 - */ -function wc_clear_template_cache() { - $cached_templates = wp_cache_get( 'cached_templates', 'woocommerce' ); - if ( is_array( $cached_templates ) ) { - foreach ( $cached_templates as $cache_key ) { - wp_cache_delete( $cache_key, 'woocommerce' ); - } - - wp_cache_delete( 'cached_templates', 'woocommerce' ); - } -} - -/** - * Get Base Currency Code. - * - * @return string - */ -function get_woocommerce_currency() { - return apply_filters( 'woocommerce_currency', get_option( 'woocommerce_currency' ) ); -} - -/** - * Get full list of currency codes. - * - * Currency symbols and names should follow the Unicode CLDR recommendation (http://cldr.unicode.org/translation/currency-names) - * - * @return array - */ -function get_woocommerce_currencies() { - static $currencies; - - if ( ! isset( $currencies ) ) { - $currencies = array_unique( - apply_filters( - 'woocommerce_currencies', - array( - 'AED' => __( 'United Arab Emirates dirham', 'woocommerce' ), - 'AFN' => __( 'Afghan afghani', 'woocommerce' ), - 'ALL' => __( 'Albanian lek', 'woocommerce' ), - 'AMD' => __( 'Armenian dram', 'woocommerce' ), - 'ANG' => __( 'Netherlands Antillean guilder', 'woocommerce' ), - 'AOA' => __( 'Angolan kwanza', 'woocommerce' ), - 'ARS' => __( 'Argentine peso', 'woocommerce' ), - 'AUD' => __( 'Australian dollar', 'woocommerce' ), - 'AWG' => __( 'Aruban florin', 'woocommerce' ), - 'AZN' => __( 'Azerbaijani manat', 'woocommerce' ), - 'BAM' => __( 'Bosnia and Herzegovina convertible mark', 'woocommerce' ), - 'BBD' => __( 'Barbadian dollar', 'woocommerce' ), - 'BDT' => __( 'Bangladeshi taka', 'woocommerce' ), - 'BGN' => __( 'Bulgarian lev', 'woocommerce' ), - 'BHD' => __( 'Bahraini dinar', 'woocommerce' ), - 'BIF' => __( 'Burundian franc', 'woocommerce' ), - 'BMD' => __( 'Bermudian dollar', 'woocommerce' ), - 'BND' => __( 'Brunei dollar', 'woocommerce' ), - 'BOB' => __( 'Bolivian boliviano', 'woocommerce' ), - 'BRL' => __( 'Brazilian real', 'woocommerce' ), - 'BSD' => __( 'Bahamian dollar', 'woocommerce' ), - 'BTC' => __( 'Bitcoin', 'woocommerce' ), - 'BTN' => __( 'Bhutanese ngultrum', 'woocommerce' ), - 'BWP' => __( 'Botswana pula', 'woocommerce' ), - 'BYR' => __( 'Belarusian ruble (old)', 'woocommerce' ), - 'BYN' => __( 'Belarusian ruble', 'woocommerce' ), - 'BZD' => __( 'Belize dollar', 'woocommerce' ), - 'CAD' => __( 'Canadian dollar', 'woocommerce' ), - 'CDF' => __( 'Congolese franc', 'woocommerce' ), - 'CHF' => __( 'Swiss franc', 'woocommerce' ), - 'CLP' => __( 'Chilean peso', 'woocommerce' ), - 'CNY' => __( 'Chinese yuan', 'woocommerce' ), - 'COP' => __( 'Colombian peso', 'woocommerce' ), - 'CRC' => __( 'Costa Rican colón', 'woocommerce' ), - 'CUC' => __( 'Cuban convertible peso', 'woocommerce' ), - 'CUP' => __( 'Cuban peso', 'woocommerce' ), - 'CVE' => __( 'Cape Verdean escudo', 'woocommerce' ), - 'CZK' => __( 'Czech koruna', 'woocommerce' ), - 'DJF' => __( 'Djiboutian franc', 'woocommerce' ), - 'DKK' => __( 'Danish krone', 'woocommerce' ), - 'DOP' => __( 'Dominican peso', 'woocommerce' ), - 'DZD' => __( 'Algerian dinar', 'woocommerce' ), - 'EGP' => __( 'Egyptian pound', 'woocommerce' ), - 'ERN' => __( 'Eritrean nakfa', 'woocommerce' ), - 'ETB' => __( 'Ethiopian birr', 'woocommerce' ), - 'EUR' => __( 'Euro', 'woocommerce' ), - 'FJD' => __( 'Fijian dollar', 'woocommerce' ), - 'FKP' => __( 'Falkland Islands pound', 'woocommerce' ), - 'GBP' => __( 'Pound sterling', 'woocommerce' ), - 'GEL' => __( 'Georgian lari', 'woocommerce' ), - 'GGP' => __( 'Guernsey pound', 'woocommerce' ), - 'GHS' => __( 'Ghana cedi', 'woocommerce' ), - 'GIP' => __( 'Gibraltar pound', 'woocommerce' ), - 'GMD' => __( 'Gambian dalasi', 'woocommerce' ), - 'GNF' => __( 'Guinean franc', 'woocommerce' ), - 'GTQ' => __( 'Guatemalan quetzal', 'woocommerce' ), - 'GYD' => __( 'Guyanese dollar', 'woocommerce' ), - 'HKD' => __( 'Hong Kong dollar', 'woocommerce' ), - 'HNL' => __( 'Honduran lempira', 'woocommerce' ), - 'HRK' => __( 'Croatian kuna', 'woocommerce' ), - 'HTG' => __( 'Haitian gourde', 'woocommerce' ), - 'HUF' => __( 'Hungarian forint', 'woocommerce' ), - 'IDR' => __( 'Indonesian rupiah', 'woocommerce' ), - 'ILS' => __( 'Israeli new shekel', 'woocommerce' ), - 'IMP' => __( 'Manx pound', 'woocommerce' ), - 'INR' => __( 'Indian rupee', 'woocommerce' ), - 'IQD' => __( 'Iraqi dinar', 'woocommerce' ), - 'IRR' => __( 'Iranian rial', 'woocommerce' ), - 'IRT' => __( 'Iranian toman', 'woocommerce' ), - 'ISK' => __( 'Icelandic króna', 'woocommerce' ), - 'JEP' => __( 'Jersey pound', 'woocommerce' ), - 'JMD' => __( 'Jamaican dollar', 'woocommerce' ), - 'JOD' => __( 'Jordanian dinar', 'woocommerce' ), - 'JPY' => __( 'Japanese yen', 'woocommerce' ), - 'KES' => __( 'Kenyan shilling', 'woocommerce' ), - 'KGS' => __( 'Kyrgyzstani som', 'woocommerce' ), - 'KHR' => __( 'Cambodian riel', 'woocommerce' ), - 'KMF' => __( 'Comorian franc', 'woocommerce' ), - 'KPW' => __( 'North Korean won', 'woocommerce' ), - 'KRW' => __( 'South Korean won', 'woocommerce' ), - 'KWD' => __( 'Kuwaiti dinar', 'woocommerce' ), - 'KYD' => __( 'Cayman Islands dollar', 'woocommerce' ), - 'KZT' => __( 'Kazakhstani tenge', 'woocommerce' ), - 'LAK' => __( 'Lao kip', 'woocommerce' ), - 'LBP' => __( 'Lebanese pound', 'woocommerce' ), - 'LKR' => __( 'Sri Lankan rupee', 'woocommerce' ), - 'LRD' => __( 'Liberian dollar', 'woocommerce' ), - 'LSL' => __( 'Lesotho loti', 'woocommerce' ), - 'LYD' => __( 'Libyan dinar', 'woocommerce' ), - 'MAD' => __( 'Moroccan dirham', 'woocommerce' ), - 'MDL' => __( 'Moldovan leu', 'woocommerce' ), - 'MGA' => __( 'Malagasy ariary', 'woocommerce' ), - 'MKD' => __( 'Macedonian denar', 'woocommerce' ), - 'MMK' => __( 'Burmese kyat', 'woocommerce' ), - 'MNT' => __( 'Mongolian tögrög', 'woocommerce' ), - 'MOP' => __( 'Macanese pataca', 'woocommerce' ), - 'MRU' => __( 'Mauritanian ouguiya', 'woocommerce' ), - 'MUR' => __( 'Mauritian rupee', 'woocommerce' ), - 'MVR' => __( 'Maldivian rufiyaa', 'woocommerce' ), - 'MWK' => __( 'Malawian kwacha', 'woocommerce' ), - 'MXN' => __( 'Mexican peso', 'woocommerce' ), - 'MYR' => __( 'Malaysian ringgit', 'woocommerce' ), - 'MZN' => __( 'Mozambican metical', 'woocommerce' ), - 'NAD' => __( 'Namibian dollar', 'woocommerce' ), - 'NGN' => __( 'Nigerian naira', 'woocommerce' ), - 'NIO' => __( 'Nicaraguan córdoba', 'woocommerce' ), - 'NOK' => __( 'Norwegian krone', 'woocommerce' ), - 'NPR' => __( 'Nepalese rupee', 'woocommerce' ), - 'NZD' => __( 'New Zealand dollar', 'woocommerce' ), - 'OMR' => __( 'Omani rial', 'woocommerce' ), - 'PAB' => __( 'Panamanian balboa', 'woocommerce' ), - 'PEN' => __( 'Sol', 'woocommerce' ), - 'PGK' => __( 'Papua New Guinean kina', 'woocommerce' ), - 'PHP' => __( 'Philippine peso', 'woocommerce' ), - 'PKR' => __( 'Pakistani rupee', 'woocommerce' ), - 'PLN' => __( 'Polish złoty', 'woocommerce' ), - 'PRB' => __( 'Transnistrian ruble', 'woocommerce' ), - 'PYG' => __( 'Paraguayan guaraní', 'woocommerce' ), - 'QAR' => __( 'Qatari riyal', 'woocommerce' ), - 'RON' => __( 'Romanian leu', 'woocommerce' ), - 'RSD' => __( 'Serbian dinar', 'woocommerce' ), - 'RUB' => __( 'Russian ruble', 'woocommerce' ), - 'RWF' => __( 'Rwandan franc', 'woocommerce' ), - 'SAR' => __( 'Saudi riyal', 'woocommerce' ), - 'SBD' => __( 'Solomon Islands dollar', 'woocommerce' ), - 'SCR' => __( 'Seychellois rupee', 'woocommerce' ), - 'SDG' => __( 'Sudanese pound', 'woocommerce' ), - 'SEK' => __( 'Swedish krona', 'woocommerce' ), - 'SGD' => __( 'Singapore dollar', 'woocommerce' ), - 'SHP' => __( 'Saint Helena pound', 'woocommerce' ), - 'SLL' => __( 'Sierra Leonean leone', 'woocommerce' ), - 'SOS' => __( 'Somali shilling', 'woocommerce' ), - 'SRD' => __( 'Surinamese dollar', 'woocommerce' ), - 'SSP' => __( 'South Sudanese pound', 'woocommerce' ), - 'STN' => __( 'São Tomé and Príncipe dobra', 'woocommerce' ), - 'SYP' => __( 'Syrian pound', 'woocommerce' ), - 'SZL' => __( 'Swazi lilangeni', 'woocommerce' ), - 'THB' => __( 'Thai baht', 'woocommerce' ), - 'TJS' => __( 'Tajikistani somoni', 'woocommerce' ), - 'TMT' => __( 'Turkmenistan manat', 'woocommerce' ), - 'TND' => __( 'Tunisian dinar', 'woocommerce' ), - 'TOP' => __( 'Tongan paʻanga', 'woocommerce' ), - 'TRY' => __( 'Turkish lira', 'woocommerce' ), - 'TTD' => __( 'Trinidad and Tobago dollar', 'woocommerce' ), - 'TWD' => __( 'New Taiwan dollar', 'woocommerce' ), - 'TZS' => __( 'Tanzanian shilling', 'woocommerce' ), - 'UAH' => __( 'Ukrainian hryvnia', 'woocommerce' ), - 'UGX' => __( 'Ugandan shilling', 'woocommerce' ), - 'USD' => __( 'United States (US) dollar', 'woocommerce' ), - 'UYU' => __( 'Uruguayan peso', 'woocommerce' ), - 'UZS' => __( 'Uzbekistani som', 'woocommerce' ), - 'VEF' => __( 'Venezuelan bolívar', 'woocommerce' ), - 'VES' => __( 'Bolívar soberano', 'woocommerce' ), - 'VND' => __( 'Vietnamese đồng', 'woocommerce' ), - 'VUV' => __( 'Vanuatu vatu', 'woocommerce' ), - 'WST' => __( 'Samoan tālā', 'woocommerce' ), - 'XAF' => __( 'Central African CFA franc', 'woocommerce' ), - 'XCD' => __( 'East Caribbean dollar', 'woocommerce' ), - 'XOF' => __( 'West African CFA franc', 'woocommerce' ), - 'XPF' => __( 'CFP franc', 'woocommerce' ), - 'YER' => __( 'Yemeni rial', 'woocommerce' ), - 'ZAR' => __( 'South African rand', 'woocommerce' ), - 'ZMW' => __( 'Zambian kwacha', 'woocommerce' ), - ) - ) - ); - } - - return $currencies; -} - -/** - * Get all available Currency symbols. - * - * Currency symbols and names should follow the Unicode CLDR recommendation (http://cldr.unicode.org/translation/currency-names) - * - * @since 4.1.0 - * @return array - */ -function get_woocommerce_currency_symbols() { - - $symbols = apply_filters( - 'woocommerce_currency_symbols', - array( - 'AED' => 'د.إ', - 'AFN' => '؋', - 'ALL' => 'L', - 'AMD' => 'AMD', - 'ANG' => 'ƒ', - 'AOA' => 'Kz', - 'ARS' => '$', - 'AUD' => '$', - 'AWG' => 'Afl.', - 'AZN' => 'AZN', - 'BAM' => 'KM', - 'BBD' => '$', - 'BDT' => '৳ ', - 'BGN' => 'лв.', - 'BHD' => '.د.ب', - 'BIF' => 'Fr', - 'BMD' => '$', - 'BND' => '$', - 'BOB' => 'Bs.', - 'BRL' => 'R$', - 'BSD' => '$', - 'BTC' => '฿', - 'BTN' => 'Nu.', - 'BWP' => 'P', - 'BYR' => 'Br', - 'BYN' => 'Br', - 'BZD' => '$', - 'CAD' => '$', - 'CDF' => 'Fr', - 'CHF' => 'CHF', - 'CLP' => '$', - 'CNY' => '¥', - 'COP' => '$', - 'CRC' => '₡', - 'CUC' => '$', - 'CUP' => '$', - 'CVE' => '$', - 'CZK' => 'Kč', - 'DJF' => 'Fr', - 'DKK' => 'DKK', - 'DOP' => 'RD$', - 'DZD' => 'د.ج', - 'EGP' => 'EGP', - 'ERN' => 'Nfk', - 'ETB' => 'Br', - 'EUR' => '€', - 'FJD' => '$', - 'FKP' => '£', - 'GBP' => '£', - 'GEL' => '₾', - 'GGP' => '£', - 'GHS' => '₵', - 'GIP' => '£', - 'GMD' => 'D', - 'GNF' => 'Fr', - 'GTQ' => 'Q', - 'GYD' => '$', - 'HKD' => '$', - 'HNL' => 'L', - 'HRK' => 'kn', - 'HTG' => 'G', - 'HUF' => 'Ft', - 'IDR' => 'Rp', - 'ILS' => '₪', - 'IMP' => '£', - 'INR' => '₹', - 'IQD' => 'ع.د', - 'IRR' => '﷼', - 'IRT' => 'تومان', - 'ISK' => 'kr.', - 'JEP' => '£', - 'JMD' => '$', - 'JOD' => 'د.ا', - 'JPY' => '¥', - 'KES' => 'KSh', - 'KGS' => 'сом', - 'KHR' => '៛', - 'KMF' => 'Fr', - 'KPW' => '₩', - 'KRW' => '₩', - 'KWD' => 'د.ك', - 'KYD' => '$', - 'KZT' => '₸', - 'LAK' => '₭', - 'LBP' => 'ل.ل', - 'LKR' => 'රු', - 'LRD' => '$', - 'LSL' => 'L', - 'LYD' => 'ل.د', - 'MAD' => 'د.م.', - 'MDL' => 'MDL', - 'MGA' => 'Ar', - 'MKD' => 'ден', - 'MMK' => 'Ks', - 'MNT' => '₮', - 'MOP' => 'P', - 'MRU' => 'UM', - 'MUR' => '₨', - 'MVR' => '.ރ', - 'MWK' => 'MK', - 'MXN' => '$', - 'MYR' => 'RM', - 'MZN' => 'MT', - 'NAD' => 'N$', - 'NGN' => '₦', - 'NIO' => 'C$', - 'NOK' => 'kr', - 'NPR' => '₨', - 'NZD' => '$', - 'OMR' => 'ر.ع.', - 'PAB' => 'B/.', - 'PEN' => 'S/', - 'PGK' => 'K', - 'PHP' => '₱', - 'PKR' => '₨', - 'PLN' => 'zł', - 'PRB' => 'р.', - 'PYG' => '₲', - 'QAR' => 'ر.ق', - 'RMB' => '¥', - 'RON' => 'lei', - 'RSD' => 'рсд', - 'RUB' => '₽', - 'RWF' => 'Fr', - 'SAR' => 'ر.س', - 'SBD' => '$', - 'SCR' => '₨', - 'SDG' => 'ج.س.', - 'SEK' => 'kr', - 'SGD' => '$', - 'SHP' => '£', - 'SLL' => 'Le', - 'SOS' => 'Sh', - 'SRD' => '$', - 'SSP' => '£', - 'STN' => 'Db', - 'SYP' => 'ل.س', - 'SZL' => 'L', - 'THB' => '฿', - 'TJS' => 'ЅМ', - 'TMT' => 'm', - 'TND' => 'د.ت', - 'TOP' => 'T$', - 'TRY' => '₺', - 'TTD' => '$', - 'TWD' => 'NT$', - 'TZS' => 'Sh', - 'UAH' => '₴', - 'UGX' => 'UGX', - 'USD' => '$', - 'UYU' => '$', - 'UZS' => 'UZS', - 'VEF' => 'Bs F', - 'VES' => 'Bs.S', - 'VND' => '₫', - 'VUV' => 'Vt', - 'WST' => 'T', - 'XAF' => 'CFA', - 'XCD' => '$', - 'XOF' => 'CFA', - 'XPF' => 'Fr', - 'YER' => '﷼', - 'ZAR' => 'R', - 'ZMW' => 'ZK', - ) - ); - - return $symbols; -} - -/** - * Get Currency symbol. - * - * Currency symbols and names should follow the Unicode CLDR recommendation (http://cldr.unicode.org/translation/currency-names) - * - * @param string $currency Currency. (default: ''). - * @return string - */ -function get_woocommerce_currency_symbol( $currency = '' ) { - if ( ! $currency ) { - $currency = get_woocommerce_currency(); - } - - $symbols = get_woocommerce_currency_symbols(); - - $currency_symbol = isset( $symbols[ $currency ] ) ? $symbols[ $currency ] : ''; - - return apply_filters( 'woocommerce_currency_symbol', $currency_symbol, $currency ); -} - -/** - * Send HTML emails from WooCommerce. - * - * @param mixed $to Receiver. - * @param mixed $subject Subject. - * @param mixed $message Message. - * @param string $headers Headers. (default: "Content-Type: text/html\r\n"). - * @param string $attachments Attachments. (default: ""). - * @return bool - */ -function wc_mail( $to, $subject, $message, $headers = "Content-Type: text/html\r\n", $attachments = '' ) { - $mailer = WC()->mailer(); - - return $mailer->send( $to, $subject, $message, $headers, $attachments ); -} - -/** - * Return "theme support" values from the current theme, if set. - * - * @since 3.3.0 - * @param string $prop Name of prop (or key::subkey for arrays of props) if you want a specific value. Leave blank to get all props as an array. - * @param mixed $default Optional value to return if the theme does not declare support for a prop. - * @return mixed Value of prop(s). - */ -function wc_get_theme_support( $prop = '', $default = null ) { - $theme_support = get_theme_support( 'woocommerce' ); - $theme_support = is_array( $theme_support ) ? $theme_support[0] : false; - - if ( ! $theme_support ) { - return $default; - } - - if ( $prop ) { - $prop_stack = explode( '::', $prop ); - $prop_key = array_shift( $prop_stack ); - - if ( isset( $theme_support[ $prop_key ] ) ) { - $value = $theme_support[ $prop_key ]; - - if ( count( $prop_stack ) ) { - foreach ( $prop_stack as $prop_key ) { - if ( is_array( $value ) && isset( $value[ $prop_key ] ) ) { - $value = $value[ $prop_key ]; - } else { - $value = $default; - break; - } - } - } - } else { - $value = $default; - } - - return $value; - } - - return $theme_support; -} - -/** - * Get an image size by name or defined dimensions. - * - * The returned variable is filtered by woocommerce_get_image_size_{image_size} filter to - * allow 3rd party customisation. - * - * Sizes defined by the theme take priority over settings. Settings are hidden when a theme - * defines sizes. - * - * @param array|string $image_size Name of the image size to get, or an array of dimensions. - * @return array Array of dimensions including width, height, and cropping mode. Cropping mode is 0 for no crop, and 1 for hard crop. - */ -function wc_get_image_size( $image_size ) { - $cache_key = 'size-' . ( is_array( $image_size ) ? implode( '-', $image_size ) : $image_size ); - $size = wp_cache_get( $cache_key, 'woocommerce' ); - - if ( $size ) { - return $size; - } - - $size = array( - 'width' => 600, - 'height' => 600, - 'crop' => 1, - ); - - if ( is_array( $image_size ) ) { - $size = array( - 'width' => isset( $image_size[0] ) ? absint( $image_size[0] ) : 600, - 'height' => isset( $image_size[1] ) ? absint( $image_size[1] ) : 600, - 'crop' => isset( $image_size[2] ) ? absint( $image_size[2] ) : 1, - ); - $image_size = $size['width'] . '_' . $size['height']; - } else { - $image_size = str_replace( 'woocommerce_', '', $image_size ); - - // Legacy size mapping. - if ( 'shop_single' === $image_size ) { - $image_size = 'single'; - } elseif ( 'shop_catalog' === $image_size ) { - $image_size = 'thumbnail'; - } elseif ( 'shop_thumbnail' === $image_size ) { - $image_size = 'gallery_thumbnail'; - } - - if ( 'single' === $image_size ) { - $size['width'] = absint( wc_get_theme_support( 'single_image_width', get_option( 'woocommerce_single_image_width', 600 ) ) ); - $size['height'] = ''; - $size['crop'] = 0; - - } elseif ( 'gallery_thumbnail' === $image_size ) { - $size['width'] = absint( wc_get_theme_support( 'gallery_thumbnail_image_width', 100 ) ); - $size['height'] = $size['width']; - $size['crop'] = 1; - - } elseif ( 'thumbnail' === $image_size ) { - $size['width'] = absint( wc_get_theme_support( 'thumbnail_image_width', get_option( 'woocommerce_thumbnail_image_width', 300 ) ) ); - $cropping = get_option( 'woocommerce_thumbnail_cropping', '1:1' ); - - if ( 'uncropped' === $cropping ) { - $size['height'] = ''; - $size['crop'] = 0; - } elseif ( 'custom' === $cropping ) { - $width = max( 1, get_option( 'woocommerce_thumbnail_cropping_custom_width', '4' ) ); - $height = max( 1, get_option( 'woocommerce_thumbnail_cropping_custom_height', '3' ) ); - $size['height'] = absint( NumberUtil::round( ( $size['width'] / $width ) * $height ) ); - $size['crop'] = 1; - } else { - $cropping_split = explode( ':', $cropping ); - $width = max( 1, current( $cropping_split ) ); - $height = max( 1, end( $cropping_split ) ); - $size['height'] = absint( NumberUtil::round( ( $size['width'] / $width ) * $height ) ); - $size['crop'] = 1; - } - } - } - - $size = apply_filters( 'woocommerce_get_image_size_' . $image_size, $size ); - - wp_cache_set( $cache_key, $size, 'woocommerce' ); - - return $size; -} - -/** - * Queue some JavaScript code to be output in the footer. - * - * @param string $code Code. - */ -function wc_enqueue_js( $code ) { - global $wc_queued_js; - - if ( empty( $wc_queued_js ) ) { - $wc_queued_js = ''; - } - - $wc_queued_js .= "\n" . $code . "\n"; -} - -/** - * Output any queued javascript code in the footer. - */ -function wc_print_js() { - global $wc_queued_js; - - if ( ! empty( $wc_queued_js ) ) { - // Sanitize. - $wc_queued_js = wp_check_invalid_utf8( $wc_queued_js ); - $wc_queued_js = preg_replace( '/&#(x)?0*(?(1)27|39);?/i', "'", $wc_queued_js ); - $wc_queued_js = str_replace( "\r", '', $wc_queued_js ); - - $js = "\n\n"; - - /** - * Queued jsfilter. - * - * @since 2.6.0 - * @param string $js JavaScript code. - */ - echo apply_filters( 'woocommerce_queued_js', $js ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - - unset( $wc_queued_js ); - } -} - -/** - * Set a cookie - wrapper for setcookie using WP constants. - * - * @param string $name Name of the cookie being set. - * @param string $value Value of the cookie. - * @param integer $expire Expiry of the cookie. - * @param bool $secure Whether the cookie should be served only over https. - * @param bool $httponly Whether the cookie is only accessible over HTTP, not scripting languages like JavaScript. @since 3.6.0. - */ -function wc_setcookie( $name, $value, $expire = 0, $secure = false, $httponly = false ) { - if ( ! headers_sent() ) { - setcookie( $name, $value, $expire, COOKIEPATH ? COOKIEPATH : '/', COOKIE_DOMAIN, $secure, apply_filters( 'woocommerce_cookie_httponly', $httponly, $name, $value, $expire, $secure ) ); - } elseif ( Constants::is_true( 'WP_DEBUG' ) ) { - headers_sent( $file, $line ); - trigger_error( "{$name} cookie cannot be set - headers already sent by {$file} on line {$line}", E_USER_NOTICE ); // @codingStandardsIgnoreLine - } -} - -/** - * Get the URL to the WooCommerce REST API. - * - * @since 2.1 - * @param string $path an endpoint to include in the URL. - * @return string the URL. - */ -function get_woocommerce_api_url( $path ) { - if ( Constants::is_defined( 'WC_API_REQUEST_VERSION' ) ) { - $version = Constants::get_constant( 'WC_API_REQUEST_VERSION' ); - } else { - $version = substr( WC_API::VERSION, 0, 1 ); - } - - $url = get_home_url( null, "wc-api/v{$version}/", is_ssl() ? 'https' : 'http' ); - - if ( ! empty( $path ) && is_string( $path ) ) { - $url .= ltrim( $path, '/' ); - } - - return $url; -} - -/** - * Get a log file path. - * - * @since 2.2 - * - * @param string $handle name. - * @return string the log file path. - */ -function wc_get_log_file_path( $handle ) { - return WC_Log_Handler_File::get_log_file_path( $handle ); -} - -/** - * Get a log file name. - * - * @since 3.3 - * - * @param string $handle Name. - * @return string The log file name. - */ -function wc_get_log_file_name( $handle ) { - return WC_Log_Handler_File::get_log_file_name( $handle ); -} - -/** - * Recursively get page children. - * - * @param int $page_id Page ID. - * @return int[] - */ -function wc_get_page_children( $page_id ) { - $page_ids = get_posts( - array( - 'post_parent' => $page_id, - 'post_type' => 'page', - 'numberposts' => -1, // @codingStandardsIgnoreLine - 'post_status' => 'any', - 'fields' => 'ids', - ) - ); - - if ( ! empty( $page_ids ) ) { - foreach ( $page_ids as $page_id ) { - $page_ids = array_merge( $page_ids, wc_get_page_children( $page_id ) ); - } - } - - return $page_ids; -} - -/** - * Flushes rewrite rules when the shop page (or it's children) gets saved. - */ -function flush_rewrite_rules_on_shop_page_save() { - $screen = get_current_screen(); - $screen_id = $screen ? $screen->id : ''; - - // Check if this is the edit page. - if ( 'page' !== $screen_id ) { - return; - } - - // Check if page is edited. - if ( empty( $_GET['post'] ) || empty( $_GET['action'] ) || ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - return; - } - - $post_id = intval( $_GET['post'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $shop_page_id = wc_get_page_id( 'shop' ); - - if ( $shop_page_id === $post_id || in_array( $post_id, wc_get_page_children( $shop_page_id ), true ) ) { - do_action( 'woocommerce_flush_rewrite_rules' ); - } -} -add_action( 'admin_footer', 'flush_rewrite_rules_on_shop_page_save' ); - -/** - * Various rewrite rule fixes. - * - * @since 2.2 - * @param array $rules Rules. - * @return array - */ -function wc_fix_rewrite_rules( $rules ) { - global $wp_rewrite; - - $permalinks = wc_get_permalink_structure(); - - // Fix the rewrite rules when the product permalink have %product_cat% flag. - if ( preg_match( '`/(.+)(/%product_cat%)`', $permalinks['product_rewrite_slug'], $matches ) ) { - foreach ( $rules as $rule => $rewrite ) { - if ( preg_match( '`^' . preg_quote( $matches[1], '`' ) . '/\(`', $rule ) && preg_match( '/^(index\.php\?product_cat)(?!(.*product))/', $rewrite ) ) { - unset( $rules[ $rule ] ); - } - } - } - - // If the shop page is used as the base, we need to handle shop page subpages to avoid 404s. - if ( ! $permalinks['use_verbose_page_rules'] ) { - return $rules; - } - - $shop_page_id = wc_get_page_id( 'shop' ); - if ( $shop_page_id ) { - $page_rewrite_rules = array(); - $subpages = wc_get_page_children( $shop_page_id ); - - // Subpage rules. - foreach ( $subpages as $subpage ) { - $uri = get_page_uri( $subpage ); - $page_rewrite_rules[ $uri . '/?$' ] = 'index.php?pagename=' . $uri; - $wp_generated_rewrite_rules = $wp_rewrite->generate_rewrite_rules( $uri, EP_PAGES, true, true, false, false ); - foreach ( $wp_generated_rewrite_rules as $key => $value ) { - $wp_generated_rewrite_rules[ $key ] = $value . '&pagename=' . $uri; - } - $page_rewrite_rules = array_merge( $page_rewrite_rules, $wp_generated_rewrite_rules ); - } - - // Merge with rules. - $rules = array_merge( $page_rewrite_rules, $rules ); - } - - return $rules; -} -add_filter( 'rewrite_rules_array', 'wc_fix_rewrite_rules' ); - -/** - * Prevent product attachment links from breaking when using complex rewrite structures. - * - * @param string $link Link. - * @param int $post_id Post ID. - * @return string - */ -function wc_fix_product_attachment_link( $link, $post_id ) { - $parent_type = get_post_type( wp_get_post_parent_id( $post_id ) ); - if ( 'product' === $parent_type || 'product_variation' === $parent_type ) { - $link = home_url( '/?attachment_id=' . $post_id ); - } - return $link; -} -add_filter( 'attachment_link', 'wc_fix_product_attachment_link', 10, 2 ); - -/** - * Protect downloads from ms-files.php in multisite. - * - * @param string $rewrite rewrite rules. - * @return string - */ -function wc_ms_protect_download_rewite_rules( $rewrite ) { - if ( ! is_multisite() || 'redirect' === get_option( 'woocommerce_file_download_method' ) ) { - return $rewrite; - } - - $rule = "\n# WooCommerce Rules - Protect Files from ms-files.php\n\n"; - $rule .= "\n"; - $rule .= "RewriteEngine On\n"; - $rule .= "RewriteCond %{QUERY_STRING} file=woocommerce_uploads/ [NC]\n"; - $rule .= "RewriteRule /ms-files.php$ - [F]\n"; - $rule .= "\n\n"; - - return $rule . $rewrite; -} -add_filter( 'mod_rewrite_rules', 'wc_ms_protect_download_rewite_rules' ); - -/** - * Formats a string in the format COUNTRY:STATE into an array. - * - * @since 2.3.0 - * @param string $country_string Country string. - * @return array - */ -function wc_format_country_state_string( $country_string ) { - if ( strstr( $country_string, ':' ) ) { - list( $country, $state ) = explode( ':', $country_string ); - } else { - $country = $country_string; - $state = ''; - } - return array( - 'country' => $country, - 'state' => $state, - ); -} - -/** - * Get the store's base location. - * - * @since 2.3.0 - * @return array - */ -function wc_get_base_location() { - $default = apply_filters( 'woocommerce_get_base_location', get_option( 'woocommerce_default_country' ) ); - - return wc_format_country_state_string( $default ); -} - -/** - * Get the customer's default location. - * - * Filtered, and set to base location or left blank. If cache-busting, - * this should only be used when 'location' is set in the querystring. - * - * @since 2.3.0 - * @return array - */ -function wc_get_customer_default_location() { - $set_default_location_to = get_option( 'woocommerce_default_customer_address', 'base' ); - $default_location = '' === $set_default_location_to ? '' : get_option( 'woocommerce_default_country', '' ); - $location = wc_format_country_state_string( apply_filters( 'woocommerce_customer_default_location', $default_location ) ); - - // Geolocation takes priority if used and if geolocation is possible. - if ( 'geolocation' === $set_default_location_to || 'geolocation_ajax' === $set_default_location_to ) { - $ua = wc_get_user_agent(); - - // Exclude common bots from geolocation by user agent. - if ( ! stristr( $ua, 'bot' ) && ! stristr( $ua, 'spider' ) && ! stristr( $ua, 'crawl' ) ) { - $geolocation = WC_Geolocation::geolocate_ip( '', true, false ); - - if ( ! empty( $geolocation['country'] ) ) { - $location = $geolocation; - } - } - } - - // Once we have a location, ensure it's valid, otherwise fallback to a valid location. - $allowed_country_codes = WC()->countries->get_allowed_countries(); - - if ( ! empty( $location['country'] ) && ! array_key_exists( $location['country'], $allowed_country_codes ) ) { - $location['country'] = current( array_keys( $allowed_country_codes ) ); - $location['state'] = ''; - } - - return apply_filters( 'woocommerce_customer_default_location_array', $location ); -} - -/** - * Get user agent string. - * - * @since 3.0.0 - * @return string - */ -function wc_get_user_agent() { - return isset( $_SERVER['HTTP_USER_AGENT'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; // @codingStandardsIgnoreLine -} - -/** - * Generate a rand hash. - * - * @since 2.4.0 - * @return string - */ -function wc_rand_hash() { - if ( ! function_exists( 'openssl_random_pseudo_bytes' ) ) { - return sha1( wp_rand() ); - } - - return bin2hex( openssl_random_pseudo_bytes( 20 ) ); // @codingStandardsIgnoreLine -} - -/** - * WC API - Hash. - * - * @since 2.4.0 - * @param string $data Message to be hashed. - * @return string - */ -function wc_api_hash( $data ) { - return hash_hmac( 'sha256', $data, 'wc-api' ); -} - -/** - * Find all possible combinations of values from the input array and return in a logical order. - * - * @since 2.5.0 - * @param array $input Input. - * @return array - */ -function wc_array_cartesian( $input ) { - $input = array_filter( $input ); - $results = array(); - $indexes = array(); - $index = 0; - - // Generate indexes from keys and values so we have a logical sort order. - foreach ( $input as $key => $values ) { - foreach ( $values as $value ) { - $indexes[ $key ][ $value ] = $index++; - } - } - - // Loop over the 2D array of indexes and generate all combinations. - foreach ( $indexes as $key => $values ) { - // When result is empty, fill with the values of the first looped array. - if ( empty( $results ) ) { - foreach ( $values as $value ) { - $results[] = array( $key => $value ); - } - } else { - // Second and subsequent input sub-array merging. - foreach ( $results as $result_key => $result ) { - foreach ( $values as $value ) { - // If the key is not set, we can set it. - if ( ! isset( $results[ $result_key ][ $key ] ) ) { - $results[ $result_key ][ $key ] = $value; - } else { - // If the key is set, we can add a new combination to the results array. - $new_combination = $results[ $result_key ]; - $new_combination[ $key ] = $value; - $results[] = $new_combination; - } - } - } - } - } - - // Sort the indexes. - arsort( $results ); - - // Convert indexes back to values. - foreach ( $results as $result_key => $result ) { - $converted_values = array(); - - // Sort the values. - arsort( $results[ $result_key ] ); - - // Convert the values. - foreach ( $results[ $result_key ] as $key => $value ) { - $converted_values[ $key ] = array_search( $value, $indexes[ $key ], true ); - } - - $results[ $result_key ] = $converted_values; - } - - return $results; -} - -/** - * Run a MySQL transaction query, if supported. - * - * @since 2.5.0 - * @param string $type Types: start (default), commit, rollback. - * @param bool $force use of transactions. - */ -function wc_transaction_query( $type = 'start', $force = false ) { - global $wpdb; - - $wpdb->hide_errors(); - - wc_maybe_define_constant( 'WC_USE_TRANSACTIONS', true ); - - if ( Constants::is_true( 'WC_USE_TRANSACTIONS' ) || $force ) { - switch ( $type ) { - case 'commit': - $wpdb->query( 'COMMIT' ); - break; - case 'rollback': - $wpdb->query( 'ROLLBACK' ); - break; - default: - $wpdb->query( 'START TRANSACTION' ); - break; - } - } -} - -/** - * Gets the url to the cart page. - * - * @since 2.5.0 - * - * @return string Url to cart page - */ -function wc_get_cart_url() { - return apply_filters( 'woocommerce_get_cart_url', wc_get_page_permalink( 'cart' ) ); -} - -/** - * Gets the url to the checkout page. - * - * @since 2.5.0 - * - * @return string Url to checkout page - */ -function wc_get_checkout_url() { - $checkout_url = wc_get_page_permalink( 'checkout' ); - if ( $checkout_url ) { - // Force SSL if needed. - if ( is_ssl() || 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ) { - $checkout_url = str_replace( 'http:', 'https:', $checkout_url ); - } - } - - return apply_filters( 'woocommerce_get_checkout_url', $checkout_url ); -} - -/** - * Register a shipping method. - * - * @since 1.5.7 - * @param string|object $shipping_method class name (string) or a class object. - */ -function woocommerce_register_shipping_method( $shipping_method ) { - WC()->shipping()->register_shipping_method( $shipping_method ); -} - -if ( ! function_exists( 'wc_get_shipping_zone' ) ) { - /** - * Get the shipping zone matching a given package from the cart. - * - * @since 2.6.0 - * @uses WC_Shipping_Zones::get_zone_matching_package - * @param array $package Shipping package. - * @return WC_Shipping_Zone - */ - function wc_get_shipping_zone( $package ) { - return WC_Shipping_Zones::get_zone_matching_package( $package ); - } -} - -/** - * Get a nice name for credit card providers. - * - * @since 2.6.0 - * @param string $type Provider Slug/Type. - * @return string - */ -function wc_get_credit_card_type_label( $type ) { - // Normalize. - $type = strtolower( $type ); - $type = str_replace( '-', ' ', $type ); - $type = str_replace( '_', ' ', $type ); - - $labels = apply_filters( - 'woocommerce_credit_card_type_labels', - array( - 'mastercard' => __( 'MasterCard', 'woocommerce' ), - 'visa' => __( 'Visa', 'woocommerce' ), - 'discover' => __( 'Discover', 'woocommerce' ), - 'american express' => __( 'American Express', 'woocommerce' ), - 'diners' => __( 'Diners', 'woocommerce' ), - 'jcb' => __( 'JCB', 'woocommerce' ), - ) - ); - - return apply_filters( 'woocommerce_get_credit_card_type_label', ( array_key_exists( $type, $labels ) ? $labels[ $type ] : ucfirst( $type ) ) ); -} - -/** - * Outputs a "back" link so admin screens can easily jump back a page. - * - * @param string $label Title of the page to return to. - * @param string $url URL of the page to return to. - */ -function wc_back_link( $label, $url ) { - echo ''; -} - -/** - * Display a WooCommerce help tip. - * - * @since 2.5.0 - * - * @param string $tip Help tip text. - * @param bool $allow_html Allow sanitized HTML if true or escape. - * @return string - */ -function wc_help_tip( $tip, $allow_html = false ) { - if ( $allow_html ) { - $tip = wc_sanitize_tooltip( $tip ); - } else { - $tip = esc_attr( $tip ); - } - - return ''; -} - -/** - * Return a list of potential postcodes for wildcard searching. - * - * @since 2.6.0 - * @param string $postcode Postcode. - * @param string $country Country to format postcode for matching. - * @return string[] - */ -function wc_get_wildcard_postcodes( $postcode, $country = '' ) { - $formatted_postcode = wc_format_postcode( $postcode, $country ); - $length = function_exists( 'mb_strlen' ) ? mb_strlen( $formatted_postcode ) : strlen( $formatted_postcode ); - $postcodes = array( - $postcode, - $formatted_postcode, - $formatted_postcode . '*', - ); - - for ( $i = 0; $i < $length; $i ++ ) { - $postcodes[] = ( function_exists( 'mb_substr' ) ? mb_substr( $formatted_postcode, 0, ( $i + 1 ) * -1 ) : substr( $formatted_postcode, 0, ( $i + 1 ) * -1 ) ) . '*'; - } - - return $postcodes; -} - -/** - * Used by shipping zones and taxes to compare a given $postcode to stored - * postcodes to find matches for numerical ranges, and wildcards. - * - * @since 2.6.0 - * @param string $postcode Postcode you want to match against stored postcodes. - * @param array $objects Array of postcode objects from Database. - * @param string $object_id_key DB column name for the ID. - * @param string $object_compare_key DB column name for the value. - * @param string $country Country from which this postcode belongs. Allows for formatting. - * @return array Array of matching object ID and matching values. - */ -function wc_postcode_location_matcher( $postcode, $objects, $object_id_key, $object_compare_key, $country = '' ) { - $postcode = wc_normalize_postcode( $postcode ); - $wildcard_postcodes = array_map( 'wc_clean', wc_get_wildcard_postcodes( $postcode, $country ) ); - $matches = array(); - - foreach ( $objects as $object ) { - $object_id = $object->$object_id_key; - $compare_against = $object->$object_compare_key; - - // Handle postcodes containing ranges. - if ( strstr( $compare_against, '...' ) ) { - $range = array_map( 'trim', explode( '...', $compare_against ) ); - - if ( 2 !== count( $range ) ) { - continue; - } - - list( $min, $max ) = $range; - - // If the postcode is non-numeric, make it numeric. - if ( ! is_numeric( $min ) || ! is_numeric( $max ) ) { - $compare = wc_make_numeric_postcode( $postcode ); - $min = str_pad( wc_make_numeric_postcode( $min ), strlen( $compare ), '0' ); - $max = str_pad( wc_make_numeric_postcode( $max ), strlen( $compare ), '0' ); - } else { - $compare = $postcode; - } - - if ( $compare >= $min && $compare <= $max ) { - $matches[ $object_id ] = isset( $matches[ $object_id ] ) ? $matches[ $object_id ] : array(); - $matches[ $object_id ][] = $compare_against; - } - } elseif ( in_array( $compare_against, $wildcard_postcodes, true ) ) { - // Wildcard and standard comparison. - $matches[ $object_id ] = isset( $matches[ $object_id ] ) ? $matches[ $object_id ] : array(); - $matches[ $object_id ][] = $compare_against; - } - } - - return $matches; -} - -/** - * Gets number of shipping methods currently enabled. Used to identify if - * shipping is configured. - * - * @since 2.6.0 - * @param bool $include_legacy Count legacy shipping methods too. - * @param bool $enabled_only Whether non-legacy shipping methods should be - * restricted to enabled ones. It doesn't affect - * legacy shipping methods. @since 4.3.0. - * @return int - */ -function wc_get_shipping_method_count( $include_legacy = false, $enabled_only = false ) { - global $wpdb; - - $transient_name = $include_legacy ? 'wc_shipping_method_count_legacy' : 'wc_shipping_method_count'; - $transient_version = WC_Cache_Helper::get_transient_version( 'shipping' ); - $transient_value = get_transient( $transient_name ); - - if ( isset( $transient_value['value'], $transient_value['version'] ) && $transient_value['version'] === $transient_version ) { - return absint( $transient_value['value'] ); - } - - $where_clause = $enabled_only ? 'WHERE is_enabled=1' : ''; - $method_count = absint( $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods ${where_clause}" ) ); - - if ( $include_legacy ) { - // Count activated methods that don't support shipping zones. - $methods = WC()->shipping()->get_shipping_methods(); - - foreach ( $methods as $method ) { - if ( isset( $method->enabled ) && 'yes' === $method->enabled && ! $method->supports( 'shipping-zones' ) ) { - $method_count++; - } - } - } - - $transient_value = array( - 'version' => $transient_version, - 'value' => $method_count, - ); - - set_transient( $transient_name, $transient_value, DAY_IN_SECONDS * 30 ); - - return $method_count; -} - -/** - * Wrapper for set_time_limit to see if it is enabled. - * - * @since 2.6.0 - * @param int $limit Time limit. - */ -function wc_set_time_limit( $limit = 0 ) { - if ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) { // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.safe_modeDeprecatedRemoved - @set_time_limit( $limit ); // @codingStandardsIgnoreLine - } -} - -/** - * Wrapper for nocache_headers which also disables page caching. - * - * @since 3.2.4 - */ -function wc_nocache_headers() { - WC_Cache_Helper::set_nocache_constants(); - nocache_headers(); -} - -/** - * Used to sort products attributes with uasort. - * - * @since 2.6.0 - * @param array $a First attribute to compare. - * @param array $b Second attribute to compare. - * @return int - */ -function wc_product_attribute_uasort_comparison( $a, $b ) { - $a_position = is_null( $a ) ? null : $a['position']; - $b_position = is_null( $b ) ? null : $b['position']; - return wc_uasort_comparison( $a_position, $b_position ); -} - -/** - * Used to sort shipping zone methods with uasort. - * - * @since 3.0.0 - * @param array $a First shipping zone method to compare. - * @param array $b Second shipping zone method to compare. - * @return int - */ -function wc_shipping_zone_method_order_uasort_comparison( $a, $b ) { - return wc_uasort_comparison( $a->method_order, $b->method_order ); -} - -/** - * User to sort checkout fields based on priority with uasort. - * - * @since 3.5.1 - * @param array $a First field to compare. - * @param array $b Second field to compare. - * @return int - */ -function wc_checkout_fields_uasort_comparison( $a, $b ) { - /* - * We are not guaranteed to get a priority - * setting. So don't compare if they don't - * exist. - */ - if ( ! isset( $a['priority'], $b['priority'] ) ) { - return 0; - } - - return wc_uasort_comparison( $a['priority'], $b['priority'] ); -} - -/** - * User to sort two values with ausort. - * - * @since 3.5.1 - * @param int $a First value to compare. - * @param int $b Second value to compare. - * @return int - */ -function wc_uasort_comparison( $a, $b ) { - if ( $a === $b ) { - return 0; - } - return ( $a < $b ) ? -1 : 1; -} - -/** - * Sort values based on ascii, usefull for special chars in strings. - * - * @param string $a First value. - * @param string $b Second value. - * @return int - */ -function wc_ascii_uasort_comparison( $a, $b ) { - // 'setlocale' is required for compatibility with PHP 8. - // Without it, 'iconv' will return '?'s instead of transliterated characters. - $prev_locale = setlocale( LC_CTYPE, 0 ); - setlocale( LC_ALL, 'C.UTF-8' ); - - // phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged - if ( function_exists( 'iconv' ) && defined( 'ICONV_IMPL' ) && @strcasecmp( ICONV_IMPL, 'unknown' ) !== 0 ) { - $a = @iconv( 'UTF-8', 'ASCII//TRANSLIT//IGNORE', $a ); - $b = @iconv( 'UTF-8', 'ASCII//TRANSLIT//IGNORE', $b ); - } - // phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged - - setlocale( LC_ALL, $prev_locale ); - return strcmp( $a, $b ); -} - -/** - * Sort array according to current locale rules and maintaining index association. - * By default tries to use Collator from PHP Internationalization Functions if available. - * If PHP Collator class doesn't exists it fallback to removing accepts from a array - * and by sorting with `uasort( $data, 'strcmp' )` giving support for ASCII values. - * - * @since 4.6.0 - * @param array $data List of values to sort. - * @param string $locale Locale. - * @return array - */ -function wc_asort_by_locale( &$data, $locale = '' ) { - // Use Collator if PHP Internationalization Functions (php-intl) is available. - if ( class_exists( 'Collator' ) ) { - $locale = $locale ? $locale : get_locale(); - $collator = new Collator( $locale ); - $collator->asort( $data, Collator::SORT_STRING ); - return $data; - } - - $raw_data = $data; - - array_walk( - $data, - function ( &$value ) { - $value = remove_accents( html_entity_decode( $value ) ); - } - ); - - uasort( $data, 'strcmp' ); - - foreach ( $data as $key => $val ) { - $data[ $key ] = $raw_data[ $key ]; - } - - return $data; -} - -/** - * Get rounding mode for internal tax calculations. - * - * @since 3.2.4 - * @return int - */ -function wc_get_tax_rounding_mode() { - $constant = WC_TAX_ROUNDING_MODE; - - if ( 'auto' === $constant ) { - return 'yes' === get_option( 'woocommerce_prices_include_tax', 'no' ) ? 2 : 1; - } - - return intval( $constant ); -} - -/** - * Get rounding precision for internal WC calculations. - * Will increase the precision of wc_get_price_decimals by 2 decimals, unless WC_ROUNDING_PRECISION is set to a higher number. - * - * @since 2.6.3 - * @return int - */ -function wc_get_rounding_precision() { - $precision = wc_get_price_decimals() + 2; - if ( absint( WC_ROUNDING_PRECISION ) > $precision ) { - $precision = absint( WC_ROUNDING_PRECISION ); - } - return $precision; -} - -/** - * Add precision to a number and return a number. - * - * @since 3.2.0 - * @param float $value Number to add precision to. - * @param bool $round If should round after adding precision. - * @return int|float - */ -function wc_add_number_precision( $value, $round = true ) { - $cent_precision = pow( 10, wc_get_price_decimals() ); - $value = $value * $cent_precision; - return $round ? NumberUtil::round( $value, wc_get_rounding_precision() - wc_get_price_decimals() ) : $value; -} - -/** - * Remove precision from a number and return a float. - * - * @since 3.2.0 - * @param float $value Number to add precision to. - * @return float - */ -function wc_remove_number_precision( $value ) { - $cent_precision = pow( 10, wc_get_price_decimals() ); - return $value / $cent_precision; -} - -/** - * Add precision to an array of number and return an array of int. - * - * @since 3.2.0 - * @param array $value Number to add precision to. - * @param bool $round Should we round after adding precision?. - * @return int|array - */ -function wc_add_number_precision_deep( $value, $round = true ) { - if ( ! is_array( $value ) ) { - return wc_add_number_precision( $value, $round ); - } - - foreach ( $value as $key => $sub_value ) { - $value[ $key ] = wc_add_number_precision_deep( $sub_value, $round ); - } - - return $value; -} - -/** - * Remove precision from an array of number and return an array of int. - * - * @since 3.2.0 - * @param array $value Number to add precision to. - * @return int|array - */ -function wc_remove_number_precision_deep( $value ) { - if ( ! is_array( $value ) ) { - return wc_remove_number_precision( $value ); - } - - foreach ( $value as $key => $sub_value ) { - $value[ $key ] = wc_remove_number_precision_deep( $sub_value ); - } - - return $value; -} - -/** - * Get a shared logger instance. - * - * Use the woocommerce_logging_class filter to change the logging class. You may provide one of the following: - * - a class name which will be instantiated as `new $class` with no arguments - * - an instance which will be used directly as the logger - * In either case, the class or instance *must* implement WC_Logger_Interface. - * - * @see WC_Logger_Interface - * - * @return WC_Logger - */ -function wc_get_logger() { - static $logger = null; - - $class = apply_filters( 'woocommerce_logging_class', 'WC_Logger' ); - - if ( null !== $logger && is_string( $class ) && is_a( $logger, $class ) ) { - return $logger; - } - - $implements = class_implements( $class ); - - if ( is_array( $implements ) && in_array( 'WC_Logger_Interface', $implements, true ) ) { - $logger = is_object( $class ) ? $class : new $class(); - } else { - wc_doing_it_wrong( - __FUNCTION__, - sprintf( - /* translators: 1: class name 2: woocommerce_logging_class 3: WC_Logger_Interface */ - __( 'The class %1$s provided by %2$s filter must implement %3$s.', 'woocommerce' ), - '' . esc_html( is_object( $class ) ? get_class( $class ) : $class ) . '', - 'woocommerce_logging_class', - 'WC_Logger_Interface' - ), - '3.0' - ); - - $logger = is_a( $logger, 'WC_Logger' ) ? $logger : new WC_Logger(); - } - - return $logger; -} - -/** - * Trigger logging cleanup using the logging class. - * - * @since 3.4.0 - */ -function wc_cleanup_logs() { - $logger = wc_get_logger(); - - if ( is_callable( array( $logger, 'clear_expired_logs' ) ) ) { - $logger->clear_expired_logs(); - } -} -add_action( 'woocommerce_cleanup_logs', 'wc_cleanup_logs' ); - -/** - * Prints human-readable information about a variable. - * - * Some server environments block some debugging functions. This function provides a safe way to - * turn an expression into a printable, readable form without calling blocked functions. - * - * @since 3.0 - * - * @param mixed $expression The expression to be printed. - * @param bool $return Optional. Default false. Set to true to return the human-readable string. - * @return string|bool False if expression could not be printed. True if the expression was printed. - * If $return is true, a string representation will be returned. - */ -function wc_print_r( $expression, $return = false ) { - $alternatives = array( - array( - 'func' => 'print_r', - 'args' => array( $expression, true ), - ), - array( - 'func' => 'var_export', - 'args' => array( $expression, true ), - ), - array( - 'func' => 'json_encode', - 'args' => array( $expression ), - ), - array( - 'func' => 'serialize', - 'args' => array( $expression ), - ), - ); - - $alternatives = apply_filters( 'woocommerce_print_r_alternatives', $alternatives, $expression ); - - foreach ( $alternatives as $alternative ) { - if ( function_exists( $alternative['func'] ) ) { - $res = $alternative['func']( ...$alternative['args'] ); - if ( $return ) { - return $res; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } - - echo $res; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - return true; - } - } - - return false; -} - -/** - * Registers the default log handler. - * - * @since 3.0 - * @param array $handlers Handlers. - * @return array - */ -function wc_register_default_log_handler( $handlers ) { - $handler_class = Constants::get_constant( 'WC_LOG_HANDLER' ); - if ( ! class_exists( $handler_class ) ) { - $handler_class = WC_Log_Handler_File::class; - } - - array_push( $handlers, new $handler_class() ); - - return $handlers; -} -add_filter( 'woocommerce_register_log_handlers', 'wc_register_default_log_handler' ); - -/** - * Based on wp_list_pluck, this calls a method instead of returning a property. - * - * @since 3.0.0 - * @param array $list List of objects or arrays. - * @param int|string $callback_or_field Callback method from the object to place instead of the entire object. - * @param int|string $index_key Optional. Field from the object to use as keys for the new array. - * Default null. - * @return array Array of values. - */ -function wc_list_pluck( $list, $callback_or_field, $index_key = null ) { - // Use wp_list_pluck if this isn't a callback. - $first_el = current( $list ); - if ( ! is_object( $first_el ) || ! is_callable( array( $first_el, $callback_or_field ) ) ) { - return wp_list_pluck( $list, $callback_or_field, $index_key ); - } - if ( ! $index_key ) { - /* - * This is simple. Could at some point wrap array_column() - * if we knew we had an array of arrays. - */ - foreach ( $list as $key => $value ) { - $list[ $key ] = $value->{$callback_or_field}(); - } - return $list; - } - - /* - * When index_key is not set for a particular item, push the value - * to the end of the stack. This is how array_column() behaves. - */ - $newlist = array(); - foreach ( $list as $value ) { - // Get index. @since 3.2.0 this supports a callback. - if ( is_callable( array( $value, $index_key ) ) ) { - $newlist[ $value->{$index_key}() ] = $value->{$callback_or_field}(); - } elseif ( isset( $value->$index_key ) ) { - $newlist[ $value->$index_key ] = $value->{$callback_or_field}(); - } else { - $newlist[] = $value->{$callback_or_field}(); - } - } - return $newlist; -} - -/** - * Get permalink settings for things like products and taxonomies. - * - * As of 3.3.0, the permalink settings are stored to the option instead of - * being blank and inheritting from the locale. This speeds up page loading - * times by negating the need to switch locales on each page load. - * - * This is more inline with WP core behavior which does not localize slugs. - * - * @since 3.0.0 - * @return array - */ -function wc_get_permalink_structure() { - $saved_permalinks = (array) get_option( 'woocommerce_permalinks', array() ); - $permalinks = wp_parse_args( - array_filter( $saved_permalinks ), - array( - 'product_base' => _x( 'product', 'slug', 'woocommerce' ), - 'category_base' => _x( 'product-category', 'slug', 'woocommerce' ), - 'tag_base' => _x( 'product-tag', 'slug', 'woocommerce' ), - 'attribute_base' => '', - 'use_verbose_page_rules' => false, - ) - ); - - if ( $saved_permalinks !== $permalinks ) { - update_option( 'woocommerce_permalinks', $permalinks ); - } - - $permalinks['product_rewrite_slug'] = untrailingslashit( $permalinks['product_base'] ); - $permalinks['category_rewrite_slug'] = untrailingslashit( $permalinks['category_base'] ); - $permalinks['tag_rewrite_slug'] = untrailingslashit( $permalinks['tag_base'] ); - $permalinks['attribute_rewrite_slug'] = untrailingslashit( $permalinks['attribute_base'] ); - - return $permalinks; -} - -/** - * Switch WooCommerce to site language. - * - * @since 3.1.0 - */ -function wc_switch_to_site_locale() { - if ( function_exists( 'switch_to_locale' ) ) { - switch_to_locale( get_locale() ); - - // Filter on plugin_locale so load_plugin_textdomain loads the correct locale. - add_filter( 'plugin_locale', 'get_locale' ); - - // Init WC locale. - WC()->load_plugin_textdomain(); - } -} - -/** - * Switch WooCommerce language to original. - * - * @since 3.1.0 - */ -function wc_restore_locale() { - if ( function_exists( 'restore_previous_locale' ) ) { - restore_previous_locale(); - - // Remove filter. - remove_filter( 'plugin_locale', 'get_locale' ); - - // Init WC locale. - WC()->load_plugin_textdomain(); - } -} - -/** - * Convert plaintext phone number to clickable phone number. - * - * Remove formatting and allow "+". - * Example and specs: https://developer.mozilla.org/en/docs/Web/HTML/Element/a#Creating_a_phone_link - * - * @since 3.1.0 - * - * @param string $phone Content to convert phone number. - * @return string Content with converted phone number. - */ -function wc_make_phone_clickable( $phone ) { - $number = trim( preg_replace( '/[^\d|\+]/', '', $phone ) ); - - return $number ? '' . esc_html( $phone ) . '' : ''; -} - -/** - * Get an item of post data if set, otherwise return a default value. - * - * @since 3.0.9 - * @param string $key Meta key. - * @param string $default Default value. - * @return mixed Value sanitized by wc_clean. - */ -function wc_get_post_data_by_key( $key, $default = '' ) { - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing - return wc_clean( wp_unslash( wc_get_var( $_POST[ $key ], $default ) ) ); -} - -/** - * Get data if set, otherwise return a default value or null. Prevents notices when data is not set. - * - * @since 3.2.0 - * @param mixed $var Variable. - * @param string $default Default value. - * @return mixed - */ -function wc_get_var( &$var, $default = null ) { - return isset( $var ) ? $var : $default; -} - -/** - * Read in WooCommerce headers when reading plugin headers. - * - * @since 3.2.0 - * @param array $headers Headers. - * @return array - */ -function wc_enable_wc_plugin_headers( $headers ) { - if ( ! class_exists( 'WC_Plugin_Updates' ) ) { - include_once dirname( __FILE__ ) . '/admin/plugin-updates/class-wc-plugin-updates.php'; - } - - // WC requires at least - allows developers to define which version of WooCommerce the plugin requires to run. - $headers[] = WC_Plugin_Updates::VERSION_REQUIRED_HEADER; - - // WC tested up to - allows developers to define which version of WooCommerce they have tested up to. - $headers[] = WC_Plugin_Updates::VERSION_TESTED_HEADER; - - // Woo - This is used in WooCommerce extensions and is picked up by the helper. - $headers[] = 'Woo'; - - return $headers; -} -add_filter( 'extra_theme_headers', 'wc_enable_wc_plugin_headers' ); -add_filter( 'extra_plugin_headers', 'wc_enable_wc_plugin_headers' ); - -/** - * Prevent auto-updating the WooCommerce plugin on major releases if there are untested extensions active. - * - * @since 3.2.0 - * @param bool $should_update If should update. - * @param object $plugin Plugin data. - * @return bool - */ -function wc_prevent_dangerous_auto_updates( $should_update, $plugin ) { - if ( ! isset( $plugin->plugin, $plugin->new_version ) ) { - return $should_update; - } - - if ( 'woocommerce/woocommerce.php' !== $plugin->plugin ) { - return $should_update; - } - - if ( ! class_exists( 'WC_Plugin_Updates' ) ) { - include_once dirname( __FILE__ ) . '/admin/plugin-updates/class-wc-plugin-updates.php'; - } - - $new_version = wc_clean( $plugin->new_version ); - $plugin_updates = new WC_Plugin_Updates(); - $untested_plugins = $plugin_updates->get_untested_plugins( $new_version, 'major' ); - if ( ! empty( $untested_plugins ) ) { - return false; - } - - return $should_update; -} -add_filter( 'auto_update_plugin', 'wc_prevent_dangerous_auto_updates', 99, 2 ); - -/** - * Delete expired transients. - * - * Deletes all expired transients. The multi-table delete syntax is used. - * to delete the transient record from table a, and the corresponding. - * transient_timeout record from table b. - * - * Based on code inside core's upgrade_network() function. - * - * @since 3.2.0 - * @return int Number of transients that were cleared. - */ -function wc_delete_expired_transients() { - global $wpdb; - - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared - $sql = "DELETE a, b FROM $wpdb->options a, $wpdb->options b - WHERE a.option_name LIKE %s - AND a.option_name NOT LIKE %s - AND b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) ) - AND b.option_value < %d"; - $rows = $wpdb->query( $wpdb->prepare( $sql, $wpdb->esc_like( '_transient_' ) . '%', $wpdb->esc_like( '_transient_timeout_' ) . '%', time() ) ); - - $sql = "DELETE a, b FROM $wpdb->options a, $wpdb->options b - WHERE a.option_name LIKE %s - AND a.option_name NOT LIKE %s - AND b.option_name = CONCAT( '_site_transient_timeout_', SUBSTRING( a.option_name, 17 ) ) - AND b.option_value < %d"; - $rows2 = $wpdb->query( $wpdb->prepare( $sql, $wpdb->esc_like( '_site_transient_' ) . '%', $wpdb->esc_like( '_site_transient_timeout_' ) . '%', time() ) ); - // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared - - return absint( $rows + $rows2 ); -} -add_action( 'woocommerce_installed', 'wc_delete_expired_transients' ); - -/** - * Make a URL relative, if possible. - * - * @since 3.2.0 - * @param string $url URL to make relative. - * @return string - */ -function wc_get_relative_url( $url ) { - return wc_is_external_resource( $url ) ? $url : str_replace( array( 'http://', 'https://' ), '//', $url ); -} - -/** - * See if a resource is remote. - * - * @since 3.2.0 - * @param string $url URL to check. - * @return bool - */ -function wc_is_external_resource( $url ) { - $wp_base = str_replace( array( 'http://', 'https://' ), '//', get_home_url( null, '/', 'http' ) ); - - return strstr( $url, '://' ) && ! strstr( $url, $wp_base ); -} - -/** - * See if theme/s is activate or not. - * - * @since 3.3.0 - * @param string|array $theme Theme name or array of theme names to check. - * @return boolean - */ -function wc_is_active_theme( $theme ) { - return is_array( $theme ) ? in_array( get_template(), $theme, true ) : get_template() === $theme; -} - -/** - * Is the site using a default WP theme? - * - * @return boolean - */ -function wc_is_wp_default_theme_active() { - return wc_is_active_theme( - array( - 'twentytwentyone', - 'twentytwenty', - 'twentynineteen', - 'twentyseventeen', - 'twentysixteen', - 'twentyfifteen', - 'twentyfourteen', - 'twentythirteen', - 'twentyeleven', - 'twentytwelve', - 'twentyten', - ) - ); -} - -/** - * Cleans up session data - cron callback. - * - * @since 3.3.0 - */ -function wc_cleanup_session_data() { - $session_class = apply_filters( 'woocommerce_session_handler', 'WC_Session_Handler' ); - $session = new $session_class(); - - if ( is_callable( array( $session, 'cleanup_sessions' ) ) ) { - $session->cleanup_sessions(); - } -} -add_action( 'woocommerce_cleanup_sessions', 'wc_cleanup_session_data' ); - -/** - * Convert a decimal (e.g. 3.5) to a fraction (e.g. 7/2). - * From: https://www.designedbyaturtle.co.uk/2015/converting-a-decimal-to-a-fraction-in-php/ - * - * @param float $decimal the decimal number. - * @return array|bool a 1/2 would be [1, 2] array (this can be imploded with '/' to form a string). - */ -function wc_decimal_to_fraction( $decimal ) { - if ( 0 > $decimal || ! is_numeric( $decimal ) ) { - // Negative digits need to be passed in as positive numbers and prefixed as negative once the response is imploded. - return false; - } - - if ( 0 === $decimal ) { - return array( 0, 1 ); - } - - $tolerance = 1.e-4; - $numerator = 1; - $h2 = 0; - $denominator = 0; - $k2 = 1; - $b = 1 / $decimal; - - do { - $b = 1 / $b; - $a = floor( $b ); - $aux = $numerator; - $numerator = $a * $numerator + $h2; - $h2 = $aux; - $aux = $denominator; - $denominator = $a * $denominator + $k2; - $k2 = $aux; - $b = $b - $a; - } while ( abs( $decimal - $numerator / $denominator ) > $decimal * $tolerance ); - - return array( $numerator, $denominator ); -} - -/** - * Round discount. - * - * @param double $value Amount to round. - * @param int $precision DP to round. - * @return float - */ -function wc_round_discount( $value, $precision ) { - if ( version_compare( PHP_VERSION, '5.3.0', '>=' ) ) { - return NumberUtil::round( $value, $precision, WC_DISCOUNT_ROUNDING_MODE ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.round_modeFound - } - - if ( 2 === WC_DISCOUNT_ROUNDING_MODE ) { - return wc_legacy_round_half_down( $value, $precision ); - } - - return NumberUtil::round( $value, $precision ); -} - -/** - * Return the html selected attribute if stringified $value is found in array of stringified $options - * or if stringified $value is the same as scalar stringified $options. - * - * @param string|int $value Value to find within options. - * @param string|int|array $options Options to go through when looking for value. - * @return string - */ -function wc_selected( $value, $options ) { - if ( is_array( $options ) ) { - $options = array_map( 'strval', $options ); - return selected( in_array( (string) $value, $options, true ), true, false ); - } - - return selected( $value, $options, false ); -} - -/** - * Retrieves the MySQL server version. Based on $wpdb. - * - * @since 3.4.1 - * @return array Vesion information. - */ -function wc_get_server_database_version() { - global $wpdb; - - if ( empty( $wpdb->is_mysql ) ) { - return array( - 'string' => '', - 'number' => '', - ); - } - - // phpcs:disable WordPress.DB.RestrictedFunctions, PHPCompatibility.Extensions.RemovedExtensions.mysql_DeprecatedRemoved - if ( $wpdb->use_mysqli ) { - $server_info = mysqli_get_server_info( $wpdb->dbh ); - } else { - $server_info = mysql_get_server_info( $wpdb->dbh ); - } - // phpcs:enable WordPress.DB.RestrictedFunctions, PHPCompatibility.Extensions.RemovedExtensions.mysql_DeprecatedRemoved - - return array( - 'string' => $server_info, - 'number' => preg_replace( '/([^\d.]+).*/', '', $server_info ), - ); -} - -/** - * Initialize and load the cart functionality. - * - * @since 3.6.4 - * @return void - */ -function wc_load_cart() { - if ( ! did_action( 'before_woocommerce_init' ) || doing_action( 'before_woocommerce_init' ) ) { - /* translators: 1: wc_load_cart 2: woocommerce_init */ - wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%1$s should not be called before the %2$s action.', 'woocommerce' ), 'wc_load_cart', 'woocommerce_init' ), '3.7' ); - return; - } - - // Ensure dependencies are loaded in all contexts. - include_once WC_ABSPATH . 'includes/wc-cart-functions.php'; - include_once WC_ABSPATH . 'includes/wc-notice-functions.php'; - - WC()->initialize_session(); - WC()->initialize_cart(); -} - -/** - * Test whether the context of execution comes from async action scheduler. - * - * @since 4.0.0 - * @return bool - */ -function wc_is_running_from_async_action_scheduler() { - // phpcs:ignore WordPress.Security.NonceVerification.Recommended - return isset( $_REQUEST['action'] ) && 'as_async_request_queue_runner' === $_REQUEST['action']; -} - -/** - * Polyfill for wp_cache_get_multiple for WP versions before 5.5. - * - * @param array $keys Array of keys to get from group. - * @param string $group Optional. Where the cache contents are grouped. Default empty. - * @param bool $force Optional. Whether to force an update of the local cache from the persistent - * cache. Default false. - * @return array|bool Array of values. - */ -function wc_cache_get_multiple( $keys, $group = '', $force = false ) { - if ( function_exists( 'wp_cache_get_multiple' ) ) { - return wp_cache_get_multiple( $keys, $group, $force ); - } - $values = array(); - foreach ( $keys as $key ) { - $values[ $key ] = wp_cache_get( $key, $group, $force ); - } - return $values; -} diff --git a/includes/wc-deprecated-functions.php b/includes/wc-deprecated-functions.php deleted file mode 100644 index ea5f57f7191..00000000000 --- a/includes/wc-deprecated-functions.php +++ /dev/null @@ -1,1125 +0,0 @@ -is_rest_api_request() ) { - do_action( 'deprecated_function_run', $function, $replacement, $version ); - $log_string = "The {$function} function is deprecated since version {$version}."; - $log_string .= $replacement ? " Replace with {$replacement}." : ''; - error_log( $log_string ); - } else { - _deprecated_function( $function, $version, $replacement ); - } - // @codingStandardsIgnoreEnd -} - -/** - * Wrapper for deprecated hook so we can apply some extra logic. - * - * @since 3.3.0 - * @param string $hook The hook that was used. - * @param string $version The version of WordPress that deprecated the hook. - * @param string $replacement The hook that should have been used. - * @param string $message A message regarding the change. - */ -function wc_deprecated_hook( $hook, $version, $replacement = null, $message = null ) { - // @codingStandardsIgnoreStart - if ( is_ajax() || WC()->is_rest_api_request() ) { - do_action( 'deprecated_hook_run', $hook, $replacement, $version, $message ); - - $message = empty( $message ) ? '' : ' ' . $message; - $log_string = "{$hook} is deprecated since version {$version}"; - $log_string .= $replacement ? "! Use {$replacement} instead." : ' with no alternative available.'; - - error_log( $log_string . $message ); - } else { - _deprecated_hook( $hook, $version, $replacement, $message ); - } - // @codingStandardsIgnoreEnd -} - -/** - * When catching an exception, this allows us to log it if unexpected. - * - * @since 3.3.0 - * @param Exception $exception_object The exception object. - * @param string $function The function which threw exception. - * @param array $args The args passed to the function. - */ -function wc_caught_exception( $exception_object, $function = '', $args = array() ) { - // @codingStandardsIgnoreStart - $message = $exception_object->getMessage(); - $message .= '. Args: ' . print_r( $args, true ) . '.'; - - do_action( 'woocommerce_caught_exception', $exception_object, $function, $args ); - error_log( "Exception caught in {$function}. {$message}." ); - // @codingStandardsIgnoreEnd -} - -/** - * Wrapper for _doing_it_wrong(). - * - * @since 3.0.0 - * @param string $function Function used. - * @param string $message Message to log. - * @param string $version Version the message was added in. - */ -function wc_doing_it_wrong( $function, $message, $version ) { - // @codingStandardsIgnoreStart - $message .= ' Backtrace: ' . wp_debug_backtrace_summary(); - - if ( is_ajax() || WC()->is_rest_api_request() ) { - do_action( 'doing_it_wrong_run', $function, $message, $version ); - error_log( "{$function} was called incorrectly. {$message}. This message was added in version {$version}." ); - } else { - _doing_it_wrong( $function, $message, $version ); - } - // @codingStandardsIgnoreEnd -} - -/** - * Wrapper for deprecated arguments so we can apply some extra logic. - * - * @since 3.0.0 - * @param string $argument - * @param string $version - * @param string $replacement - */ -function wc_deprecated_argument( $argument, $version, $message = null ) { - if ( is_ajax() || WC()->is_rest_api_request() ) { - do_action( 'deprecated_argument_run', $argument, $message, $version ); - error_log( "The {$argument} argument is deprecated since version {$version}. {$message}" ); - } else { - _deprecated_argument( $argument, $version, $message ); - } -} - -/** - * @deprecated 2.1 - */ -function woocommerce_show_messages() { - wc_deprecated_function( 'woocommerce_show_messages', '2.1', 'wc_print_notices' ); - wc_print_notices(); -} - -/** - * @deprecated 2.1 - */ -function woocommerce_weekend_area_js() { - wc_deprecated_function( 'woocommerce_weekend_area_js', '2.1' ); -} - -/** - * @deprecated 2.1 - */ -function woocommerce_tooltip_js() { - wc_deprecated_function( 'woocommerce_tooltip_js', '2.1' ); -} - -/** - * @deprecated 2.1 - */ -function woocommerce_datepicker_js() { - wc_deprecated_function( 'woocommerce_datepicker_js', '2.1' ); -} - -/** - * @deprecated 2.1 - */ -function woocommerce_admin_scripts() { - wc_deprecated_function( 'woocommerce_admin_scripts', '2.1' ); -} - -/** - * @deprecated 2.1 - */ -function woocommerce_create_page( $slug, $option = '', $page_title = '', $page_content = '', $post_parent = 0 ) { - wc_deprecated_function( 'woocommerce_create_page', '2.1', 'wc_create_page' ); - return wc_create_page( $slug, $option, $page_title, $page_content, $post_parent ); -} - -/** - * @deprecated 2.1 - */ -function woocommerce_readfile_chunked( $file, $retbytes = true ) { - wc_deprecated_function( 'woocommerce_readfile_chunked', '2.1', 'WC_Download_Handler::readfile_chunked()' ); - return WC_Download_Handler::readfile_chunked( $file ); -} - -/** - * Formal total costs - format to the number of decimal places for the base currency. - * - * @access public - * @param mixed $number - * @deprecated 2.1 - * @return string - */ -function woocommerce_format_total( $number ) { - wc_deprecated_function( __FUNCTION__, '2.1', 'wc_format_decimal()' ); - return wc_format_decimal( $number, wc_get_price_decimals(), false ); -} - -/** - * Get product name with extra details such as SKU price and attributes. Used within admin. - * - * @access public - * @param WC_Product $product - * @deprecated 2.1 - * @return string - */ -function woocommerce_get_formatted_product_name( $product ) { - wc_deprecated_function( __FUNCTION__, '2.1', 'WC_Product::get_formatted_name()' ); - return $product->get_formatted_name(); -} - -/** - * Handle IPN requests for the legacy paypal gateway by calling gateways manually if needed. - * - * @access public - */ -function woocommerce_legacy_paypal_ipn() { - if ( ! empty( $_GET['paypalListener'] ) && 'paypal_standard_IPN' === $_GET['paypalListener'] ) { - WC()->payment_gateways(); - do_action( 'woocommerce_api_wc_gateway_paypal' ); - } -} -add_action( 'init', 'woocommerce_legacy_paypal_ipn' ); - -/** - * @deprecated 3.0 - */ -function get_product( $the_product = false, $args = array() ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_product' ); - return wc_get_product( $the_product, $args ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_protected_product_add_to_cart( $passed, $product_id ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_protected_product_add_to_cart' ); - return wc_protected_product_add_to_cart( $passed, $product_id ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_empty_cart() { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_empty_cart' ); - wc_empty_cart(); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_load_persistent_cart( $user_login, $user = 0 ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_load_persistent_cart' ); - return wc_load_persistent_cart( $user_login, $user ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_add_to_cart_message( $product_id ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_add_to_cart_message' ); - wc_add_to_cart_message( $product_id ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_clear_cart_after_payment() { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_clear_cart_after_payment' ); - wc_clear_cart_after_payment(); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_cart_totals_subtotal_html() { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_subtotal_html' ); - wc_cart_totals_subtotal_html(); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_cart_totals_shipping_html() { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_shipping_html' ); - wc_cart_totals_shipping_html(); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_cart_totals_coupon_html( $coupon ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_coupon_html' ); - wc_cart_totals_coupon_html( $coupon ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_cart_totals_order_total_html() { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_order_total_html' ); - wc_cart_totals_order_total_html(); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_cart_totals_fee_html( $fee ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_fee_html' ); - wc_cart_totals_fee_html( $fee ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_cart_totals_shipping_method_label( $method ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_shipping_method_label' ); - return wc_cart_totals_shipping_method_label( $method ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_get_template_part( $slug, $name = '' ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_template_part' ); - wc_get_template_part( $slug, $name ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_get_template( $template_name, $args = array(), $template_path = '', $default_path = '' ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_template' ); - wc_get_template( $template_name, $args, $template_path, $default_path ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_locate_template( $template_name, $template_path = '', $default_path = '' ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_locate_template' ); - return wc_locate_template( $template_name, $template_path, $default_path ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_mail( $to, $subject, $message, $headers = "Content-Type: text/html\r\n", $attachments = "" ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_mail' ); - wc_mail( $to, $subject, $message, $headers, $attachments ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_disable_admin_bar( $show_admin_bar ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_disable_admin_bar' ); - return wc_disable_admin_bar( $show_admin_bar ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_create_new_customer( $email, $username = '', $password = '' ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_create_new_customer' ); - return wc_create_new_customer( $email, $username, $password ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_set_customer_auth_cookie( $customer_id ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_set_customer_auth_cookie' ); - wc_set_customer_auth_cookie( $customer_id ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_update_new_customer_past_orders( $customer_id ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_update_new_customer_past_orders' ); - return wc_update_new_customer_past_orders( $customer_id ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_paying_customer( $order_id ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_paying_customer' ); - wc_paying_customer( $order_id ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_customer_bought_product( $customer_email, $user_id, $product_id ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_customer_bought_product' ); - return wc_customer_bought_product( $customer_email, $user_id, $product_id ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_customer_has_capability( $allcaps, $caps, $args ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_customer_has_capability' ); - return wc_customer_has_capability( $allcaps, $caps, $args ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_sanitize_taxonomy_name( $taxonomy ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_sanitize_taxonomy_name' ); - return wc_sanitize_taxonomy_name( $taxonomy ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_get_filename_from_url( $file_url ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_filename_from_url' ); - return wc_get_filename_from_url( $file_url ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_get_dimension( $dim, $to_unit ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_dimension' ); - return wc_get_dimension( $dim, $to_unit ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_get_weight( $weight, $to_unit ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_weight' ); - return wc_get_weight( $weight, $to_unit ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_trim_zeros( $price ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_trim_zeros' ); - return wc_trim_zeros( $price ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_round_tax_total( $tax ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_round_tax_total' ); - return wc_round_tax_total( $tax ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_format_decimal( $number, $dp = false, $trim_zeros = false ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_format_decimal' ); - return wc_format_decimal( $number, $dp, $trim_zeros ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_clean( $var ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_clean' ); - return wc_clean( $var ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_array_overlay( $a1, $a2 ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_array_overlay' ); - return wc_array_overlay( $a1, $a2 ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_price( $price, $args = array() ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_price' ); - return wc_price( $price, $args ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_let_to_num( $size ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_let_to_num' ); - return wc_let_to_num( $size ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_date_format() { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_date_format' ); - return wc_date_format(); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_time_format() { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_time_format' ); - return wc_time_format(); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_timezone_string() { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_timezone_string' ); - return wc_timezone_string(); -} - -if ( ! function_exists( 'woocommerce_rgb_from_hex' ) ) { - /** - * @deprecated 3.0 - */ - function woocommerce_rgb_from_hex( $color ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_rgb_from_hex' ); - return wc_rgb_from_hex( $color ); - } -} - -if ( ! function_exists( 'woocommerce_hex_darker' ) ) { - /** - * @deprecated 3.0 - */ - function woocommerce_hex_darker( $color, $factor = 30 ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_hex_darker' ); - return wc_hex_darker( $color, $factor ); - } -} - -if ( ! function_exists( 'woocommerce_hex_lighter' ) ) { - /** - * @deprecated 3.0 - */ - function woocommerce_hex_lighter( $color, $factor = 30 ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_hex_lighter' ); - return wc_hex_lighter( $color, $factor ); - } -} - -if ( ! function_exists( 'woocommerce_light_or_dark' ) ) { - /** - * @deprecated 3.0 - */ - function woocommerce_light_or_dark( $color, $dark = '#000000', $light = '#FFFFFF' ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_light_or_dark' ); - return wc_light_or_dark( $color, $dark, $light ); - } -} - -if ( ! function_exists( 'woocommerce_format_hex' ) ) { - /** - * @deprecated 3.0 - */ - function woocommerce_format_hex( $hex ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_format_hex' ); - return wc_format_hex( $hex ); - } -} - -/** - * @deprecated 3.0 - */ -function woocommerce_get_order_id_by_order_key( $order_key ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_order_id_by_order_key' ); - return wc_get_order_id_by_order_key( $order_key ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_downloadable_file_permission( $download_id, $product_id, $order ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_downloadable_file_permission' ); - return wc_downloadable_file_permission( $download_id, $product_id, $order ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_downloadable_product_permissions( $order_id ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_downloadable_product_permissions' ); - wc_downloadable_product_permissions( $order_id ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_add_order_item( $order_id, $item ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_add_order_item' ); - return wc_add_order_item( $order_id, $item ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_delete_order_item( $item_id ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_delete_order_item' ); - return wc_delete_order_item( $item_id ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_update_order_item_meta( $item_id, $meta_key, $meta_value, $prev_value = '' ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_update_order_item_meta' ); - return wc_update_order_item_meta( $item_id, $meta_key, $meta_value, $prev_value ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_add_order_item_meta( $item_id, $meta_key, $meta_value, $unique = false ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_add_order_item_meta' ); - return wc_add_order_item_meta( $item_id, $meta_key, $meta_value, $unique ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_delete_order_item_meta( $item_id, $meta_key, $meta_value = '', $delete_all = false ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_delete_order_item_meta' ); - return wc_delete_order_item_meta( $item_id, $meta_key, $meta_value, $delete_all ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_get_order_item_meta( $item_id, $key, $single = true ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_order_item_meta' ); - return wc_get_order_item_meta( $item_id, $key, $single ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_cancel_unpaid_orders() { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cancel_unpaid_orders' ); - wc_cancel_unpaid_orders(); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_processing_order_count() { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_processing_order_count' ); - return wc_processing_order_count(); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_get_page_id( $page ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_page_id' ); - return wc_get_page_id( $page ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_get_endpoint_url( $endpoint, $value = '', $permalink = '' ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_endpoint_url' ); - return wc_get_endpoint_url( $endpoint, $value, $permalink ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_lostpassword_url( $url ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_lostpassword_url' ); - return wc_lostpassword_url( $url ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_customer_edit_account_url() { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_customer_edit_account_url' ); - return wc_customer_edit_account_url(); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_nav_menu_items( $items, $args ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_nav_menu_items' ); - return wc_nav_menu_items( $items ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_nav_menu_item_classes( $menu_items, $args ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_nav_menu_item_classes' ); - return wc_nav_menu_item_classes( $menu_items ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_list_pages( $pages ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_list_pages' ); - return wc_list_pages( $pages ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_product_dropdown_categories( $args = array(), $deprecated_hierarchical = 1, $deprecated_show_uncategorized = 1, $deprecated_orderby = '' ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_product_dropdown_categories' ); - return wc_product_dropdown_categories( $args, $deprecated_hierarchical, $deprecated_show_uncategorized, $deprecated_orderby ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_walk_category_dropdown_tree( $a1 = '', $a2 = '', $a3 = '' ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_walk_category_dropdown_tree' ); - return wc_walk_category_dropdown_tree( $a1, $a2, $a3 ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_taxonomy_metadata_wpdbfix() { - wc_deprecated_function( __FUNCTION__, '3.0' ); -} - -/** - * @deprecated 3.0 - */ -function wc_taxonomy_metadata_wpdbfix() { - wc_deprecated_function( __FUNCTION__, '3.0' ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_order_terms( $the_term, $next_id, $taxonomy, $index = 0, $terms = null ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_reorder_terms' ); - return wc_reorder_terms( $the_term, $next_id, $taxonomy, $index, $terms ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_set_term_order( $term_id, $index, $taxonomy, $recursive = false ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_set_term_order' ); - return wc_set_term_order( $term_id, $index, $taxonomy, $recursive ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_terms_clauses( $clauses, $taxonomies, $args ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_terms_clauses' ); - return wc_terms_clauses( $clauses, $taxonomies, $args ); -} - -/** - * @deprecated 3.0 - */ -function _woocommerce_term_recount( $terms, $taxonomy, $callback, $terms_are_term_taxonomy_ids ) { - wc_deprecated_function( __FUNCTION__, '3.0', '_wc_term_recount' ); - return _wc_term_recount( $terms, $taxonomy, $callback, $terms_are_term_taxonomy_ids ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_recount_after_stock_change( $product_id ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_recount_after_stock_change' ); - return wc_recount_after_stock_change( $product_id ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_change_term_counts( $terms, $taxonomies, $args ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_change_term_counts' ); - return wc_change_term_counts( $terms, $taxonomies ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_get_product_ids_on_sale() { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_product_ids_on_sale' ); - return wc_get_product_ids_on_sale(); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_get_featured_product_ids() { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_featured_product_ids' ); - return wc_get_featured_product_ids(); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_get_product_terms( $object_id, $taxonomy, $fields = 'all' ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_product_terms' ); - return wc_get_product_terms( $object_id, $taxonomy, array( 'fields' => $fields ) ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_product_post_type_link( $permalink, $post ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_product_post_type_link' ); - return wc_product_post_type_link( $permalink, $post ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_placeholder_img_src() { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_placeholder_img_src' ); - return wc_placeholder_img_src(); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_placeholder_img( $size = 'woocommerce_thumbnail' ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_placeholder_img' ); - return wc_placeholder_img( $size ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_get_formatted_variation( $variation = '', $flat = false ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_formatted_variation' ); - return wc_get_formatted_variation( $variation, $flat ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_scheduled_sales() { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_scheduled_sales' ); - return wc_scheduled_sales(); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_get_attachment_image_attributes( $attr ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_attachment_image_attributes' ); - return wc_get_attachment_image_attributes( $attr ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_prepare_attachment_for_js( $response ) { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_prepare_attachment_for_js' ); - return wc_prepare_attachment_for_js( $response ); -} - -/** - * @deprecated 3.0 - */ -function woocommerce_track_product_view() { - wc_deprecated_function( __FUNCTION__, '3.0', 'wc_track_product_view' ); - return wc_track_product_view(); -} - -/** - * @deprecated 2.3 has no replacement - */ -function woocommerce_compile_less_styles() { - wc_deprecated_function( 'woocommerce_compile_less_styles', '2.3' ); -} - -/** - * woocommerce_calc_shipping was an option used to determine if shipping was enabled prior to version 2.6.0. This has since been replaced with wc_shipping_enabled() function and - * the woocommerce_ship_to_countries setting. - * @deprecated 2.6.0 - * @return string - */ -function woocommerce_calc_shipping_backwards_compatibility( $value ) { - if ( Constants::is_defined( 'WC_UPDATING' ) ) { - return $value; - } - return 'disabled' === get_option( 'woocommerce_ship_to_countries' ) ? 'no' : 'yes'; -} -add_filter( 'pre_option_woocommerce_calc_shipping', 'woocommerce_calc_shipping_backwards_compatibility' ); - -/** - * @deprecated 3.0.0 - * @see WC_Structured_Data class - * - * @return string - */ -function woocommerce_get_product_schema() { - wc_deprecated_function( 'woocommerce_get_product_schema', '3.0' ); - - global $product; - - $schema = "Product"; - - // Downloadable product schema handling - if ( $product->is_downloadable() ) { - switch ( $product->download_type ) { - case 'application' : - $schema = "SoftwareApplication"; - break; - case 'music' : - $schema = "MusicAlbum"; - break; - default : - $schema = "Product"; - break; - } - } - - return 'http://schema.org/' . $schema; -} - -/** - * Save product price. - * - * This is a private function (internal use ONLY) used until a data manipulation api is built. - * - * @deprecated 3.0.0 - * @param int $product_id - * @param float $regular_price - * @param float $sale_price - * @param string $date_from - * @param string $date_to - */ -function _wc_save_product_price( $product_id, $regular_price, $sale_price = '', $date_from = '', $date_to = '' ) { - wc_doing_it_wrong( '_wc_save_product_price()', 'This function is not for developer use and is deprecated.', '3.0' ); - - $product_id = absint( $product_id ); - $regular_price = wc_format_decimal( $regular_price ); - $sale_price = '' === $sale_price ? '' : wc_format_decimal( $sale_price ); - $date_from = wc_clean( $date_from ); - $date_to = wc_clean( $date_to ); - - update_post_meta( $product_id, '_regular_price', $regular_price ); - update_post_meta( $product_id, '_sale_price', $sale_price ); - - // Save Dates - update_post_meta( $product_id, '_sale_price_dates_from', $date_from ? strtotime( $date_from ) : '' ); - update_post_meta( $product_id, '_sale_price_dates_to', $date_to ? strtotime( $date_to ) : '' ); - - if ( $date_to && ! $date_from ) { - $date_from = strtotime( 'NOW', current_time( 'timestamp' ) ); - update_post_meta( $product_id, '_sale_price_dates_from', $date_from ); - } - - // Update price if on sale - if ( '' !== $sale_price && '' === $date_to && '' === $date_from ) { - update_post_meta( $product_id, '_price', $sale_price ); - } else { - update_post_meta( $product_id, '_price', $regular_price ); - } - - if ( '' !== $sale_price && $date_from && strtotime( $date_from ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { - update_post_meta( $product_id, '_price', $sale_price ); - } - - if ( $date_to && strtotime( $date_to ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { - update_post_meta( $product_id, '_price', $regular_price ); - update_post_meta( $product_id, '_sale_price_dates_from', '' ); - update_post_meta( $product_id, '_sale_price_dates_to', '' ); - } -} - -/** - * Return customer avatar URL. - * - * @deprecated 3.1.0 - * @since 2.6.0 - * @param string $email the customer's email. - * @return string the URL to the customer's avatar. - */ -function wc_get_customer_avatar_url( $email ) { - // Deprecated in favor of WordPress get_avatar_url() function. - wc_deprecated_function( 'wc_get_customer_avatar_url()', '3.1', 'get_avatar_url()' ); - - return get_avatar_url( $email ); -} - -/** - * WooCommerce Core Supported Themes. - * - * @deprecated 3.3.0 - * @since 2.2 - * @return string[] - */ -function wc_get_core_supported_themes() { - wc_deprecated_function( 'wc_get_core_supported_themes()', '3.3' ); - return array( 'twentyseventeen', 'twentysixteen', 'twentyfifteen', 'twentyfourteen', 'twentythirteen', 'twentyeleven', 'twentytwelve', 'twentyten' ); -} - -/** - * Get min/max price meta query args. - * - * @deprecated 3.6.0 - * @since 3.0.0 - * @param array $args Min price and max price arguments. - * @return array - */ -function wc_get_min_max_price_meta_query( $args ) { - wc_deprecated_function( 'wc_get_min_max_price_meta_query()', '3.6' ); - - $current_min_price = isset( $args['min_price'] ) ? floatval( $args['min_price'] ) : 0; - $current_max_price = isset( $args['max_price'] ) ? floatval( $args['max_price'] ) : PHP_INT_MAX; - - return apply_filters( - 'woocommerce_get_min_max_price_meta_query', - array( - 'key' => '_price', - 'value' => array( $current_min_price, $current_max_price ), - 'compare' => 'BETWEEN', - 'type' => 'DECIMAL(10,' . wc_get_price_decimals() . ')', - ), - $args - ); -} - -/** - * When a term is split, ensure meta data maintained. - * - * @deprecated 3.6.0 - * @param int $old_term_id Old term ID. - * @param int $new_term_id New term ID. - * @param string $term_taxonomy_id Term taxonomy ID. - * @param string $taxonomy Taxonomy. - */ -function wc_taxonomy_metadata_update_content_for_split_terms( $old_term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) { - wc_deprecated_function( 'wc_taxonomy_metadata_update_content_for_split_terms', '3.6' ); -} - -/** - * WooCommerce Term Meta API. - * - * WC tables for storing term meta are deprecated from WordPress 4.4 since 4.4 has its own table. - * This function serves as a wrapper, using the new table if present, or falling back to the WC table. - * - * @deprecated 3.6.0 - * @param int $term_id Term ID. - * @param string $meta_key Meta key. - * @param mixed $meta_value Meta value. - * @param string $prev_value Previous value. (default: ''). - * @return bool - */ -function update_woocommerce_term_meta( $term_id, $meta_key, $meta_value, $prev_value = '' ) { - wc_deprecated_function( 'update_woocommerce_term_meta', '3.6', 'update_term_meta' ); - return function_exists( 'update_term_meta' ) ? update_term_meta( $term_id, $meta_key, $meta_value, $prev_value ) : update_metadata( 'woocommerce_term', $term_id, $meta_key, $meta_value, $prev_value ); -} - -/** - * WooCommerce Term Meta API. - * - * WC tables for storing term meta are deprecated from WordPress 4.4 since 4.4 has its own table. - * This function serves as a wrapper, using the new table if present, or falling back to the WC table. - * - * @deprecated 3.6.0 - * @param int $term_id Term ID. - * @param string $meta_key Meta key. - * @param mixed $meta_value Meta value. - * @param bool $unique Make meta key unique. (default: false). - * @return bool - */ -function add_woocommerce_term_meta( $term_id, $meta_key, $meta_value, $unique = false ) { - wc_deprecated_function( 'add_woocommerce_term_meta', '3.6', 'add_term_meta' ); - return function_exists( 'add_term_meta' ) ? add_term_meta( $term_id, $meta_key, $meta_value, $unique ) : add_metadata( 'woocommerce_term', $term_id, $meta_key, $meta_value, $unique ); -} - -/** - * WooCommerce Term Meta API - * - * WC tables for storing term meta are deprecated from WordPress 4.4 since 4.4 has its own table. - * This function serves as a wrapper, using the new table if present, or falling back to the WC table. - * - * @deprecated 3.6.0 - * @param int $term_id Term ID. - * @param string $meta_key Meta key. - * @param string $meta_value Meta value (default: ''). - * @param bool $deprecated Deprecated param (default: false). - * @return bool - */ -function delete_woocommerce_term_meta( $term_id, $meta_key, $meta_value = '', $deprecated = false ) { - wc_deprecated_function( 'delete_woocommerce_term_meta', '3.6', 'delete_term_meta' ); - return function_exists( 'delete_term_meta' ) ? delete_term_meta( $term_id, $meta_key, $meta_value ) : delete_metadata( 'woocommerce_term', $term_id, $meta_key, $meta_value ); -} - -/** - * WooCommerce Term Meta API - * - * WC tables for storing term meta are deprecated from WordPress 4.4 since 4.4 has its own table. - * This function serves as a wrapper, using the new table if present, or falling back to the WC table. - * - * @deprecated 3.6.0 - * @param int $term_id Term ID. - * @param string $key Meta key. - * @param bool $single Whether to return a single value. (default: true). - * @return mixed - */ -function get_woocommerce_term_meta( $term_id, $key, $single = true ) { - wc_deprecated_function( 'get_woocommerce_term_meta', '3.6', 'get_term_meta' ); - return function_exists( 'get_term_meta' ) ? get_term_meta( $term_id, $key, $single ) : get_metadata( 'woocommerce_term', $term_id, $key, $single ); -} diff --git a/includes/wc-formatting-functions.php b/includes/wc-formatting-functions.php deleted file mode 100644 index a48b3df5937..00000000000 --- a/includes/wc-formatting-functions.php +++ /dev/null @@ -1,1516 +0,0 @@ -strip_invalid_text_for_column( $wpdb->options, 'option_value', $value ); - - if ( is_wp_error( $value ) ) { - $value = ''; - } - - $value = esc_url_raw( trim( $value ) ); - $value = str_replace( 'http://', '', $value ); - return untrailingslashit( $value ); -} - -/** - * Gets the filename part of a download URL. - * - * @param string $file_url File URL. - * @return string - */ -function wc_get_filename_from_url( $file_url ) { - $parts = wp_parse_url( $file_url ); - if ( isset( $parts['path'] ) ) { - return basename( $parts['path'] ); - } -} - -/** - * Normalise dimensions, unify to cm then convert to wanted unit value. - * - * Usage: - * wc_get_dimension( 55, 'in' ); - * wc_get_dimension( 55, 'in', 'm' ); - * - * @param int|float $dimension Dimension. - * @param string $to_unit Unit to convert to. - * Options: 'in', 'm', 'cm', 'm'. - * @param string $from_unit Unit to convert from. - * Defaults to ''. - * Options: 'in', 'm', 'cm', 'm'. - * @return float - */ -function wc_get_dimension( $dimension, $to_unit, $from_unit = '' ) { - $to_unit = strtolower( $to_unit ); - - if ( empty( $from_unit ) ) { - $from_unit = strtolower( get_option( 'woocommerce_dimension_unit' ) ); - } - - // Unify all units to cm first. - if ( $from_unit !== $to_unit ) { - switch ( $from_unit ) { - case 'in': - $dimension *= 2.54; - break; - case 'm': - $dimension *= 100; - break; - case 'mm': - $dimension *= 0.1; - break; - case 'yd': - $dimension *= 91.44; - break; - } - - // Output desired unit. - switch ( $to_unit ) { - case 'in': - $dimension *= 0.3937; - break; - case 'm': - $dimension *= 0.01; - break; - case 'mm': - $dimension *= 10; - break; - case 'yd': - $dimension *= 0.010936133; - break; - } - } - - return ( $dimension < 0 ) ? 0 : $dimension; -} - -/** - * Normalise weights, unify to kg then convert to wanted unit value. - * - * Usage: - * wc_get_weight(55, 'kg'); - * wc_get_weight(55, 'kg', 'lbs'); - * - * @param int|float $weight Weight. - * @param string $to_unit Unit to convert to. - * Options: 'g', 'kg', 'lbs', 'oz'. - * @param string $from_unit Unit to convert from. - * Defaults to ''. - * Options: 'g', 'kg', 'lbs', 'oz'. - * @return float - */ -function wc_get_weight( $weight, $to_unit, $from_unit = '' ) { - $weight = (float) $weight; - $to_unit = strtolower( $to_unit ); - - if ( empty( $from_unit ) ) { - $from_unit = strtolower( get_option( 'woocommerce_weight_unit' ) ); - } - - // Unify all units to kg first. - if ( $from_unit !== $to_unit ) { - switch ( $from_unit ) { - case 'g': - $weight *= 0.001; - break; - case 'lbs': - $weight *= 0.453592; - break; - case 'oz': - $weight *= 0.0283495; - break; - } - - // Output desired unit. - switch ( $to_unit ) { - case 'g': - $weight *= 1000; - break; - case 'lbs': - $weight *= 2.20462; - break; - case 'oz': - $weight *= 35.274; - break; - } - } - - return ( $weight < 0 ) ? 0 : $weight; -} - -/** - * Trim trailing zeros off prices. - * - * @param string|float|int $price Price. - * @return string - */ -function wc_trim_zeros( $price ) { - return preg_replace( '/' . preg_quote( wc_get_price_decimal_separator(), '/' ) . '0++$/', '', $price ); -} - -/** - * Round a tax amount. - * - * @param double $value Amount to round. - * @param int $precision DP to round. Defaults to wc_get_price_decimals. - * @return float - */ -function wc_round_tax_total( $value, $precision = null ) { - $precision = is_null( $precision ) ? wc_get_price_decimals() : intval( $precision ); - - if ( version_compare( PHP_VERSION, '5.3.0', '>=' ) ) { - $rounded_tax = NumberUtil::round( $value, $precision, wc_get_tax_rounding_mode() ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.round_modeFound - } elseif ( 2 === wc_get_tax_rounding_mode() ) { - $rounded_tax = wc_legacy_round_half_down( $value, $precision ); - } else { - $rounded_tax = NumberUtil::round( $value, $precision ); - } - - return apply_filters( 'wc_round_tax_total', $rounded_tax, $value, $precision, WC_TAX_ROUNDING_MODE ); -} - -/** - * Round half down in PHP 5.2. - * - * @since 3.2.6 - * @param float $value Value to round. - * @param int $precision Precision to round down to. - * @return float - */ -function wc_legacy_round_half_down( $value, $precision ) { - $value = wc_float_to_string( $value ); - - if ( false !== strstr( $value, '.' ) ) { - $value = explode( '.', $value ); - - if ( strlen( $value[1] ) > $precision && substr( $value[1], -1 ) === '5' ) { - $value[1] = substr( $value[1], 0, -1 ) . '4'; - } - - $value = implode( '.', $value ); - } - - return NumberUtil::round( floatval( $value ), $precision ); -} - -/** - * Make a refund total negative. - * - * @param float $amount Refunded amount. - * - * @return float - */ -function wc_format_refund_total( $amount ) { - return $amount * -1; -} - -/** - * Format decimal numbers ready for DB storage. - * - * Sanitize, remove decimals, and optionally round + trim off zeros. - * - * This function does not remove thousands - this should be done before passing a value to the function. - * - * @param float|string $number Expects either a float or a string with a decimal separator only (no thousands). - * @param mixed $dp number Number of decimal points to use, blank to use woocommerce_price_num_decimals, or false to avoid all rounding. - * @param bool $trim_zeros From end of string. - * @return string - */ -function wc_format_decimal( $number, $dp = false, $trim_zeros = false ) { - $locale = localeconv(); - $decimals = array( wc_get_price_decimal_separator(), $locale['decimal_point'], $locale['mon_decimal_point'] ); - - // Remove locale from string. - if ( ! is_float( $number ) ) { - $number = str_replace( $decimals, '.', $number ); - - // Convert multiple dots to just one. - $number = preg_replace( '/\.(?![^.]+$)|[^0-9.-]/', '', wc_clean( $number ) ); - } - - if ( false !== $dp ) { - $dp = intval( '' === $dp ? wc_get_price_decimals() : $dp ); - $number = number_format( floatval( $number ), $dp, '.', '' ); - } elseif ( is_float( $number ) ) { - // DP is false - don't use number format, just return a string using whatever is given. Remove scientific notation using sprintf. - $number = str_replace( $decimals, '.', sprintf( '%.' . wc_get_rounding_precision() . 'f', $number ) ); - // We already had a float, so trailing zeros are not needed. - $trim_zeros = true; - } - - if ( $trim_zeros && strstr( $number, '.' ) ) { - $number = rtrim( rtrim( $number, '0' ), '.' ); - } - - return $number; -} - -/** - * Convert a float to a string without locale formatting which PHP adds when changing floats to strings. - * - * @param float $float Float value to format. - * @return string - */ -function wc_float_to_string( $float ) { - if ( ! is_float( $float ) ) { - return $float; - } - - $locale = localeconv(); - $string = strval( $float ); - $string = str_replace( $locale['decimal_point'], '.', $string ); - - return $string; -} - -/** - * Format a price with WC Currency Locale settings. - * - * @param string $value Price to localize. - * @return string - */ -function wc_format_localized_price( $value ) { - return apply_filters( 'woocommerce_format_localized_price', str_replace( '.', wc_get_price_decimal_separator(), strval( $value ) ), $value ); -} - -/** - * Format a decimal with PHP Locale settings. - * - * @param string $value Decimal to localize. - * @return string - */ -function wc_format_localized_decimal( $value ) { - $locale = localeconv(); - return apply_filters( 'woocommerce_format_localized_decimal', str_replace( '.', $locale['decimal_point'], strval( $value ) ), $value ); -} - -/** - * Format a coupon code. - * - * @since 3.0.0 - * @param string $value Coupon code to format. - * @return string - */ -function wc_format_coupon_code( $value ) { - return apply_filters( 'woocommerce_coupon_code', $value ); -} - -/** - * Sanitize a coupon code. - * - * Uses sanitize_post_field since coupon codes are stored as - * post_titles - the sanitization and escaping must match. - * - * @since 3.6.0 - * @param string $value Coupon code to format. - * @return string - */ -function wc_sanitize_coupon_code( $value ) { - return wp_filter_kses( sanitize_post_field( 'post_title', $value, 0, 'db' ) ); -} - -/** - * Clean variables using sanitize_text_field. Arrays are cleaned recursively. - * Non-scalar values are ignored. - * - * @param string|array $var Data to sanitize. - * @return string|array - */ -function wc_clean( $var ) { - if ( is_array( $var ) ) { - return array_map( 'wc_clean', $var ); - } else { - return is_scalar( $var ) ? sanitize_text_field( $var ) : $var; - } -} - -/** - * Function wp_check_invalid_utf8 with recursive array support. - * - * @param string|array $var Data to sanitize. - * @return string|array - */ -function wc_check_invalid_utf8( $var ) { - if ( is_array( $var ) ) { - return array_map( 'wc_check_invalid_utf8', $var ); - } else { - return wp_check_invalid_utf8( $var ); - } -} - -/** - * Run wc_clean over posted textarea but maintain line breaks. - * - * @since 3.0.0 - * @param string $var Data to sanitize. - * @return string - */ -function wc_sanitize_textarea( $var ) { - return implode( "\n", array_map( 'wc_clean', explode( "\n", $var ) ) ); -} - -/** - * Sanitize a string destined to be a tooltip. - * - * @since 2.3.10 Tooltips are encoded with htmlspecialchars to prevent XSS. Should not be used in conjunction with esc_attr() - * @param string $var Data to sanitize. - * @return string - */ -function wc_sanitize_tooltip( $var ) { - return htmlspecialchars( - wp_kses( - html_entity_decode( $var ), - array( - 'br' => array(), - 'em' => array(), - 'strong' => array(), - 'small' => array(), - 'span' => array(), - 'ul' => array(), - 'li' => array(), - 'ol' => array(), - 'p' => array(), - ) - ) - ); -} - -/** - * Merge two arrays. - * - * @param array $a1 First array to merge. - * @param array $a2 Second array to merge. - * @return array - */ -function wc_array_overlay( $a1, $a2 ) { - foreach ( $a1 as $k => $v ) { - if ( ! array_key_exists( $k, $a2 ) ) { - continue; - } - if ( is_array( $v ) && is_array( $a2[ $k ] ) ) { - $a1[ $k ] = wc_array_overlay( $v, $a2[ $k ] ); - } else { - $a1[ $k ] = $a2[ $k ]; - } - } - return $a1; -} - -/** - * Formats a stock amount by running it through a filter. - * - * @param int|float $amount Stock amount. - * @return int|float - */ -function wc_stock_amount( $amount ) { - return apply_filters( 'woocommerce_stock_amount', $amount ); -} - -/** - * Get the price format depending on the currency position. - * - * @return string - */ -function get_woocommerce_price_format() { - $currency_pos = get_option( 'woocommerce_currency_pos' ); - $format = '%1$s%2$s'; - - switch ( $currency_pos ) { - case 'left': - $format = '%1$s%2$s'; - break; - case 'right': - $format = '%2$s%1$s'; - break; - case 'left_space': - $format = '%1$s %2$s'; - break; - case 'right_space': - $format = '%2$s %1$s'; - break; - } - - return apply_filters( 'woocommerce_price_format', $format, $currency_pos ); -} - -/** - * Return the thousand separator for prices. - * - * @since 2.3 - * @return string - */ -function wc_get_price_thousand_separator() { - return stripslashes( apply_filters( 'wc_get_price_thousand_separator', get_option( 'woocommerce_price_thousand_sep' ) ) ); -} - -/** - * Return the decimal separator for prices. - * - * @since 2.3 - * @return string - */ -function wc_get_price_decimal_separator() { - $separator = apply_filters( 'wc_get_price_decimal_separator', get_option( 'woocommerce_price_decimal_sep' ) ); - return $separator ? stripslashes( $separator ) : '.'; -} - -/** - * Return the number of decimals after the decimal point. - * - * @since 2.3 - * @return int - */ -function wc_get_price_decimals() { - return absint( apply_filters( 'wc_get_price_decimals', get_option( 'woocommerce_price_num_decimals', 2 ) ) ); -} - -/** - * Format the price with a currency symbol. - * - * @param float $price Raw price. - * @param array $args Arguments to format a price { - * Array of arguments. - * Defaults to empty array. - * - * @type bool $ex_tax_label Adds exclude tax label. - * Defaults to false. - * @type string $currency Currency code. - * Defaults to empty string (Use the result from get_woocommerce_currency()). - * @type string $decimal_separator Decimal separator. - * Defaults the result of wc_get_price_decimal_separator(). - * @type string $thousand_separator Thousand separator. - * Defaults the result of wc_get_price_thousand_separator(). - * @type string $decimals Number of decimals. - * Defaults the result of wc_get_price_decimals(). - * @type string $price_format Price format depending on the currency position. - * Defaults the result of get_woocommerce_price_format(). - * } - * @return string - */ -function wc_price( $price, $args = array() ) { - $args = apply_filters( - 'wc_price_args', - wp_parse_args( - $args, - array( - 'ex_tax_label' => false, - 'currency' => '', - 'decimal_separator' => wc_get_price_decimal_separator(), - 'thousand_separator' => wc_get_price_thousand_separator(), - 'decimals' => wc_get_price_decimals(), - 'price_format' => get_woocommerce_price_format(), - ) - ) - ); - - $original_price = $price; - - // Convert to float to avoid issues on PHP 8. - $price = (float) $price; - - $unformatted_price = $price; - $negative = $price < 0; - - /** - * Filter raw price. - * - * @param float $raw_price Raw price. - * @param float|string $original_price Original price as float, or empty string. Since 5.0.0. - */ - $price = apply_filters( 'raw_woocommerce_price', $negative ? $price * -1 : $price, $original_price ); - - /** - * Filter formatted price. - * - * @param float $formatted_price Formatted price. - * @param float $price Unformatted price. - * @param int $decimals Number of decimals. - * @param string $decimal_separator Decimal separator. - * @param string $thousand_separator Thousand separator. - * @param float|string $original_price Original price as float, or empty string. Since 5.0.0. - */ - $price = apply_filters( 'formatted_woocommerce_price', number_format( $price, $args['decimals'], $args['decimal_separator'], $args['thousand_separator'] ), $price, $args['decimals'], $args['decimal_separator'], $args['thousand_separator'], $original_price ); - - if ( apply_filters( 'woocommerce_price_trim_zeros', false ) && $args['decimals'] > 0 ) { - $price = wc_trim_zeros( $price ); - } - - $formatted_price = ( $negative ? '-' : '' ) . sprintf( $args['price_format'], '' . get_woocommerce_currency_symbol( $args['currency'] ) . '', $price ); - $return = '' . $formatted_price . ''; - - if ( $args['ex_tax_label'] && wc_tax_enabled() ) { - $return .= ' ' . WC()->countries->ex_tax_or_vat() . ''; - } - - /** - * Filters the string of price markup. - * - * @param string $return Price HTML markup. - * @param string $price Formatted price. - * @param array $args Pass on the args. - * @param float $unformatted_price Price as float to allow plugins custom formatting. Since 3.2.0. - * @param float|string $original_price Original price as float, or empty string. Since 5.0.0. - */ - return apply_filters( 'wc_price', $return, $price, $args, $unformatted_price, $original_price ); -} - -/** - * Notation to numbers. - * - * This function transforms the php.ini notation for numbers (like '2M') to an integer. - * - * @param string $size Size value. - * @return int - */ -function wc_let_to_num( $size ) { - $l = substr( $size, -1 ); - $ret = (int) substr( $size, 0, -1 ); - switch ( strtoupper( $l ) ) { - case 'P': - $ret *= 1024; - // No break. - case 'T': - $ret *= 1024; - // No break. - case 'G': - $ret *= 1024; - // No break. - case 'M': - $ret *= 1024; - // No break. - case 'K': - $ret *= 1024; - // No break. - } - return $ret; -} - -/** - * WooCommerce Date Format - Allows to change date format for everything WooCommerce. - * - * @return string - */ -function wc_date_format() { - $date_format = get_option( 'date_format' ); - if ( empty( $date_format ) ) { - // Return default date format if the option is empty. - $date_format = 'F j, Y'; - } - return apply_filters( 'woocommerce_date_format', $date_format ); -} - -/** - * WooCommerce Time Format - Allows to change time format for everything WooCommerce. - * - * @return string - */ -function wc_time_format() { - $time_format = get_option( 'time_format' ); - if ( empty( $time_format ) ) { - // Return default time format if the option is empty. - $time_format = 'g:i a'; - } - return apply_filters( 'woocommerce_time_format', $time_format ); -} - -/** - * Convert mysql datetime to PHP timestamp, forcing UTC. Wrapper for strtotime. - * - * Based on wcs_strtotime_dark_knight() from WC Subscriptions by Prospress. - * - * @since 3.0.0 - * @param string $time_string Time string. - * @param int|null $from_timestamp Timestamp to convert from. - * @return int - */ -function wc_string_to_timestamp( $time_string, $from_timestamp = null ) { - $original_timezone = date_default_timezone_get(); - - // @codingStandardsIgnoreStart - date_default_timezone_set( 'UTC' ); - - if ( null === $from_timestamp ) { - $next_timestamp = strtotime( $time_string ); - } else { - $next_timestamp = strtotime( $time_string, $from_timestamp ); - } - - date_default_timezone_set( $original_timezone ); - // @codingStandardsIgnoreEnd - - return $next_timestamp; -} - -/** - * Convert a date string to a WC_DateTime. - * - * @since 3.1.0 - * @param string $time_string Time string. - * @return WC_DateTime - */ -function wc_string_to_datetime( $time_string ) { - // Strings are defined in local WP timezone. Convert to UTC. - if ( 1 === preg_match( '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(Z|((-|\+)\d{2}:\d{2}))$/', $time_string, $date_bits ) ) { - $offset = ! empty( $date_bits[7] ) ? iso8601_timezone_to_offset( $date_bits[7] ) : wc_timezone_offset(); - $timestamp = gmmktime( $date_bits[4], $date_bits[5], $date_bits[6], $date_bits[2], $date_bits[3], $date_bits[1] ) - $offset; - } else { - $timestamp = wc_string_to_timestamp( get_gmt_from_date( gmdate( 'Y-m-d H:i:s', wc_string_to_timestamp( $time_string ) ) ) ); - } - $datetime = new WC_DateTime( "@{$timestamp}", new DateTimeZone( 'UTC' ) ); - - // Set local timezone or offset. - if ( get_option( 'timezone_string' ) ) { - $datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) ); - } else { - $datetime->set_utc_offset( wc_timezone_offset() ); - } - - return $datetime; -} - -/** - * WooCommerce Timezone - helper to retrieve the timezone string for a site until. - * a WP core method exists (see https://core.trac.wordpress.org/ticket/24730). - * - * Adapted from https://secure.php.net/manual/en/function.timezone-name-from-abbr.php#89155. - * - * @since 2.1 - * @return string PHP timezone string for the site - */ -function wc_timezone_string() { - // Added in WordPress 5.3 Ref https://developer.wordpress.org/reference/functions/wp_timezone_string/. - if ( function_exists( 'wp_timezone_string' ) ) { - return wp_timezone_string(); - } - - // If site timezone string exists, return it. - $timezone = get_option( 'timezone_string' ); - if ( $timezone ) { - return $timezone; - } - - // Get UTC offset, if it isn't set then return UTC. - $utc_offset = floatval( get_option( 'gmt_offset', 0 ) ); - if ( ! is_numeric( $utc_offset ) || 0.0 === $utc_offset ) { - return 'UTC'; - } - - // Adjust UTC offset from hours to seconds. - $utc_offset = (int) ( $utc_offset * 3600 ); - - // Attempt to guess the timezone string from the UTC offset. - $timezone = timezone_name_from_abbr( '', $utc_offset ); - if ( $timezone ) { - return $timezone; - } - - // Last try, guess timezone string manually. - foreach ( timezone_abbreviations_list() as $abbr ) { - foreach ( $abbr as $city ) { - // WordPress restrict the use of date(), since it's affected by timezone settings, but in this case is just what we need to guess the correct timezone. - if ( (bool) date( 'I' ) === (bool) $city['dst'] && $city['timezone_id'] && intval( $city['offset'] ) === $utc_offset ) { // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date - return $city['timezone_id']; - } - } - } - - // Fallback to UTC. - return 'UTC'; -} - -/** - * Get timezone offset in seconds. - * - * @since 3.0.0 - * @return float - */ -function wc_timezone_offset() { - $timezone = get_option( 'timezone_string' ); - - if ( $timezone ) { - $timezone_object = new DateTimeZone( $timezone ); - return $timezone_object->getOffset( new DateTime( 'now' ) ); - } else { - return floatval( get_option( 'gmt_offset', 0 ) ) * HOUR_IN_SECONDS; - } -} - -/** - * Callback which can flatten post meta (gets the first value if it's an array). - * - * @since 3.0.0 - * @param array $value Value to flatten. - * @return mixed - */ -function wc_flatten_meta_callback( $value ) { - return is_array( $value ) ? current( $value ) : $value; -} - -if ( ! function_exists( 'wc_rgb_from_hex' ) ) { - - /** - * Convert RGB to HEX. - * - * @param mixed $color Color. - * - * @return array - */ - function wc_rgb_from_hex( $color ) { - $color = str_replace( '#', '', $color ); - // Convert shorthand colors to full format, e.g. "FFF" -> "FFFFFF". - $color = preg_replace( '~^(.)(.)(.)$~', '$1$1$2$2$3$3', $color ); - - $rgb = array(); - $rgb['R'] = hexdec( $color[0] . $color[1] ); - $rgb['G'] = hexdec( $color[2] . $color[3] ); - $rgb['B'] = hexdec( $color[4] . $color[5] ); - - return $rgb; - } -} - -if ( ! function_exists( 'wc_hex_darker' ) ) { - - /** - * Make HEX color darker. - * - * @param mixed $color Color. - * @param int $factor Darker factor. - * Defaults to 30. - * @return string - */ - function wc_hex_darker( $color, $factor = 30 ) { - $base = wc_rgb_from_hex( $color ); - $color = '#'; - - foreach ( $base as $k => $v ) { - $amount = $v / 100; - $amount = NumberUtil::round( $amount * $factor ); - $new_decimal = $v - $amount; - - $new_hex_component = dechex( $new_decimal ); - if ( strlen( $new_hex_component ) < 2 ) { - $new_hex_component = '0' . $new_hex_component; - } - $color .= $new_hex_component; - } - - return $color; - } -} - -if ( ! function_exists( 'wc_hex_lighter' ) ) { - - /** - * Make HEX color lighter. - * - * @param mixed $color Color. - * @param int $factor Lighter factor. - * Defaults to 30. - * @return string - */ - function wc_hex_lighter( $color, $factor = 30 ) { - $base = wc_rgb_from_hex( $color ); - $color = '#'; - - foreach ( $base as $k => $v ) { - $amount = 255 - $v; - $amount = $amount / 100; - $amount = NumberUtil::round( $amount * $factor ); - $new_decimal = $v + $amount; - - $new_hex_component = dechex( $new_decimal ); - if ( strlen( $new_hex_component ) < 2 ) { - $new_hex_component = '0' . $new_hex_component; - } - $color .= $new_hex_component; - } - - return $color; - } -} - -if ( ! function_exists( 'wc_hex_is_light' ) ) { - - /** - * Determine whether a hex color is light. - * - * @param mixed $color Color. - * @return bool True if a light color. - */ - function wc_hex_is_light( $color ) { - $hex = str_replace( '#', '', $color ); - - $c_r = hexdec( substr( $hex, 0, 2 ) ); - $c_g = hexdec( substr( $hex, 2, 2 ) ); - $c_b = hexdec( substr( $hex, 4, 2 ) ); - - $brightness = ( ( $c_r * 299 ) + ( $c_g * 587 ) + ( $c_b * 114 ) ) / 1000; - - return $brightness > 155; - } -} - -if ( ! function_exists( 'wc_light_or_dark' ) ) { - - /** - * Detect if we should use a light or dark color on a background color. - * - * @param mixed $color Color. - * @param string $dark Darkest reference. - * Defaults to '#000000'. - * @param string $light Lightest reference. - * Defaults to '#FFFFFF'. - * @return string - */ - function wc_light_or_dark( $color, $dark = '#000000', $light = '#FFFFFF' ) { - return wc_hex_is_light( $color ) ? $dark : $light; - } -} - -if ( ! function_exists( 'wc_format_hex' ) ) { - - /** - * Format string as hex. - * - * @param string $hex HEX color. - * @return string|null - */ - function wc_format_hex( $hex ) { - $hex = trim( str_replace( '#', '', $hex ) ); - - if ( strlen( $hex ) === 3 ) { - $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; - } - - return $hex ? '#' . $hex : null; - } -} - -/** - * Format the postcode according to the country and length of the postcode. - * - * @param string $postcode Unformatted postcode. - * @param string $country Base country. - * @return string - */ -function wc_format_postcode( $postcode, $country ) { - $postcode = wc_normalize_postcode( $postcode ); - - switch ( $country ) { - case 'CA': - case 'GB': - $postcode = substr_replace( $postcode, ' ', -3, 0 ); - break; - case 'IE': - $postcode = substr_replace( $postcode, ' ', 3, 0 ); - break; - case 'BR': - case 'PL': - $postcode = substr_replace( $postcode, '-', -3, 0 ); - break; - case 'JP': - $postcode = substr_replace( $postcode, '-', 3, 0 ); - break; - case 'PT': - $postcode = substr_replace( $postcode, '-', 4, 0 ); - break; - case 'PR': - case 'US': - $postcode = rtrim( substr_replace( $postcode, '-', 5, 0 ), '-' ); - break; - case 'NL': - $postcode = substr_replace( $postcode, ' ', 4, 0 ); - break; - } - - return apply_filters( 'woocommerce_format_postcode', trim( $postcode ), $country ); -} - -/** - * Normalize postcodes. - * - * Remove spaces and convert characters to uppercase. - * - * @since 2.6.0 - * @param string $postcode Postcode. - * @return string - */ -function wc_normalize_postcode( $postcode ) { - return preg_replace( '/[\s\-]/', '', trim( wc_strtoupper( $postcode ) ) ); -} - -/** - * Format phone numbers. - * - * @param string $phone Phone number. - * @return string - */ -function wc_format_phone_number( $phone ) { - if ( ! WC_Validation::is_phone( $phone ) ) { - return ''; - } - return preg_replace( '/[^0-9\+\-\(\)\s]/', '-', preg_replace( '/[\x00-\x1F\x7F-\xFF]/', '', $phone ) ); -} - -/** - * Sanitize phone number. - * Allows only numbers and "+" (plus sign). - * - * @since 3.6.0 - * @param string $phone Phone number. - * @return string - */ -function wc_sanitize_phone_number( $phone ) { - return preg_replace( '/[^\d+]/', '', $phone ); -} - -/** - * Wrapper for mb_strtoupper which see's if supported first. - * - * @since 3.1.0 - * @param string $string String to format. - * @return string - */ -function wc_strtoupper( $string ) { - return function_exists( 'mb_strtoupper' ) ? mb_strtoupper( $string ) : strtoupper( $string ); -} - -/** - * Make a string lowercase. - * Try to use mb_strtolower() when available. - * - * @since 2.3 - * @param string $string String to format. - * @return string - */ -function wc_strtolower( $string ) { - return function_exists( 'mb_strtolower' ) ? mb_strtolower( $string ) : strtolower( $string ); -} - -/** - * Trim a string and append a suffix. - * - * @param string $string String to trim. - * @param integer $chars Amount of characters. - * Defaults to 200. - * @param string $suffix Suffix. - * Defaults to '...'. - * @return string - */ -function wc_trim_string( $string, $chars = 200, $suffix = '...' ) { - if ( strlen( $string ) > $chars ) { - if ( function_exists( 'mb_substr' ) ) { - $string = mb_substr( $string, 0, ( $chars - mb_strlen( $suffix ) ) ) . $suffix; - } else { - $string = substr( $string, 0, ( $chars - strlen( $suffix ) ) ) . $suffix; - } - } - return $string; -} - -/** - * Format content to display shortcodes. - * - * @since 2.3.0 - * @param string $raw_string Raw string. - * @return string - */ -function wc_format_content( $raw_string ) { - return apply_filters( 'woocommerce_format_content', apply_filters( 'woocommerce_short_description', $raw_string ), $raw_string ); -} - -/** - * Format product short description. - * Adds support for Jetpack Markdown. - * - * @codeCoverageIgnore - * @since 2.4.0 - * @param string $content Product short description. - * @return string - */ -function wc_format_product_short_description( $content ) { - // Add support for Jetpack Markdown. - if ( class_exists( 'WPCom_Markdown' ) ) { - $markdown = WPCom_Markdown::get_instance(); - - return wpautop( - $markdown->transform( - $content, - array( - 'unslash' => false, - ) - ) - ); - } - - return $content; -} - -/** - * Formats curency symbols when saved in settings. - * - * @codeCoverageIgnore - * @param string $value Option value. - * @param array $option Option name. - * @param string $raw_value Raw value. - * @return string - */ -function wc_format_option_price_separators( $value, $option, $raw_value ) { - return wp_kses_post( $raw_value ); -} -add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_decimal_sep', 'wc_format_option_price_separators', 10, 3 ); -add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_thousand_sep', 'wc_format_option_price_separators', 10, 3 ); - -/** - * Formats decimals when saved in settings. - * - * @codeCoverageIgnore - * @param string $value Option value. - * @param array $option Option name. - * @param string $raw_value Raw value. - * @return string - */ -function wc_format_option_price_num_decimals( $value, $option, $raw_value ) { - return is_null( $raw_value ) ? 2 : absint( $raw_value ); -} -add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_num_decimals', 'wc_format_option_price_num_decimals', 10, 3 ); - -/** - * Formats hold stock option and sets cron event up. - * - * @codeCoverageIgnore - * @param string $value Option value. - * @param array $option Option name. - * @param string $raw_value Raw value. - * @return string - */ -function wc_format_option_hold_stock_minutes( $value, $option, $raw_value ) { - $value = ! empty( $raw_value ) ? absint( $raw_value ) : ''; // Allow > 0 or set to ''. - - wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' ); - - if ( '' !== $value ) { - $cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $value ) ); - wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' ); - } - - return $value; -} -add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_hold_stock_minutes', 'wc_format_option_hold_stock_minutes', 10, 3 ); - -/** - * Sanitize terms from an attribute text based. - * - * @since 2.4.5 - * @param string $term Term value. - * @return string - */ -function wc_sanitize_term_text_based( $term ) { - return trim( wp_strip_all_tags( wp_unslash( $term ) ) ); -} - -if ( ! function_exists( 'wc_make_numeric_postcode' ) ) { - /** - * Make numeric postcode. - * - * Converts letters to numbers so we can do a simple range check on postcodes. - * E.g. PE30 becomes 16050300 (P = 16, E = 05, 3 = 03, 0 = 00) - * - * @since 2.6.0 - * @param string $postcode Regular postcode. - * @return string - */ - function wc_make_numeric_postcode( $postcode ) { - $postcode = str_replace( array( ' ', '-' ), '', $postcode ); - $postcode_length = strlen( $postcode ); - $letters_to_numbers = array_merge( array( 0 ), range( 'A', 'Z' ) ); - $letters_to_numbers = array_flip( $letters_to_numbers ); - $numeric_postcode = ''; - - for ( $i = 0; $i < $postcode_length; $i ++ ) { - if ( is_numeric( $postcode[ $i ] ) ) { - $numeric_postcode .= str_pad( $postcode[ $i ], 2, '0', STR_PAD_LEFT ); - } elseif ( isset( $letters_to_numbers[ $postcode[ $i ] ] ) ) { - $numeric_postcode .= str_pad( $letters_to_numbers[ $postcode[ $i ] ], 2, '0', STR_PAD_LEFT ); - } else { - $numeric_postcode .= '00'; - } - } - - return $numeric_postcode; - } -} - -/** - * Format the stock amount ready for display based on settings. - * - * @since 3.0.0 - * @param WC_Product $product Product object for which the stock you need to format. - * @return string - */ -function wc_format_stock_for_display( $product ) { - $display = __( 'In stock', 'woocommerce' ); - $stock_amount = $product->get_stock_quantity(); - - switch ( get_option( 'woocommerce_stock_format' ) ) { - case 'low_amount': - if ( $stock_amount <= wc_get_low_stock_amount( $product ) ) { - /* translators: %s: stock amount */ - $display = sprintf( __( 'Only %s left in stock', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_amount, $product ) ); - } - break; - case '': - /* translators: %s: stock amount */ - $display = sprintf( __( '%s in stock', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_amount, $product ) ); - break; - } - - if ( $product->backorders_allowed() && $product->backorders_require_notification() ) { - $display .= ' ' . __( '(can be backordered)', 'woocommerce' ); - } - - return $display; -} - -/** - * Format the stock quantity ready for display. - * - * @since 3.0.0 - * @param int $stock_quantity Stock quantity. - * @param WC_Product $product Product instance so that we can pass through the filters. - * @return string - */ -function wc_format_stock_quantity_for_display( $stock_quantity, $product ) { - return apply_filters( 'woocommerce_format_stock_quantity', $stock_quantity, $product ); -} - -/** - * Format a sale price for display. - * - * @since 3.0.0 - * @param string $regular_price Regular price. - * @param string $sale_price Sale price. - * @return string - */ -function wc_format_sale_price( $regular_price, $sale_price ) { - $price = '' . ( is_numeric( $regular_price ) ? wc_price( $regular_price ) : $regular_price ) . ' ' . ( is_numeric( $sale_price ) ? wc_price( $sale_price ) : $sale_price ) . ''; - return apply_filters( 'woocommerce_format_sale_price', $price, $regular_price, $sale_price ); -} - -/** - * Format a price range for display. - * - * @param string $from Price from. - * @param string $to Price to. - * @return string - */ -function wc_format_price_range( $from, $to ) { - /* translators: 1: price from 2: price to */ - $price = sprintf( _x( '%1$s – %2$s', 'Price range: from-to', 'woocommerce' ), is_numeric( $from ) ? wc_price( $from ) : $from, is_numeric( $to ) ? wc_price( $to ) : $to ); - return apply_filters( 'woocommerce_format_price_range', $price, $from, $to ); -} - -/** - * Format a weight for display. - * - * @since 3.0.0 - * @param float $weight Weight. - * @return string - */ -function wc_format_weight( $weight ) { - $weight_string = wc_format_localized_decimal( $weight ); - - if ( ! empty( $weight_string ) ) { - $weight_string .= ' ' . get_option( 'woocommerce_weight_unit' ); - } else { - $weight_string = __( 'N/A', 'woocommerce' ); - } - - return apply_filters( 'woocommerce_format_weight', $weight_string, $weight ); -} - -/** - * Format dimensions for display. - * - * @since 3.0.0 - * @param array $dimensions Array of dimensions. - * @return string - */ -function wc_format_dimensions( $dimensions ) { - $dimension_string = implode( ' × ', array_filter( array_map( 'wc_format_localized_decimal', $dimensions ) ) ); - - if ( ! empty( $dimension_string ) ) { - $dimension_string .= ' ' . get_option( 'woocommerce_dimension_unit' ); - } else { - $dimension_string = __( 'N/A', 'woocommerce' ); - } - - return apply_filters( 'woocommerce_format_dimensions', $dimension_string, $dimensions ); -} - -/** - * Format a date for output. - * - * @since 3.0.0 - * @param WC_DateTime $date Instance of WC_DateTime. - * @param string $format Data format. - * Defaults to the wc_date_format function if not set. - * @return string - */ -function wc_format_datetime( $date, $format = '' ) { - if ( ! $format ) { - $format = wc_date_format(); - } - if ( ! is_a( $date, 'WC_DateTime' ) ) { - return ''; - } - return $date->date_i18n( $format ); -} - -/** - * Process oEmbeds. - * - * @since 3.1.0 - * @param string $content Content. - * @return string - */ -function wc_do_oembeds( $content ) { - global $wp_embed; - - $content = $wp_embed->autoembed( $content ); - - return $content; -} - -/** - * Get part of a string before :. - * - * Used for example in shipping methods ids where they take the format - * method_id:instance_id - * - * @since 3.2.0 - * @param string $string String to extract. - * @return string - */ -function wc_get_string_before_colon( $string ) { - return trim( current( explode( ':', (string) $string ) ) ); -} - -/** - * Array merge and sum function. - * - * Source: https://gist.github.com/Nickology/f700e319cbafab5eaedc - * - * @since 3.2.0 - * @return array - */ -function wc_array_merge_recursive_numeric() { - $arrays = func_get_args(); - - // If there's only one array, it's already merged. - if ( 1 === count( $arrays ) ) { - return $arrays[0]; - } - - // Remove any items in $arrays that are NOT arrays. - foreach ( $arrays as $key => $array ) { - if ( ! is_array( $array ) ) { - unset( $arrays[ $key ] ); - } - } - - // We start by setting the first array as our final array. - // We will merge all other arrays with this one. - $final = array_shift( $arrays ); - - foreach ( $arrays as $b ) { - foreach ( $final as $key => $value ) { - // If $key does not exist in $b, then it is unique and can be safely merged. - if ( ! isset( $b[ $key ] ) ) { - $final[ $key ] = $value; - } else { - // If $key is present in $b, then we need to merge and sum numeric values in both. - if ( is_numeric( $value ) && is_numeric( $b[ $key ] ) ) { - // If both values for these keys are numeric, we sum them. - $final[ $key ] = $value + $b[ $key ]; - } elseif ( is_array( $value ) && is_array( $b[ $key ] ) ) { - // If both values are arrays, we recursively call ourself. - $final[ $key ] = wc_array_merge_recursive_numeric( $value, $b[ $key ] ); - } else { - // If both keys exist but differ in type, then we cannot merge them. - // In this scenario, we will $b's value for $key is used. - $final[ $key ] = $b[ $key ]; - } - } - } - - // Finally, we need to merge any keys that exist only in $b. - foreach ( $b as $key => $value ) { - if ( ! isset( $final[ $key ] ) ) { - $final[ $key ] = $value; - } - } - } - - return $final; -} - -/** - * Implode and escape HTML attributes for output. - * - * @since 3.3.0 - * @param array $raw_attributes Attribute name value pairs. - * @return string - */ -function wc_implode_html_attributes( $raw_attributes ) { - $attributes = array(); - foreach ( $raw_attributes as $name => $value ) { - $attributes[] = esc_attr( $name ) . '="' . esc_attr( $value ) . '"'; - } - return implode( ' ', $attributes ); -} - -/** - * Escape JSON for use on HTML or attribute text nodes. - * - * @since 3.5.5 - * @param string $json JSON to escape. - * @param bool $html True if escaping for HTML text node, false for attributes. Determines how quotes are handled. - * @return string Escaped JSON. - */ -function wc_esc_json( $json, $html = false ) { - return _wp_specialchars( - $json, - $html ? ENT_NOQUOTES : ENT_QUOTES, // Escape quotes in attribute nodes only. - 'UTF-8', // json_encode() outputs UTF-8 (really just ASCII), not the blog's charset. - true // Double escape entities: `&` -> `&amp;`. - ); -} - -/** - * Parse a relative date option from the settings API into a standard format. - * - * @since 3.4.0 - * @param mixed $raw_value Value stored in DB. - * @return array Nicely formatted array with number and unit values. - */ -function wc_parse_relative_date_option( $raw_value ) { - $periods = array( - 'days' => __( 'Day(s)', 'woocommerce' ), - 'weeks' => __( 'Week(s)', 'woocommerce' ), - 'months' => __( 'Month(s)', 'woocommerce' ), - 'years' => __( 'Year(s)', 'woocommerce' ), - ); - - $value = wp_parse_args( - (array) $raw_value, - array( - 'number' => '', - 'unit' => 'days', - ) - ); - - $value['number'] = ! empty( $value['number'] ) ? absint( $value['number'] ) : ''; - - if ( ! in_array( $value['unit'], array_keys( $periods ), true ) ) { - $value['unit'] = 'days'; - } - - return $value; -} - -/** - * Format the endpoint slug, strip out anything not allowed in a url. - * - * @since 3.5.0 - * @param string $raw_value The raw value. - * @return string - */ -function wc_sanitize_endpoint_slug( $raw_value ) { - return sanitize_title( $raw_value ); -} -add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_checkout_pay_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); -add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_checkout_order_received_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); -add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_add_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); -add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_delete_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); -add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_set_default_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); -add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_orders_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); -add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_view_order_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); -add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_downloads_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); -add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_edit_account_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); -add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_edit_address_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); -add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_payment_methods_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); -add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_lost_password_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); -add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_logout_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); diff --git a/includes/wc-order-functions.php b/includes/wc-order-functions.php deleted file mode 100644 index 2047a6969a8..00000000000 --- a/includes/wc-order-functions.php +++ /dev/null @@ -1,1092 +0,0 @@ - 'limit', - 'post_type' => 'type', - 'post_status' => 'status', - 'post_parent' => 'parent', - 'author' => 'customer', - 'email' => 'billing_email', - 'posts_per_page' => 'limit', - 'paged' => 'page', - ); - - foreach ( $map_legacy as $from => $to ) { - if ( isset( $args[ $from ] ) ) { - $args[ $to ] = $args[ $from ]; - } - } - - // Map legacy date args to modern date args. - $date_before = false; - $date_after = false; - - if ( ! empty( $args['date_before'] ) ) { - $datetime = wc_string_to_datetime( $args['date_before'] ); - $date_before = strpos( $args['date_before'], ':' ) ? $datetime->getOffsetTimestamp() : $datetime->date( 'Y-m-d' ); - } - if ( ! empty( $args['date_after'] ) ) { - $datetime = wc_string_to_datetime( $args['date_after'] ); - $date_after = strpos( $args['date_after'], ':' ) ? $datetime->getOffsetTimestamp() : $datetime->date( 'Y-m-d' ); - } - - if ( $date_before && $date_after ) { - $args['date_created'] = $date_after . '...' . $date_before; - } elseif ( $date_before ) { - $args['date_created'] = '<' . $date_before; - } elseif ( $date_after ) { - $args['date_created'] = '>' . $date_after; - } - - $query = new WC_Order_Query( $args ); - return $query->get_orders(); -} - -/** - * Main function for returning orders, uses the WC_Order_Factory class. - * - * @since 2.2 - * - * @param mixed $the_order Post object or post ID of the order. - * - * @return bool|WC_Order|WC_Order_Refund - */ -function wc_get_order( $the_order = false ) { - if ( ! did_action( 'woocommerce_after_register_post_type' ) ) { - wc_doing_it_wrong( __FUNCTION__, 'wc_get_order should not be called before post types are registered (woocommerce_after_register_post_type action)', '2.5' ); - return false; - } - return WC()->order_factory->get_order( $the_order ); -} - -/** - * Get all order statuses. - * - * @since 2.2 - * @used-by WC_Order::set_status - * @return array - */ -function wc_get_order_statuses() { - $order_statuses = array( - 'wc-pending' => _x( 'Pending payment', 'Order status', 'woocommerce' ), - 'wc-processing' => _x( 'Processing', 'Order status', 'woocommerce' ), - 'wc-on-hold' => _x( 'On hold', 'Order status', 'woocommerce' ), - 'wc-completed' => _x( 'Completed', 'Order status', 'woocommerce' ), - 'wc-cancelled' => _x( 'Cancelled', 'Order status', 'woocommerce' ), - 'wc-refunded' => _x( 'Refunded', 'Order status', 'woocommerce' ), - 'wc-failed' => _x( 'Failed', 'Order status', 'woocommerce' ), - ); - return apply_filters( 'wc_order_statuses', $order_statuses ); -} - -/** - * See if a string is an order status. - * - * @param string $maybe_status Status, including any wc- prefix. - * @return bool - */ -function wc_is_order_status( $maybe_status ) { - $order_statuses = wc_get_order_statuses(); - return isset( $order_statuses[ $maybe_status ] ); -} - -/** - * Get list of statuses which are consider 'paid'. - * - * @since 3.0.0 - * @return array - */ -function wc_get_is_paid_statuses() { - return apply_filters( 'woocommerce_order_is_paid_statuses', array( 'processing', 'completed' ) ); -} - -/** - * Get list of statuses which are consider 'pending payment'. - * - * @since 3.6.0 - * @return array - */ -function wc_get_is_pending_statuses() { - return apply_filters( 'woocommerce_order_is_pending_statuses', array( 'pending' ) ); -} - -/** - * Get the nice name for an order status. - * - * @since 2.2 - * @param string $status Status. - * @return string - */ -function wc_get_order_status_name( $status ) { - $statuses = wc_get_order_statuses(); - $status = 'wc-' === substr( $status, 0, 3 ) ? substr( $status, 3 ) : $status; - $status = isset( $statuses[ 'wc-' . $status ] ) ? $statuses[ 'wc-' . $status ] : $status; - return $status; -} - -/** - * Generate an order key with prefix. - * - * @since 3.5.4 - * @param string $key Order key without a prefix. By default generates a 13 digit secret. - * @return string The order key. - */ -function wc_generate_order_key( $key = '' ) { - if ( '' === $key ) { - $key = wp_generate_password( 13, false ); - } - - return 'wc_' . apply_filters( 'woocommerce_generate_order_key', 'order_' . $key ); -} - -/** - * Finds an Order ID based on an order key. - * - * @param string $order_key An order key has generated by. - * @return int The ID of an order, or 0 if the order could not be found. - */ -function wc_get_order_id_by_order_key( $order_key ) { - $data_store = WC_Data_Store::load( 'order' ); - return $data_store->get_order_id_by_order_key( $order_key ); -} - -/** - * Get all registered order types. - * - * @since 2.2 - * @param string $for Optionally define what you are getting order types for so - * only relevant types are returned. - * e.g. for 'order-meta-boxes', 'order-count'. - * @return array - */ -function wc_get_order_types( $for = '' ) { - global $wc_order_types; - - if ( ! is_array( $wc_order_types ) ) { - $wc_order_types = array(); - } - - $order_types = array(); - - switch ( $for ) { - case 'order-count': - foreach ( $wc_order_types as $type => $args ) { - if ( ! $args['exclude_from_order_count'] ) { - $order_types[] = $type; - } - } - break; - case 'order-meta-boxes': - foreach ( $wc_order_types as $type => $args ) { - if ( $args['add_order_meta_boxes'] ) { - $order_types[] = $type; - } - } - break; - case 'view-orders': - foreach ( $wc_order_types as $type => $args ) { - if ( ! $args['exclude_from_order_views'] ) { - $order_types[] = $type; - } - } - break; - case 'reports': - foreach ( $wc_order_types as $type => $args ) { - if ( ! $args['exclude_from_order_reports'] ) { - $order_types[] = $type; - } - } - break; - case 'sales-reports': - foreach ( $wc_order_types as $type => $args ) { - if ( ! $args['exclude_from_order_sales_reports'] ) { - $order_types[] = $type; - } - } - break; - case 'order-webhooks': - foreach ( $wc_order_types as $type => $args ) { - if ( ! $args['exclude_from_order_webhooks'] ) { - $order_types[] = $type; - } - } - break; - default: - $order_types = array_keys( $wc_order_types ); - break; - } - - return apply_filters( 'wc_order_types', $order_types, $for ); -} - -/** - * Get an order type by post type name. - * - * @param string $type Post type name. - * @return bool|array Details about the order type. - */ -function wc_get_order_type( $type ) { - global $wc_order_types; - - if ( isset( $wc_order_types[ $type ] ) ) { - return $wc_order_types[ $type ]; - } - - return false; -} - -/** - * Register order type. Do not use before init. - * - * Wrapper for register post type, as well as a method of telling WC which. - * post types are types of orders, and having them treated as such. - * - * $args are passed to register_post_type, but there are a few specific to this function: - * - exclude_from_orders_screen (bool) Whether or not this order type also get shown in the main. - * orders screen. - * - add_order_meta_boxes (bool) Whether or not the order type gets shop_order meta boxes. - * - exclude_from_order_count (bool) Whether or not this order type is excluded from counts. - * - exclude_from_order_views (bool) Whether or not this order type is visible by customers when. - * viewing orders e.g. on the my account page. - * - exclude_from_order_reports (bool) Whether or not to exclude this type from core reports. - * - exclude_from_order_sales_reports (bool) Whether or not to exclude this type from core sales reports. - * - * @since 2.2 - * @see register_post_type for $args used in that function - * @param string $type Post type. (max. 20 characters, can not contain capital letters or spaces). - * @param array $args An array of arguments. - * @return bool Success or failure - */ -function wc_register_order_type( $type, $args = array() ) { - if ( post_type_exists( $type ) ) { - return false; - } - - global $wc_order_types; - - if ( ! is_array( $wc_order_types ) ) { - $wc_order_types = array(); - } - - // Register as a post type. - if ( is_wp_error( register_post_type( $type, $args ) ) ) { - return false; - } - - // Register for WC usage. - $order_type_args = array( - 'exclude_from_orders_screen' => false, - 'add_order_meta_boxes' => true, - 'exclude_from_order_count' => false, - 'exclude_from_order_views' => false, - 'exclude_from_order_webhooks' => false, - 'exclude_from_order_reports' => false, - 'exclude_from_order_sales_reports' => false, - 'class_name' => 'WC_Order', - ); - - $args = array_intersect_key( $args, $order_type_args ); - $args = wp_parse_args( $args, $order_type_args ); - $wc_order_types[ $type ] = $args; - - return true; -} - -/** - * Return the count of processing orders. - * - * @return int - */ -function wc_processing_order_count() { - return wc_orders_count( 'processing' ); -} - -/** - * Return the orders count of a specific order status. - * - * @param string $status Status. - * @return int - */ -function wc_orders_count( $status ) { - $count = 0; - $status = 'wc-' . $status; - $order_statuses = array_keys( wc_get_order_statuses() ); - - if ( ! in_array( $status, $order_statuses, true ) ) { - return 0; - } - - $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . $status; - $cached_count = wp_cache_get( $cache_key, 'counts' ); - - if ( false !== $cached_count ) { - return $cached_count; - } - - foreach ( wc_get_order_types( 'order-count' ) as $type ) { - $data_store = WC_Data_Store::load( 'shop_order' === $type ? 'order' : $type ); - if ( $data_store ) { - $count += $data_store->get_order_count( $status ); - } - } - - wp_cache_set( $cache_key, $count, 'counts' ); - - return $count; -} - -/** - * Grant downloadable product access to the file identified by $download_id. - * - * @param string $download_id File identifier. - * @param int|WC_Product $product Product instance or ID. - * @param WC_Order $order Order data. - * @param int $qty Quantity purchased. - * @return int|bool insert id or false on failure. - */ -function wc_downloadable_file_permission( $download_id, $product, $order, $qty = 1 ) { - if ( is_numeric( $product ) ) { - $product = wc_get_product( $product ); - } - $download = new WC_Customer_Download(); - $download->set_download_id( $download_id ); - $download->set_product_id( $product->get_id() ); - $download->set_user_id( $order->get_customer_id() ); - $download->set_order_id( $order->get_id() ); - $download->set_user_email( $order->get_billing_email() ); - $download->set_order_key( $order->get_order_key() ); - $download->set_downloads_remaining( 0 > $product->get_download_limit() ? '' : $product->get_download_limit() * $qty ); - $download->set_access_granted( time() ); - $download->set_download_count( 0 ); - - $expiry = $product->get_download_expiry(); - - if ( $expiry > 0 ) { - $from_date = $order->get_date_completed() ? $order->get_date_completed()->format( 'Y-m-d' ) : current_time( 'mysql', true ); - $download->set_access_expires( strtotime( $from_date . ' + ' . $expiry . ' DAY' ) ); - } - - $download = apply_filters( 'woocommerce_downloadable_file_permission', $download, $product, $order, $qty ); - - return $download->save(); -} - -/** - * Order Status completed - give downloadable product access to customer. - * - * @param int $order_id Order ID. - * @param bool $force Force downloadable permissions. - */ -function wc_downloadable_product_permissions( $order_id, $force = false ) { - $order = wc_get_order( $order_id ); - - if ( ! $order || ( $order->get_data_store()->get_download_permissions_granted( $order ) && ! $force ) ) { - return; - } - - if ( $order->has_status( 'processing' ) && 'no' === get_option( 'woocommerce_downloads_grant_access_after_payment' ) ) { - return; - } - - if ( count( $order->get_items() ) > 0 ) { - foreach ( $order->get_items() as $item ) { - $product = $item->get_product(); - - if ( $product && $product->exists() && $product->is_downloadable() ) { - $downloads = $product->get_downloads(); - - foreach ( array_keys( $downloads ) as $download_id ) { - wc_downloadable_file_permission( $download_id, $product, $order, $item->get_quantity() ); - } - } - } - } - - $order->get_data_store()->set_download_permissions_granted( $order, true ); - do_action( 'woocommerce_grant_product_download_permissions', $order_id ); -} -add_action( 'woocommerce_order_status_completed', 'wc_downloadable_product_permissions' ); -add_action( 'woocommerce_order_status_processing', 'wc_downloadable_product_permissions' ); - -/** - * Clear all transients cache for order data. - * - * @param int|WC_Order $order Order instance or ID. - */ -function wc_delete_shop_order_transients( $order = 0 ) { - if ( is_numeric( $order ) ) { - $order = wc_get_order( $order ); - } - $reports = WC_Admin_Reports::get_reports(); - $transients_to_clear = array( - 'wc_admin_report', - ); - - foreach ( $reports as $report_group ) { - foreach ( $report_group['reports'] as $report_key => $report ) { - $transients_to_clear[] = 'wc_report_' . $report_key; - } - } - - foreach ( $transients_to_clear as $transient ) { - delete_transient( $transient ); - } - - // Clear customer's order related caches. - if ( is_a( $order, 'WC_Order' ) ) { - $order_id = $order->get_id(); - delete_user_meta( $order->get_customer_id(), '_money_spent' ); - delete_user_meta( $order->get_customer_id(), '_order_count' ); - delete_user_meta( $order->get_customer_id(), '_last_order' ); - } else { - $order_id = 0; - } - - // Increments the transient version to invalidate cache. - WC_Cache_Helper::get_transient_version( 'orders', true ); - - // Do the same for regular cache. - WC_Cache_Helper::invalidate_cache_group( 'orders' ); - - do_action( 'woocommerce_delete_shop_order_transients', $order_id ); -} - -/** - * See if we only ship to billing addresses. - * - * @return bool - */ -function wc_ship_to_billing_address_only() { - return 'billing_only' === get_option( 'woocommerce_ship_to_destination' ); -} - -/** - * Create a new order refund programmatically. - * - * Returns a new refund object on success which can then be used to add additional data. - * - * @since 2.2 - * @throws Exception Throws exceptions when fail to create, but returns WP_Error instead. - * @param array $args New refund arguments. - * @return WC_Order_Refund|WP_Error - */ -function wc_create_refund( $args = array() ) { - $default_args = array( - 'amount' => 0, - 'reason' => null, - 'order_id' => 0, - 'refund_id' => 0, - 'line_items' => array(), - 'refund_payment' => false, - 'restock_items' => false, - ); - - try { - $args = wp_parse_args( $args, $default_args ); - $order = wc_get_order( $args['order_id'] ); - - if ( ! $order ) { - throw new Exception( __( 'Invalid order ID.', 'woocommerce' ) ); - } - - $remaining_refund_amount = $order->get_remaining_refund_amount(); - $remaining_refund_items = $order->get_remaining_refund_items(); - $refund_item_count = 0; - $refund = new WC_Order_Refund( $args['refund_id'] ); - - if ( 0 > $args['amount'] || $args['amount'] > $remaining_refund_amount ) { - throw new Exception( __( 'Invalid refund amount.', 'woocommerce' ) ); - } - - $refund->set_currency( $order->get_currency() ); - $refund->set_amount( $args['amount'] ); - $refund->set_parent_id( absint( $args['order_id'] ) ); - $refund->set_refunded_by( get_current_user_id() ? get_current_user_id() : 1 ); - $refund->set_prices_include_tax( $order->get_prices_include_tax() ); - - if ( ! is_null( $args['reason'] ) ) { - $refund->set_reason( $args['reason'] ); - } - - // Negative line items. - if ( count( $args['line_items'] ) > 0 ) { - $items = $order->get_items( array( 'line_item', 'fee', 'shipping' ) ); - - foreach ( $items as $item_id => $item ) { - if ( ! isset( $args['line_items'][ $item_id ] ) ) { - continue; - } - - $qty = isset( $args['line_items'][ $item_id ]['qty'] ) ? $args['line_items'][ $item_id ]['qty'] : 0; - $refund_total = $args['line_items'][ $item_id ]['refund_total']; - $refund_tax = isset( $args['line_items'][ $item_id ]['refund_tax'] ) ? array_filter( (array) $args['line_items'][ $item_id ]['refund_tax'] ) : array(); - - if ( empty( $qty ) && empty( $refund_total ) && empty( $args['line_items'][ $item_id ]['refund_tax'] ) ) { - continue; - } - - $class = get_class( $item ); - $refunded_item = new $class( $item ); - $refunded_item->set_id( 0 ); - $refunded_item->add_meta_data( '_refunded_item_id', $item_id, true ); - $refunded_item->set_total( wc_format_refund_total( $refund_total ) ); - $refunded_item->set_taxes( - array( - 'total' => array_map( 'wc_format_refund_total', $refund_tax ), - 'subtotal' => array_map( 'wc_format_refund_total', $refund_tax ), - ) - ); - - if ( is_callable( array( $refunded_item, 'set_subtotal' ) ) ) { - $refunded_item->set_subtotal( wc_format_refund_total( $refund_total ) ); - } - - if ( is_callable( array( $refunded_item, 'set_quantity' ) ) ) { - $refunded_item->set_quantity( $qty * -1 ); - } - - $refund->add_item( $refunded_item ); - $refund_item_count += $qty; - } - } - - $refund->update_taxes(); - $refund->calculate_totals( false ); - $refund->set_total( $args['amount'] * -1 ); - - // this should remain after update_taxes(), as this will save the order, and write the current date to the db - // so we must wait until the order is persisted to set the date. - if ( isset( $args['date_created'] ) ) { - $refund->set_date_created( $args['date_created'] ); - } - - /** - * Action hook to adjust refund before save. - * - * @since 3.0.0 - */ - do_action( 'woocommerce_create_refund', $refund, $args ); - - if ( $refund->save() ) { - if ( $args['refund_payment'] ) { - $result = wc_refund_payment( $order, $refund->get_amount(), $refund->get_reason() ); - - if ( is_wp_error( $result ) ) { - $refund->delete(); - return $result; - } - - $refund->set_refunded_payment( true ); - $refund->save(); - } - - if ( $args['restock_items'] ) { - wc_restock_refunded_items( $order, $args['line_items'] ); - } - - // Trigger notification emails. - if ( ( $remaining_refund_amount - $args['amount'] ) > 0 || ( $order->has_free_item() && ( $remaining_refund_items - $refund_item_count ) > 0 ) ) { - do_action( 'woocommerce_order_partially_refunded', $order->get_id(), $refund->get_id() ); - } else { - do_action( 'woocommerce_order_fully_refunded', $order->get_id(), $refund->get_id() ); - - $parent_status = apply_filters( 'woocommerce_order_fully_refunded_status', 'refunded', $order->get_id(), $refund->get_id() ); - - if ( $parent_status ) { - $order->update_status( $parent_status ); - } - } - } - - do_action( 'woocommerce_refund_created', $refund->get_id(), $args ); - do_action( 'woocommerce_order_refunded', $order->get_id(), $refund->get_id() ); - - } catch ( Exception $e ) { - if ( isset( $refund ) && is_a( $refund, 'WC_Order_Refund' ) ) { - wp_delete_post( $refund->get_id(), true ); - } - return new WP_Error( 'error', $e->getMessage() ); - } - - return $refund; -} - -/** - * Try to refund the payment for an order via the gateway. - * - * @since 3.0.0 - * @throws Exception Throws exceptions when fail to refund, but returns WP_Error instead. - * @param WC_Order $order Order instance. - * @param string $amount Amount to refund. - * @param string $reason Refund reason. - * @return bool|WP_Error - */ -function wc_refund_payment( $order, $amount, $reason = '' ) { - try { - if ( ! is_a( $order, 'WC_Order' ) ) { - throw new Exception( __( 'Invalid order.', 'woocommerce' ) ); - } - - $gateway_controller = WC_Payment_Gateways::instance(); - $all_gateways = $gateway_controller->payment_gateways(); - $payment_method = $order->get_payment_method(); - $gateway = isset( $all_gateways[ $payment_method ] ) ? $all_gateways[ $payment_method ] : false; - - if ( ! $gateway ) { - throw new Exception( __( 'The payment gateway for this order does not exist.', 'woocommerce' ) ); - } - - if ( ! $gateway->supports( 'refunds' ) ) { - throw new Exception( __( 'The payment gateway for this order does not support automatic refunds.', 'woocommerce' ) ); - } - - $result = $gateway->process_refund( $order->get_id(), $amount, $reason ); - - if ( ! $result ) { - throw new Exception( __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ) ); - } - - if ( is_wp_error( $result ) ) { - throw new Exception( $result->get_error_message() ); - } - - return true; - - } catch ( Exception $e ) { - return new WP_Error( 'error', $e->getMessage() ); - } -} - -/** - * Restock items during refund. - * - * @since 3.0.0 - * @param WC_Order $order Order instance. - * @param array $refunded_line_items Refunded items list. - */ -function wc_restock_refunded_items( $order, $refunded_line_items ) { - if ( ! apply_filters( 'woocommerce_can_restock_refunded_items', true, $order, $refunded_line_items ) ) { - return; - } - - $line_items = $order->get_items(); - - foreach ( $line_items as $item_id => $item ) { - if ( ! isset( $refunded_line_items[ $item_id ], $refunded_line_items[ $item_id ]['qty'] ) ) { - continue; - } - $product = $item->get_product(); - $item_stock_reduced = $item->get_meta( '_reduced_stock', true ); - $qty_to_refund = $refunded_line_items[ $item_id ]['qty']; - - if ( ! $item_stock_reduced || ! $qty_to_refund || ! $product || ! $product->managing_stock() ) { - continue; - } - - $old_stock = $product->get_stock_quantity(); - $new_stock = wc_update_product_stock( $product, $qty_to_refund, 'increase' ); - - // Update _reduced_stock meta to track changes. - $item_stock_reduced = $item_stock_reduced - $qty_to_refund; - - if ( 0 < $item_stock_reduced ) { - $item->update_meta_data( '_reduced_stock', $item_stock_reduced ); - } else { - $item->delete_meta_data( '_reduced_stock' ); - } - - /* translators: 1: product ID 2: old stock level 3: new stock level */ - $order->add_order_note( sprintf( __( 'Item #%1$s stock increased from %2$s to %3$s.', 'woocommerce' ), $product->get_id(), $old_stock, $new_stock ) ); - - $item->save(); - - do_action( 'woocommerce_restock_refunded_item', $product->get_id(), $old_stock, $new_stock, $order, $product ); - } -} - -/** - * Get tax class by tax id. - * - * @since 2.2 - * @param int $tax_id Tax ID. - * @return string - */ -function wc_get_tax_class_by_tax_id( $tax_id ) { - global $wpdb; - return $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_class FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d", $tax_id ) ); -} - -/** - * Get payment gateway class by order data. - * - * @since 2.2 - * @param int|WC_Order $order Order instance. - * @return WC_Payment_Gateway|bool - */ -function wc_get_payment_gateway_by_order( $order ) { - if ( WC()->payment_gateways() ) { - $payment_gateways = WC()->payment_gateways()->payment_gateways(); - } else { - $payment_gateways = array(); - } - - if ( ! is_object( $order ) ) { - $order_id = absint( $order ); - $order = wc_get_order( $order_id ); - } - - return is_a( $order, 'WC_Order' ) && isset( $payment_gateways[ $order->get_payment_method() ] ) ? $payment_gateways[ $order->get_payment_method() ] : false; -} - -/** - * When refunding an order, create a refund line item if the partial refunds do not match order total. - * - * This is manual; no gateway refund will be performed. - * - * @since 2.4 - * @param int $order_id Order ID. - */ -function wc_order_fully_refunded( $order_id ) { - $order = wc_get_order( $order_id ); - $max_refund = wc_format_decimal( $order->get_total() - $order->get_total_refunded() ); - - if ( ! $max_refund ) { - return; - } - - // Create the refund object. - wc_switch_to_site_locale(); - wc_create_refund( - array( - 'amount' => $max_refund, - 'reason' => __( 'Order fully refunded.', 'woocommerce' ), - 'order_id' => $order_id, - 'line_items' => array(), - ) - ); - wc_restore_locale(); - - $order->add_order_note( __( 'Order status set to refunded. To return funds to the customer you will need to issue a refund through your payment gateway.', 'woocommerce' ) ); -} -add_action( 'woocommerce_order_status_refunded', 'wc_order_fully_refunded' ); - -/** - * Search orders. - * - * @since 2.6.0 - * @param string $term Term to search. - * @return array List of orders ID. - */ -function wc_order_search( $term ) { - $data_store = WC_Data_Store::load( 'order' ); - return $data_store->search_orders( str_replace( 'Order #', '', wc_clean( $term ) ) ); -} - -/** - * Update total sales amount for each product within a paid order. - * - * @since 3.0.0 - * @param int $order_id Order ID. - */ -function wc_update_total_sales_counts( $order_id ) { - $order = wc_get_order( $order_id ); - - if ( ! $order || $order->get_data_store()->get_recorded_sales( $order ) ) { - return; - } - - if ( count( $order->get_items() ) > 0 ) { - foreach ( $order->get_items() as $item ) { - $product_id = $item->get_product_id(); - - if ( $product_id ) { - $data_store = WC_Data_Store::load( 'product' ); - $data_store->update_product_sales( $product_id, absint( $item->get_quantity() ), 'increase' ); - } - } - } - - $order->get_data_store()->set_recorded_sales( $order, true ); - - /** - * Called when sales for an order are recorded - * - * @param int $order_id order id - */ - do_action( 'woocommerce_recorded_sales', $order_id ); -} -add_action( 'woocommerce_order_status_completed', 'wc_update_total_sales_counts' ); -add_action( 'woocommerce_order_status_processing', 'wc_update_total_sales_counts' ); -add_action( 'woocommerce_order_status_on-hold', 'wc_update_total_sales_counts' ); - -/** - * Update used coupon amount for each coupon within an order. - * - * @since 3.0.0 - * @param int $order_id Order ID. - */ -function wc_update_coupon_usage_counts( $order_id ) { - $order = wc_get_order( $order_id ); - - if ( ! $order ) { - return; - } - - $has_recorded = $order->get_data_store()->get_recorded_coupon_usage_counts( $order ); - - if ( $order->has_status( 'cancelled' ) && $has_recorded ) { - $action = 'reduce'; - $order->get_data_store()->set_recorded_coupon_usage_counts( $order, false ); - } elseif ( ! $order->has_status( 'cancelled' ) && ! $has_recorded ) { - $action = 'increase'; - $order->get_data_store()->set_recorded_coupon_usage_counts( $order, true ); - } elseif ( $order->has_status( 'cancelled' ) ) { - $order->get_data_store()->release_held_coupons( $order, true ); - return; - } else { - return; - } - - if ( count( $order->get_coupon_codes() ) > 0 ) { - foreach ( $order->get_coupon_codes() as $code ) { - if ( ! $code ) { - continue; - } - - $coupon = new WC_Coupon( $code ); - $used_by = $order->get_user_id(); - - if ( ! $used_by ) { - $used_by = $order->get_billing_email(); - } - - switch ( $action ) { - case 'reduce': - $coupon->decrease_usage_count( $used_by ); - break; - case 'increase': - $coupon->increase_usage_count( $used_by, $order ); - break; - } - } - $order->get_data_store()->release_held_coupons( $order, true ); - } -} -add_action( 'woocommerce_order_status_pending', 'wc_update_coupon_usage_counts' ); -add_action( 'woocommerce_order_status_completed', 'wc_update_coupon_usage_counts' ); -add_action( 'woocommerce_order_status_processing', 'wc_update_coupon_usage_counts' ); -add_action( 'woocommerce_order_status_on-hold', 'wc_update_coupon_usage_counts' ); -add_action( 'woocommerce_order_status_cancelled', 'wc_update_coupon_usage_counts' ); - -/** - * Cancel all unpaid orders after held duration to prevent stock lock for those products. - */ -function wc_cancel_unpaid_orders() { - $held_duration = get_option( 'woocommerce_hold_stock_minutes' ); - - if ( $held_duration < 1 || 'yes' !== get_option( 'woocommerce_manage_stock' ) ) { - return; - } - - $data_store = WC_Data_Store::load( 'order' ); - $unpaid_orders = $data_store->get_unpaid_orders( strtotime( '-' . absint( $held_duration ) . ' MINUTES', current_time( 'timestamp' ) ) ); - - if ( $unpaid_orders ) { - foreach ( $unpaid_orders as $unpaid_order ) { - $order = wc_get_order( $unpaid_order ); - - if ( apply_filters( 'woocommerce_cancel_unpaid_order', 'checkout' === $order->get_created_via(), $order ) ) { - $order->update_status( 'cancelled', __( 'Unpaid order cancelled - time limit reached.', 'woocommerce' ) ); - } - } - } - wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' ); - $cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $held_duration ) ); - wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' ); -} -add_action( 'woocommerce_cancel_unpaid_orders', 'wc_cancel_unpaid_orders' ); - -/** - * Sanitize order id removing unwanted characters. - * - * E.g Users can sometimes try to track an order id using # with no success. - * This function will fix this. - * - * @since 3.1.0 - * @param int $order_id Order ID. - */ -function wc_sanitize_order_id( $order_id ) { - return (int) filter_var( $order_id, FILTER_SANITIZE_NUMBER_INT ); -} -add_filter( 'woocommerce_shortcode_order_tracking_order_id', 'wc_sanitize_order_id' ); - -/** - * Get an order note. - * - * @since 3.2.0 - * @param int|WP_Comment $data Note ID (or WP_Comment instance for internal use only). - * @return stdClass|null Object with order note details or null when does not exists. - */ -function wc_get_order_note( $data ) { - if ( is_numeric( $data ) ) { - $data = get_comment( $data ); - } - - if ( ! is_a( $data, 'WP_Comment' ) ) { - return null; - } - - return (object) apply_filters( - 'woocommerce_get_order_note', - array( - 'id' => (int) $data->comment_ID, - 'date_created' => wc_string_to_datetime( $data->comment_date ), - 'content' => $data->comment_content, - 'customer_note' => (bool) get_comment_meta( $data->comment_ID, 'is_customer_note', true ), - 'added_by' => __( 'WooCommerce', 'woocommerce' ) === $data->comment_author ? 'system' : $data->comment_author, - ), - $data - ); -} - -/** - * Get order notes. - * - * @since 3.2.0 - * @param array $args Query arguments { - * Array of query parameters. - * - * @type string $limit Maximum number of notes to retrieve. - * Default empty (no limit). - * @type int $order_id Limit results to those affiliated with a given order ID. - * Default 0. - * @type array $order__in Array of order IDs to include affiliated notes for. - * Default empty. - * @type array $order__not_in Array of order IDs to exclude affiliated notes for. - * Default empty. - * @type string $orderby Define how should sort notes. - * Accepts 'date_created', 'date_created_gmt' or 'id'. - * Default: 'id'. - * @type string $order How to order retrieved notes. - * Accepts 'ASC' or 'DESC'. - * Default: 'DESC'. - * @type string $type Define what type of note should retrieve. - * Accepts 'customer', 'internal' or empty for both. - * Default empty. - * } - * @return stdClass[] Array of stdClass objects with order notes details. - */ -function wc_get_order_notes( $args ) { - $key_mapping = array( - 'limit' => 'number', - 'order_id' => 'post_id', - 'order__in' => 'post__in', - 'order__not_in' => 'post__not_in', - ); - - foreach ( $key_mapping as $query_key => $db_key ) { - if ( isset( $args[ $query_key ] ) ) { - $args[ $db_key ] = $args[ $query_key ]; - unset( $args[ $query_key ] ); - } - } - - // Define orderby. - $orderby_mapping = array( - 'date_created' => 'comment_date', - 'date_created_gmt' => 'comment_date_gmt', - 'id' => 'comment_ID', - ); - - $args['orderby'] = ! empty( $args['orderby'] ) && in_array( $args['orderby'], array( 'date_created', 'date_created_gmt', 'id' ), true ) ? $orderby_mapping[ $args['orderby'] ] : 'comment_ID'; - - // Set WooCommerce order type. - if ( isset( $args['type'] ) && 'customer' === $args['type'] ) { - $args['meta_query'] = array( // WPCS: slow query ok. - array( - 'key' => 'is_customer_note', - 'value' => 1, - 'compare' => '=', - ), - ); - } elseif ( isset( $args['type'] ) && 'internal' === $args['type'] ) { - $args['meta_query'] = array( // WPCS: slow query ok. - array( - 'key' => 'is_customer_note', - 'compare' => 'NOT EXISTS', - ), - ); - } - - // Set correct comment type. - $args['type'] = 'order_note'; - - // Always approved. - $args['status'] = 'approve'; - - // Does not support 'count' or 'fields'. - unset( $args['count'], $args['fields'] ); - - remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); - - $notes = get_comments( $args ); - - add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); - - return array_filter( array_map( 'wc_get_order_note', $notes ) ); -} - -/** - * Create an order note. - * - * @since 3.2.0 - * @param int $order_id Order ID. - * @param string $note Note to add. - * @param bool $is_customer_note If is a costumer note. - * @param bool $added_by_user If note is create by an user. - * @return int|WP_Error Integer when created or WP_Error when found an error. - */ -function wc_create_order_note( $order_id, $note, $is_customer_note = false, $added_by_user = false ) { - $order = wc_get_order( $order_id ); - - if ( ! $order ) { - return new WP_Error( 'invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 400 ) ); - } - - return $order->add_order_note( $note, (int) $is_customer_note, $added_by_user ); -} - -/** - * Delete an order note. - * - * @since 3.2.0 - * @param int $note_id Order note. - * @return bool True on success, false on failure. - */ -function wc_delete_order_note( $note_id ) { - return wp_delete_comment( $note_id, true ); -} diff --git a/includes/wc-product-functions.php b/includes/wc-product-functions.php deleted file mode 100644 index 8e473271159..00000000000 --- a/includes/wc-product-functions.php +++ /dev/null @@ -1,1612 +0,0 @@ - 'limit', - 'post_status' => 'status', - 'post_parent' => 'parent', - 'posts_per_page' => 'limit', - 'paged' => 'page', - ); - - foreach ( $map_legacy as $from => $to ) { - if ( isset( $args[ $from ] ) ) { - $args[ $to ] = $args[ $from ]; - } - } - - $query = new WC_Product_Query( $args ); - return $query->get_products(); -} - -/** - * Main function for returning products, uses the WC_Product_Factory class. - * - * This function should only be called after 'init' action is finished, as there might be taxonomies that are getting - * registered during the init action. - * - * @since 2.2.0 - * - * @param mixed $the_product Post object or post ID of the product. - * @param array $deprecated Previously used to pass arguments to the factory, e.g. to force a type. - * @return WC_Product|null|false - */ -function wc_get_product( $the_product = false, $deprecated = array() ) { - if ( ! did_action( 'woocommerce_init' ) || ! did_action( 'woocommerce_after_register_taxonomy' ) || ! did_action( 'woocommerce_after_register_post_type' ) ) { - /* translators: 1: wc_get_product 2: woocommerce_init 3: woocommerce_after_register_taxonomy 4: woocommerce_after_register_post_type */ - wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%1$s should not be called before the %2$s, %3$s and %4$s actions have finished.', 'woocommerce' ), 'wc_get_product', 'woocommerce_init', 'woocommerce_after_register_taxonomy', 'woocommerce_after_register_post_type' ), '3.9' ); - return false; - } - if ( ! empty( $deprecated ) ) { - wc_deprecated_argument( 'args', '3.0', 'Passing args to wc_get_product is deprecated. If you need to force a type, construct the product class directly.' ); - } - return WC()->product_factory->get_product( $the_product, $deprecated ); -} - -/** - * Get a product object. - * - * @see WC_Product_Factory::get_product_classname - * @since 3.9.0 - * @param string $product_type Product type. If used an invalid type a WC_Product_Simple instance will be returned. - * @param int $product_id Product ID. - * @return WC_Product - */ -function wc_get_product_object( $product_type, $product_id = 0 ) { - $classname = WC_Product_Factory::get_product_classname( $product_id, $product_type ); - - return new $classname( $product_id ); -} - -/** - * Returns whether or not SKUS are enabled. - * - * @return bool - */ -function wc_product_sku_enabled() { - return apply_filters( 'wc_product_sku_enabled', true ); -} - -/** - * Returns whether or not product weights are enabled. - * - * @return bool - */ -function wc_product_weight_enabled() { - return apply_filters( 'wc_product_weight_enabled', true ); -} - -/** - * Returns whether or not product dimensions (HxWxD) are enabled. - * - * @return bool - */ -function wc_product_dimensions_enabled() { - return apply_filters( 'wc_product_dimensions_enabled', true ); -} - -/** - * Clear transient cache for product data. - * - * @param int $post_id (default: 0) The product ID. - */ -function wc_delete_product_transients( $post_id = 0 ) { - // Transient data to clear with a fixed name which may be stale after product updates. - $transients_to_clear = array( - 'wc_products_onsale', - 'wc_featured_products', - 'wc_outofstock_count', - 'wc_low_stock_count', - ); - - foreach ( $transients_to_clear as $transient ) { - delete_transient( $transient ); - } - - if ( $post_id > 0 ) { - // Transient names that include an ID - since they are dynamic they cannot be cleaned in bulk without the ID. - $post_transient_names = array( - 'wc_product_children_', - 'wc_var_prices_', - 'wc_related_', - 'wc_child_has_weight_', - 'wc_child_has_dimensions_', - ); - - foreach ( $post_transient_names as $transient ) { - delete_transient( $transient . $post_id ); - } - } - - // Increments the transient version to invalidate cache. - WC_Cache_Helper::get_transient_version( 'product', true ); - - do_action( 'woocommerce_delete_product_transients', $post_id ); -} - -/** - * Function that returns an array containing the IDs of the products that are on sale. - * - * @since 2.0 - * @return array - */ -function wc_get_product_ids_on_sale() { - // Load from cache. - $product_ids_on_sale = get_transient( 'wc_products_onsale' ); - - // Valid cache found. - if ( false !== $product_ids_on_sale ) { - return $product_ids_on_sale; - } - - $data_store = WC_Data_Store::load( 'product' ); - $on_sale_products = $data_store->get_on_sale_products(); - $product_ids_on_sale = wp_parse_id_list( array_merge( wp_list_pluck( $on_sale_products, 'id' ), array_diff( wp_list_pluck( $on_sale_products, 'parent_id' ), array( 0 ) ) ) ); - - set_transient( 'wc_products_onsale', $product_ids_on_sale, DAY_IN_SECONDS * 30 ); - - return $product_ids_on_sale; -} - -/** - * Function that returns an array containing the IDs of the featured products. - * - * @since 2.1 - * @return array - */ -function wc_get_featured_product_ids() { - // Load from cache. - $featured_product_ids = get_transient( 'wc_featured_products' ); - - // Valid cache found. - if ( false !== $featured_product_ids ) { - return $featured_product_ids; - } - - $data_store = WC_Data_Store::load( 'product' ); - $featured = $data_store->get_featured_product_ids(); - $product_ids = array_keys( $featured ); - $parent_ids = array_values( array_filter( $featured ) ); - $featured_product_ids = array_unique( array_merge( $product_ids, $parent_ids ) ); - - set_transient( 'wc_featured_products', $featured_product_ids, DAY_IN_SECONDS * 30 ); - - return $featured_product_ids; -} - -/** - * Filter to allow product_cat in the permalinks for products. - * - * @param string $permalink The existing permalink URL. - * @param WP_Post $post WP_Post object. - * @return string - */ -function wc_product_post_type_link( $permalink, $post ) { - // Abort if post is not a product. - if ( 'product' !== $post->post_type ) { - return $permalink; - } - - // Abort early if the placeholder rewrite tag isn't in the generated URL. - if ( false === strpos( $permalink, '%' ) ) { - return $permalink; - } - - // Get the custom taxonomy terms in use by this post. - $terms = get_the_terms( $post->ID, 'product_cat' ); - - if ( ! empty( $terms ) ) { - $terms = wp_list_sort( - $terms, - array( - 'parent' => 'DESC', - 'term_id' => 'ASC', - ) - ); - $category_object = apply_filters( 'wc_product_post_type_link_product_cat', $terms[0], $terms, $post ); - $product_cat = $category_object->slug; - - if ( $category_object->parent ) { - $ancestors = get_ancestors( $category_object->term_id, 'product_cat' ); - foreach ( $ancestors as $ancestor ) { - $ancestor_object = get_term( $ancestor, 'product_cat' ); - if ( apply_filters( 'woocommerce_product_post_type_link_parent_category_only', false ) ) { - $product_cat = $ancestor_object->slug; - } else { - $product_cat = $ancestor_object->slug . '/' . $product_cat; - } - } - } - } else { - // If no terms are assigned to this post, use a string instead (can't leave the placeholder there). - $product_cat = _x( 'uncategorized', 'slug', 'woocommerce' ); - } - - $find = array( - '%year%', - '%monthnum%', - '%day%', - '%hour%', - '%minute%', - '%second%', - '%post_id%', - '%category%', - '%product_cat%', - ); - - $replace = array( - date_i18n( 'Y', strtotime( $post->post_date ) ), - date_i18n( 'm', strtotime( $post->post_date ) ), - date_i18n( 'd', strtotime( $post->post_date ) ), - date_i18n( 'H', strtotime( $post->post_date ) ), - date_i18n( 'i', strtotime( $post->post_date ) ), - date_i18n( 's', strtotime( $post->post_date ) ), - $post->ID, - $product_cat, - $product_cat, - ); - - $permalink = str_replace( $find, $replace, $permalink ); - - return $permalink; -} -add_filter( 'post_type_link', 'wc_product_post_type_link', 10, 2 ); - -/** - * Get the placeholder image URL either from media, or use the fallback image. - * - * @param string $size Thumbnail size to use. - * @return string - */ -function wc_placeholder_img_src( $size = 'woocommerce_thumbnail' ) { - $src = WC()->plugin_url() . '/assets/images/placeholder.png'; - $placeholder_image = get_option( 'woocommerce_placeholder_image', 0 ); - - if ( ! empty( $placeholder_image ) ) { - if ( is_numeric( $placeholder_image ) ) { - $image = wp_get_attachment_image_src( $placeholder_image, $size ); - - if ( ! empty( $image[0] ) ) { - $src = $image[0]; - } - } else { - $src = $placeholder_image; - } - } - - return apply_filters( 'woocommerce_placeholder_img_src', $src ); -} - -/** - * Get the placeholder image. - * - * Uses wp_get_attachment_image if using an attachment ID @since 3.6.0 to handle responsiveness. - * - * @param string $size Image size. - * @param string|array $attr Optional. Attributes for the image markup. Default empty. - * @return string - */ -function wc_placeholder_img( $size = 'woocommerce_thumbnail', $attr = '' ) { - $dimensions = wc_get_image_size( $size ); - $placeholder_image = get_option( 'woocommerce_placeholder_image', 0 ); - - $default_attr = array( - 'class' => 'woocommerce-placeholder wp-post-image', - 'alt' => __( 'Placeholder', 'woocommerce' ), - ); - - $attr = wp_parse_args( $attr, $default_attr ); - - if ( wp_attachment_is_image( $placeholder_image ) ) { - $image_html = wp_get_attachment_image( - $placeholder_image, - $size, - false, - $attr - ); - } else { - $image = wc_placeholder_img_src( $size ); - $hwstring = image_hwstring( $dimensions['width'], $dimensions['height'] ); - $attributes = array(); - - foreach ( $attr as $name => $value ) { - $attribute[] = esc_attr( $name ) . '="' . esc_attr( $value ) . '"'; - } - - $image_html = ''; - } - - return apply_filters( 'woocommerce_placeholder_img', $image_html, $size, $dimensions ); -} - -/** - * Variation Formatting. - * - * Gets a formatted version of variation data or item meta. - * - * @param array|WC_Product_Variation $variation Variation object. - * @param bool $flat Should this be a flat list or HTML list? (default: false). - * @param bool $include_names include attribute names/labels in the list. - * @param bool $skip_attributes_in_name Do not list attributes already part of the variation name. - * @return string - */ -function wc_get_formatted_variation( $variation, $flat = false, $include_names = true, $skip_attributes_in_name = false ) { - $return = ''; - - if ( is_a( $variation, 'WC_Product_Variation' ) ) { - $variation_attributes = $variation->get_attributes(); - $product = $variation; - $variation_name = $variation->get_name(); - } else { - $product = false; - $variation_name = ''; - // Remove attribute_ prefix from names. - $variation_attributes = array(); - if ( is_array( $variation ) ) { - foreach ( $variation as $key => $value ) { - $variation_attributes[ str_replace( 'attribute_', '', $key ) ] = $value; - } - } - } - - $list_type = $include_names ? 'dl' : 'ul'; - - if ( is_array( $variation_attributes ) ) { - - if ( ! $flat ) { - $return = '<' . $list_type . ' class="variation">'; - } - - $variation_list = array(); - - foreach ( $variation_attributes as $name => $value ) { - // If this is a term slug, get the term's nice name. - if ( taxonomy_exists( $name ) ) { - $term = get_term_by( 'slug', $value, $name ); - if ( ! is_wp_error( $term ) && ! empty( $term->name ) ) { - $value = $term->name; - } - } - - // Do not list attributes already part of the variation name. - if ( '' === $value || ( $skip_attributes_in_name && wc_is_attribute_in_product_name( $value, $variation_name ) ) ) { - continue; - } - - if ( $include_names ) { - if ( $flat ) { - $variation_list[] = wc_attribute_label( $name, $product ) . ': ' . rawurldecode( $value ); - } else { - $variation_list[] = '
    ' . wc_attribute_label( $name, $product ) . ':
    ' . rawurldecode( $value ) . '
    '; - } - } else { - if ( $flat ) { - $variation_list[] = rawurldecode( $value ); - } else { - $variation_list[] = '
  • ' . rawurldecode( $value ) . '
  • '; - } - } - } - - if ( $flat ) { - $return .= implode( ', ', $variation_list ); - } else { - $return .= implode( '', $variation_list ); - } - - if ( ! $flat ) { - $return .= ''; - } - } - return $return; -} - -/** - * Function which handles the start and end of scheduled sales via cron. - */ -function wc_scheduled_sales() { - $data_store = WC_Data_Store::load( 'product' ); - - // Sales which are due to start. - $product_ids = $data_store->get_starting_sales(); - if ( $product_ids ) { - do_action( 'wc_before_products_starting_sales', $product_ids ); - foreach ( $product_ids as $product_id ) { - $product = wc_get_product( $product_id ); - - if ( $product ) { - $sale_price = $product->get_sale_price(); - - if ( $sale_price ) { - $product->set_price( $sale_price ); - $product->set_date_on_sale_from( '' ); - } else { - $product->set_date_on_sale_to( '' ); - $product->set_date_on_sale_from( '' ); - } - - $product->save(); - } - } - do_action( 'wc_after_products_starting_sales', $product_ids ); - - WC_Cache_Helper::get_transient_version( 'product', true ); - delete_transient( 'wc_products_onsale' ); - } - - // Sales which are due to end. - $product_ids = $data_store->get_ending_sales(); - if ( $product_ids ) { - do_action( 'wc_before_products_ending_sales', $product_ids ); - foreach ( $product_ids as $product_id ) { - $product = wc_get_product( $product_id ); - - if ( $product ) { - $regular_price = $product->get_regular_price(); - $product->set_price( $regular_price ); - $product->set_sale_price( '' ); - $product->set_date_on_sale_to( '' ); - $product->set_date_on_sale_from( '' ); - $product->save(); - } - } - do_action( 'wc_after_products_ending_sales', $product_ids ); - - WC_Cache_Helper::get_transient_version( 'product', true ); - delete_transient( 'wc_products_onsale' ); - } -} -add_action( 'woocommerce_scheduled_sales', 'wc_scheduled_sales' ); - -/** - * Get attachment image attributes. - * - * @param array $attr Image attributes. - * @return array - */ -function wc_get_attachment_image_attributes( $attr ) { - if ( isset( $attr['src'] ) && strstr( $attr['src'], 'woocommerce_uploads/' ) ) { - $attr['src'] = wc_placeholder_img_src(); - - if ( isset( $attr['srcset'] ) ) { - $attr['srcset'] = ''; - } - } - return $attr; -} -add_filter( 'wp_get_attachment_image_attributes', 'wc_get_attachment_image_attributes' ); - - -/** - * Prepare attachment for JavaScript. - * - * @param array $response JS version of a attachment post object. - * @return array - */ -function wc_prepare_attachment_for_js( $response ) { - - if ( isset( $response['url'] ) && strstr( $response['url'], 'woocommerce_uploads/' ) ) { - $response['full']['url'] = wc_placeholder_img_src(); - if ( isset( $response['sizes'] ) ) { - foreach ( $response['sizes'] as $size => $value ) { - $response['sizes'][ $size ]['url'] = wc_placeholder_img_src(); - } - } - } - - return $response; -} -add_filter( 'wp_prepare_attachment_for_js', 'wc_prepare_attachment_for_js' ); - -/** - * Track product views. - */ -function wc_track_product_view() { - if ( ! is_singular( 'product' ) || ! is_active_widget( false, false, 'woocommerce_recently_viewed_products', true ) ) { - return; - } - - global $post; - - if ( empty( $_COOKIE['woocommerce_recently_viewed'] ) ) { // @codingStandardsIgnoreLine. - $viewed_products = array(); - } else { - $viewed_products = wp_parse_id_list( (array) explode( '|', wp_unslash( $_COOKIE['woocommerce_recently_viewed'] ) ) ); // @codingStandardsIgnoreLine. - } - - // Unset if already in viewed products list. - $keys = array_flip( $viewed_products ); - - if ( isset( $keys[ $post->ID ] ) ) { - unset( $viewed_products[ $keys[ $post->ID ] ] ); - } - - $viewed_products[] = $post->ID; - - if ( count( $viewed_products ) > 15 ) { - array_shift( $viewed_products ); - } - - // Store for session only. - wc_setcookie( 'woocommerce_recently_viewed', implode( '|', $viewed_products ) ); -} - -add_action( 'template_redirect', 'wc_track_product_view', 20 ); - -/** - * Get product types. - * - * @since 2.2 - * @return array - */ -function wc_get_product_types() { - return (array) apply_filters( - 'product_type_selector', - array( - 'simple' => __( 'Simple product', 'woocommerce' ), - 'grouped' => __( 'Grouped product', 'woocommerce' ), - 'external' => __( 'External/Affiliate product', 'woocommerce' ), - 'variable' => __( 'Variable product', 'woocommerce' ), - ) - ); -} - -/** - * Check if product sku is unique. - * - * @since 2.2 - * @param int $product_id Product ID. - * @param string $sku Product SKU. - * @return bool - */ -function wc_product_has_unique_sku( $product_id, $sku ) { - $data_store = WC_Data_Store::load( 'product' ); - $sku_found = $data_store->is_existing_sku( $product_id, $sku ); - - if ( apply_filters( 'wc_product_has_unique_sku', $sku_found, $product_id, $sku ) ) { - return false; - } - - return true; -} - -/** - * Force a unique SKU. - * - * @since 3.0.0 - * @param integer $product_id Product ID. - */ -function wc_product_force_unique_sku( $product_id ) { - $product = wc_get_product( $product_id ); - $current_sku = $product ? $product->get_sku( 'edit' ) : ''; - - if ( $current_sku ) { - try { - $new_sku = wc_product_generate_unique_sku( $product_id, $current_sku ); - - if ( $current_sku !== $new_sku ) { - $product->set_sku( $new_sku ); - $product->save(); - } - } catch ( Exception $e ) {} // @codingStandardsIgnoreLine. - } -} - -/** - * Recursively appends a suffix until a unique SKU is found. - * - * @since 3.0.0 - * @param integer $product_id Product ID. - * @param string $sku Product SKU. - * @param integer $index An optional index that can be added to the product SKU. - * @return string - */ -function wc_product_generate_unique_sku( $product_id, $sku, $index = 0 ) { - $generated_sku = 0 < $index ? $sku . '-' . $index : $sku; - - if ( ! wc_product_has_unique_sku( $product_id, $generated_sku ) ) { - $generated_sku = wc_product_generate_unique_sku( $product_id, $sku, ( $index + 1 ) ); - } - - return $generated_sku; -} - -/** - * Get product ID by SKU. - * - * @since 2.3.0 - * @param string $sku Product SKU. - * @return int - */ -function wc_get_product_id_by_sku( $sku ) { - $data_store = WC_Data_Store::load( 'product' ); - return $data_store->get_product_id_by_sku( $sku ); -} - -/** - * Get attributes/data for an individual variation from the database and maintain it's integrity. - * - * @since 2.4.0 - * @param int $variation_id Variation ID. - * @return array - */ -function wc_get_product_variation_attributes( $variation_id ) { - // Build variation data from meta. - $all_meta = get_post_meta( $variation_id ); - $parent_id = wp_get_post_parent_id( $variation_id ); - $parent_attributes = array_filter( (array) get_post_meta( $parent_id, '_product_attributes', true ) ); - $found_parent_attributes = array(); - $variation_attributes = array(); - - // Compare to parent variable product attributes and ensure they match. - foreach ( $parent_attributes as $attribute_name => $options ) { - if ( ! empty( $options['is_variation'] ) ) { - $attribute = 'attribute_' . sanitize_title( $attribute_name ); - $found_parent_attributes[] = $attribute; - if ( ! array_key_exists( $attribute, $variation_attributes ) ) { - $variation_attributes[ $attribute ] = ''; // Add it - 'any' will be asumed. - } - } - } - - // Get the variation attributes from meta. - foreach ( $all_meta as $name => $value ) { - // Only look at valid attribute meta, and also compare variation level attributes and remove any which do not exist at parent level. - if ( 0 !== strpos( $name, 'attribute_' ) || ! in_array( $name, $found_parent_attributes, true ) ) { - unset( $variation_attributes[ $name ] ); - continue; - } - /** - * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute. - * Attempt to get full version of the text attribute from the parent. - */ - if ( sanitize_title( $value[0] ) === $value[0] && version_compare( get_post_meta( $parent_id, '_product_version', true ), '2.4.0', '<' ) ) { - foreach ( $parent_attributes as $attribute ) { - if ( 'attribute_' . sanitize_title( $attribute['name'] ) !== $name ) { - continue; - } - $text_attributes = wc_get_text_attributes( $attribute['value'] ); - - foreach ( $text_attributes as $text_attribute ) { - if ( sanitize_title( $text_attribute ) === $value[0] ) { - $value[0] = $text_attribute; - break; - } - } - } - } - - $variation_attributes[ $name ] = $value[0]; - } - - return $variation_attributes; -} - -/** - * Get all product cats for a product by ID, including hierarchy - * - * @since 2.5.0 - * @param int $product_id Product ID. - * @return array - */ -function wc_get_product_cat_ids( $product_id ) { - $product_cats = wc_get_product_term_ids( $product_id, 'product_cat' ); - - foreach ( $product_cats as $product_cat ) { - $product_cats = array_merge( $product_cats, get_ancestors( $product_cat, 'product_cat' ) ); - } - - return $product_cats; -} - -/** - * Gets data about an attachment, such as alt text and captions. - * - * @since 2.6.0 - * - * @param int|null $attachment_id Attachment ID. - * @param WC_Product|bool $product WC_Product object. - * - * @return array - */ -function wc_get_product_attachment_props( $attachment_id = null, $product = false ) { - $props = array( - 'title' => '', - 'caption' => '', - 'url' => '', - 'alt' => '', - 'src' => '', - 'srcset' => false, - 'sizes' => false, - ); - $attachment = get_post( $attachment_id ); - - if ( $attachment && 'attachment' === $attachment->post_type ) { - $props['title'] = wp_strip_all_tags( $attachment->post_title ); - $props['caption'] = wp_strip_all_tags( $attachment->post_excerpt ); - $props['url'] = wp_get_attachment_url( $attachment_id ); - - // Alt text. - $alt_text = array( wp_strip_all_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ), $props['caption'], wp_strip_all_tags( $attachment->post_title ) ); - - if ( $product && $product instanceof WC_Product ) { - $alt_text[] = wp_strip_all_tags( get_the_title( $product->get_id() ) ); - } - - $alt_text = array_filter( $alt_text ); - $props['alt'] = isset( $alt_text[0] ) ? $alt_text[0] : ''; - - // Large version. - $full_size = apply_filters( 'woocommerce_gallery_full_size', apply_filters( 'woocommerce_product_thumbnails_large_size', 'full' ) ); - $src = wp_get_attachment_image_src( $attachment_id, $full_size ); - $props['full_src'] = $src[0]; - $props['full_src_w'] = $src[1]; - $props['full_src_h'] = $src[2]; - - // Gallery thumbnail. - $gallery_thumbnail = wc_get_image_size( 'gallery_thumbnail' ); - $gallery_thumbnail_size = apply_filters( 'woocommerce_gallery_thumbnail_size', array( $gallery_thumbnail['width'], $gallery_thumbnail['height'] ) ); - $src = wp_get_attachment_image_src( $attachment_id, $gallery_thumbnail_size ); - $props['gallery_thumbnail_src'] = $src[0]; - $props['gallery_thumbnail_src_w'] = $src[1]; - $props['gallery_thumbnail_src_h'] = $src[2]; - - // Thumbnail version. - $thumbnail_size = apply_filters( 'woocommerce_thumbnail_size', 'woocommerce_thumbnail' ); - $src = wp_get_attachment_image_src( $attachment_id, $thumbnail_size ); - $props['thumb_src'] = $src[0]; - $props['thumb_src_w'] = $src[1]; - $props['thumb_src_h'] = $src[2]; - - // Image source. - $image_size = apply_filters( 'woocommerce_gallery_image_size', 'woocommerce_single' ); - $src = wp_get_attachment_image_src( $attachment_id, $image_size ); - $props['src'] = $src[0]; - $props['src_w'] = $src[1]; - $props['src_h'] = $src[2]; - $props['srcset'] = function_exists( 'wp_get_attachment_image_srcset' ) ? wp_get_attachment_image_srcset( $attachment_id, $image_size ) : false; - $props['sizes'] = function_exists( 'wp_get_attachment_image_sizes' ) ? wp_get_attachment_image_sizes( $attachment_id, $image_size ) : false; - } - return $props; -} - -/** - * Get product visibility options. - * - * @since 3.0.0 - * @return array - */ -function wc_get_product_visibility_options() { - return apply_filters( - 'woocommerce_product_visibility_options', - array( - 'visible' => __( 'Shop and search results', 'woocommerce' ), - 'catalog' => __( 'Shop only', 'woocommerce' ), - 'search' => __( 'Search results only', 'woocommerce' ), - 'hidden' => __( 'Hidden', 'woocommerce' ), - ) - ); -} - -/** - * Get product tax class options. - * - * @since 3.0.0 - * @return array - */ -function wc_get_product_tax_class_options() { - $tax_classes = WC_Tax::get_tax_classes(); - $tax_class_options = array(); - $tax_class_options[''] = __( 'Standard', 'woocommerce' ); - - if ( ! empty( $tax_classes ) ) { - foreach ( $tax_classes as $class ) { - $tax_class_options[ sanitize_title( $class ) ] = $class; - } - } - return $tax_class_options; -} - -/** - * Get stock status options. - * - * @since 3.0.0 - * @return array - */ -function wc_get_product_stock_status_options() { - return apply_filters( - 'woocommerce_product_stock_status_options', - array( - 'instock' => __( 'In stock', 'woocommerce' ), - 'outofstock' => __( 'Out of stock', 'woocommerce' ), - 'onbackorder' => __( 'On backorder', 'woocommerce' ), - ) - ); -} - -/** - * Get backorder options. - * - * @since 3.0.0 - * @return array - */ -function wc_get_product_backorder_options() { - return array( - 'no' => __( 'Do not allow', 'woocommerce' ), - 'notify' => __( 'Allow, but notify customer', 'woocommerce' ), - 'yes' => __( 'Allow', 'woocommerce' ), - ); -} - -/** - * Get related products based on product category and tags. - * - * @since 3.0.0 - * @param int $product_id Product ID. - * @param int $limit Limit of results. - * @param array $exclude_ids Exclude IDs from the results. - * @return array - */ -function wc_get_related_products( $product_id, $limit = 5, $exclude_ids = array() ) { - - $product_id = absint( $product_id ); - $limit = $limit >= -1 ? $limit : 5; - $exclude_ids = array_merge( array( 0, $product_id ), $exclude_ids ); - $transient_name = 'wc_related_' . $product_id; - $query_args = http_build_query( - array( - 'limit' => $limit, - 'exclude_ids' => $exclude_ids, - ) - ); - - $transient = get_transient( $transient_name ); - $related_posts = $transient && isset( $transient[ $query_args ] ) ? $transient[ $query_args ] : false; - - // We want to query related posts if they are not cached, or we don't have enough. - if ( false === $related_posts || count( $related_posts ) < $limit ) { - - $cats_array = apply_filters( 'woocommerce_product_related_posts_relate_by_category', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_cat_terms', wc_get_product_term_ids( $product_id, 'product_cat' ), $product_id ) : array(); - $tags_array = apply_filters( 'woocommerce_product_related_posts_relate_by_tag', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_tag_terms', wc_get_product_term_ids( $product_id, 'product_tag' ), $product_id ) : array(); - - // Don't bother if none are set, unless woocommerce_product_related_posts_force_display is set to true in which case all products are related. - if ( empty( $cats_array ) && empty( $tags_array ) && ! apply_filters( 'woocommerce_product_related_posts_force_display', false, $product_id ) ) { - $related_posts = array(); - } else { - $data_store = WC_Data_Store::load( 'product' ); - $related_posts = $data_store->get_related_products( $cats_array, $tags_array, $exclude_ids, $limit + 10, $product_id ); - } - - if ( $transient ) { - $transient[ $query_args ] = $related_posts; - } else { - $transient = array( $query_args => $related_posts ); - } - - set_transient( $transient_name, $transient, DAY_IN_SECONDS ); - } - - $related_posts = apply_filters( - 'woocommerce_related_products', - $related_posts, - $product_id, - array( - 'limit' => $limit, - 'excluded_ids' => $exclude_ids, - ) - ); - - if ( apply_filters( 'woocommerce_product_related_posts_shuffle', true ) ) { - shuffle( $related_posts ); - } - - return array_slice( $related_posts, 0, $limit ); -} - -/** - * Retrieves product term ids for a taxonomy. - * - * @since 3.0.0 - * @param int $product_id Product ID. - * @param string $taxonomy Taxonomy slug. - * @return array - */ -function wc_get_product_term_ids( $product_id, $taxonomy ) { - $terms = get_the_terms( $product_id, $taxonomy ); - return ( empty( $terms ) || is_wp_error( $terms ) ) ? array() : wp_list_pluck( $terms, 'term_id' ); -} - -/** - * For a given product, and optionally price/qty, work out the price with tax included, based on store settings. - * - * @since 3.0.0 - * @param WC_Product $product WC_Product object. - * @param array $args Optional arguments to pass product quantity and price. - * @return float|string Price with tax included, or an empty string if price calculation failed. - */ -function wc_get_price_including_tax( $product, $args = array() ) { - $args = wp_parse_args( - $args, - array( - 'qty' => '', - 'price' => '', - ) - ); - - $price = '' !== $args['price'] ? max( 0.0, (float) $args['price'] ) : $product->get_price(); - $qty = '' !== $args['qty'] ? max( 0.0, (float) $args['qty'] ) : 1; - - if ( '' === $price ) { - return ''; - } elseif ( empty( $qty ) ) { - return 0.0; - } - - $line_price = $price * $qty; - $return_price = $line_price; - - if ( $product->is_taxable() ) { - if ( ! wc_prices_include_tax() ) { - $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); - $taxes = WC_Tax::calc_tax( $line_price, $tax_rates, false ); - - if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { - $taxes_total = array_sum( $taxes ); - } else { - $taxes_total = array_sum( array_map( 'wc_round_tax_total', $taxes ) ); - } - - $return_price = NumberUtil::round( $line_price + $taxes_total, wc_get_price_decimals() ); - } else { - $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); - $base_tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) ); - - /** - * If the customer is excempt from VAT, remove the taxes here. - * Either remove the base or the user taxes depending on woocommerce_adjust_non_base_location_prices setting. - */ - if ( ! empty( WC()->customer ) && WC()->customer->get_is_vat_exempt() ) { // @codingStandardsIgnoreLine. - $remove_taxes = apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ? WC_Tax::calc_tax( $line_price, $base_tax_rates, true ) : WC_Tax::calc_tax( $line_price, $tax_rates, true ); - - if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { - $remove_taxes_total = array_sum( $remove_taxes ); - } else { - $remove_taxes_total = array_sum( array_map( 'wc_round_tax_total', $remove_taxes ) ); - } - - $return_price = NumberUtil::round( $line_price - $remove_taxes_total, wc_get_price_decimals() ); - - /** - * The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing with out of base locations. - * e.g. If a product costs 10 including tax, all users will pay 10 regardless of location and taxes. - * This feature is experimental @since 2.4.7 and may change in the future. Use at your risk. - */ - } elseif ( $tax_rates !== $base_tax_rates && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) { - $base_taxes = WC_Tax::calc_tax( $line_price, $base_tax_rates, true ); - $modded_taxes = WC_Tax::calc_tax( $line_price - array_sum( $base_taxes ), $tax_rates, false ); - - if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { - $base_taxes_total = array_sum( $base_taxes ); - $modded_taxes_total = array_sum( $modded_taxes ); - } else { - $base_taxes_total = array_sum( array_map( 'wc_round_tax_total', $base_taxes ) ); - $modded_taxes_total = array_sum( array_map( 'wc_round_tax_total', $modded_taxes ) ); - } - - $return_price = NumberUtil::round( $line_price - $base_taxes_total + $modded_taxes_total, wc_get_price_decimals() ); - } - } - } - return apply_filters( 'woocommerce_get_price_including_tax', $return_price, $qty, $product ); -} - -/** - * For a given product, and optionally price/qty, work out the price with tax excluded, based on store settings. - * - * @since 3.0.0 - * @param WC_Product $product WC_Product object. - * @param array $args Optional arguments to pass product quantity and price. - * @return float|string Price with tax excluded, or an empty string if price calculation failed. - */ -function wc_get_price_excluding_tax( $product, $args = array() ) { - $args = wp_parse_args( - $args, - array( - 'qty' => '', - 'price' => '', - ) - ); - - $price = '' !== $args['price'] ? max( 0.0, (float) $args['price'] ) : $product->get_price(); - $qty = '' !== $args['qty'] ? max( 0.0, (float) $args['qty'] ) : 1; - - if ( '' === $price ) { - return ''; - } elseif ( empty( $qty ) ) { - return 0.0; - } - - $line_price = $price * $qty; - - if ( $product->is_taxable() && wc_prices_include_tax() ) { - $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); - $base_tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) ); - $remove_taxes = apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ? WC_Tax::calc_tax( $line_price, $base_tax_rates, true ) : WC_Tax::calc_tax( $line_price, $tax_rates, true ); - $return_price = $line_price - array_sum( $remove_taxes ); // Unrounded since we're dealing with tax inclusive prices. Matches logic in cart-totals class. @see adjust_non_base_location_price. - } else { - $return_price = $line_price; - } - - return apply_filters( 'woocommerce_get_price_excluding_tax', $return_price, $qty, $product ); -} - -/** - * Returns the price including or excluding tax, based on the 'woocommerce_tax_display_shop' setting. - * - * @since 3.0.0 - * @param WC_Product $product WC_Product object. - * @param array $args Optional arguments to pass product quantity and price. - * @return float - */ -function wc_get_price_to_display( $product, $args = array() ) { - $args = wp_parse_args( - $args, - array( - 'qty' => 1, - 'price' => $product->get_price(), - ) - ); - - $price = $args['price']; - $qty = $args['qty']; - - return 'incl' === get_option( 'woocommerce_tax_display_shop' ) ? - wc_get_price_including_tax( - $product, - array( - 'qty' => $qty, - 'price' => $price, - ) - ) : - wc_get_price_excluding_tax( - $product, - array( - 'qty' => $qty, - 'price' => $price, - ) - ); -} - -/** - * Returns the product categories in a list. - * - * @param int $product_id Product ID. - * @param string $sep (default: ', '). - * @param string $before (default: ''). - * @param string $after (default: ''). - * @return string - */ -function wc_get_product_category_list( $product_id, $sep = ', ', $before = '', $after = '' ) { - return get_the_term_list( $product_id, 'product_cat', $before, $sep, $after ); -} - -/** - * Returns the product tags in a list. - * - * @param int $product_id Product ID. - * @param string $sep (default: ', '). - * @param string $before (default: ''). - * @param string $after (default: ''). - * @return string - */ -function wc_get_product_tag_list( $product_id, $sep = ', ', $before = '', $after = '' ) { - return get_the_term_list( $product_id, 'product_tag', $before, $sep, $after ); -} - -/** - * Callback for array filter to get visible only. - * - * @since 3.0.0 - * @param WC_Product $product WC_Product object. - * @return bool - */ -function wc_products_array_filter_visible( $product ) { - return $product && is_a( $product, 'WC_Product' ) && $product->is_visible(); -} - -/** - * Callback for array filter to get visible grouped products only. - * - * @since 3.1.0 - * @param WC_Product $product WC_Product object. - * @return bool - */ -function wc_products_array_filter_visible_grouped( $product ) { - return $product && is_a( $product, 'WC_Product' ) && ( 'publish' === $product->get_status() || current_user_can( 'edit_product', $product->get_id() ) ); -} - -/** - * Callback for array filter to get products the user can edit only. - * - * @since 3.0.0 - * @param WC_Product $product WC_Product object. - * @return bool - */ -function wc_products_array_filter_editable( $product ) { - return $product && is_a( $product, 'WC_Product' ) && current_user_can( 'edit_product', $product->get_id() ); -} - -/** - * Callback for array filter to get products the user can view only. - * - * @since 3.4.0 - * @param WC_Product $product WC_Product object. - * @return bool - */ -function wc_products_array_filter_readable( $product ) { - return $product && is_a( $product, 'WC_Product' ) && current_user_can( 'read_product', $product->get_id() ); -} - -/** - * Sort an array of products by a value. - * - * @since 3.0.0 - * - * @param array $products List of products to be ordered. - * @param string $orderby Optional order criteria. - * @param string $order Ascending or descending order. - * - * @return array - */ -function wc_products_array_orderby( $products, $orderby = 'date', $order = 'desc' ) { - $orderby = strtolower( $orderby ); - $order = strtolower( $order ); - switch ( $orderby ) { - case 'title': - case 'id': - case 'date': - case 'modified': - case 'menu_order': - case 'price': - usort( $products, 'wc_products_array_orderby_' . $orderby ); - break; - case 'none': - break; - default: - shuffle( $products ); - break; - } - if ( 'desc' === $order ) { - $products = array_reverse( $products ); - } - return $products; -} - -/** - * Sort by title. - * - * @since 3.0.0 - * @param WC_Product $a First WC_Product object. - * @param WC_Product $b Second WC_Product object. - * @return int - */ -function wc_products_array_orderby_title( $a, $b ) { - return strcasecmp( $a->get_name(), $b->get_name() ); -} - -/** - * Sort by id. - * - * @since 3.0.0 - * @param WC_Product $a First WC_Product object. - * @param WC_Product $b Second WC_Product object. - * @return int - */ -function wc_products_array_orderby_id( $a, $b ) { - if ( $a->get_id() === $b->get_id() ) { - return 0; - } - return ( $a->get_id() < $b->get_id() ) ? -1 : 1; -} - -/** - * Sort by date. - * - * @since 3.0.0 - * @param WC_Product $a First WC_Product object. - * @param WC_Product $b Second WC_Product object. - * @return int - */ -function wc_products_array_orderby_date( $a, $b ) { - if ( $a->get_date_created() === $b->get_date_created() ) { - return 0; - } - return ( $a->get_date_created() < $b->get_date_created() ) ? -1 : 1; -} - -/** - * Sort by modified. - * - * @since 3.0.0 - * @param WC_Product $a First WC_Product object. - * @param WC_Product $b Second WC_Product object. - * @return int - */ -function wc_products_array_orderby_modified( $a, $b ) { - if ( $a->get_date_modified() === $b->get_date_modified() ) { - return 0; - } - return ( $a->get_date_modified() < $b->get_date_modified() ) ? -1 : 1; -} - -/** - * Sort by menu order. - * - * @since 3.0.0 - * @param WC_Product $a First WC_Product object. - * @param WC_Product $b Second WC_Product object. - * @return int - */ -function wc_products_array_orderby_menu_order( $a, $b ) { - if ( $a->get_menu_order() === $b->get_menu_order() ) { - return 0; - } - return ( $a->get_menu_order() < $b->get_menu_order() ) ? -1 : 1; -} - -/** - * Sort by price low to high. - * - * @since 3.0.0 - * @param WC_Product $a First WC_Product object. - * @param WC_Product $b Second WC_Product object. - * @return int - */ -function wc_products_array_orderby_price( $a, $b ) { - if ( $a->get_price() === $b->get_price() ) { - return 0; - } - return ( $a->get_price() < $b->get_price() ) ? -1 : 1; -} - -/** - * Queue a product for syncing at the end of the request. - * - * @param int $product_id Product ID. - */ -function wc_deferred_product_sync( $product_id ) { - global $wc_deferred_product_sync; - - if ( empty( $wc_deferred_product_sync ) ) { - $wc_deferred_product_sync = array(); - } - - $wc_deferred_product_sync[] = $product_id; -} - -/** - * See if the lookup table is being generated already. - * - * @since 3.6.0 - * @return bool - */ -function wc_update_product_lookup_tables_is_running() { - $table_updates_pending = WC()->queue()->search( - array( - 'status' => 'pending', - 'group' => 'wc_update_product_lookup_tables', - 'per_page' => 1, - ) - ); - - return (bool) count( $table_updates_pending ); -} - -/** - * Populate lookup table data for products. - * - * @since 3.6.0 - */ -function wc_update_product_lookup_tables() { - global $wpdb; - - $is_cli = Constants::is_true( 'WP_CLI' ); - - if ( ! $is_cli ) { - WC_Admin_Notices::add_notice( 'regenerating_lookup_table' ); - } - - // Note that the table is not yet generated. - update_option( 'woocommerce_product_lookup_table_is_generating', true ); - - // Make a row per product in lookup table. - $wpdb->query( - " - INSERT IGNORE INTO {$wpdb->wc_product_meta_lookup} (`product_id`) - SELECT - posts.ID - FROM {$wpdb->posts} posts - WHERE - posts.post_type IN ('product', 'product_variation') - " - ); - - // List of column names in the lookup table we need to populate. - $columns = array( - 'min_max_price', - 'stock_quantity', - 'sku', - 'stock_status', - 'average_rating', - 'total_sales', - 'downloadable', - 'virtual', - 'onsale', - 'tax_class', - 'tax_status', // When last column is updated, woocommerce_product_lookup_table_is_generating is updated. - ); - - foreach ( $columns as $index => $column ) { - if ( $is_cli ) { - wc_update_product_lookup_tables_column( $column ); - } else { - WC()->queue()->schedule_single( - time() + $index, - 'wc_update_product_lookup_tables_column', - array( - 'column' => $column, - ), - 'wc_update_product_lookup_tables' - ); - } - } - - // Rating counts are serialised so they have to be unserialised before populating the lookup table. - if ( $is_cli ) { - $rating_count_rows = $wpdb->get_results( - " - SELECT post_id, meta_value FROM {$wpdb->postmeta} - WHERE meta_key = '_wc_rating_count' - AND meta_value != '' - AND meta_value != 'a:0:{}' - ", - ARRAY_A - ); - wc_update_product_lookup_tables_rating_count( $rating_count_rows ); - } else { - WC()->queue()->schedule_single( - time() + 10, - 'wc_update_product_lookup_tables_rating_count_batch', - array( - 'offset' => 0, - 'limit' => 50, - ), - 'wc_update_product_lookup_tables' - ); - } -} - -/** - * Populate lookup table column data. - * - * @since 3.6.0 - * @param string $column Column name to set. - */ -function wc_update_product_lookup_tables_column( $column ) { - if ( empty( $column ) ) { - return; - } - global $wpdb; - switch ( $column ) { - case 'min_max_price': - $wpdb->query( - " - UPDATE - {$wpdb->wc_product_meta_lookup} lookup_table - INNER JOIN ( - SELECT lookup_table.product_id, MIN( meta_value+0 ) as min_price, MAX( meta_value+0 ) as max_price - FROM {$wpdb->wc_product_meta_lookup} lookup_table - LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_price' - WHERE - meta1.meta_value <> '' - GROUP BY lookup_table.product_id - ) as source on source.product_id = lookup_table.product_id - SET - lookup_table.min_price = source.min_price, - lookup_table.max_price = source.max_price - " - ); - break; - case 'stock_quantity': - $wpdb->query( - " - UPDATE - {$wpdb->wc_product_meta_lookup} lookup_table - LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_manage_stock' - LEFT JOIN {$wpdb->postmeta} meta2 ON lookup_table.product_id = meta2.post_id AND meta2.meta_key = '_stock' - SET - lookup_table.stock_quantity = meta2.meta_value - WHERE - meta1.meta_value = 'yes' - " - ); - break; - case 'sku': - case 'stock_status': - case 'average_rating': - case 'total_sales': - case 'tax_class': - case 'tax_status': - if ( 'total_sales' === $column ) { - $meta_key = 'total_sales'; - } elseif ( 'average_rating' === $column ) { - $meta_key = '_wc_average_rating'; - } else { - $meta_key = '_' . $column; - } - $column = esc_sql( $column ); - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $wpdb->query( - $wpdb->prepare( - " - UPDATE - {$wpdb->wc_product_meta_lookup} lookup_table - LEFT JOIN {$wpdb->postmeta} meta ON lookup_table.product_id = meta.post_id AND meta.meta_key = %s - SET - lookup_table.`{$column}` = meta.meta_value - ", - $meta_key - ) - ); - // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared - break; - case 'downloadable': - case 'virtual': - $column = esc_sql( $column ); - $meta_key = '_' . $column; - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $wpdb->query( - $wpdb->prepare( - " - UPDATE - {$wpdb->wc_product_meta_lookup} lookup_table - LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = %s - SET - lookup_table.`{$column}` = IF ( meta1.meta_value = 'yes', 1, 0 ) - ", - $meta_key - ) - ); - // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared - break; - case 'onsale': - $column = esc_sql( $column ); - $decimals = absint( wc_get_price_decimals() ); - - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $wpdb->query( - $wpdb->prepare( - " - UPDATE - {$wpdb->wc_product_meta_lookup} lookup_table - LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_price' - LEFT JOIN {$wpdb->postmeta} meta2 ON lookup_table.product_id = meta2.post_id AND meta2.meta_key = '_sale_price' - SET - lookup_table.`{$column}` = IF ( - CAST( meta1.meta_value AS DECIMAL ) >= 0 - AND CAST( meta2.meta_value AS CHAR ) != '' - AND CAST( meta1.meta_value AS DECIMAL( 10, %d ) ) = CAST( meta2.meta_value AS DECIMAL( 10, %d ) ) - , 1, 0 ) - ", - $decimals, - $decimals - ) - ); - // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared - break; - } - - // Final column - mark complete. - if ( 'tax_status' === $column ) { - delete_option( 'woocommerce_product_lookup_table_is_generating' ); - } -} -add_action( 'wc_update_product_lookup_tables_column', 'wc_update_product_lookup_tables_column' ); - -/** - * Populate rating count lookup table data for products. - * - * @since 3.6.0 - * @param array $rows Rows of rating counts to update in lookup table. - */ -function wc_update_product_lookup_tables_rating_count( $rows ) { - if ( ! $rows || ! is_array( $rows ) ) { - return; - } - global $wpdb; - - foreach ( $rows as $row ) { - $count = array_sum( (array) maybe_unserialize( $row['meta_value'] ) ); - $wpdb->update( - $wpdb->wc_product_meta_lookup, - array( - 'rating_count' => absint( $count ), - ), - array( - 'product_id' => absint( $row['post_id'] ), - ) - ); - } -} - -/** - * Populate a batch of rating count lookup table data for products. - * - * @since 3.6.2 - * @param array $offset Offset to query. - * @param array $limit Limit to query. - */ -function wc_update_product_lookup_tables_rating_count_batch( $offset = 0, $limit = 0 ) { - global $wpdb; - - if ( ! $limit ) { - return; - } - - $rating_count_rows = $wpdb->get_results( - $wpdb->prepare( - " - SELECT post_id, meta_value FROM {$wpdb->postmeta} - WHERE meta_key = '_wc_rating_count' - AND meta_value != '' - AND meta_value != 'a:0:{}' - ORDER BY post_id ASC - LIMIT %d, %d - ", - $offset, - $limit - ), - ARRAY_A - ); - - if ( $rating_count_rows ) { - wc_update_product_lookup_tables_rating_count( $rating_count_rows ); - WC()->queue()->schedule_single( - time() + 1, - 'wc_update_product_lookup_tables_rating_count_batch', - array( - 'offset' => $offset + $limit, - 'limit' => $limit, - ), - 'wc_update_product_lookup_tables' - ); - } -} -add_action( 'wc_update_product_lookup_tables_rating_count_batch', 'wc_update_product_lookup_tables_rating_count_batch', 10, 2 ); diff --git a/includes/wc-rest-functions.php b/includes/wc-rest-functions.php deleted file mode 100644 index 54029b4074e..00000000000 --- a/includes/wc-rest-functions.php +++ /dev/null @@ -1,356 +0,0 @@ -setTimezone( new DateTimeZone( wc_timezone_string() ) ); - } elseif ( is_string( $date ) ) { - $date = new WC_DateTime( $date, new DateTimeZone( 'UTC' ) ); - $date->setTimezone( new DateTimeZone( wc_timezone_string() ) ); - } - - if ( ! is_a( $date, 'WC_DateTime' ) ) { - return null; - } - - // Get timestamp before changing timezone to UTC. - return gmdate( 'Y-m-d\TH:i:s', $utc ? $date->getTimestamp() : $date->getOffsetTimestamp() ); -} - -/** - * Returns image mime types users are allowed to upload via the API. - * - * @since 2.6.4 - * @return array - */ -function wc_rest_allowed_image_mime_types() { - return apply_filters( - 'woocommerce_rest_allowed_image_mime_types', - array( - 'jpg|jpeg|jpe' => 'image/jpeg', - 'gif' => 'image/gif', - 'png' => 'image/png', - 'bmp' => 'image/bmp', - 'tiff|tif' => 'image/tiff', - 'ico' => 'image/x-icon', - ) - ); -} - -/** - * Upload image from URL. - * - * @since 2.6.0 - * @param string $image_url Image URL. - * @return array|WP_Error Attachment data or error message. - */ -function wc_rest_upload_image_from_url( $image_url ) { - $parsed_url = wp_parse_url( $image_url ); - - // Check parsed URL. - if ( ! $parsed_url || ! is_array( $parsed_url ) ) { - /* translators: %s: image URL */ - return new WP_Error( 'woocommerce_rest_invalid_image_url', sprintf( __( 'Invalid URL %s.', 'woocommerce' ), $image_url ), array( 'status' => 400 ) ); - } - - // Ensure url is valid. - $image_url = esc_url_raw( $image_url ); - - // download_url function is part of wp-admin. - if ( ! function_exists( 'download_url' ) ) { - include_once ABSPATH . 'wp-admin/includes/file.php'; - } - - $file_array = array(); - $file_array['name'] = basename( current( explode( '?', $image_url ) ) ); - - // Download file to temp location. - $file_array['tmp_name'] = download_url( $image_url ); - - // If error storing temporarily, return the error. - if ( is_wp_error( $file_array['tmp_name'] ) ) { - return new WP_Error( - 'woocommerce_rest_invalid_remote_image_url', - /* translators: %s: image URL */ - sprintf( __( 'Error getting remote image %s.', 'woocommerce' ), $image_url ) . ' ' - /* translators: %s: error message */ - . sprintf( __( 'Error: %s', 'woocommerce' ), $file_array['tmp_name']->get_error_message() ), - array( 'status' => 400 ) - ); - } - - // Do the validation and storage stuff. - $file = wp_handle_sideload( - $file_array, - array( - 'test_form' => false, - 'mimes' => wc_rest_allowed_image_mime_types(), - ), - current_time( 'Y/m' ) - ); - - if ( isset( $file['error'] ) ) { - @unlink( $file_array['tmp_name'] ); // @codingStandardsIgnoreLine. - - /* translators: %s: error message */ - return new WP_Error( 'woocommerce_rest_invalid_image', sprintf( __( 'Invalid image: %s', 'woocommerce' ), $file['error'] ), array( 'status' => 400 ) ); - } - - do_action( 'woocommerce_rest_api_uploaded_image_from_url', $file, $image_url ); - - return $file; -} - -/** - * Set uploaded image as attachment. - * - * @since 2.6.0 - * @param array $upload Upload information from wp_upload_bits. - * @param int $id Post ID. Default to 0. - * @return int Attachment ID - */ -function wc_rest_set_uploaded_image_as_attachment( $upload, $id = 0 ) { - $info = wp_check_filetype( $upload['file'] ); - $title = ''; - $content = ''; - - if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) { - include_once ABSPATH . 'wp-admin/includes/image.php'; - } - - $image_meta = wp_read_image_metadata( $upload['file'] ); - if ( $image_meta ) { - if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { - $title = wc_clean( $image_meta['title'] ); - } - if ( trim( $image_meta['caption'] ) ) { - $content = wc_clean( $image_meta['caption'] ); - } - } - - $attachment = array( - 'post_mime_type' => $info['type'], - 'guid' => $upload['url'], - 'post_parent' => $id, - 'post_title' => $title ? $title : basename( $upload['file'] ), - 'post_content' => $content, - ); - - $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id ); - if ( ! is_wp_error( $attachment_id ) ) { - wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); - } - - return $attachment_id; -} - -/** - * Validate reports request arguments. - * - * @since 2.6.0 - * @param mixed $value Value to valdate. - * @param WP_REST_Request $request Request instance. - * @param string $param Param to validate. - * @return WP_Error|boolean - */ -function wc_rest_validate_reports_request_arg( $value, $request, $param ) { - - $attributes = $request->get_attributes(); - if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) { - return true; - } - $args = $attributes['args'][ $param ]; - - if ( 'string' === $args['type'] && ! is_string( $value ) ) { - /* translators: 1: param 2: type */ - return new WP_Error( 'woocommerce_rest_invalid_param', sprintf( __( '%1$s is not of type %2$s', 'woocommerce' ), $param, 'string' ) ); - } - - if ( 'date' === $args['format'] ) { - $regex = '#^\d{4}-\d{2}-\d{2}$#'; - - if ( ! preg_match( $regex, $value, $matches ) ) { - return new WP_Error( 'woocommerce_rest_invalid_date', __( 'The date you provided is invalid.', 'woocommerce' ) ); - } - } - - return true; -} - -/** - * Encodes a value according to RFC 3986. - * Supports multidimensional arrays. - * - * @since 2.6.0 - * @param string|array $value The value to encode. - * @return string|array Encoded values. - */ -function wc_rest_urlencode_rfc3986( $value ) { - if ( is_array( $value ) ) { - return array_map( 'wc_rest_urlencode_rfc3986', $value ); - } - - return str_replace( array( '+', '%7E' ), array( ' ', '~' ), rawurlencode( $value ) ); -} - -/** - * Check permissions of posts on REST API. - * - * @since 2.6.0 - * @param string $post_type Post type. - * @param string $context Request context. - * @param int $object_id Post ID. - * @return bool - */ -function wc_rest_check_post_permissions( $post_type, $context = 'read', $object_id = 0 ) { - $contexts = array( - 'read' => 'read_private_posts', - 'create' => 'publish_posts', - 'edit' => 'edit_post', - 'delete' => 'delete_post', - 'batch' => 'edit_others_posts', - ); - - if ( 'revision' === $post_type ) { - $permission = false; - } else { - $cap = $contexts[ $context ]; - $post_type_object = get_post_type_object( $post_type ); - $permission = current_user_can( $post_type_object->cap->$cap, $object_id ); - } - - return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, $post_type ); -} - -/** - * Check permissions of users on REST API. - * - * @since 2.6.0 - * @param string $context Request context. - * @param int $object_id Post ID. - * @return bool - */ -function wc_rest_check_user_permissions( $context = 'read', $object_id = 0 ) { - $contexts = array( - 'read' => 'list_users', - 'create' => 'promote_users', // Check if current user can create users, shop managers are not allowed to create users. - 'edit' => 'edit_users', - 'delete' => 'delete_users', - 'batch' => 'promote_users', - ); - - // Check to allow shop_managers to manage only customers. - if ( in_array( $context, array( 'edit', 'delete' ), true ) && wc_current_user_has_role( 'shop_manager' ) ) { - $permission = false; - $user_data = get_userdata( $object_id ); - $shop_manager_editable_roles = apply_filters( 'woocommerce_shop_manager_editable_roles', array( 'customer' ) ); - - if ( isset( $user_data->roles ) ) { - $can_manage_users = array_intersect( $user_data->roles, array_unique( $shop_manager_editable_roles ) ); - - // Check if Shop Manager can edit customer or with the is same shop manager. - if ( 0 < count( $can_manage_users ) || intval( $object_id ) === intval( get_current_user_id() ) ) { - $permission = current_user_can( $contexts[ $context ], $object_id ); - } - } - } else { - $permission = current_user_can( $contexts[ $context ], $object_id ); - } - - return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, 'user' ); -} - -/** - * Check permissions of product terms on REST API. - * - * @since 2.6.0 - * @param string $taxonomy Taxonomy. - * @param string $context Request context. - * @param int $object_id Post ID. - * @return bool - */ -function wc_rest_check_product_term_permissions( $taxonomy, $context = 'read', $object_id = 0 ) { - $contexts = array( - 'read' => 'manage_terms', - 'create' => 'edit_terms', - 'edit' => 'edit_terms', - 'delete' => 'delete_terms', - 'batch' => 'edit_terms', - ); - - $cap = $contexts[ $context ]; - $taxonomy_object = get_taxonomy( $taxonomy ); - $permission = current_user_can( $taxonomy_object->cap->$cap, $object_id ); - - return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, $taxonomy ); -} - -/** - * Check manager permissions on REST API. - * - * @since 2.6.0 - * @param string $object Object. - * @param string $context Request context. - * @return bool - */ -function wc_rest_check_manager_permissions( $object, $context = 'read' ) { - $objects = array( - 'reports' => 'view_woocommerce_reports', - 'settings' => 'manage_woocommerce', - 'system_status' => 'manage_woocommerce', - 'attributes' => 'manage_product_terms', - 'shipping_methods' => 'manage_woocommerce', - 'payment_gateways' => 'manage_woocommerce', - 'webhooks' => 'manage_woocommerce', - ); - - $permission = current_user_can( $objects[ $object ] ); - - return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, 0, $object ); -} - -/** - * Check product reviews permissions on REST API. - * - * @since 3.5.0 - * @param string $context Request context. - * @param string $object_id Object ID. - * @return bool - */ -function wc_rest_check_product_reviews_permissions( $context = 'read', $object_id = 0 ) { - $permission = false; - $contexts = array( - 'read' => 'moderate_comments', - 'create' => 'moderate_comments', - 'edit' => 'moderate_comments', - 'delete' => 'moderate_comments', - 'batch' => 'moderate_comments', - ); - - if ( isset( $contexts[ $context ] ) ) { - $permission = current_user_can( $contexts[ $context ] ); - } - - return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, 'product_review' ); -} diff --git a/includes/wc-stock-functions.php b/includes/wc-stock-functions.php deleted file mode 100644 index 4cbf1ed90cc..00000000000 --- a/includes/wc-stock-functions.php +++ /dev/null @@ -1,407 +0,0 @@ -managing_stock() ) { - // Some products (variations) can have their stock managed by their parent. Get the correct object to be updated here. - $product_id_with_stock = $product->get_stock_managed_by_id(); - $product_with_stock = $product_id_with_stock !== $product->get_id() ? wc_get_product( $product_id_with_stock ) : $product; - $data_store = WC_Data_Store::load( 'product' ); - - // Fire actions to let 3rd parties know the stock is about to be changed. - if ( $product_with_stock->is_type( 'variation' ) ) { - do_action( 'woocommerce_variation_before_set_stock', $product_with_stock ); - } else { - do_action( 'woocommerce_product_before_set_stock', $product_with_stock ); - } - - // Update the database. - $new_stock = $data_store->update_product_stock( $product_id_with_stock, $stock_quantity, $operation ); - - // Update the product object. - $data_store->read_stock_quantity( $product_with_stock, $new_stock ); - - // If this is not being called during an update routine, save the product so stock status etc is in sync, and caches are cleared. - if ( ! $updating ) { - $product_with_stock->save(); - } - - // Fire actions to let 3rd parties know the stock changed. - if ( $product_with_stock->is_type( 'variation' ) ) { - do_action( 'woocommerce_variation_set_stock', $product_with_stock ); - } else { - do_action( 'woocommerce_product_set_stock', $product_with_stock ); - } - - return $product_with_stock->get_stock_quantity(); - } - return $product->get_stock_quantity(); -} - -/** - * Update a product's stock status. - * - * @param int $product_id Product ID. - * @param string $status Status. - */ -function wc_update_product_stock_status( $product_id, $status ) { - $product = wc_get_product( $product_id ); - - if ( $product ) { - $product->set_stock_status( $status ); - $product->save(); - } -} - -/** - * When a payment is complete, we can reduce stock levels for items within an order. - * - * @since 3.0.0 - * @param int $order_id Order ID. - */ -function wc_maybe_reduce_stock_levels( $order_id ) { - $order = wc_get_order( $order_id ); - - if ( ! $order ) { - return; - } - - $stock_reduced = $order->get_data_store()->get_stock_reduced( $order_id ); - $trigger_reduce = apply_filters( 'woocommerce_payment_complete_reduce_order_stock', ! $stock_reduced, $order_id ); - - // Only continue if we're reducing stock. - if ( ! $trigger_reduce ) { - return; - } - - wc_reduce_stock_levels( $order ); - - // Ensure stock is marked as "reduced" in case payment complete or other stock actions are called. - $order->get_data_store()->set_stock_reduced( $order_id, true ); -} -add_action( 'woocommerce_payment_complete', 'wc_maybe_reduce_stock_levels' ); -add_action( 'woocommerce_order_status_completed', 'wc_maybe_reduce_stock_levels' ); -add_action( 'woocommerce_order_status_processing', 'wc_maybe_reduce_stock_levels' ); -add_action( 'woocommerce_order_status_on-hold', 'wc_maybe_reduce_stock_levels' ); - -/** - * When a payment is cancelled, restore stock. - * - * @since 3.0.0 - * @param int $order_id Order ID. - */ -function wc_maybe_increase_stock_levels( $order_id ) { - $order = wc_get_order( $order_id ); - - if ( ! $order ) { - return; - } - - $stock_reduced = $order->get_data_store()->get_stock_reduced( $order_id ); - $trigger_increase = (bool) $stock_reduced; - - // Only continue if we're increasing stock. - if ( ! $trigger_increase ) { - return; - } - - wc_increase_stock_levels( $order ); - - // Ensure stock is not marked as "reduced" anymore. - $order->get_data_store()->set_stock_reduced( $order_id, false ); -} -add_action( 'woocommerce_order_status_cancelled', 'wc_maybe_increase_stock_levels' ); -add_action( 'woocommerce_order_status_pending', 'wc_maybe_increase_stock_levels' ); - -/** - * Reduce stock levels for items within an order, if stock has not already been reduced for the items. - * - * @since 3.0.0 - * @param int|WC_Order $order_id Order ID or order instance. - */ -function wc_reduce_stock_levels( $order_id ) { - if ( is_a( $order_id, 'WC_Order' ) ) { - $order = $order_id; - $order_id = $order->get_id(); - } else { - $order = wc_get_order( $order_id ); - } - // We need an order, and a store with stock management to continue. - if ( ! $order || 'yes' !== get_option( 'woocommerce_manage_stock' ) || ! apply_filters( 'woocommerce_can_reduce_order_stock', true, $order ) ) { - return; - } - - $changes = array(); - - // Loop over all items. - foreach ( $order->get_items() as $item ) { - if ( ! $item->is_type( 'line_item' ) ) { - continue; - } - - // Only reduce stock once for each item. - $product = $item->get_product(); - $item_stock_reduced = $item->get_meta( '_reduced_stock', true ); - - if ( $item_stock_reduced || ! $product || ! $product->managing_stock() ) { - continue; - } - - /** - * Filter order item quantity. - * - * @param int|float $quantity Quantity. - * @param WC_Order $order Order data. - * @param WC_Order_Item_Product $item Order item data. - */ - $qty = apply_filters( 'woocommerce_order_item_quantity', $item->get_quantity(), $order, $item ); - $item_name = $product->get_formatted_name(); - $new_stock = wc_update_product_stock( $product, $qty, 'decrease' ); - - if ( is_wp_error( $new_stock ) ) { - /* translators: %s item name. */ - $order->add_order_note( sprintf( __( 'Unable to reduce stock for item %s.', 'woocommerce' ), $item_name ) ); - continue; - } - - $item->add_meta_data( '_reduced_stock', $qty, true ); - $item->save(); - - $changes[] = array( - 'product' => $product, - 'from' => $new_stock + $qty, - 'to' => $new_stock, - ); - } - - wc_trigger_stock_change_notifications( $order, $changes ); - - do_action( 'woocommerce_reduce_order_stock', $order ); -} - -/** - * After stock change events, triggers emails and adds order notes. - * - * @since 3.5.0 - * @param WC_Order $order order object. - * @param array $changes Array of changes. - */ -function wc_trigger_stock_change_notifications( $order, $changes ) { - if ( empty( $changes ) ) { - return; - } - - $order_notes = array(); - $no_stock_amount = absint( get_option( 'woocommerce_notify_no_stock_amount', 0 ) ); - - foreach ( $changes as $change ) { - $order_notes[] = $change['product']->get_formatted_name() . ' ' . $change['from'] . '→' . $change['to']; - $low_stock_amount = absint( wc_get_low_stock_amount( wc_get_product( $change['product']->get_id() ) ) ); - if ( $change['to'] <= $no_stock_amount ) { - do_action( 'woocommerce_no_stock', wc_get_product( $change['product']->get_id() ) ); - } elseif ( $change['to'] <= $low_stock_amount ) { - do_action( 'woocommerce_low_stock', wc_get_product( $change['product']->get_id() ) ); - } - - if ( $change['to'] < 0 ) { - do_action( - 'woocommerce_product_on_backorder', - array( - 'product' => wc_get_product( $change['product']->get_id() ), - 'order_id' => $order->get_id(), - 'quantity' => abs( $change['from'] - $change['to'] ), - ) - ); - } - } - - $order->add_order_note( __( 'Stock levels reduced:', 'woocommerce' ) . ' ' . implode( ', ', $order_notes ) ); -} - -/** - * Increase stock levels for items within an order. - * - * @since 3.0.0 - * @param int|WC_Order $order_id Order ID or order instance. - */ -function wc_increase_stock_levels( $order_id ) { - if ( is_a( $order_id, 'WC_Order' ) ) { - $order = $order_id; - $order_id = $order->get_id(); - } else { - $order = wc_get_order( $order_id ); - } - - // We need an order, and a store with stock management to continue. - if ( ! $order || 'yes' !== get_option( 'woocommerce_manage_stock' ) || ! apply_filters( 'woocommerce_can_restore_order_stock', true, $order ) ) { - return; - } - - $changes = array(); - - // Loop over all items. - foreach ( $order->get_items() as $item ) { - if ( ! $item->is_type( 'line_item' ) ) { - continue; - } - - // Only increase stock once for each item. - $product = $item->get_product(); - $item_stock_reduced = $item->get_meta( '_reduced_stock', true ); - - if ( ! $item_stock_reduced || ! $product || ! $product->managing_stock() ) { - continue; - } - - $item_name = $product->get_formatted_name(); - $new_stock = wc_update_product_stock( $product, $item_stock_reduced, 'increase' ); - - if ( is_wp_error( $new_stock ) ) { - /* translators: %s item name. */ - $order->add_order_note( sprintf( __( 'Unable to restore stock for item %s.', 'woocommerce' ), $item_name ) ); - continue; - } - - $item->delete_meta_data( '_reduced_stock' ); - $item->save(); - - $changes[] = $item_name . ' ' . ( $new_stock - $item_stock_reduced ) . '→' . $new_stock; - } - - if ( $changes ) { - $order->add_order_note( __( 'Stock levels increased:', 'woocommerce' ) . ' ' . implode( ', ', $changes ) ); - } - - do_action( 'woocommerce_restore_order_stock', $order ); -} - -/** - * See how much stock is being held in pending orders. - * - * @since 3.5.0 - * @param WC_Product $product Product to check. - * @param integer $exclude_order_id Order ID to exclude. - * @return int - */ -function wc_get_held_stock_quantity( WC_Product $product, $exclude_order_id = 0 ) { - /** - * Filter: woocommerce_hold_stock_for_checkout - * Allows enable/disable hold stock functionality on checkout. - * - * @since 4.3.0 - * @param bool $enabled Default to true if managing stock globally. - */ - if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) { - return 0; - } - - return ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->get_reserved_stock( $product, $exclude_order_id ); -} - -/** - * Hold stock for an order. - * - * @throws ReserveStockException If reserve stock fails. - * - * @since 4.1.0 - * @param \WC_Order|int $order Order ID or instance. - */ -function wc_reserve_stock_for_order( $order ) { - /** - * Filter: woocommerce_hold_stock_for_checkout - * Allows enable/disable hold stock functionality on checkout. - * - * @since @since 4.1.0 - * @param bool $enabled Default to true if managing stock globally. - */ - if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) { - return; - } - - $order = $order instanceof WC_Order ? $order : wc_get_order( $order ); - - if ( $order ) { - ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->reserve_stock_for_order( $order ); - } -} -add_action( 'woocommerce_checkout_order_created', 'wc_reserve_stock_for_order' ); - -/** - * Release held stock for an order. - * - * @since 4.3.0 - * @param \WC_Order|int $order Order ID or instance. - */ -function wc_release_stock_for_order( $order ) { - /** - * Filter: woocommerce_hold_stock_for_checkout - * Allows enable/disable hold stock functionality on checkout. - * - * @since 4.3.0 - * @param bool $enabled Default to true if managing stock globally. - */ - if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) { - return; - } - - $order = $order instanceof WC_Order ? $order : wc_get_order( $order ); - - if ( $order ) { - ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->release_stock_for_order( $order ); - } -} -add_action( 'woocommerce_checkout_order_exception', 'wc_release_stock_for_order' ); -add_action( 'woocommerce_payment_complete', 'wc_release_stock_for_order', 11 ); -add_action( 'woocommerce_order_status_cancelled', 'wc_release_stock_for_order', 11 ); -add_action( 'woocommerce_order_status_completed', 'wc_release_stock_for_order', 11 ); -add_action( 'woocommerce_order_status_processing', 'wc_release_stock_for_order', 11 ); -add_action( 'woocommerce_order_status_on-hold', 'wc_release_stock_for_order', 11 ); - -/** - * Return low stock amount to determine if notification needs to be sent - * - * @param WC_Product $product Product to get data from. - * @since 3.5.0 - * @return int - */ -function wc_get_low_stock_amount( WC_Product $product ) { - if ( $product->is_type( 'variation' ) ) { - $product = wc_get_product( $product->get_parent_id() ); - } - $low_stock_amount = $product->get_low_stock_amount(); - if ( '' === $low_stock_amount ) { - $low_stock_amount = get_option( 'woocommerce_notify_low_stock_amount', 2 ); - } - - return $low_stock_amount; -} diff --git a/includes/wc-template-functions.php b/includes/wc-template-functions.php deleted file mode 100644 index e6bcf282656..00000000000 --- a/includes/wc-template-functions.php +++ /dev/null @@ -1,3787 +0,0 @@ -cart->is_empty() && empty( $wp->query_vars['order-pay'] ) && ! isset( $wp->query_vars['order-received'] ) && ! is_customize_preview() && apply_filters( 'woocommerce_checkout_redirect_empty_cart', true ) ) { - wc_add_notice( __( 'Checkout is not available whilst your cart is empty.', 'woocommerce' ), 'notice' ); - wp_safe_redirect( wc_get_cart_url() ); - exit; - - } - - // Logout. - if ( isset( $wp->query_vars['customer-logout'] ) && ! empty( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'customer-logout' ) ) { - wp_safe_redirect( str_replace( '&', '&', wp_logout_url( wc_get_page_permalink( 'myaccount' ) ) ) ); - exit; - } - - // Redirect to the correct logout endpoint. - if ( isset( $wp->query_vars['customer-logout'] ) && 'true' === $wp->query_vars['customer-logout'] ) { - wp_safe_redirect( esc_url_raw( wc_get_account_endpoint_url( 'customer-logout' ) ) ); - exit; - } - - // Trigger 404 if trying to access an endpoint on wrong page. - if ( is_wc_endpoint_url() && ! is_account_page() && ! is_checkout() && apply_filters( 'woocommerce_account_endpoint_page_not_found', true ) ) { - $wp_query->set_404(); - status_header( 404 ); - include get_query_template( '404' ); - exit; - } - - // Redirect to the product page if we have a single product. - if ( is_search() && is_post_type_archive( 'product' ) && apply_filters( 'woocommerce_redirect_single_search_result', true ) && 1 === absint( $wp_query->found_posts ) ) { - $product = wc_get_product( $wp_query->post ); - - if ( $product && $product->is_visible() ) { - wp_safe_redirect( get_permalink( $product->get_id() ), 302 ); - exit; - } - } - - // Ensure gateways and shipping methods are loaded early. - if ( is_add_payment_method_page() || is_checkout() ) { - // Buffer the checkout page. - ob_start(); - - // Ensure gateways and shipping methods are loaded early. - WC()->payment_gateways(); - WC()->shipping(); - } -} -add_action( 'template_redirect', 'wc_template_redirect' ); - -/** - * When loading sensitive checkout or account pages, send a HTTP header to limit rendering of pages to same origin iframes for security reasons. - * - * Can be disabled with: remove_action( 'template_redirect', 'wc_send_frame_options_header' ); - * - * @since 2.3.10 - */ -function wc_send_frame_options_header() { - - if ( ( is_checkout() || is_account_page() ) && ! is_customize_preview() ) { - send_frame_options_header(); - } -} -add_action( 'template_redirect', 'wc_send_frame_options_header' ); - -/** - * No index our endpoints. - * Prevent indexing pages like order-received. - * - * @since 2.5.3 - */ -function wc_prevent_endpoint_indexing() { - // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.PHP.NoSilencedErrors.Discouraged - if ( is_wc_endpoint_url() || isset( $_GET['download_file'] ) ) { - @header( 'X-Robots-Tag: noindex' ); - } - // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.PHP.NoSilencedErrors.Discouraged -} -add_action( 'template_redirect', 'wc_prevent_endpoint_indexing' ); - -/** - * Remove adjacent_posts_rel_link_wp_head - pointless for products. - * - * @since 3.0.0 - */ -function wc_prevent_adjacent_posts_rel_link_wp_head() { - if ( is_singular( 'product' ) ) { - remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head', 10, 0 ); - } -} -add_action( 'template_redirect', 'wc_prevent_adjacent_posts_rel_link_wp_head' ); - -/** - * Show the gallery if JS is disabled. - * - * @since 3.0.6 - */ -function wc_gallery_noscript() { - ?> - - post_type ) || ! in_array( $post->post_type, array( 'product', 'product_variation' ), true ) ) { - return; - } - - $GLOBALS['product'] = wc_get_product( $post ); - - return $GLOBALS['product']; -} -add_action( 'the_post', 'wc_setup_product_data' ); - -/** - * Sets up the woocommerce_loop global from the passed args or from the main query. - * - * @since 3.3.0 - * @param array $args Args to pass into the global. - */ -function wc_setup_loop( $args = array() ) { - $default_args = array( - 'loop' => 0, - 'columns' => wc_get_default_products_per_row(), - 'name' => '', - 'is_shortcode' => false, - 'is_paginated' => true, - 'is_search' => false, - 'is_filtered' => false, - 'total' => 0, - 'total_pages' => 0, - 'per_page' => 0, - 'current_page' => 1, - ); - - // If this is a main WC query, use global args as defaults. - if ( $GLOBALS['wp_query']->get( 'wc_query' ) ) { - $default_args = array_merge( - $default_args, - array( - 'is_search' => $GLOBALS['wp_query']->is_search(), - 'is_filtered' => is_filtered(), - 'total' => $GLOBALS['wp_query']->found_posts, - 'total_pages' => $GLOBALS['wp_query']->max_num_pages, - 'per_page' => $GLOBALS['wp_query']->get( 'posts_per_page' ), - 'current_page' => max( 1, $GLOBALS['wp_query']->get( 'paged', 1 ) ), - ) - ); - } - - // Merge any existing values. - if ( isset( $GLOBALS['woocommerce_loop'] ) ) { - $default_args = array_merge( $default_args, $GLOBALS['woocommerce_loop'] ); - } - - $GLOBALS['woocommerce_loop'] = wp_parse_args( $args, $default_args ); -} -add_action( 'woocommerce_before_shop_loop', 'wc_setup_loop' ); - -/** - * Resets the woocommerce_loop global. - * - * @since 3.3.0 - */ -function wc_reset_loop() { - unset( $GLOBALS['woocommerce_loop'] ); -} -add_action( 'woocommerce_after_shop_loop', 'woocommerce_reset_loop', 999 ); - -/** - * Gets a property from the woocommerce_loop global. - * - * @since 3.3.0 - * @param string $prop Prop to get. - * @param string $default Default if the prop does not exist. - * @return mixed - */ -function wc_get_loop_prop( $prop, $default = '' ) { - wc_setup_loop(); // Ensure shop loop is setup. - - return isset( $GLOBALS['woocommerce_loop'], $GLOBALS['woocommerce_loop'][ $prop ] ) ? $GLOBALS['woocommerce_loop'][ $prop ] : $default; -} - -/** - * Sets a property in the woocommerce_loop global. - * - * @since 3.3.0 - * @param string $prop Prop to set. - * @param string $value Value to set. - */ -function wc_set_loop_prop( $prop, $value = '' ) { - if ( ! isset( $GLOBALS['woocommerce_loop'] ) ) { - wc_setup_loop(); - } - $GLOBALS['woocommerce_loop'][ $prop ] = $value; -} - -/** - * Set the current visbility for a product in the woocommerce_loop global. - * - * @since 4.4.0 - * @param int $product_id Product it to cache visbiility for. - * @param bool $value The poduct visibility value to cache. - */ -function wc_set_loop_product_visibility( $product_id, $value ) { - wc_set_loop_prop( "product_visibility_$product_id", $value ); -} - -/** - * Gets the cached current visibility for a product from the woocommerce_loop global. - * - * @since 4.4.0 - * @param int $product_id Product id to get the cached visibility for. - * - * @return bool|null The cached product visibility, or null if on visibility has been cached for that product. - */ -function wc_get_loop_product_visibility( $product_id ) { - return wc_get_loop_prop( "product_visibility_$product_id", null ); -} - -/** - * Should the WooCommerce loop be displayed? - * - * This will return true if we have posts (products) or if we have subcats to display. - * - * @since 3.4.0 - * @return bool - */ -function woocommerce_product_loop() { - return have_posts() || 'products' !== woocommerce_get_loop_display_mode(); -} - -/** - * Output generator tag to aid debugging. - * - * @param string $gen Generator. - * @param string $type Type. - * @return string - */ -function wc_generator_tag( $gen, $type ) { - $version = Constants::get_constant( 'WC_VERSION' ); - - switch ( $type ) { - case 'html': - $gen .= "\n" . ''; - break; - case 'xhtml': - $gen .= "\n" . ''; - break; - } - return $gen; -} - -/** - * Add body classes for WC pages. - * - * @param array $classes Body Classes. - * @return array - */ -function wc_body_class( $classes ) { - $classes = (array) $classes; - - if ( is_shop() ) { - - $classes[] = 'woocommerce-shop'; - - } - - if ( is_woocommerce() ) { - - $classes[] = 'woocommerce'; - $classes[] = 'woocommerce-page'; - - } elseif ( is_checkout() ) { - - $classes[] = 'woocommerce-checkout'; - $classes[] = 'woocommerce-page'; - - } elseif ( is_cart() ) { - - $classes[] = 'woocommerce-cart'; - $classes[] = 'woocommerce-page'; - - } elseif ( is_account_page() ) { - - $classes[] = 'woocommerce-account'; - $classes[] = 'woocommerce-page'; - - } - - if ( is_store_notice_showing() ) { - $classes[] = 'woocommerce-demo-store'; - } - - foreach ( WC()->query->get_query_vars() as $key => $value ) { - if ( is_wc_endpoint_url( $key ) ) { - $classes[] = 'woocommerce-' . sanitize_html_class( $key ); - } - } - - $classes[] = 'woocommerce-no-js'; - - add_action( 'wp_footer', 'wc_no_js' ); - - return array_unique( $classes ); -} - -/** - * NO JS handling. - * - * @since 3.4.0 - */ -function wc_no_js() { - ?> - - $max_columns ) { - $columns = $max_columns; - update_option( 'woocommerce_catalog_columns', $columns ); - } - - if ( has_filter( 'loop_shop_columns' ) ) { // Legacy filter handling. - $columns = apply_filters( 'loop_shop_columns', $columns ); - } - - $columns = absint( $columns ); - - return max( 1, $columns ); -} - -/** - * Get the default rows setting - this is how many product rows will be shown in loops. - * - * @since 3.3.0 - * @return int - */ -function wc_get_default_product_rows_per_page() { - $rows = absint( get_option( 'woocommerce_catalog_rows', 4 ) ); - $product_grid = wc_get_theme_support( 'product_grid' ); - $min_rows = isset( $product_grid['min_rows'] ) ? absint( $product_grid['min_rows'] ) : 0; - $max_rows = isset( $product_grid['max_rows'] ) ? absint( $product_grid['max_rows'] ) : 0; - - if ( $min_rows && $rows < $min_rows ) { - $rows = $min_rows; - update_option( 'woocommerce_catalog_rows', $rows ); - } elseif ( $max_rows && $rows > $max_rows ) { - $rows = $max_rows; - update_option( 'woocommerce_catalog_rows', $rows ); - } - - return $rows; -} - -/** - * Reset the product grid settings when a new theme is activated. - * - * @since 3.3.0 - */ -function wc_reset_product_grid_settings() { - $product_grid = wc_get_theme_support( 'product_grid' ); - - if ( ! empty( $product_grid['default_rows'] ) ) { - update_option( 'woocommerce_catalog_rows', absint( $product_grid['default_rows'] ) ); - } - - if ( ! empty( $product_grid['default_columns'] ) ) { - update_option( 'woocommerce_catalog_columns', absint( $product_grid['default_columns'] ) ); - } - - wp_cache_flush(); // Flush any caches which could impact settings or templates. -} -add_action( 'after_switch_theme', 'wc_reset_product_grid_settings' ); - -/** - * Get classname for woocommerce loops. - * - * @since 2.6.0 - * @return string - */ -function wc_get_loop_class() { - $loop_index = wc_get_loop_prop( 'loop', 0 ); - $columns = absint( max( 1, wc_get_loop_prop( 'columns', wc_get_default_products_per_row() ) ) ); - - $loop_index ++; - wc_set_loop_prop( 'loop', $loop_index ); - - if ( 0 === ( $loop_index - 1 ) % $columns || 1 === $columns ) { - return 'first'; - } - - if ( 0 === $loop_index % $columns ) { - return 'last'; - } - - return ''; -} - - -/** - * Get the classes for the product cat div. - * - * @since 2.4.0 - * - * @param string|array $class One or more classes to add to the class list. - * @param object $category object Optional. - * - * @return array - */ -function wc_get_product_cat_class( $class = '', $category = null ) { - $classes = is_array( $class ) ? $class : array_map( 'trim', explode( ' ', $class ) ); - $classes[] = 'product-category'; - $classes[] = 'product'; - $classes[] = wc_get_loop_class(); - $classes = apply_filters( 'product_cat_class', $classes, $class, $category ); - - return array_unique( array_filter( $classes ) ); -} - -/** - * Adds extra post classes for products via the WordPress post_class hook, if used. - * - * Note: For performance reasons we instead recommend using wc_product_class/wc_get_product_class instead. - * - * @since 2.1.0 - * @param array $classes Current classes. - * @param string|array $class Additional class. - * @param int $post_id Post ID. - * @return array - */ -function wc_product_post_class( $classes, $class = '', $post_id = 0 ) { - if ( ! $post_id || ! in_array( get_post_type( $post_id ), array( 'product', 'product_variation' ), true ) ) { - return $classes; - } - - $product = wc_get_product( $post_id ); - - if ( ! $product ) { - return $classes; - } - - $classes[] = 'product'; - $classes[] = wc_get_loop_class(); - $classes[] = $product->get_stock_status(); - - if ( $product->is_on_sale() ) { - $classes[] = 'sale'; - } - if ( $product->is_featured() ) { - $classes[] = 'featured'; - } - if ( $product->is_downloadable() ) { - $classes[] = 'downloadable'; - } - if ( $product->is_virtual() ) { - $classes[] = 'virtual'; - } - if ( $product->is_sold_individually() ) { - $classes[] = 'sold-individually'; - } - if ( $product->is_taxable() ) { - $classes[] = 'taxable'; - } - if ( $product->is_shipping_taxable() ) { - $classes[] = 'shipping-taxable'; - } - if ( $product->is_purchasable() ) { - $classes[] = 'purchasable'; - } - if ( $product->get_type() ) { - $classes[] = 'product-type-' . $product->get_type(); - } - if ( $product->is_type( 'variable' ) && $product->get_default_attributes() ) { - $classes[] = 'has-default-attributes'; - } - - $key = array_search( 'hentry', $classes, true ); - if ( false !== $key ) { - unset( $classes[ $key ] ); - } - - return $classes; -} - -/** - * Get product taxonomy HTML classes. - * - * @since 3.4.0 - * @param array $term_ids Array of terms IDs or objects. - * @param string $taxonomy Taxonomy. - * @return array - */ -function wc_get_product_taxonomy_class( $term_ids, $taxonomy ) { - $classes = array(); - - foreach ( $term_ids as $term_id ) { - $term = get_term( $term_id, $taxonomy ); - - if ( empty( $term->slug ) ) { - continue; - } - - $term_class = sanitize_html_class( $term->slug, $term->term_id ); - if ( is_numeric( $term_class ) || ! trim( $term_class, '-' ) ) { - $term_class = $term->term_id; - } - - // 'post_tag' uses the 'tag' prefix for backward compatibility. - if ( 'post_tag' === $taxonomy ) { - $classes[] = 'tag-' . $term_class; - } else { - $classes[] = sanitize_html_class( $taxonomy . '-' . $term_class, $taxonomy . '-' . $term->term_id ); - } - } - - return $classes; -} - -/** - * Retrieves the classes for the post div as an array. - * - * This method was modified from WordPress's get_post_class() to allow the removal of taxonomies - * (for performance reasons). Previously wc_product_post_class was hooked into post_class. @since 3.6.0 - * - * @since 3.4.0 - * @param string|array $class One or more classes to add to the class list. - * @param int|WP_Post|WC_Product $product Product ID or product object. - * @return array - */ -function wc_get_product_class( $class = '', $product = null ) { - if ( is_null( $product ) && ! empty( $GLOBALS['product'] ) ) { - // Product was null so pull from global. - $product = $GLOBALS['product']; - } - - if ( $product && ! is_a( $product, 'WC_Product' ) ) { - // Make sure we have a valid product, or set to false. - $product = wc_get_product( $product ); - } - - if ( $class ) { - if ( ! is_array( $class ) ) { - $class = preg_split( '#\s+#', $class ); - } - } else { - $class = array(); - } - - $post_classes = array_map( 'esc_attr', $class ); - - if ( ! $product ) { - return $post_classes; - } - - // Run through the post_class hook so 3rd parties using this previously can still append classes. - // Note, to change classes you will need to use the newer woocommerce_post_class filter. - // @internal This removes the wc_product_post_class filter so classes are not duplicated. - $filtered = has_filter( 'post_class', 'wc_product_post_class' ); - - if ( $filtered ) { - remove_filter( 'post_class', 'wc_product_post_class', 20 ); - } - - $post_classes = apply_filters( 'post_class', $post_classes, $class, $product->get_id() ); - - if ( $filtered ) { - add_filter( 'post_class', 'wc_product_post_class', 20, 3 ); - } - - $classes = array_merge( - $post_classes, - array( - 'product', - 'type-product', - 'post-' . $product->get_id(), - 'status-' . $product->get_status(), - wc_get_loop_class(), - $product->get_stock_status(), - ), - wc_get_product_taxonomy_class( $product->get_category_ids(), 'product_cat' ), - wc_get_product_taxonomy_class( $product->get_tag_ids(), 'product_tag' ) - ); - - if ( $product->get_image_id() ) { - $classes[] = 'has-post-thumbnail'; - } - if ( $product->get_post_password() ) { - $classes[] = post_password_required( $product->get_id() ) ? 'post-password-required' : 'post-password-protected'; - } - if ( $product->is_on_sale() ) { - $classes[] = 'sale'; - } - if ( $product->is_featured() ) { - $classes[] = 'featured'; - } - if ( $product->is_downloadable() ) { - $classes[] = 'downloadable'; - } - if ( $product->is_virtual() ) { - $classes[] = 'virtual'; - } - if ( $product->is_sold_individually() ) { - $classes[] = 'sold-individually'; - } - if ( $product->is_taxable() ) { - $classes[] = 'taxable'; - } - if ( $product->is_shipping_taxable() ) { - $classes[] = 'shipping-taxable'; - } - if ( $product->is_purchasable() ) { - $classes[] = 'purchasable'; - } - if ( $product->get_type() ) { - $classes[] = 'product-type-' . $product->get_type(); - } - if ( $product->is_type( 'variable' ) && $product->get_default_attributes() ) { - $classes[] = 'has-default-attributes'; - } - - // Include attributes and any extra taxonomies only if enabled via the hook - this is a performance issue. - if ( apply_filters( 'woocommerce_get_product_class_include_taxonomies', false ) ) { - $taxonomies = get_taxonomies( array( 'public' => true ) ); - $type = 'variation' === $product->get_type() ? 'product_variation' : 'product'; - foreach ( (array) $taxonomies as $taxonomy ) { - if ( is_object_in_taxonomy( $type, $taxonomy ) && ! in_array( $taxonomy, array( 'product_cat', 'product_tag' ), true ) ) { - $classes = array_merge( $classes, wc_get_product_taxonomy_class( (array) get_the_terms( $product->get_id(), $taxonomy ), $taxonomy ) ); - } - } - } - - /** - * WooCommerce Post Class filter. - * - * @since 3.6.2 - * @param array $classes Array of CSS classes. - * @param WC_Product $product Product object. - */ - $classes = apply_filters( 'woocommerce_post_class', $classes, $product ); - - return array_map( 'esc_attr', array_unique( array_filter( $classes ) ) ); -} - -/** - * Display the classes for the product div. - * - * @since 3.4.0 - * @param string|array $class One or more classes to add to the class list. - * @param int|WP_Post|WC_Product $product_id Product ID or product object. - */ -function wc_product_class( $class = '', $product_id = null ) { - echo 'class="' . esc_attr( implode( ' ', wc_get_product_class( $class, $product_id ) ) ) . '"'; -} - -/** - * Outputs hidden form inputs for each query string variable. - * - * @since 3.0.0 - * @param string|array $values Name value pairs, or a URL to parse. - * @param array $exclude Keys to exclude. - * @param string $current_key Current key we are outputting. - * @param bool $return Whether to return. - * @return string - */ -function wc_query_string_form_fields( $values = null, $exclude = array(), $current_key = '', $return = false ) { - if ( is_null( $values ) ) { - // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $values = $_GET; - } elseif ( is_string( $values ) ) { - $url_parts = wp_parse_url( $values ); - $values = array(); - - if ( ! empty( $url_parts['query'] ) ) { - // This is to preserve full-stops, pluses and spaces in the query string when ran through parse_str. - $replace_chars = array( - '.' => '{dot}', - '+' => '{plus}', - ); - - $query_string = str_replace( array_keys( $replace_chars ), array_values( $replace_chars ), $url_parts['query'] ); - - // Parse the string. - parse_str( $query_string, $parsed_query_string ); - - // Convert the full-stops, pluses and spaces back and add to values array. - foreach ( $parsed_query_string as $key => $value ) { - $new_key = str_replace( array_values( $replace_chars ), array_keys( $replace_chars ), $key ); - $new_value = str_replace( array_values( $replace_chars ), array_keys( $replace_chars ), $value ); - $values[ $new_key ] = $new_value; - } - } - } - $html = ''; - - foreach ( $values as $key => $value ) { - if ( in_array( $key, $exclude, true ) ) { - continue; - } - if ( $current_key ) { - $key = $current_key . '[' . $key . ']'; - } - if ( is_array( $value ) ) { - $html .= wc_query_string_form_fields( $value, $exclude, $key, true ); - } else { - $html .= ''; - } - } - - if ( $return ) { - return $html; - } - - echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -} - -/** - * Get the terms and conditons page ID. - * - * @since 3.4.0 - * @return int - */ -function wc_terms_and_conditions_page_id() { - $page_id = wc_get_page_id( 'terms' ); - return apply_filters( 'woocommerce_terms_and_conditions_page_id', 0 < $page_id ? absint( $page_id ) : 0 ); -} - -/** - * Get the privacy policy page ID. - * - * @since 3.4.0 - * @return int - */ -function wc_privacy_policy_page_id() { - $page_id = get_option( 'wp_page_for_privacy_policy', 0 ); - return apply_filters( 'woocommerce_privacy_policy_page_id', 0 < $page_id ? absint( $page_id ) : 0 ); -} - -/** - * See if the checkbox is enabled or not based on the existance of the terms page and checkbox text. - * - * @since 3.4.0 - * @return bool - */ -function wc_terms_and_conditions_checkbox_enabled() { - $page_id = wc_terms_and_conditions_page_id(); - $page = $page_id ? get_post( $page_id ) : false; - return $page && wc_get_terms_and_conditions_checkbox_text(); -} - -/** - * Get the terms and conditons checkbox text, if set. - * - * @since 3.4.0 - * @return string - */ -function wc_get_terms_and_conditions_checkbox_text() { - /* translators: %s terms and conditions page name and link */ - return trim( apply_filters( 'woocommerce_get_terms_and_conditions_checkbox_text', get_option( 'woocommerce_checkout_terms_and_conditions_checkbox_text', sprintf( __( 'I have read and agree to the website %s', 'woocommerce' ), '[terms]' ) ) ) ); -} - -/** - * Get the privacy policy text, if set. - * - * @since 3.4.0 - * @param string $type Type of policy to load. Valid values include registration and checkout. - * @return string - */ -function wc_get_privacy_policy_text( $type = '' ) { - $text = ''; - - switch ( $type ) { - case 'checkout': - /* translators: %s privacy policy page name and link */ - $text = get_option( 'woocommerce_checkout_privacy_policy_text', sprintf( __( 'Your personal data will be used to process your order, support your experience throughout this website, and for other purposes described in our %s.', 'woocommerce' ), '[privacy_policy]' ) ); - break; - case 'registration': - /* translators: %s privacy policy page name and link */ - $text = get_option( 'woocommerce_registration_privacy_policy_text', sprintf( __( 'Your personal data will be used to support your experience throughout this website, to manage access to your account, and for other purposes described in our %s.', 'woocommerce' ), '[privacy_policy]' ) ); - break; - } - - return trim( apply_filters( 'woocommerce_get_privacy_policy_text', $text, $type ) ); -} - -/** - * Output t&c checkbox text. - * - * @since 3.4.0 - */ -function wc_terms_and_conditions_checkbox_text() { - $text = wc_get_terms_and_conditions_checkbox_text(); - - if ( ! $text ) { - return; - } - - echo wp_kses_post( wc_replace_policy_page_link_placeholders( $text ) ); -} - -/** - * Output t&c page's content (if set). The page can be set from checkout settings. - * - * @since 3.4.0 - */ -function wc_terms_and_conditions_page_content() { - $terms_page_id = wc_terms_and_conditions_page_id(); - - if ( ! $terms_page_id ) { - return; - } - - $page = get_post( $terms_page_id ); - - if ( $page && 'publish' === $page->post_status && $page->post_content && ! has_shortcode( $page->post_content, 'woocommerce_checkout' ) ) { - echo ''; - } -} - -/** - * Render privacy policy text on the checkout. - * - * @since 3.4.0 - */ -function wc_checkout_privacy_policy_text() { - echo '
    '; - wc_privacy_policy_text( 'checkout' ); - echo '
    '; -} - -/** - * Render privacy policy text on the register forms. - * - * @since 3.4.0 - */ -function wc_registration_privacy_policy_text() { - echo '
    '; - wc_privacy_policy_text( 'registration' ); - echo '
    '; -} - -/** - * Output privacy policy text. This is custom text which can be added via the customizer/privacy settings section. - * - * Loads the relevant policy for the current page unless a specific policy text is required. - * - * @since 3.4.0 - * @param string $type Type of policy to load. Valid values include registration and checkout. - */ -function wc_privacy_policy_text( $type = 'checkout' ) { - if ( ! wc_privacy_policy_page_id() ) { - return; - } - echo wp_kses_post( wpautop( wc_replace_policy_page_link_placeholders( wc_get_privacy_policy_text( $type ) ) ) ); -} - -/** - * Replaces placeholders with links to WooCommerce policy pages. - * - * @since 3.4.0 - * @param string $text Text to find/replace within. - * @return string - */ -function wc_replace_policy_page_link_placeholders( $text ) { - $privacy_page_id = wc_privacy_policy_page_id(); - $terms_page_id = wc_terms_and_conditions_page_id(); - $privacy_link = $privacy_page_id ? '' . __( 'privacy policy', 'woocommerce' ) . '' : __( 'privacy policy', 'woocommerce' ); - $terms_link = $terms_page_id ? '' . __( 'terms and conditions', 'woocommerce' ) . '' : __( 'terms and conditions', 'woocommerce' ); - - $find_replace = array( - '[terms]' => $terms_link, - '[privacy_policy]' => $privacy_link, - ); - - return str_replace( array_keys( $find_replace ), array_values( $find_replace ), $text ); -} - -/** - * Template pages - */ - -if ( ! function_exists( 'woocommerce_content' ) ) { - - /** - * Output WooCommerce content. - * - * This function is only used in the optional 'woocommerce.php' template. - * which people can add to their themes to add basic woocommerce support. - * without hooks or modifying core templates. - */ - function woocommerce_content() { - - if ( is_singular( 'product' ) ) { - - while ( have_posts() ) : - the_post(); - wc_get_template_part( 'content', 'single-product' ); - endwhile; - - } else { - ?> - - - -

    - - - - - - - - - - - - - - - - - - - - - - - ' . wp_kses_post( $notice ) . ' ' . esc_html__( 'Dismiss', 'woocommerce' ) . '

    ', $notice ); - } -} - -/** - * Loop - */ - -if ( ! function_exists( 'woocommerce_page_title' ) ) { - - /** - * Page Title function. - * - * @param bool $echo Should echo title. - * @return string - */ - function woocommerce_page_title( $echo = true ) { - - if ( is_search() ) { - /* translators: %s: search query */ - $page_title = sprintf( __( 'Search results: “%s”', 'woocommerce' ), get_search_query() ); - - if ( get_query_var( 'paged' ) ) { - /* translators: %s: page number */ - $page_title .= sprintf( __( ' – Page %s', 'woocommerce' ), get_query_var( 'paged' ) ); - } - } elseif ( is_tax() ) { - - $page_title = single_term_title( '', false ); - - } else { - - $shop_page_id = wc_get_page_id( 'shop' ); - $page_title = get_the_title( $shop_page_id ); - - } - - $page_title = apply_filters( 'woocommerce_page_title', $page_title ); - - if ( $echo ) { - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo $page_title; - } else { - return $page_title; - } - } -} - -if ( ! function_exists( 'woocommerce_product_loop_start' ) ) { - - /** - * Output the start of a product loop. By default this is a UL. - * - * @param bool $echo Should echo?. - * @return string - */ - function woocommerce_product_loop_start( $echo = true ) { - ob_start(); - - wc_set_loop_prop( 'loop', 0 ); - - wc_get_template( 'loop/loop-start.php' ); - - $loop_start = apply_filters( 'woocommerce_product_loop_start', ob_get_clean() ); - - if ( $echo ) { - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo $loop_start; - } else { - return $loop_start; - } - } -} - -if ( ! function_exists( 'woocommerce_product_loop_end' ) ) { - - /** - * Output the end of a product loop. By default this is a UL. - * - * @param bool $echo Should echo?. - * @return string - */ - function woocommerce_product_loop_end( $echo = true ) { - ob_start(); - - wc_get_template( 'loop/loop-end.php' ); - - $loop_end = apply_filters( 'woocommerce_product_loop_end', ob_get_clean() ); - - if ( $echo ) { - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo $loop_end; - } else { - return $loop_end; - } - } -} -if ( ! function_exists( 'woocommerce_template_loop_product_title' ) ) { - - /** - * Show the product title in the product loop. By default this is an H2. - */ - function woocommerce_template_loop_product_title() { - echo '

    ' . get_the_title() . '

    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } -} -if ( ! function_exists( 'woocommerce_template_loop_category_title' ) ) { - - /** - * Show the subcategory title in the product loop. - * - * @param object $category Category object. - */ - function woocommerce_template_loop_category_title( $category ) { - ?> -

    - name ); - - if ( $category->count > 0 ) { - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo apply_filters( 'woocommerce_subcategory_count_html', ' (' . esc_html( $category->count ) . ')', $category ); - } - ?> -

    - '; - } -} - -if ( ! function_exists( 'woocommerce_template_loop_product_link_close' ) ) { - /** - * Insert the closing anchor tag for products in the loop. - */ - function woocommerce_template_loop_product_link_close() { - echo ''; - } -} - -if ( ! function_exists( 'woocommerce_template_loop_category_link_open' ) ) { - /** - * Insert the opening anchor tag for categories in the loop. - * - * @param int|object|string $category Category ID, Object or String. - */ - function woocommerce_template_loop_category_link_open( $category ) { - echo ''; - } -} - -if ( ! function_exists( 'woocommerce_template_loop_category_link_close' ) ) { - /** - * Insert the closing anchor tag for categories in the loop. - */ - function woocommerce_template_loop_category_link_close() { - echo ''; - } -} - -if ( ! function_exists( 'woocommerce_taxonomy_archive_description' ) ) { - - /** - * Show an archive description on taxonomy archives. - */ - function woocommerce_taxonomy_archive_description() { - if ( is_product_taxonomy() && 0 === absint( get_query_var( 'paged' ) ) ) { - $term = get_queried_object(); - - if ( $term && ! empty( $term->description ) ) { - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo '
    ' . wc_format_content( $term->description ) . '
    '; - } - } - } -} -if ( ! function_exists( 'woocommerce_product_archive_description' ) ) { - - /** - * Show a shop page description on product archives. - */ - function woocommerce_product_archive_description() { - // Don't display the description on search results page. - if ( is_search() ) { - return; - } - - if ( is_post_type_archive( 'product' ) && in_array( absint( get_query_var( 'paged' ) ), array( 0, 1 ), true ) ) { - $shop_page = get_post( wc_get_page_id( 'shop' ) ); - if ( $shop_page ) { - $description = wc_format_content( $shop_page->post_content ); - if ( $description ) { - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo '
    ' . $description . '
    '; - } - } - } - } -} - -if ( ! function_exists( 'woocommerce_template_loop_add_to_cart' ) ) { - - /** - * Get the add to cart template for the loop. - * - * @param array $args Arguments. - */ - function woocommerce_template_loop_add_to_cart( $args = array() ) { - global $product; - - if ( $product ) { - $defaults = array( - 'quantity' => 1, - 'class' => implode( - ' ', - array_filter( - array( - 'button', - 'product_type_' . $product->get_type(), - $product->is_purchasable() && $product->is_in_stock() ? 'add_to_cart_button' : '', - $product->supports( 'ajax_add_to_cart' ) && $product->is_purchasable() && $product->is_in_stock() ? 'ajax_add_to_cart' : '', - ) - ) - ), - 'attributes' => array( - 'data-product_id' => $product->get_id(), - 'data-product_sku' => $product->get_sku(), - 'aria-label' => $product->add_to_cart_description(), - 'rel' => 'nofollow', - ), - ); - - $args = apply_filters( 'woocommerce_loop_add_to_cart_args', wp_parse_args( $args, $defaults ), $product ); - - if ( isset( $args['attributes']['aria-label'] ) ) { - $args['attributes']['aria-label'] = wp_strip_all_tags( $args['attributes']['aria-label'] ); - } - - wc_get_template( 'loop/add-to-cart.php', $args ); - } - } -} - -if ( ! function_exists( 'woocommerce_template_loop_product_thumbnail' ) ) { - - /** - * Get the product thumbnail for the loop. - */ - function woocommerce_template_loop_product_thumbnail() { - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo woocommerce_get_product_thumbnail(); - } -} -if ( ! function_exists( 'woocommerce_template_loop_price' ) ) { - - /** - * Get the product price for the loop. - */ - function woocommerce_template_loop_price() { - wc_get_template( 'loop/price.php' ); - } -} -if ( ! function_exists( 'woocommerce_template_loop_rating' ) ) { - - /** - * Display the average rating in the loop. - */ - function woocommerce_template_loop_rating() { - wc_get_template( 'loop/rating.php' ); - } -} -if ( ! function_exists( 'woocommerce_show_product_loop_sale_flash' ) ) { - - /** - * Get the sale flash for the loop. - */ - function woocommerce_show_product_loop_sale_flash() { - wc_get_template( 'loop/sale-flash.php' ); - } -} - -if ( ! function_exists( 'woocommerce_get_product_thumbnail' ) ) { - - /** - * Get the product thumbnail, or the placeholder if not set. - * - * @param string $size (default: 'woocommerce_thumbnail'). - * @param int $deprecated1 Deprecated since WooCommerce 2.0 (default: 0). - * @param int $deprecated2 Deprecated since WooCommerce 2.0 (default: 0). - * @return string - */ - function woocommerce_get_product_thumbnail( $size = 'woocommerce_thumbnail', $deprecated1 = 0, $deprecated2 = 0 ) { - global $product; - - $image_size = apply_filters( 'single_product_archive_thumbnail_size', $size ); - - return $product ? $product->get_image( $image_size ) : ''; - } -} - -if ( ! function_exists( 'woocommerce_result_count' ) ) { - - /** - * Output the result count text (Showing x - x of x results). - */ - function woocommerce_result_count() { - if ( ! wc_get_loop_prop( 'is_paginated' ) || ! woocommerce_products_will_display() ) { - return; - } - $args = array( - 'total' => wc_get_loop_prop( 'total' ), - 'per_page' => wc_get_loop_prop( 'per_page' ), - 'current' => wc_get_loop_prop( 'current_page' ), - ); - - wc_get_template( 'loop/result-count.php', $args ); - } -} - -if ( ! function_exists( 'woocommerce_catalog_ordering' ) ) { - - /** - * Output the product sorting options. - */ - function woocommerce_catalog_ordering() { - if ( ! wc_get_loop_prop( 'is_paginated' ) || ! woocommerce_products_will_display() ) { - return; - } - $show_default_orderby = 'menu_order' === apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby', 'menu_order' ) ); - $catalog_orderby_options = apply_filters( - 'woocommerce_catalog_orderby', - array( - 'menu_order' => __( 'Default sorting', 'woocommerce' ), - 'popularity' => __( 'Sort by popularity', 'woocommerce' ), - 'rating' => __( 'Sort by average rating', 'woocommerce' ), - 'date' => __( 'Sort by latest', 'woocommerce' ), - 'price' => __( 'Sort by price: low to high', 'woocommerce' ), - 'price-desc' => __( 'Sort by price: high to low', 'woocommerce' ), - ) - ); - - $default_orderby = wc_get_loop_prop( 'is_search' ) ? 'relevance' : apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby', '' ) ); - // phpcs:disable WordPress.Security.NonceVerification.Recommended - $orderby = isset( $_GET['orderby'] ) ? wc_clean( wp_unslash( $_GET['orderby'] ) ) : $default_orderby; - // phpcs:enable WordPress.Security.NonceVerification.Recommended - - if ( wc_get_loop_prop( 'is_search' ) ) { - $catalog_orderby_options = array_merge( array( 'relevance' => __( 'Relevance', 'woocommerce' ) ), $catalog_orderby_options ); - - unset( $catalog_orderby_options['menu_order'] ); - } - - if ( ! $show_default_orderby ) { - unset( $catalog_orderby_options['menu_order'] ); - } - - if ( ! wc_review_ratings_enabled() ) { - unset( $catalog_orderby_options['rating'] ); - } - - if ( ! array_key_exists( $orderby, $catalog_orderby_options ) ) { - $orderby = current( array_keys( $catalog_orderby_options ) ); - } - - wc_get_template( - 'loop/orderby.php', - array( - 'catalog_orderby_options' => $catalog_orderby_options, - 'orderby' => $orderby, - 'show_default_orderby' => $show_default_orderby, - ) - ); - } -} - -if ( ! function_exists( 'woocommerce_pagination' ) ) { - - /** - * Output the pagination. - */ - function woocommerce_pagination() { - if ( ! wc_get_loop_prop( 'is_paginated' ) || ! woocommerce_products_will_display() ) { - return; - } - - $args = array( - 'total' => wc_get_loop_prop( 'total_pages' ), - 'current' => wc_get_loop_prop( 'current_page' ), - 'base' => esc_url_raw( add_query_arg( 'product-page', '%#%', false ) ), - 'format' => '?product-page=%#%', - ); - - if ( ! wc_get_loop_prop( 'is_shortcode' ) ) { - $args['format'] = ''; - $args['base'] = esc_url_raw( str_replace( 999999999, '%#%', remove_query_arg( 'add-to-cart', get_pagenum_link( 999999999, false ) ) ) ); - } - - wc_get_template( 'loop/pagination.php', $args ); - } -} - -/** - * Single Product - */ - -if ( ! function_exists( 'woocommerce_show_product_images' ) ) { - - /** - * Output the product image before the single product summary. - */ - function woocommerce_show_product_images() { - wc_get_template( 'single-product/product-image.php' ); - } -} -if ( ! function_exists( 'woocommerce_show_product_thumbnails' ) ) { - - /** - * Output the product thumbnails. - */ - function woocommerce_show_product_thumbnails() { - wc_get_template( 'single-product/product-thumbnails.php' ); - } -} - -/** - * Get HTML for a gallery image. - * - * Hooks: woocommerce_gallery_thumbnail_size, woocommerce_gallery_image_size and woocommerce_gallery_full_size accept name based image sizes, or an array of width/height values. - * - * @since 3.3.2 - * @param int $attachment_id Attachment ID. - * @param bool $main_image Is this the main image or a thumbnail?. - * @return string - */ -function wc_get_gallery_image_html( $attachment_id, $main_image = false ) { - $flexslider = (bool) apply_filters( 'woocommerce_single_product_flexslider_enabled', get_theme_support( 'wc-product-gallery-slider' ) ); - $gallery_thumbnail = wc_get_image_size( 'gallery_thumbnail' ); - $thumbnail_size = apply_filters( 'woocommerce_gallery_thumbnail_size', array( $gallery_thumbnail['width'], $gallery_thumbnail['height'] ) ); - $image_size = apply_filters( 'woocommerce_gallery_image_size', $flexslider || $main_image ? 'woocommerce_single' : $thumbnail_size ); - $full_size = apply_filters( 'woocommerce_gallery_full_size', apply_filters( 'woocommerce_product_thumbnails_large_size', 'full' ) ); - $thumbnail_src = wp_get_attachment_image_src( $attachment_id, $thumbnail_size ); - $full_src = wp_get_attachment_image_src( $attachment_id, $full_size ); - $alt_text = trim( wp_strip_all_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) ); - $image = wp_get_attachment_image( - $attachment_id, - $image_size, - false, - apply_filters( - 'woocommerce_gallery_image_html_attachment_image_params', - array( - 'title' => _wp_specialchars( get_post_field( 'post_title', $attachment_id ), ENT_QUOTES, 'UTF-8', true ), - 'data-caption' => _wp_specialchars( get_post_field( 'post_excerpt', $attachment_id ), ENT_QUOTES, 'UTF-8', true ), - 'data-src' => esc_url( $full_src[0] ), - 'data-large_image' => esc_url( $full_src[0] ), - 'data-large_image_width' => esc_attr( $full_src[1] ), - 'data-large_image_height' => esc_attr( $full_src[2] ), - 'class' => esc_attr( $main_image ? 'wp-post-image' : '' ), - ), - $attachment_id, - $image_size, - $main_image - ) - ); - - return ''; -} - -if ( ! function_exists( 'woocommerce_output_product_data_tabs' ) ) { - - /** - * Output the product tabs. - */ - function woocommerce_output_product_data_tabs() { - wc_get_template( 'single-product/tabs/tabs.php' ); - } -} -if ( ! function_exists( 'woocommerce_template_single_title' ) ) { - - /** - * Output the product title. - */ - function woocommerce_template_single_title() { - wc_get_template( 'single-product/title.php' ); - } -} -if ( ! function_exists( 'woocommerce_template_single_rating' ) ) { - - /** - * Output the product rating. - */ - function woocommerce_template_single_rating() { - if ( post_type_supports( 'product', 'comments' ) ) { - wc_get_template( 'single-product/rating.php' ); - } - } -} -if ( ! function_exists( 'woocommerce_template_single_price' ) ) { - - /** - * Output the product price. - */ - function woocommerce_template_single_price() { - wc_get_template( 'single-product/price.php' ); - } -} -if ( ! function_exists( 'woocommerce_template_single_excerpt' ) ) { - - /** - * Output the product short description (excerpt). - */ - function woocommerce_template_single_excerpt() { - wc_get_template( 'single-product/short-description.php' ); - } -} -if ( ! function_exists( 'woocommerce_template_single_meta' ) ) { - - /** - * Output the product meta. - */ - function woocommerce_template_single_meta() { - wc_get_template( 'single-product/meta.php' ); - } -} -if ( ! function_exists( 'woocommerce_template_single_sharing' ) ) { - - /** - * Output the product sharing. - */ - function woocommerce_template_single_sharing() { - wc_get_template( 'single-product/share.php' ); - } -} -if ( ! function_exists( 'woocommerce_show_product_sale_flash' ) ) { - - /** - * Output the product sale flash. - */ - function woocommerce_show_product_sale_flash() { - wc_get_template( 'single-product/sale-flash.php' ); - } -} - -if ( ! function_exists( 'woocommerce_template_single_add_to_cart' ) ) { - - /** - * Trigger the single product add to cart action. - */ - function woocommerce_template_single_add_to_cart() { - global $product; - do_action( 'woocommerce_' . $product->get_type() . '_add_to_cart' ); - } -} -if ( ! function_exists( 'woocommerce_simple_add_to_cart' ) ) { - - /** - * Output the simple product add to cart area. - */ - function woocommerce_simple_add_to_cart() { - wc_get_template( 'single-product/add-to-cart/simple.php' ); - } -} -if ( ! function_exists( 'woocommerce_grouped_add_to_cart' ) ) { - - /** - * Output the grouped product add to cart area. - */ - function woocommerce_grouped_add_to_cart() { - global $product; - - $products = array_filter( array_map( 'wc_get_product', $product->get_children() ), 'wc_products_array_filter_visible_grouped' ); - - if ( $products ) { - wc_get_template( - 'single-product/add-to-cart/grouped.php', - array( - 'grouped_product' => $product, - 'grouped_products' => $products, - 'quantites_required' => false, - ) - ); - } - } -} -if ( ! function_exists( 'woocommerce_variable_add_to_cart' ) ) { - - /** - * Output the variable product add to cart area. - */ - function woocommerce_variable_add_to_cart() { - global $product; - - // Enqueue variation scripts. - wp_enqueue_script( 'wc-add-to-cart-variation' ); - - // Get Available variations? - $get_variations = count( $product->get_children() ) <= apply_filters( 'woocommerce_ajax_variation_threshold', 30, $product ); - - // Load the template. - wc_get_template( - 'single-product/add-to-cart/variable.php', - array( - 'available_variations' => $get_variations ? $product->get_available_variations() : false, - 'attributes' => $product->get_variation_attributes(), - 'selected_attributes' => $product->get_default_attributes(), - ) - ); - } -} -if ( ! function_exists( 'woocommerce_external_add_to_cart' ) ) { - - /** - * Output the external product add to cart area. - */ - function woocommerce_external_add_to_cart() { - global $product; - - if ( ! $product->add_to_cart_url() ) { - return; - } - - wc_get_template( - 'single-product/add-to-cart/external.php', - array( - 'product_url' => $product->add_to_cart_url(), - 'button_text' => $product->single_add_to_cart_text(), - ) - ); - } -} - -if ( ! function_exists( 'woocommerce_quantity_input' ) ) { - - /** - * Output the quantity input for add to cart forms. - * - * @param array $args Args for the input. - * @param WC_Product|null $product Product. - * @param boolean $echo Whether to return or echo|string. - * - * @return string - */ - function woocommerce_quantity_input( $args = array(), $product = null, $echo = true ) { - if ( is_null( $product ) ) { - $product = $GLOBALS['product']; - } - - $defaults = array( - 'input_id' => uniqid( 'quantity_' ), - 'input_name' => 'quantity', - 'input_value' => '1', - 'classes' => apply_filters( 'woocommerce_quantity_input_classes', array( 'input-text', 'qty', 'text' ), $product ), - 'max_value' => apply_filters( 'woocommerce_quantity_input_max', -1, $product ), - 'min_value' => apply_filters( 'woocommerce_quantity_input_min', 0, $product ), - 'step' => apply_filters( 'woocommerce_quantity_input_step', 1, $product ), - 'pattern' => apply_filters( 'woocommerce_quantity_input_pattern', has_filter( 'woocommerce_stock_amount', 'intval' ) ? '[0-9]*' : '' ), - 'inputmode' => apply_filters( 'woocommerce_quantity_input_inputmode', has_filter( 'woocommerce_stock_amount', 'intval' ) ? 'numeric' : '' ), - 'product_name' => $product ? $product->get_title() : '', - 'placeholder' => apply_filters( 'woocommerce_quantity_input_placeholder', '', $product ), - ); - - $args = apply_filters( 'woocommerce_quantity_input_args', wp_parse_args( $args, $defaults ), $product ); - - // Apply sanity to min/max args - min cannot be lower than 0. - $args['min_value'] = max( $args['min_value'], 0 ); - $args['max_value'] = 0 < $args['max_value'] ? $args['max_value'] : ''; - - // Max cannot be lower than min if defined. - if ( '' !== $args['max_value'] && $args['max_value'] < $args['min_value'] ) { - $args['max_value'] = $args['min_value']; - } - - ob_start(); - - wc_get_template( 'global/quantity-input.php', $args ); - - if ( $echo ) { - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo ob_get_clean(); - } else { - return ob_get_clean(); - } - } -} - -if ( ! function_exists( 'woocommerce_product_description_tab' ) ) { - - /** - * Output the description tab content. - */ - function woocommerce_product_description_tab() { - wc_get_template( 'single-product/tabs/description.php' ); - } -} -if ( ! function_exists( 'woocommerce_product_additional_information_tab' ) ) { - - /** - * Output the attributes tab content. - */ - function woocommerce_product_additional_information_tab() { - wc_get_template( 'single-product/tabs/additional-information.php' ); - } -} -if ( ! function_exists( 'woocommerce_default_product_tabs' ) ) { - - /** - * Add default product tabs to product pages. - * - * @param array $tabs Array of tabs. - * @return array - */ - function woocommerce_default_product_tabs( $tabs = array() ) { - global $product, $post; - - // Description tab - shows product content. - if ( $post->post_content ) { - $tabs['description'] = array( - 'title' => __( 'Description', 'woocommerce' ), - 'priority' => 10, - 'callback' => 'woocommerce_product_description_tab', - ); - } - - // Additional information tab - shows attributes. - if ( $product && ( $product->has_attributes() || apply_filters( 'wc_product_enable_dimensions_display', $product->has_weight() || $product->has_dimensions() ) ) ) { - $tabs['additional_information'] = array( - 'title' => __( 'Additional information', 'woocommerce' ), - 'priority' => 20, - 'callback' => 'woocommerce_product_additional_information_tab', - ); - } - - // Reviews tab - shows comments. - if ( comments_open() ) { - $tabs['reviews'] = array( - /* translators: %s: reviews count */ - 'title' => sprintf( __( 'Reviews (%d)', 'woocommerce' ), $product->get_review_count() ), - 'priority' => 30, - 'callback' => 'comments_template', - ); - } - - return $tabs; - } -} - -if ( ! function_exists( 'woocommerce_sort_product_tabs' ) ) { - - /** - * Sort tabs by priority. - * - * @param array $tabs Array of tabs. - * @return array - */ - function woocommerce_sort_product_tabs( $tabs = array() ) { - - // Make sure the $tabs parameter is an array. - if ( ! is_array( $tabs ) ) { - // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error - trigger_error( 'Function woocommerce_sort_product_tabs() expects an array as the first parameter. Defaulting to empty array.' ); - $tabs = array(); - } - - // Re-order tabs by priority. - if ( ! function_exists( '_sort_priority_callback' ) ) { - /** - * Sort Priority Callback Function - * - * @param array $a Comparison A. - * @param array $b Comparison B. - * @return bool - */ - function _sort_priority_callback( $a, $b ) { - if ( ! isset( $a['priority'], $b['priority'] ) || $a['priority'] === $b['priority'] ) { - return 0; - } - return ( $a['priority'] < $b['priority'] ) ? -1 : 1; - } - } - - uasort( $tabs, '_sort_priority_callback' ); - - return $tabs; - } -} - -if ( ! function_exists( 'woocommerce_comments' ) ) { - - /** - * Output the Review comments template. - * - * @param WP_Comment $comment Comment object. - * @param array $args Arguments. - * @param int $depth Depth. - */ - function woocommerce_comments( $comment, $args, $depth ) { - // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - $GLOBALS['comment'] = $comment; - wc_get_template( - 'single-product/review.php', - array( - 'comment' => $comment, - 'args' => $args, - 'depth' => $depth, - ) - ); - } -} - -if ( ! function_exists( 'woocommerce_review_display_gravatar' ) ) { - /** - * Display the review authors gravatar - * - * @param array $comment WP_Comment. - * @return void - */ - function woocommerce_review_display_gravatar( $comment ) { - echo get_avatar( $comment, apply_filters( 'woocommerce_review_gravatar_size', '60' ), '' ); - } -} - -if ( ! function_exists( 'woocommerce_review_display_rating' ) ) { - /** - * Display the reviewers star rating - * - * @return void - */ - function woocommerce_review_display_rating() { - if ( post_type_supports( 'product', 'comments' ) ) { - wc_get_template( 'single-product/review-rating.php' ); - } - } -} - -if ( ! function_exists( 'woocommerce_review_display_meta' ) ) { - /** - * Display the review authors meta (name, verified owner, review date) - * - * @return void - */ - function woocommerce_review_display_meta() { - wc_get_template( 'single-product/review-meta.php' ); - } -} - -if ( ! function_exists( 'woocommerce_review_display_comment_text' ) ) { - - /** - * Display the review content. - */ - function woocommerce_review_display_comment_text() { - echo '
    '; - comment_text(); - echo '
    '; - } -} - -if ( ! function_exists( 'woocommerce_output_related_products' ) ) { - - /** - * Output the related products. - */ - function woocommerce_output_related_products() { - - $args = array( - 'posts_per_page' => 4, - 'columns' => 4, - 'orderby' => 'rand', // @codingStandardsIgnoreLine. - ); - - woocommerce_related_products( apply_filters( 'woocommerce_output_related_products_args', $args ) ); - } -} - -if ( ! function_exists( 'woocommerce_related_products' ) ) { - - /** - * Output the related products. - * - * @param array $args Provided arguments. - */ - function woocommerce_related_products( $args = array() ) { - global $product; - - if ( ! $product ) { - return; - } - - $defaults = array( - 'posts_per_page' => 2, - 'columns' => 2, - 'orderby' => 'rand', // @codingStandardsIgnoreLine. - 'order' => 'desc', - ); - - $args = wp_parse_args( $args, $defaults ); - - // Get visible related products then sort them at random. - $args['related_products'] = array_filter( array_map( 'wc_get_product', wc_get_related_products( $product->get_id(), $args['posts_per_page'], $product->get_upsell_ids() ) ), 'wc_products_array_filter_visible' ); - - // Handle orderby. - $args['related_products'] = wc_products_array_orderby( $args['related_products'], $args['orderby'], $args['order'] ); - - // Set global loop values. - wc_set_loop_prop( 'name', 'related' ); - wc_set_loop_prop( 'columns', apply_filters( 'woocommerce_related_products_columns', $args['columns'] ) ); - - wc_get_template( 'single-product/related.php', $args ); - } -} - -if ( ! function_exists( 'woocommerce_upsell_display' ) ) { - - /** - * Output product up sells. - * - * @param int $limit (default: -1). - * @param int $columns (default: 4). - * @param string $orderby Supported values - rand, title, ID, date, modified, menu_order, price. - * @param string $order Sort direction. - */ - function woocommerce_upsell_display( $limit = '-1', $columns = 4, $orderby = 'rand', $order = 'desc' ) { - global $product; - - if ( ! $product ) { - return; - } - - // Handle the legacy filter which controlled posts per page etc. - $args = apply_filters( - 'woocommerce_upsell_display_args', - array( - 'posts_per_page' => $limit, - 'orderby' => $orderby, - 'order' => $order, - 'columns' => $columns, - ) - ); - wc_set_loop_prop( 'name', 'up-sells' ); - wc_set_loop_prop( 'columns', apply_filters( 'woocommerce_upsells_columns', isset( $args['columns'] ) ? $args['columns'] : $columns ) ); - - $orderby = apply_filters( 'woocommerce_upsells_orderby', isset( $args['orderby'] ) ? $args['orderby'] : $orderby ); - $order = apply_filters( 'woocommerce_upsells_order', isset( $args['order'] ) ? $args['order'] : $order ); - $limit = apply_filters( 'woocommerce_upsells_total', isset( $args['posts_per_page'] ) ? $args['posts_per_page'] : $limit ); - - // Get visible upsells then sort them at random, then limit result set. - $upsells = wc_products_array_orderby( array_filter( array_map( 'wc_get_product', $product->get_upsell_ids() ), 'wc_products_array_filter_visible' ), $orderby, $order ); - $upsells = $limit > 0 ? array_slice( $upsells, 0, $limit ) : $upsells; - - wc_get_template( - 'single-product/up-sells.php', - array( - 'upsells' => $upsells, - - // Not used now, but used in previous version of up-sells.php. - 'posts_per_page' => $limit, - 'orderby' => $orderby, - 'columns' => $columns, - ) - ); - } -} - -/** Cart */ - -if ( ! function_exists( 'woocommerce_shipping_calculator' ) ) { - - /** - * Output the cart shipping calculator. - * - * @param string $button_text Text for the shipping calculation toggle. - */ - function woocommerce_shipping_calculator( $button_text = '' ) { - if ( 'no' === get_option( 'woocommerce_enable_shipping_calc' ) || ! WC()->cart->needs_shipping() ) { - return; - } - wp_enqueue_script( 'wc-country-select' ); - wc_get_template( - 'cart/shipping-calculator.php', - array( - 'button_text' => $button_text, - ) - ); - } -} - -if ( ! function_exists( 'woocommerce_cart_totals' ) ) { - - /** - * Output the cart totals. - */ - function woocommerce_cart_totals() { - if ( is_checkout() ) { - return; - } - wc_get_template( 'cart/cart-totals.php' ); - } -} - -if ( ! function_exists( 'woocommerce_cross_sell_display' ) ) { - - /** - * Output the cart cross-sells. - * - * @param int $limit (default: 2). - * @param int $columns (default: 2). - * @param string $orderby (default: 'rand'). - * @param string $order (default: 'desc'). - */ - function woocommerce_cross_sell_display( $limit = 2, $columns = 2, $orderby = 'rand', $order = 'desc' ) { - if ( is_checkout() ) { - return; - } - // Get visible cross sells then sort them at random. - $cross_sells = array_filter( array_map( 'wc_get_product', WC()->cart->get_cross_sells() ), 'wc_products_array_filter_visible' ); - - wc_set_loop_prop( 'name', 'cross-sells' ); - wc_set_loop_prop( 'columns', apply_filters( 'woocommerce_cross_sells_columns', $columns ) ); - - // Handle orderby and limit results. - $orderby = apply_filters( 'woocommerce_cross_sells_orderby', $orderby ); - $order = apply_filters( 'woocommerce_cross_sells_order', $order ); - $cross_sells = wc_products_array_orderby( $cross_sells, $orderby, $order ); - $limit = apply_filters( 'woocommerce_cross_sells_total', $limit ); - $cross_sells = $limit > 0 ? array_slice( $cross_sells, 0, $limit ) : $cross_sells; - - wc_get_template( - 'cart/cross-sells.php', - array( - 'cross_sells' => $cross_sells, - - // Not used now, but used in previous version of up-sells.php. - 'posts_per_page' => $limit, - 'orderby' => $orderby, - 'columns' => $columns, - ) - ); - } -} - -if ( ! function_exists( 'woocommerce_button_proceed_to_checkout' ) ) { - - /** - * Output the proceed to checkout button. - */ - function woocommerce_button_proceed_to_checkout() { - wc_get_template( 'cart/proceed-to-checkout-button.php' ); - } -} - -if ( ! function_exists( 'woocommerce_widget_shopping_cart_button_view_cart' ) ) { - - /** - * Output the view cart button. - */ - function woocommerce_widget_shopping_cart_button_view_cart() { - echo '' . esc_html__( 'View cart', 'woocommerce' ) . ''; - } -} - -if ( ! function_exists( 'woocommerce_widget_shopping_cart_proceed_to_checkout' ) ) { - - /** - * Output the proceed to checkout button. - */ - function woocommerce_widget_shopping_cart_proceed_to_checkout() { - echo '' . esc_html__( 'Checkout', 'woocommerce' ) . ''; - } -} - -if ( ! function_exists( 'woocommerce_widget_shopping_cart_subtotal' ) ) { - /** - * Output to view cart subtotal. - * - * @since 3.7.0 - */ - function woocommerce_widget_shopping_cart_subtotal() { - echo '' . esc_html__( 'Subtotal:', 'woocommerce' ) . ' ' . WC()->cart->get_cart_subtotal(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } -} - -/** Mini-Cart */ - -if ( ! function_exists( 'woocommerce_mini_cart' ) ) { - - /** - * Output the Mini-cart - used by cart widget. - * - * @param array $args Arguments. - */ - function woocommerce_mini_cart( $args = array() ) { - - $defaults = array( - 'list_class' => '', - ); - - $args = wp_parse_args( $args, $defaults ); - - wc_get_template( 'cart/mini-cart.php', $args ); - } -} - -/** Login */ - -if ( ! function_exists( 'woocommerce_login_form' ) ) { - - /** - * Output the WooCommerce Login Form. - * - * @param array $args Arguments. - */ - function woocommerce_login_form( $args = array() ) { - - $defaults = array( - 'message' => '', - 'redirect' => '', - 'hidden' => false, - ); - - $args = wp_parse_args( $args, $defaults ); - - wc_get_template( 'global/form-login.php', $args ); - } -} - -if ( ! function_exists( 'woocommerce_checkout_login_form' ) ) { - - /** - * Output the WooCommerce Checkout Login Form. - */ - function woocommerce_checkout_login_form() { - wc_get_template( - 'checkout/form-login.php', - array( - 'checkout' => WC()->checkout(), - ) - ); - } -} - -if ( ! function_exists( 'woocommerce_breadcrumb' ) ) { - - /** - * Output the WooCommerce Breadcrumb. - * - * @param array $args Arguments. - */ - function woocommerce_breadcrumb( $args = array() ) { - $args = wp_parse_args( - $args, - apply_filters( - 'woocommerce_breadcrumb_defaults', - array( - 'delimiter' => ' / ', - 'wrap_before' => '', - 'before' => '', - 'after' => '', - 'home' => _x( 'Home', 'breadcrumb', 'woocommerce' ), - ) - ) - ); - - $breadcrumbs = new WC_Breadcrumb(); - - if ( ! empty( $args['home'] ) ) { - $breadcrumbs->add_crumb( $args['home'], apply_filters( 'woocommerce_breadcrumb_home_url', home_url() ) ); - } - - $args['breadcrumb'] = $breadcrumbs->generate(); - - /** - * WooCommerce Breadcrumb hook - * - * @hooked WC_Structured_Data::generate_breadcrumblist_data() - 10 - */ - do_action( 'woocommerce_breadcrumb', $breadcrumbs, $args ); - - wc_get_template( 'global/breadcrumb.php', $args ); - } -} - -if ( ! function_exists( 'woocommerce_order_review' ) ) { - - /** - * Output the Order review table for the checkout. - * - * @param bool $deprecated Deprecated param. - */ - function woocommerce_order_review( $deprecated = false ) { - wc_get_template( - 'checkout/review-order.php', - array( - 'checkout' => WC()->checkout(), - ) - ); - } -} - -if ( ! function_exists( 'woocommerce_checkout_payment' ) ) { - - /** - * Output the Payment Methods on the checkout. - */ - function woocommerce_checkout_payment() { - if ( WC()->cart->needs_payment() ) { - $available_gateways = WC()->payment_gateways()->get_available_payment_gateways(); - WC()->payment_gateways()->set_current_gateway( $available_gateways ); - } else { - $available_gateways = array(); - } - - wc_get_template( - 'checkout/payment.php', - array( - 'checkout' => WC()->checkout(), - 'available_gateways' => $available_gateways, - 'order_button_text' => apply_filters( 'woocommerce_order_button_text', __( 'Place order', 'woocommerce' ) ), - ) - ); - } -} - -if ( ! function_exists( 'woocommerce_checkout_coupon_form' ) ) { - - /** - * Output the Coupon form for the checkout. - */ - function woocommerce_checkout_coupon_form() { - wc_get_template( - 'checkout/form-coupon.php', - array( - 'checkout' => WC()->checkout(), - ) - ); - } -} - -if ( ! function_exists( 'woocommerce_products_will_display' ) ) { - - /** - * Check if we will be showing products or not (and not sub-categories only). - * - * @return bool - */ - function woocommerce_products_will_display() { - $display_type = woocommerce_get_loop_display_mode(); - - return 0 < wc_get_loop_prop( 'total', 0 ) && 'subcategories' !== $display_type; - } -} - -if ( ! function_exists( 'woocommerce_get_loop_display_mode' ) ) { - - /** - * See what is going to display in the loop. - * - * @since 3.3.0 - * @return string Either products, subcategories, or both, based on current page. - */ - function woocommerce_get_loop_display_mode() { - // Only return products when filtering things. - if ( wc_get_loop_prop( 'is_search' ) || wc_get_loop_prop( 'is_filtered' ) ) { - return 'products'; - } - - $parent_id = 0; - $display_type = ''; - - if ( is_shop() ) { - $display_type = get_option( 'woocommerce_shop_page_display', '' ); - } elseif ( is_product_category() ) { - $parent_id = get_queried_object_id(); - $display_type = get_term_meta( $parent_id, 'display_type', true ); - $display_type = '' === $display_type ? get_option( 'woocommerce_category_archive_display', '' ) : $display_type; - } - - if ( ( ! is_shop() || 'subcategories' !== $display_type ) && 1 < wc_get_loop_prop( 'current_page' ) ) { - return 'products'; - } - - // Ensure valid value. - if ( '' === $display_type || ! in_array( $display_type, array( 'products', 'subcategories', 'both' ), true ) ) { - $display_type = 'products'; - } - - // If we're showing categories, ensure we actually have something to show. - if ( in_array( $display_type, array( 'subcategories', 'both' ), true ) ) { - $subcategories = woocommerce_get_product_subcategories( $parent_id ); - - if ( empty( $subcategories ) ) { - $display_type = 'products'; - } - } - - return $display_type; - } -} - -if ( ! function_exists( 'woocommerce_maybe_show_product_subcategories' ) ) { - - /** - * Maybe display categories before, or instead of, a product loop. - * - * @since 3.3.0 - * @param string $loop_html HTML. - * @return string - */ - function woocommerce_maybe_show_product_subcategories( $loop_html = '' ) { - if ( wc_get_loop_prop( 'is_shortcode' ) && ! WC_Template_Loader::in_content_filter() ) { - return $loop_html; - } - - $display_type = woocommerce_get_loop_display_mode(); - - // If displaying categories, append to the loop. - if ( 'subcategories' === $display_type || 'both' === $display_type ) { - ob_start(); - woocommerce_output_product_categories( - array( - 'parent_id' => is_product_category() ? get_queried_object_id() : 0, - ) - ); - $loop_html .= ob_get_clean(); - - if ( 'subcategories' === $display_type ) { - wc_set_loop_prop( 'total', 0 ); - - // This removes pagination and products from display for themes not using wc_get_loop_prop in their product loops. @todo Remove in future major version. - global $wp_query; - - if ( $wp_query->is_main_query() ) { - $wp_query->post_count = 0; - $wp_query->max_num_pages = 0; - } - } - } - - return $loop_html; - } -} - -if ( ! function_exists( 'woocommerce_product_subcategories' ) ) { - /** - * This is a legacy function which used to check if we needed to display subcats and then output them. It was called by templates. - * - * From 3.3 onwards this is all handled via hooks and the woocommerce_maybe_show_product_subcategories function. - * - * Since some templates have not updated compatibility, to avoid showing incorrect categories this function has been deprecated and will - * return nothing. Replace usage with woocommerce_output_product_categories to render the category list manually. - * - * This is a legacy function which also checks if things should display. - * Themes no longer need to call these functions. It's all done via hooks. - * - * @deprecated 3.3.1 @todo Add a notice in a future version. - * @param array $args Arguments. - * @return null|boolean - */ - function woocommerce_product_subcategories( $args = array() ) { - $defaults = array( - 'before' => '', - 'after' => '', - 'force_display' => false, - ); - - $args = wp_parse_args( $args, $defaults ); - - if ( $args['force_display'] ) { - // We can still render if display is forced. - woocommerce_output_product_categories( - array( - 'before' => $args['before'], - 'after' => $args['after'], - 'parent_id' => is_product_category() ? get_queried_object_id() : 0, - ) - ); - return true; - } else { - // Output nothing. woocommerce_maybe_show_product_subcategories will handle the output of cats. - $display_type = woocommerce_get_loop_display_mode(); - - if ( 'subcategories' === $display_type ) { - // This removes pagination and products from display for themes not using wc_get_loop_prop in their product loops. @todo Remove in future major version. - global $wp_query; - - if ( $wp_query->is_main_query() ) { - $wp_query->post_count = 0; - $wp_query->max_num_pages = 0; - } - } - - return 'subcategories' === $display_type || 'both' === $display_type; - } - } -} - -if ( ! function_exists( 'woocommerce_output_product_categories' ) ) { - /** - * Display product sub categories as thumbnails. - * - * This is a replacement for woocommerce_product_subcategories which also does some logic - * based on the loop. This function however just outputs when called. - * - * @since 3.3.1 - * @param array $args Arguments. - * @return boolean - */ - function woocommerce_output_product_categories( $args = array() ) { - $args = wp_parse_args( - $args, - array( - 'before' => apply_filters( 'woocommerce_before_output_product_categories', '' ), - 'after' => apply_filters( 'woocommerce_after_output_product_categories', '' ), - 'parent_id' => 0, - ) - ); - - $product_categories = woocommerce_get_product_subcategories( $args['parent_id'] ); - - if ( ! $product_categories ) { - return false; - } - - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo $args['before']; - - foreach ( $product_categories as $category ) { - wc_get_template( - 'content-product_cat.php', - array( - 'category' => $category, - ) - ); - } - - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo $args['after']; - - return true; - } -} - -if ( ! function_exists( 'woocommerce_get_product_subcategories' ) ) { - /** - * Get (and cache) product subcategories. - * - * @param int $parent_id Get subcategories of this ID. - * @return array - */ - function woocommerce_get_product_subcategories( $parent_id = 0 ) { - $parent_id = absint( $parent_id ); - $cache_key = apply_filters( 'woocommerce_get_product_subcategories_cache_key', 'product-category-hierarchy-' . $parent_id, $parent_id ); - $product_categories = $cache_key ? wp_cache_get( $cache_key, 'product_cat' ) : false; - - if ( false === $product_categories ) { - // NOTE: using child_of instead of parent - this is not ideal but due to a WP bug ( https://core.trac.wordpress.org/ticket/15626 ) pad_counts won't work. - $product_categories = get_categories( - apply_filters( - 'woocommerce_product_subcategories_args', - array( - 'parent' => $parent_id, - 'hide_empty' => 0, - 'hierarchical' => 1, - 'taxonomy' => 'product_cat', - 'pad_counts' => 1, - ) - ) - ); - - if ( $cache_key ) { - wp_cache_set( $cache_key, $product_categories, 'product_cat' ); - } - } - - if ( apply_filters( 'woocommerce_product_subcategories_hide_empty', true ) ) { - $product_categories = wp_list_filter( $product_categories, array( 'count' => 0 ), 'NOT' ); - } - - return $product_categories; - } -} - -if ( ! function_exists( 'woocommerce_subcategory_thumbnail' ) ) { - - /** - * Show subcategory thumbnails. - * - * @param mixed $category Category. - */ - function woocommerce_subcategory_thumbnail( $category ) { - $small_thumbnail_size = apply_filters( 'subcategory_archive_thumbnail_size', 'woocommerce_thumbnail' ); - $dimensions = wc_get_image_size( $small_thumbnail_size ); - $thumbnail_id = get_term_meta( $category->term_id, 'thumbnail_id', true ); - - if ( $thumbnail_id ) { - $image = wp_get_attachment_image_src( $thumbnail_id, $small_thumbnail_size ); - $image = $image[0]; - $image_srcset = function_exists( 'wp_get_attachment_image_srcset' ) ? wp_get_attachment_image_srcset( $thumbnail_id, $small_thumbnail_size ) : false; - $image_sizes = function_exists( 'wp_get_attachment_image_sizes' ) ? wp_get_attachment_image_sizes( $thumbnail_id, $small_thumbnail_size ) : false; - } else { - $image = wc_placeholder_img_src(); - $image_srcset = false; - $image_sizes = false; - } - - if ( $image ) { - // Prevent esc_url from breaking spaces in urls for image embeds. - // Ref: https://core.trac.wordpress.org/ticket/23605. - $image = str_replace( ' ', '%20', $image ); - - // Add responsive image markup if available. - if ( $image_srcset && $image_sizes ) { - echo '' . esc_attr( $category->name ) . ''; - } else { - echo '' . esc_attr( $category->name ) . ''; - } - } - } -} - -if ( ! function_exists( 'woocommerce_order_details_table' ) ) { - - /** - * Displays order details in a table. - * - * @param mixed $order_id Order ID. - */ - function woocommerce_order_details_table( $order_id ) { - if ( ! $order_id ) { - return; - } - - wc_get_template( - 'order/order-details.php', - array( - 'order_id' => $order_id, - ) - ); - } -} - -if ( ! function_exists( 'woocommerce_order_downloads_table' ) ) { - - /** - * Displays order downloads in a table. - * - * @since 3.2.0 - * @param array $downloads Downloads. - */ - function woocommerce_order_downloads_table( $downloads ) { - if ( ! $downloads ) { - return; - } - wc_get_template( - 'order/order-downloads.php', - array( - 'downloads' => $downloads, - ) - ); - } -} - -if ( ! function_exists( 'woocommerce_order_again_button' ) ) { - - /** - * Display an 'order again' button on the view order page. - * - * @param object $order Order. - */ - function woocommerce_order_again_button( $order ) { - if ( ! $order || ! $order->has_status( apply_filters( 'woocommerce_valid_order_statuses_for_order_again', array( 'completed' ) ) ) || ! is_user_logged_in() ) { - return; - } - - wc_get_template( - 'order/order-again.php', - array( - 'order' => $order, - 'order_again_url' => wp_nonce_url( add_query_arg( 'order_again', $order->get_id(), wc_get_cart_url() ), 'woocommerce-order_again' ), - ) - ); - } -} - -/** Forms */ - -if ( ! function_exists( 'woocommerce_form_field' ) ) { - - /** - * Outputs a checkout/address form field. - * - * @param string $key Key. - * @param mixed $args Arguments. - * @param string $value (default: null). - * @return string - */ - function woocommerce_form_field( $key, $args, $value = null ) { - $defaults = array( - 'type' => 'text', - 'label' => '', - 'description' => '', - 'placeholder' => '', - 'maxlength' => false, - 'required' => false, - 'autocomplete' => false, - 'id' => $key, - 'class' => array(), - 'label_class' => array(), - 'input_class' => array(), - 'return' => false, - 'options' => array(), - 'custom_attributes' => array(), - 'validate' => array(), - 'default' => '', - 'autofocus' => '', - 'priority' => '', - ); - - $args = wp_parse_args( $args, $defaults ); - $args = apply_filters( 'woocommerce_form_field_args', $args, $key, $value ); - - if ( $args['required'] ) { - $args['class'][] = 'validate-required'; - $required = ' *'; - } else { - $required = ' (' . esc_html__( 'optional', 'woocommerce' ) . ')'; - } - - if ( is_string( $args['label_class'] ) ) { - $args['label_class'] = array( $args['label_class'] ); - } - - if ( is_null( $value ) ) { - $value = $args['default']; - } - - // Custom attribute handling. - $custom_attributes = array(); - $args['custom_attributes'] = array_filter( (array) $args['custom_attributes'], 'strlen' ); - - if ( $args['maxlength'] ) { - $args['custom_attributes']['maxlength'] = absint( $args['maxlength'] ); - } - - if ( ! empty( $args['autocomplete'] ) ) { - $args['custom_attributes']['autocomplete'] = $args['autocomplete']; - } - - if ( true === $args['autofocus'] ) { - $args['custom_attributes']['autofocus'] = 'autofocus'; - } - - if ( $args['description'] ) { - $args['custom_attributes']['aria-describedby'] = $args['id'] . '-description'; - } - - if ( ! empty( $args['custom_attributes'] ) && is_array( $args['custom_attributes'] ) ) { - foreach ( $args['custom_attributes'] as $attribute => $attribute_value ) { - $custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $attribute_value ) . '"'; - } - } - - if ( ! empty( $args['validate'] ) ) { - foreach ( $args['validate'] as $validate ) { - $args['class'][] = 'validate-' . $validate; - } - } - - $field = ''; - $label_id = $args['id']; - $sort = $args['priority'] ? $args['priority'] : ''; - $field_container = '

    %3$s

    '; - - switch ( $args['type'] ) { - case 'country': - $countries = 'shipping_country' === $key ? WC()->countries->get_shipping_countries() : WC()->countries->get_allowed_countries(); - - if ( 1 === count( $countries ) ) { - - $field .= '' . current( array_values( $countries ) ) . ''; - - $field .= ''; - - } else { - - $field = ''; - - $field .= ''; - - } - - break; - case 'state': - /* Get country this state field is representing */ - $for_country = isset( $args['country'] ) ? $args['country'] : WC()->checkout->get_value( 'billing_state' === $key ? 'billing_country' : 'shipping_country' ); - $states = WC()->countries->get_states( $for_country ); - - if ( is_array( $states ) && empty( $states ) ) { - - $field_container = ''; - - $field .= ''; - - } elseif ( ! is_null( $for_country ) && is_array( $states ) ) { - - $field .= ''; - - } else { - - $field .= ''; - - } - - break; - case 'textarea': - $field .= ''; - - break; - case 'checkbox': - $field = ''; - - break; - case 'text': - case 'password': - case 'datetime': - case 'datetime-local': - case 'date': - case 'month': - case 'time': - case 'week': - case 'number': - case 'email': - case 'url': - case 'tel': - $field .= ''; - - break; - case 'hidden': - $field .= ''; - - break; - case 'select': - $field = ''; - $options = ''; - - if ( ! empty( $args['options'] ) ) { - foreach ( $args['options'] as $option_key => $option_text ) { - if ( '' === $option_key ) { - // If we have a blank option, select2 needs a placeholder. - if ( empty( $args['placeholder'] ) ) { - $args['placeholder'] = $option_text ? $option_text : __( 'Choose an option', 'woocommerce' ); - } - $custom_attributes[] = 'data-allow_clear="true"'; - } - $options .= ''; - } - - $field .= ''; - } - - break; - case 'radio': - $label_id .= '_' . current( array_keys( $args['options'] ) ); - - if ( ! empty( $args['options'] ) ) { - foreach ( $args['options'] as $option_key => $option_text ) { - $field .= ''; - $field .= ''; - } - } - - break; - } - - if ( ! empty( $field ) ) { - $field_html = ''; - - if ( $args['label'] && 'checkbox' !== $args['type'] ) { - $field_html .= ''; - } - - $field_html .= '' . $field; - - if ( $args['description'] ) { - $field_html .= ''; - } - - $field_html .= ''; - - $container_class = esc_attr( implode( ' ', $args['class'] ) ); - $container_id = esc_attr( $args['id'] ) . '_field'; - $field = sprintf( $field_container, $container_class, $container_id, $field_html ); - } - - /** - * Filter by type. - */ - $field = apply_filters( 'woocommerce_form_field_' . $args['type'], $field, $key, $args, $value ); - - /** - * General filter on form fields. - * - * @since 3.4.0 - */ - $field = apply_filters( 'woocommerce_form_field', $field, $key, $args, $value ); - - if ( $args['return'] ) { - return $field; - } else { - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo $field; - } - } -} - -if ( ! function_exists( 'get_product_search_form' ) ) { - - /** - * Display product search form. - * - * Will first attempt to locate the product-searchform.php file in either the child or. - * the parent, then load it. If it doesn't exist, then the default search form. - * will be displayed. - * - * The default searchform uses html5. - * - * @param bool $echo (default: true). - * @return string - */ - function get_product_search_form( $echo = true ) { - global $product_search_form_index; - - ob_start(); - - if ( empty( $product_search_form_index ) ) { - $product_search_form_index = 0; - } - - do_action( 'pre_get_product_search_form' ); - - wc_get_template( - 'product-searchform.php', - array( - 'index' => $product_search_form_index++, - ) - ); - - $form = apply_filters( 'get_product_search_form', ob_get_clean() ); - - if ( ! $echo ) { - return $form; - } - - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo $form; - } -} - -if ( ! function_exists( 'woocommerce_output_auth_header' ) ) { - - /** - * Output the Auth header. - */ - function woocommerce_output_auth_header() { - wc_get_template( 'auth/header.php' ); - } -} - -if ( ! function_exists( 'woocommerce_output_auth_footer' ) ) { - - /** - * Output the Auth footer. - */ - function woocommerce_output_auth_footer() { - wc_get_template( 'auth/footer.php' ); - } -} - -if ( ! function_exists( 'woocommerce_single_variation' ) ) { - - /** - * Output placeholders for the single variation. - */ - function woocommerce_single_variation() { - echo '
    '; - } -} - -if ( ! function_exists( 'woocommerce_single_variation_add_to_cart_button' ) ) { - - /** - * Output the add to cart button for variations. - */ - function woocommerce_single_variation_add_to_cart_button() { - wc_get_template( 'single-product/add-to-cart/variation-add-to-cart-button.php' ); - } -} - -if ( ! function_exists( 'wc_dropdown_variation_attribute_options' ) ) { - - /** - * Output a list of variation attributes for use in the cart forms. - * - * @param array $args Arguments. - * @since 2.4.0 - */ - function wc_dropdown_variation_attribute_options( $args = array() ) { - $args = wp_parse_args( - apply_filters( 'woocommerce_dropdown_variation_attribute_options_args', $args ), - array( - 'options' => false, - 'attribute' => false, - 'product' => false, - 'selected' => false, - 'name' => '', - 'id' => '', - 'class' => '', - 'show_option_none' => __( 'Choose an option', 'woocommerce' ), - ) - ); - - // Get selected value. - if ( false === $args['selected'] && $args['attribute'] && $args['product'] instanceof WC_Product ) { - $selected_key = 'attribute_' . sanitize_title( $args['attribute'] ); - // phpcs:disable WordPress.Security.NonceVerification.Recommended - $args['selected'] = isset( $_REQUEST[ $selected_key ] ) ? wc_clean( wp_unslash( $_REQUEST[ $selected_key ] ) ) : $args['product']->get_variation_default_attribute( $args['attribute'] ); - // phpcs:enable WordPress.Security.NonceVerification.Recommended - } - - $options = $args['options']; - $product = $args['product']; - $attribute = $args['attribute']; - $name = $args['name'] ? $args['name'] : 'attribute_' . sanitize_title( $attribute ); - $id = $args['id'] ? $args['id'] : sanitize_title( $attribute ); - $class = $args['class']; - $show_option_none = (bool) $args['show_option_none']; - $show_option_none_text = $args['show_option_none'] ? $args['show_option_none'] : __( 'Choose an option', 'woocommerce' ); // We'll do our best to hide the placeholder, but we'll need to show something when resetting options. - - if ( empty( $options ) && ! empty( $product ) && ! empty( $attribute ) ) { - $attributes = $product->get_variation_attributes(); - $options = $attributes[ $attribute ]; - } - - $html = ''; - - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo apply_filters( 'woocommerce_dropdown_variation_attribute_options_html', $html, $args ); - } -} - -if ( ! function_exists( 'woocommerce_account_content' ) ) { - - /** - * My Account content output. - */ - function woocommerce_account_content() { - global $wp; - - if ( ! empty( $wp->query_vars ) ) { - foreach ( $wp->query_vars as $key => $value ) { - // Ignore pagename param. - if ( 'pagename' === $key ) { - continue; - } - - if ( has_action( 'woocommerce_account_' . $key . '_endpoint' ) ) { - do_action( 'woocommerce_account_' . $key . '_endpoint', $value ); - return; - } - } - } - - // No endpoint found? Default to dashboard. - wc_get_template( - 'myaccount/dashboard.php', - array( - 'current_user' => get_user_by( 'id', get_current_user_id() ), - ) - ); - } -} - -if ( ! function_exists( 'woocommerce_account_navigation' ) ) { - - /** - * My Account navigation template. - */ - function woocommerce_account_navigation() { - wc_get_template( 'myaccount/navigation.php' ); - } -} - -if ( ! function_exists( 'woocommerce_account_orders' ) ) { - - /** - * My Account > Orders template. - * - * @param int $current_page Current page number. - */ - function woocommerce_account_orders( $current_page ) { - $current_page = empty( $current_page ) ? 1 : absint( $current_page ); - $customer_orders = wc_get_orders( - apply_filters( - 'woocommerce_my_account_my_orders_query', - array( - 'customer' => get_current_user_id(), - 'page' => $current_page, - 'paginate' => true, - ) - ) - ); - - wc_get_template( - 'myaccount/orders.php', - array( - 'current_page' => absint( $current_page ), - 'customer_orders' => $customer_orders, - 'has_orders' => 0 < $customer_orders->total, - ) - ); - } -} - -if ( ! function_exists( 'woocommerce_account_view_order' ) ) { - - /** - * My Account > View order template. - * - * @param int $order_id Order ID. - */ - function woocommerce_account_view_order( $order_id ) { - WC_Shortcode_My_Account::view_order( absint( $order_id ) ); - } -} - -if ( ! function_exists( 'woocommerce_account_downloads' ) ) { - - /** - * My Account > Downloads template. - */ - function woocommerce_account_downloads() { - wc_get_template( 'myaccount/downloads.php' ); - } -} - -if ( ! function_exists( 'woocommerce_account_edit_address' ) ) { - - /** - * My Account > Edit address template. - * - * @param string $type Address type. - */ - function woocommerce_account_edit_address( $type ) { - $type = wc_edit_address_i18n( sanitize_title( $type ), true ); - - WC_Shortcode_My_Account::edit_address( $type ); - } -} - -if ( ! function_exists( 'woocommerce_account_payment_methods' ) ) { - - /** - * My Account > Downloads template. - */ - function woocommerce_account_payment_methods() { - wc_get_template( 'myaccount/payment-methods.php' ); - } -} - -if ( ! function_exists( 'woocommerce_account_add_payment_method' ) ) { - - /** - * My Account > Add payment method template. - */ - function woocommerce_account_add_payment_method() { - WC_Shortcode_My_Account::add_payment_method(); - } -} - -if ( ! function_exists( 'woocommerce_account_edit_account' ) ) { - - /** - * My Account > Edit account template. - */ - function woocommerce_account_edit_account() { - WC_Shortcode_My_Account::edit_account(); - } -} - -if ( ! function_exists( 'wc_no_products_found' ) ) { - - /** - * Handles the loop when no products were found/no product exist. - */ - function wc_no_products_found() { - wc_get_template( 'loop/no-products-found.php' ); - } -} - - -if ( ! function_exists( 'wc_get_email_order_items' ) ) { - /** - * Get HTML for the order items to be shown in emails. - * - * @param WC_Order $order Order object. - * @param array $args Arguments. - * - * @since 3.0.0 - * @return string - */ - function wc_get_email_order_items( $order, $args = array() ) { - ob_start(); - - $defaults = array( - 'show_sku' => false, - 'show_image' => false, - 'image_size' => array( 32, 32 ), - 'plain_text' => false, - 'sent_to_admin' => false, - ); - - $args = wp_parse_args( $args, $defaults ); - $template = $args['plain_text'] ? 'emails/plain/email-order-items.php' : 'emails/email-order-items.php'; - - wc_get_template( - $template, - apply_filters( - 'woocommerce_email_order_items_args', - array( - 'order' => $order, - 'items' => $order->get_items(), - 'show_download_links' => $order->is_download_permitted() && ! $args['sent_to_admin'], - 'show_sku' => $args['show_sku'], - 'show_purchase_note' => $order->is_paid() && ! $args['sent_to_admin'], - 'show_image' => $args['show_image'], - 'image_size' => $args['image_size'], - 'plain_text' => $args['plain_text'], - 'sent_to_admin' => $args['sent_to_admin'], - ) - ) - ); - - return apply_filters( 'woocommerce_email_order_items_table', ob_get_clean(), $order ); - } -} - -if ( ! function_exists( 'wc_display_item_meta' ) ) { - /** - * Display item meta data. - * - * @since 3.0.0 - * @param WC_Order_Item $item Order Item. - * @param array $args Arguments. - * @return string|void - */ - function wc_display_item_meta( $item, $args = array() ) { - $strings = array(); - $html = ''; - $args = wp_parse_args( - $args, - array( - 'before' => '
    • ', - 'after' => '
    ', - 'separator' => '
  • ', - 'echo' => true, - 'autop' => false, - 'label_before' => '', - 'label_after' => ': ', - ) - ); - - foreach ( $item->get_formatted_meta_data() as $meta_id => $meta ) { - $value = $args['autop'] ? wp_kses_post( $meta->display_value ) : wp_kses_post( make_clickable( trim( $meta->display_value ) ) ); - $strings[] = $args['label_before'] . wp_kses_post( $meta->display_key ) . $args['label_after'] . $value; - } - - if ( $strings ) { - $html = $args['before'] . implode( $args['separator'], $strings ) . $args['after']; - } - - $html = apply_filters( 'woocommerce_display_item_meta', $html, $item, $args ); - - if ( $args['echo'] ) { - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo $html; - } else { - return $html; - } - } -} - -if ( ! function_exists( 'wc_display_item_downloads' ) ) { - /** - * Display item download links. - * - * @since 3.0.0 - * @param WC_Order_Item $item Order Item. - * @param array $args Arguments. - * @return string|void - */ - function wc_display_item_downloads( $item, $args = array() ) { - $strings = array(); - $html = ''; - $args = wp_parse_args( - $args, - array( - 'before' => '
    • ', - 'after' => '
    ', - 'separator' => '
  • ', - 'echo' => true, - 'show_url' => false, - ) - ); - - $downloads = is_object( $item ) && $item->is_type( 'line_item' ) ? $item->get_item_downloads() : array(); - - if ( $downloads ) { - $i = 0; - foreach ( $downloads as $file ) { - $i ++; - - if ( $args['show_url'] ) { - $strings[] = '' . esc_html( $file['name'] ) . ': ' . esc_html( $file['download_url'] ); - } else { - /* translators: %d: downloads count */ - $prefix = count( $downloads ) > 1 ? sprintf( __( 'Download %d', 'woocommerce' ), $i ) : __( 'Download', 'woocommerce' ); - $strings[] = '' . $prefix . ': ' . esc_html( $file['name'] ) . ''; - } - } - } - - if ( $strings ) { - $html = $args['before'] . implode( $args['separator'], $strings ) . $args['after']; - } - - $html = apply_filters( 'woocommerce_display_item_downloads', $html, $item, $args ); - - if ( $args['echo'] ) { - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo $html; - } else { - return $html; - } - } -} - -if ( ! function_exists( 'woocommerce_photoswipe' ) ) { - - /** - * Get the shop sidebar template. - */ - function woocommerce_photoswipe() { - if ( current_theme_supports( 'wc-product-gallery-lightbox' ) ) { - wc_get_template( 'single-product/photoswipe.php' ); - } - } -} - -/** - * Outputs a list of product attributes for a product. - * - * @since 3.0.0 - * @param WC_Product $product Product Object. - */ -function wc_display_product_attributes( $product ) { - $product_attributes = array(); - - // Display weight and dimensions before attribute list. - $display_dimensions = apply_filters( 'wc_product_enable_dimensions_display', $product->has_weight() || $product->has_dimensions() ); - - if ( $display_dimensions && $product->has_weight() ) { - $product_attributes['weight'] = array( - 'label' => __( 'Weight', 'woocommerce' ), - 'value' => wc_format_weight( $product->get_weight() ), - ); - } - - if ( $display_dimensions && $product->has_dimensions() ) { - $product_attributes['dimensions'] = array( - 'label' => __( 'Dimensions', 'woocommerce' ), - 'value' => wc_format_dimensions( $product->get_dimensions( false ) ), - ); - } - - // Add product attributes to list. - $attributes = array_filter( $product->get_attributes(), 'wc_attributes_array_filter_visible' ); - - foreach ( $attributes as $attribute ) { - $values = array(); - - if ( $attribute->is_taxonomy() ) { - $attribute_taxonomy = $attribute->get_taxonomy_object(); - $attribute_values = wc_get_product_terms( $product->get_id(), $attribute->get_name(), array( 'fields' => 'all' ) ); - - foreach ( $attribute_values as $attribute_value ) { - $value_name = esc_html( $attribute_value->name ); - - if ( $attribute_taxonomy->attribute_public ) { - $values[] = ''; - } else { - $values[] = $value_name; - } - } - } else { - $values = $attribute->get_options(); - - foreach ( $values as &$value ) { - $value = make_clickable( esc_html( $value ) ); - } - } - - $product_attributes[ 'attribute_' . sanitize_title_with_dashes( $attribute->get_name() ) ] = array( - 'label' => wc_attribute_label( $attribute->get_name() ), - 'value' => apply_filters( 'woocommerce_attribute', wpautop( wptexturize( implode( ', ', $values ) ) ), $attribute, $values ), - ); - } - - /** - * Hook: woocommerce_display_product_attributes. - * - * @since 3.6.0. - * @param array $product_attributes Array of atributes to display; label, value. - * @param WC_Product $product Showing attributes for this product. - */ - $product_attributes = apply_filters( 'woocommerce_display_product_attributes', $product_attributes, $product ); - - wc_get_template( - 'single-product/product-attributes.php', - array( - 'product_attributes' => $product_attributes, - // Legacy params. - 'product' => $product, - 'attributes' => $attributes, - 'display_dimensions' => $display_dimensions, - ) - ); -} - -/** - * Get HTML to show product stock. - * - * @since 3.0.0 - * @param WC_Product $product Product Object. - * @return string - */ -function wc_get_stock_html( $product ) { - $html = ''; - $availability = $product->get_availability(); - - if ( ! empty( $availability['availability'] ) ) { - ob_start(); - - wc_get_template( - 'single-product/stock.php', - array( - 'product' => $product, - 'class' => $availability['class'], - 'availability' => $availability['availability'], - ) - ); - - $html = ob_get_clean(); - } - - if ( has_filter( 'woocommerce_stock_html' ) ) { - wc_deprecated_function( 'The woocommerce_stock_html filter', '', 'woocommerce_get_stock_html' ); - $html = apply_filters( 'woocommerce_stock_html', $html, $availability['availability'], $product ); - } - - return apply_filters( 'woocommerce_get_stock_html', $html, $product ); -} - -/** - * Get HTML for ratings. - * - * @since 3.0.0 - * @param float $rating Rating being shown. - * @param int $count Total number of ratings. - * @return string - */ -function wc_get_rating_html( $rating, $count = 0 ) { - $html = ''; - - if ( 0 < $rating ) { - /* translators: %s: rating */ - $label = sprintf( __( 'Rated %s out of 5', 'woocommerce' ), $rating ); - $html = ''; - } - - return apply_filters( 'woocommerce_product_get_rating_html', $html, $rating, $count ); -} - -/** - * Get HTML for star rating. - * - * @since 3.1.0 - * @param float $rating Rating being shown. - * @param int $count Total number of ratings. - * @return string - */ -function wc_get_star_rating_html( $rating, $count = 0 ) { - $html = ''; - - if ( 0 < $count ) { - /* translators: 1: rating 2: rating count */ - $html .= sprintf( _n( 'Rated %1$s out of 5 based on %2$s customer rating', 'Rated %1$s out of 5 based on %2$s customer ratings', $count, 'woocommerce' ), '' . esc_html( $rating ) . '', '' . esc_html( $count ) . '' ); - } else { - /* translators: %s: rating */ - $html .= sprintf( esc_html__( 'Rated %s out of 5', 'woocommerce' ), '' . esc_html( $rating ) . '' ); - } - - $html .= ''; - - return apply_filters( 'woocommerce_get_star_rating_html', $html, $rating, $count ); -} - -/** - * Returns a 'from' prefix if you want to show where prices start at. - * - * @since 3.0.0 - * @return string - */ -function wc_get_price_html_from_text() { - return apply_filters( 'woocommerce_get_price_html_from_text', '' . _x( 'From:', 'min_price', 'woocommerce' ) . ' ' ); -} - -/** - * Get logout endpoint. - * - * @since 2.6.9 - * - * @param string $redirect Redirect URL. - * - * @return string - */ -function wc_logout_url( $redirect = '' ) { - $redirect = $redirect ? $redirect : apply_filters( 'woocommerce_logout_default_redirect_url', wc_get_page_permalink( 'myaccount' ) ); - - if ( get_option( 'woocommerce_logout_endpoint' ) ) { - return wp_nonce_url( wc_get_endpoint_url( 'customer-logout', '', $redirect ), 'customer-logout' ); - } - - return wp_logout_url( $redirect ); -} - -/** - * Show notice if cart is empty. - * - * @since 3.1.0 - */ -function wc_empty_cart_message() { - echo '

    ' . wp_kses_post( apply_filters( 'wc_empty_cart_message', __( 'Your cart is currently empty.', 'woocommerce' ) ) ) . '

    '; -} - -/** - * Disable search engines indexing core, dynamic, cart/checkout pages. - * - * @todo Deprecated this function after dropping support for WP 5.6. - * @since 3.2.0 - */ -function wc_page_noindex() { - // wp_no_robots is deprecated since WP 5.7. - if ( function_exists( 'wp_robots_no_robots' ) ) { - return; - } - - if ( is_page( wc_get_page_id( 'cart' ) ) || is_page( wc_get_page_id( 'checkout' ) ) || is_page( wc_get_page_id( 'myaccount' ) ) ) { - wp_no_robots(); - } -} -add_action( 'wp_head', 'wc_page_noindex' ); - -/** - * Disable search engines indexing core, dynamic, cart/checkout pages. - * Uses "wp_robots" filter introduced in WP 5.7. - * - * @since 5.0.0 - * @param array $robots Associative array of robots directives. - * @return array Filtered robots directives. - */ -function wc_page_no_robots( $robots ) { - if ( is_page( wc_get_page_id( 'cart' ) ) || is_page( wc_get_page_id( 'checkout' ) ) || is_page( wc_get_page_id( 'myaccount' ) ) ) { - return wp_robots_no_robots( $robots ); - } - - return $robots; -} -add_filter( 'wp_robots', 'wc_page_no_robots' ); - -/** - * Get a slug identifying the current theme. - * - * @since 3.3.0 - * @return string - */ -function wc_get_theme_slug_for_templates() { - return apply_filters( 'woocommerce_theme_slug_for_templates', get_option( 'template' ) ); -} - -/** - * Gets and formats a list of cart item data + variations for display on the frontend. - * - * @since 3.3.0 - * @param array $cart_item Cart item object. - * @param bool $flat Should the data be returned flat or in a list. - * @return string - */ -function wc_get_formatted_cart_item_data( $cart_item, $flat = false ) { - $item_data = array(); - - // Variation values are shown only if they are not found in the title as of 3.0. - // This is because variation titles display the attributes. - if ( $cart_item['data']->is_type( 'variation' ) && is_array( $cart_item['variation'] ) ) { - foreach ( $cart_item['variation'] as $name => $value ) { - $taxonomy = wc_attribute_taxonomy_name( str_replace( 'attribute_pa_', '', urldecode( $name ) ) ); - - if ( taxonomy_exists( $taxonomy ) ) { - // If this is a term slug, get the term's nice name. - $term = get_term_by( 'slug', $value, $taxonomy ); - if ( ! is_wp_error( $term ) && $term && $term->name ) { - $value = $term->name; - } - $label = wc_attribute_label( $taxonomy ); - } else { - // If this is a custom option slug, get the options name. - $value = apply_filters( 'woocommerce_variation_option_name', $value, null, $taxonomy, $cart_item['data'] ); - $label = wc_attribute_label( str_replace( 'attribute_', '', $name ), $cart_item['data'] ); - } - - // Check the nicename against the title. - if ( '' === $value || wc_is_attribute_in_product_name( $value, $cart_item['data']->get_name() ) ) { - continue; - } - - $item_data[] = array( - 'key' => $label, - 'value' => $value, - ); - } - } - - // Filter item data to allow 3rd parties to add more to the array. - $item_data = apply_filters( 'woocommerce_get_item_data', $item_data, $cart_item ); - - // Format item data ready to display. - foreach ( $item_data as $key => $data ) { - // Set hidden to true to not display meta on cart. - if ( ! empty( $data['hidden'] ) ) { - unset( $item_data[ $key ] ); - continue; - } - $item_data[ $key ]['key'] = ! empty( $data['key'] ) ? $data['key'] : $data['name']; - $item_data[ $key ]['display'] = ! empty( $data['display'] ) ? $data['display'] : $data['value']; - } - - // Output flat or in list format. - if ( count( $item_data ) > 0 ) { - ob_start(); - - if ( $flat ) { - foreach ( $item_data as $data ) { - echo esc_html( $data['key'] ) . ': ' . wp_kses_post( $data['display'] ) . "\n"; - } - } else { - wc_get_template( 'cart/cart-item-data.php', array( 'item_data' => $item_data ) ); - } - - return ob_get_clean(); - } - - return ''; -} - -/** - * Gets the url to remove an item from the cart. - * - * @since 3.3.0 - * @param string $cart_item_key contains the id of the cart item. - * @return string url to page - */ -function wc_get_cart_remove_url( $cart_item_key ) { - $cart_page_url = wc_get_cart_url(); - return apply_filters( 'woocommerce_get_remove_url', $cart_page_url ? wp_nonce_url( add_query_arg( 'remove_item', $cart_item_key, $cart_page_url ), 'woocommerce-cart' ) : '' ); -} - -/** - * Gets the url to re-add an item into the cart. - * - * @since 3.3.0 - * @param string $cart_item_key Cart item key to undo. - * @return string url to page - */ -function wc_get_cart_undo_url( $cart_item_key ) { - $cart_page_url = wc_get_cart_url(); - - $query_args = array( - 'undo_item' => $cart_item_key, - ); - - return apply_filters( 'woocommerce_get_undo_url', $cart_page_url ? wp_nonce_url( add_query_arg( $query_args, $cart_page_url ), 'woocommerce-cart' ) : '', $cart_item_key ); -} - -/** - * Outputs all queued notices on WC pages. - * - * @since 3.5.0 - */ -function woocommerce_output_all_notices() { - echo '
    '; - wc_print_notices(); - echo '
    '; -} - -/** - * Products RSS Feed. - * - * @deprecated 2.6 - */ -function wc_products_rss_feed() { - wc_deprecated_function( 'wc_products_rss_feed', '2.6' ); -} - -if ( ! function_exists( 'woocommerce_reset_loop' ) ) { - - /** - * Reset the loop's index and columns when we're done outputting a product loop. - * - * @deprecated 3.3 - */ - function woocommerce_reset_loop() { - wc_reset_loop(); - } -} - -if ( ! function_exists( 'woocommerce_product_reviews_tab' ) ) { - /** - * Output the reviews tab content. - * - * @deprecated 2.4.0 Unused. - */ - function woocommerce_product_reviews_tab() { - wc_deprecated_function( 'woocommerce_product_reviews_tab', '2.4' ); - } -} - -/** - * Display pay buttons HTML. - * - * @since 3.9.0 - */ -function wc_get_pay_buttons() { - $supported_gateways = array(); - $available_gateways = WC()->payment_gateways()->get_available_payment_gateways(); - - foreach ( $available_gateways as $gateway ) { - if ( $gateway->supports( 'pay_button' ) ) { - $supported_gateways[] = $gateway->get_pay_button_id(); - } - } - - if ( ! $supported_gateways ) { - return; - } - - echo '
    '; - foreach ( $supported_gateways as $pay_button_id ) { - echo sprintf( '
    ', esc_attr( $pay_button_id ) ); - } - echo '
    '; -} - -// phpcs:enable Generic.Commenting.Todo.TaskFound diff --git a/includes/wc-term-functions.php b/includes/wc-term-functions.php deleted file mode 100644 index f40a89fb6b8..00000000000 --- a/includes/wc-term-functions.php +++ /dev/null @@ -1,638 +0,0 @@ -query_vars; - - // Put back valid orderby values. - if ( 'menu_order' === $args['orderby'] ) { - $args['orderby'] = 'name'; - $args['force_menu_order_sort'] = true; - } - - if ( 'name_num' === $args['orderby'] ) { - $args['orderby'] = 'name'; - $args['force_numeric_name'] = true; - } - - // When COUNTING, disable custom sorting. - if ( 'count' === $args['fields'] ) { - return; - } - - // Support menu_order arg used in previous versions. - if ( ! empty( $args['menu_order'] ) ) { - $args['order'] = 'DESC' === strtoupper( $args['menu_order'] ) ? 'DESC' : 'ASC'; - $args['force_menu_order_sort'] = true; - } - - if ( ! empty( $args['force_menu_order_sort'] ) ) { - $args['orderby'] = 'meta_value_num'; - $args['meta_key'] = 'order'; // phpcs:ignore - $terms_query->meta_query->parse_query_vars( $args ); - } -} -add_action( 'pre_get_terms', 'wc_change_pre_get_terms', 10, 1 ); - -/** - * Adjust term query to handle custom sorting parameters. - * - * @param array $clauses Clauses. - * @param array $taxonomies Taxonomies. - * @param array $args Arguments. - * @return array - */ -function wc_terms_clauses( $clauses, $taxonomies, $args ) { - global $wpdb; - - // No need to filter when counting. - if ( strpos( $clauses['fields'], 'COUNT(*)' ) !== false ) { - return $clauses; - } - - // Force numeric sort if using name_num custom sorting param. - if ( ! empty( $args['force_numeric_name'] ) ) { - $clauses['orderby'] = str_replace( 'ORDER BY t.name', 'ORDER BY t.name+0', $clauses['orderby'] ); - } - - // For sorting, force left join in case order meta is missing. - if ( ! empty( $args['force_menu_order_sort'] ) ) { - $clauses['join'] = str_replace( "INNER JOIN {$wpdb->termmeta} ON ( t.term_id = {$wpdb->termmeta}.term_id )", "LEFT JOIN {$wpdb->termmeta} ON ( t.term_id = {$wpdb->termmeta}.term_id AND {$wpdb->termmeta}.meta_key='order')", $clauses['join'] ); - $clauses['where'] = str_replace( "{$wpdb->termmeta}.meta_key = 'order'", "( {$wpdb->termmeta}.meta_key = 'order' OR {$wpdb->termmeta}.meta_key IS NULL )", $clauses['where'] ); - $clauses['orderby'] = 'DESC' === $args['order'] ? str_replace( 'meta_value+0', 'meta_value+0 DESC, t.name', $clauses['orderby'] ) : str_replace( 'meta_value+0', 'meta_value+0 ASC, t.name', $clauses['orderby'] ); - } - - return $clauses; -} -add_filter( 'terms_clauses', 'wc_terms_clauses', 99, 3 ); - -/** - * Helper to get cached object terms and filter by field using wp_list_pluck(). - * Works as a cached alternative for wp_get_post_terms() and wp_get_object_terms(). - * - * @since 3.0.0 - * @param int $object_id Object ID. - * @param string $taxonomy Taxonomy slug. - * @param string $field Field name. - * @param string $index_key Index key name. - * @return array - */ -function wc_get_object_terms( $object_id, $taxonomy, $field = null, $index_key = null ) { - // Test if terms exists. get_the_terms() return false when it finds no terms. - $terms = get_the_terms( $object_id, $taxonomy ); - - if ( ! $terms || is_wp_error( $terms ) ) { - return array(); - } - - return is_null( $field ) ? $terms : wp_list_pluck( $terms, $field, $index_key ); -} - -/** - * Cached version of wp_get_post_terms(). - * This is a private function (internal use ONLY). - * - * @since 3.0.0 - * @param int $product_id Product ID. - * @param string $taxonomy Taxonomy slug. - * @param array $args Query arguments. - * @return array - */ -function _wc_get_cached_product_terms( $product_id, $taxonomy, $args = array() ) { - $cache_key = 'wc_' . $taxonomy . md5( wp_json_encode( $args ) ); - $cache_group = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . $product_id; - $terms = wp_cache_get( $cache_key, $cache_group ); - - if ( false !== $terms ) { - return $terms; - } - - $terms = wp_get_post_terms( $product_id, $taxonomy, $args ); - - wp_cache_add( $cache_key, $terms, $cache_group ); - - return $terms; -} - -/** - * Wrapper used to get terms for a product. - * - * @param int $product_id Product ID. - * @param string $taxonomy Taxonomy slug. - * @param array $args Query arguments. - * @return array - */ -function wc_get_product_terms( $product_id, $taxonomy, $args = array() ) { - if ( ! taxonomy_exists( $taxonomy ) ) { - return array(); - } - - return apply_filters( 'woocommerce_get_product_terms', _wc_get_cached_product_terms( $product_id, $taxonomy, $args ), $product_id, $taxonomy, $args ); -} - -/** - * Sort by name (numeric). - * - * @param WP_Post $a First item to compare. - * @param WP_Post $b Second item to compare. - * @return int - */ -function _wc_get_product_terms_name_num_usort_callback( $a, $b ) { - $a_name = (float) $a->name; - $b_name = (float) $b->name; - - if ( abs( $a_name - $b_name ) < 0.001 ) { - return 0; - } - - return ( $a_name < $b_name ) ? -1 : 1; -} - -/** - * Sort by parent. - * - * @param WP_Post $a First item to compare. - * @param WP_Post $b Second item to compare. - * @return int - */ -function _wc_get_product_terms_parent_usort_callback( $a, $b ) { - if ( $a->parent === $b->parent ) { - return 0; - } - return ( $a->parent < $b->parent ) ? 1 : -1; -} - -/** - * WooCommerce Dropdown categories. - * - * @param array $args Args to control display of dropdown. - */ -function wc_product_dropdown_categories( $args = array() ) { - global $wp_query; - - $args = wp_parse_args( - $args, - array( - 'pad_counts' => 1, - 'show_count' => 1, - 'hierarchical' => 1, - 'hide_empty' => 1, - 'show_uncategorized' => 1, - 'orderby' => 'name', - 'selected' => isset( $wp_query->query_vars['product_cat'] ) ? $wp_query->query_vars['product_cat'] : '', - 'show_option_none' => __( 'Select a category', 'woocommerce' ), - 'option_none_value' => '', - 'value_field' => 'slug', - 'taxonomy' => 'product_cat', - 'name' => 'product_cat', - 'class' => 'dropdown_product_cat', - ) - ); - - if ( 'order' === $args['orderby'] ) { - $args['orderby'] = 'meta_value_num'; - $args['meta_key'] = 'order'; // phpcs:ignore - } - - wp_dropdown_categories( $args ); -} - -/** - * Custom walker for Product Categories. - * - * Previously used by wc_product_dropdown_categories, but wp_dropdown_categories has been fixed in core. - * - * @param mixed ...$args Variable number of parameters to be passed to the walker. - * @return mixed - */ -function wc_walk_category_dropdown_tree( ...$args ) { - if ( ! class_exists( 'WC_Product_Cat_Dropdown_Walker', false ) ) { - include_once WC()->plugin_path() . '/includes/walkers/class-wc-product-cat-dropdown-walker.php'; - } - - // The user's options are the third parameter. - if ( empty( $args[2]['walker'] ) || ! is_a( $args[2]['walker'], 'Walker' ) ) { - $walker = new WC_Product_Cat_Dropdown_Walker(); - } else { - $walker = $args[2]['walker']; - } - - return $walker->walk( ...$args ); -} - -/** - * Migrate data from WC term meta to WP term meta. - * - * When the database is updated to support term meta, migrate WC term meta data across. - * We do this when the new version is >= 34370, and the old version is < 34370 (34370 is when term meta table was added). - * - * @param string $wp_db_version The new $wp_db_version. - * @param string $wp_current_db_version The old (current) $wp_db_version. - */ -function wc_taxonomy_metadata_migrate_data( $wp_db_version, $wp_current_db_version ) { - if ( $wp_db_version >= 34370 && $wp_current_db_version < 34370 ) { - global $wpdb; - if ( $wpdb->query( "INSERT INTO {$wpdb->termmeta} ( term_id, meta_key, meta_value ) SELECT woocommerce_term_id, meta_key, meta_value FROM {$wpdb->prefix}woocommerce_termmeta;" ) ) { - $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}woocommerce_termmeta" ); - } - } -} -add_action( 'wp_upgrade', 'wc_taxonomy_metadata_migrate_data', 10, 2 ); - -/** - * Move a term before the a given element of its hierarchy level. - * - * @param int $the_term Term ID. - * @param int $next_id The id of the next sibling element in save hierarchy level. - * @param string $taxonomy Taxnomy. - * @param int $index Term index (default: 0). - * @param mixed $terms List of terms. (default: null). - * @return int - */ -function wc_reorder_terms( $the_term, $next_id, $taxonomy, $index = 0, $terms = null ) { - if ( ! $terms ) { - $terms = get_terms( $taxonomy, 'hide_empty=0&parent=0&menu_order=ASC' ); - } - if ( empty( $terms ) ) { - return $index; - } - - $id = intval( $the_term->term_id ); - - $term_in_level = false; // Flag: is our term to order in this level of terms. - - foreach ( $terms as $term ) { - $term_id = intval( $term->term_id ); - - if ( $term_id === $id ) { // Our term to order, we skip. - $term_in_level = true; - continue; // Our term to order, we skip. - } - // the nextid of our term to order, lets move our term here. - if ( null !== $next_id && $term_id === $next_id ) { - $index++; - $index = wc_set_term_order( $id, $index, $taxonomy, true ); - } - - // Set order. - $index++; - $index = wc_set_term_order( $term_id, $index, $taxonomy ); - - /** - * After a term has had it's order set. - */ - do_action( 'woocommerce_after_set_term_order', $term, $index, $taxonomy ); - - // If that term has children we walk through them. - $children = get_terms( $taxonomy, "parent={$term_id}&hide_empty=0&menu_order=ASC" ); - if ( ! empty( $children ) ) { - $index = wc_reorder_terms( $the_term, $next_id, $taxonomy, $index, $children ); - } - } - - // No nextid meaning our term is in last position. - if ( $term_in_level && null === $next_id ) { - $index = wc_set_term_order( $id, $index + 1, $taxonomy, true ); - } - - return $index; -} - -/** - * Set the sort order of a term. - * - * @param int $term_id Term ID. - * @param int $index Index. - * @param string $taxonomy Taxonomy. - * @param bool $recursive Recursive (default: false). - * @return int - */ -function wc_set_term_order( $term_id, $index, $taxonomy, $recursive = false ) { - - $term_id = (int) $term_id; - $index = (int) $index; - - update_term_meta( $term_id, 'order', $index ); - - if ( ! $recursive ) { - return $index; - } - - $children = get_terms( $taxonomy, "parent=$term_id&hide_empty=0&menu_order=ASC" ); - - foreach ( $children as $term ) { - $index++; - $index = wc_set_term_order( $term->term_id, $index, $taxonomy, true ); - } - - clean_term_cache( $term_id, $taxonomy ); - - return $index; -} - -/** - * Function for recounting product terms, ignoring hidden products. - * - * @param array $terms List of terms. - * @param object $taxonomy Taxonomy. - * @param bool $callback Callback. - * @param bool $terms_are_term_taxonomy_ids If terms are from term_taxonomy_id column. - */ -function _wc_term_recount( $terms, $taxonomy, $callback = true, $terms_are_term_taxonomy_ids = true ) { - global $wpdb; - - // Standard callback. - if ( $callback ) { - _update_post_term_count( $terms, $taxonomy ); - } - - $exclude_term_ids = array(); - $product_visibility_term_ids = wc_get_product_visibility_term_ids(); - - if ( $product_visibility_term_ids['exclude-from-catalog'] ) { - $exclude_term_ids[] = $product_visibility_term_ids['exclude-from-catalog']; - } - - if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) { - $exclude_term_ids[] = $product_visibility_term_ids['outofstock']; - } - - $query = array( - 'fields' => " - SELECT COUNT( DISTINCT ID ) FROM {$wpdb->posts} p - ", - 'join' => '', - 'where' => " - WHERE 1=1 - AND p.post_status = 'publish' - AND p.post_type = 'product' - - ", - ); - - if ( count( $exclude_term_ids ) ) { - $query['join'] .= " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = p.ID'; - $query['where'] .= ' AND exclude_join.object_id IS NULL'; - } - - // Pre-process term taxonomy ids. - if ( ! $terms_are_term_taxonomy_ids ) { - // We passed in an array of TERMS in format id=>parent. - $terms = array_filter( (array) array_keys( $terms ) ); - } else { - // If we have term taxonomy IDs we need to get the term ID. - $term_taxonomy_ids = $terms; - $terms = array(); - foreach ( $term_taxonomy_ids as $term_taxonomy_id ) { - $term = get_term_by( 'term_taxonomy_id', $term_taxonomy_id, $taxonomy->name ); - $terms[] = $term->term_id; - } - } - - // Exit if we have no terms to count. - if ( empty( $terms ) ) { - return; - } - - // Ancestors need counting. - if ( is_taxonomy_hierarchical( $taxonomy->name ) ) { - foreach ( $terms as $term_id ) { - $terms = array_merge( $terms, get_ancestors( $term_id, $taxonomy->name ) ); - } - } - - // Unique terms only. - $terms = array_unique( $terms ); - - // Count the terms. - foreach ( $terms as $term_id ) { - $terms_to_count = array( absint( $term_id ) ); - - if ( is_taxonomy_hierarchical( $taxonomy->name ) ) { - // We need to get the $term's hierarchy so we can count its children too. - $children = get_term_children( $term_id, $taxonomy->name ); - - if ( $children && ! is_wp_error( $children ) ) { - $terms_to_count = array_unique( array_map( 'absint', array_merge( $terms_to_count, $children ) ) ); - } - } - - // Generate term query. - $term_query = $query; - $term_query['join'] .= " INNER JOIN ( SELECT object_id FROM {$wpdb->term_relationships} INNER JOIN {$wpdb->term_taxonomy} using( term_taxonomy_id ) WHERE term_id IN ( " . implode( ',', array_map( 'absint', $terms_to_count ) ) . ' ) ) AS include_join ON include_join.object_id = p.ID'; - - // Get the count. - $count = $wpdb->get_var( implode( ' ', $term_query ) ); // WPCS: unprepared SQL ok. - - // Update the count. - update_term_meta( $term_id, 'product_count_' . $taxonomy->name, absint( $count ) ); - } - - delete_transient( 'wc_term_counts' ); -} - -/** - * Recount terms after the stock amount changes. - * - * @param int $product_id Product ID. - */ -function wc_recount_after_stock_change( $product_id ) { - if ( 'yes' !== get_option( 'woocommerce_hide_out_of_stock_items' ) ) { - return; - } - - $product_terms = get_the_terms( $product_id, 'product_cat' ); - - if ( $product_terms ) { - $product_cats = array(); - - foreach ( $product_terms as $term ) { - $product_cats[ $term->term_id ] = $term->parent; - } - - _wc_term_recount( $product_cats, get_taxonomy( 'product_cat' ), false, false ); - } - - $product_terms = get_the_terms( $product_id, 'product_tag' ); - - if ( $product_terms ) { - $product_tags = array(); - - foreach ( $product_terms as $term ) { - $product_tags[ $term->term_id ] = $term->parent; - } - - _wc_term_recount( $product_tags, get_taxonomy( 'product_tag' ), false, false ); - } -} -add_action( 'woocommerce_product_set_stock_status', 'wc_recount_after_stock_change' ); - - -/** - * Overrides the original term count for product categories and tags with the product count. - * that takes catalog visibility into account. - * - * @param array $terms List of terms. - * @param string|array $taxonomies Single taxonomy or list of taxonomies. - * @return array - */ -function wc_change_term_counts( $terms, $taxonomies ) { - if ( is_admin() || is_ajax() ) { - return $terms; - } - - if ( ! isset( $taxonomies[0] ) || ! in_array( $taxonomies[0], apply_filters( 'woocommerce_change_term_counts', array( 'product_cat', 'product_tag' ) ), true ) ) { - return $terms; - } - - $o_term_counts = get_transient( 'wc_term_counts' ); - $term_counts = $o_term_counts; - - foreach ( $terms as &$term ) { - if ( is_object( $term ) ) { - $term_counts[ $term->term_id ] = isset( $term_counts[ $term->term_id ] ) ? $term_counts[ $term->term_id ] : get_term_meta( $term->term_id, 'product_count_' . $taxonomies[0], true ); - - if ( '' !== $term_counts[ $term->term_id ] ) { - $term->count = absint( $term_counts[ $term->term_id ] ); - } - } - } - - // Update transient. - if ( $term_counts !== $o_term_counts ) { - set_transient( 'wc_term_counts', $term_counts, DAY_IN_SECONDS * 30 ); - } - - return $terms; -} -add_filter( 'get_terms', 'wc_change_term_counts', 10, 2 ); - -/** - * Return products in a given term, and cache value. - * - * To keep in sync, product_count will be cleared on "set_object_terms". - * - * @param int $term_id Term ID. - * @param string $taxonomy Taxonomy. - * @return array - */ -function wc_get_term_product_ids( $term_id, $taxonomy ) { - $product_ids = get_term_meta( $term_id, 'product_ids', true ); - - if ( false === $product_ids || ! is_array( $product_ids ) ) { - $product_ids = get_objects_in_term( $term_id, $taxonomy ); - update_term_meta( $term_id, 'product_ids', $product_ids ); - } - - return $product_ids; -} - -/** - * When a post is updated and terms recounted (called by _update_post_term_count), clear the ids. - * - * @param int $object_id Object ID. - * @param array $terms An array of object terms. - * @param array $tt_ids An array of term taxonomy IDs. - * @param string $taxonomy Taxonomy slug. - * @param bool $append Whether to append new terms to the old terms. - * @param array $old_tt_ids Old array of term taxonomy IDs. - */ -function wc_clear_term_product_ids( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) { - foreach ( $old_tt_ids as $term_id ) { - delete_term_meta( $term_id, 'product_ids' ); - } - foreach ( $tt_ids as $term_id ) { - delete_term_meta( $term_id, 'product_ids' ); - } -} -add_action( 'set_object_terms', 'wc_clear_term_product_ids', 10, 6 ); - -/** - * Get full list of product visibilty term ids. - * - * @since 3.0.0 - * @return int[] - */ -function wc_get_product_visibility_term_ids() { - if ( ! taxonomy_exists( 'product_visibility' ) ) { - wc_doing_it_wrong( __FUNCTION__, 'wc_get_product_visibility_term_ids should not be called before taxonomies are registered (woocommerce_after_register_post_type action).', '3.1' ); - return array(); - } - return array_map( - 'absint', - wp_parse_args( - wp_list_pluck( - get_terms( - array( - 'taxonomy' => 'product_visibility', - 'hide_empty' => false, - ) - ), - 'term_taxonomy_id', - 'name' - ), - array( - 'exclude-from-catalog' => 0, - 'exclude-from-search' => 0, - 'featured' => 0, - 'outofstock' => 0, - 'rated-1' => 0, - 'rated-2' => 0, - 'rated-3' => 0, - 'rated-4' => 0, - 'rated-5' => 0, - ) - ) - ); -} diff --git a/includes/wc-update-functions.php b/includes/wc-update-functions.php deleted file mode 100644 index 439095d5482..00000000000 --- a/includes/wc-update-functions.php +++ /dev/null @@ -1,2285 +0,0 @@ -get_results( "SELECT meta_value, meta_id, post_id FROM {$wpdb->postmeta} WHERE meta_key = '_file_path' AND meta_value != '';" ); - - if ( $existing_file_paths ) { - - foreach ( $existing_file_paths as $existing_file_path ) { - - $old_file_path = trim( $existing_file_path->meta_value ); - - if ( ! empty( $old_file_path ) ) { - // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize - $file_paths = serialize( array( md5( $old_file_path ) => $old_file_path ) ); - - $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_key = '_file_paths', meta_value = %s WHERE meta_id = %d", $file_paths, $existing_file_path->meta_id ) ); - - $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}woocommerce_downloadable_product_permissions SET download_id = %s WHERE product_id = %d", md5( $old_file_path ), $existing_file_path->post_id ) ); - - } - } - } -} - -/** - * Update permalinks for 2.0 - * - * @return void - */ -function wc_update_200_permalinks() { - // Setup default permalinks if shop page is defined. - $permalinks = get_option( 'woocommerce_permalinks' ); - $shop_page_id = wc_get_page_id( 'shop' ); - - if ( empty( $permalinks ) && $shop_page_id > 0 ) { - - $base_slug = $shop_page_id > 0 && get_post( $shop_page_id ) ? get_page_uri( $shop_page_id ) : 'shop'; - - $category_base = 'yes' === get_option( 'woocommerce_prepend_shop_page_to_urls' ) ? trailingslashit( $base_slug ) : ''; - $category_slug = get_option( 'woocommerce_product_category_slug' ) ? get_option( 'woocommerce_product_category_slug' ) : _x( 'product-category', 'slug', 'woocommerce' ); - $tag_slug = get_option( 'woocommerce_product_tag_slug' ) ? get_option( 'woocommerce_product_tag_slug' ) : _x( 'product-tag', 'slug', 'woocommerce' ); - - if ( 'yes' === get_option( 'woocommerce_prepend_shop_page_to_products' ) ) { - $product_base = trailingslashit( $base_slug ); - } else { - $product_slug = get_option( 'woocommerce_product_slug' ); - if ( false !== $product_slug && ! empty( $product_slug ) ) { - $product_base = trailingslashit( $product_slug ); - } else { - $product_base = trailingslashit( _x( 'product', 'slug', 'woocommerce' ) ); - } - } - - if ( 'yes' === get_option( 'woocommerce_prepend_category_to_products' ) ) { - $product_base .= trailingslashit( '%product_cat%' ); - } - - $permalinks = array( - 'product_base' => untrailingslashit( $product_base ), - 'category_base' => untrailingslashit( $category_base . $category_slug ), - 'attribute_base' => untrailingslashit( $category_base ), - 'tag_base' => untrailingslashit( $category_base . $tag_slug ), - ); - - update_option( 'woocommerce_permalinks', $permalinks ); - } -} - -/** - * Update sub-category display options for 2.0 - * - * @return void - */ -function wc_update_200_subcat_display() { - // Update subcat display settings. - if ( 'yes' === get_option( 'woocommerce_shop_show_subcategories' ) ) { - if ( 'yes' === get_option( 'woocommerce_hide_products_when_showing_subcategories' ) ) { - update_option( 'woocommerce_shop_page_display', 'subcategories' ); - } else { - update_option( 'woocommerce_shop_page_display', 'both' ); - } - } - - if ( 'yes' === get_option( 'woocommerce_show_subcategories' ) ) { - if ( 'yes' === get_option( 'woocommerce_hide_products_when_showing_subcategories' ) ) { - update_option( 'woocommerce_category_archive_display', 'subcategories' ); - } else { - update_option( 'woocommerce_category_archive_display', 'both' ); - } - } -} - -/** - * Update tax rates for 2.0 - * - * @return void - */ -function wc_update_200_taxrates() { - global $wpdb; - - // Update tax rates. - $loop = 0; - $tax_rates = get_option( 'woocommerce_tax_rates' ); - - if ( $tax_rates ) { - foreach ( $tax_rates as $tax_rate ) { - - foreach ( $tax_rate['countries'] as $country => $states ) { - - $states = array_reverse( $states ); - - foreach ( $states as $state ) { - - if ( '*' === $state ) { - $state = ''; - } - - $wpdb->insert( - $wpdb->prefix . 'woocommerce_tax_rates', - array( - 'tax_rate_country' => $country, - 'tax_rate_state' => $state, - 'tax_rate' => $tax_rate['rate'], - 'tax_rate_name' => $tax_rate['label'], - 'tax_rate_priority' => 1, - 'tax_rate_compound' => ( 'yes' === $tax_rate['compound'] ) ? 1 : 0, - 'tax_rate_shipping' => ( 'yes' === $tax_rate['shipping'] ) ? 1 : 0, - 'tax_rate_order' => $loop, - 'tax_rate_class' => $tax_rate['class'], - ) - ); - - $loop++; - } - } - } - } - - $local_tax_rates = get_option( 'woocommerce_local_tax_rates' ); - - if ( $local_tax_rates ) { - foreach ( $local_tax_rates as $tax_rate ) { - - $location_type = ( 'postcode' === $tax_rate['location_type'] ) ? 'postcode' : 'city'; - - if ( '*' === $tax_rate['state'] ) { - $tax_rate['state'] = ''; - } - - $wpdb->insert( - $wpdb->prefix . 'woocommerce_tax_rates', - array( - 'tax_rate_country' => $tax_rate['country'], - 'tax_rate_state' => $tax_rate['state'], - 'tax_rate' => $tax_rate['rate'], - 'tax_rate_name' => $tax_rate['label'], - 'tax_rate_priority' => 2, - 'tax_rate_compound' => ( 'yes' === $tax_rate['compound'] ) ? 1 : 0, - 'tax_rate_shipping' => ( 'yes' === $tax_rate['shipping'] ) ? 1 : 0, - 'tax_rate_order' => $loop, - 'tax_rate_class' => $tax_rate['class'], - ) - ); - - $tax_rate_id = $wpdb->insert_id; - - if ( $tax_rate['locations'] ) { - foreach ( $tax_rate['locations'] as $location ) { - - $wpdb->insert( - $wpdb->prefix . 'woocommerce_tax_rate_locations', - array( - 'location_code' => $location, - 'tax_rate_id' => $tax_rate_id, - 'location_type' => $location_type, - ) - ); - - } - } - - $loop++; - } - } - - update_option( 'woocommerce_tax_rates_backup', $tax_rates ); - update_option( 'woocommerce_local_tax_rates_backup', $local_tax_rates ); - delete_option( 'woocommerce_tax_rates' ); - delete_option( 'woocommerce_local_tax_rates' ); -} - -/** - * Update order item line items for 2.0 - * - * @return void - */ -function wc_update_200_line_items() { - global $wpdb; - - // Now its time for the massive update to line items - move them to the new DB tables. - // Reverse with UPDATE `wpwc_postmeta` SET meta_key = '_order_items' WHERE meta_key = '_order_items_old'. - $order_item_rows = $wpdb->get_results( - "SELECT meta_value, post_id FROM {$wpdb->postmeta} WHERE meta_key = '_order_items'" - ); - - foreach ( $order_item_rows as $order_item_row ) { - - $order_items = (array) maybe_unserialize( $order_item_row->meta_value ); - - foreach ( $order_items as $order_item ) { - - if ( ! isset( $order_item['line_total'] ) && isset( $order_item['taxrate'] ) && isset( $order_item['cost'] ) ) { - $order_item['line_tax'] = number_format( ( $order_item['cost'] * $order_item['qty'] ) * ( $order_item['taxrate'] / 100 ), 2, '.', '' ); - $order_item['line_total'] = $order_item['cost'] * $order_item['qty']; - $order_item['line_subtotal_tax'] = $order_item['line_tax']; - $order_item['line_subtotal'] = $order_item['line_total']; - } - - $order_item['line_tax'] = isset( $order_item['line_tax'] ) ? $order_item['line_tax'] : 0; - $order_item['line_total'] = isset( $order_item['line_total'] ) ? $order_item['line_total'] : 0; - $order_item['line_subtotal_tax'] = isset( $order_item['line_subtotal_tax'] ) ? $order_item['line_subtotal_tax'] : 0; - $order_item['line_subtotal'] = isset( $order_item['line_subtotal'] ) ? $order_item['line_subtotal'] : 0; - - $item_id = wc_add_order_item( - $order_item_row->post_id, - array( - 'order_item_name' => $order_item['name'], - 'order_item_type' => 'line_item', - ) - ); - - // Add line item meta. - if ( $item_id ) { - wc_add_order_item_meta( $item_id, '_qty', absint( $order_item['qty'] ) ); - wc_add_order_item_meta( $item_id, '_tax_class', $order_item['tax_class'] ); - wc_add_order_item_meta( $item_id, '_product_id', $order_item['id'] ); - wc_add_order_item_meta( $item_id, '_variation_id', $order_item['variation_id'] ); - wc_add_order_item_meta( $item_id, '_line_subtotal', wc_format_decimal( $order_item['line_subtotal'] ) ); - wc_add_order_item_meta( $item_id, '_line_subtotal_tax', wc_format_decimal( $order_item['line_subtotal_tax'] ) ); - wc_add_order_item_meta( $item_id, '_line_total', wc_format_decimal( $order_item['line_total'] ) ); - wc_add_order_item_meta( $item_id, '_line_tax', wc_format_decimal( $order_item['line_tax'] ) ); - - $meta_rows = array(); - - // Insert meta. - if ( ! empty( $order_item['item_meta'] ) ) { - foreach ( $order_item['item_meta'] as $key => $meta ) { - // Backwards compatibility. - if ( is_array( $meta ) && isset( $meta['meta_name'] ) ) { - $meta_rows[] = '(' . $item_id . ',"' . esc_sql( $meta['meta_name'] ) . '","' . esc_sql( $meta['meta_value'] ) . '")'; - } else { - $meta_rows[] = '(' . $item_id . ',"' . esc_sql( $key ) . '","' . esc_sql( $meta ) . '")'; - } - } - } - - // Insert meta rows at once. - if ( count( $meta_rows ) > 0 ) { - $wpdb->query( - $wpdb->prepare( - "INSERT INTO {$wpdb->prefix}woocommerce_order_itemmeta ( order_item_id, meta_key, meta_value ) - VALUES " . implode( ',', $meta_rows ) . ';', // @codingStandardsIgnoreLine - $order_item_row->post_id - ) - ); - } - - // Delete from DB (rename). - $wpdb->query( - $wpdb->prepare( - "UPDATE {$wpdb->postmeta} - SET meta_key = '_order_items_old' - WHERE meta_key = '_order_items' - AND post_id = %d", - $order_item_row->post_id - ) - ); - } - - unset( $meta_rows, $item_id, $order_item ); - } - } - - // Do the same kind of update for order_taxes - move to lines. - // Reverse with UPDATE `wpwc_postmeta` SET meta_key = '_order_taxes' WHERE meta_key = '_order_taxes_old'. - $order_tax_rows = $wpdb->get_results( - "SELECT meta_value, post_id FROM {$wpdb->postmeta} - WHERE meta_key = '_order_taxes'" - ); - - foreach ( $order_tax_rows as $order_tax_row ) { - - $order_taxes = (array) maybe_unserialize( $order_tax_row->meta_value ); - - if ( ! empty( $order_taxes ) ) { - foreach ( $order_taxes as $order_tax ) { - - if ( ! isset( $order_tax['label'] ) || ! isset( $order_tax['cart_tax'] ) || ! isset( $order_tax['shipping_tax'] ) ) { - continue; - } - - $item_id = wc_add_order_item( - $order_tax_row->post_id, - array( - 'order_item_name' => $order_tax['label'], - 'order_item_type' => 'tax', - ) - ); - - // Add line item meta. - if ( $item_id ) { - wc_add_order_item_meta( $item_id, 'compound', absint( isset( $order_tax['compound'] ) ? $order_tax['compound'] : 0 ) ); - wc_add_order_item_meta( $item_id, 'tax_amount', wc_clean( $order_tax['cart_tax'] ) ); - wc_add_order_item_meta( $item_id, 'shipping_tax_amount', wc_clean( $order_tax['shipping_tax'] ) ); - } - - // Delete from DB (rename). - $wpdb->query( - $wpdb->prepare( - "UPDATE {$wpdb->postmeta} - SET meta_key = '_order_taxes_old' - WHERE meta_key = '_order_taxes' - AND post_id = %d", - $order_tax_row->post_id - ) - ); - - unset( $tax_amount ); - } - } - } -} - -/** - * Update image settings for 2.0 - * - * @return void - */ -function wc_update_200_images() { - // Grab the pre 2.0 Image options and use to populate the new image options settings, - // cleaning up afterwards like nice people do. - foreach ( array( 'catalog', 'single', 'thumbnail' ) as $value ) { - - $old_settings = array_filter( - array( - 'width' => get_option( 'woocommerce_' . $value . '_image_width' ), - 'height' => get_option( 'woocommerce_' . $value . '_image_height' ), - 'crop' => get_option( 'woocommerce_' . $value . '_image_crop' ), - ) - ); - - if ( ! empty( $old_settings ) && update_option( 'shop_' . $value . '_image_size', $old_settings ) ) { - - delete_option( 'woocommerce_' . $value . '_image_width' ); - delete_option( 'woocommerce_' . $value . '_image_height' ); - delete_option( 'woocommerce_' . $value . '_image_crop' ); - - } - } -} - -/** - * Update DB version for 2.0 - * - * @return void - */ -function wc_update_200_db_version() { - WC_Install::update_db_version( '2.0.0' ); -} - -/** - * Update Brazilian States for 2.0.9 - * - * @return void - */ -function wc_update_209_brazillian_state() { - global $wpdb; - - // phpcs:disable WordPress.DB.SlowDBQuery - - // Update brazillian state codes. - $wpdb->update( - $wpdb->postmeta, - array( - 'meta_value' => 'BA', - ), - array( - 'meta_key' => '_billing_state', - 'meta_value' => 'BH', - ) - ); - $wpdb->update( - $wpdb->postmeta, - array( - 'meta_value' => 'BA', - ), - array( - 'meta_key' => '_shipping_state', - 'meta_value' => 'BH', - ) - ); - $wpdb->update( - $wpdb->usermeta, - array( - 'meta_value' => 'BA', - ), - array( - 'meta_key' => 'billing_state', - 'meta_value' => 'BH', - ) - ); - $wpdb->update( - $wpdb->usermeta, - array( - 'meta_value' => 'BA', - ), - array( - 'meta_key' => 'shipping_state', - 'meta_value' => 'BH', - ) - ); - - // phpcs:enable WordPress.DB.SlowDBQuery -} - -/** - * Update DB version for 2.0.9 - * - * @return void - */ -function wc_update_209_db_version() { - WC_Install::update_db_version( '2.0.9' ); -} - -/** - * Remove pages for 2.1 - * - * @return void - */ -function wc_update_210_remove_pages() { - // Pages no longer used. - wp_trash_post( get_option( 'woocommerce_pay_page_id' ) ); - wp_trash_post( get_option( 'woocommerce_thanks_page_id' ) ); - wp_trash_post( get_option( 'woocommerce_view_order_page_id' ) ); - wp_trash_post( get_option( 'woocommerce_change_password_page_id' ) ); - wp_trash_post( get_option( 'woocommerce_edit_address_page_id' ) ); - wp_trash_post( get_option( 'woocommerce_lost_password_page_id' ) ); -} - -/** - * Update file paths to support multiple files for 2.1 - * - * @return void - */ -function wc_update_210_file_paths() { - global $wpdb; - - // Upgrade file paths to support multiple file paths + names etc. - $existing_file_paths = $wpdb->get_results( "SELECT meta_value, meta_id FROM {$wpdb->postmeta} WHERE meta_key = '_file_paths' AND meta_value != '';" ); - - if ( $existing_file_paths ) { - - foreach ( $existing_file_paths as $existing_file_path ) { - - $needs_update = false; - $new_value = array(); - $value = maybe_unserialize( trim( $existing_file_path->meta_value ) ); - - if ( $value ) { - foreach ( $value as $key => $file ) { - if ( ! is_array( $file ) ) { - $needs_update = true; - $new_value[ $key ] = array( - 'file' => $file, - 'name' => wc_get_filename_from_url( $file ), - ); - } else { - $new_value[ $key ] = $file; - } - } - if ( $needs_update ) { - // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize - $new_value = serialize( $new_value ); - - $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_key = %s, meta_value = %s WHERE meta_id = %d", '_downloadable_files', $new_value, $existing_file_path->meta_id ) ); - } - } - } - } -} - -/** - * Update DB version for 2.1 - * - * @return void - */ -function wc_update_210_db_version() { - WC_Install::update_db_version( '2.1.0' ); -} - -/** - * Update shipping options for 2.2 - * - * @return void - */ -function wc_update_220_shipping() { - $woocommerce_ship_to_destination = 'shipping'; - - if ( get_option( 'woocommerce_ship_to_billing_address_only' ) === 'yes' ) { - $woocommerce_ship_to_destination = 'billing_only'; - } elseif ( get_option( 'woocommerce_ship_to_billing' ) === 'yes' ) { - $woocommerce_ship_to_destination = 'billing'; - } - - add_option( 'woocommerce_ship_to_destination', $woocommerce_ship_to_destination, '', 'no' ); -} - -/** - * Update order statuses for 2.2 - * - * @return void - */ -function wc_update_220_order_status() { - global $wpdb; - $wpdb->query( - "UPDATE {$wpdb->posts} as posts - LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id - LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) - LEFT JOIN {$wpdb->terms} AS term USING( term_id ) - SET posts.post_status = 'wc-pending' - WHERE posts.post_type = 'shop_order' - AND posts.post_status = 'publish' - AND tax.taxonomy = 'shop_order_status' - AND term.slug LIKE 'pending%';" - ); - $wpdb->query( - "UPDATE {$wpdb->posts} as posts - LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id - LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) - LEFT JOIN {$wpdb->terms} AS term USING( term_id ) - SET posts.post_status = 'wc-processing' - WHERE posts.post_type = 'shop_order' - AND posts.post_status = 'publish' - AND tax.taxonomy = 'shop_order_status' - AND term.slug LIKE 'processing%';" - ); - $wpdb->query( - "UPDATE {$wpdb->posts} as posts - LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id - LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) - LEFT JOIN {$wpdb->terms} AS term USING( term_id ) - SET posts.post_status = 'wc-on-hold' - WHERE posts.post_type = 'shop_order' - AND posts.post_status = 'publish' - AND tax.taxonomy = 'shop_order_status' - AND term.slug LIKE 'on-hold%';" - ); - $wpdb->query( - "UPDATE {$wpdb->posts} as posts - LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id - LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) - LEFT JOIN {$wpdb->terms} AS term USING( term_id ) - SET posts.post_status = 'wc-completed' - WHERE posts.post_type = 'shop_order' - AND posts.post_status = 'publish' - AND tax.taxonomy = 'shop_order_status' - AND term.slug LIKE 'completed%';" - ); - $wpdb->query( - "UPDATE {$wpdb->posts} as posts - LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id - LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) - LEFT JOIN {$wpdb->terms} AS term USING( term_id ) - SET posts.post_status = 'wc-cancelled' - WHERE posts.post_type = 'shop_order' - AND posts.post_status = 'publish' - AND tax.taxonomy = 'shop_order_status' - AND term.slug LIKE 'cancelled%';" - ); - $wpdb->query( - "UPDATE {$wpdb->posts} as posts - LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id - LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) - LEFT JOIN {$wpdb->terms} AS term USING( term_id ) - SET posts.post_status = 'wc-refunded' - WHERE posts.post_type = 'shop_order' - AND posts.post_status = 'publish' - AND tax.taxonomy = 'shop_order_status' - AND term.slug LIKE 'refunded%';" - ); - $wpdb->query( - "UPDATE {$wpdb->posts} as posts - LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id - LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) - LEFT JOIN {$wpdb->terms} AS term USING( term_id ) - SET posts.post_status = 'wc-failed' - WHERE posts.post_type = 'shop_order' - AND posts.post_status = 'publish' - AND tax.taxonomy = 'shop_order_status' - AND term.slug LIKE 'failed%';" - ); -} - -/** - * Update variations for 2.2 - * - * @return void - */ -function wc_update_220_variations() { - global $wpdb; - // Update variations which manage stock. - $update_variations = $wpdb->get_results( - "SELECT DISTINCT posts.ID AS variation_id, posts.post_parent AS variation_parent FROM {$wpdb->posts} as posts - LEFT OUTER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id AND postmeta.meta_key = '_stock' - LEFT OUTER JOIN {$wpdb->postmeta} as postmeta2 ON posts.ID = postmeta2.post_id AND postmeta2.meta_key = '_manage_stock' - WHERE posts.post_type = 'product_variation' - AND postmeta.meta_value IS NOT NULL - AND postmeta.meta_value != '' - AND postmeta2.meta_value IS NULL" - ); - - foreach ( $update_variations as $variation ) { - $parent_backorders = get_post_meta( $variation->variation_parent, '_backorders', true ); - add_post_meta( $variation->variation_id, '_manage_stock', 'yes', true ); - add_post_meta( $variation->variation_id, '_backorders', $parent_backorders ? $parent_backorders : 'no', true ); - } -} - -/** - * Update attributes for 2.2 - * - * @return void - */ -function wc_update_220_attributes() { - global $wpdb; - // Update taxonomy names with correct sanitized names. - $attribute_taxonomies = $wpdb->get_results( 'SELECT attribute_name, attribute_id FROM ' . $wpdb->prefix . 'woocommerce_attribute_taxonomies' ); - - foreach ( $attribute_taxonomies as $attribute_taxonomy ) { - $sanitized_attribute_name = wc_sanitize_taxonomy_name( $attribute_taxonomy->attribute_name ); - if ( $sanitized_attribute_name !== $attribute_taxonomy->attribute_name ) { - if ( ! $wpdb->get_var( $wpdb->prepare( "SELECT 1=1 FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_name = %s;", $sanitized_attribute_name ) ) ) { - // Update attribute. - $wpdb->update( - "{$wpdb->prefix}woocommerce_attribute_taxonomies", - array( - 'attribute_name' => $sanitized_attribute_name, - ), - array( - 'attribute_id' => $attribute_taxonomy->attribute_id, - ) - ); - - // Update terms. - $wpdb->update( - $wpdb->term_taxonomy, - array( 'taxonomy' => wc_attribute_taxonomy_name( $sanitized_attribute_name ) ), - array( 'taxonomy' => 'pa_' . $attribute_taxonomy->attribute_name ) - ); - } - } - } - - delete_transient( 'wc_attribute_taxonomies' ); - WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); -} - -/** - * Update DB version for 2.2 - * - * @return void - */ -function wc_update_220_db_version() { - WC_Install::update_db_version( '2.2.0' ); -} - -/** - * Update options for 2.3 - * - * @return void - */ -function wc_update_230_options() { - // _money_spent and _order_count may be out of sync - clear them - delete_metadata( 'user', 0, '_money_spent', '', true ); - delete_metadata( 'user', 0, '_order_count', '', true ); - delete_metadata( 'user', 0, '_last_order', '', true ); - - // To prevent taxes being hidden when using a default 'no address' in a store with tax inc prices, set the woocommerce_default_customer_address to use the store base address by default. - if ( '' === get_option( 'woocommerce_default_customer_address', false ) && wc_prices_include_tax() ) { - update_option( 'woocommerce_default_customer_address', 'base' ); - } -} - -/** - * Update DB version for 2.3 - * - * @return void - */ -function wc_update_230_db_version() { - WC_Install::update_db_version( '2.3.0' ); -} - -/** - * Update calc discount options for 2.4 - * - * @return void - */ -function wc_update_240_options() { - /** - * Coupon discount calculations. - * Maintain the old coupon logic for upgrades. - */ - update_option( 'woocommerce_calc_discounts_sequentially', 'yes' ); -} - -/** - * Update shipping methods for 2.4 - * - * @return void - */ -function wc_update_240_shipping_methods() { - /** - * Flat Rate Shipping. - * Update legacy options to new math based options. - */ - $shipping_methods = array( - 'woocommerce_flat_rates' => new WC_Shipping_Legacy_Flat_Rate(), - 'woocommerce_international_delivery_flat_rates' => new WC_Shipping_Legacy_International_Delivery(), - ); - foreach ( $shipping_methods as $flat_rate_option_key => $shipping_method ) { - // Stop this running more than once if routine is repeated. - if ( version_compare( $shipping_method->get_option( 'version', 0 ), '2.4.0', '<' ) ) { - $shipping_classes = WC()->shipping()->get_shipping_classes(); - $has_classes = count( $shipping_classes ) > 0; - $cost_key = $has_classes ? 'no_class_cost' : 'cost'; - $min_fee = $shipping_method->get_option( 'minimum_fee' ); - $math_cost_strings = array( - 'cost' => array(), - 'no_class_cost' => array(), - ); - - $math_cost_strings[ $cost_key ][] = $shipping_method->get_option( 'cost' ); - $fee = $shipping_method->get_option( 'fee' ); - - if ( $fee ) { - $math_cost_strings[ $cost_key ][] = strstr( $fee, '%' ) ? '[fee percent="' . str_replace( '%', '', $fee ) . '" min="' . esc_attr( $min_fee ) . '"]' : $fee; - } - - foreach ( $shipping_classes as $shipping_class ) { - $rate_key = 'class_cost_' . $shipping_class->slug; - $math_cost_strings[ $rate_key ] = $math_cost_strings['no_class_cost']; - } - - $flat_rates = array_filter( (array) get_option( $flat_rate_option_key, array() ) ); - - if ( $flat_rates ) { - foreach ( $flat_rates as $shipping_class => $rate ) { - $rate_key = 'class_cost_' . $shipping_class; - if ( $rate['cost'] || $rate['fee'] ) { - $math_cost_strings[ $rate_key ][] = $rate['cost']; - $math_cost_strings[ $rate_key ][] = strstr( $rate['fee'], '%' ) ? '[fee percent="' . str_replace( '%', '', $rate['fee'] ) . '" min="' . esc_attr( $min_fee ) . '"]' : $rate['fee']; - } - } - } - - if ( 'item' === $shipping_method->type ) { - foreach ( $math_cost_strings as $key => $math_cost_string ) { - $math_cost_strings[ $key ] = array_filter( array_map( 'trim', $math_cost_strings[ $key ] ) ); - if ( ! empty( $math_cost_strings[ $key ] ) ) { - $last_key = max( 0, count( $math_cost_strings[ $key ] ) - 1 ); - $math_cost_strings[ $key ][0] = '( ' . $math_cost_strings[ $key ][0]; - $math_cost_strings[ $key ][ $last_key ] .= ' ) * [qty]'; - } - } - } - - $math_cost_strings['cost'][] = $shipping_method->get_option( 'cost_per_order' ); - - // Save settings. - foreach ( $math_cost_strings as $option_id => $math_cost_string ) { - $shipping_method->settings[ $option_id ] = implode( ' + ', array_filter( $math_cost_string ) ); - } - - $shipping_method->settings['version'] = '2.4.0'; - $shipping_method->settings['type'] = 'item' === $shipping_method->settings['type'] ? 'class' : $shipping_method->settings['type']; - - update_option( $shipping_method->plugin_id . $shipping_method->id . '_settings', $shipping_method->settings ); - } - } -} - -/** - * Update API keys for 2.4 - * - * @return void - */ -function wc_update_240_api_keys() { - global $wpdb; - /** - * Update the old user API keys to the new Apps keys. - */ - $api_users = $wpdb->get_results( "SELECT user_id FROM $wpdb->usermeta WHERE meta_key = 'woocommerce_api_consumer_key'" ); - $apps_keys = array(); - - // Get user data. - foreach ( $api_users as $_user ) { - $user = get_userdata( $_user->user_id ); - $apps_keys[] = array( - 'user_id' => $user->ID, - 'permissions' => $user->woocommerce_api_key_permissions, - 'consumer_key' => wc_api_hash( $user->woocommerce_api_consumer_key ), - 'consumer_secret' => $user->woocommerce_api_consumer_secret, - 'truncated_key' => substr( $user->woocommerce_api_consumer_secret, -7 ), - ); - } - - if ( ! empty( $apps_keys ) ) { - // Create new apps. - foreach ( $apps_keys as $app ) { - $wpdb->insert( - $wpdb->prefix . 'woocommerce_api_keys', - $app, - array( - '%d', - '%s', - '%s', - '%s', - '%s', - ) - ); - } - - // Delete old user keys from usermeta. - foreach ( $api_users as $_user ) { - $user_id = intval( $_user->user_id ); - delete_user_meta( $user_id, 'woocommerce_api_consumer_key' ); - delete_user_meta( $user_id, 'woocommerce_api_consumer_secret' ); - delete_user_meta( $user_id, 'woocommerce_api_key_permissions' ); - } - } -} - -/** - * Update webhooks for 2.4 - * - * @return void - */ -function wc_update_240_webhooks() { - // phpcs:disable WordPress.DB.SlowDBQuery - - /** - * Webhooks. - * Make sure order.update webhooks get the woocommerce_order_edit_status hook. - */ - $order_update_webhooks = get_posts( - array( - 'posts_per_page' => -1, - 'post_type' => 'shop_webhook', - 'meta_key' => '_topic', - 'meta_value' => 'order.updated', - ) - ); - foreach ( $order_update_webhooks as $order_update_webhook ) { - $webhook = new WC_Webhook( $order_update_webhook->ID ); - $webhook->set_topic( 'order.updated' ); - } - - // phpcs:enable WordPress.DB.SlowDBQuery -} - -/** - * Update refunds for 2.4 - * - * @return void - */ -function wc_update_240_refunds() { - global $wpdb; - /** - * Refunds for full refunded orders. - * Update fully refunded orders to ensure they have a refund line item so reports add up. - */ - $refunded_orders = get_posts( - array( - 'posts_per_page' => -1, - 'post_type' => 'shop_order', - 'post_status' => array( 'wc-refunded' ), - ) - ); - - // Ensure emails are disabled during this update routine. - remove_all_actions( 'woocommerce_order_status_refunded_notification' ); - remove_all_actions( 'woocommerce_order_partially_refunded_notification' ); - remove_action( 'woocommerce_order_status_refunded', array( 'WC_Emails', 'send_transactional_email' ) ); - remove_action( 'woocommerce_order_partially_refunded', array( 'WC_Emails', 'send_transactional_email' ) ); - - foreach ( $refunded_orders as $refunded_order ) { - $order_total = get_post_meta( $refunded_order->ID, '_order_total', true ); - $refunded_total = $wpdb->get_var( - $wpdb->prepare( - "SELECT SUM( postmeta.meta_value ) - FROM $wpdb->postmeta AS postmeta - INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) - WHERE postmeta.meta_key = '_refund_amount' - AND postmeta.post_id = posts.ID", - $refunded_order->ID - ) - ); - - if ( $order_total > $refunded_total ) { - wc_create_refund( - array( - 'amount' => $order_total - $refunded_total, - 'reason' => __( 'Order fully refunded', 'woocommerce' ), - 'order_id' => $refunded_order->ID, - 'line_items' => array(), - 'date' => $refunded_order->post_modified, - ) - ); - } - } - - wc_delete_shop_order_transients(); -} - -/** - * Update DB version for 2.4 - * - * @return void - */ -function wc_update_240_db_version() { - WC_Install::update_db_version( '2.4.0' ); -} - -/** - * Update variations for 2.4.1 - * - * @return void - */ -function wc_update_241_variations() { - global $wpdb; - - // Select variations that don't have any _stock_status implemented on WooCommerce 2.2. - $update_variations = $wpdb->get_results( - "SELECT DISTINCT posts.ID AS variation_id, posts.post_parent AS variation_parent - FROM {$wpdb->posts} as posts - LEFT OUTER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id AND postmeta.meta_key = '_stock_status' - WHERE posts.post_type = 'product_variation' - AND postmeta.meta_value IS NULL" - ); - - foreach ( $update_variations as $variation ) { - // Get the parent _stock_status. - $parent_stock_status = get_post_meta( $variation->variation_parent, '_stock_status', true ); - - // Set the _stock_status. - add_post_meta( $variation->variation_id, '_stock_status', $parent_stock_status ? $parent_stock_status : 'instock', true ); - - // Delete old product children array. - delete_transient( 'wc_product_children_' . $variation->variation_parent ); - } - - // Invalidate old transients such as wc_var_price. - WC_Cache_Helper::get_transient_version( 'product', true ); -} - -/** - * Update DB version for 2.4.1 - * - * @return void - */ -function wc_update_241_db_version() { - WC_Install::update_db_version( '2.4.1' ); -} - -/** - * Update currency settings for 2.5 - * - * @return void - */ -function wc_update_250_currency() { - global $wpdb; - // Fix currency settings for LAK currency. - $current_currency = get_option( 'woocommerce_currency' ); - - if ( 'KIP' === $current_currency ) { - update_option( 'woocommerce_currency', 'LAK' ); - } - - // phpcs:disable WordPress.DB.SlowDBQuery - - // Update LAK currency code. - $wpdb->update( - $wpdb->postmeta, - array( - 'meta_value' => 'LAK', - ), - array( - 'meta_key' => '_order_currency', - 'meta_value' => 'KIP', - ) - ); - - // phpcs:enable WordPress.DB.SlowDBQuery -} - -/** - * Update DB version for 2.5 - * - * @return void - */ -function wc_update_250_db_version() { - WC_Install::update_db_version( '2.5.0' ); -} - -/** - * Update ship to countries options for 2.6 - * - * @return void - */ -function wc_update_260_options() { - // woocommerce_calc_shipping option has been removed in 2.6. - if ( 'no' === get_option( 'woocommerce_calc_shipping' ) ) { - update_option( 'woocommerce_ship_to_countries', 'disabled' ); - } - - WC_Admin_Notices::add_notice( 'legacy_shipping' ); -} - -/** - * Update term meta for 2.6 - * - * @return void - */ -function wc_update_260_termmeta() { - global $wpdb; - /** - * Migrate term meta to WordPress tables. - */ - if ( get_option( 'db_version' ) >= 34370 && $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_termmeta';" ) ) { - if ( $wpdb->query( "INSERT INTO {$wpdb->termmeta} ( term_id, meta_key, meta_value ) SELECT woocommerce_term_id, meta_key, meta_value FROM {$wpdb->prefix}woocommerce_termmeta;" ) ) { - $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}woocommerce_termmeta" ); - wp_cache_flush(); - } - } -} - -/** - * Update zones for 2.6 - * - * @return void - */ -function wc_update_260_zones() { - global $wpdb; - /** - * Old (table rate) shipping zones to new core shipping zones migration. - * zone_enabled and zone_type are no longer used, but it's safe to leave them be. - */ - if ( $wpdb->get_var( "SHOW COLUMNS FROM `{$wpdb->prefix}woocommerce_shipping_zones` LIKE 'zone_enabled';" ) ) { - $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_shipping_zones CHANGE `zone_type` `zone_type` VARCHAR(40) NOT NULL DEFAULT '';" ); - $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_shipping_zones CHANGE `zone_enabled` `zone_enabled` INT(1) NOT NULL DEFAULT 1;" ); - } -} - -/** - * Update zone methods for 2.6 - * - * @return void - */ -function wc_update_260_zone_methods() { - global $wpdb; - - /** - * Shipping zones in WC 2.6.0 use a table named woocommerce_shipping_zone_methods. - * Migrate the old data out of woocommerce_shipping_zone_shipping_methods into the new table and port over any known options (used by table rates and flat rate boxes). - */ - if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_shipping_zone_shipping_methods';" ) ) { - $old_methods = $wpdb->get_results( "SELECT zone_id, shipping_method_type, shipping_method_order, shipping_method_id FROM {$wpdb->prefix}woocommerce_shipping_zone_shipping_methods;" ); - - if ( $old_methods ) { - $max_new_id = $wpdb->get_var( "SELECT MAX(instance_id) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods" ); - $max_old_id = $wpdb->get_var( "SELECT MAX(shipping_method_id) FROM {$wpdb->prefix}woocommerce_shipping_zone_shipping_methods" ); - - // Avoid ID conflicts. - $wpdb->query( $wpdb->prepare( "ALTER TABLE {$wpdb->prefix}woocommerce_shipping_zone_methods AUTO_INCREMENT = %d;", max( $max_new_id, $max_old_id ) + 1 ) ); - - // Store changes. - $changes = array(); - - // Move data. - foreach ( $old_methods as $old_method ) { - $wpdb->insert( - $wpdb->prefix . 'woocommerce_shipping_zone_methods', - array( - 'zone_id' => $old_method->zone_id, - 'method_id' => $old_method->shipping_method_type, - 'method_order' => $old_method->shipping_method_order, - ) - ); - - $new_instance_id = $wpdb->insert_id; - - // Move main settings. - $older_settings_key = 'woocommerce_' . $old_method->shipping_method_type . '-' . $old_method->shipping_method_id . '_settings'; - $old_settings_key = 'woocommerce_' . $old_method->shipping_method_type . '_' . $old_method->shipping_method_id . '_settings'; - add_option( 'woocommerce_' . $old_method->shipping_method_type . '_' . $new_instance_id . '_settings', get_option( $old_settings_key, get_option( $older_settings_key ) ) ); - - // Handling for table rate and flat rate box shipping. - if ( 'table_rate' === $old_method->shipping_method_type ) { - // Move priority settings. - add_option( 'woocommerce_table_rate_default_priority_' . $new_instance_id, get_option( 'woocommerce_table_rate_default_priority_' . $old_method->shipping_method_id ) ); - add_option( 'woocommerce_table_rate_priorities_' . $new_instance_id, get_option( 'woocommerce_table_rate_priorities_' . $old_method->shipping_method_id ) ); - - // Move rates. - $wpdb->update( - $wpdb->prefix . 'woocommerce_shipping_table_rates', - array( - 'shipping_method_id' => $new_instance_id, - ), - array( - 'shipping_method_id' => $old_method->shipping_method_id, - ) - ); - } elseif ( 'flat_rate_boxes' === $old_method->shipping_method_type ) { - $wpdb->update( - $wpdb->prefix . 'woocommerce_shipping_flat_rate_boxes', - array( - 'shipping_method_id' => $new_instance_id, - ), - array( - 'shipping_method_id' => $old_method->shipping_method_id, - ) - ); - } - - $changes[ $old_method->shipping_method_id ] = $new_instance_id; - } - - // $changes contains keys (old method ids) and values (new instance ids) if extra processing is needed in plugins. - // Store this to an option so extensions can pick it up later, then fire an action. - update_option( 'woocommerce_updated_instance_ids', $changes ); - do_action( 'woocommerce_updated_instance_ids', $changes ); - } - } - - // Change ranges used to ... - $wpdb->query( "UPDATE {$wpdb->prefix}woocommerce_shipping_zone_locations SET location_code = REPLACE( location_code, '-', '...' );" ); -} - -/** - * Update refunds for 2.6 - * - * @return void - */ -function wc_update_260_refunds() { - global $wpdb; - /** - * Refund item qty should be negative. - */ - $wpdb->query( - "UPDATE {$wpdb->prefix}woocommerce_order_itemmeta as item_meta - LEFT JOIN {$wpdb->prefix}woocommerce_order_items as items ON item_meta.order_item_id = items.order_item_id - LEFT JOIN {$wpdb->posts} as posts ON items.order_id = posts.ID - SET item_meta.meta_value = item_meta.meta_value * -1 - WHERE item_meta.meta_value > 0 AND item_meta.meta_key = '_qty' AND posts.post_type = 'shop_order_refund'" - ); -} - -/** - * Update DB version for 2.6 - * - * @return void - */ -function wc_update_260_db_version() { - WC_Install::update_db_version( '2.6.0' ); -} - -/** - * Update webhooks for 3.0 - * - * @return void - */ -function wc_update_300_webhooks() { - // phpcs:disable WordPress.DB.SlowDBQuery - - /** - * Make sure product.update webhooks get the woocommerce_product_quick_edit_save - * and woocommerce_product_bulk_edit_save hooks. - */ - $product_update_webhooks = get_posts( - array( - 'posts_per_page' => -1, - 'post_type' => 'shop_webhook', - 'meta_key' => '_topic', - 'meta_value' => 'product.updated', - ) - ); - foreach ( $product_update_webhooks as $product_update_webhook ) { - $webhook = new WC_Webhook( $product_update_webhook->ID ); - $webhook->set_topic( 'product.updated' ); - } - - // phpcs:enable WordPress.DB.SlowDBQuery -} - -/** - * Add an index to the field comment_type to improve the response time of the query - * used by WC_Comments::wp_count_comments() to get the number of comments by type. - */ -function wc_update_300_comment_type_index() { - global $wpdb; - - $index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->comments} WHERE column_name = 'comment_type' and key_name = 'woo_idx_comment_type'" ); - - if ( is_null( $index_exists ) ) { - // Add an index to the field comment_type to improve the response time of the query - // used by WC_Comments::wp_count_comments() to get the number of comments by type. - $wpdb->query( "ALTER TABLE {$wpdb->comments} ADD INDEX woo_idx_comment_type (comment_type)" ); - } -} - -/** - * Update grouped products for 3.0 - * - * @return void - */ -function wc_update_300_grouped_products() { - global $wpdb; - $parents = $wpdb->get_col( "SELECT DISTINCT( post_parent ) FROM {$wpdb->posts} WHERE post_parent > 0 AND post_type = 'product';" ); - foreach ( $parents as $parent_id ) { - $parent = wc_get_product( $parent_id ); - if ( $parent && $parent->is_type( 'grouped' ) ) { - $children_ids = get_posts( - array( - 'post_parent' => $parent_id, - 'posts_per_page' => -1, - 'post_type' => 'product', - 'fields' => 'ids', - ) - ); - update_post_meta( $parent_id, '_children', $children_ids ); - - // Update children to remove the parent. - $wpdb->update( - $wpdb->posts, - array( - 'post_parent' => 0, - ), - array( - 'post_parent' => $parent_id, - ) - ); - } - } -} - -/** - * Update shipping tax classes for 3.0 - * - * @return void - */ -function wc_update_300_settings() { - $woocommerce_shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' ); - if ( '' === $woocommerce_shipping_tax_class ) { - update_option( 'woocommerce_shipping_tax_class', 'inherit' ); - } elseif ( 'standard' === $woocommerce_shipping_tax_class ) { - update_option( 'woocommerce_shipping_tax_class', '' ); - } -} - -/** - * Convert meta values into term for product visibility. - */ -function wc_update_300_product_visibility() { - global $wpdb; - - WC_Install::create_terms(); - - $featured_term = get_term_by( 'name', 'featured', 'product_visibility' ); - - if ( $featured_term ) { - $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_featured' AND meta_value = 'yes';", $featured_term->term_taxonomy_id ) ); - } - - $exclude_search_term = get_term_by( 'name', 'exclude-from-search', 'product_visibility' ); - - if ( $exclude_search_term ) { - $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_visibility' AND meta_value IN ('hidden', 'catalog');", $exclude_search_term->term_taxonomy_id ) ); - } - - $exclude_catalog_term = get_term_by( 'name', 'exclude-from-catalog', 'product_visibility' ); - - if ( $exclude_catalog_term ) { - $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_visibility' AND meta_value IN ('hidden', 'search');", $exclude_catalog_term->term_taxonomy_id ) ); - } - - $outofstock_term = get_term_by( 'name', 'outofstock', 'product_visibility' ); - - if ( $outofstock_term ) { - $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_stock_status' AND meta_value = 'outofstock';", $outofstock_term->term_taxonomy_id ) ); - } - - $rating_term = get_term_by( 'name', 'rated-1', 'product_visibility' ); - - if ( $rating_term ) { - $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 1;", $rating_term->term_taxonomy_id ) ); - } - - $rating_term = get_term_by( 'name', 'rated-2', 'product_visibility' ); - - if ( $rating_term ) { - $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 2;", $rating_term->term_taxonomy_id ) ); - } - - $rating_term = get_term_by( 'name', 'rated-3', 'product_visibility' ); - - if ( $rating_term ) { - $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 3;", $rating_term->term_taxonomy_id ) ); - } - - $rating_term = get_term_by( 'name', 'rated-4', 'product_visibility' ); - - if ( $rating_term ) { - $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 4;", $rating_term->term_taxonomy_id ) ); - } - - $rating_term = get_term_by( 'name', 'rated-5', 'product_visibility' ); - - if ( $rating_term ) { - $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 5;", $rating_term->term_taxonomy_id ) ); - } -} - -/** - * Update DB Version. - */ -function wc_update_300_db_version() { - WC_Install::update_db_version( '3.0.0' ); -} - -/** - * Add an index to the downloadable product permissions table to improve performance of update_user_by_order_id. - */ -function wc_update_310_downloadable_products() { - global $wpdb; - - $index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE column_name = 'order_id' and key_name = 'order_id'" ); - - if ( is_null( $index_exists ) ) { - $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions ADD INDEX order_id (order_id)" ); - } -} - -/** - * Find old order notes and ensure they have the correct type for exclusion. - */ -function wc_update_310_old_comments() { - global $wpdb; - - $wpdb->query( "UPDATE $wpdb->comments comments LEFT JOIN $wpdb->posts as posts ON comments.comment_post_ID = posts.ID SET comment_type = 'order_note' WHERE posts.post_type = 'shop_order' AND comment_type = '';" ); -} - -/** - * Update DB Version. - */ -function wc_update_310_db_version() { - WC_Install::update_db_version( '3.1.0' ); -} - -/** - * Update shop_manager capabilities. - */ -function wc_update_312_shop_manager_capabilities() { - $role = get_role( 'shop_manager' ); - $role->remove_cap( 'unfiltered_html' ); -} - -/** - * Update DB Version. - */ -function wc_update_312_db_version() { - WC_Install::update_db_version( '3.1.2' ); -} - -/** - * Update state codes for Mexico. - */ -function wc_update_320_mexican_states() { - global $wpdb; - - $mx_states = array( - 'Distrito Federal' => 'CMX', - 'Jalisco' => 'JAL', - 'Nuevo Leon' => 'NLE', - 'Aguascalientes' => 'AGS', - 'Baja California' => 'BCN', - 'Baja California Sur' => 'BCS', - 'Campeche' => 'CAM', - 'Chiapas' => 'CHP', - 'Chihuahua' => 'CHH', - 'Coahuila' => 'COA', - 'Colima' => 'COL', - 'Durango' => 'DGO', - 'Guanajuato' => 'GTO', - 'Guerrero' => 'GRO', - 'Hidalgo' => 'HGO', - 'Estado de Mexico' => 'MEX', - 'Michoacan' => 'MIC', - 'Morelos' => 'MOR', - 'Nayarit' => 'NAY', - 'Oaxaca' => 'OAX', - 'Puebla' => 'PUE', - 'Queretaro' => 'QRO', - 'Quintana Roo' => 'ROO', - 'San Luis Potosi' => 'SLP', - 'Sinaloa' => 'SIN', - 'Sonora' => 'SON', - 'Tabasco' => 'TAB', - 'Tamaulipas' => 'TMP', - 'Tlaxcala' => 'TLA', - 'Veracruz' => 'VER', - 'Yucatan' => 'YUC', - 'Zacatecas' => 'ZAC', - ); - - foreach ( $mx_states as $old => $new ) { - $wpdb->query( - $wpdb->prepare( - "UPDATE $wpdb->postmeta - SET meta_value = %s - WHERE meta_key IN ( '_billing_state', '_shipping_state' ) - AND meta_value = %s", - $new, - $old - ) - ); - $wpdb->update( - "{$wpdb->prefix}woocommerce_shipping_zone_locations", - array( - 'location_code' => 'MX:' . $new, - ), - array( - 'location_code' => 'MX:' . $old, - ) - ); - $wpdb->update( - "{$wpdb->prefix}woocommerce_tax_rates", - array( - 'tax_rate_state' => strtoupper( $new ), - ), - array( - 'tax_rate_state' => strtoupper( $old ), - ) - ); - } -} - -/** - * Update DB Version. - */ -function wc_update_320_db_version() { - WC_Install::update_db_version( '3.2.0' ); -} - -/** - * Update image settings to use new aspect ratios and widths. - */ -function wc_update_330_image_options() { - $old_thumbnail_size = get_option( 'shop_catalog_image_size', array() ); - $old_single_size = get_option( 'shop_single_image_size', array() ); - - if ( ! empty( $old_thumbnail_size['width'] ) ) { - $width = absint( $old_thumbnail_size['width'] ); - $height = absint( $old_thumbnail_size['height'] ); - $hard_crop = ! empty( $old_thumbnail_size['crop'] ); - - if ( ! $width ) { - $width = 300; - } - - if ( ! $height ) { - $height = $width; - } - - update_option( 'woocommerce_thumbnail_image_width', $width ); - - // Calculate cropping mode from old image options. - if ( ! $hard_crop ) { - update_option( 'woocommerce_thumbnail_cropping', 'uncropped' ); - } elseif ( $width === $height ) { - update_option( 'woocommerce_thumbnail_cropping', '1:1' ); - } else { - $ratio = $width / $height; - $fraction = wc_decimal_to_fraction( $ratio ); - - if ( $fraction ) { - update_option( 'woocommerce_thumbnail_cropping', 'custom' ); - update_option( 'woocommerce_thumbnail_cropping_custom_width', $fraction[0] ); - update_option( 'woocommerce_thumbnail_cropping_custom_height', $fraction[1] ); - } - } - } - - // Single is uncropped. - if ( ! empty( $old_single_size['width'] ) ) { - update_option( 'woocommerce_single_image_width', absint( $old_single_size['width'] ) ); - } -} - -/** - * Migrate webhooks from post type to CRUD. - */ -function wc_update_330_webhooks() { - register_post_type( 'shop_webhook' ); - - // Map statuses from post_type to Webhooks CRUD. - $statuses = array( - 'publish' => 'active', - 'draft' => 'paused', - 'pending' => 'disabled', - ); - - $posts = get_posts( - array( - 'posts_per_page' => -1, - 'post_type' => 'shop_webhook', - 'post_status' => 'any', - ) - ); - - foreach ( $posts as $post ) { - $webhook = new WC_Webhook(); - $webhook->set_name( $post->post_title ); - $webhook->set_status( isset( $statuses[ $post->post_status ] ) ? $statuses[ $post->post_status ] : 'disabled' ); - $webhook->set_delivery_url( get_post_meta( $post->ID, '_delivery_url', true ) ); - $webhook->set_secret( get_post_meta( $post->ID, '_secret', true ) ); - $webhook->set_topic( get_post_meta( $post->ID, '_topic', true ) ); - $webhook->set_api_version( get_post_meta( $post->ID, '_api_version', true ) ); - $webhook->set_user_id( $post->post_author ); - $webhook->set_pending_delivery( false ); - $webhook->save(); - - wp_delete_post( $post->ID, true ); - } - - unregister_post_type( 'shop_webhook' ); -} - -/** - * Assign default cat to all products with no cats. - */ -function wc_update_330_set_default_product_cat() { - global $wpdb; - - $default_category = get_option( 'default_product_cat', 0 ); - - if ( $default_category ) { - $wpdb->query( - $wpdb->prepare( - "INSERT INTO {$wpdb->term_relationships} (object_id, term_taxonomy_id) - SELECT DISTINCT posts.ID, %s FROM {$wpdb->posts} posts - LEFT JOIN - ( - SELECT object_id FROM {$wpdb->term_relationships} term_relationships - LEFT JOIN {$wpdb->term_taxonomy} term_taxonomy ON term_relationships.term_taxonomy_id = term_taxonomy.term_taxonomy_id - WHERE term_taxonomy.taxonomy = 'product_cat' - ) AS tax_query - ON posts.ID = tax_query.object_id - WHERE posts.post_type = 'product' - AND tax_query.object_id IS NULL", - $default_category - ) - ); - wp_cache_flush(); - delete_transient( 'wc_term_counts' ); - wp_update_term_count_now( array( $default_category ), 'product_cat' ); - } -} - -/** - * Update product stock status to use the new onbackorder status. - */ -function wc_update_330_product_stock_status() { - global $wpdb; - - if ( 'yes' !== get_option( 'woocommerce_manage_stock' ) ) { - return; - } - - $min_stock_amount = (int) get_option( 'woocommerce_notify_no_stock_amount', 0 ); - - // Get all products that have stock management enabled, stock less than or equal to min stock amount, and backorders enabled. - $post_ids = $wpdb->get_col( - $wpdb->prepare( - "SELECT t1.post_id FROM $wpdb->postmeta t1 - INNER JOIN $wpdb->postmeta t2 - ON t1.post_id = t2.post_id - AND t1.meta_key = '_manage_stock' AND t1.meta_value = 'yes' - AND t2.meta_key = '_stock' AND t2.meta_value <= %d - INNER JOIN $wpdb->postmeta t3 - ON t2.post_id = t3.post_id - AND t3.meta_key = '_backorders' AND ( t3.meta_value = 'yes' OR t3.meta_value = 'notify' )", - $min_stock_amount - ) - ); - - if ( empty( $post_ids ) ) { - return; - } - - $post_ids = array_map( 'absint', $post_ids ); - - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared - // Set the status to onbackorder for those products. - $wpdb->query( - "UPDATE $wpdb->postmeta - SET meta_value = 'onbackorder' - WHERE meta_key = '_stock_status' AND post_id IN ( " . implode( ',', $post_ids ) . ' )' - ); - // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared -} - -/** - * Clear addons page transients - */ -function wc_update_330_clear_transients() { - delete_transient( 'wc_addons_sections' ); - delete_transient( 'wc_addons_featured' ); -} - -/** - * Set PayPal's sandbox credentials. - */ -function wc_update_330_set_paypal_sandbox_credentials() { - - $paypal_settings = get_option( 'woocommerce_paypal_settings' ); - - if ( isset( $paypal_settings['testmode'] ) && 'yes' === $paypal_settings['testmode'] ) { - foreach ( array( 'api_username', 'api_password', 'api_signature' ) as $credential ) { - if ( ! empty( $paypal_settings[ $credential ] ) ) { - $paypal_settings[ 'sandbox_' . $credential ] = $paypal_settings[ $credential ]; - } - } - - update_option( 'woocommerce_paypal_settings', $paypal_settings ); - } -} - -/** - * Update DB Version. - */ -function wc_update_330_db_version() { - WC_Install::update_db_version( '3.3.0' ); -} - -/** - * Update state codes for Ireland and BD. - */ -function wc_update_340_states() { - $country_states = array( - 'IE' => array( - 'CK' => 'CO', - 'DN' => 'D', - 'GY' => 'G', - 'TY' => 'TA', - ), - 'BD' => array( - 'BAG' => 'BD-05', - 'BAN' => 'BD-01', - 'BAR' => 'BD-02', - 'BARI' => 'BD-06', - 'BHO' => 'BD-07', - 'BOG' => 'BD-03', - 'BRA' => 'BD-04', - 'CHA' => 'BD-09', - 'CHI' => 'BD-10', - 'CHU' => 'BD-12', - 'COX' => 'BD-11', - 'COM' => 'BD-08', - 'DHA' => 'BD-13', - 'DIN' => 'BD-14', - 'FAR' => 'BD-15', - 'FEN' => 'BD-16', - 'GAI' => 'BD-19', - 'GAZI' => 'BD-18', - 'GOP' => 'BD-17', - 'HAB' => 'BD-20', - 'JAM' => 'BD-21', - 'JES' => 'BD-22', - 'JHA' => 'BD-25', - 'JHE' => 'BD-23', - 'JOY' => 'BD-24', - 'KHA' => 'BD-29', - 'KHU' => 'BD-27', - 'KIS' => 'BD-26', - 'KUR' => 'BD-28', - 'KUS' => 'BD-30', - 'LAK' => 'BD-31', - 'LAL' => 'BD-32', - 'MAD' => 'BD-36', - 'MAG' => 'BD-37', - 'MAN' => 'BD-33', - 'MEH' => 'BD-39', - 'MOU' => 'BD-38', - 'MUN' => 'BD-35', - 'MYM' => 'BD-34', - 'NAO' => 'BD-48', - 'NAR' => 'BD-43', - 'NARG' => 'BD-40', - 'NARD' => 'BD-42', - 'NAT' => 'BD-44', - 'NAW' => 'BD-45', - 'NET' => 'BD-41', - 'NIL' => 'BD-46', - 'NOA' => 'BD-47', - 'PAB' => 'BD-49', - 'PAN' => 'BD-52', - 'PAT' => 'BD-51', - 'PIR' => 'BD-50', - 'RAJB' => 'BD-53', - 'RAJ' => 'BD-54', - 'RAN' => 'BD-56', - 'RANP' => 'BD-55', - 'SAT' => 'BD-58', - 'SHA' => 'BD-57', - 'SIR' => 'BD-59', - 'SUN' => 'BD-61', - 'SYL' => 'BD-60', - 'TAN' => 'BD-63', - 'THA' => 'BD-64', - ), - ); - - update_option( 'woocommerce_update_340_states', $country_states ); -} - -/** - * Update next state in the queue. - * - * @return bool True to run again, false if completed. - */ -function wc_update_340_state() { - global $wpdb; - - $country_states = array_filter( (array) get_option( 'woocommerce_update_340_states', array() ) ); - - if ( empty( $country_states ) ) { - return false; - } - - foreach ( $country_states as $country => $states ) { - foreach ( $states as $old => $new ) { - $wpdb->query( - $wpdb->prepare( - "UPDATE $wpdb->postmeta - SET meta_value = %s - WHERE meta_key IN ( '_billing_state', '_shipping_state' ) - AND meta_value = %s", - $new, - $old - ) - ); - $wpdb->update( - "{$wpdb->prefix}woocommerce_shipping_zone_locations", - array( - 'location_code' => $country . ':' . $new, - ), - array( - 'location_code' => $country . ':' . $old, - ) - ); - $wpdb->update( - "{$wpdb->prefix}woocommerce_tax_rates", - array( - 'tax_rate_state' => strtoupper( $new ), - ), - array( - 'tax_rate_state' => strtoupper( $old ), - ) - ); - unset( $country_states[ $country ][ $old ] ); - - if ( empty( $country_states[ $country ] ) ) { - unset( $country_states[ $country ] ); - } - break 2; - } - } - - if ( ! empty( $country_states ) ) { - return update_option( 'woocommerce_update_340_states', $country_states ); - } - - delete_option( 'woocommerce_update_340_states' ); - - return false; -} - -/** - * Set last active prop for users. - */ -function wc_update_340_last_active() { - global $wpdb; - // @codingStandardsIgnoreStart. - $wpdb->query( - $wpdb->prepare( " - INSERT INTO {$wpdb->usermeta} (user_id, meta_key, meta_value) - SELECT DISTINCT users.ID, 'wc_last_active', %s - FROM {$wpdb->users} as users - LEFT OUTER JOIN {$wpdb->usermeta} AS usermeta ON users.ID = usermeta.user_id AND usermeta.meta_key = 'wc_last_active' - WHERE usermeta.meta_value IS NULL - ", - (string) strtotime( date( 'Y-m-d', current_time( 'timestamp', true ) ) ) - ) - ); - // @codingStandardsIgnoreEnd. -} - -/** - * Update DB Version. - */ -function wc_update_340_db_version() { - WC_Install::update_db_version( '3.4.0' ); -} - -/** - * Remove duplicate foreign keys - * - * @return void - */ -function wc_update_343_cleanup_foreign_keys() { - global $wpdb; - - $results = $wpdb->get_results( - "SELECT CONSTRAINT_NAME - FROM information_schema.TABLE_CONSTRAINTS - WHERE CONSTRAINT_SCHEMA = '{$wpdb->dbname}' - AND CONSTRAINT_NAME LIKE '%wc_download_log_ib%' - AND CONSTRAINT_TYPE = 'FOREIGN KEY' - AND TABLE_NAME = '{$wpdb->prefix}wc_download_log'" - ); - - if ( $results ) { - foreach ( $results as $fk ) { - $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_download_log DROP FOREIGN KEY {$fk->CONSTRAINT_NAME}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - } - } -} - -/** - * Update DB version. - * - * @return void - */ -function wc_update_343_db_version() { - WC_Install::update_db_version( '3.4.3' ); -} - -/** - * Recreate user roles so existing users will get the new capabilities. - * - * @return void - */ -function wc_update_344_recreate_roles() { - WC_Install::remove_roles(); - WC_Install::create_roles(); -} - -/** - * Update DB version. - * - * @return void - */ -function wc_update_344_db_version() { - WC_Install::update_db_version( '3.4.4' ); -} - -/** - * Set the comment type to 'review' for product reviews that don't have a comment type. - */ -function wc_update_350_reviews_comment_type() { - global $wpdb; - - $wpdb->query( - "UPDATE {$wpdb->prefix}comments JOIN {$wpdb->prefix}posts ON {$wpdb->prefix}posts.ID = {$wpdb->prefix}comments.comment_post_ID AND ( {$wpdb->prefix}posts.post_type = 'product' OR {$wpdb->prefix}posts.post_type = 'product_variation' ) SET {$wpdb->prefix}comments.comment_type = 'review' WHERE {$wpdb->prefix}comments.comment_type = ''" - ); -} - -/** - * Update DB Version. - */ -function wc_update_350_db_version() { - WC_Install::update_db_version( '3.5.0' ); -} - -/** - * Drop the fk_wc_download_log_permission_id FK as we use a new one with the table and blog prefix for MS compatability. - * - * @return void - */ -function wc_update_352_drop_download_log_fk() { - global $wpdb; - $results = $wpdb->get_results( - "SELECT CONSTRAINT_NAME - FROM information_schema.TABLE_CONSTRAINTS - WHERE CONSTRAINT_SCHEMA = '{$wpdb->dbname}' - AND CONSTRAINT_NAME = 'fk_wc_download_log_permission_id' - AND CONSTRAINT_TYPE = 'FOREIGN KEY' - AND TABLE_NAME = '{$wpdb->prefix}wc_download_log'" - ); - - // We only need to drop the old key as WC_Install::create_tables() takes care of creating the new FK. - if ( $results ) { - $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_download_log DROP FOREIGN KEY fk_wc_download_log_permission_id" ); // phpcs:ignore WordPress.WP.PreparedSQL.NotPrepared - } -} - -/** - * Remove edit_user capabilities from shop managers and use "translated" capabilities instead. - * See wc_shop_manager_has_capability function. - */ -function wc_update_354_modify_shop_manager_caps() { - global $wp_roles; - - if ( ! class_exists( 'WP_Roles' ) ) { - return; - } - - if ( ! isset( $wp_roles ) ) { - $wp_roles = new WP_Roles(); // @codingStandardsIgnoreLine - } - - $wp_roles->remove_cap( 'shop_manager', 'edit_users' ); -} - -/** - * Update DB Version. - */ -function wc_update_354_db_version() { - WC_Install::update_db_version( '3.5.4' ); -} - -/** - * Update product lookup tables in bulk. - */ -function wc_update_360_product_lookup_tables() { - wc_update_product_lookup_tables(); -} - -/** - * Renames ordering meta to be consistent across taxonomies. - */ -function wc_update_360_term_meta() { - global $wpdb; - - $wpdb->query( "UPDATE {$wpdb->termmeta} SET meta_key = 'order' WHERE meta_key LIKE 'order_pa_%';" ); -} - -/** - * Add new user_order_remaining_expires to speed up user download permission fetching. - * - * @return void - */ -function wc_update_360_downloadable_product_permissions_index() { - global $wpdb; - - $index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE key_name = 'user_order_remaining_expires'" ); - - if ( is_null( $index_exists ) ) { - $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions ADD INDEX user_order_remaining_expires (user_id,order_id,downloads_remaining,access_expires)" ); - } -} - -/** - * Update DB Version. - */ -function wc_update_360_db_version() { - WC_Install::update_db_version( '3.6.0' ); -} - -/** - * Put tax classes into a DB table. - * - * @return void - */ -function wc_update_370_tax_rate_classes() { - global $wpdb; - - $classes = array_map( 'trim', explode( "\n", get_option( 'woocommerce_tax_classes' ) ) ); - - if ( $classes ) { - foreach ( $classes as $class ) { - if ( empty( $class ) ) { - continue; - } - WC_Tax::create_tax_class( $class ); - } - } - delete_option( 'woocommerce_tax_classes' ); -} - -/** - * Update currency settings for 3.7.0 - * - * @return void - */ -function wc_update_370_mro_std_currency() { - global $wpdb; - - // Fix currency settings for MRU and STN currency. - $current_currency = get_option( 'woocommerce_currency' ); - - if ( 'MRO' === $current_currency ) { - update_option( 'woocommerce_currency', 'MRU' ); - } - - if ( 'STD' === $current_currency ) { - update_option( 'woocommerce_currency', 'STN' ); - } - - // Update MRU currency code. - $wpdb->update( - $wpdb->postmeta, - array( - 'meta_value' => 'MRU', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value - ), - array( - 'meta_key' => '_order_currency', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key - 'meta_value' => 'MRO', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value - ) - ); - - // Update STN currency code. - $wpdb->update( - $wpdb->postmeta, - array( - 'meta_value' => 'STN', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value - ), - array( - 'meta_key' => '_order_currency', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key - 'meta_value' => 'STD', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value - ) - ); -} - -/** - * Update DB Version. - */ -function wc_update_370_db_version() { - WC_Install::update_db_version( '3.7.0' ); -} - -/** - * We've moved the MaxMind database to a new location, as per the TOS' requirement that the database not - * be publicly accessible. - */ -function wc_update_390_move_maxmind_database() { - // Make sure to use all of the correct filters to pull the local database path. - $old_path = apply_filters( 'woocommerce_geolocation_local_database_path', WP_CONTENT_DIR . '/uploads/GeoLite2-Country.mmdb', 2 ); - - // Generate a prefix for the old file and store it in the integration as it would expect it. - $prefix = wp_generate_password( 32, false ); - update_option( 'woocommerce_maxmind_geolocation_settings', array( 'database_prefix' => $prefix ) ); - - // Generate the new path in the same way that the integration will. - $uploads_dir = wp_upload_dir(); - $new_path = trailingslashit( $uploads_dir['basedir'] ) . 'woocommerce_uploads/' . $prefix . '-GeoLite2-Country.mmdb'; - $new_path = apply_filters( 'woocommerce_geolocation_local_database_path', $new_path, 2 ); - $new_path = apply_filters( 'woocommerce_maxmind_geolocation_database_path', $new_path ); - - // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged - @rename( $old_path, $new_path ); -} - -/** - * So that we can best meet MaxMind's TOS, the geolocation database update cron should run once per 15 days. - */ -function wc_update_390_change_geolocation_database_update_cron() { - wp_clear_scheduled_hook( 'woocommerce_geoip_updater' ); - wp_schedule_event( time() + ( DAY_IN_SECONDS * 15 ), 'fifteendays', 'woocommerce_geoip_updater' ); -} - -/** - * Update DB version. - */ -function wc_update_390_db_version() { - WC_Install::update_db_version( '3.9.0' ); -} - -/** - * Increase column size - */ -function wc_update_400_increase_size_of_column() { - global $wpdb; - $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_product_meta_lookup MODIFY COLUMN `min_price` decimal(19,4) NULL default NULL" ); - $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_product_meta_lookup MODIFY COLUMN `max_price` decimal(19,4) NULL default NULL" ); -} - -/** - * Reset ActionScheduler migration status. Needs AS >= 3.0 shipped with WC >= 4.0. - */ -function wc_update_400_reset_action_scheduler_migration_status() { - if ( - class_exists( 'ActionScheduler_DataController' ) && - method_exists( 'ActionScheduler_DataController', 'mark_migration_incomplete' ) - ) { - \ActionScheduler_DataController::mark_migration_incomplete(); - } -} - -/** - * Update DB version. - */ -function wc_update_400_db_version() { - WC_Install::update_db_version( '4.0.0' ); -} - -/** - * Register attributes as terms for variable products, in increments of 100 products. - * - * This migration was added to support a new mechanism to improve the filtering of - * variable products by attribute (https://github.com/woocommerce/woocommerce/pull/26260), - * however that mechanism was later reverted (https://github.com/woocommerce/woocommerce/pull/27625) - * due to numerous issues found. Thus the migration is no longer needed. - * - * @return bool true if the migration needs to be run again. - */ -function wc_update_440_insert_attribute_terms_for_variable_products() { - return false; -} - -/** - * Update DB version. - */ -function wc_update_440_db_version() { - WC_Install::update_db_version( '4.4.0' ); -} - -/** - * Update DB version to 4.5.0. - */ -function wc_update_450_db_version() { - WC_Install::update_db_version( '4.5.0' ); -} - -/** - * Sanitize all coupons code. - * - * @return bool True to run again, false if completed. - */ -function wc_update_450_sanitize_coupons_code() { - global $wpdb; - - $coupon_id = 0; - $last_coupon_id = get_option( 'woocommerce_update_450_last_coupon_id', '0' ); - - $coupons = $wpdb->get_results( - $wpdb->prepare( - "SELECT ID, post_title FROM $wpdb->posts WHERE ID > %d AND post_type = 'shop_coupon' LIMIT 10", - $last_coupon_id - ), - ARRAY_A - ); - - if ( empty( $coupons ) ) { - delete_option( 'woocommerce_update_450_last_coupon_id' ); - return false; - } - - foreach ( $coupons as $key => $data ) { - $coupon_id = intval( $data['ID'] ); - $code = trim( wp_filter_kses( $data['post_title'] ) ); - - if ( ! empty( $code ) && $data['post_title'] !== $code ) { - $wpdb->update( - $wpdb->posts, - array( - 'post_title' => $code, - ), - array( - 'ID' => $coupon_id, - ), - array( - '%s', - ), - array( - '%d', - ) - ); - - // Clean cache. - clean_post_cache( $coupon_id ); - wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $data['post_title'], 'coupons' ); - } - } - - // Start the run again. - if ( $coupon_id ) { - return update_option( 'woocommerce_update_450_last_coupon_id', $coupon_id ); - } - - delete_option( 'woocommerce_update_450_last_coupon_id' ); - return false; -} - -/** - * Fixes product review count that might have been incorrect. - * - * See @link https://github.com/woocommerce/woocommerce/issues/27688. - */ -function wc_update_500_fix_product_review_count() { - global $wpdb; - - $product_id = 0; - $last_product_id = get_option( 'woocommerce_update_500_last_product_id', '0' ); - - $products_data = $wpdb->get_results( - $wpdb->prepare( - " - SELECT post_id, meta_value - FROM $wpdb->postmeta - JOIN $wpdb->posts - ON $wpdb->postmeta.post_id = $wpdb->posts.ID - WHERE - post_type = 'product' - AND post_status = 'publish' - AND post_id > %d - AND meta_key = '_wc_review_count' - ORDER BY post_id ASC - LIMIT 10 - ", - $last_product_id - ), - ARRAY_A - ); - - if ( empty( $products_data ) ) { - delete_option( 'woocommerce_update_500_last_product_id' ); - return false; - } - - $product_ids_to_check = array_column( $products_data, 'post_id' ); - $actual_review_counts = WC_Comments::get_review_counts_for_product_ids( $product_ids_to_check ); - - foreach ( $products_data as $product_data ) { - $product_id = intval( $product_data['post_id'] ); - $current_review_count = intval( $product_data['meta_value'] ); - - if ( intval( $actual_review_counts[ $product_id ] ) !== $current_review_count ) { - WC_Comments::clear_transients( $product_id ); - } - } - - // Start the run again. - if ( $product_id ) { - return update_option( 'woocommerce_update_500_last_product_id', $product_id ); - } - - delete_option( 'woocommerce_update_500_last_product_id' ); - return false; -} - -/** - * Update DB version to 5.0.0. - */ -function wc_update_500_db_version() { - WC_Install::update_db_version( '5.0.0' ); -} diff --git a/includes/wc-user-functions.php b/includes/wc-user-functions.php deleted file mode 100644 index 0c30c4f1aeb..00000000000 --- a/includes/wc-user-functions.php +++ /dev/null @@ -1,926 +0,0 @@ -Please log in.', 'woocommerce' ), $email ) ); - } - - if ( 'yes' === get_option( 'woocommerce_registration_generate_username', 'yes' ) && empty( $username ) ) { - $username = wc_create_new_customer_username( $email, $args ); - } - - $username = sanitize_user( $username ); - - if ( empty( $username ) || ! validate_username( $username ) ) { - return new WP_Error( 'registration-error-invalid-username', __( 'Please enter a valid account username.', 'woocommerce' ) ); - } - - if ( username_exists( $username ) ) { - return new WP_Error( 'registration-error-username-exists', __( 'An account is already registered with that username. Please choose another.', 'woocommerce' ) ); - } - - // Handle password creation. - $password_generated = false; - if ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) && empty( $password ) ) { - $password = wp_generate_password(); - $password_generated = true; - } - - if ( empty( $password ) ) { - return new WP_Error( 'registration-error-missing-password', __( 'Please enter an account password.', 'woocommerce' ) ); - } - - // Use WP_Error to handle registration errors. - $errors = new WP_Error(); - - do_action( 'woocommerce_register_post', $username, $email, $errors ); - - $errors = apply_filters( 'woocommerce_registration_errors', $errors, $username, $email ); - - if ( $errors->get_error_code() ) { - return $errors; - } - - $new_customer_data = apply_filters( - 'woocommerce_new_customer_data', - array_merge( - $args, - array( - 'user_login' => $username, - 'user_pass' => $password, - 'user_email' => $email, - 'role' => 'customer', - ) - ) - ); - - $customer_id = wp_insert_user( $new_customer_data ); - - if ( is_wp_error( $customer_id ) ) { - return $customer_id; - } - - do_action( 'woocommerce_created_customer', $customer_id, $new_customer_data, $password_generated ); - - return $customer_id; - } -} - -/** - * Create a unique username for a new customer. - * - * @since 3.6.0 - * @param string $email New customer email address. - * @param array $new_user_args Array of new user args, maybe including first and last names. - * @param string $suffix Append string to username to make it unique. - * @return string Generated username. - */ -function wc_create_new_customer_username( $email, $new_user_args = array(), $suffix = '' ) { - $username_parts = array(); - - if ( isset( $new_user_args['first_name'] ) ) { - $username_parts[] = sanitize_user( $new_user_args['first_name'], true ); - } - - if ( isset( $new_user_args['last_name'] ) ) { - $username_parts[] = sanitize_user( $new_user_args['last_name'], true ); - } - - // Remove empty parts. - $username_parts = array_filter( $username_parts ); - - // If there are no parts, e.g. name had unicode chars, or was not provided, fallback to email. - if ( empty( $username_parts ) ) { - $email_parts = explode( '@', $email ); - $email_username = $email_parts[0]; - - // Exclude common prefixes. - if ( in_array( - $email_username, - array( - 'sales', - 'hello', - 'mail', - 'contact', - 'info', - ), - true - ) ) { - // Get the domain part. - $email_username = $email_parts[1]; - } - - $username_parts[] = sanitize_user( $email_username, true ); - } - - $username = wc_strtolower( implode( '.', $username_parts ) ); - - if ( $suffix ) { - $username .= $suffix; - } - - /** - * WordPress 4.4 - filters the list of blocked usernames. - * - * @since 3.7.0 - * @param array $usernames Array of blocked usernames. - */ - $illegal_logins = (array) apply_filters( 'illegal_user_logins', array() ); - - // Stop illegal logins and generate a new random username. - if ( in_array( strtolower( $username ), array_map( 'strtolower', $illegal_logins ), true ) ) { - $new_args = array(); - - /** - * Filter generated customer username. - * - * @since 3.7.0 - * @param string $username Generated username. - * @param string $email New customer email address. - * @param array $new_user_args Array of new user args, maybe including first and last names. - * @param string $suffix Append string to username to make it unique. - */ - $new_args['first_name'] = apply_filters( - 'woocommerce_generated_customer_username', - 'woo_user_' . zeroise( wp_rand( 0, 9999 ), 4 ), - $email, - $new_user_args, - $suffix - ); - - return wc_create_new_customer_username( $email, $new_args, $suffix ); - } - - if ( username_exists( $username ) ) { - // Generate something unique to append to the username in case of a conflict with another user. - $suffix = '-' . zeroise( wp_rand( 0, 9999 ), 4 ); - return wc_create_new_customer_username( $email, $new_user_args, $suffix ); - } - - /** - * Filter new customer username. - * - * @since 3.7.0 - * @param string $username Customer username. - * @param string $email New customer email address. - * @param array $new_user_args Array of new user args, maybe including first and last names. - * @param string $suffix Append string to username to make it unique. - */ - return apply_filters( 'woocommerce_new_customer_username', $username, $email, $new_user_args, $suffix ); -} - -/** - * Login a customer (set auth cookie and set global user object). - * - * @param int $customer_id Customer ID. - */ -function wc_set_customer_auth_cookie( $customer_id ) { - wp_set_current_user( $customer_id ); - wp_set_auth_cookie( $customer_id, true ); - - // Update session. - WC()->session->init_session_cookie(); -} - -/** - * Get past orders (by email) and update them. - * - * @param int $customer_id Customer ID. - * @return int - */ -function wc_update_new_customer_past_orders( $customer_id ) { - $linked = 0; - $complete = 0; - $customer = get_user_by( 'id', absint( $customer_id ) ); - $customer_orders = wc_get_orders( - array( - 'limit' => -1, - 'customer' => array( array( 0, $customer->user_email ) ), - 'return' => 'ids', - ) - ); - - if ( ! empty( $customer_orders ) ) { - foreach ( $customer_orders as $order_id ) { - $order = wc_get_order( $order_id ); - if ( ! $order ) { - continue; - } - - $order->set_customer_id( $customer->ID ); - $order->save(); - - if ( $order->has_downloadable_item() ) { - $data_store = WC_Data_Store::load( 'customer-download' ); - $data_store->delete_by_order_id( $order->get_id() ); - wc_downloadable_product_permissions( $order->get_id(), true ); - } - - do_action( 'woocommerce_update_new_customer_past_order', $order_id, $customer ); - - if ( get_post_status( $order_id ) === 'wc-completed' ) { - $complete++; - } - - $linked++; - } - } - - if ( $complete ) { - update_user_meta( $customer_id, 'paying_customer', 1 ); - update_user_meta( $customer_id, '_order_count', '' ); - update_user_meta( $customer_id, '_money_spent', '' ); - delete_user_meta( $customer_id, '_last_order' ); - } - - return $linked; -} - -/** - * Order payment completed - This is a paying customer. - * - * @param int $order_id Order ID. - */ -function wc_paying_customer( $order_id ) { - $order = wc_get_order( $order_id ); - $customer_id = $order->get_customer_id(); - - if ( $customer_id > 0 && 'shop_order_refund' !== $order->get_type() ) { - $customer = new WC_Customer( $customer_id ); - - if ( ! $customer->get_is_paying_customer() ) { - $customer->set_is_paying_customer( true ); - $customer->save(); - } - } -} -add_action( 'woocommerce_payment_complete', 'wc_paying_customer' ); -add_action( 'woocommerce_order_status_completed', 'wc_paying_customer' ); - -/** - * Checks if a user (by email or ID or both) has bought an item. - * - * @param string $customer_email Customer email to check. - * @param int $user_id User ID to check. - * @param int $product_id Product ID to check. - * @return bool - */ -function wc_customer_bought_product( $customer_email, $user_id, $product_id ) { - global $wpdb; - - $result = apply_filters( 'woocommerce_pre_customer_bought_product', null, $customer_email, $user_id, $product_id ); - - if ( null !== $result ) { - return $result; - } - - $transient_name = 'wc_customer_bought_product_' . md5( $customer_email . $user_id ); - $transient_version = WC_Cache_Helper::get_transient_version( 'orders' ); - $transient_value = get_transient( $transient_name ); - - if ( isset( $transient_value['value'], $transient_value['version'] ) && $transient_value['version'] === $transient_version ) { - $result = $transient_value['value']; - } else { - $customer_data = array( $user_id ); - - if ( $user_id ) { - $user = get_user_by( 'id', $user_id ); - - if ( isset( $user->user_email ) ) { - $customer_data[] = $user->user_email; - } - } - - if ( is_email( $customer_email ) ) { - $customer_data[] = $customer_email; - } - - $customer_data = array_map( 'esc_sql', array_filter( array_unique( $customer_data ) ) ); - $statuses = array_map( 'esc_sql', wc_get_is_paid_statuses() ); - - if ( count( $customer_data ) === 0 ) { - return false; - } - - $result = $wpdb->get_col( - " - SELECT im.meta_value FROM {$wpdb->posts} AS p - INNER JOIN {$wpdb->postmeta} AS pm ON p.ID = pm.post_id - INNER JOIN {$wpdb->prefix}woocommerce_order_items AS i ON p.ID = i.order_id - INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS im ON i.order_item_id = im.order_item_id - WHERE p.post_status IN ( 'wc-" . implode( "','wc-", $statuses ) . "' ) - AND pm.meta_key IN ( '_billing_email', '_customer_user' ) - AND im.meta_key IN ( '_product_id', '_variation_id' ) - AND im.meta_value != 0 - AND pm.meta_value IN ( '" . implode( "','", $customer_data ) . "' ) - " - ); // WPCS: unprepared SQL ok. - $result = array_map( 'absint', $result ); - - $transient_value = array( - 'version' => $transient_version, - 'value' => $result, - ); - - set_transient( $transient_name, $transient_value, DAY_IN_SECONDS * 30 ); - } - return in_array( absint( $product_id ), $result, true ); -} - -/** - * Checks if the current user has a role. - * - * @param string $role The role. - * @return bool - */ -function wc_current_user_has_role( $role ) { - return wc_user_has_role( wp_get_current_user(), $role ); -} - -/** - * Checks if a user has a role. - * - * @param int|\WP_User $user The user. - * @param string $role The role. - * @return bool - */ -function wc_user_has_role( $user, $role ) { - if ( ! is_object( $user ) ) { - $user = get_userdata( $user ); - } - - if ( ! $user || ! $user->exists() ) { - return false; - } - - return in_array( $role, $user->roles, true ); -} - -/** - * Checks if a user has a certain capability. - * - * @param array $allcaps All capabilities. - * @param array $caps Capabilities. - * @param array $args Arguments. - * - * @return array The filtered array of all capabilities. - */ -function wc_customer_has_capability( $allcaps, $caps, $args ) { - if ( isset( $caps[0] ) ) { - switch ( $caps[0] ) { - case 'view_order': - $user_id = intval( $args[1] ); - $order = wc_get_order( $args[2] ); - - if ( $order && $user_id === $order->get_user_id() ) { - $allcaps['view_order'] = true; - } - break; - case 'pay_for_order': - $user_id = intval( $args[1] ); - $order_id = isset( $args[2] ) ? $args[2] : null; - - // When no order ID, we assume it's a new order - // and thus, customer can pay for it. - if ( ! $order_id ) { - $allcaps['pay_for_order'] = true; - break; - } - - $order = wc_get_order( $order_id ); - - if ( $order && ( $user_id === $order->get_user_id() || ! $order->get_user_id() ) ) { - $allcaps['pay_for_order'] = true; - } - break; - case 'order_again': - $user_id = intval( $args[1] ); - $order = wc_get_order( $args[2] ); - - if ( $order && $user_id === $order->get_user_id() ) { - $allcaps['order_again'] = true; - } - break; - case 'cancel_order': - $user_id = intval( $args[1] ); - $order = wc_get_order( $args[2] ); - - if ( $order && $user_id === $order->get_user_id() ) { - $allcaps['cancel_order'] = true; - } - break; - case 'download_file': - $user_id = intval( $args[1] ); - $download = $args[2]; - - if ( $download && $user_id === $download->get_user_id() ) { - $allcaps['download_file'] = true; - } - break; - } - } - return $allcaps; -} -add_filter( 'user_has_cap', 'wc_customer_has_capability', 10, 3 ); - -/** - * Safe way of allowing shop managers restricted capabilities that will remove - * access to the capabilities if WooCommerce is deactivated. - * - * @since 3.5.4 - * @param bool[] $allcaps Array of key/value pairs where keys represent a capability name and boolean values - * represent whether the user has that capability. - * @param string[] $caps Required primitive capabilities for the requested capability. - * @param array $args Arguments that accompany the requested capability check. - * @param WP_User $user The user object. - * @return bool[] - */ -function wc_shop_manager_has_capability( $allcaps, $caps, $args, $user ) { - - if ( wc_user_has_role( $user, 'shop_manager' ) ) { - // @see wc_modify_map_meta_cap, which limits editing to customers. - $allcaps['edit_users'] = true; - } - - return $allcaps; -} -add_filter( 'user_has_cap', 'wc_shop_manager_has_capability', 10, 4 ); - -/** - * Modify the list of editable roles to prevent non-admin adding admin users. - * - * @param array $roles Roles. - * @return array - */ -function wc_modify_editable_roles( $roles ) { - if ( is_multisite() && is_super_admin() ) { - return $roles; - } - if ( ! wc_current_user_has_role( 'administrator' ) ) { - unset( $roles['administrator'] ); - - if ( wc_current_user_has_role( 'shop_manager' ) ) { - $shop_manager_editable_roles = apply_filters( 'woocommerce_shop_manager_editable_roles', array( 'customer' ) ); - return array_intersect_key( $roles, array_flip( $shop_manager_editable_roles ) ); - } - } - - return $roles; -} -add_filter( 'editable_roles', 'wc_modify_editable_roles' ); - -/** - * Modify capabilities to prevent non-admin users editing admin users. - * - * $args[0] will be the user being edited in this case. - * - * @param array $caps Array of caps. - * @param string $cap Name of the cap we are checking. - * @param int $user_id ID of the user being checked against. - * @param array $args Arguments. - * @return array - */ -function wc_modify_map_meta_cap( $caps, $cap, $user_id, $args ) { - if ( is_multisite() && is_super_admin() ) { - return $caps; - } - switch ( $cap ) { - case 'edit_user': - case 'remove_user': - case 'promote_user': - case 'delete_user': - if ( ! isset( $args[0] ) || $args[0] === $user_id ) { - break; - } else { - if ( ! wc_current_user_has_role( 'administrator' ) ) { - if ( wc_user_has_role( $args[0], 'administrator' ) ) { - $caps[] = 'do_not_allow'; - } elseif ( wc_current_user_has_role( 'shop_manager' ) ) { - // Shop managers can only edit customer info. - $userdata = get_userdata( $args[0] ); - $shop_manager_editable_roles = apply_filters( 'woocommerce_shop_manager_editable_roles', array( 'customer' ) ); - if ( property_exists( $userdata, 'roles' ) && ! empty( $userdata->roles ) && ! array_intersect( $userdata->roles, $shop_manager_editable_roles ) ) { - $caps[] = 'do_not_allow'; - } - } - } - } - break; - } - return $caps; -} -add_filter( 'map_meta_cap', 'wc_modify_map_meta_cap', 10, 4 ); - -/** - * Get customer download permissions from the database. - * - * @param int $customer_id Customer/User ID. - * @return array - */ -function wc_get_customer_download_permissions( $customer_id ) { - $data_store = WC_Data_Store::load( 'customer-download' ); - return apply_filters( 'woocommerce_permission_list', $data_store->get_downloads_for_customer( $customer_id ), $customer_id ); -} - -/** - * Get customer available downloads. - * - * @param int $customer_id Customer/User ID. - * @return array - */ -function wc_get_customer_available_downloads( $customer_id ) { - $downloads = array(); - $_product = null; - $order = null; - $file_number = 0; - - // Get results from valid orders only. - $results = wc_get_customer_download_permissions( $customer_id ); - - if ( $results ) { - foreach ( $results as $result ) { - $order_id = intval( $result->order_id ); - - if ( ! $order || $order->get_id() !== $order_id ) { - // New order. - $order = wc_get_order( $order_id ); - $_product = null; - } - - // Make sure the order exists for this download. - if ( ! $order ) { - continue; - } - - // Check if downloads are permitted. - if ( ! $order->is_download_permitted() ) { - continue; - } - - $product_id = intval( $result->product_id ); - - if ( ! $_product || $_product->get_id() !== $product_id ) { - // New product. - $file_number = 0; - $_product = wc_get_product( $product_id ); - } - - // Check product exists and has the file. - if ( ! $_product || ! $_product->exists() || ! $_product->has_file( $result->download_id ) ) { - continue; - } - - $download_file = $_product->get_file( $result->download_id ); - - // Download name will be 'Product Name' for products with a single downloadable file, and 'Product Name - File X' for products with multiple files. - $download_name = apply_filters( - 'woocommerce_downloadable_product_name', - $download_file['name'], - $_product, - $result->download_id, - $file_number - ); - - $downloads[] = array( - 'download_url' => add_query_arg( - array( - 'download_file' => $product_id, - 'order' => $result->order_key, - 'email' => rawurlencode( $result->user_email ), - 'key' => $result->download_id, - ), - home_url( '/' ) - ), - 'download_id' => $result->download_id, - 'product_id' => $_product->get_id(), - 'product_name' => $_product->get_name(), - 'product_url' => $_product->is_visible() ? $_product->get_permalink() : '', // Since 3.3.0. - 'download_name' => $download_name, - 'order_id' => $order->get_id(), - 'order_key' => $order->get_order_key(), - 'downloads_remaining' => $result->downloads_remaining, - 'access_expires' => $result->access_expires, - 'file' => array( - 'name' => $download_file->get_name(), - 'file' => $download_file->get_file(), - ), - ); - - $file_number++; - } - } - - return apply_filters( 'woocommerce_customer_available_downloads', $downloads, $customer_id ); -} - -/** - * Get total spent by customer. - * - * @param int $user_id User ID. - * @return string - */ -function wc_get_customer_total_spent( $user_id ) { - $customer = new WC_Customer( $user_id ); - return $customer->get_total_spent(); -} - -/** - * Get total orders by customer. - * - * @param int $user_id User ID. - * @return int - */ -function wc_get_customer_order_count( $user_id ) { - $customer = new WC_Customer( $user_id ); - return $customer->get_order_count(); -} - -/** - * Reset _customer_user on orders when a user is deleted. - * - * @param int $user_id User ID. - */ -function wc_reset_order_customer_id_on_deleted_user( $user_id ) { - global $wpdb; - - $wpdb->update( - $wpdb->postmeta, - array( - 'meta_value' => 0, - ), - array( - 'meta_key' => '_customer_user', - 'meta_value' => $user_id, - ) - ); // WPCS: slow query ok. -} - -add_action( 'deleted_user', 'wc_reset_order_customer_id_on_deleted_user' ); - -/** - * Get review verification status. - * - * @param int $comment_id Comment ID. - * @return bool - */ -function wc_review_is_from_verified_owner( $comment_id ) { - $verified = get_comment_meta( $comment_id, 'verified', true ); - return '' === $verified ? WC_Comments::add_comment_purchase_verification( $comment_id ) : (bool) $verified; -} - -/** - * Disable author archives for customers. - * - * @since 2.5.0 - */ -function wc_disable_author_archives_for_customers() { - global $author; - - if ( is_author() ) { - $user = get_user_by( 'id', $author ); - - if ( user_can( $user, 'customer' ) && ! user_can( $user, 'edit_posts' ) ) { - wp_safe_redirect( wc_get_page_permalink( 'shop' ) ); - exit; - } - } -} - -add_action( 'template_redirect', 'wc_disable_author_archives_for_customers' ); - -/** - * Hooks into the `profile_update` hook to set the user last updated timestamp. - * - * @since 2.6.0 - * @param int $user_id The user that was updated. - * @param array $old The profile fields pre-change. - */ -function wc_update_profile_last_update_time( $user_id, $old ) { - wc_set_user_last_update_time( $user_id ); -} - -add_action( 'profile_update', 'wc_update_profile_last_update_time', 10, 2 ); - -/** - * Hooks into the update user meta function to set the user last updated timestamp. - * - * @since 2.6.0 - * @param int $meta_id ID of the meta object that was changed. - * @param int $user_id The user that was updated. - * @param string $meta_key Name of the meta key that was changed. - * @param string $_meta_value Value of the meta that was changed. - */ -function wc_meta_update_last_update_time( $meta_id, $user_id, $meta_key, $_meta_value ) { - $keys_to_track = apply_filters( 'woocommerce_user_last_update_fields', array( 'first_name', 'last_name' ) ); - - $update_time = in_array( $meta_key, $keys_to_track, true ) ? true : false; - $update_time = 'billing_' === substr( $meta_key, 0, 8 ) ? true : $update_time; - $update_time = 'shipping_' === substr( $meta_key, 0, 9 ) ? true : $update_time; - - if ( $update_time ) { - wc_set_user_last_update_time( $user_id ); - } -} - -add_action( 'update_user_meta', 'wc_meta_update_last_update_time', 10, 4 ); - -/** - * Sets a user's "last update" time to the current timestamp. - * - * @since 2.6.0 - * @param int $user_id The user to set a timestamp for. - */ -function wc_set_user_last_update_time( $user_id ) { - update_user_meta( $user_id, 'last_update', gmdate( 'U' ) ); -} - -/** - * Get customer saved payment methods list. - * - * @since 2.6.0 - * @param int $customer_id Customer ID. - * @return array - */ -function wc_get_customer_saved_methods_list( $customer_id ) { - return apply_filters( 'woocommerce_saved_payment_methods_list', array(), $customer_id ); -} - -/** - * Get info about customer's last order. - * - * @since 2.6.0 - * @param int $customer_id Customer ID. - * @return WC_Order|bool Order object if successful or false. - */ -function wc_get_customer_last_order( $customer_id ) { - $customer = new WC_Customer( $customer_id ); - - return $customer->get_last_order(); -} - -/** - * Add support for searching by display_name. - * - * @since 3.2.0 - * @param array $search_columns Column names. - * @return array - */ -function wc_user_search_columns( $search_columns ) { - $search_columns[] = 'display_name'; - return $search_columns; -} -add_filter( 'user_search_columns', 'wc_user_search_columns' ); - -/** - * When a user is deleted in WordPress, delete corresponding WooCommerce data. - * - * @param int $user_id User ID being deleted. - */ -function wc_delete_user_data( $user_id ) { - global $wpdb; - - // Clean up sessions. - $wpdb->delete( - $wpdb->prefix . 'woocommerce_sessions', - array( - 'session_key' => $user_id, - ) - ); - - // Revoke API keys. - $wpdb->delete( - $wpdb->prefix . 'woocommerce_api_keys', - array( - 'user_id' => $user_id, - ) - ); - - // Clean up payment tokens. - $payment_tokens = WC_Payment_Tokens::get_customer_tokens( $user_id ); - - foreach ( $payment_tokens as $payment_token ) { - $payment_token->delete(); - } -} -add_action( 'delete_user', 'wc_delete_user_data' ); - -/** - * Store user agents. Used for tracker. - * - * @since 3.0.0 - * @param string $user_login User login. - * @param int|object $user User. - */ -function wc_maybe_store_user_agent( $user_login, $user ) { - if ( 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ) && user_can( $user, 'manage_woocommerce' ) ) { - $admin_user_agents = array_filter( (array) get_option( 'woocommerce_tracker_ua', array() ) ); - $admin_user_agents[] = wc_get_user_agent(); - update_option( 'woocommerce_tracker_ua', array_unique( $admin_user_agents ) ); - } -} -add_action( 'wp_login', 'wc_maybe_store_user_agent', 10, 2 ); - -/** - * Update logic triggered on login. - * - * @since 3.4.0 - * @param string $user_login User login. - * @param object $user User. - */ -function wc_user_logged_in( $user_login, $user ) { - wc_update_user_last_active( $user->ID ); - update_user_meta( $user->ID, '_woocommerce_load_saved_cart_after_login', 1 ); -} -add_action( 'wp_login', 'wc_user_logged_in', 10, 2 ); - -/** - * Update when the user was last active. - * - * @since 3.4.0 - */ -function wc_current_user_is_active() { - if ( ! is_user_logged_in() ) { - return; - } - wc_update_user_last_active( get_current_user_id() ); -} -add_action( 'wp', 'wc_current_user_is_active', 10 ); - -/** - * Set the user last active timestamp to now. - * - * @since 3.4.0 - * @param int $user_id User ID to mark active. - */ -function wc_update_user_last_active( $user_id ) { - if ( ! $user_id ) { - return; - } - update_user_meta( $user_id, 'wc_last_active', (string) strtotime( gmdate( 'Y-m-d', time() ) ) ); -} - -/** - * Translate WC roles using the woocommerce textdomain. - * - * @since 3.7.0 - * @param string $translation Translated text. - * @param string $text Text to translate. - * @param string $context Context information for the translators. - * @param string $domain Text domain. Unique identifier for retrieving translated strings. - * @return string - */ -function wc_translate_user_roles( $translation, $text, $context, $domain ) { - // translate_user_role() only accepts a second parameter starting in WP 5.2. - if ( version_compare( get_bloginfo( 'version' ), '5.2', '<' ) ) { - return $translation; - } - - if ( 'User role' === $context && 'default' === $domain && in_array( $text, array( 'Shop manager', 'Customer' ), true ) ) { - return translate_user_role( $text, 'woocommerce' ); - } - - return $translation; -} -add_filter( 'gettext_with_context', 'wc_translate_user_roles', 10, 4 ); diff --git a/includes/wc-webhook-functions.php b/includes/wc-webhook-functions.php deleted file mode 100644 index bc1d15998eb..00000000000 --- a/includes/wc-webhook-functions.php +++ /dev/null @@ -1,204 +0,0 @@ - $data['webhook']->get_id(), - 'arg' => $data['arg'], - ); - - $next_scheduled_date = WC()->queue()->get_next( 'woocommerce_deliver_webhook_async', $queue_args, 'woocommerce-webhooks' ); - - // Make webhooks unique - only schedule one webhook every 10 minutes to maintain backward compatibility with WP Cron behaviour seen in WC < 3.5.0. - if ( is_null( $next_scheduled_date ) || $next_scheduled_date->getTimestamp() >= ( 600 + gmdate( 'U' ) ) ) { - WC()->queue()->add( 'woocommerce_deliver_webhook_async', $queue_args, 'woocommerce-webhooks' ); - } - } else { - // Deliver immediately. - $data['webhook']->deliver( $data['arg'] ); - } - } -} -add_action( 'shutdown', 'wc_webhook_execute_queue' ); - -/** - * Process webhook delivery. - * - * @since 3.3.0 - * @param WC_Webhook $webhook Webhook instance. - * @param array $arg Delivery arguments. - */ -function wc_webhook_process_delivery( $webhook, $arg ) { - // We need to queue the webhook so that it can be ran after the request has finished processing. - global $wc_queued_webhooks; - if ( ! isset( $wc_queued_webhooks ) ) { - $wc_queued_webhooks = array(); - } - $wc_queued_webhooks[] = array( - 'webhook' => $webhook, - 'arg' => $arg, - ); -} -add_action( 'woocommerce_webhook_process_delivery', 'wc_webhook_process_delivery', 10, 2 ); - -/** - * Wrapper function to execute the `woocommerce_deliver_webhook_async` cron. - * hook, see WC_Webhook::process(). - * - * @since 2.2.0 - * @param int $webhook_id Webhook ID to deliver. - * @throws Exception If webhook cannot be read/found and $data parameter of WC_Webhook class constructor is set. - * @param mixed $arg Hook argument. - */ -function wc_deliver_webhook_async( $webhook_id, $arg ) { - $webhook = new WC_Webhook( $webhook_id ); - $webhook->deliver( $arg ); -} -add_action( 'woocommerce_deliver_webhook_async', 'wc_deliver_webhook_async', 10, 2 ); - -/** - * Check if the given topic is a valid webhook topic, a topic is valid if: - * - * + starts with `action.woocommerce_` or `action.wc_`. - * + it has a valid resource & event. - * - * @since 2.2.0 - * @param string $topic Webhook topic. - * @return bool - */ -function wc_is_webhook_valid_topic( $topic ) { - $invalid_topics = array( - 'action.woocommerce_login_credentials', - 'action.woocommerce_product_csv_importer_check_import_file_path', - 'action.woocommerce_webhook_should_deliver', - ); - - if ( in_array( $topic, $invalid_topics, true ) ) { - return false; - } - - // Custom topics are prefixed with woocommerce_ or wc_ are valid. - if ( 0 === strpos( $topic, 'action.woocommerce_' ) || 0 === strpos( $topic, 'action.wc_' ) ) { - return true; - } - - $data = explode( '.', $topic ); - - if ( ! isset( $data[0] ) || ! isset( $data[1] ) ) { - return false; - } - - $valid_resources = apply_filters( 'woocommerce_valid_webhook_resources', array( 'coupon', 'customer', 'order', 'product' ) ); - $valid_events = apply_filters( 'woocommerce_valid_webhook_events', array( 'created', 'updated', 'deleted', 'restored' ) ); - - if ( in_array( $data[0], $valid_resources, true ) && in_array( $data[1], $valid_events, true ) ) { - return true; - } - - return false; -} - -/** - * Check if given status is a valid webhook status. - * - * @since 3.5.3 - * @param string $status Status to check. - * @return bool - */ -function wc_is_webhook_valid_status( $status ) { - return in_array( $status, array_keys( wc_get_webhook_statuses() ), true ); -} - -/** - * Get Webhook statuses. - * - * @since 2.3.0 - * @return array - */ -function wc_get_webhook_statuses() { - return apply_filters( - 'woocommerce_webhook_statuses', - array( - 'active' => __( 'Active', 'woocommerce' ), - 'paused' => __( 'Paused', 'woocommerce' ), - 'disabled' => __( 'Disabled', 'woocommerce' ), - ) - ); -} - -/** - * Load webhooks. - * - * @since 3.3.0 - * @throws Exception If webhook cannot be read/found and $data parameter of WC_Webhook class constructor is set. - * @param string $status Optional - status to filter results by. Must be a key in return value of @see wc_get_webhook_statuses(). @since 3.5.0. - * @param null|int $limit Limit number of webhooks loaded. @since 3.6.0. - * @return bool - */ -function wc_load_webhooks( $status = '', $limit = null ) { - $data_store = WC_Data_Store::load( 'webhook' ); - $webhooks = $data_store->get_webhooks_ids( $status ); - $loaded = 0; - - foreach ( $webhooks as $webhook_id ) { - $webhook = new WC_Webhook( $webhook_id ); - $webhook->enqueue(); - $loaded ++; - - if ( ! is_null( $limit ) && $loaded >= $limit ) { - break; - } - } - - return 0 < $loaded; -} - -/** - * Get webhook. - * - * @param int|WC_Webhook $id Webhook ID or object. - * @throws Exception If webhook cannot be read/found and $data parameter of WC_Webhook class constructor is set. - * @return WC_Webhook|null - */ -function wc_get_webhook( $id ) { - $webhook = new WC_Webhook( $id ); - - return 0 !== $webhook->get_id() ? $webhook : null; -} - -/** - * Get webhoook REST API versions. - * - * @since 3.5.1 - * @return array - */ -function wc_get_webhook_rest_api_versions() { - return array( - 'wp_api_v1', - 'wp_api_v2', - 'wp_api_v3', - ); -} diff --git a/includes/widgets/class-wc-widget-layered-nav.php b/includes/widgets/class-wc-widget-layered-nav.php deleted file mode 100644 index 1ad76f4d633..00000000000 --- a/includes/widgets/class-wc-widget-layered-nav.php +++ /dev/null @@ -1,532 +0,0 @@ -widget_cssclass = 'woocommerce widget_layered_nav woocommerce-widget-layered-nav'; - $this->widget_description = __( 'Display a list of attributes to filter products in your store.', 'woocommerce' ); - $this->widget_id = 'woocommerce_layered_nav'; - $this->widget_name = __( 'Filter Products by Attribute', 'woocommerce' ); - parent::__construct(); - } - - /** - * Updates a particular instance of a widget. - * - * @see WP_Widget->update - * - * @param array $new_instance New Instance. - * @param array $old_instance Old Instance. - * - * @return array - */ - public function update( $new_instance, $old_instance ) { - $this->init_settings(); - return parent::update( $new_instance, $old_instance ); - } - - /** - * Outputs the settings update form. - * - * @see WP_Widget->form - * - * @param array $instance Instance. - */ - public function form( $instance ) { - $this->init_settings(); - parent::form( $instance ); - } - - /** - * Init settings after post types are registered. - */ - public function init_settings() { - $attribute_array = array(); - $std_attribute = ''; - $attribute_taxonomies = wc_get_attribute_taxonomies(); - - if ( ! empty( $attribute_taxonomies ) ) { - foreach ( $attribute_taxonomies as $tax ) { - if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) { - $attribute_array[ $tax->attribute_name ] = $tax->attribute_name; - } - } - $std_attribute = current( $attribute_array ); - } - - $this->settings = array( - 'title' => array( - 'type' => 'text', - 'std' => __( 'Filter by', 'woocommerce' ), - 'label' => __( 'Title', 'woocommerce' ), - ), - 'attribute' => array( - 'type' => 'select', - 'std' => $std_attribute, - 'label' => __( 'Attribute', 'woocommerce' ), - 'options' => $attribute_array, - ), - 'display_type' => array( - 'type' => 'select', - 'std' => 'list', - 'label' => __( 'Display type', 'woocommerce' ), - 'options' => array( - 'list' => __( 'List', 'woocommerce' ), - 'dropdown' => __( 'Dropdown', 'woocommerce' ), - ), - ), - 'query_type' => array( - 'type' => 'select', - 'std' => 'and', - 'label' => __( 'Query type', 'woocommerce' ), - 'options' => array( - 'and' => __( 'AND', 'woocommerce' ), - 'or' => __( 'OR', 'woocommerce' ), - ), - ), - ); - } - - /** - * Get this widgets taxonomy. - * - * @param array $instance Array of instance options. - * @return string - */ - protected function get_instance_taxonomy( $instance ) { - if ( isset( $instance['attribute'] ) ) { - return wc_attribute_taxonomy_name( $instance['attribute'] ); - } - - $attribute_taxonomies = wc_get_attribute_taxonomies(); - - if ( ! empty( $attribute_taxonomies ) ) { - foreach ( $attribute_taxonomies as $tax ) { - if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) { - return wc_attribute_taxonomy_name( $tax->attribute_name ); - } - } - } - - return ''; - } - - /** - * Get this widgets query type. - * - * @param array $instance Array of instance options. - * @return string - */ - protected function get_instance_query_type( $instance ) { - return isset( $instance['query_type'] ) ? $instance['query_type'] : 'and'; - } - - /** - * Get this widgets display type. - * - * @param array $instance Array of instance options. - * @return string - */ - protected function get_instance_display_type( $instance ) { - return isset( $instance['display_type'] ) ? $instance['display_type'] : 'list'; - } - - /** - * Output widget. - * - * @see WP_Widget - * - * @param array $args Arguments. - * @param array $instance Instance. - */ - public function widget( $args, $instance ) { - if ( ! is_shop() && ! is_product_taxonomy() ) { - return; - } - - $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes(); - $taxonomy = $this->get_instance_taxonomy( $instance ); - $query_type = $this->get_instance_query_type( $instance ); - $display_type = $this->get_instance_display_type( $instance ); - - if ( ! taxonomy_exists( $taxonomy ) ) { - return; - } - - $terms = get_terms( $taxonomy, array( 'hide_empty' => '1' ) ); - - if ( 0 === count( $terms ) ) { - return; - } - - ob_start(); - - $this->widget_start( $args, $instance ); - - if ( 'dropdown' === $display_type ) { - wp_enqueue_script( 'selectWoo' ); - wp_enqueue_style( 'select2' ); - $found = $this->layered_nav_dropdown( $terms, $taxonomy, $query_type ); - } else { - $found = $this->layered_nav_list( $terms, $taxonomy, $query_type ); - } - - $this->widget_end( $args ); - - // Force found when option is selected - do not force found on taxonomy attributes. - if ( ! is_tax() && is_array( $_chosen_attributes ) && array_key_exists( $taxonomy, $_chosen_attributes ) ) { - $found = true; - } - - if ( ! $found ) { - ob_end_clean(); - } else { - echo ob_get_clean(); // @codingStandardsIgnoreLine - } - } - - /** - * Return the currently viewed taxonomy name. - * - * @return string - */ - protected function get_current_taxonomy() { - return is_tax() ? get_queried_object()->taxonomy : ''; - } - - /** - * Return the currently viewed term ID. - * - * @return int - */ - protected function get_current_term_id() { - return absint( is_tax() ? get_queried_object()->term_id : 0 ); - } - - /** - * Return the currently viewed term slug. - * - * @return int - */ - protected function get_current_term_slug() { - return absint( is_tax() ? get_queried_object()->slug : 0 ); - } - - /** - * Show dropdown layered nav. - * - * @param array $terms Terms. - * @param string $taxonomy Taxonomy. - * @param string $query_type Query Type. - * @return bool Will nav display? - */ - protected function layered_nav_dropdown( $terms, $taxonomy, $query_type ) { - global $wp; - $found = false; - - if ( $taxonomy !== $this->get_current_taxonomy() ) { - $term_counts = $this->get_filtered_term_product_counts( wp_list_pluck( $terms, 'term_id' ), $taxonomy, $query_type ); - $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes(); - $taxonomy_filter_name = wc_attribute_taxonomy_slug( $taxonomy ); - $taxonomy_label = wc_attribute_label( $taxonomy ); - - /* translators: %s: taxonomy name */ - $any_label = apply_filters( 'woocommerce_layered_nav_any_label', sprintf( __( 'Any %s', 'woocommerce' ), $taxonomy_label ), $taxonomy_label, $taxonomy ); - $multiple = 'or' === $query_type; - $current_values = isset( $_chosen_attributes[ $taxonomy ]['terms'] ) ? $_chosen_attributes[ $taxonomy ]['terms'] : array(); - - if ( '' === get_option( 'permalink_structure' ) ) { - $form_action = remove_query_arg( array( 'page', 'paged' ), add_query_arg( $wp->query_string, '', home_url( $wp->request ) ) ); - } else { - $form_action = preg_replace( '%\/page/[0-9]+%', '', home_url( trailingslashit( $wp->request ) ) ); - } - - echo '
    '; - echo ''; - - if ( $multiple ) { - echo ''; - } - - if ( 'or' === $query_type ) { - echo ''; - } - - echo ''; - echo wc_query_string_form_fields( null, array( 'filter_' . $taxonomy_filter_name, 'query_type_' . $taxonomy_filter_name ), '', true ); // @codingStandardsIgnoreLine - echo '
    '; - - wc_enqueue_js( - " - // Update value on change. - jQuery( '.dropdown_layered_nav_" . esc_js( $taxonomy_filter_name ) . "' ).change( function() { - var slug = jQuery( this ).val(); - jQuery( ':input[name=\"filter_" . esc_js( $taxonomy_filter_name ) . "\"]' ).val( slug ); - - // Submit form on change if standard dropdown. - if ( ! jQuery( this ).attr( 'multiple' ) ) { - jQuery( this ).closest( 'form' ).submit(); - } - }); - - // Use Select2 enhancement if possible - if ( jQuery().selectWoo ) { - var wc_layered_nav_select = function() { - jQuery( '.dropdown_layered_nav_" . esc_js( $taxonomy_filter_name ) . "' ).selectWoo( { - placeholder: decodeURIComponent('" . rawurlencode( (string) wp_specialchars_decode( $any_label ) ) . "'), - minimumResultsForSearch: 5, - width: '100%', - allowClear: " . ( $multiple ? 'false' : 'true' ) . ", - language: { - noResults: function() { - return '" . esc_js( _x( 'No matches found', 'enhanced select', 'woocommerce' ) ) . "'; - } - } - } ); - }; - wc_layered_nav_select(); - } - " - ); - } - - return $found; - } - - /** - * Count products within certain terms, taking the main WP query into consideration. - * - * This query allows counts to be generated based on the viewed products, not all products. - * - * @param array $term_ids Term IDs. - * @param string $taxonomy Taxonomy. - * @param string $query_type Query Type. - * @return array - */ - protected function get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type ) { - global $wpdb; - - $tax_query = $this->get_main_tax_query(); - $meta_query = $this->get_main_meta_query(); - - if ( 'or' === $query_type ) { - foreach ( $tax_query as $key => $query ) { - if ( is_array( $query ) && $taxonomy === $query['taxonomy'] ) { - unset( $tax_query[ $key ] ); - } - } - } - - $meta_query = new WP_Meta_Query( $meta_query ); - $tax_query = new WP_Tax_Query( $tax_query ); - $meta_query_sql = $meta_query->get_sql( 'post', $wpdb->posts, 'ID' ); - $tax_query_sql = $tax_query->get_sql( $wpdb->posts, 'ID' ); - $term_ids_sql = '(' . implode( ',', array_map( 'absint', $term_ids ) ) . ')'; - - // Generate query. - $query = array(); - $query['select'] = "SELECT COUNT( DISTINCT {$wpdb->posts}.ID ) AS term_count, terms.term_id AS term_count_id"; - $query['from'] = "FROM {$wpdb->posts}"; - $query['join'] = " - INNER JOIN {$wpdb->term_relationships} AS term_relationships ON {$wpdb->posts}.ID = term_relationships.object_id - INNER JOIN {$wpdb->term_taxonomy} AS term_taxonomy USING( term_taxonomy_id ) - INNER JOIN {$wpdb->terms} AS terms USING( term_id ) - " . $tax_query_sql['join'] . $meta_query_sql['join']; - - $query['where'] = " - WHERE {$wpdb->posts}.post_type IN ( 'product' ) - AND {$wpdb->posts}.post_status = 'publish' - {$tax_query_sql['where']} {$meta_query_sql['where']} - AND terms.term_id IN $term_ids_sql"; - - $search = $this->get_main_search_query_sql(); - if ( $search ) { - $query['where'] .= ' AND ' . $search; - } - - $query['group_by'] = 'GROUP BY terms.term_id'; - $query = apply_filters( 'woocommerce_get_filtered_term_product_counts_query', $query ); - $query_sql = implode( ' ', $query ); - - // We have a query - let's see if cached results of this query already exist. - $query_hash = md5( $query_sql ); - - // Maybe store a transient of the count values. - $cache = apply_filters( 'woocommerce_layered_nav_count_maybe_cache', true ); - if ( true === $cache ) { - $cached_counts = (array) get_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ) ); - } else { - $cached_counts = array(); - } - - if ( ! isset( $cached_counts[ $query_hash ] ) ) { - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $results = $wpdb->get_results( $query_sql, ARRAY_A ); - $counts = array_map( 'absint', wp_list_pluck( $results, 'term_count', 'term_count_id' ) ); - $cached_counts[ $query_hash ] = $counts; - if ( true === $cache ) { - set_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ), $cached_counts, DAY_IN_SECONDS ); - } - } - - return array_map( 'absint', (array) $cached_counts[ $query_hash ] ); - } - - /** - * Wrapper for WC_Query::get_main_tax_query() to ease unit testing. - * - * @since 4.4.0 - * @return array - */ - protected function get_main_tax_query() { - return WC_Query::get_main_tax_query(); - } - - /** - * Wrapper for WC_Query::get_main_search_query_sql() to ease unit testing. - * - * @since 4.4.0 - * @return string - */ - protected function get_main_search_query_sql() { - return WC_Query::get_main_search_query_sql(); - } - - /** - * Wrapper for WC_Query::get_main_search_queryget_main_meta_query to ease unit testing. - * - * @since 4.4.0 - * @return array - */ - protected function get_main_meta_query() { - return WC_Query::get_main_meta_query(); - } - - /** - * Show list based layered nav. - * - * @param array $terms Terms. - * @param string $taxonomy Taxonomy. - * @param string $query_type Query Type. - * @return bool Will nav display? - */ - protected function layered_nav_list( $terms, $taxonomy, $query_type ) { - // List display. - echo '
      '; - - $term_counts = $this->get_filtered_term_product_counts( wp_list_pluck( $terms, 'term_id' ), $taxonomy, $query_type ); - $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes(); - $found = false; - $base_link = $this->get_current_page_url(); - - foreach ( $terms as $term ) { - $current_values = isset( $_chosen_attributes[ $taxonomy ]['terms'] ) ? $_chosen_attributes[ $taxonomy ]['terms'] : array(); - $option_is_set = in_array( $term->slug, $current_values, true ); - $count = isset( $term_counts[ $term->term_id ] ) ? $term_counts[ $term->term_id ] : 0; - - // Skip the term for the current archive. - if ( $this->get_current_term_id() === $term->term_id ) { - continue; - } - - // Only show options with count > 0. - if ( 0 < $count ) { - $found = true; - } elseif ( 0 === $count && ! $option_is_set ) { - continue; - } - - $filter_name = 'filter_' . wc_attribute_taxonomy_slug( $taxonomy ); - // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $current_filter = isset( $_GET[ $filter_name ] ) ? explode( ',', wc_clean( wp_unslash( $_GET[ $filter_name ] ) ) ) : array(); - $current_filter = array_map( 'sanitize_title', $current_filter ); - - if ( ! in_array( $term->slug, $current_filter, true ) ) { - $current_filter[] = $term->slug; - } - - $link = remove_query_arg( $filter_name, $base_link ); - - // Add current filters to URL. - foreach ( $current_filter as $key => $value ) { - // Exclude query arg for current term archive term. - if ( $value === $this->get_current_term_slug() ) { - unset( $current_filter[ $key ] ); - } - - // Exclude self so filter can be unset on click. - if ( $option_is_set && $value === $term->slug ) { - unset( $current_filter[ $key ] ); - } - } - - if ( ! empty( $current_filter ) ) { - asort( $current_filter ); - $link = add_query_arg( $filter_name, implode( ',', $current_filter ), $link ); - - // Add Query type Arg to URL. - if ( 'or' === $query_type && ! ( 1 === count( $current_filter ) && $option_is_set ) ) { - $link = add_query_arg( 'query_type_' . wc_attribute_taxonomy_slug( $taxonomy ), 'or', $link ); - } - $link = str_replace( '%2C', ',', $link ); - } - - if ( $count > 0 || $option_is_set ) { - $link = apply_filters( 'woocommerce_layered_nav_link', $link, $term, $taxonomy ); - $term_html = '' . esc_html( $term->name ) . ''; - } else { - $link = false; - $term_html = '' . esc_html( $term->name ) . ''; - } - - $term_html .= ' ' . apply_filters( 'woocommerce_layered_nav_count', '(' . absint( $count ) . ')', $count, $term ); - - echo '
    • '; - // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.EscapeOutput.OutputNotEscaped - echo apply_filters( 'woocommerce_layered_nav_term_html', $term_html, $term, $link, $count ); - echo '
    • '; - } - - echo '
    '; - - return $found; - } -} diff --git a/includes/widgets/class-wc-widget-product-categories.php b/includes/widgets/class-wc-widget-product-categories.php deleted file mode 100644 index 26b590bcdf7..00000000000 --- a/includes/widgets/class-wc-widget-product-categories.php +++ /dev/null @@ -1,301 +0,0 @@ -widget_cssclass = 'woocommerce widget_product_categories'; - $this->widget_description = __( 'A list or dropdown of product categories.', 'woocommerce' ); - $this->widget_id = 'woocommerce_product_categories'; - $this->widget_name = __( 'Product Categories', 'woocommerce' ); - $this->settings = array( - 'title' => array( - 'type' => 'text', - 'std' => __( 'Product categories', 'woocommerce' ), - 'label' => __( 'Title', 'woocommerce' ), - ), - 'orderby' => array( - 'type' => 'select', - 'std' => 'name', - 'label' => __( 'Order by', 'woocommerce' ), - 'options' => array( - 'order' => __( 'Category order', 'woocommerce' ), - 'name' => __( 'Name', 'woocommerce' ), - ), - ), - 'dropdown' => array( - 'type' => 'checkbox', - 'std' => 0, - 'label' => __( 'Show as dropdown', 'woocommerce' ), - ), - 'count' => array( - 'type' => 'checkbox', - 'std' => 0, - 'label' => __( 'Show product counts', 'woocommerce' ), - ), - 'hierarchical' => array( - 'type' => 'checkbox', - 'std' => 1, - 'label' => __( 'Show hierarchy', 'woocommerce' ), - ), - 'show_children_only' => array( - 'type' => 'checkbox', - 'std' => 0, - 'label' => __( 'Only show children of the current category', 'woocommerce' ), - ), - 'hide_empty' => array( - 'type' => 'checkbox', - 'std' => 0, - 'label' => __( 'Hide empty categories', 'woocommerce' ), - ), - 'max_depth' => array( - 'type' => 'text', - 'std' => '', - 'label' => __( 'Maximum depth', 'woocommerce' ), - ), - ); - - parent::__construct(); - } - - /** - * Output widget. - * - * @see WP_Widget - * @param array $args Widget arguments. - * @param array $instance Widget instance. - */ - public function widget( $args, $instance ) { - global $wp_query, $post; - - $count = isset( $instance['count'] ) ? $instance['count'] : $this->settings['count']['std']; - $hierarchical = isset( $instance['hierarchical'] ) ? $instance['hierarchical'] : $this->settings['hierarchical']['std']; - $show_children_only = isset( $instance['show_children_only'] ) ? $instance['show_children_only'] : $this->settings['show_children_only']['std']; - $dropdown = isset( $instance['dropdown'] ) ? $instance['dropdown'] : $this->settings['dropdown']['std']; - $orderby = isset( $instance['orderby'] ) ? $instance['orderby'] : $this->settings['orderby']['std']; - $hide_empty = isset( $instance['hide_empty'] ) ? $instance['hide_empty'] : $this->settings['hide_empty']['std']; - $dropdown_args = array( - 'hide_empty' => $hide_empty, - ); - $list_args = array( - 'show_count' => $count, - 'hierarchical' => $hierarchical, - 'taxonomy' => 'product_cat', - 'hide_empty' => $hide_empty, - ); - $max_depth = absint( isset( $instance['max_depth'] ) ? $instance['max_depth'] : $this->settings['max_depth']['std'] ); - - $list_args['menu_order'] = false; - $dropdown_args['depth'] = $max_depth; - $list_args['depth'] = $max_depth; - - if ( 'order' === $orderby ) { - $list_args['orderby'] = 'meta_value_num'; - $dropdown_args['orderby'] = 'meta_value_num'; - $list_args['meta_key'] = 'order'; - $dropdown_args['meta_key'] = 'order'; - } - - $this->current_cat = false; - $this->cat_ancestors = array(); - - if ( is_tax( 'product_cat' ) ) { - $this->current_cat = $wp_query->queried_object; - $this->cat_ancestors = get_ancestors( $this->current_cat->term_id, 'product_cat' ); - - } elseif ( is_singular( 'product' ) ) { - $terms = wc_get_product_terms( - $post->ID, - 'product_cat', - apply_filters( - 'woocommerce_product_categories_widget_product_terms_args', - array( - 'orderby' => 'parent', - 'order' => 'DESC', - ) - ) - ); - - if ( $terms ) { - $main_term = apply_filters( 'woocommerce_product_categories_widget_main_term', $terms[0], $terms ); - $this->current_cat = $main_term; - $this->cat_ancestors = get_ancestors( $main_term->term_id, 'product_cat' ); - } - } - - // Show Siblings and Children Only. - if ( $show_children_only && $this->current_cat ) { - if ( $hierarchical ) { - $include = array_merge( - $this->cat_ancestors, - array( $this->current_cat->term_id ), - get_terms( - 'product_cat', - array( - 'fields' => 'ids', - 'parent' => 0, - 'hierarchical' => true, - 'hide_empty' => false, - ) - ), - get_terms( - 'product_cat', - array( - 'fields' => 'ids', - 'parent' => $this->current_cat->term_id, - 'hierarchical' => true, - 'hide_empty' => false, - ) - ) - ); - // Gather siblings of ancestors. - if ( $this->cat_ancestors ) { - foreach ( $this->cat_ancestors as $ancestor ) { - $include = array_merge( - $include, - get_terms( - 'product_cat', - array( - 'fields' => 'ids', - 'parent' => $ancestor, - 'hierarchical' => false, - 'hide_empty' => false, - ) - ) - ); - } - } - } else { - // Direct children. - $include = get_terms( - 'product_cat', - array( - 'fields' => 'ids', - 'parent' => $this->current_cat->term_id, - 'hierarchical' => true, - 'hide_empty' => false, - ) - ); - } - - $list_args['include'] = implode( ',', $include ); - $dropdown_args['include'] = $list_args['include']; - - if ( empty( $include ) ) { - return; - } - } elseif ( $show_children_only ) { - $dropdown_args['depth'] = 1; - $dropdown_args['child_of'] = 0; - $dropdown_args['hierarchical'] = 1; - $list_args['depth'] = 1; - $list_args['child_of'] = 0; - $list_args['hierarchical'] = 1; - } - - $this->widget_start( $args, $instance ); - - if ( $dropdown ) { - wc_product_dropdown_categories( - apply_filters( - 'woocommerce_product_categories_widget_dropdown_args', - wp_parse_args( - $dropdown_args, - array( - 'show_count' => $count, - 'hierarchical' => $hierarchical, - 'show_uncategorized' => 0, - 'selected' => $this->current_cat ? $this->current_cat->slug : '', - ) - ) - ) - ); - - wp_enqueue_script( 'selectWoo' ); - wp_enqueue_style( 'select2' ); - - wc_enqueue_js( - " - jQuery( '.dropdown_product_cat' ).change( function() { - if ( jQuery(this).val() != '' ) { - var this_page = ''; - var home_url = '" . esc_js( home_url( '/' ) ) . "'; - if ( home_url.indexOf( '?' ) > 0 ) { - this_page = home_url + '&product_cat=' + jQuery(this).val(); - } else { - this_page = home_url + '?product_cat=' + jQuery(this).val(); - } - location.href = this_page; - } else { - location.href = '" . esc_js( wc_get_page_permalink( 'shop' ) ) . "'; - } - }); - - if ( jQuery().selectWoo ) { - var wc_product_cat_select = function() { - jQuery( '.dropdown_product_cat' ).selectWoo( { - placeholder: '" . esc_js( __( 'Select a category', 'woocommerce' ) ) . "', - minimumResultsForSearch: 5, - width: '100%', - allowClear: true, - language: { - noResults: function() { - return '" . esc_js( _x( 'No matches found', 'enhanced select', 'woocommerce' ) ) . "'; - } - } - } ); - }; - wc_product_cat_select(); - } - " - ); - } else { - include_once WC()->plugin_path() . '/includes/walkers/class-wc-product-cat-list-walker.php'; - - $list_args['walker'] = new WC_Product_Cat_List_Walker(); - $list_args['title_li'] = ''; - $list_args['pad_counts'] = 1; - $list_args['show_option_none'] = __( 'No product categories exist.', 'woocommerce' ); - $list_args['current_category'] = ( $this->current_cat ) ? $this->current_cat->term_id : ''; - $list_args['current_category_ancestors'] = $this->cat_ancestors; - $list_args['max_depth'] = $max_depth; - - echo '
      '; - - wp_list_categories( apply_filters( 'woocommerce_product_categories_widget_args', $list_args ) ); - - echo '
    '; - } - - $this->widget_end( $args ); - } -} diff --git a/includes/widgets/class-wc-widget-products.php b/includes/widgets/class-wc-widget-products.php deleted file mode 100644 index be373488920..00000000000 --- a/includes/widgets/class-wc-widget-products.php +++ /dev/null @@ -1,214 +0,0 @@ -widget_cssclass = 'woocommerce widget_products'; - $this->widget_description = __( "A list of your store's products.", 'woocommerce' ); - $this->widget_id = 'woocommerce_products'; - $this->widget_name = __( 'Products', 'woocommerce' ); - $this->settings = array( - 'title' => array( - 'type' => 'text', - 'std' => __( 'Products', 'woocommerce' ), - 'label' => __( 'Title', 'woocommerce' ), - ), - 'number' => array( - 'type' => 'number', - 'step' => 1, - 'min' => 1, - 'max' => '', - 'std' => 5, - 'label' => __( 'Number of products to show', 'woocommerce' ), - ), - 'show' => array( - 'type' => 'select', - 'std' => '', - 'label' => __( 'Show', 'woocommerce' ), - 'options' => array( - '' => __( 'All products', 'woocommerce' ), - 'featured' => __( 'Featured products', 'woocommerce' ), - 'onsale' => __( 'On-sale products', 'woocommerce' ), - ), - ), - 'orderby' => array( - 'type' => 'select', - 'std' => 'date', - 'label' => __( 'Order by', 'woocommerce' ), - 'options' => array( - 'date' => __( 'Date', 'woocommerce' ), - 'price' => __( 'Price', 'woocommerce' ), - 'rand' => __( 'Random', 'woocommerce' ), - 'sales' => __( 'Sales', 'woocommerce' ), - ), - ), - 'order' => array( - 'type' => 'select', - 'std' => 'desc', - 'label' => _x( 'Order', 'Sorting order', 'woocommerce' ), - 'options' => array( - 'asc' => __( 'ASC', 'woocommerce' ), - 'desc' => __( 'DESC', 'woocommerce' ), - ), - ), - 'hide_free' => array( - 'type' => 'checkbox', - 'std' => 0, - 'label' => __( 'Hide free products', 'woocommerce' ), - ), - 'show_hidden' => array( - 'type' => 'checkbox', - 'std' => 0, - 'label' => __( 'Show hidden products', 'woocommerce' ), - ), - ); - - parent::__construct(); - } - - /** - * Query the products and return them. - * - * @param array $args Arguments. - * @param array $instance Widget instance. - * - * @return WP_Query - */ - public function get_products( $args, $instance ) { - $number = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : $this->settings['number']['std']; - $show = ! empty( $instance['show'] ) ? sanitize_title( $instance['show'] ) : $this->settings['show']['std']; - $orderby = ! empty( $instance['orderby'] ) ? sanitize_title( $instance['orderby'] ) : $this->settings['orderby']['std']; - $order = ! empty( $instance['order'] ) ? sanitize_title( $instance['order'] ) : $this->settings['order']['std']; - $product_visibility_term_ids = wc_get_product_visibility_term_ids(); - - $query_args = array( - 'posts_per_page' => $number, - 'post_status' => 'publish', - 'post_type' => 'product', - 'no_found_rows' => 1, - 'order' => $order, - 'meta_query' => array(), - 'tax_query' => array( - 'relation' => 'AND', - ), - ); // WPCS: slow query ok. - - if ( empty( $instance['show_hidden'] ) ) { - $query_args['tax_query'][] = array( - 'taxonomy' => 'product_visibility', - 'field' => 'term_taxonomy_id', - 'terms' => is_search() ? $product_visibility_term_ids['exclude-from-search'] : $product_visibility_term_ids['exclude-from-catalog'], - 'operator' => 'NOT IN', - ); - $query_args['post_parent'] = 0; - } - - if ( ! empty( $instance['hide_free'] ) ) { - $query_args['meta_query'][] = array( - 'key' => '_price', - 'value' => 0, - 'compare' => '>', - 'type' => 'DECIMAL', - ); - } - - if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { - $query_args['tax_query'][] = array( - array( - 'taxonomy' => 'product_visibility', - 'field' => 'term_taxonomy_id', - 'terms' => $product_visibility_term_ids['outofstock'], - 'operator' => 'NOT IN', - ), - ); // WPCS: slow query ok. - } - - switch ( $show ) { - case 'featured': - $query_args['tax_query'][] = array( - 'taxonomy' => 'product_visibility', - 'field' => 'term_taxonomy_id', - 'terms' => $product_visibility_term_ids['featured'], - ); - break; - case 'onsale': - $product_ids_on_sale = wc_get_product_ids_on_sale(); - $product_ids_on_sale[] = 0; - $query_args['post__in'] = $product_ids_on_sale; - break; - } - - switch ( $orderby ) { - case 'price': - $query_args['meta_key'] = '_price'; // WPCS: slow query ok. - $query_args['orderby'] = 'meta_value_num'; - break; - case 'rand': - $query_args['orderby'] = 'rand'; - break; - case 'sales': - $query_args['meta_key'] = 'total_sales'; // WPCS: slow query ok. - $query_args['orderby'] = 'meta_value_num'; - break; - default: - $query_args['orderby'] = 'date'; - } - - return new WP_Query( apply_filters( 'woocommerce_products_widget_query_args', $query_args ) ); - } - - /** - * Output widget. - * - * @param array $args Arguments. - * @param array $instance Widget instance. - * - * @see WP_Widget - */ - public function widget( $args, $instance ) { - if ( $this->get_cached_widget( $args ) ) { - return; - } - - ob_start(); - - $products = $this->get_products( $args, $instance ); - if ( $products && $products->have_posts() ) { - $this->widget_start( $args, $instance ); - - echo wp_kses_post( apply_filters( 'woocommerce_before_widget_product_list', '
      ' ) ); - - $template_args = array( - 'widget_id' => isset( $args['widget_id'] ) ? $args['widget_id'] : $this->widget_id, - 'show_rating' => true, - ); - - while ( $products->have_posts() ) { - $products->the_post(); - wc_get_template( 'content-widget-product.php', $template_args ); - } - - echo wp_kses_post( apply_filters( 'woocommerce_after_widget_product_list', '
    ' ) ); - - $this->widget_end( $args ); - } - - wp_reset_postdata(); - - echo $this->cache_widget( $args, ob_get_clean() ); // WPCS: XSS ok. - } -} diff --git a/includes/widgets/class-wc-widget-recently-viewed.php b/includes/widgets/class-wc-widget-recently-viewed.php deleted file mode 100644 index e023927f6aa..00000000000 --- a/includes/widgets/class-wc-widget-recently-viewed.php +++ /dev/null @@ -1,110 +0,0 @@ -widget_cssclass = 'woocommerce widget_recently_viewed_products'; - $this->widget_description = __( "Display a list of a customer's recently viewed products.", 'woocommerce' ); - $this->widget_id = 'woocommerce_recently_viewed_products'; - $this->widget_name = __( 'Recent Viewed Products', 'woocommerce' ); - $this->settings = array( - 'title' => array( - 'type' => 'text', - 'std' => __( 'Recently Viewed Products', 'woocommerce' ), - 'label' => __( 'Title', 'woocommerce' ), - ), - 'number' => array( - 'type' => 'number', - 'step' => 1, - 'min' => 1, - 'max' => 15, - 'std' => 10, - 'label' => __( 'Number of products to show', 'woocommerce' ), - ), - ); - - parent::__construct(); - } - - /** - * Output widget. - * - * @see WP_Widget - * @param array $args Arguments. - * @param array $instance Widget instance. - */ - public function widget( $args, $instance ) { - $viewed_products = ! empty( $_COOKIE['woocommerce_recently_viewed'] ) ? (array) explode( '|', wp_unslash( $_COOKIE['woocommerce_recently_viewed'] ) ) : array(); // @codingStandardsIgnoreLine - $viewed_products = array_reverse( array_filter( array_map( 'absint', $viewed_products ) ) ); - - if ( empty( $viewed_products ) ) { - return; - } - - ob_start(); - - $number = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : $this->settings['number']['std']; - - $query_args = array( - 'posts_per_page' => $number, - 'no_found_rows' => 1, - 'post_status' => 'publish', - 'post_type' => 'product', - 'post__in' => $viewed_products, - 'orderby' => 'post__in', - ); - - if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { - $query_args['tax_query'] = array( - array( - 'taxonomy' => 'product_visibility', - 'field' => 'name', - 'terms' => 'outofstock', - 'operator' => 'NOT IN', - ), - ); // WPCS: slow query ok. - } - - $r = new WP_Query( apply_filters( 'woocommerce_recently_viewed_products_widget_query_args', $query_args ) ); - - if ( $r->have_posts() ) { - - $this->widget_start( $args, $instance ); - - echo wp_kses_post( apply_filters( 'woocommerce_before_widget_product_list', '
      ' ) ); - - $template_args = array( - 'widget_id' => isset( $args['widget_id'] ) ? $args['widget_id'] : $this->widget_id, - ); - - while ( $r->have_posts() ) { - $r->the_post(); - wc_get_template( 'content-widget-product.php', $template_args ); - } - - echo wp_kses_post( apply_filters( 'woocommerce_after_widget_product_list', '
    ' ) ); - - $this->widget_end( $args ); - } - - wp_reset_postdata(); - - $content = ob_get_clean(); - - echo $content; // WPCS: XSS ok. - } -} diff --git a/includes/widgets/class-wc-widget-top-rated-products.php b/includes/widgets/class-wc-widget-top-rated-products.php deleted file mode 100644 index a5241ece94c..00000000000 --- a/includes/widgets/class-wc-widget-top-rated-products.php +++ /dev/null @@ -1,107 +0,0 @@ -widget_cssclass = 'woocommerce widget_top_rated_products'; - $this->widget_description = __( "A list of your store's top-rated products.", 'woocommerce' ); - $this->widget_id = 'woocommerce_top_rated_products'; - $this->widget_name = __( 'Products by Rating', 'woocommerce' ); - $this->settings = array( - 'title' => array( - 'type' => 'text', - 'std' => __( 'Top rated products', 'woocommerce' ), - 'label' => __( 'Title', 'woocommerce' ), - ), - 'number' => array( - 'type' => 'number', - 'step' => 1, - 'min' => 1, - 'max' => '', - 'std' => 5, - 'label' => __( 'Number of products to show', 'woocommerce' ), - ), - ); - - parent::__construct(); - } - - /** - * Output widget. - * - * @see WP_Widget - * @param array $args Arguments. - * @param array $instance Widget instance. - */ - public function widget( $args, $instance ) { - - if ( $this->get_cached_widget( $args ) ) { - return; - } - - ob_start(); - - $number = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : $this->settings['number']['std']; - - $query_args = apply_filters( - 'woocommerce_top_rated_products_widget_args', - array( - 'posts_per_page' => $number, - 'no_found_rows' => 1, - 'post_status' => 'publish', - 'post_type' => 'product', - 'meta_key' => '_wc_average_rating', - 'orderby' => 'meta_value_num', - 'order' => 'DESC', - 'meta_query' => WC()->query->get_meta_query(), - 'tax_query' => WC()->query->get_tax_query(), - ) - ); // WPCS: slow query ok. - - $r = new WP_Query( $query_args ); - - if ( $r->have_posts() ) { - - $this->widget_start( $args, $instance ); - - echo wp_kses_post( apply_filters( 'woocommerce_before_widget_product_list', '
      ' ) ); - - $template_args = array( - 'widget_id' => isset( $args['widget_id'] ) ? $args['widget_id'] : $this->widget_id, - 'show_rating' => true, - ); - - while ( $r->have_posts() ) { - $r->the_post(); - wc_get_template( 'content-widget-product.php', $template_args ); - } - - echo wp_kses_post( apply_filters( 'woocommerce_after_widget_product_list', '
    ' ) ); - - $this->widget_end( $args ); - } - - wp_reset_postdata(); - - $content = ob_get_clean(); - - echo $content; // WPCS: XSS ok. - - $this->cache_widget( $args, $content ); - } -} diff --git a/lerna.json b/lerna.json deleted file mode 100644 index eb74a66b0dd..00000000000 --- a/lerna.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "command": { - "publish": { - "message": "chore(release): publish" - } - }, - "ignoreChanges": [ "**/CHANGELOG.md", "**/test/**" ], - "packages": [ "tests/e2e/*" ], - "version": "independent" -} diff --git a/lib/composer.json b/lib/composer.json deleted file mode 100644 index 7e9e4481f95..00000000000 --- a/lib/composer.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "woocommerce/woocommerce-lib", - "description": "A package for hiding re-namespaced dependencies and executing them", - "prefer-stable": true, - "minimum-stability": "dev", - "require": { - "php": ">=7.0", - "psr/container": "^1.0" - }, - "require-dev": { - "league/container": "3.3.3" - }, - "config": { - "platform": { - "php": "7.0" - } - }, - "scripts": { - "post-install-cmd": [ - "\"../vendor/bin/mozart\" compose" - ], - "post-update-cmd": [ - "\"../vendor/bin/mozart\" compose" - ] - }, - "extra": { - "mozart": { - "dep_namespace": "Automattic\\WooCommerce\\Vendor\\", - "dep_directory": "/packages/", - "packages": [ - "league/container" - ], - "excluded_packages": [ - "psr/container" - ], - "classmap_directory": "/classes/", - "classmap_prefix": "WC_Vendor_", - "delete_vendor_directories": true - } - } -} diff --git a/lib/composer.lock b/lib/composer.lock deleted file mode 100644 index ac31c819004..00000000000 --- a/lib/composer.lock +++ /dev/null @@ -1,146 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "df548645b5c00d585705cd10c6ffd3f7", - "packages": [ - { - "name": "psr/container", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "time": "2017-02-14T16:28:37+00:00" - } - ], - "packages-dev": [ - { - "name": "league/container", - "version": "3.3.3", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/container.git", - "reference": "7dc67bdf89efc338e674863c0ea70a63efe4de05" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/container/zipball/7dc67bdf89efc338e674863c0ea70a63efe4de05", - "reference": "7dc67bdf89efc338e674863c0ea70a63efe4de05", - "shasum": "" - }, - "require": { - "php": "^7.0 || ^8.0", - "psr/container": "^1.0" - }, - "provide": { - "psr/container-implementation": "^1.0" - }, - "replace": { - "orno/di": "~2.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0", - "squizlabs/php_codesniffer": "^3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-3.x": "3.x-dev", - "dev-2.x": "2.x-dev", - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Container\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Phil Bennett", - "email": "philipobenito@gmail.com", - "homepage": "http://www.philipobenito.com", - "role": "Developer" - } - ], - "description": "A fast and intuitive dependency injection container.", - "homepage": "https://github.com/thephpleague/container", - "keywords": [ - "container", - "dependency", - "di", - "injection", - "league", - "provider", - "service" - ], - "funding": [ - { - "url": "https://github.com/philipobenito", - "type": "github" - } - ], - "time": "2020-09-28T13:38:44+00:00" - } - ], - "aliases": [], - "minimum-stability": "dev", - "stability-flags": [], - "prefer-stable": true, - "prefer-lowest": false, - "platform": { - "php": ">=7.0" - }, - "platform-dev": [], - "platform-overrides": { - "php": "7.0" - }, - "plugin-api-version": "1.1.0" -} diff --git a/nx.json b/nx.json new file mode 100644 index 00000000000..fa1ac820d8b --- /dev/null +++ b/nx.json @@ -0,0 +1,41 @@ +{ + "extends": "@nrwl/workspace/presets/npm.json", + "npmScope": "woocommerce-monorepo", + "tasksRunnerOptions": { + "default": { + "runner": "@nrwl/workspace/tasks-runners/default", + "options": { + "cacheableOperations": [ + "build", + "test", + "lint", + "package", + "prepare" + ] + } + } + }, + "targetDependencies": { + "build": [ + { + "target": "build", + "projects": "dependencies" + } + ], + "prepare": [ + { + "target": "prepare", + "projects": "dependencies" + } + ], + "package": [ + { + "target": "package", + "projects": "dependencies" + } + ] + }, + "affected": { + "defaultBase": "trunk" + } +} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 687029b381d..00000000000 --- a/package-lock.json +++ /dev/null @@ -1,38473 +0,0 @@ -{ - "name": "woocommerce", - "version": "5.1.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/cli": { - "version": "7.12.8", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.12.8.tgz", - "integrity": "sha512-/6nQj11oaGhLmZiuRUfxsujiPDc9BBReemiXgIbxc+M5W+MIiFKYwvNDJvBfnGKNsJTKbUfEheKc9cwoPHAVQA==", - "dev": true, - "requires": { - "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents", - "chokidar": "^3.4.0", - "commander": "^4.0.1", - "convert-source-map": "^1.1.0", - "fs-readdir-recursive": "^1.1.0", - "glob": "^7.0.0", - "lodash": "^4.17.19", - "make-dir": "^2.1.0", - "slash": "^2.0.0", - "source-map": "^0.5.0" - } - }, - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/compat-data": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.12.7.tgz", - "integrity": "sha512-YaxPMGs/XIWtYqrdEOZOCPsVWfEoriXopnsz3/i7apYPXQ3698UFhS6dVT1KN5qOsWmVgw/FOrmQgpRaZayGsw==", - "dev": true - }, - "@babel/core": { - "version": "7.12.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.9.tgz", - "integrity": "sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.12.5", - "@babel/helper-module-transforms": "^7.12.1", - "@babel/helpers": "^7.12.5", - "@babel/parser": "^7.12.7", - "@babel/template": "^7.12.7", - "@babel/traverse": "^7.12.9", - "@babel/types": "^7.12.7", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.1", - "json5": "^2.1.2", - "lodash": "^4.17.19", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "@babel/generator": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz", - "integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==", - "dev": true, - "requires": { - "@babel/types": "^7.12.5", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/parser": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz", - "integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==", - "dev": true - }, - "@babel/template": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", - "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.12.7", - "@babel/types": "^7.12.7" - } - }, - "@babel/traverse": { - "version": "7.12.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.9.tgz", - "integrity": "sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.12.5", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.7", - "@babel/types": "^7.12.7", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" - } - }, - "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.11.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", - "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", - "dev": true, - "requires": { - "@babel/types": "^7.11.5", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", - "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz", - "integrity": "sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg==", - "dev": true, - "requires": { - "@babel/helper-explode-assignable-expression": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-builder-react-jsx": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.4.tgz", - "integrity": "sha512-5nPcIZ7+KKDxT1427oBivl9V9YTal7qk0diccnh7RrcgrT/pGFOjgGw1dgryyx1GvHEpXVfoDF6Ak3rTiWh8Rg==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-builder-react-jsx-experimental": { - "version": "7.11.5", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.11.5.tgz", - "integrity": "sha512-Vc4aPJnRZKWfzeCBsqTBnzulVNjABVdahSPhtdMD3Vs80ykx4a87jTHtF/VR+alSrDmNvat7l13yrRHauGcHVw==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.10.4", - "@babel/helper-module-imports": "^7.10.4", - "@babel/types": "^7.11.5" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.5.tgz", - "integrity": "sha512-+qH6NrscMolUlzOYngSBMIOQpKUGPPsc61Bu5W10mg84LxZ7cmvnBHzARKbDoFxVvqqAbj6Tg6N7bSrWSPXMyw==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.12.5", - "@babel/helper-validator-option": "^7.12.1", - "browserslist": "^4.14.5", - "semver": "^5.5.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "@babel/helper-create-class-features-plugin": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz", - "integrity": "sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-member-expression-to-functions": "^7.12.1", - "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/helper-replace-supers": "^7.12.1", - "@babel/helper-split-export-declaration": "^7.10.4" - } - }, - "@babel/helper-create-regexp-features-plugin": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.7.tgz", - "integrity": "sha512-idnutvQPdpbduutvi3JVfEgcVIHooQnhvhx0Nk9isOINOIGYkZea1Pk2JlJRiUnMefrlvr0vkByATBY/mB4vjQ==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.10.4", - "regexpu-core": "^4.7.1" - } - }, - "@babel/helper-define-map": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz", - "integrity": "sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.10.4", - "@babel/types": "^7.10.5", - "lodash": "^4.17.19" - } - }, - "@babel/helper-explode-assignable-expression": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.12.1.tgz", - "integrity": "sha512-dmUwH8XmlrUpVqgtZ737tK88v07l840z9j3OEhCLwKTkjlvKpfqXVIZ0wpK3aeOxspwGrf/5AP5qLx4rO3w5rA==", - "dev": true, - "requires": { - "@babel/types": "^7.12.1" - }, - "dependencies": { - "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz", - "integrity": "sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", - "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", - "dev": true, - "requires": { - "@babel/types": "^7.12.7" - }, - "dependencies": { - "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-module-imports": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", - "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-module-transforms": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz", - "integrity": "sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.12.1", - "@babel/helper-replace-supers": "^7.12.1", - "@babel/helper-simple-access": "^7.12.1", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/helper-validator-identifier": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/traverse": "^7.12.1", - "@babel/types": "^7.12.1", - "lodash": "^4.17.19" - }, - "dependencies": { - "@babel/generator": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz", - "integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==", - "dev": true, - "requires": { - "@babel/types": "^7.12.5", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-module-imports": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz", - "integrity": "sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==", - "dev": true, - "requires": { - "@babel/types": "^7.12.5" - } - }, - "@babel/parser": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz", - "integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==", - "dev": true - }, - "@babel/traverse": { - "version": "7.12.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.9.tgz", - "integrity": "sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.12.5", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.7", - "@babel/types": "^7.12.7", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" - } - }, - "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.7.tgz", - "integrity": "sha512-I5xc9oSJ2h59OwyUqjv95HRyzxj53DAubUERgQMrpcCEYQyToeHA+NEcUEsVWB4j53RDeskeBJ0SgRAYHDBckw==", - "dev": true, - "requires": { - "@babel/types": "^7.12.7" - }, - "dependencies": { - "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.11.4", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.11.4.tgz", - "integrity": "sha512-tR5vJ/vBa9wFy3m5LLv2faapJLnDFxNWff2SAYkSE4rLUdbp7CdObYFgI7wK4T/Mj4UzpjPwzR8Pzmr5m7MHGA==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.10.4", - "@babel/helper-wrap-function": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-replace-supers": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz", - "integrity": "sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.12.1", - "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/traverse": "^7.12.5", - "@babel/types": "^7.12.5" - }, - "dependencies": { - "@babel/generator": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz", - "integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==", - "dev": true, - "requires": { - "@babel/types": "^7.12.5", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/parser": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz", - "integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==", - "dev": true - }, - "@babel/traverse": { - "version": "7.12.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.9.tgz", - "integrity": "sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.12.5", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.7", - "@babel/types": "^7.12.7", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" - } - }, - "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/helper-simple-access": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz", - "integrity": "sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA==", - "dev": true, - "requires": { - "@babel/types": "^7.12.1" - }, - "dependencies": { - "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz", - "integrity": "sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA==", - "dev": true, - "requires": { - "@babel/types": "^7.12.1" - }, - "dependencies": { - "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", - "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", - "dev": true, - "requires": { - "@babel/types": "^7.11.0" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.1.tgz", - "integrity": "sha512-YpJabsXlJVWP0USHjnC/AQDTLlZERbON577YUVO/wLpqyj6HAtVYnWaQaN0iUN+1/tWn3c+uKKXjRut5115Y2A==", - "dev": true - }, - "@babel/helper-wrap-function": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz", - "integrity": "sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/traverse": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helpers": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.5.tgz", - "integrity": "sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA==", - "dev": true, - "requires": { - "@babel/template": "^7.10.4", - "@babel/traverse": "^7.12.5", - "@babel/types": "^7.12.5" - }, - "dependencies": { - "@babel/generator": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz", - "integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==", - "dev": true, - "requires": { - "@babel/types": "^7.12.5", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/parser": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz", - "integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==", - "dev": true - }, - "@babel/traverse": { - "version": "7.12.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.9.tgz", - "integrity": "sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.12.5", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.7", - "@babel/types": "^7.12.7", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" - } - }, - "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.11.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", - "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", - "dev": true - }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz", - "integrity": "sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-remap-async-to-generator": "^7.10.4", - "@babel/plugin-syntax-async-generators": "^7.8.0" - } - }, - "@babel/plugin-proposal-class-properties": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz", - "integrity": "sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-proposal-dynamic-import": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz", - "integrity": "sha512-a4rhUSZFuq5W8/OO8H7BL5zspjnc1FLd9hlOxIK/f7qG4a0qsqk8uvF/ywgBA8/OmjsapjpvaEOYItfGG1qIvQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-dynamic-import": "^7.8.0" - } - }, - "@babel/plugin-proposal-export-namespace-from": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.1.tgz", - "integrity": "sha512-6CThGf0irEkzujYS5LQcjBx8j/4aQGiVv7J9+2f7pGfxqyKh3WnmVJYW3hdrQjyksErMGBPQrCnHfOtna+WLbw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - } - }, - "@babel/plugin-proposal-json-strings": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.1.tgz", - "integrity": "sha512-GoLDUi6U9ZLzlSda2Df++VSqDJg3CG+dR0+iWsv6XRw1rEq+zwt4DirM9yrxW6XWaTpmai1cWJLMfM8qQJf+yw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.0" - } - }, - "@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.1.tgz", - "integrity": "sha512-k8ZmVv0JU+4gcUGeCDZOGd0lCIamU/sMtIiX3UWnUc5yzgq6YUGyEolNYD+MLYKfSzgECPcqetVcJP9Afe/aCA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - } - }, - "@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.1.tgz", - "integrity": "sha512-nZY0ESiaQDI1y96+jk6VxMOaL4LPo/QDHBqL+SF3/vl6dHkTwHlOI8L4ZwuRBHgakRBw5zsVylel7QPbbGuYgg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" - } - }, - "@babel/plugin-proposal-numeric-separator": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.7.tgz", - "integrity": "sha512-8c+uy0qmnRTeukiGsjLGy6uVs/TFjJchGXUeBqlG4VWYOdJWkhhVPdQ3uHwbmalfJwv2JsV0qffXP4asRfL2SQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - } - }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz", - "integrity": "sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.0", - "@babel/plugin-transform-parameters": "^7.10.4" - } - }, - "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.1.tgz", - "integrity": "sha512-hFvIjgprh9mMw5v42sJWLI1lzU5L2sznP805zeT6rySVRA0Y18StRhDqhSxlap0oVgItRsB6WSROp4YnJTJz0g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" - } - }, - "@babel/plugin-proposal-optional-chaining": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.7.tgz", - "integrity": "sha512-4ovylXZ0PWmwoOvhU2vhnzVNnm88/Sm9nx7V8BPgMvAzn5zDou3/Awy0EjglyubVHasJj+XCEkr/r1X3P5elCA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", - "@babel/plugin-syntax-optional-chaining": "^7.8.0" - } - }, - "@babel/plugin-proposal-private-methods": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.1.tgz", - "integrity": "sha512-mwZ1phvH7/NHK6Kf8LP7MYDogGV+DKB1mryFOEwx5EBNQrosvIczzZFTUmWaeujd5xT6G1ELYWUz3CutMhjE1w==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.1.tgz", - "integrity": "sha512-MYq+l+PvHuw/rKUz1at/vb6nCnQ2gmJBNaM62z0OgH7B2W1D9pvkpYtlti9bGtizNIU1K3zm4bZF9F91efVY0w==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz", - "integrity": "sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-jsx": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.4.tgz", - "integrity": "sha512-KCg9mio9jwiARCB7WAcQ7Y1q+qicILjoK8LP/VkPkEKaf5dkaZZK1EcTe91a3JJlZ3qy6L5s9X52boEYi8DM9g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-top-level-await": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz", - "integrity": "sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz", - "integrity": "sha512-5QB50qyN44fzzz4/qxDPQMBCTHgxg3n0xRBLJUmBlLoU/sFvxVWGZF/ZUfMVDQuJUKXaBhbupxIzIfZ6Fwk/0A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.1.tgz", - "integrity": "sha512-SDtqoEcarK1DFlRJ1hHRY5HvJUj5kX4qmtpMAm2QnhOlyuMC4TMdCRgW6WXpv93rZeYNeLP22y8Aq2dbcDRM1A==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-remap-async-to-generator": "^7.12.1" - }, - "dependencies": { - "@babel/helper-module-imports": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz", - "integrity": "sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==", - "dev": true, - "requires": { - "@babel/types": "^7.12.5" - } - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz", - "integrity": "sha512-9d0KQCRM8clMPcDwo8SevNs+/9a8yWVVmaE80FGJcEP8N1qToREmWEGnBn8BUlJhYRFz6fqxeRL1sl5Ogsed7A==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.10.4", - "@babel/helper-wrap-function": "^7.10.4", - "@babel/types": "^7.12.1" - } - }, - "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.1.tgz", - "integrity": "sha512-5OpxfuYnSgPalRpo8EWGPzIYf0lHBWORCkj5M0oLBwHdlux9Ri36QqGW3/LR13RSVOAoUUMzoPI/jpE4ABcHoA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.1.tgz", - "integrity": "sha512-zJyAC9sZdE60r1nVQHblcfCj29Dh2Y0DOvlMkcqSo0ckqjiCwNiUezUKw+RjOCwGfpLRwnAeQ2XlLpsnGkvv9w==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.1.tgz", - "integrity": "sha512-/74xkA7bVdzQTBeSUhLLJgYIcxw/dpEpCdRDiHgPJ3Mv6uC11UhjpOhl72CgqbBCmt1qtssCyB2xnJm1+PFjog==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.10.4", - "@babel/helper-define-map": "^7.10.4", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-replace-supers": "^7.12.1", - "@babel/helper-split-export-declaration": "^7.10.4", - "globals": "^11.1.0" - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.12.1.tgz", - "integrity": "sha512-vVUOYpPWB7BkgUWPo4C44mUQHpTZXakEqFjbv8rQMg7TC6S6ZhGZ3otQcRH6u7+adSlE5i0sp63eMC/XGffrzg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.12.1.tgz", - "integrity": "sha512-fRMYFKuzi/rSiYb2uRLiUENJOKq4Gnl+6qOv5f8z0TZXg3llUwUhsNNwrwaT/6dUhJTzNpBr+CUvEWBtfNY1cw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.1.tgz", - "integrity": "sha512-B2pXeRKoLszfEW7J4Hg9LoFaWEbr/kzo3teWHmtFCszjRNa/b40f9mfeqZsIDLLt/FjwQ6pz/Gdlwy85xNckBA==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.1.tgz", - "integrity": "sha512-iRght0T0HztAb/CazveUpUQrZY+aGKKaWXMJ4uf9YJtqxSUe09j3wteztCUDRHs+SRAL7yMuFqUsLoAKKzgXjw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.1.tgz", - "integrity": "sha512-7tqwy2bv48q+c1EHbXK0Zx3KXd2RVQp6OC7PbwFNt/dPTAV3Lu5sWtWuAj8owr5wqtWnqHfl2/mJlUmqkChKug==", - "dev": true, - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.12.1.tgz", - "integrity": "sha512-Zaeq10naAsuHo7heQvyV0ptj4dlZJwZgNAtBYBnu5nNKJoW62m0zKcIEyVECrUKErkUkg6ajMy4ZfnVZciSBhg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.1.tgz", - "integrity": "sha512-JF3UgJUILoFrFMEnOJLJkRHSk6LUSXLmEFsA23aR2O5CSLUxbeUX1IZ1YQ7Sn0aXb601Ncwjx73a+FVqgcljVw==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.1.tgz", - "integrity": "sha512-+PxVGA+2Ag6uGgL0A5f+9rklOnnMccwEBzwYFL3EUaKuiyVnUipyXncFcfjSkbimLrODoqki1U9XxZzTvfN7IQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-member-expression-literals": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.1.tgz", - "integrity": "sha512-1sxePl6z9ad0gFMB9KqmYofk34flq62aqMt9NqliS/7hPEpURUCMbyHXrMPlo282iY7nAvUB1aQd5mg79UD9Jg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.12.1.tgz", - "integrity": "sha512-tDW8hMkzad5oDtzsB70HIQQRBiTKrhfgwC/KkJeGsaNFTdWhKNt/BiE8c5yj19XiGyrxpbkOfH87qkNg1YGlOQ==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4", - "babel-plugin-dynamic-import-node": "^2.3.3" - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.1.tgz", - "integrity": "sha512-dY789wq6l0uLY8py9c1B48V8mVL5gZh/+PQ5ZPrylPYsnAvnEMjqsUXkuoDVPeVK+0VyGar+D08107LzDQ6pag==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-simple-access": "^7.12.1", - "babel-plugin-dynamic-import-node": "^2.3.3" - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.1.tgz", - "integrity": "sha512-Hn7cVvOavVh8yvW6fLwveFqSnd7rbQN3zJvoPNyNaQSvgfKmDBO9U1YL9+PCXGRlZD9tNdWTy5ACKqMuzyn32Q==", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.10.4", - "@babel/helper-module-transforms": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-validator-identifier": "^7.10.4", - "babel-plugin-dynamic-import-node": "^2.3.3" - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.12.1.tgz", - "integrity": "sha512-aEIubCS0KHKM0zUos5fIoQm+AZUMt1ZvMpqz0/H5qAQ7vWylr9+PLYurT+Ic7ID/bKLd4q8hDovaG3Zch2uz5Q==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.1.tgz", - "integrity": "sha512-tB43uQ62RHcoDp9v2Nsf+dSM8sbNodbEicbQNA53zHz8pWUhsgHSJCGpt7daXxRydjb0KnfmB+ChXOv3oADp1Q==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.12.1" - } - }, - "@babel/plugin-transform-new-target": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.1.tgz", - "integrity": "sha512-+eW/VLcUL5L9IvJH7rT1sT0CzkdUTvPrXC2PXTn/7z7tXLBuKvezYbGdxD5WMRoyvyaujOq2fWoKl869heKjhw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.1.tgz", - "integrity": "sha512-AvypiGJH9hsquNUn+RXVcBdeE3KHPZexWRdimhuV59cSoOt5kFBmqlByorAeUlGG2CJWd0U+4ZtNKga/TB0cAw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-replace-supers": "^7.12.1" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz", - "integrity": "sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-property-literals": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.1.tgz", - "integrity": "sha512-6MTCR/mZ1MQS+AwZLplX4cEySjCpnIF26ToWo942nqn8hXSm7McaHQNeGx/pt7suI1TWOWMfa/NgBhiqSnX0cQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-react-jsx": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.10.4.tgz", - "integrity": "sha512-L+MfRhWjX0eI7Js093MM6MacKU4M6dnCRa/QPDwYMxjljzSCzzlzKzj9Pk4P3OtrPcxr2N3znR419nr3Xw+65A==", - "dev": true, - "requires": { - "@babel/helper-builder-react-jsx": "^7.10.4", - "@babel/helper-builder-react-jsx-experimental": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-jsx": "^7.10.4" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.1.tgz", - "integrity": "sha512-gYrHqs5itw6i4PflFX3OdBPMQdPbF4bj2REIUxlMRUFk0/ZOAIpDFuViuxPjUL7YC8UPnf+XG7/utJvqXdPKng==", - "dev": true, - "requires": { - "regenerator-transform": "^0.14.2" - } - }, - "@babel/plugin-transform-reserved-words": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.1.tgz", - "integrity": "sha512-pOnUfhyPKvZpVyBHhSBoX8vfA09b7r00Pmm1sH+29ae2hMTKVmSp4Ztsr8KBKjLjx17H0eJqaRC3bR2iThM54A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-runtime": { - "version": "7.11.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.11.5.tgz", - "integrity": "sha512-9aIoee+EhjySZ6vY5hnLjigHzunBlscx9ANKutkeWTJTx6m5Rbq6Ic01tLvO54lSusR+BxV7u4UDdCmXv5aagg==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4", - "resolve": "^1.8.1", - "semver": "^5.5.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.1.tgz", - "integrity": "sha512-GFZS3c/MhX1OusqB1MZ1ct2xRzX5ppQh2JU1h2Pnfk88HtFTM+TWQqJNfwkmxtPQtb/s1tk87oENfXJlx7rSDw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.12.1.tgz", - "integrity": "sha512-vuLp8CP0BE18zVYjsEBZ5xoCecMK6LBMMxYzJnh01rxQRvhNhH1csMMmBfNo5tGpGO+NhdSNW2mzIvBu3K1fng==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.7.tgz", - "integrity": "sha512-VEiqZL5N/QvDbdjfYQBhruN0HYjSPjC4XkeqW4ny/jNtH9gcbgaqBIXYEZCNnESMAGs0/K/R7oFGMhOyu/eIxg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.12.1.tgz", - "integrity": "sha512-b4Zx3KHi+taXB1dVRBhVJtEPi9h1THCeKmae2qP0YdUHIFhVjtpqqNfxeVAa1xeHVhAy4SbHxEwx5cltAu5apw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.1.tgz", - "integrity": "sha512-EPGgpGy+O5Kg5pJFNDKuxt9RdmTgj5sgrus2XVeMp/ZIbOESadgILUbm50SNpghOh3/6yrbsH+NB5+WJTmsA7Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-unicode-escapes": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz", - "integrity": "sha512-I8gNHJLIc7GdApm7wkVnStWssPNbSRMPtgHdmH3sRM1zopz09UWPS4x5V4n1yz/MIWTVnJ9sp6IkuXdWM4w+2Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.1.tgz", - "integrity": "sha512-SqH4ClNngh/zGwHZOOQMTD+e8FGWexILV+ePMyiDJttAWRh5dhDL8rcl5lSgU3Huiq6Zn6pWTMvdPAb21Dwdyg==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/polyfill": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.12.1.tgz", - "integrity": "sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g==", - "dev": true, - "requires": { - "core-js": "^2.6.5", - "regenerator-runtime": "^0.13.4" - } - }, - "@babel/preset-env": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.12.7.tgz", - "integrity": "sha512-OnNdfAr1FUQg7ksb7bmbKoby4qFOHw6DKWWUNB9KqnnCldxhxJlP+21dpyaWFmf2h0rTbOkXJtAGevY3XW1eew==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.12.7", - "@babel/helper-compilation-targets": "^7.12.5", - "@babel/helper-module-imports": "^7.12.5", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-validator-option": "^7.12.1", - "@babel/plugin-proposal-async-generator-functions": "^7.12.1", - "@babel/plugin-proposal-class-properties": "^7.12.1", - "@babel/plugin-proposal-dynamic-import": "^7.12.1", - "@babel/plugin-proposal-export-namespace-from": "^7.12.1", - "@babel/plugin-proposal-json-strings": "^7.12.1", - "@babel/plugin-proposal-logical-assignment-operators": "^7.12.1", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", - "@babel/plugin-proposal-numeric-separator": "^7.12.7", - "@babel/plugin-proposal-object-rest-spread": "^7.12.1", - "@babel/plugin-proposal-optional-catch-binding": "^7.12.1", - "@babel/plugin-proposal-optional-chaining": "^7.12.7", - "@babel/plugin-proposal-private-methods": "^7.12.1", - "@babel/plugin-proposal-unicode-property-regex": "^7.12.1", - "@babel/plugin-syntax-async-generators": "^7.8.0", - "@babel/plugin-syntax-class-properties": "^7.12.1", - "@babel/plugin-syntax-dynamic-import": "^7.8.0", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.0", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.0", - "@babel/plugin-syntax-top-level-await": "^7.12.1", - "@babel/plugin-transform-arrow-functions": "^7.12.1", - "@babel/plugin-transform-async-to-generator": "^7.12.1", - "@babel/plugin-transform-block-scoped-functions": "^7.12.1", - "@babel/plugin-transform-block-scoping": "^7.12.1", - "@babel/plugin-transform-classes": "^7.12.1", - "@babel/plugin-transform-computed-properties": "^7.12.1", - "@babel/plugin-transform-destructuring": "^7.12.1", - "@babel/plugin-transform-dotall-regex": "^7.12.1", - "@babel/plugin-transform-duplicate-keys": "^7.12.1", - "@babel/plugin-transform-exponentiation-operator": "^7.12.1", - "@babel/plugin-transform-for-of": "^7.12.1", - "@babel/plugin-transform-function-name": "^7.12.1", - "@babel/plugin-transform-literals": "^7.12.1", - "@babel/plugin-transform-member-expression-literals": "^7.12.1", - "@babel/plugin-transform-modules-amd": "^7.12.1", - "@babel/plugin-transform-modules-commonjs": "^7.12.1", - "@babel/plugin-transform-modules-systemjs": "^7.12.1", - "@babel/plugin-transform-modules-umd": "^7.12.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.12.1", - "@babel/plugin-transform-new-target": "^7.12.1", - "@babel/plugin-transform-object-super": "^7.12.1", - "@babel/plugin-transform-parameters": "^7.12.1", - "@babel/plugin-transform-property-literals": "^7.12.1", - "@babel/plugin-transform-regenerator": "^7.12.1", - "@babel/plugin-transform-reserved-words": "^7.12.1", - "@babel/plugin-transform-shorthand-properties": "^7.12.1", - "@babel/plugin-transform-spread": "^7.12.1", - "@babel/plugin-transform-sticky-regex": "^7.12.7", - "@babel/plugin-transform-template-literals": "^7.12.1", - "@babel/plugin-transform-typeof-symbol": "^7.12.1", - "@babel/plugin-transform-unicode-escapes": "^7.12.1", - "@babel/plugin-transform-unicode-regex": "^7.12.1", - "@babel/preset-modules": "^0.1.3", - "@babel/types": "^7.12.7", - "core-js-compat": "^3.7.0", - "semver": "^5.5.0" - }, - "dependencies": { - "@babel/helper-module-imports": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz", - "integrity": "sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==", - "dev": true, - "requires": { - "@babel/types": "^7.12.5" - } - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz", - "integrity": "sha512-9d0KQCRM8clMPcDwo8SevNs+/9a8yWVVmaE80FGJcEP8N1qToREmWEGnBn8BUlJhYRFz6fqxeRL1sl5Ogsed7A==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.10.4", - "@babel/helper-wrap-function": "^7.10.4", - "@babel/types": "^7.12.1" - } - }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.1.tgz", - "integrity": "sha512-d+/o30tJxFxrA1lhzJqiUcEJdI6jKlNregCv5bASeGf2Q4MXmnwH7viDo7nhx1/ohf09oaH8j1GVYG/e3Yqk6A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-remap-async-to-generator": "^7.12.1", - "@babel/plugin-syntax-async-generators": "^7.8.0" - } - }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz", - "integrity": "sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.0", - "@babel/plugin-transform-parameters": "^7.12.1" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz", - "integrity": "sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.12.1.tgz", - "integrity": "sha512-xq9C5EQhdPK23ZeCdMxl8bbRnAgHFrw5EOC3KJUsSylZqdkCaFEXxGSBuTSObOpiiHHNyb82es8M1QYgfQGfNg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "@babel/preset-modules": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz", - "integrity": "sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - } - }, - "@babel/register": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.12.1.tgz", - "integrity": "sha512-XWcmseMIncOjoydKZnWvWi0/5CUCD+ZYKhRwgYlWOrA8fGZ/FjuLRpqtIhLOVD/fvR1b9DQHtZPn68VvhpYf+Q==", - "dev": true, - "requires": { - "find-cache-dir": "^2.0.0", - "lodash": "^4.17.19", - "make-dir": "^2.1.0", - "pirates": "^4.0.0", - "source-map-support": "^0.5.16" - } - }, - "@babel/runtime": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", - "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "@babel/runtime-corejs3": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.12.5.tgz", - "integrity": "sha512-roGr54CsTmNPPzZoCP1AmDXuBoNao7tnSA83TXTwt+UK5QVyh1DIJnrgYRPWKCF2flqZQXwa7Yr8v7VmLzF0YQ==", - "dev": true, - "requires": { - "core-js-pure": "^3.0.0", - "regenerator-runtime": "^0.13.4" - } - }, - "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/traverse": { - "version": "7.11.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", - "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.11.5", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.11.5", - "@babel/types": "^7.11.5", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" - }, - "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/types": { - "version": "7.11.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", - "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, - "@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "@cnakazawa/watch": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", - "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", - "dev": true, - "requires": { - "exec-sh": "^0.3.2", - "minimist": "^1.2.0" - } - }, - "@evocateur/libnpmaccess": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@evocateur/libnpmaccess/-/libnpmaccess-3.1.2.tgz", - "integrity": "sha512-KSCAHwNWro0CF2ukxufCitT9K5LjL/KuMmNzSu8wuwN2rjyKHD8+cmOsiybK+W5hdnwc5M1SmRlVCaMHQo+3rg==", - "dev": true, - "requires": { - "@evocateur/npm-registry-fetch": "^4.0.0", - "aproba": "^2.0.0", - "figgy-pudding": "^3.5.1", - "get-stream": "^4.0.0", - "npm-package-arg": "^6.1.0" - }, - "dependencies": { - "aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "dev": true - } - } - }, - "@evocateur/libnpmpublish": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@evocateur/libnpmpublish/-/libnpmpublish-1.2.2.tgz", - "integrity": "sha512-MJrrk9ct1FeY9zRlyeoyMieBjGDG9ihyyD9/Ft6MMrTxql9NyoEx2hw9casTIP4CdqEVu+3nQ2nXxoJ8RCXyFg==", - "dev": true, - "requires": { - "@evocateur/npm-registry-fetch": "^4.0.0", - "aproba": "^2.0.0", - "figgy-pudding": "^3.5.1", - "get-stream": "^4.0.0", - "lodash.clonedeep": "^4.5.0", - "normalize-package-data": "^2.4.0", - "npm-package-arg": "^6.1.0", - "semver": "^5.5.1", - "ssri": "^6.0.1" - }, - "dependencies": { - "aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "@evocateur/npm-registry-fetch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@evocateur/npm-registry-fetch/-/npm-registry-fetch-4.0.0.tgz", - "integrity": "sha512-k1WGfKRQyhJpIr+P17O5vLIo2ko1PFLKwoetatdduUSt/aQ4J2sJrJwwatdI5Z3SiYk/mRH9S3JpdmMFd/IK4g==", - "dev": true, - "requires": { - "JSONStream": "^1.3.4", - "bluebird": "^3.5.1", - "figgy-pudding": "^3.4.1", - "lru-cache": "^5.1.1", - "make-fetch-happen": "^5.0.0", - "npm-package-arg": "^6.1.0", - "safe-buffer": "^5.1.2" - } - }, - "@evocateur/pacote": { - "version": "9.6.5", - "resolved": "https://registry.npmjs.org/@evocateur/pacote/-/pacote-9.6.5.tgz", - "integrity": "sha512-EI552lf0aG2nOV8NnZpTxNo2PcXKPmDbF9K8eCBFQdIZwHNGN/mi815fxtmUMa2wTa1yndotICIDt/V0vpEx2w==", - "dev": true, - "requires": { - "@evocateur/npm-registry-fetch": "^4.0.0", - "bluebird": "^3.5.3", - "cacache": "^12.0.3", - "chownr": "^1.1.2", - "figgy-pudding": "^3.5.1", - "get-stream": "^4.1.0", - "glob": "^7.1.4", - "infer-owner": "^1.0.4", - "lru-cache": "^5.1.1", - "make-fetch-happen": "^5.0.0", - "minimatch": "^3.0.4", - "minipass": "^2.3.5", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "normalize-package-data": "^2.5.0", - "npm-package-arg": "^6.1.0", - "npm-packlist": "^1.4.4", - "npm-pick-manifest": "^3.0.0", - "osenv": "^0.1.5", - "promise-inflight": "^1.0.1", - "promise-retry": "^1.1.1", - "protoduck": "^5.0.1", - "rimraf": "^2.6.3", - "safe-buffer": "^5.2.0", - "semver": "^5.7.0", - "ssri": "^6.0.1", - "tar": "^4.4.10", - "unique-filename": "^1.1.1", - "which": "^1.3.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "@istanbuljs/schema": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", - "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", - "dev": true - }, - "@jest/console": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-25.5.0.tgz", - "integrity": "sha512-T48kZa6MK1Y6k4b89sexwmSF4YLeZS/Udqg3Jj3jG/cHH+N/sLFCEoXEDMOKugJQ9FxPN1osxIknvKkxt6MKyw==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "jest-message-util": "^25.5.0", - "jest-util": "^25.5.0", - "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/core": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-25.5.4.tgz", - "integrity": "sha512-3uSo7laYxF00Dg/DMgbn4xMJKmDdWvZnf89n8Xj/5/AeQ2dOQmn6b6Hkj/MleyzZWXpwv+WSdYWl4cLsy2JsoA==", - "dev": true, - "requires": { - "@jest/console": "^25.5.0", - "@jest/reporters": "^25.5.1", - "@jest/test-result": "^25.5.0", - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "ansi-escapes": "^4.2.1", - "chalk": "^3.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-changed-files": "^25.5.0", - "jest-config": "^25.5.4", - "jest-haste-map": "^25.5.1", - "jest-message-util": "^25.5.0", - "jest-regex-util": "^25.2.6", - "jest-resolve": "^25.5.1", - "jest-resolve-dependencies": "^25.5.4", - "jest-runner": "^25.5.4", - "jest-runtime": "^25.5.4", - "jest-snapshot": "^25.5.1", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", - "jest-watcher": "^25.5.0", - "micromatch": "^4.0.2", - "p-each-series": "^2.1.0", - "realpath-native": "^2.0.0", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "@jest/environment": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.5.0.tgz", - "integrity": "sha512-U2VXPEqL07E/V7pSZMSQCvV5Ea4lqOlT+0ZFijl/i316cRMHvZ4qC+jBdryd+lmRetjQo0YIQr6cVPNxxK87mA==", - "dev": true, - "requires": { - "@jest/fake-timers": "^25.5.0", - "@jest/types": "^25.5.0", - "jest-mock": "^25.5.0" - } - }, - "@jest/fake-timers": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.5.0.tgz", - "integrity": "sha512-9y2+uGnESw/oyOI3eww9yaxdZyHq7XvprfP/eeoCsjqKYts2yRlsHS/SgjPDV8FyMfn2nbMy8YzUk6nyvdLOpQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-util": "^25.5.0", - "lolex": "^5.0.0" - } - }, - "@jest/reporters": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-25.5.1.tgz", - "integrity": "sha512-3jbd8pPDTuhYJ7vqiHXbSwTJQNavczPs+f1kRprRDxETeE3u6srJ+f0NPuwvOmk+lmunZzPkYWIFZDLHQPkviw==", - "dev": true, - "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.2.4", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^4.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.2", - "jest-haste-map": "^25.5.1", - "jest-resolve": "^25.5.1", - "jest-util": "^25.5.0", - "jest-worker": "^25.5.0", - "node-notifier": "^6.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.0", - "string-length": "^3.1.0", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^4.1.3" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", - "dev": true, - "requires": { - "@babel/core": "^7.7.5", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - } - }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", - "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - } - }, - "istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/source-map": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-25.5.0.tgz", - "integrity": "sha512-eIGx0xN12yVpMcPaVpjXPnn3N30QGJCJQSkEDUt9x1fI1Gdvb07Ml6K5iN2hG7NmMP6FDmtPEssE3z6doOYUwQ==", - "dev": true, - "requires": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.4", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "@jest/test-result": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-25.5.0.tgz", - "integrity": "sha512-oV+hPJgXN7IQf/fHWkcS99y0smKLU2czLBJ9WA0jHITLst58HpQMtzSYxzaBvYc6U5U6jfoMthqsUlUlbRXs0A==", - "dev": true, - "requires": { - "@jest/console": "^25.5.0", - "@jest/types": "^25.5.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - } - }, - "@jest/test-sequencer": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-25.5.4.tgz", - "integrity": "sha512-pTJGEkSeg1EkCO2YWq6hbFvKNXk8ejqlxiOg1jBNLnWrgXOkdY6UmqZpwGFXNnRt9B8nO1uWMzLLZ4eCmhkPNA==", - "dev": true, - "requires": { - "@jest/test-result": "^25.5.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^25.5.1", - "jest-runner": "^25.5.4", - "jest-runtime": "^25.5.4" - } - }, - "@jest/transform": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.5.1.tgz", - "integrity": "sha512-Y8CEoVwXb4QwA6Y/9uDkn0Xfz0finGkieuV0xkdF9UtZGJeLukD5nLkaVrVsODB1ojRWlaoD0AJZpVHCSnJEvg==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^25.5.0", - "babel-plugin-istanbul": "^6.0.0", - "chalk": "^3.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^25.5.1", - "jest-regex-util": "^25.2.6", - "jest-util": "^25.5.0", - "micromatch": "^4.0.2", - "pirates": "^4.0.1", - "realpath-native": "^2.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "@jest/types": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", - "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", - "@types/yargs": "^15.0.0", - "chalk": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@lerna/add": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@lerna/add/-/add-3.21.0.tgz", - "integrity": "sha512-vhUXXF6SpufBE1EkNEXwz1VLW03f177G9uMOFMQkp6OJ30/PWg4Ekifuz9/3YfgB2/GH8Tu4Lk3O51P2Hskg/A==", - "dev": true, - "requires": { - "@evocateur/pacote": "^9.6.3", - "@lerna/bootstrap": "3.21.0", - "@lerna/command": "3.21.0", - "@lerna/filter-options": "3.20.0", - "@lerna/npm-conf": "3.16.0", - "@lerna/validation-error": "3.13.0", - "dedent": "^0.7.0", - "npm-package-arg": "^6.1.0", - "p-map": "^2.1.0", - "semver": "^6.2.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@lerna/bootstrap": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-3.21.0.tgz", - "integrity": "sha512-mtNHlXpmvJn6JTu0KcuTTPl2jLsDNud0QacV/h++qsaKbhAaJr/FElNZ5s7MwZFUM3XaDmvWzHKaszeBMHIbBw==", - "dev": true, - "requires": { - "@lerna/command": "3.21.0", - "@lerna/filter-options": "3.20.0", - "@lerna/has-npm-version": "3.16.5", - "@lerna/npm-install": "3.16.5", - "@lerna/package-graph": "3.18.5", - "@lerna/pulse-till-done": "3.13.0", - "@lerna/rimraf-dir": "3.16.5", - "@lerna/run-lifecycle": "3.16.2", - "@lerna/run-topologically": "3.18.5", - "@lerna/symlink-binary": "3.17.0", - "@lerna/symlink-dependencies": "3.17.0", - "@lerna/validation-error": "3.13.0", - "dedent": "^0.7.0", - "get-port": "^4.2.0", - "multimatch": "^3.0.0", - "npm-package-arg": "^6.1.0", - "npmlog": "^4.1.2", - "p-finally": "^1.0.0", - "p-map": "^2.1.0", - "p-map-series": "^1.0.0", - "p-waterfall": "^1.0.0", - "read-package-tree": "^5.1.6", - "semver": "^6.2.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@lerna/changed": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@lerna/changed/-/changed-3.21.0.tgz", - "integrity": "sha512-hzqoyf8MSHVjZp0gfJ7G8jaz+++mgXYiNs9iViQGA8JlN/dnWLI5sWDptEH3/B30Izo+fdVz0S0s7ydVE3pWIw==", - "dev": true, - "requires": { - "@lerna/collect-updates": "3.20.0", - "@lerna/command": "3.21.0", - "@lerna/listable": "3.18.5", - "@lerna/output": "3.13.0" - } - }, - "@lerna/check-working-tree": { - "version": "3.16.5", - "resolved": "https://registry.npmjs.org/@lerna/check-working-tree/-/check-working-tree-3.16.5.tgz", - "integrity": "sha512-xWjVBcuhvB8+UmCSb5tKVLB5OuzSpw96WEhS2uz6hkWVa/Euh1A0/HJwn2cemyK47wUrCQXtczBUiqnq9yX5VQ==", - "dev": true, - "requires": { - "@lerna/collect-uncommitted": "3.16.5", - "@lerna/describe-ref": "3.16.5", - "@lerna/validation-error": "3.13.0" - } - }, - "@lerna/child-process": { - "version": "3.16.5", - "resolved": "https://registry.npmjs.org/@lerna/child-process/-/child-process-3.16.5.tgz", - "integrity": "sha512-vdcI7mzei9ERRV4oO8Y1LHBZ3A5+ampRKg1wq5nutLsUA4mEBN6H7JqjWOMY9xZemv6+kATm2ofjJ3lW5TszQg==", - "dev": true, - "requires": { - "chalk": "^2.3.1", - "execa": "^1.0.0", - "strong-log-transformer": "^2.0.0" - } - }, - "@lerna/clean": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@lerna/clean/-/clean-3.21.0.tgz", - "integrity": "sha512-b/L9l+MDgE/7oGbrav6rG8RTQvRiZLO1zTcG17zgJAAuhlsPxJExMlh2DFwJEVi2les70vMhHfST3Ue1IMMjpg==", - "dev": true, - "requires": { - "@lerna/command": "3.21.0", - "@lerna/filter-options": "3.20.0", - "@lerna/prompt": "3.18.5", - "@lerna/pulse-till-done": "3.13.0", - "@lerna/rimraf-dir": "3.16.5", - "p-map": "^2.1.0", - "p-map-series": "^1.0.0", - "p-waterfall": "^1.0.0" - } - }, - "@lerna/cli": { - "version": "3.18.5", - "resolved": "https://registry.npmjs.org/@lerna/cli/-/cli-3.18.5.tgz", - "integrity": "sha512-erkbxkj9jfc89vVs/jBLY/fM0I80oLmJkFUV3Q3wk9J3miYhP14zgVEBsPZY68IZlEjT6T3Xlq2xO1AVaatHsA==", - "dev": true, - "requires": { - "@lerna/global-options": "3.13.0", - "dedent": "^0.7.0", - "npmlog": "^4.1.2", - "yargs": "^14.2.2" - }, - "dependencies": { - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "yargs": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", - "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^15.0.1" - } - }, - "yargs-parser": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz", - "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "@lerna/collect-uncommitted": { - "version": "3.16.5", - "resolved": "https://registry.npmjs.org/@lerna/collect-uncommitted/-/collect-uncommitted-3.16.5.tgz", - "integrity": "sha512-ZgqnGwpDZiWyzIQVZtQaj9tRizsL4dUOhuOStWgTAw1EMe47cvAY2kL709DzxFhjr6JpJSjXV5rZEAeU3VE0Hg==", - "dev": true, - "requires": { - "@lerna/child-process": "3.16.5", - "chalk": "^2.3.1", - "figgy-pudding": "^3.5.1", - "npmlog": "^4.1.2" - } - }, - "@lerna/collect-updates": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@lerna/collect-updates/-/collect-updates-3.20.0.tgz", - "integrity": "sha512-qBTVT5g4fupVhBFuY4nI/3FSJtQVcDh7/gEPOpRxoXB/yCSnT38MFHXWl+y4einLciCjt/+0x6/4AG80fjay2Q==", - "dev": true, - "requires": { - "@lerna/child-process": "3.16.5", - "@lerna/describe-ref": "3.16.5", - "minimatch": "^3.0.4", - "npmlog": "^4.1.2", - "slash": "^2.0.0" - } - }, - "@lerna/command": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@lerna/command/-/command-3.21.0.tgz", - "integrity": "sha512-T2bu6R8R3KkH5YoCKdutKv123iUgUbW8efVjdGCDnCMthAQzoentOJfDeodBwn0P2OqCl3ohsiNVtSn9h78fyQ==", - "dev": true, - "requires": { - "@lerna/child-process": "3.16.5", - "@lerna/package-graph": "3.18.5", - "@lerna/project": "3.21.0", - "@lerna/validation-error": "3.13.0", - "@lerna/write-log-file": "3.13.0", - "clone-deep": "^4.0.1", - "dedent": "^0.7.0", - "execa": "^1.0.0", - "is-ci": "^2.0.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/conventional-commits": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@lerna/conventional-commits/-/conventional-commits-3.22.0.tgz", - "integrity": "sha512-z4ZZk1e8Mhz7+IS8NxHr64wyklHctCJyWpJKEZZPJiLFJ8yKto/x38O80R10pIzC0rr8Sy/OsjSH4bl0TbbgqA==", - "dev": true, - "requires": { - "@lerna/validation-error": "3.13.0", - "conventional-changelog-angular": "^5.0.3", - "conventional-changelog-core": "^3.1.6", - "conventional-recommended-bump": "^5.0.0", - "fs-extra": "^8.1.0", - "get-stream": "^4.0.0", - "lodash.template": "^4.5.0", - "npm-package-arg": "^6.1.0", - "npmlog": "^4.1.2", - "pify": "^4.0.1", - "semver": "^6.2.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@lerna/create": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@lerna/create/-/create-3.22.0.tgz", - "integrity": "sha512-MdiQQzCcB4E9fBF1TyMOaAEz9lUjIHp1Ju9H7f3lXze5JK6Fl5NYkouAvsLgY6YSIhXMY8AHW2zzXeBDY4yWkw==", - "dev": true, - "requires": { - "@evocateur/pacote": "^9.6.3", - "@lerna/child-process": "3.16.5", - "@lerna/command": "3.21.0", - "@lerna/npm-conf": "3.16.0", - "@lerna/validation-error": "3.13.0", - "camelcase": "^5.0.0", - "dedent": "^0.7.0", - "fs-extra": "^8.1.0", - "globby": "^9.2.0", - "init-package-json": "^1.10.3", - "npm-package-arg": "^6.1.0", - "p-reduce": "^1.0.0", - "pify": "^4.0.1", - "semver": "^6.2.0", - "slash": "^2.0.0", - "validate-npm-package-license": "^3.0.3", - "validate-npm-package-name": "^3.0.0", - "whatwg-url": "^7.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@lerna/create-symlink": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@lerna/create-symlink/-/create-symlink-3.16.2.tgz", - "integrity": "sha512-pzXIJp6av15P325sgiIRpsPXLFmkisLhMBCy4764d+7yjf2bzrJ4gkWVMhsv4AdF0NN3OyZ5jjzzTtLNqfR+Jw==", - "dev": true, - "requires": { - "@zkochan/cmd-shim": "^3.1.0", - "fs-extra": "^8.1.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/describe-ref": { - "version": "3.16.5", - "resolved": "https://registry.npmjs.org/@lerna/describe-ref/-/describe-ref-3.16.5.tgz", - "integrity": "sha512-c01+4gUF0saOOtDBzbLMFOTJDHTKbDFNErEY6q6i9QaXuzy9LNN62z+Hw4acAAZuJQhrVWncVathcmkkjvSVGw==", - "dev": true, - "requires": { - "@lerna/child-process": "3.16.5", - "npmlog": "^4.1.2" - } - }, - "@lerna/diff": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@lerna/diff/-/diff-3.21.0.tgz", - "integrity": "sha512-5viTR33QV3S7O+bjruo1SaR40m7F2aUHJaDAC7fL9Ca6xji+aw1KFkpCtVlISS0G8vikUREGMJh+c/VMSc8Usw==", - "dev": true, - "requires": { - "@lerna/child-process": "3.16.5", - "@lerna/command": "3.21.0", - "@lerna/validation-error": "3.13.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/exec": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@lerna/exec/-/exec-3.21.0.tgz", - "integrity": "sha512-iLvDBrIE6rpdd4GIKTY9mkXyhwsJ2RvQdB9ZU+/NhR3okXfqKc6py/24tV111jqpXTtZUW6HNydT4dMao2hi1Q==", - "dev": true, - "requires": { - "@lerna/child-process": "3.16.5", - "@lerna/command": "3.21.0", - "@lerna/filter-options": "3.20.0", - "@lerna/profiler": "3.20.0", - "@lerna/run-topologically": "3.18.5", - "@lerna/validation-error": "3.13.0", - "p-map": "^2.1.0" - } - }, - "@lerna/filter-options": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-3.20.0.tgz", - "integrity": "sha512-bmcHtvxn7SIl/R9gpiNMVG7yjx7WyT0HSGw34YVZ9B+3xF/83N3r5Rgtjh4hheLZ+Q91Or0Jyu5O3Nr+AwZe2g==", - "dev": true, - "requires": { - "@lerna/collect-updates": "3.20.0", - "@lerna/filter-packages": "3.18.0", - "dedent": "^0.7.0", - "figgy-pudding": "^3.5.1", - "npmlog": "^4.1.2" - } - }, - "@lerna/filter-packages": { - "version": "3.18.0", - "resolved": "https://registry.npmjs.org/@lerna/filter-packages/-/filter-packages-3.18.0.tgz", - "integrity": "sha512-6/0pMM04bCHNATIOkouuYmPg6KH3VkPCIgTfQmdkPJTullERyEQfNUKikrefjxo1vHOoCACDpy65JYyKiAbdwQ==", - "dev": true, - "requires": { - "@lerna/validation-error": "3.13.0", - "multimatch": "^3.0.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/get-npm-exec-opts": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/get-npm-exec-opts/-/get-npm-exec-opts-3.13.0.tgz", - "integrity": "sha512-Y0xWL0rg3boVyJk6An/vurKzubyJKtrxYv2sj4bB8Mc5zZ3tqtv0ccbOkmkXKqbzvNNF7VeUt1OJ3DRgtC/QZw==", - "dev": true, - "requires": { - "npmlog": "^4.1.2" - } - }, - "@lerna/get-packed": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/@lerna/get-packed/-/get-packed-3.16.0.tgz", - "integrity": "sha512-AjsFiaJzo1GCPnJUJZiTW6J1EihrPkc2y3nMu6m3uWFxoleklsSCyImumzVZJssxMi3CPpztj8LmADLedl9kXw==", - "dev": true, - "requires": { - "fs-extra": "^8.1.0", - "ssri": "^6.0.1", - "tar": "^4.4.8" - } - }, - "@lerna/github-client": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@lerna/github-client/-/github-client-3.22.0.tgz", - "integrity": "sha512-O/GwPW+Gzr3Eb5bk+nTzTJ3uv+jh5jGho9BOqKlajXaOkMYGBELEAqV5+uARNGWZFvYAiF4PgqHb6aCUu7XdXg==", - "dev": true, - "requires": { - "@lerna/child-process": "3.16.5", - "@octokit/plugin-enterprise-rest": "^6.0.1", - "@octokit/rest": "^16.28.4", - "git-url-parse": "^11.1.2", - "npmlog": "^4.1.2" - } - }, - "@lerna/gitlab-client": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/@lerna/gitlab-client/-/gitlab-client-3.15.0.tgz", - "integrity": "sha512-OsBvRSejHXUBMgwWQqNoioB8sgzL/Pf1pOUhHKtkiMl6aAWjklaaq5HPMvTIsZPfS6DJ9L5OK2GGZuooP/5c8Q==", - "dev": true, - "requires": { - "node-fetch": "^2.5.0", - "npmlog": "^4.1.2", - "whatwg-url": "^7.0.0" - }, - "dependencies": { - "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "dev": true - } - } - }, - "@lerna/global-options": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/global-options/-/global-options-3.13.0.tgz", - "integrity": "sha512-SlZvh1gVRRzYLVluz9fryY1nJpZ0FHDGB66U9tFfvnnxmueckRQxLopn3tXj3NU1kc3QANT2I5BsQkOqZ4TEFQ==", - "dev": true - }, - "@lerna/has-npm-version": { - "version": "3.16.5", - "resolved": "https://registry.npmjs.org/@lerna/has-npm-version/-/has-npm-version-3.16.5.tgz", - "integrity": "sha512-WL7LycR9bkftyqbYop5rEGJ9sRFIV55tSGmbN1HLrF9idwOCD7CLrT64t235t3t4O5gehDnwKI5h2U3oxTrF8Q==", - "dev": true, - "requires": { - "@lerna/child-process": "3.16.5", - "semver": "^6.2.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@lerna/import": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@lerna/import/-/import-3.22.0.tgz", - "integrity": "sha512-uWOlexasM5XR6tXi4YehODtH9Y3OZrFht3mGUFFT3OIl2s+V85xIGFfqFGMTipMPAGb2oF1UBLL48kR43hRsOg==", - "dev": true, - "requires": { - "@lerna/child-process": "3.16.5", - "@lerna/command": "3.21.0", - "@lerna/prompt": "3.18.5", - "@lerna/pulse-till-done": "3.13.0", - "@lerna/validation-error": "3.13.0", - "dedent": "^0.7.0", - "fs-extra": "^8.1.0", - "p-map-series": "^1.0.0" - } - }, - "@lerna/info": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@lerna/info/-/info-3.21.0.tgz", - "integrity": "sha512-0XDqGYVBgWxUquFaIptW2bYSIu6jOs1BtkvRTWDDhw4zyEdp6q4eaMvqdSap1CG+7wM5jeLCi6z94wS0AuiuwA==", - "dev": true, - "requires": { - "@lerna/command": "3.21.0", - "@lerna/output": "3.13.0", - "envinfo": "^7.3.1" - } - }, - "@lerna/init": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@lerna/init/-/init-3.21.0.tgz", - "integrity": "sha512-6CM0z+EFUkFfurwdJCR+LQQF6MqHbYDCBPyhu/d086LRf58GtYZYj49J8mKG9ktayp/TOIxL/pKKjgLD8QBPOg==", - "dev": true, - "requires": { - "@lerna/child-process": "3.16.5", - "@lerna/command": "3.21.0", - "fs-extra": "^8.1.0", - "p-map": "^2.1.0", - "write-json-file": "^3.2.0" - } - }, - "@lerna/link": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@lerna/link/-/link-3.21.0.tgz", - "integrity": "sha512-tGu9GxrX7Ivs+Wl3w1+jrLi1nQ36kNI32dcOssij6bg0oZ2M2MDEFI9UF2gmoypTaN9uO5TSsjCFS7aR79HbdQ==", - "dev": true, - "requires": { - "@lerna/command": "3.21.0", - "@lerna/package-graph": "3.18.5", - "@lerna/symlink-dependencies": "3.17.0", - "p-map": "^2.1.0", - "slash": "^2.0.0" - } - }, - "@lerna/list": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@lerna/list/-/list-3.21.0.tgz", - "integrity": "sha512-KehRjE83B1VaAbRRkRy6jLX1Cin8ltsrQ7FHf2bhwhRHK0S54YuA6LOoBnY/NtA8bHDX/Z+G5sMY78X30NS9tg==", - "dev": true, - "requires": { - "@lerna/command": "3.21.0", - "@lerna/filter-options": "3.20.0", - "@lerna/listable": "3.18.5", - "@lerna/output": "3.13.0" - } - }, - "@lerna/listable": { - "version": "3.18.5", - "resolved": "https://registry.npmjs.org/@lerna/listable/-/listable-3.18.5.tgz", - "integrity": "sha512-Sdr3pVyaEv5A7ZkGGYR7zN+tTl2iDcinryBPvtuv20VJrXBE8wYcOks1edBTcOWsPjCE/rMP4bo1pseyk3UTsg==", - "dev": true, - "requires": { - "@lerna/query-graph": "3.18.5", - "chalk": "^2.3.1", - "columnify": "^1.5.4" - } - }, - "@lerna/log-packed": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/@lerna/log-packed/-/log-packed-3.16.0.tgz", - "integrity": "sha512-Fp+McSNBV/P2mnLUYTaSlG8GSmpXM7krKWcllqElGxvAqv6chk2K3c2k80MeVB4WvJ9tRjUUf+i7HUTiQ9/ckQ==", - "dev": true, - "requires": { - "byte-size": "^5.0.1", - "columnify": "^1.5.4", - "has-unicode": "^2.0.1", - "npmlog": "^4.1.2" - } - }, - "@lerna/npm-conf": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-conf/-/npm-conf-3.16.0.tgz", - "integrity": "sha512-HbO3DUrTkCAn2iQ9+FF/eisDpWY5POQAOF1m7q//CZjdC2HSW3UYbKEGsSisFxSfaF9Z4jtrV+F/wX6qWs3CuA==", - "dev": true, - "requires": { - "config-chain": "^1.1.11", - "pify": "^4.0.1" - } - }, - "@lerna/npm-dist-tag": { - "version": "3.18.5", - "resolved": "https://registry.npmjs.org/@lerna/npm-dist-tag/-/npm-dist-tag-3.18.5.tgz", - "integrity": "sha512-xw0HDoIG6HreVsJND9/dGls1c+lf6vhu7yJoo56Sz5bvncTloYGLUppIfDHQr4ZvmPCK8rsh0euCVh2giPxzKQ==", - "dev": true, - "requires": { - "@evocateur/npm-registry-fetch": "^4.0.0", - "@lerna/otplease": "3.18.5", - "figgy-pudding": "^3.5.1", - "npm-package-arg": "^6.1.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/npm-install": { - "version": "3.16.5", - "resolved": "https://registry.npmjs.org/@lerna/npm-install/-/npm-install-3.16.5.tgz", - "integrity": "sha512-hfiKk8Eku6rB9uApqsalHHTHY+mOrrHeWEs+gtg7+meQZMTS3kzv4oVp5cBZigndQr3knTLjwthT/FX4KvseFg==", - "dev": true, - "requires": { - "@lerna/child-process": "3.16.5", - "@lerna/get-npm-exec-opts": "3.13.0", - "fs-extra": "^8.1.0", - "npm-package-arg": "^6.1.0", - "npmlog": "^4.1.2", - "signal-exit": "^3.0.2", - "write-pkg": "^3.1.0" - } - }, - "@lerna/npm-publish": { - "version": "3.18.5", - "resolved": "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-3.18.5.tgz", - "integrity": "sha512-3etLT9+2L8JAx5F8uf7qp6iAtOLSMj+ZYWY6oUgozPi/uLqU0/gsMsEXh3F0+YVW33q0M61RpduBoAlOOZnaTg==", - "dev": true, - "requires": { - "@evocateur/libnpmpublish": "^1.2.2", - "@lerna/otplease": "3.18.5", - "@lerna/run-lifecycle": "3.16.2", - "figgy-pudding": "^3.5.1", - "fs-extra": "^8.1.0", - "npm-package-arg": "^6.1.0", - "npmlog": "^4.1.2", - "pify": "^4.0.1", - "read-package-json": "^2.0.13" - } - }, - "@lerna/npm-run-script": { - "version": "3.16.5", - "resolved": "https://registry.npmjs.org/@lerna/npm-run-script/-/npm-run-script-3.16.5.tgz", - "integrity": "sha512-1asRi+LjmVn3pMjEdpqKJZFT/3ZNpb+VVeJMwrJaV/3DivdNg7XlPK9LTrORuKU4PSvhdEZvJmSlxCKyDpiXsQ==", - "dev": true, - "requires": { - "@lerna/child-process": "3.16.5", - "@lerna/get-npm-exec-opts": "3.13.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/otplease": { - "version": "3.18.5", - "resolved": "https://registry.npmjs.org/@lerna/otplease/-/otplease-3.18.5.tgz", - "integrity": "sha512-S+SldXAbcXTEDhzdxYLU0ZBKuYyURP/ND2/dK6IpKgLxQYh/z4ScljPDMyKymmEvgiEJmBsPZAAPfmNPEzxjog==", - "dev": true, - "requires": { - "@lerna/prompt": "3.18.5", - "figgy-pudding": "^3.5.1" - } - }, - "@lerna/output": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/output/-/output-3.13.0.tgz", - "integrity": "sha512-7ZnQ9nvUDu/WD+bNsypmPG5MwZBwu86iRoiW6C1WBuXXDxM5cnIAC1m2WxHeFnjyMrYlRXM9PzOQ9VDD+C15Rg==", - "dev": true, - "requires": { - "npmlog": "^4.1.2" - } - }, - "@lerna/pack-directory": { - "version": "3.16.4", - "resolved": "https://registry.npmjs.org/@lerna/pack-directory/-/pack-directory-3.16.4.tgz", - "integrity": "sha512-uxSF0HZeGyKaaVHz5FroDY9A5NDDiCibrbYR6+khmrhZtY0Bgn6hWq8Gswl9iIlymA+VzCbshWIMX4o2O8C8ng==", - "dev": true, - "requires": { - "@lerna/get-packed": "3.16.0", - "@lerna/package": "3.16.0", - "@lerna/run-lifecycle": "3.16.2", - "figgy-pudding": "^3.5.1", - "npm-packlist": "^1.4.4", - "npmlog": "^4.1.2", - "tar": "^4.4.10", - "temp-write": "^3.4.0" - } - }, - "@lerna/package": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/@lerna/package/-/package-3.16.0.tgz", - "integrity": "sha512-2lHBWpaxcBoiNVbtyLtPUuTYEaB/Z+eEqRS9duxpZs6D+mTTZMNy6/5vpEVSCBmzvdYpyqhqaYjjSLvjjr5Riw==", - "dev": true, - "requires": { - "load-json-file": "^5.3.0", - "npm-package-arg": "^6.1.0", - "write-pkg": "^3.1.0" - } - }, - "@lerna/package-graph": { - "version": "3.18.5", - "resolved": "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-3.18.5.tgz", - "integrity": "sha512-8QDrR9T+dBegjeLr+n9WZTVxUYUhIUjUgZ0gvNxUBN8S1WB9r6H5Yk56/MVaB64tA3oGAN9IIxX6w0WvTfFudA==", - "dev": true, - "requires": { - "@lerna/prerelease-id-from-version": "3.16.0", - "@lerna/validation-error": "3.13.0", - "npm-package-arg": "^6.1.0", - "npmlog": "^4.1.2", - "semver": "^6.2.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@lerna/prerelease-id-from-version": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/@lerna/prerelease-id-from-version/-/prerelease-id-from-version-3.16.0.tgz", - "integrity": "sha512-qZyeUyrE59uOK8rKdGn7jQz+9uOpAaF/3hbslJVFL1NqF9ELDTqjCPXivuejMX/lN4OgD6BugTO4cR7UTq/sZA==", - "dev": true, - "requires": { - "semver": "^6.2.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@lerna/profiler": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@lerna/profiler/-/profiler-3.20.0.tgz", - "integrity": "sha512-bh8hKxAlm6yu8WEOvbLENm42i2v9SsR4WbrCWSbsmOElx3foRnMlYk7NkGECa+U5c3K4C6GeBbwgqs54PP7Ljg==", - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1", - "fs-extra": "^8.1.0", - "npmlog": "^4.1.2", - "upath": "^1.2.0" - } - }, - "@lerna/project": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@lerna/project/-/project-3.21.0.tgz", - "integrity": "sha512-xT1mrpET2BF11CY32uypV2GPtPVm6Hgtha7D81GQP9iAitk9EccrdNjYGt5UBYASl4CIDXBRxwmTTVGfrCx82A==", - "dev": true, - "requires": { - "@lerna/package": "3.16.0", - "@lerna/validation-error": "3.13.0", - "cosmiconfig": "^5.1.0", - "dedent": "^0.7.0", - "dot-prop": "^4.2.0", - "glob-parent": "^5.0.0", - "globby": "^9.2.0", - "load-json-file": "^5.3.0", - "npmlog": "^4.1.2", - "p-map": "^2.1.0", - "resolve-from": "^4.0.0", - "write-json-file": "^3.2.0" - }, - "dependencies": { - "cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", - "dev": true, - "requires": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" - } - }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", - "dev": true, - "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - } - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - } - } - }, - "@lerna/prompt": { - "version": "3.18.5", - "resolved": "https://registry.npmjs.org/@lerna/prompt/-/prompt-3.18.5.tgz", - "integrity": "sha512-rkKj4nm1twSbBEb69+Em/2jAERK8htUuV8/xSjN0NPC+6UjzAwY52/x9n5cfmpa9lyKf/uItp7chCI7eDmNTKQ==", - "dev": true, - "requires": { - "inquirer": "^6.2.0", - "npmlog": "^4.1.2" - }, - "dependencies": { - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "cli-width": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", - "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", - "dev": true - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "inquirer": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", - "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", - "dev": true, - "requires": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", - "dev": true - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - } - } - }, - "@lerna/publish": { - "version": "3.22.1", - "resolved": "https://registry.npmjs.org/@lerna/publish/-/publish-3.22.1.tgz", - "integrity": "sha512-PG9CM9HUYDreb1FbJwFg90TCBQooGjj+n/pb3gw/eH5mEDq0p8wKdLFe0qkiqUkm/Ub5C8DbVFertIo0Vd0zcw==", - "dev": true, - "requires": { - "@evocateur/libnpmaccess": "^3.1.2", - "@evocateur/npm-registry-fetch": "^4.0.0", - "@evocateur/pacote": "^9.6.3", - "@lerna/check-working-tree": "3.16.5", - "@lerna/child-process": "3.16.5", - "@lerna/collect-updates": "3.20.0", - "@lerna/command": "3.21.0", - "@lerna/describe-ref": "3.16.5", - "@lerna/log-packed": "3.16.0", - "@lerna/npm-conf": "3.16.0", - "@lerna/npm-dist-tag": "3.18.5", - "@lerna/npm-publish": "3.18.5", - "@lerna/otplease": "3.18.5", - "@lerna/output": "3.13.0", - "@lerna/pack-directory": "3.16.4", - "@lerna/prerelease-id-from-version": "3.16.0", - "@lerna/prompt": "3.18.5", - "@lerna/pulse-till-done": "3.13.0", - "@lerna/run-lifecycle": "3.16.2", - "@lerna/run-topologically": "3.18.5", - "@lerna/validation-error": "3.13.0", - "@lerna/version": "3.22.1", - "figgy-pudding": "^3.5.1", - "fs-extra": "^8.1.0", - "npm-package-arg": "^6.1.0", - "npmlog": "^4.1.2", - "p-finally": "^1.0.0", - "p-map": "^2.1.0", - "p-pipe": "^1.2.0", - "semver": "^6.2.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@lerna/pulse-till-done": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/pulse-till-done/-/pulse-till-done-3.13.0.tgz", - "integrity": "sha512-1SOHpy7ZNTPulzIbargrgaJX387csN7cF1cLOGZiJQA6VqnS5eWs2CIrG8i8wmaUavj2QlQ5oEbRMVVXSsGrzA==", - "dev": true, - "requires": { - "npmlog": "^4.1.2" - } - }, - "@lerna/query-graph": { - "version": "3.18.5", - "resolved": "https://registry.npmjs.org/@lerna/query-graph/-/query-graph-3.18.5.tgz", - "integrity": "sha512-50Lf4uuMpMWvJ306be3oQDHrWV42nai9gbIVByPBYJuVW8dT8O8pA3EzitNYBUdLL9/qEVbrR0ry1HD7EXwtRA==", - "dev": true, - "requires": { - "@lerna/package-graph": "3.18.5", - "figgy-pudding": "^3.5.1" - } - }, - "@lerna/resolve-symlink": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/@lerna/resolve-symlink/-/resolve-symlink-3.16.0.tgz", - "integrity": "sha512-Ibj5e7njVHNJ/NOqT4HlEgPFPtPLWsO7iu59AM5bJDcAJcR96mLZ7KGVIsS2tvaO7akMEJvt2P+ErwCdloG3jQ==", - "dev": true, - "requires": { - "fs-extra": "^8.1.0", - "npmlog": "^4.1.2", - "read-cmd-shim": "^1.0.1" - } - }, - "@lerna/rimraf-dir": { - "version": "3.16.5", - "resolved": "https://registry.npmjs.org/@lerna/rimraf-dir/-/rimraf-dir-3.16.5.tgz", - "integrity": "sha512-bQlKmO0pXUsXoF8lOLknhyQjOZsCc0bosQDoX4lujBXSWxHVTg1VxURtWf2lUjz/ACsJVDfvHZbDm8kyBk5okA==", - "dev": true, - "requires": { - "@lerna/child-process": "3.16.5", - "npmlog": "^4.1.2", - "path-exists": "^3.0.0", - "rimraf": "^2.6.2" - } - }, - "@lerna/run": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@lerna/run/-/run-3.21.0.tgz", - "integrity": "sha512-fJF68rT3veh+hkToFsBmUJ9MHc9yGXA7LSDvhziAojzOb0AI/jBDp6cEcDQyJ7dbnplba2Lj02IH61QUf9oW0Q==", - "dev": true, - "requires": { - "@lerna/command": "3.21.0", - "@lerna/filter-options": "3.20.0", - "@lerna/npm-run-script": "3.16.5", - "@lerna/output": "3.13.0", - "@lerna/profiler": "3.20.0", - "@lerna/run-topologically": "3.18.5", - "@lerna/timer": "3.13.0", - "@lerna/validation-error": "3.13.0", - "p-map": "^2.1.0" - } - }, - "@lerna/run-lifecycle": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@lerna/run-lifecycle/-/run-lifecycle-3.16.2.tgz", - "integrity": "sha512-RqFoznE8rDpyyF0rOJy3+KjZCeTkO8y/OB9orPauR7G2xQ7PTdCpgo7EO6ZNdz3Al+k1BydClZz/j78gNCmL2A==", - "dev": true, - "requires": { - "@lerna/npm-conf": "3.16.0", - "figgy-pudding": "^3.5.1", - "npm-lifecycle": "^3.1.2", - "npmlog": "^4.1.2" - } - }, - "@lerna/run-topologically": { - "version": "3.18.5", - "resolved": "https://registry.npmjs.org/@lerna/run-topologically/-/run-topologically-3.18.5.tgz", - "integrity": "sha512-6N1I+6wf4hLOnPW+XDZqwufyIQ6gqoPfHZFkfWlvTQ+Ue7CuF8qIVQ1Eddw5HKQMkxqN10thKOFfq/9NQZ4NUg==", - "dev": true, - "requires": { - "@lerna/query-graph": "3.18.5", - "figgy-pudding": "^3.5.1", - "p-queue": "^4.0.0" - } - }, - "@lerna/symlink-binary": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/@lerna/symlink-binary/-/symlink-binary-3.17.0.tgz", - "integrity": "sha512-RLpy9UY6+3nT5J+5jkM5MZyMmjNHxZIZvXLV+Q3MXrf7Eaa1hNqyynyj4RO95fxbS+EZc4XVSk25DGFQbcRNSQ==", - "dev": true, - "requires": { - "@lerna/create-symlink": "3.16.2", - "@lerna/package": "3.16.0", - "fs-extra": "^8.1.0", - "p-map": "^2.1.0" - } - }, - "@lerna/symlink-dependencies": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/@lerna/symlink-dependencies/-/symlink-dependencies-3.17.0.tgz", - "integrity": "sha512-KmjU5YT1bpt6coOmdFueTJ7DFJL4H1w5eF8yAQ2zsGNTtZ+i5SGFBWpb9AQaw168dydc3s4eu0W0Sirda+F59Q==", - "dev": true, - "requires": { - "@lerna/create-symlink": "3.16.2", - "@lerna/resolve-symlink": "3.16.0", - "@lerna/symlink-binary": "3.17.0", - "fs-extra": "^8.1.0", - "p-finally": "^1.0.0", - "p-map": "^2.1.0", - "p-map-series": "^1.0.0" - } - }, - "@lerna/timer": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/timer/-/timer-3.13.0.tgz", - "integrity": "sha512-RHWrDl8U4XNPqY5MQHkToWS9jHPnkLZEt5VD+uunCKTfzlxGnRCr3/zVr8VGy/uENMYpVP3wJa4RKGY6M0vkRw==", - "dev": true - }, - "@lerna/validation-error": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/validation-error/-/validation-error-3.13.0.tgz", - "integrity": "sha512-SiJP75nwB8GhgwLKQfdkSnDufAaCbkZWJqEDlKOUPUvVOplRGnfL+BPQZH5nvq2BYSRXsksXWZ4UHVnQZI/HYA==", - "dev": true, - "requires": { - "npmlog": "^4.1.2" - } - }, - "@lerna/version": { - "version": "3.22.1", - "resolved": "https://registry.npmjs.org/@lerna/version/-/version-3.22.1.tgz", - "integrity": "sha512-PSGt/K1hVqreAFoi3zjD0VEDupQ2WZVlVIwesrE5GbrL2BjXowjCsTDPqblahDUPy0hp6h7E2kG855yLTp62+g==", - "dev": true, - "requires": { - "@lerna/check-working-tree": "3.16.5", - "@lerna/child-process": "3.16.5", - "@lerna/collect-updates": "3.20.0", - "@lerna/command": "3.21.0", - "@lerna/conventional-commits": "3.22.0", - "@lerna/github-client": "3.22.0", - "@lerna/gitlab-client": "3.15.0", - "@lerna/output": "3.13.0", - "@lerna/prerelease-id-from-version": "3.16.0", - "@lerna/prompt": "3.18.5", - "@lerna/run-lifecycle": "3.16.2", - "@lerna/run-topologically": "3.18.5", - "@lerna/validation-error": "3.13.0", - "chalk": "^2.3.1", - "dedent": "^0.7.0", - "load-json-file": "^5.3.0", - "minimatch": "^3.0.4", - "npmlog": "^4.1.2", - "p-map": "^2.1.0", - "p-pipe": "^1.2.0", - "p-reduce": "^1.0.0", - "p-waterfall": "^1.0.0", - "semver": "^6.2.0", - "slash": "^2.0.0", - "temp-write": "^3.4.0", - "write-json-file": "^3.2.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@lerna/write-log-file": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@lerna/write-log-file/-/write-log-file-3.13.0.tgz", - "integrity": "sha512-RibeMnDPvlL8bFYW5C8cs4mbI3AHfQef73tnJCQ/SgrXZHehmHnsyWUiE7qDQCAo+B1RfTapvSyFF69iPj326A==", - "dev": true, - "requires": { - "npmlog": "^4.1.2", - "write-file-atomic": "^2.3.0" - }, - "dependencies": { - "write-file-atomic": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - } - } - }, - "@mrmlnc/readdir-enhanced": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", - "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", - "dev": true, - "requires": { - "call-me-maybe": "^1.0.1", - "glob-to-regexp": "^0.3.0" - } - }, - "@nicolo-ribaudo/chokidar-2": { - "version": "2.1.8-no-fsevents", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.tgz", - "integrity": "sha512-+nb9vWloHNNMFHjGofEam3wopE3m1yuambrrd/fnPc+lFOMB9ROTqQlche9ByFWNkdNqfSgR/kkQtQ8DzEWt2w==", - "dev": true, - "optional": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "@nodelib/fs.scandir": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", - "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.3", - "run-parallel": "^1.1.9" - }, - "dependencies": { - "@nodelib/fs.stat": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", - "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", - "dev": true - } - } - }, - "@nodelib/fs.stat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", - "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", - "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.3", - "fastq": "^1.6.0" - } - }, - "@octokit/auth-token": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.2.tgz", - "integrity": "sha512-jE/lE/IKIz2v1+/P0u4fJqv0kYwXOTujKemJMFr6FeopsxlIK3+wKDCJGnysg81XID5TgZQbIfuJ5J0lnTiuyQ==", - "dev": true, - "requires": { - "@octokit/types": "^5.0.0" - } - }, - "@octokit/endpoint": { - "version": "6.0.8", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.8.tgz", - "integrity": "sha512-MuRrgv+bM4Q+e9uEvxAB/Kf+Sj0O2JAOBA131uo1o6lgdq1iS8ejKwtqHgdfY91V3rN9R/hdGKFiQYMzVzVBEQ==", - "dev": true, - "requires": { - "@octokit/types": "^5.0.0", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - }, - "dependencies": { - "is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true - }, - "universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", - "dev": true - } - } - }, - "@octokit/plugin-enterprise-rest": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz", - "integrity": "sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw==", - "dev": true - }, - "@octokit/plugin-paginate-rest": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz", - "integrity": "sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q==", - "dev": true, - "requires": { - "@octokit/types": "^2.0.1" - }, - "dependencies": { - "@octokit/types": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", - "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", - "dev": true, - "requires": { - "@types/node": ">= 8" - } - } - } - }, - "@octokit/plugin-request-log": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz", - "integrity": "sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw==", - "dev": true - }, - "@octokit/plugin-rest-endpoint-methods": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz", - "integrity": "sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ==", - "dev": true, - "requires": { - "@octokit/types": "^2.0.1", - "deprecation": "^2.3.1" - }, - "dependencies": { - "@octokit/types": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", - "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", - "dev": true, - "requires": { - "@types/node": ">= 8" - } - } - } - }, - "@octokit/request": { - "version": "5.4.9", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.9.tgz", - "integrity": "sha512-CzwVvRyimIM1h2n9pLVYfTDmX9m+KHSgCpqPsY8F1NdEK8IaWqXhSBXsdjOBFZSpEcxNEeg4p0UO9cQ8EnOCLA==", - "dev": true, - "requires": { - "@octokit/endpoint": "^6.0.1", - "@octokit/request-error": "^2.0.0", - "@octokit/types": "^5.0.0", - "deprecation": "^2.0.0", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.1", - "once": "^1.4.0", - "universal-user-agent": "^6.0.0" - }, - "dependencies": { - "@octokit/request-error": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.2.tgz", - "integrity": "sha512-2BrmnvVSV1MXQvEkrb9zwzP0wXFNbPJij922kYBTLIlIafukrGOb+ABBT2+c6wZiuyWDH1K1zmjGQ0toN/wMWw==", - "dev": true, - "requires": { - "@octokit/types": "^5.0.1", - "deprecation": "^2.0.0", - "once": "^1.4.0" - } - }, - "is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true - }, - "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "dev": true - }, - "universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", - "dev": true - } - } - }, - "@octokit/request-error": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz", - "integrity": "sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA==", - "dev": true, - "requires": { - "@octokit/types": "^2.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - }, - "dependencies": { - "@octokit/types": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", - "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", - "dev": true, - "requires": { - "@types/node": ">= 8" - } - } - } - }, - "@octokit/rest": { - "version": "16.43.2", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.43.2.tgz", - "integrity": "sha512-ngDBevLbBTFfrHZeiS7SAMAZ6ssuVmXuya+F/7RaVvlysgGa1JKJkKWY+jV6TCJYcW0OALfJ7nTIGXcBXzycfQ==", - "dev": true, - "requires": { - "@octokit/auth-token": "^2.4.0", - "@octokit/plugin-paginate-rest": "^1.1.1", - "@octokit/plugin-request-log": "^1.0.0", - "@octokit/plugin-rest-endpoint-methods": "2.4.0", - "@octokit/request": "^5.2.0", - "@octokit/request-error": "^1.0.2", - "atob-lite": "^2.0.0", - "before-after-hook": "^2.0.0", - "btoa-lite": "^1.0.0", - "deprecation": "^2.0.0", - "lodash.get": "^4.4.2", - "lodash.set": "^4.3.2", - "lodash.uniq": "^4.5.0", - "octokit-pagination-methods": "^1.1.0", - "once": "^1.4.0", - "universal-user-agent": "^4.0.0" - } - }, - "@octokit/types": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-5.5.0.tgz", - "integrity": "sha512-UZ1pErDue6bZNjYOotCNveTXArOMZQFG6hKJfOnGnulVCMcVVi7YIIuuR4WfBhjo7zgpmzn/BkPDnUXtNx+PcQ==", - "dev": true, - "requires": { - "@types/node": ">= 8" - } - }, - "@samverschueren/stream-to-observable": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz", - "integrity": "sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==", - "dev": true, - "requires": { - "any-observable": "^0.3.0" - } - }, - "@sinonjs/commons": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", - "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/fake-timers": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", - "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, - "@tannin/compile": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@tannin/compile/-/compile-1.1.0.tgz", - "integrity": "sha512-n8m9eNDfoNZoxdvWiTfW/hSPhehzLJ3zW7f8E7oT6mCROoMNWCB4TYtv041+2FMAxweiE0j7i1jubQU4MEC/Gg==", - "dev": true, - "requires": { - "@tannin/evaluate": "^1.2.0", - "@tannin/postfix": "^1.1.0" - } - }, - "@tannin/evaluate": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@tannin/evaluate/-/evaluate-1.2.0.tgz", - "integrity": "sha512-3ioXvNowbO/wSrxsDG5DKIMxC81P0QrQTYai8zFNY+umuoHWRPbQ/TuuDEOju9E+jQDXmj6yI5GyejNuh8I+eg==", - "dev": true - }, - "@tannin/plural-forms": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@tannin/plural-forms/-/plural-forms-1.1.0.tgz", - "integrity": "sha512-xl9R2mDZO/qiHam1AgMnAES6IKIg7OBhcXqy6eDsRCdXuxAFPcjrej9HMjyCLE0DJ/8cHf0i5OQTstuBRhpbHw==", - "dev": true, - "requires": { - "@tannin/compile": "^1.1.0" - } - }, - "@tannin/postfix": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@tannin/postfix/-/postfix-1.1.0.tgz", - "integrity": "sha512-oocsqY7g0cR+Gur5jRQLSrX2OtpMLMse1I10JQBm8CdGMrDkh1Mg2gjsiquMHRtBs4Qwu5wgEp5GgIYHk4SNPw==", - "dev": true - }, - "@types/babel__core": { - "version": "7.1.10", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.10.tgz", - "integrity": "sha512-x8OM8XzITIMyiwl5Vmo2B1cR1S1Ipkyv4mdlbJjMa1lmuKvKY9FrBbEANIaMlnWn5Rf7uO+rC/VgYabNkE17Hw==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "@types/babel__generator": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.2.tgz", - "integrity": "sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@types/babel__template": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.3.tgz", - "integrity": "sha512-uCoznIPDmnickEi6D0v11SBpW0OuVqHJCa7syXqQHy5uktSCreIlt0iglsCnmvz8yCb38hGcWeseA8cWJSwv5Q==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@types/babel__traverse": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.15.tgz", - "integrity": "sha512-Pzh9O3sTK8V6I1olsXpCfj2k/ygO2q1X0vhhnDrEQyYLHZesWz+zMZMVcwXLCYf0U36EtmyYaFGPfXlTtDHe3A==", - "dev": true, - "requires": { - "@babel/types": "^7.3.0" - } - }, - "@types/cheerio": { - "version": "0.22.22", - "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.22.tgz", - "integrity": "sha512-05DYX4zU96IBfZFY+t3Mh88nlwSMtmmzSYaQkKN48T495VV1dkHSah6qYyDTN5ngaS0i0VonH37m+RuzSM0YiA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true - }, - "@types/eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", - "dev": true - }, - "@types/glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", - "dev": true, - "requires": { - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "@types/graceful-fs": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.3.tgz", - "integrity": "sha512-AiHRaEB50LQg0pZmm659vNBb9f4SJ0qrAnteuzhSeAUcJKxoYgEnprg/83kppCnc2zvtCKbdZry1a5pVY3lOTQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", - "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", - "dev": true - }, - "@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*" - } - }, - "@types/istanbul-reports": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", - "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*", - "@types/istanbul-lib-report": "*" - } - }, - "@types/json-schema": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", - "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", - "dev": true - }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true - }, - "@types/minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=", - "dev": true - }, - "@types/node": { - "version": "13.13.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.5.tgz", - "integrity": "sha512-3ySmiBYJPqgjiHA7oEaIo2Rzz0HrOZ7yrNO5HWyaE5q0lQ3BppDZ3N53Miz8bw2I7gh1/zir2MGVZBvpb1zq9g==", - "dev": true - }, - "@types/normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", - "dev": true - }, - "@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "dev": true - }, - "@types/prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==", - "dev": true - }, - "@types/stack-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", - "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", - "dev": true - }, - "@types/unist": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", - "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==", - "dev": true - }, - "@types/vfile": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/vfile/-/vfile-3.0.2.tgz", - "integrity": "sha512-b3nLFGaGkJ9rzOcuXRfHkZMdjsawuDD0ENL9fzTophtBg8FJHSGbH7daXkEpcwy3v7Xol3pAvsmlYyFhR4pqJw==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/unist": "*", - "@types/vfile-message": "*" - } - }, - "@types/vfile-message": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/vfile-message/-/vfile-message-2.0.0.tgz", - "integrity": "sha512-GpTIuDpb9u4zIO165fUy9+fXcULdD8HFRNli04GehoMVbeNq7D6OBnqSmg3lxZnC+UvgUhEWKxdKiwYUkGltIw==", - "dev": true, - "requires": { - "vfile-message": "*" - } - }, - "@types/yargs": { - "version": "15.0.7", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.7.tgz", - "integrity": "sha512-Gf4u3EjaPNcC9cTu4/j2oN14nSVhr8PQ+BvBcBQHAhDZfl0bVIiLgvnRXv/dn58XhTm9UXvBpvJpDlwV65QxOA==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "@types/yargs-parser": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", - "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.1.tgz", - "integrity": "sha512-PQg0emRtzZFWq6PxBcdxRH3QIQiyFO3WCVpRL3fgj5oQS3CDs3AeAKfv4DxNhzn8ITdNJGJ4D3Qw8eAJf3lXeQ==", - "dev": true, - "requires": { - "@typescript-eslint/experimental-utils": "3.10.1", - "debug": "^4.1.1", - "functional-red-black-tree": "^1.0.1", - "regexpp": "^3.0.0", - "semver": "^7.3.2", - "tsutils": "^3.17.1" - }, - "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", - "dev": true - } - } - }, - "@typescript-eslint/experimental-utils": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz", - "integrity": "sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/types": "3.10.1", - "@typescript-eslint/typescript-estree": "3.10.1", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" - } - }, - "@typescript-eslint/parser": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.10.1.tgz", - "integrity": "sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw==", - "dev": true, - "requires": { - "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "3.10.1", - "@typescript-eslint/types": "3.10.1", - "@typescript-eslint/typescript-estree": "3.10.1", - "eslint-visitor-keys": "^1.1.0" - } - }, - "@typescript-eslint/types": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz", - "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz", - "integrity": "sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==", - "dev": true, - "requires": { - "@typescript-eslint/types": "3.10.1", - "@typescript-eslint/visitor-keys": "3.10.1", - "debug": "^4.1.1", - "glob": "^7.1.6", - "is-glob": "^4.0.1", - "lodash": "^4.17.15", - "semver": "^7.3.2", - "tsutils": "^3.17.1" - }, - "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", - "dev": true - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz", - "integrity": "sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "@webassemblyjs/ast": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", - "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", - "dev": true, - "requires": { - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0" - } - }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", - "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==", - "dev": true - }, - "@webassemblyjs/helper-api-error": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", - "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", - "dev": true - }, - "@webassemblyjs/helper-buffer": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", - "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==", - "dev": true - }, - "@webassemblyjs/helper-code-frame": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", - "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", - "dev": true, - "requires": { - "@webassemblyjs/wast-printer": "1.9.0" - } - }, - "@webassemblyjs/helper-fsm": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", - "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==", - "dev": true - }, - "@webassemblyjs/helper-module-context": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", - "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0" - } - }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", - "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", - "dev": true - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", - "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0" - } - }, - "@webassemblyjs/ieee754": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", - "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", - "dev": true, - "requires": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "@webassemblyjs/leb128": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", - "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", - "dev": true, - "requires": { - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/utf8": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", - "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==", - "dev": true - }, - "@webassemblyjs/wasm-edit": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", - "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/helper-wasm-section": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0", - "@webassemblyjs/wasm-opt": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "@webassemblyjs/wast-printer": "1.9.0" - } - }, - "@webassemblyjs/wasm-gen": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", - "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/ieee754": "1.9.0", - "@webassemblyjs/leb128": "1.9.0", - "@webassemblyjs/utf8": "1.9.0" - } - }, - "@webassemblyjs/wasm-opt": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", - "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0" - } - }, - "@webassemblyjs/wasm-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", - "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-api-error": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/ieee754": "1.9.0", - "@webassemblyjs/leb128": "1.9.0", - "@webassemblyjs/utf8": "1.9.0" - } - }, - "@webassemblyjs/wast-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", - "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/floating-point-hex-parser": "1.9.0", - "@webassemblyjs/helper-api-error": "1.9.0", - "@webassemblyjs/helper-code-frame": "1.9.0", - "@webassemblyjs/helper-fsm": "1.9.0", - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/wast-printer": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", - "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0", - "@xtuc/long": "4.2.2" - } - }, - "@woocommerce/api": { - "version": "file:tests/e2e/api", - "dev": true, - "requires": { - "axios": "0.19.2", - "create-hmac": "1.1.7", - "oauth-1.0a": "2.2.6" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/core": { - "version": "7.11.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.6.tgz", - "integrity": "sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==", - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.11.6", - "@babel/helper-module-transforms": "^7.11.0", - "@babel/helpers": "^7.10.4", - "@babel/parser": "^7.11.5", - "@babel/template": "^7.10.4", - "@babel/traverse": "^7.11.5", - "@babel/types": "^7.11.5", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.1", - "json5": "^2.1.2", - "lodash": "^4.17.19", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } - } - }, - "@babel/generator": { - "version": "7.11.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", - "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", - "requires": { - "@babel/types": "^7.11.5", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } - } - }, - "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz", - "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==", - "requires": { - "@babel/types": "^7.11.0" - } - }, - "@babel/helper-module-imports": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", - "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-module-transforms": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz", - "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==", - "requires": { - "@babel/helper-module-imports": "^7.10.4", - "@babel/helper-replace-supers": "^7.10.4", - "@babel/helper-simple-access": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/template": "^7.10.4", - "@babel/types": "^7.11.0", - "lodash": "^4.17.19" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", - "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==" - }, - "@babel/helper-replace-supers": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", - "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", - "requires": { - "@babel/helper-member-expression-to-functions": "^7.10.4", - "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/traverse": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-simple-access": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", - "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", - "requires": { - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", - "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", - "requires": { - "@babel/types": "^7.11.0" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==" - }, - "@babel/helpers": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz", - "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==", - "requires": { - "@babel/template": "^7.10.4", - "@babel/traverse": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/parser": { - "version": "7.11.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", - "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==" - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz", - "integrity": "sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA==", - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/traverse": { - "version": "7.11.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", - "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.11.5", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.11.5", - "@babel/types": "^7.11.5", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" - }, - "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "@babel/types": { - "version": "7.11.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", - "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, - "@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" - }, - "@cnakazawa/watch": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", - "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", - "requires": { - "exec-sh": "^0.3.2", - "minimist": "^1.2.0" - } - }, - "@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "requires": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - } - }, - "@istanbuljs/schema": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", - "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==" - }, - "@jest/console": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-25.5.0.tgz", - "integrity": "sha512-T48kZa6MK1Y6k4b89sexwmSF4YLeZS/Udqg3Jj3jG/cHH+N/sLFCEoXEDMOKugJQ9FxPN1osxIknvKkxt6MKyw==", - "requires": { - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "jest-message-util": "^25.5.0", - "jest-util": "^25.5.0", - "slash": "^3.0.0" - } - }, - "@jest/core": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-25.5.4.tgz", - "integrity": "sha512-3uSo7laYxF00Dg/DMgbn4xMJKmDdWvZnf89n8Xj/5/AeQ2dOQmn6b6Hkj/MleyzZWXpwv+WSdYWl4cLsy2JsoA==", - "requires": { - "@jest/console": "^25.5.0", - "@jest/reporters": "^25.5.1", - "@jest/test-result": "^25.5.0", - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "ansi-escapes": "^4.2.1", - "chalk": "^3.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-changed-files": "^25.5.0", - "jest-config": "^25.5.4", - "jest-haste-map": "^25.5.1", - "jest-message-util": "^25.5.0", - "jest-regex-util": "^25.2.6", - "jest-resolve": "^25.5.1", - "jest-resolve-dependencies": "^25.5.4", - "jest-runner": "^25.5.4", - "jest-runtime": "^25.5.4", - "jest-snapshot": "^25.5.1", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", - "jest-watcher": "^25.5.0", - "micromatch": "^4.0.2", - "p-each-series": "^2.1.0", - "realpath-native": "^2.0.0", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "@jest/environment": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.5.0.tgz", - "integrity": "sha512-U2VXPEqL07E/V7pSZMSQCvV5Ea4lqOlT+0ZFijl/i316cRMHvZ4qC+jBdryd+lmRetjQo0YIQr6cVPNxxK87mA==", - "requires": { - "@jest/fake-timers": "^25.5.0", - "@jest/types": "^25.5.0", - "jest-mock": "^25.5.0" - } - }, - "@jest/fake-timers": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.5.0.tgz", - "integrity": "sha512-9y2+uGnESw/oyOI3eww9yaxdZyHq7XvprfP/eeoCsjqKYts2yRlsHS/SgjPDV8FyMfn2nbMy8YzUk6nyvdLOpQ==", - "requires": { - "@jest/types": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-util": "^25.5.0", - "lolex": "^5.0.0" - } - }, - "@jest/globals": { - "version": "25.5.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-25.5.2.tgz", - "integrity": "sha512-AgAS/Ny7Q2RCIj5kZ+0MuKM1wbF0WMLxbCVl/GOMoCNbODRdJ541IxJ98xnZdVSZXivKpJlNPIWa3QmY0l4CXA==", - "requires": { - "@jest/environment": "^25.5.0", - "@jest/types": "^25.5.0", - "expect": "^25.5.0" - } - }, - "@jest/reporters": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-25.5.1.tgz", - "integrity": "sha512-3jbd8pPDTuhYJ7vqiHXbSwTJQNavczPs+f1kRprRDxETeE3u6srJ+f0NPuwvOmk+lmunZzPkYWIFZDLHQPkviw==", - "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.2.4", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^4.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.2", - "jest-haste-map": "^25.5.1", - "jest-resolve": "^25.5.1", - "jest-util": "^25.5.0", - "jest-worker": "^25.5.0", - "node-notifier": "^6.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.0", - "string-length": "^3.1.0", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^4.1.3" - } - }, - "@jest/source-map": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-25.5.0.tgz", - "integrity": "sha512-eIGx0xN12yVpMcPaVpjXPnn3N30QGJCJQSkEDUt9x1fI1Gdvb07Ml6K5iN2hG7NmMP6FDmtPEssE3z6doOYUwQ==", - "requires": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.4", - "source-map": "^0.6.0" - } - }, - "@jest/test-result": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-25.5.0.tgz", - "integrity": "sha512-oV+hPJgXN7IQf/fHWkcS99y0smKLU2czLBJ9WA0jHITLst58HpQMtzSYxzaBvYc6U5U6jfoMthqsUlUlbRXs0A==", - "requires": { - "@jest/console": "^25.5.0", - "@jest/types": "^25.5.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - } - }, - "@jest/test-sequencer": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-25.5.4.tgz", - "integrity": "sha512-pTJGEkSeg1EkCO2YWq6hbFvKNXk8ejqlxiOg1jBNLnWrgXOkdY6UmqZpwGFXNnRt9B8nO1uWMzLLZ4eCmhkPNA==", - "requires": { - "@jest/test-result": "^25.5.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^25.5.1", - "jest-runner": "^25.5.4", - "jest-runtime": "^25.5.4" - } - }, - "@jest/transform": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.5.1.tgz", - "integrity": "sha512-Y8CEoVwXb4QwA6Y/9uDkn0Xfz0finGkieuV0xkdF9UtZGJeLukD5nLkaVrVsODB1ojRWlaoD0AJZpVHCSnJEvg==", - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^25.5.0", - "babel-plugin-istanbul": "^6.0.0", - "chalk": "^3.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^25.5.1", - "jest-regex-util": "^25.2.6", - "jest-util": "^25.5.0", - "micromatch": "^4.0.2", - "pirates": "^4.0.1", - "realpath-native": "^2.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - } - }, - "@jest/types": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", - "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", - "@types/yargs": "^15.0.0", - "chalk": "^3.0.0" - } - }, - "@sinonjs/commons": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", - "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==", - "requires": { - "type-detect": "4.0.8" - } - }, - "@types/babel__core": { - "version": "7.1.10", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.10.tgz", - "integrity": "sha512-x8OM8XzITIMyiwl5Vmo2B1cR1S1Ipkyv4mdlbJjMa1lmuKvKY9FrBbEANIaMlnWn5Rf7uO+rC/VgYabNkE17Hw==", - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "@types/babel__generator": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.1.tgz", - "integrity": "sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew==", - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@types/babel__template": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.3.tgz", - "integrity": "sha512-uCoznIPDmnickEi6D0v11SBpW0OuVqHJCa7syXqQHy5uktSCreIlt0iglsCnmvz8yCb38hGcWeseA8cWJSwv5Q==", - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@types/babel__traverse": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.14.tgz", - "integrity": "sha512-8w9szzKs14ZtBVuP6Wn7nMLRJ0D6dfB0VEBEyRgxrZ/Ln49aNMykrghM2FaNn4FJRzNppCSa0Rv9pBRM5Xc3wg==", - "requires": { - "@babel/types": "^7.3.0" - } - }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" - }, - "@types/create-hmac": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@types/create-hmac/-/create-hmac-1.1.0.tgz", - "integrity": "sha512-BNYNdzdhOZZQWCOpwvIll3FSvgo3e55Y2M6s/jOY6TuOCwqt3cLmQsK4tSmJ5fayDot8EG4k3+hcZagfww9JlQ==", - "requires": { - "@types/node": "*" - } - }, - "@types/graceful-fs": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.3.tgz", - "integrity": "sha512-AiHRaEB50LQg0pZmm659vNBb9f4SJ0qrAnteuzhSeAUcJKxoYgEnprg/83kppCnc2zvtCKbdZry1a5pVY3lOTQ==", - "requires": { - "@types/node": "*" - } - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", - "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==" - }, - "@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "requires": { - "@types/istanbul-lib-coverage": "*" - } - }, - "@types/istanbul-reports": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", - "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", - "requires": { - "@types/istanbul-lib-coverage": "*", - "@types/istanbul-lib-report": "*" - } - }, - "@types/jest": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-25.2.1.tgz", - "integrity": "sha512-msra1bCaAeEdkSyA0CZ6gW1ukMIvZ5YoJkdXw/qhQdsuuDlFTcEUrUw8CLCPt2rVRUfXlClVvK2gvPs9IokZaA==", - "requires": { - "jest-diff": "^25.2.1", - "pretty-format": "^25.2.1" - } - }, - "@types/moxios": { - "version": "0.4.9", - "resolved": "https://registry.npmjs.org/@types/moxios/-/moxios-0.4.9.tgz", - "integrity": "sha512-Sd1b24QRW2N194j2LEDPQAZK1h0TBtpN+2EIH+rERCgm38qm14JZwC7NlpE7n3jULhlCIPZBG8uNcbjF8KcCaQ==", - "requires": { - "axios": "^0.19.0" - } - }, - "@types/node": { - "version": "13.13.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.5.tgz", - "integrity": "sha512-3ySmiBYJPqgjiHA7oEaIo2Rzz0HrOZ7yrNO5HWyaE5q0lQ3BppDZ3N53Miz8bw2I7gh1/zir2MGVZBvpb1zq9g==" - }, - "@types/normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==" - }, - "@types/prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==" - }, - "@types/stack-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", - "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==" - }, - "@types/yargs": { - "version": "15.0.7", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.7.tgz", - "integrity": "sha512-Gf4u3EjaPNcC9cTu4/j2oN14nSVhr8PQ+BvBcBQHAhDZfl0bVIiLgvnRXv/dn58XhTm9UXvBpvJpDlwV65QxOA==", - "requires": { - "@types/yargs-parser": "*" - } - }, - "@types/yargs-parser": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", - "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==" - }, - "abab": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", - "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==" - }, - "acorn": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", - "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==" - }, - "acorn-globals": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", - "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", - "requires": { - "acorn": "^6.0.1", - "acorn-walk": "^6.0.1" - }, - "dependencies": { - "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==" - } - } - }, - "acorn-walk": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", - "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==" - }, - "ajv": { - "version": "6.12.5", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz", - "integrity": "sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-escapes": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", - "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", - "requires": { - "type-fest": "^0.11.0" - }, - "dependencies": { - "type-fest": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", - "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==" - } - } - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" - }, - "array-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz", - "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==" - }, - "axios": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", - "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", - "requires": { - "follow-redirects": "1.5.10" - } - }, - "babel-jest": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.5.1.tgz", - "integrity": "sha512-9dA9+GmMjIzgPnYtkhBg73gOo/RHqPmLruP3BaGL4KEX3Dwz6pI8auSN8G8+iuEG90+GSswyKvslN+JYSaacaQ==", - "requires": { - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "@types/babel__core": "^7.1.7", - "babel-plugin-istanbul": "^6.0.0", - "babel-preset-jest": "^25.5.0", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "slash": "^3.0.0" - } - }, - "babel-plugin-istanbul": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", - "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^4.0.0", - "test-exclude": "^6.0.0" - } - }, - "babel-plugin-jest-hoist": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.5.0.tgz", - "integrity": "sha512-u+/W+WAjMlvoocYGTwthAiQSxDcJAyHpQ6oWlHdFZaaN+Rlk8Q7iiwDPg2lN/FyJtAYnKjFxbn7xus4HCFkg5g==", - "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__traverse": "^7.0.6" - } - }, - "babel-preset-current-node-syntax": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.3.tgz", - "integrity": "sha512-uyexu1sVwcdFnyq9o8UQYsXwXflIh8LvrF5+cKrYam93ned1CStffB3+BEcsxGSgagoA3GEyjDqO4a/58hyPYQ==", - "requires": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - } - }, - "babel-preset-jest": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-25.5.0.tgz", - "integrity": "sha512-8ZczygctQkBU+63DtSOKGh7tFL0CeCuz+1ieud9lJ1WPQ9O6A1a/r+LGn6Y705PA6whHQ3T1XuB/PmpfNYf8Fw==", - "requires": { - "babel-plugin-jest-hoist": "^25.5.0", - "babel-preset-current-node-syntax": "^0.1.2" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { - "fill-range": "^7.0.1" - } - }, - "browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" - }, - "browser-resolve": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", - "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", - "requires": { - "resolve": "1.1.7" - }, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" - } - } - }, - "bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "requires": { - "fast-json-stable-stringify": "2.x" - } - }, - "bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "requires": { - "node-int64": "^0.4.0" - } - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, - "capture-exit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", - "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", - "requires": { - "rsvp": "^4.8.4" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==" - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "requires": { - "safe-buffer": "~5.1.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==" - }, - "cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "requires": { - "cssom": "~0.3.6" - }, - "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" - } - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "data-urls": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", - "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", - "requires": { - "abab": "^2.0.0", - "whatwg-mimetype": "^2.2.0", - "whatwg-url": "^7.0.0" - } - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" - }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==" - }, - "diff-sequences": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", - "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==" - }, - "domexception": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", - "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", - "requires": { - "webidl-conversions": "^4.0.2" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { - "once": "^1.4.0" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "requires": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "exec-sh": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", - "integrity": "sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==" - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expect": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-25.5.0.tgz", - "integrity": "sha512-w7KAXo0+6qqZZhovCaBVPSIqQp7/UTcx4M9uKt2m6pd2VB1voyC8JizLRqeEqud3AAVP02g+hbErDu5gu64tlA==", - "requires": { - "@jest/types": "^25.5.0", - "ansi-styles": "^4.0.0", - "jest-get-type": "^25.2.6", - "jest-matcher-utils": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-regex-util": "^25.2.6" - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" - }, - "fb-watchman": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", - "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", - "requires": { - "bser": "2.1.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "requires": { - "debug": "=3.1.0" - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "requires": { - "map-cache": "^0.2.2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "optional": true - }, - "gensync": { - "version": "1.0.0-beta.1", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", - "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==" - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==" - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "requires": { - "pump": "^3.0.0" - } - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" - }, - "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" - }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "optional": true - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dev": true, - "requires": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - } - }, - "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" - }, - "html-encoding-sniffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", - "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", - "requires": { - "whatwg-encoding": "^1.0.1" - } - }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==" - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "import-local": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", - "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "requires": { - "ci-info": "^2.0.0" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "is-docker": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", - "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==", - "optional": true - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==" - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "requires": { - "isobject": "^3.0.1" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" - }, - "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "optional": true, - "requires": { - "is-docker": "^2.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==" - }, - "istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", - "requires": { - "@babel/core": "^7.7.5", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - } - }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", - "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "jest": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest/-/jest-25.5.4.tgz", - "integrity": "sha512-hHFJROBTqZahnO+X+PMtT6G2/ztqAZJveGqz//FnWWHurizkD05PQGzRZOhF3XP6z7SJmL+5tCfW8qV06JypwQ==", - "requires": { - "@jest/core": "^25.5.4", - "import-local": "^3.0.2", - "jest-cli": "^25.5.4" - }, - "dependencies": { - "jest-cli": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-25.5.4.tgz", - "integrity": "sha512-rG8uJkIiOUpnREh1768/N3n27Cm+xPFkSNFO91tgg+8o2rXeVLStz+vkXkGr4UtzH6t1SNbjwoiswd7p4AhHTw==", - "requires": { - "@jest/core": "^25.5.4", - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "import-local": "^3.0.2", - "is-ci": "^2.0.0", - "jest-config": "^25.5.4", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", - "prompts": "^2.0.1", - "realpath-native": "^2.0.0", - "yargs": "^15.3.1" - } - } - } - }, - "jest-changed-files": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-25.5.0.tgz", - "integrity": "sha512-EOw9QEqapsDT7mKF162m8HFzRPbmP8qJQny6ldVOdOVBz3ACgPm/1nAn5fPQ/NDaYhX/AHkrGwwkCncpAVSXcw==", - "requires": { - "@jest/types": "^25.5.0", - "execa": "^3.2.0", - "throat": "^5.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "execa": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz", - "integrity": "sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==", - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "p-finally": "^2.0.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "requires": { - "pump": "^3.0.0" - } - }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "requires": { - "path-key": "^3.0.0" - } - }, - "p-finally": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", - "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==" - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - } - } - }, - "jest-config": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-25.5.4.tgz", - "integrity": "sha512-SZwR91SwcdK6bz7Gco8qL7YY2sx8tFJYzvg216DLihTWf+LKY/DoJXpM9nTzYakSyfblbqeU48p/p7Jzy05Atg==", - "requires": { - "@babel/core": "^7.1.0", - "@jest/test-sequencer": "^25.5.4", - "@jest/types": "^25.5.0", - "babel-jest": "^25.5.1", - "chalk": "^3.0.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.1", - "graceful-fs": "^4.2.4", - "jest-environment-jsdom": "^25.5.0", - "jest-environment-node": "^25.5.0", - "jest-get-type": "^25.2.6", - "jest-jasmine2": "^25.5.4", - "jest-regex-util": "^25.2.6", - "jest-resolve": "^25.5.1", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", - "micromatch": "^4.0.2", - "pretty-format": "^25.5.0", - "realpath-native": "^2.0.0" - } - }, - "jest-diff": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", - "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", - "requires": { - "chalk": "^3.0.0", - "diff-sequences": "^25.2.6", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - } - }, - "jest-docblock": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-25.3.0.tgz", - "integrity": "sha512-aktF0kCar8+zxRHxQZwxMy70stc9R1mOmrLsT5VO3pIT0uzGRSDAXxSlz4NqQWpuLjPpuMhPRl7H+5FRsvIQAg==", - "requires": { - "detect-newline": "^3.0.0" - } - }, - "jest-each": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-25.5.0.tgz", - "integrity": "sha512-QBogUxna3D8vtiItvn54xXde7+vuzqRrEeaw8r1s+1TG9eZLVJE5ZkKoSUlqFwRjnlaA4hyKGiu9OlkFIuKnjA==", - "requires": { - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "jest-get-type": "^25.2.6", - "jest-util": "^25.5.0", - "pretty-format": "^25.5.0" - } - }, - "jest-environment-jsdom": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.5.0.tgz", - "integrity": "sha512-7Jr02ydaq4jaWMZLY+Skn8wL5nVIYpWvmeatOHL3tOcV3Zw8sjnPpx+ZdeBfc457p8jCR9J6YCc+Lga0oIy62A==", - "requires": { - "@jest/environment": "^25.5.0", - "@jest/fake-timers": "^25.5.0", - "@jest/types": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-util": "^25.5.0", - "jsdom": "^15.2.1" - } - }, - "jest-environment-node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-25.5.0.tgz", - "integrity": "sha512-iuxK6rQR2En9EID+2k+IBs5fCFd919gVVK5BeND82fYeLWPqvRcFNPKu9+gxTwfB5XwBGBvZ0HFQa+cHtIoslA==", - "requires": { - "@jest/environment": "^25.5.0", - "@jest/fake-timers": "^25.5.0", - "@jest/types": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-util": "^25.5.0", - "semver": "^6.3.0" - } - }, - "jest-get-type": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", - "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==" - }, - "jest-haste-map": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.5.1.tgz", - "integrity": "sha512-dddgh9UZjV7SCDQUrQ+5t9yy8iEgKc1AKqZR9YDww8xsVOtzPQSMVLDChc21+g29oTRexb9/B0bIlZL+sWmvAQ==", - "requires": { - "@jest/types": "^25.5.0", - "@types/graceful-fs": "^4.1.2", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.1.2", - "graceful-fs": "^4.2.4", - "jest-serializer": "^25.5.0", - "jest-util": "^25.5.0", - "jest-worker": "^25.5.0", - "micromatch": "^4.0.2", - "sane": "^4.0.3", - "walker": "^1.0.7", - "which": "^2.0.2" - } - }, - "jest-jasmine2": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-25.5.4.tgz", - "integrity": "sha512-9acbWEfbmS8UpdcfqnDO+uBUgKa/9hcRh983IHdM+pKmJPL77G0sWAAK0V0kr5LK3a8cSBfkFSoncXwQlRZfkQ==", - "requires": { - "@babel/traverse": "^7.1.0", - "@jest/environment": "^25.5.0", - "@jest/source-map": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "co": "^4.6.0", - "expect": "^25.5.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^25.5.0", - "jest-matcher-utils": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-runtime": "^25.5.4", - "jest-snapshot": "^25.5.1", - "jest-util": "^25.5.0", - "pretty-format": "^25.5.0", - "throat": "^5.0.0" - } - }, - "jest-leak-detector": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-25.5.0.tgz", - "integrity": "sha512-rV7JdLsanS8OkdDpZtgBf61L5xZ4NnYLBq72r6ldxahJWWczZjXawRsoHyXzibM5ed7C2QRjpp6ypgwGdKyoVA==", - "requires": { - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - } - }, - "jest-matcher-utils": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz", - "integrity": "sha512-VWI269+9JS5cpndnpCwm7dy7JtGQT30UHfrnM3mXl22gHGt/b7NkjBqXfbhZ8V4B7ANUsjK18PlSBmG0YH7gjw==", - "requires": { - "chalk": "^3.0.0", - "jest-diff": "^25.5.0", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - } - }, - "jest-message-util": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.5.0.tgz", - "integrity": "sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA==", - "requires": { - "@babel/code-frame": "^7.0.0", - "@jest/types": "^25.5.0", - "@types/stack-utils": "^1.0.1", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "micromatch": "^4.0.2", - "slash": "^3.0.0", - "stack-utils": "^1.0.1" - } - }, - "jest-mock": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.5.0.tgz", - "integrity": "sha512-eXWuTV8mKzp/ovHc5+3USJMYsTBhyQ+5A1Mak35dey/RG8GlM4YWVylZuGgVXinaW6tpvk/RSecmF37FKUlpXA==", - "requires": { - "@jest/types": "^25.5.0" - } - }, - "jest-mock-extended": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-1.0.10.tgz", - "integrity": "sha512-R2wKiOgEUPoHZ2kLsAQeQP2IfVEgo3oQqWLSXKdMXK06t3UHkQirA2Xnsdqg/pX6KPWTsdnrzE2ig6nqNjdgVw==", - "requires": { - "ts-essentials": "^4.0.0" - } - }, - "jest-pnp-resolver": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", - "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==" - }, - "jest-regex-util": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", - "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==" - }, - "jest-resolve": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-25.5.1.tgz", - "integrity": "sha512-Hc09hYch5aWdtejsUZhA+vSzcotf7fajSlPA6EZPE1RmPBAD39XtJhvHWFStid58iit4IPDLI/Da4cwdDmAHiQ==", - "requires": { - "@jest/types": "^25.5.0", - "browser-resolve": "^1.11.3", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "jest-pnp-resolver": "^1.2.1", - "read-pkg-up": "^7.0.1", - "realpath-native": "^2.0.0", - "resolve": "^1.17.0", - "slash": "^3.0.0" - } - }, - "jest-resolve-dependencies": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-25.5.4.tgz", - "integrity": "sha512-yFmbPd+DAQjJQg88HveObcGBA32nqNZ02fjYmtL16t1xw9bAttSn5UGRRhzMHIQbsep7znWvAvnD4kDqOFM0Uw==", - "requires": { - "@jest/types": "^25.5.0", - "jest-regex-util": "^25.2.6", - "jest-snapshot": "^25.5.1" - } - }, - "jest-runner": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-25.5.4.tgz", - "integrity": "sha512-V/2R7fKZo6blP8E9BL9vJ8aTU4TH2beuqGNxHbxi6t14XzTb+x90B3FRgdvuHm41GY8ch4xxvf0ATH4hdpjTqg==", - "requires": { - "@jest/console": "^25.5.0", - "@jest/environment": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-config": "^25.5.4", - "jest-docblock": "^25.3.0", - "jest-haste-map": "^25.5.1", - "jest-jasmine2": "^25.5.4", - "jest-leak-detector": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-resolve": "^25.5.1", - "jest-runtime": "^25.5.4", - "jest-util": "^25.5.0", - "jest-worker": "^25.5.0", - "source-map-support": "^0.5.6", - "throat": "^5.0.0" - } - }, - "jest-runtime": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-25.5.4.tgz", - "integrity": "sha512-RWTt8LeWh3GvjYtASH2eezkc8AehVoWKK20udV6n3/gC87wlTbE1kIA+opCvNWyyPeBs6ptYsc6nyHUb1GlUVQ==", - "requires": { - "@jest/console": "^25.5.0", - "@jest/environment": "^25.5.0", - "@jest/globals": "^25.5.2", - "@jest/source-map": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "@types/yargs": "^15.0.0", - "chalk": "^3.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.4", - "jest-config": "^25.5.4", - "jest-haste-map": "^25.5.1", - "jest-message-util": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-regex-util": "^25.2.6", - "jest-resolve": "^25.5.1", - "jest-snapshot": "^25.5.1", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", - "realpath-native": "^2.0.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0", - "yargs": "^15.3.1" - } - }, - "jest-serializer": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.5.0.tgz", - "integrity": "sha512-LxD8fY1lByomEPflwur9o4e2a5twSQ7TaVNLlFUuToIdoJuBt8tzHfCsZ42Ok6LkKXWzFWf3AGmheuLAA7LcCA==", - "requires": { - "graceful-fs": "^4.2.4" - } - }, - "jest-snapshot": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-25.5.1.tgz", - "integrity": "sha512-C02JE1TUe64p2v1auUJ2ze5vcuv32tkv9PyhEb318e8XOKF7MOyXdJ7kdjbvrp3ChPLU2usI7Rjxs97Dj5P0uQ==", - "requires": { - "@babel/types": "^7.0.0", - "@jest/types": "^25.5.0", - "@types/prettier": "^1.19.0", - "chalk": "^3.0.0", - "expect": "^25.5.0", - "graceful-fs": "^4.2.4", - "jest-diff": "^25.5.0", - "jest-get-type": "^25.2.6", - "jest-matcher-utils": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-resolve": "^25.5.1", - "make-dir": "^3.0.0", - "natural-compare": "^1.4.0", - "pretty-format": "^25.5.0", - "semver": "^6.3.0" - } - }, - "jest-util": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.5.0.tgz", - "integrity": "sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA==", - "requires": { - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "make-dir": "^3.0.0" - } - }, - "jest-validate": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-25.5.0.tgz", - "integrity": "sha512-okUFKqhZIpo3jDdtUXUZ2LxGUZJIlfdYBvZb1aczzxrlyMlqdnnws9MOxezoLGhSaFc2XYaHNReNQfj5zPIWyQ==", - "requires": { - "@jest/types": "^25.5.0", - "camelcase": "^5.3.1", - "chalk": "^3.0.0", - "jest-get-type": "^25.2.6", - "leven": "^3.1.0", - "pretty-format": "^25.5.0" - } - }, - "jest-watcher": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-25.5.0.tgz", - "integrity": "sha512-XrSfJnVASEl+5+bb51V0Q7WQx65dTSk7NL4yDdVjPnRNpM0hG+ncFmDYJo9O8jaSRcAitVbuVawyXCRoxGrT5Q==", - "requires": { - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "ansi-escapes": "^4.2.1", - "chalk": "^3.0.0", - "jest-util": "^25.5.0", - "string-length": "^3.1.0" - } - }, - "jest-worker": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.5.0.tgz", - "integrity": "sha512-/dsSmUkIy5EBGfv/IjjqmFxrNAUpBERfGs1oHROyD7yxjG/w+t0GOJDX8O1k32ySmd7+a5IhnJU2qQFcJ4n1vw==", - "requires": { - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "js-yaml": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", - "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, - "jsdom": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", - "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", - "requires": { - "abab": "^2.0.0", - "acorn": "^7.1.0", - "acorn-globals": "^4.3.2", - "array-equal": "^1.0.0", - "cssom": "^0.4.1", - "cssstyle": "^2.0.0", - "data-urls": "^1.1.0", - "domexception": "^1.0.1", - "escodegen": "^1.11.1", - "html-encoding-sniffer": "^1.0.2", - "nwsapi": "^2.2.0", - "parse5": "5.1.0", - "pn": "^1.1.0", - "request": "^2.88.0", - "request-promise-native": "^1.0.7", - "saxes": "^3.1.9", - "symbol-tree": "^3.2.2", - "tough-cookie": "^3.0.1", - "w3c-hr-time": "^1.0.1", - "w3c-xmlserializer": "^1.1.2", - "webidl-conversions": "^4.0.2", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^7.0.0", - "ws": "^7.0.0", - "xml-name-validator": "^3.0.0" - } - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "json5": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", - "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", - "requires": { - "minimist": "^1.2.5" - } - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" - }, - "kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" - }, - "leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==" - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "requires": { - "p-locate": "^4.1.0" - } - }, - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" - }, - "lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" - }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" - }, - "lolex": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", - "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "requires": { - "semver": "^6.0.0" - } - }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" - }, - "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", - "requires": { - "tmpl": "1.0.x" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "requires": { - "object-visit": "^1.0.0" - } - }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "mime-db": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" - }, - "mime-types": { - "version": "2.1.27", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", - "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", - "requires": { - "mime-db": "1.44.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { - "minimist": "^1.2.5" - } - }, - "moxios": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/moxios/-/moxios-0.4.0.tgz", - "integrity": "sha1-/A2ixlR31yXKa5Z51YNw7QxS9Ts=" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" - }, - "node-modules-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", - "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=" - }, - "node-notifier": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-6.0.0.tgz", - "integrity": "sha512-SVfQ/wMw+DesunOm5cKqr6yDcvUTDl/yc97ybGHMrteNEY6oekXpNpS3lZwgLlwz0FLgHoiW28ZpmBHUDg37cw==", - "optional": true, - "requires": { - "growly": "^1.3.0", - "is-wsl": "^2.1.1", - "semver": "^6.3.0", - "shellwords": "^0.1.1", - "which": "^1.3.1" - }, - "dependencies": { - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "optional": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - } - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "requires": { - "path-key": "^2.0.0" - } - }, - "nwsapi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", - "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==" - }, - "oauth-1.0a": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/oauth-1.0a/-/oauth-1.0a-2.2.6.tgz", - "integrity": "sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "requires": { - "isobject": "^3.0.0" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "requires": { - "isobject": "^3.0.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "p-each-series": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.1.0.tgz", - "integrity": "sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ==" - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, - "parse-json": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", - "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "parse5": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", - "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==" - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" - }, - "pirates": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", - "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", - "requires": { - "node-modules-regexp": "^1.0.0" - } - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "requires": { - "find-up": "^4.0.0" - } - }, - "pn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", - "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==" - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" - }, - "pretty-format": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", - "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", - "requires": { - "@jest/types": "^25.5.0", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^16.12.0" - } - }, - "prompts": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.3.2.tgz", - "integrity": "sha512-Q06uKs2CkNYVID0VqwfAl9mipo99zkBv/n2JtWY89Yxa3ZabWSrs0e2KTudKVa3peLUvYXMefDqIleLPVUBZMA==", - "requires": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.4" - } - }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==" - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "realpath-native": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz", - "integrity": "sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==" - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - } - } - }, - "request-promise-core": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", - "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", - "requires": { - "lodash": "^4.17.19" - } - }, - "request-promise-native": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", - "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", - "requires": { - "request-promise-core": "1.1.4", - "stealthy-require": "^1.1.1", - "tough-cookie": "^2.3.3" - }, - "dependencies": { - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - } - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "resolve": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", - "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "requires": { - "resolve-from": "^5.0.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "rsvp": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", - "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==" - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sane": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", - "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", - "requires": { - "@cnakazawa/watch": "^1.0.3", - "anymatch": "^2.0.0", - "capture-exit": "^2.0.0", - "exec-sh": "^0.3.2", - "execa": "^1.0.0", - "fb-watchman": "^2.0.0", - "micromatch": "^3.1.4", - "minimist": "^1.1.1", - "walker": "~1.0.5" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - } - } - }, - "saxes": { - "version": "3.1.11", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", - "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", - "requires": { - "xmlchars": "^2.1.1" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" - }, - "shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "optional": true - }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" - }, - "sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" - }, - "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz", - "integrity": "sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==" - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "stack-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", - "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==" - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" - }, - "string-length": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz", - "integrity": "sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA==", - "requires": { - "astral-regex": "^1.0.0", - "strip-ansi": "^5.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==" - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" - }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - }, - "supports-hyperlinks": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz", - "integrity": "sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==", - "requires": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - } - }, - "symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" - }, - "terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "requires": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - } - }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, - "throat": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", - "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==" - }, - "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=" - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { - "is-number": "^7.0.0" - } - }, - "tough-cookie": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", - "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", - "requires": { - "ip-regex": "^2.1.0", - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "requires": { - "punycode": "^2.1.0" - } - }, - "ts-essentials": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-4.0.0.tgz", - "integrity": "sha512-uQJX+SRY9mtbKU+g9kl5Fi7AEMofPCvHfJkQlaygpPmHPZrtgaBqbWFOYyiA47RhnSwwnXdepUJrgqUYxoUyhQ==" - }, - "ts-jest": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-25.5.0.tgz", - "integrity": "sha512-govrjbOk1UEzcJ5cX5k8X8IUtFuP3lp3mrF3ZuKtCdAOQzdeCM7qualhb/U8s8SWFwEDutOqfF5PLkJ+oaYD4w==", - "requires": { - "bs-logger": "0.x", - "buffer-from": "1.x", - "fast-json-stable-stringify": "2.x", - "json5": "2.x", - "lodash.memoize": "4.x", - "make-error": "1.x", - "micromatch": "4.x", - "mkdirp": "0.x", - "semver": "6.x", - "yargs-parser": "18.x" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "typescript": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", - "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==" - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" - } - } - }, - "uri-js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", - "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" - }, - "v8-to-istanbul": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-4.1.4.tgz", - "integrity": "sha512-Rw6vJHj1mbdK8edjR7+zuJrpDtKIgNdAvTSAcpYfgMIw+u2dPDntD3dgN4XQFLU2/fvFQdzj+EeSGfd/jnY5fQ==", - "requires": { - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0", - "source-map": "^0.7.3" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" - } - } - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "requires": { - "browser-process-hrtime": "^1.0.0" - } - }, - "w3c-xmlserializer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", - "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", - "requires": { - "domexception": "^1.0.1", - "webidl-conversions": "^4.0.2", - "xml-name-validator": "^3.0.0" - } - }, - "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", - "requires": { - "makeerror": "1.0.x" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "requires": { - "iconv-lite": "0.4.24" - } - }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" - }, - "whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "ws": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", - "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==" - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" - }, - "xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" - }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "@woocommerce/e2e-core-tests": { - "version": "file:tests/e2e/core-tests", - "dev": true, - "requires": { - "@jest/globals": "^26.4.2", - "config": "3.3.3" - }, - "dependencies": { - "@jest/environment": { - "version": "26.5.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-26.5.2.tgz", - "integrity": "sha512-YjhCD/Zhkz0/1vdlS/QN6QmuUdDkpgBdK4SdiVg4Y19e29g4VQYN5Xg8+YuHjdoWGY7wJHMxc79uDTeTOy9Ngw==", - "dev": true, - "requires": { - "@jest/fake-timers": "^26.5.2", - "@jest/types": "^26.5.2", - "@types/node": "*", - "jest-mock": "^26.5.2" - } - }, - "@jest/fake-timers": { - "version": "26.5.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.5.2.tgz", - "integrity": "sha512-09Hn5Oraqt36V1akxQeWMVL0fR9c6PnEhpgLaYvREXZJAh2H2Y+QLCsl0g7uMoJeoWJAuz4tozk1prbR1Fc1sw==", - "dev": true, - "requires": { - "@jest/types": "^26.5.2", - "@sinonjs/fake-timers": "^6.0.1", - "@types/node": "*", - "jest-message-util": "^26.5.2", - "jest-mock": "^26.5.2", - "jest-util": "^26.5.2" - } - }, - "@jest/globals": { - "version": "26.5.3", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-26.5.3.tgz", - "integrity": "sha512-7QztI0JC2CuB+Wx1VdnOUNeIGm8+PIaqngYsZXQCkH2QV0GFqzAYc9BZfU0nuqA6cbYrWh5wkuMzyii3P7deug==", - "dev": true, - "requires": { - "@jest/environment": "^26.5.2", - "@jest/types": "^26.5.2", - "expect": "^26.5.3" - } - }, - "@jest/types": { - "version": "26.5.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.5.2.tgz", - "integrity": "sha512-QDs5d0gYiyetI8q+2xWdkixVQMklReZr4ltw7GFDtb4fuJIBCE6mzj2LnitGqCuAlLap6wPyb8fpoHgwZz5fdg==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, - "@types/istanbul-reports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", - "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", - "dev": true, - "requires": { - "@types/istanbul-lib-report": "*" - } - }, - "@types/stack-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", - "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "diff-sequences": { - "version": "26.5.0", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.5.0.tgz", - "integrity": "sha512-ZXx86srb/iYy6jG71k++wBN9P9J05UNQ5hQHQd9MtMPvcqXPx/vKU69jfHV637D00Q2gSgPk2D+jSx3l1lDW/Q==", - "dev": true - }, - "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true - }, - "expect": { - "version": "26.5.3", - "resolved": "https://registry.npmjs.org/expect/-/expect-26.5.3.tgz", - "integrity": "sha512-kkpOhGRWGOr+TEFUnYAjfGvv35bfP+OlPtqPIJpOCR9DVtv8QV+p8zG0Edqafh80fsjeE+7RBcVUq1xApnYglw==", - "dev": true, - "requires": { - "@jest/types": "^26.5.2", - "ansi-styles": "^4.0.0", - "jest-get-type": "^26.3.0", - "jest-matcher-utils": "^26.5.2", - "jest-message-util": "^26.5.2", - "jest-regex-util": "^26.0.0" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "jest-diff": { - "version": "26.5.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.5.2.tgz", - "integrity": "sha512-HCSWDUGwsov5oTlGzrRM+UPJI/Dpqi9jzeV0fdRNi3Ch5bnoXhnyJMmVg2juv9081zLIy3HGPI5mcuGgXM2xRA==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^26.5.0", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.5.2" - } - }, - "jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true - }, - "jest-matcher-utils": { - "version": "26.5.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.5.2.tgz", - "integrity": "sha512-W9GO9KBIC4gIArsNqDUKsLnhivaqf8MSs6ujO/JDcPIQrmY+aasewweXVET8KdrJ6ADQaUne5UzysvF/RR7JYA==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "jest-diff": "^26.5.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.5.2" - } - }, - "jest-message-util": { - "version": "26.5.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.5.2.tgz", - "integrity": "sha512-Ocp9UYZ5Jl15C5PNsoDiGEk14A4NG0zZKknpWdZGoMzJuGAkVt10e97tnEVMYpk7LnQHZOfuK2j/izLBMcuCZw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@jest/types": "^26.5.2", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "micromatch": "^4.0.2", - "slash": "^3.0.0", - "stack-utils": "^2.0.2" - } - }, - "jest-mock": { - "version": "26.5.2", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-26.5.2.tgz", - "integrity": "sha512-9SiU4b5PtO51v0MtJwVRqeGEroH66Bnwtq4ARdNP7jNXbpT7+ByeWNAk4NeT/uHfNSVDXEXgQo1XRuwEqS6Rdw==", - "dev": true, - "requires": { - "@jest/types": "^26.5.2", - "@types/node": "*" - } - }, - "jest-regex-util": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", - "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", - "dev": true - }, - "jest-util": { - "version": "26.5.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.5.2.tgz", - "integrity": "sha512-WTL675bK+GSSAYgS8z9FWdCT2nccO1yTIplNLPlP0OD8tUk/H5IrWKMMRudIQQ0qp8bb4k+1Qa8CxGKq9qnYdg==", - "dev": true, - "requires": { - "@jest/types": "^26.5.2", - "@types/node": "*", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" - } - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "pretty-format": { - "version": "26.5.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.5.2.tgz", - "integrity": "sha512-VizyV669eqESlkOikKJI8Ryxl/kPpbdLwNdPs2GrbQs18MpySB5S0Yo0N7zkg2xTRiFq4CFw8ct5Vg4a0xP0og==", - "dev": true, - "requires": { - "@jest/types": "^26.5.2", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^16.12.0" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "stack-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.2.tgz", - "integrity": "sha512-0H7QK2ECz3fyZMzQ8rH0j2ykpfbnd20BFtfg/SqVC2+sCTtcw0aDTGB7dk+de4U4uUeuz6nOtJcrkFFLG1B0Rg==", - "dev": true, - "requires": { - "escape-string-regexp": "^2.0.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "@woocommerce/e2e-environment": { - "version": "file:tests/e2e/env", - "dev": true, - "requires": { - "@automattic/puppeteer-utils": "github:Automattic/puppeteer-utils#0f3ec50", - "@jest/test-sequencer": "^25.5.4", - "@wordpress/e2e-test-utils": "^4.15.0", - "@wordpress/jest-preset-default": "^6.4.0", - "app-root-path": "^3.0.0", - "jest": "^25.1.0", - "jest-puppeteer": "^4.4.0" - }, - "dependencies": { - "@automattic/puppeteer-utils": { - "version": "github:Automattic/puppeteer-utils#0f3ec50fc22d7bd2a4bd69fc172e8a66d958ef2d", - "from": "github:Automattic/puppeteer-utils#0f3ec50", - "dev": true, - "requires": { - "@babel/cli": "^7.8.3", - "@babel/core": "^7.8.3", - "@babel/preset-env": "^7.8.3", - "@slack/web-api": "^5.6.0", - "@wordpress/e2e-test-utils": "^3.0.0", - "config": "^3.2.4", - "eslint": "6.7.2", - "jest": "^24.9.0", - "prettier": "npm:wp-prettier@1.19.1", - "puppeteer": "^2.0.0" - }, - "dependencies": { - "@wordpress/e2e-test-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils/-/e2e-test-utils-3.0.0.tgz", - "integrity": "sha512-XMdR8DeKyDQRF5jKeUlOzP4pTRtoJuOLsNZRLUFUvnrs9y/7/hH17VmPbWp3TJGvV/eGKzO4+D+wJTsP9nJmIw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.4.4", - "@wordpress/keycodes": "^2.7.0", - "@wordpress/url": "^2.8.2", - "lodash": "^4.17.15", - "node-fetch": "^1.7.3" - } - }, - "jest": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-24.9.0.tgz", - "integrity": "sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw==", - "dev": true, - "requires": { - "import-local": "^2.0.0", - "jest-cli": "^24.9.0" - }, - "dependencies": { - "jest-cli": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-24.9.0.tgz", - "integrity": "sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg==", - "dev": true, - "requires": { - "@jest/core": "^24.9.0", - "@jest/test-result": "^24.9.0", - "@jest/types": "^24.9.0", - "chalk": "^2.0.1", - "exit": "^0.1.2", - "import-local": "^2.0.0", - "is-ci": "^2.0.0", - "jest-config": "^24.9.0", - "jest-util": "^24.9.0", - "jest-validate": "^24.9.0", - "prompts": "^2.0.1", - "realpath-native": "^1.1.0", - "yargs": "^13.3.0" - } - } - } - }, - "prettier": { - "version": "npm:prettier@1.19.1", - "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-1.19.1.tgz", - "integrity": "sha512-mqAC2r1NDmRjG+z3KCJ/i61tycKlmADIjxnDhQab+KBxSAGbF/W7/zwB2guy/ypIeKrrftNsIYkNZZQKf3vJcg==", - "dev": true - } - } - }, - "@babel/cli": { - "version": "7.11.6", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.11.6.tgz", - "integrity": "sha512-+w7BZCvkewSmaRM6H4L2QM3RL90teqEIHDIFXAmrW33+0jhlymnDAEdqVeCZATvxhQuio1ifoGVlJJbIiH9Ffg==", - "dev": true, - "requires": { - "chokidar": "^2.1.8", - "commander": "^4.0.1", - "convert-source-map": "^1.1.0", - "fs-readdir-recursive": "^1.1.0", - "glob": "^7.0.0", - "lodash": "^4.17.19", - "make-dir": "^2.1.0", - "slash": "^2.0.0", - "source-map": "^0.5.0" - } - }, - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/compat-data": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.11.0.tgz", - "integrity": "sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ==", - "dev": true, - "requires": { - "browserslist": "^4.12.0", - "invariant": "^2.2.4", - "semver": "^5.5.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "@babel/core": { - "version": "7.11.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.6.tgz", - "integrity": "sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.11.6", - "@babel/helper-module-transforms": "^7.11.0", - "@babel/helpers": "^7.10.4", - "@babel/parser": "^7.11.5", - "@babel/template": "^7.10.4", - "@babel/traverse": "^7.11.5", - "@babel/types": "^7.11.5", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.1", - "json5": "^2.1.2", - "lodash": "^4.17.19", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.11.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", - "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", - "requires": { - "@babel/types": "^7.11.5", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", - "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz", - "integrity": "sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg==", - "dev": true, - "requires": { - "@babel/helper-explode-assignable-expression": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz", - "integrity": "sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.10.4", - "browserslist": "^4.12.0", - "invariant": "^2.2.4", - "levenary": "^1.1.1", - "semver": "^5.5.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "@babel/helper-create-class-features-plugin": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz", - "integrity": "sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-member-expression-to-functions": "^7.10.5", - "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-replace-supers": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.10.4" - } - }, - "@babel/helper-create-regexp-features-plugin": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz", - "integrity": "sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.10.4", - "@babel/helper-regex": "^7.10.4", - "regexpu-core": "^4.7.0" - } - }, - "@babel/helper-define-map": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz", - "integrity": "sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.10.4", - "@babel/types": "^7.10.5", - "lodash": "^4.17.19" - } - }, - "@babel/helper-explode-assignable-expression": { - "version": "7.11.4", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.11.4.tgz", - "integrity": "sha512-ux9hm3zR4WV1Y3xXxXkdG/0gxF9nvI0YVmKVhvK9AfMoaQkemL3sJpXw+Xbz65azo8qJiEz2XVDUpK3KYhH3ZQ==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz", - "integrity": "sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz", - "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==", - "dev": true, - "requires": { - "@babel/types": "^7.11.0" - } - }, - "@babel/helper-module-imports": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", - "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-module-transforms": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz", - "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.10.4", - "@babel/helper-replace-supers": "^7.10.4", - "@babel/helper-simple-access": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/template": "^7.10.4", - "@babel/types": "^7.11.0", - "lodash": "^4.17.19" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", - "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - }, - "@babel/helper-regex": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.10.5.tgz", - "integrity": "sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg==", - "dev": true, - "requires": { - "lodash": "^4.17.19" - } - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.11.4", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.11.4.tgz", - "integrity": "sha512-tR5vJ/vBa9wFy3m5LLv2faapJLnDFxNWff2SAYkSE4rLUdbp7CdObYFgI7wK4T/Mj4UzpjPwzR8Pzmr5m7MHGA==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.10.4", - "@babel/helper-wrap-function": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-replace-supers": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", - "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.10.4", - "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/traverse": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-simple-access": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", - "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", - "dev": true, - "requires": { - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz", - "integrity": "sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q==", - "dev": true, - "requires": { - "@babel/types": "^7.11.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", - "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", - "requires": { - "@babel/types": "^7.11.0" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==" - }, - "@babel/helper-wrap-function": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz", - "integrity": "sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/traverse": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helpers": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz", - "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==", - "dev": true, - "requires": { - "@babel/template": "^7.10.4", - "@babel/traverse": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.11.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", - "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==" - }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz", - "integrity": "sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-remap-async-to-generator": "^7.10.4", - "@babel/plugin-syntax-async-generators": "^7.8.0" - } - }, - "@babel/plugin-proposal-class-properties": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz", - "integrity": "sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-proposal-dynamic-import": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz", - "integrity": "sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-dynamic-import": "^7.8.0" - } - }, - "@babel/plugin-proposal-export-namespace-from": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz", - "integrity": "sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - } - }, - "@babel/plugin-proposal-json-strings": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz", - "integrity": "sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.0" - } - }, - "@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz", - "integrity": "sha512-/f8p4z+Auz0Uaf+i8Ekf1iM7wUNLcViFUGiPxKeXvxTSl63B875YPiVdUDdem7hREcI0E0kSpEhS8tF5RphK7Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - } - }, - "@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz", - "integrity": "sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" - } - }, - "@babel/plugin-proposal-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz", - "integrity": "sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - } - }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz", - "integrity": "sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.0", - "@babel/plugin-transform-parameters": "^7.10.4" - } - }, - "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz", - "integrity": "sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" - } - }, - "@babel/plugin-proposal-optional-chaining": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz", - "integrity": "sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.0" - } - }, - "@babel/plugin-proposal-private-methods": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz", - "integrity": "sha512-wh5GJleuI8k3emgTg5KkJK6kHNsGEr0uBTDBuQUBJwckk9xs1ez79ioheEVVxMLyPscB0LfkbVHslQqIzWV6Bw==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz", - "integrity": "sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz", - "integrity": "sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-top-level-await": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz", - "integrity": "sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz", - "integrity": "sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz", - "integrity": "sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-remap-async-to-generator": "^7.10.4" - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz", - "integrity": "sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz", - "integrity": "sha512-00dYeDE0EVEHuuM+26+0w/SCL0BH2Qy7LwHuI4Hi4MH5gkC8/AqMN5uWFJIsoXZrAphiMm1iXzBw6L2T+eA0ew==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz", - "integrity": "sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.10.4", - "@babel/helper-define-map": "^7.10.4", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-replace-supers": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.10.4", - "globals": "^11.1.0" - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz", - "integrity": "sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz", - "integrity": "sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz", - "integrity": "sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz", - "integrity": "sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz", - "integrity": "sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw==", - "dev": true, - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz", - "integrity": "sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz", - "integrity": "sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz", - "integrity": "sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-member-expression-literals": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz", - "integrity": "sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz", - "integrity": "sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.10.5", - "@babel/helper-plugin-utils": "^7.10.4", - "babel-plugin-dynamic-import-node": "^2.3.3" - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz", - "integrity": "sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-simple-access": "^7.10.4", - "babel-plugin-dynamic-import-node": "^2.3.3" - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz", - "integrity": "sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw==", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.10.4", - "@babel/helper-module-transforms": "^7.10.5", - "@babel/helper-plugin-utils": "^7.10.4", - "babel-plugin-dynamic-import-node": "^2.3.3" - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz", - "integrity": "sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz", - "integrity": "sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.10.4" - } - }, - "@babel/plugin-transform-new-target": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz", - "integrity": "sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz", - "integrity": "sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-replace-supers": "^7.10.4" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz", - "integrity": "sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-property-literals": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz", - "integrity": "sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz", - "integrity": "sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw==", - "dev": true, - "requires": { - "regenerator-transform": "^0.14.2" - } - }, - "@babel/plugin-transform-reserved-words": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz", - "integrity": "sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz", - "integrity": "sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz", - "integrity": "sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz", - "integrity": "sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-regex": "^7.10.4" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz", - "integrity": "sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz", - "integrity": "sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-unicode-escapes": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz", - "integrity": "sha512-y5XJ9waMti2J+e7ij20e+aH+fho7Wb7W8rNuu72aKRwCHFqQdhkdU2lo3uZ9tQuboEJcUFayXdARhcxLQ3+6Fg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz", - "integrity": "sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/polyfill": { - "version": "7.11.5", - "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.11.5.tgz", - "integrity": "sha512-FunXnE0Sgpd61pKSj2OSOs1D44rKTD3pGOfGilZ6LGrrIH0QEtJlTjqOqdF8Bs98JmjfGhni2BBkTfv9KcKJ9g==", - "requires": { - "core-js": "^2.6.5", - "regenerator-runtime": "^0.13.4" - } - }, - "@babel/preset-env": { - "version": "7.11.5", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.11.5.tgz", - "integrity": "sha512-kXqmW1jVcnB2cdueV+fyBM8estd5mlNfaQi6lwLgRwCby4edpavgbFhiBNjmWA3JpB/yZGSISa7Srf+TwxDQoA==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.11.0", - "@babel/helper-compilation-targets": "^7.10.4", - "@babel/helper-module-imports": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-proposal-async-generator-functions": "^7.10.4", - "@babel/plugin-proposal-class-properties": "^7.10.4", - "@babel/plugin-proposal-dynamic-import": "^7.10.4", - "@babel/plugin-proposal-export-namespace-from": "^7.10.4", - "@babel/plugin-proposal-json-strings": "^7.10.4", - "@babel/plugin-proposal-logical-assignment-operators": "^7.11.0", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", - "@babel/plugin-proposal-numeric-separator": "^7.10.4", - "@babel/plugin-proposal-object-rest-spread": "^7.11.0", - "@babel/plugin-proposal-optional-catch-binding": "^7.10.4", - "@babel/plugin-proposal-optional-chaining": "^7.11.0", - "@babel/plugin-proposal-private-methods": "^7.10.4", - "@babel/plugin-proposal-unicode-property-regex": "^7.10.4", - "@babel/plugin-syntax-async-generators": "^7.8.0", - "@babel/plugin-syntax-class-properties": "^7.10.4", - "@babel/plugin-syntax-dynamic-import": "^7.8.0", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.0", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.0", - "@babel/plugin-syntax-top-level-await": "^7.10.4", - "@babel/plugin-transform-arrow-functions": "^7.10.4", - "@babel/plugin-transform-async-to-generator": "^7.10.4", - "@babel/plugin-transform-block-scoped-functions": "^7.10.4", - "@babel/plugin-transform-block-scoping": "^7.10.4", - "@babel/plugin-transform-classes": "^7.10.4", - "@babel/plugin-transform-computed-properties": "^7.10.4", - "@babel/plugin-transform-destructuring": "^7.10.4", - "@babel/plugin-transform-dotall-regex": "^7.10.4", - "@babel/plugin-transform-duplicate-keys": "^7.10.4", - "@babel/plugin-transform-exponentiation-operator": "^7.10.4", - "@babel/plugin-transform-for-of": "^7.10.4", - "@babel/plugin-transform-function-name": "^7.10.4", - "@babel/plugin-transform-literals": "^7.10.4", - "@babel/plugin-transform-member-expression-literals": "^7.10.4", - "@babel/plugin-transform-modules-amd": "^7.10.4", - "@babel/plugin-transform-modules-commonjs": "^7.10.4", - "@babel/plugin-transform-modules-systemjs": "^7.10.4", - "@babel/plugin-transform-modules-umd": "^7.10.4", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.10.4", - "@babel/plugin-transform-new-target": "^7.10.4", - "@babel/plugin-transform-object-super": "^7.10.4", - "@babel/plugin-transform-parameters": "^7.10.4", - "@babel/plugin-transform-property-literals": "^7.10.4", - "@babel/plugin-transform-regenerator": "^7.10.4", - "@babel/plugin-transform-reserved-words": "^7.10.4", - "@babel/plugin-transform-shorthand-properties": "^7.10.4", - "@babel/plugin-transform-spread": "^7.11.0", - "@babel/plugin-transform-sticky-regex": "^7.10.4", - "@babel/plugin-transform-template-literals": "^7.10.4", - "@babel/plugin-transform-typeof-symbol": "^7.10.4", - "@babel/plugin-transform-unicode-escapes": "^7.10.4", - "@babel/plugin-transform-unicode-regex": "^7.10.4", - "@babel/preset-modules": "^0.1.3", - "@babel/types": "^7.11.5", - "browserslist": "^4.12.0", - "core-js-compat": "^3.6.2", - "invariant": "^2.2.2", - "levenary": "^1.1.1", - "semver": "^5.5.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "@babel/preset-modules": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz", - "integrity": "sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - } - }, - "@babel/runtime": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", - "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "@babel/runtime-corejs3": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.11.2.tgz", - "integrity": "sha512-qh5IR+8VgFz83VBa6OkaET6uN/mJOhHONuy3m1sgF0CV6mXdPSEBdA7e1eUbVvyNtANjMbg22JUv71BaDXLY6A==", - "requires": { - "core-js-pure": "^3.0.0", - "regenerator-runtime": "^0.13.4" - } - }, - "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/traverse": { - "version": "7.11.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", - "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.11.5", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.11.5", - "@babel/types": "^7.11.5", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" - }, - "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "@babel/types": { - "version": "7.11.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", - "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, - "@cnakazawa/watch": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", - "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", - "dev": true, - "requires": { - "exec-sh": "^0.3.2", - "minimist": "^1.2.0" - } - }, - "@hapi/address": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", - "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==", - "dev": true - }, - "@hapi/bourne": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-1.3.2.tgz", - "integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==", - "dev": true - }, - "@hapi/hoek": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz", - "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==", - "dev": true - }, - "@hapi/joi": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.1.tgz", - "integrity": "sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==", - "dev": true, - "requires": { - "@hapi/address": "2.x.x", - "@hapi/bourne": "1.x.x", - "@hapi/hoek": "8.x.x", - "@hapi/topo": "3.x.x" - } - }, - "@hapi/topo": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz", - "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==", - "dev": true, - "requires": { - "@hapi/hoek": "^8.3.0" - } - }, - "@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "@istanbuljs/schema": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", - "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", - "dev": true - }, - "@jest/console": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz", - "integrity": "sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ==", - "dev": true, - "requires": { - "@jest/source-map": "^24.9.0", - "chalk": "^2.0.1", - "slash": "^2.0.0" - } - }, - "@jest/core": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-24.9.0.tgz", - "integrity": "sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A==", - "dev": true, - "requires": { - "@jest/console": "^24.7.1", - "@jest/reporters": "^24.9.0", - "@jest/test-result": "^24.9.0", - "@jest/transform": "^24.9.0", - "@jest/types": "^24.9.0", - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.1", - "exit": "^0.1.2", - "graceful-fs": "^4.1.15", - "jest-changed-files": "^24.9.0", - "jest-config": "^24.9.0", - "jest-haste-map": "^24.9.0", - "jest-message-util": "^24.9.0", - "jest-regex-util": "^24.3.0", - "jest-resolve": "^24.9.0", - "jest-resolve-dependencies": "^24.9.0", - "jest-runner": "^24.9.0", - "jest-runtime": "^24.9.0", - "jest-snapshot": "^24.9.0", - "jest-util": "^24.9.0", - "jest-validate": "^24.9.0", - "jest-watcher": "^24.9.0", - "micromatch": "^3.1.10", - "p-each-series": "^1.0.0", - "realpath-native": "^1.1.0", - "rimraf": "^2.5.4", - "slash": "^2.0.0", - "strip-ansi": "^5.0.0" - }, - "dependencies": { - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - } - } - }, - "@jest/environment": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-24.9.0.tgz", - "integrity": "sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ==", - "dev": true, - "requires": { - "@jest/fake-timers": "^24.9.0", - "@jest/transform": "^24.9.0", - "@jest/types": "^24.9.0", - "jest-mock": "^24.9.0" - } - }, - "@jest/fake-timers": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-24.9.0.tgz", - "integrity": "sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A==", - "dev": true, - "requires": { - "@jest/types": "^24.9.0", - "jest-message-util": "^24.9.0", - "jest-mock": "^24.9.0" - } - }, - "@jest/globals": { - "version": "25.5.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-25.5.2.tgz", - "integrity": "sha512-AgAS/Ny7Q2RCIj5kZ+0MuKM1wbF0WMLxbCVl/GOMoCNbODRdJ541IxJ98xnZdVSZXivKpJlNPIWa3QmY0l4CXA==", - "dev": true, - "requires": { - "@jest/environment": "^25.5.0", - "@jest/types": "^25.5.0", - "expect": "^25.5.0" - }, - "dependencies": { - "@jest/environment": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.5.0.tgz", - "integrity": "sha512-U2VXPEqL07E/V7pSZMSQCvV5Ea4lqOlT+0ZFijl/i316cRMHvZ4qC+jBdryd+lmRetjQo0YIQr6cVPNxxK87mA==", - "dev": true, - "requires": { - "@jest/fake-timers": "^25.5.0", - "@jest/types": "^25.5.0", - "jest-mock": "^25.5.0" - } - }, - "@jest/fake-timers": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.5.0.tgz", - "integrity": "sha512-9y2+uGnESw/oyOI3eww9yaxdZyHq7XvprfP/eeoCsjqKYts2yRlsHS/SgjPDV8FyMfn2nbMy8YzUk6nyvdLOpQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-util": "^25.5.0", - "lolex": "^5.0.0" - } - }, - "@jest/types": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", - "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", - "@types/yargs": "^15.0.0", - "chalk": "^3.0.0" - } - }, - "@types/yargs": { - "version": "15.0.7", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.7.tgz", - "integrity": "sha512-Gf4u3EjaPNcC9cTu4/j2oN14nSVhr8PQ+BvBcBQHAhDZfl0bVIiLgvnRXv/dn58XhTm9UXvBpvJpDlwV65QxOA==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "diff-sequences": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", - "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", - "dev": true - }, - "expect": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-25.5.0.tgz", - "integrity": "sha512-w7KAXo0+6qqZZhovCaBVPSIqQp7/UTcx4M9uKt2m6pd2VB1voyC8JizLRqeEqud3AAVP02g+hbErDu5gu64tlA==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "ansi-styles": "^4.0.0", - "jest-get-type": "^25.2.6", - "jest-matcher-utils": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-regex-util": "^25.2.6" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "jest-diff": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", - "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "diff-sequences": "^25.2.6", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - } - }, - "jest-get-type": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", - "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", - "dev": true - }, - "jest-matcher-utils": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz", - "integrity": "sha512-VWI269+9JS5cpndnpCwm7dy7JtGQT30UHfrnM3mXl22gHGt/b7NkjBqXfbhZ8V4B7ANUsjK18PlSBmG0YH7gjw==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "jest-diff": "^25.5.0", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - } - }, - "jest-message-util": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.5.0.tgz", - "integrity": "sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@jest/types": "^25.5.0", - "@types/stack-utils": "^1.0.1", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "micromatch": "^4.0.2", - "slash": "^3.0.0", - "stack-utils": "^1.0.1" - } - }, - "jest-mock": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.5.0.tgz", - "integrity": "sha512-eXWuTV8mKzp/ovHc5+3USJMYsTBhyQ+5A1Mak35dey/RG8GlM4YWVylZuGgVXinaW6tpvk/RSecmF37FKUlpXA==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0" - } - }, - "jest-regex-util": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", - "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", - "dev": true - }, - "jest-util": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.5.0.tgz", - "integrity": "sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "make-dir": "^3.0.0" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "pretty-format": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", - "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^16.12.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "@jest/reporters": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-24.9.0.tgz", - "integrity": "sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw==", - "dev": true, - "requires": { - "@jest/environment": "^24.9.0", - "@jest/test-result": "^24.9.0", - "@jest/transform": "^24.9.0", - "@jest/types": "^24.9.0", - "chalk": "^2.0.1", - "exit": "^0.1.2", - "glob": "^7.1.2", - "istanbul-lib-coverage": "^2.0.2", - "istanbul-lib-instrument": "^3.0.1", - "istanbul-lib-report": "^2.0.4", - "istanbul-lib-source-maps": "^3.0.1", - "istanbul-reports": "^2.2.6", - "jest-haste-map": "^24.9.0", - "jest-resolve": "^24.9.0", - "jest-runtime": "^24.9.0", - "jest-util": "^24.9.0", - "jest-worker": "^24.6.0", - "node-notifier": "^5.4.2", - "slash": "^2.0.0", - "source-map": "^0.6.0", - "string-length": "^2.0.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "@jest/source-map": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-24.9.0.tgz", - "integrity": "sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg==", - "dev": true, - "requires": { - "callsites": "^3.0.0", - "graceful-fs": "^4.1.15", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "@jest/test-result": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-24.9.0.tgz", - "integrity": "sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA==", - "dev": true, - "requires": { - "@jest/console": "^24.9.0", - "@jest/types": "^24.9.0", - "@types/istanbul-lib-coverage": "^2.0.0" - } - }, - "@jest/test-sequencer": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-25.5.4.tgz", - "integrity": "sha512-pTJGEkSeg1EkCO2YWq6hbFvKNXk8ejqlxiOg1jBNLnWrgXOkdY6UmqZpwGFXNnRt9B8nO1uWMzLLZ4eCmhkPNA==", - "dev": true, - "requires": { - "@jest/test-result": "^25.5.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^25.5.1", - "jest-runner": "^25.5.4", - "jest-runtime": "^25.5.4" - }, - "dependencies": { - "@jest/console": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-25.5.0.tgz", - "integrity": "sha512-T48kZa6MK1Y6k4b89sexwmSF4YLeZS/Udqg3Jj3jG/cHH+N/sLFCEoXEDMOKugJQ9FxPN1osxIknvKkxt6MKyw==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "jest-message-util": "^25.5.0", - "jest-util": "^25.5.0", - "slash": "^3.0.0" - } - }, - "@jest/environment": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.5.0.tgz", - "integrity": "sha512-U2VXPEqL07E/V7pSZMSQCvV5Ea4lqOlT+0ZFijl/i316cRMHvZ4qC+jBdryd+lmRetjQo0YIQr6cVPNxxK87mA==", - "dev": true, - "requires": { - "@jest/fake-timers": "^25.5.0", - "@jest/types": "^25.5.0", - "jest-mock": "^25.5.0" - } - }, - "@jest/fake-timers": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.5.0.tgz", - "integrity": "sha512-9y2+uGnESw/oyOI3eww9yaxdZyHq7XvprfP/eeoCsjqKYts2yRlsHS/SgjPDV8FyMfn2nbMy8YzUk6nyvdLOpQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-util": "^25.5.0", - "lolex": "^5.0.0" - } - }, - "@jest/source-map": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-25.5.0.tgz", - "integrity": "sha512-eIGx0xN12yVpMcPaVpjXPnn3N30QGJCJQSkEDUt9x1fI1Gdvb07Ml6K5iN2hG7NmMP6FDmtPEssE3z6doOYUwQ==", - "dev": true, - "requires": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.4", - "source-map": "^0.6.0" - } - }, - "@jest/test-result": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-25.5.0.tgz", - "integrity": "sha512-oV+hPJgXN7IQf/fHWkcS99y0smKLU2czLBJ9WA0jHITLst58HpQMtzSYxzaBvYc6U5U6jfoMthqsUlUlbRXs0A==", - "dev": true, - "requires": { - "@jest/console": "^25.5.0", - "@jest/types": "^25.5.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - } - }, - "@jest/transform": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.5.1.tgz", - "integrity": "sha512-Y8CEoVwXb4QwA6Y/9uDkn0Xfz0finGkieuV0xkdF9UtZGJeLukD5nLkaVrVsODB1ojRWlaoD0AJZpVHCSnJEvg==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^25.5.0", - "babel-plugin-istanbul": "^6.0.0", - "chalk": "^3.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^25.5.1", - "jest-regex-util": "^25.2.6", - "jest-util": "^25.5.0", - "micromatch": "^4.0.2", - "pirates": "^4.0.1", - "realpath-native": "^2.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - } - }, - "@jest/types": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", - "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", - "@types/yargs": "^15.0.0", - "chalk": "^3.0.0" - } - }, - "@types/yargs": { - "version": "15.0.7", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.7.tgz", - "integrity": "sha512-Gf4u3EjaPNcC9cTu4/j2oN14nSVhr8PQ+BvBcBQHAhDZfl0bVIiLgvnRXv/dn58XhTm9UXvBpvJpDlwV65QxOA==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "babel-jest": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.5.1.tgz", - "integrity": "sha512-9dA9+GmMjIzgPnYtkhBg73gOo/RHqPmLruP3BaGL4KEX3Dwz6pI8auSN8G8+iuEG90+GSswyKvslN+JYSaacaQ==", - "dev": true, - "requires": { - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "@types/babel__core": "^7.1.7", - "babel-plugin-istanbul": "^6.0.0", - "babel-preset-jest": "^25.5.0", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "slash": "^3.0.0" - } - }, - "babel-plugin-istanbul": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", - "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^4.0.0", - "test-exclude": "^6.0.0" - } - }, - "babel-plugin-jest-hoist": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.5.0.tgz", - "integrity": "sha512-u+/W+WAjMlvoocYGTwthAiQSxDcJAyHpQ6oWlHdFZaaN+Rlk8Q7iiwDPg2lN/FyJtAYnKjFxbn7xus4HCFkg5g==", - "dev": true, - "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__traverse": "^7.0.6" - } - }, - "babel-preset-jest": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-25.5.0.tgz", - "integrity": "sha512-8ZczygctQkBU+63DtSOKGh7tFL0CeCuz+1ieud9lJ1WPQ9O6A1a/r+LGn6Y705PA6whHQ3T1XuB/PmpfNYf8Fw==", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "^25.5.0", - "babel-preset-current-node-syntax": "^0.1.2" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", - "dev": true - }, - "cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dev": true, - "requires": { - "cssom": "~0.3.6" - }, - "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - } - } - }, - "detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true - }, - "diff-sequences": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", - "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", - "dev": true - }, - "expect": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-25.5.0.tgz", - "integrity": "sha512-w7KAXo0+6qqZZhovCaBVPSIqQp7/UTcx4M9uKt2m6pd2VB1voyC8JizLRqeEqud3AAVP02g+hbErDu5gu64tlA==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "ansi-styles": "^4.0.0", - "jest-get-type": "^25.2.6", - "jest-matcher-utils": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-regex-util": "^25.2.6" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "dev": true, - "optional": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", - "dev": true, - "requires": { - "@babel/core": "^7.7.5", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - } - }, - "jest-config": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-25.5.4.tgz", - "integrity": "sha512-SZwR91SwcdK6bz7Gco8qL7YY2sx8tFJYzvg216DLihTWf+LKY/DoJXpM9nTzYakSyfblbqeU48p/p7Jzy05Atg==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/test-sequencer": "^25.5.4", - "@jest/types": "^25.5.0", - "babel-jest": "^25.5.1", - "chalk": "^3.0.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.1", - "graceful-fs": "^4.2.4", - "jest-environment-jsdom": "^25.5.0", - "jest-environment-node": "^25.5.0", - "jest-get-type": "^25.2.6", - "jest-jasmine2": "^25.5.4", - "jest-regex-util": "^25.2.6", - "jest-resolve": "^25.5.1", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", - "micromatch": "^4.0.2", - "pretty-format": "^25.5.0", - "realpath-native": "^2.0.0" - } - }, - "jest-diff": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", - "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "diff-sequences": "^25.2.6", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - } - }, - "jest-docblock": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-25.3.0.tgz", - "integrity": "sha512-aktF0kCar8+zxRHxQZwxMy70stc9R1mOmrLsT5VO3pIT0uzGRSDAXxSlz4NqQWpuLjPpuMhPRl7H+5FRsvIQAg==", - "dev": true, - "requires": { - "detect-newline": "^3.0.0" - } - }, - "jest-each": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-25.5.0.tgz", - "integrity": "sha512-QBogUxna3D8vtiItvn54xXde7+vuzqRrEeaw8r1s+1TG9eZLVJE5ZkKoSUlqFwRjnlaA4hyKGiu9OlkFIuKnjA==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "jest-get-type": "^25.2.6", - "jest-util": "^25.5.0", - "pretty-format": "^25.5.0" - } - }, - "jest-environment-jsdom": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.5.0.tgz", - "integrity": "sha512-7Jr02ydaq4jaWMZLY+Skn8wL5nVIYpWvmeatOHL3tOcV3Zw8sjnPpx+ZdeBfc457p8jCR9J6YCc+Lga0oIy62A==", - "dev": true, - "requires": { - "@jest/environment": "^25.5.0", - "@jest/fake-timers": "^25.5.0", - "@jest/types": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-util": "^25.5.0", - "jsdom": "^15.2.1" - } - }, - "jest-environment-node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-25.5.0.tgz", - "integrity": "sha512-iuxK6rQR2En9EID+2k+IBs5fCFd919gVVK5BeND82fYeLWPqvRcFNPKu9+gxTwfB5XwBGBvZ0HFQa+cHtIoslA==", - "dev": true, - "requires": { - "@jest/environment": "^25.5.0", - "@jest/fake-timers": "^25.5.0", - "@jest/types": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-util": "^25.5.0", - "semver": "^6.3.0" - } - }, - "jest-get-type": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", - "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", - "dev": true - }, - "jest-haste-map": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.5.1.tgz", - "integrity": "sha512-dddgh9UZjV7SCDQUrQ+5t9yy8iEgKc1AKqZR9YDww8xsVOtzPQSMVLDChc21+g29oTRexb9/B0bIlZL+sWmvAQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "@types/graceful-fs": "^4.1.2", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.1.2", - "graceful-fs": "^4.2.4", - "jest-serializer": "^25.5.0", - "jest-util": "^25.5.0", - "jest-worker": "^25.5.0", - "micromatch": "^4.0.2", - "sane": "^4.0.3", - "walker": "^1.0.7", - "which": "^2.0.2" - } - }, - "jest-jasmine2": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-25.5.4.tgz", - "integrity": "sha512-9acbWEfbmS8UpdcfqnDO+uBUgKa/9hcRh983IHdM+pKmJPL77G0sWAAK0V0kr5LK3a8cSBfkFSoncXwQlRZfkQ==", - "dev": true, - "requires": { - "@babel/traverse": "^7.1.0", - "@jest/environment": "^25.5.0", - "@jest/source-map": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "co": "^4.6.0", - "expect": "^25.5.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^25.5.0", - "jest-matcher-utils": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-runtime": "^25.5.4", - "jest-snapshot": "^25.5.1", - "jest-util": "^25.5.0", - "pretty-format": "^25.5.0", - "throat": "^5.0.0" - } - }, - "jest-leak-detector": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-25.5.0.tgz", - "integrity": "sha512-rV7JdLsanS8OkdDpZtgBf61L5xZ4NnYLBq72r6ldxahJWWczZjXawRsoHyXzibM5ed7C2QRjpp6ypgwGdKyoVA==", - "dev": true, - "requires": { - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - } - }, - "jest-matcher-utils": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz", - "integrity": "sha512-VWI269+9JS5cpndnpCwm7dy7JtGQT30UHfrnM3mXl22gHGt/b7NkjBqXfbhZ8V4B7ANUsjK18PlSBmG0YH7gjw==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "jest-diff": "^25.5.0", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - } - }, - "jest-message-util": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.5.0.tgz", - "integrity": "sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@jest/types": "^25.5.0", - "@types/stack-utils": "^1.0.1", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "micromatch": "^4.0.2", - "slash": "^3.0.0", - "stack-utils": "^1.0.1" - } - }, - "jest-mock": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.5.0.tgz", - "integrity": "sha512-eXWuTV8mKzp/ovHc5+3USJMYsTBhyQ+5A1Mak35dey/RG8GlM4YWVylZuGgVXinaW6tpvk/RSecmF37FKUlpXA==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0" - } - }, - "jest-regex-util": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", - "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", - "dev": true - }, - "jest-resolve": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-25.5.1.tgz", - "integrity": "sha512-Hc09hYch5aWdtejsUZhA+vSzcotf7fajSlPA6EZPE1RmPBAD39XtJhvHWFStid58iit4IPDLI/Da4cwdDmAHiQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "browser-resolve": "^1.11.3", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "jest-pnp-resolver": "^1.2.1", - "read-pkg-up": "^7.0.1", - "realpath-native": "^2.0.0", - "resolve": "^1.17.0", - "slash": "^3.0.0" - } - }, - "jest-runner": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-25.5.4.tgz", - "integrity": "sha512-V/2R7fKZo6blP8E9BL9vJ8aTU4TH2beuqGNxHbxi6t14XzTb+x90B3FRgdvuHm41GY8ch4xxvf0ATH4hdpjTqg==", - "dev": true, - "requires": { - "@jest/console": "^25.5.0", - "@jest/environment": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-config": "^25.5.4", - "jest-docblock": "^25.3.0", - "jest-haste-map": "^25.5.1", - "jest-jasmine2": "^25.5.4", - "jest-leak-detector": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-resolve": "^25.5.1", - "jest-runtime": "^25.5.4", - "jest-util": "^25.5.0", - "jest-worker": "^25.5.0", - "source-map-support": "^0.5.6", - "throat": "^5.0.0" - } - }, - "jest-runtime": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-25.5.4.tgz", - "integrity": "sha512-RWTt8LeWh3GvjYtASH2eezkc8AehVoWKK20udV6n3/gC87wlTbE1kIA+opCvNWyyPeBs6ptYsc6nyHUb1GlUVQ==", - "dev": true, - "requires": { - "@jest/console": "^25.5.0", - "@jest/environment": "^25.5.0", - "@jest/globals": "^25.5.2", - "@jest/source-map": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "@types/yargs": "^15.0.0", - "chalk": "^3.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.4", - "jest-config": "^25.5.4", - "jest-haste-map": "^25.5.1", - "jest-message-util": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-regex-util": "^25.2.6", - "jest-resolve": "^25.5.1", - "jest-snapshot": "^25.5.1", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", - "realpath-native": "^2.0.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0", - "yargs": "^15.3.1" - } - }, - "jest-serializer": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.5.0.tgz", - "integrity": "sha512-LxD8fY1lByomEPflwur9o4e2a5twSQ7TaVNLlFUuToIdoJuBt8tzHfCsZ42Ok6LkKXWzFWf3AGmheuLAA7LcCA==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.4" - } - }, - "jest-snapshot": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-25.5.1.tgz", - "integrity": "sha512-C02JE1TUe64p2v1auUJ2ze5vcuv32tkv9PyhEb318e8XOKF7MOyXdJ7kdjbvrp3ChPLU2usI7Rjxs97Dj5P0uQ==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0", - "@jest/types": "^25.5.0", - "@types/prettier": "^1.19.0", - "chalk": "^3.0.0", - "expect": "^25.5.0", - "graceful-fs": "^4.2.4", - "jest-diff": "^25.5.0", - "jest-get-type": "^25.2.6", - "jest-matcher-utils": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-resolve": "^25.5.1", - "make-dir": "^3.0.0", - "natural-compare": "^1.4.0", - "pretty-format": "^25.5.0", - "semver": "^6.3.0" - } - }, - "jest-util": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.5.0.tgz", - "integrity": "sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "make-dir": "^3.0.0" - } - }, - "jest-validate": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-25.5.0.tgz", - "integrity": "sha512-okUFKqhZIpo3jDdtUXUZ2LxGUZJIlfdYBvZb1aczzxrlyMlqdnnws9MOxezoLGhSaFc2XYaHNReNQfj5zPIWyQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "camelcase": "^5.3.1", - "chalk": "^3.0.0", - "jest-get-type": "^25.2.6", - "leven": "^3.1.0", - "pretty-format": "^25.5.0" - } - }, - "jest-worker": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.5.0.tgz", - "integrity": "sha512-/dsSmUkIy5EBGfv/IjjqmFxrNAUpBERfGs1oHROyD7yxjG/w+t0GOJDX8O1k32ySmd7+a5IhnJU2qQFcJ4n1vw==", - "dev": true, - "requires": { - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - } - }, - "jsdom": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", - "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "acorn": "^7.1.0", - "acorn-globals": "^4.3.2", - "array-equal": "^1.0.0", - "cssom": "^0.4.1", - "cssstyle": "^2.0.0", - "data-urls": "^1.1.0", - "domexception": "^1.0.1", - "escodegen": "^1.11.1", - "html-encoding-sniffer": "^1.0.2", - "nwsapi": "^2.2.0", - "parse5": "5.1.0", - "pn": "^1.1.0", - "request": "^2.88.0", - "request-promise-native": "^1.0.7", - "saxes": "^3.1.9", - "symbol-tree": "^3.2.2", - "tough-cookie": "^3.0.1", - "w3c-hr-time": "^1.0.1", - "w3c-xmlserializer": "^1.1.2", - "webidl-conversions": "^4.0.2", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^7.0.0", - "ws": "^7.0.0", - "xml-name-validator": "^3.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "parse-json": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", - "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "parse5": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", - "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "pretty-format": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", - "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^16.12.0" - } - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - } - }, - "realpath-native": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz", - "integrity": "sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, - "throat": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", - "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tough-cookie": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", - "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", - "dev": true, - "requires": { - "ip-regex": "^2.1.0", - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - }, - "whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "ws": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", - "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==", - "dev": true - }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "@jest/transform": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-24.9.0.tgz", - "integrity": "sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^24.9.0", - "babel-plugin-istanbul": "^5.1.0", - "chalk": "^2.0.1", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.1.15", - "jest-haste-map": "^24.9.0", - "jest-regex-util": "^24.9.0", - "jest-util": "^24.9.0", - "micromatch": "^3.1.10", - "pirates": "^4.0.1", - "realpath-native": "^1.1.0", - "slash": "^2.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "2.4.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "@jest/types": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", - "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", - "@types/yargs": "^13.0.0" - } - }, - "@sinonjs/commons": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", - "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@slack/logger": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-2.0.0.tgz", - "integrity": "sha512-OkIJpiU2fz6HOJujhlhfIGrc8hB4ibqtf7nnbJQDerG0BqwZCfmgtK5sWzZ0TkXVRBKD5MpLrTmCYyMxoMCgPw==", - "dev": true, - "requires": { - "@types/node": ">=8.9.0" - } - }, - "@slack/types": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@slack/types/-/types-1.9.0.tgz", - "integrity": "sha512-RmwgMWqOtzd2JPXdiaD/tyrDD0vtjjRDFdxN1I3tAxwBbg4aryzDUVqFc8na16A+3Xik/UN8X1hvVTw8J4EB9w==", - "dev": true - }, - "@slack/web-api": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-5.12.0.tgz", - "integrity": "sha512-ygSnNHVid7PltGo7W36f2SNVHyliemkzxn9uSwgnWNF7CHmWBKWAylU/eoDml9l5K7akMOxbousiurOw4XqOFg==", - "dev": true, - "requires": { - "@slack/logger": ">=1.0.0 <3.0.0", - "@slack/types": "^1.7.0", - "@types/is-stream": "^1.1.0", - "@types/node": ">=8.9.0", - "@types/p-queue": "^2.3.2", - "axios": "^0.19.0", - "eventemitter3": "^3.1.0", - "form-data": "^2.5.0", - "is-stream": "^1.1.0", - "p-queue": "^2.4.2", - "p-retry": "^4.0.0" - } - }, - "@tannin/compile": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@tannin/compile/-/compile-1.1.0.tgz", - "integrity": "sha512-n8m9eNDfoNZoxdvWiTfW/hSPhehzLJ3zW7f8E7oT6mCROoMNWCB4TYtv041+2FMAxweiE0j7i1jubQU4MEC/Gg==", - "dev": true, - "requires": { - "@tannin/evaluate": "^1.2.0", - "@tannin/postfix": "^1.1.0" - } - }, - "@tannin/evaluate": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@tannin/evaluate/-/evaluate-1.2.0.tgz", - "integrity": "sha512-3ioXvNowbO/wSrxsDG5DKIMxC81P0QrQTYai8zFNY+umuoHWRPbQ/TuuDEOju9E+jQDXmj6yI5GyejNuh8I+eg==", - "dev": true - }, - "@tannin/plural-forms": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@tannin/plural-forms/-/plural-forms-1.1.0.tgz", - "integrity": "sha512-xl9R2mDZO/qiHam1AgMnAES6IKIg7OBhcXqy6eDsRCdXuxAFPcjrej9HMjyCLE0DJ/8cHf0i5OQTstuBRhpbHw==", - "dev": true, - "requires": { - "@tannin/compile": "^1.1.0" - } - }, - "@tannin/postfix": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@tannin/postfix/-/postfix-1.1.0.tgz", - "integrity": "sha512-oocsqY7g0cR+Gur5jRQLSrX2OtpMLMse1I10JQBm8CdGMrDkh1Mg2gjsiquMHRtBs4Qwu5wgEp5GgIYHk4SNPw==", - "dev": true - }, - "@types/babel__core": { - "version": "7.1.10", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.10.tgz", - "integrity": "sha512-x8OM8XzITIMyiwl5Vmo2B1cR1S1Ipkyv4mdlbJjMa1lmuKvKY9FrBbEANIaMlnWn5Rf7uO+rC/VgYabNkE17Hw==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "@types/babel__generator": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.1.tgz", - "integrity": "sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@types/babel__template": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.3.tgz", - "integrity": "sha512-uCoznIPDmnickEi6D0v11SBpW0OuVqHJCa7syXqQHy5uktSCreIlt0iglsCnmvz8yCb38hGcWeseA8cWJSwv5Q==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@types/babel__traverse": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.14.tgz", - "integrity": "sha512-8w9szzKs14ZtBVuP6Wn7nMLRJ0D6dfB0VEBEyRgxrZ/Ln49aNMykrghM2FaNn4FJRzNppCSa0Rv9pBRM5Xc3wg==", - "dev": true, - "requires": { - "@babel/types": "^7.3.0" - } - }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true - }, - "@types/graceful-fs": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.3.tgz", - "integrity": "sha512-AiHRaEB50LQg0pZmm659vNBb9f4SJ0qrAnteuzhSeAUcJKxoYgEnprg/83kppCnc2zvtCKbdZry1a5pVY3lOTQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@types/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-jkZatu4QVbR60mpIzjINmtS1ZF4a/FqdTUTBeQDVOQ2PYyidtwFKr0B5G6ERukKwliq+7mIXvxyppwzG5EgRYg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", - "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", - "dev": true - }, - "@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*" - } - }, - "@types/istanbul-reports": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", - "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*", - "@types/istanbul-lib-report": "*" - } - }, - "@types/json-schema": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", - "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==" - }, - "@types/mime-types": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.0.tgz", - "integrity": "sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM=", - "dev": true - }, - "@types/node": { - "version": "14.11.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz", - "integrity": "sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA==", - "dev": true - }, - "@types/normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", - "dev": true - }, - "@types/p-queue": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@types/p-queue/-/p-queue-2.3.2.tgz", - "integrity": "sha512-eKAv5Ql6k78dh3ULCsSBxX6bFNuGjTmof5Q/T6PiECDq0Yf8IIn46jCyp3RJvCi8owaEmm3DZH1PEImjBMd/vQ==", - "dev": true - }, - "@types/prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==", - "dev": true - }, - "@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "dev": true - }, - "@types/stack-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", - "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", - "dev": true - }, - "@types/yargs": { - "version": "13.0.11", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", - "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "@types/yargs-parser": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", - "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==", - "dev": true - }, - "@typescript-eslint/experimental-utils": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz", - "integrity": "sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==", - "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "1.13.0", - "eslint-scope": "^4.0.0" - }, - "dependencies": { - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - } - } - }, - "@typescript-eslint/typescript-estree": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz", - "integrity": "sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==", - "requires": { - "lodash.unescape": "4.0.1", - "semver": "5.5.0" - }, - "dependencies": { - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" - } - } - }, - "@wordpress/e2e-test-utils": { - "version": "4.15.0", - "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils/-/e2e-test-utils-4.15.0.tgz", - "integrity": "sha512-mCOlNDX/yERd7hIAFB+y9x56iCQ2XyDZkWNlQNMYRH0+EdrQ5H5zE7MSxzycideIC7grxKw/j4RcuyxUdSWGDw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.11.2", - "@wordpress/keycodes": "^2.16.0", - "@wordpress/url": "^2.19.0", - "lodash": "^4.17.19", - "node-fetch": "^2.6.0" - }, - "dependencies": { - "@wordpress/i18n": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-3.16.0.tgz", - "integrity": "sha512-ZyRWplETgD90caVaBuGBFcnYVpcogji1g9Ctbb5AO2bGFeHpmPpjvWm0NE64iQTtLFEJoaCiq6oqUvAOPIQJpw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.11.2", - "gettext-parser": "^1.3.1", - "lodash": "^4.17.19", - "memize": "^1.1.0", - "sprintf-js": "^1.1.1", - "tannin": "^1.2.0" - } - }, - "@wordpress/keycodes": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-2.16.0.tgz", - "integrity": "sha512-8CfxB+9f08FXMUsaO625abmbx2ZinFUz6upzXbe0Da8W3oy7+/TZz6EWsMVBEWz+alSR3Z2FUZ7xUuopHZFcow==", - "dev": true, - "requires": { - "@babel/runtime": "^7.11.2", - "@wordpress/i18n": "^3.16.0", - "lodash": "^4.17.19" - } - }, - "@wordpress/url": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-2.19.0.tgz", - "integrity": "sha512-RizWbBxYmWBlNd+q89r3N6Y2XO8eCG3VncnXDgbGnhV4e+2z9fjzp1/9C/SORftEn+ix/qBKbqygmkmBqb+wuw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.11.2", - "lodash": "^4.17.19", - "qs": "^6.5.2", - "react-native-url-polyfill": "^1.1.2" - } - }, - "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "dev": true - } - } - }, - "@wordpress/eslint-plugin": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-4.1.0.tgz", - "integrity": "sha512-AHkpSPECN9WRZPgcf1PXey23z1uDreH0siespO7K32wAIqiCXz2PS9Qs06K5BE+9jnjYOy+40krdbHYWVIZqKQ==", - "requires": { - "babel-eslint": "^10.0.3", - "eslint-config-prettier": "^6.10.0", - "eslint-plugin-jest": "^22.15.1", - "eslint-plugin-jsdoc": "^15.8.0", - "eslint-plugin-jsx-a11y": "^6.2.3", - "eslint-plugin-prettier": "^3.1.2", - "eslint-plugin-react": "^7.14.3", - "eslint-plugin-react-hooks": "^1.6.1", - "globals": "^12.0.0", - "prettier": "npm:wp-prettier@1.19.1", - "requireindex": "^1.2.0" - }, - "dependencies": { - "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", - "requires": { - "type-fest": "^0.8.1" - } - }, - "prettier": { - "version": "npm:prettier@1.19.1", - "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-1.19.1.tgz", - "integrity": "sha512-mqAC2r1NDmRjG+z3KCJ/i61tycKlmADIjxnDhQab+KBxSAGbF/W7/zwB2guy/ypIeKrrftNsIYkNZZQKf3vJcg==" - } - } - }, - "@wordpress/i18n": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-3.15.0.tgz", - "integrity": "sha512-AawJgHEGPyMoPATl8a3Qa+cCZV3S6azPfvqeStbN2pSc7v0HqYhJhWaw80WToHkN4kyOsfu1PUVf1wefuoMNEA==", - "dev": true, - "requires": { - "@babel/runtime": "^7.9.2", - "gettext-parser": "^1.3.1", - "lodash": "^4.17.19", - "memize": "^1.1.0", - "sprintf-js": "^1.1.1", - "tannin": "^1.2.0" - } - }, - "@wordpress/keycodes": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-2.15.0.tgz", - "integrity": "sha512-XHyBmhzWjp0svzwiGLOwovlQHH42KkACKTfakDizB5OaaAzlmgZ34Fdl03S7pWl+HUBa7MqItRhGsd4kxdo0bQ==", - "dev": true, - "requires": { - "@babel/runtime": "^7.9.2", - "@wordpress/i18n": "^3.15.0", - "lodash": "^4.17.19" - } - }, - "@wordpress/url": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-2.18.0.tgz", - "integrity": "sha512-FX6CYVG8vYgQnxjA9SsWTDAWPHarPSBIGk2shZ3I+cq+LV31dDaAz8OhvVMD6rvUoQW0INlWe1t2JKXoHhcTcw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.9.2", - "lodash": "^4.17.19", - "qs": "^6.5.2", - "react-native-url-polyfill": "^1.1.2" - } - }, - "abab": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", - "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", - "dev": true - }, - "acorn": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", - "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", - "dev": true - }, - "acorn-globals": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", - "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", - "dev": true, - "requires": { - "acorn": "^6.0.1", - "acorn-walk": "^6.0.1" - }, - "dependencies": { - "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", - "dev": true - } - } - }, - "acorn-jsx": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", - "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", - "dev": true - }, - "acorn-walk": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", - "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", - "dev": true - }, - "agent-base": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", - "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", - "dev": true - }, - "ajv": { - "version": "6.12.5", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz", - "integrity": "sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-align": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", - "requires": { - "string-width": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "ansi-escapes": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", - "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", - "dev": true, - "requires": { - "type-fest": "^0.11.0" - }, - "dependencies": { - "type-fest": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", - "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", - "dev": true - } - } - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "app-root-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz", - "integrity": "sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw==", - "dev": true - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - }, - "dependencies": { - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - } - } - }, - "aria-query": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", - "requires": { - "@babel/runtime": "^7.10.2", - "@babel/runtime-corejs3": "^7.10.2" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", - "dev": true - }, - "array-includes": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", - "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0", - "is-string": "^1.0.5" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", - "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-regex": "^1.1.0", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "array.prototype.flatmap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.3.tgz", - "integrity": "sha512-OOEk+lkePcg+ODXIpvuU9PAryCikCJyo7GlDG1upleEpQRx6mzL9puEBkozQ5iAx20KV0l3DbyQwqciJtqe5Pg==", - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", - "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-regex": "^1.1.0", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=" - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true - }, - "async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true, - "optional": true - }, - "async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz", - "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==", - "dev": true - }, - "axe-core": { - "version": "3.5.5", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-3.5.5.tgz", - "integrity": "sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q==" - }, - "axios": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", - "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", - "dev": true, - "requires": { - "follow-redirects": "1.5.10" - } - }, - "axobject-query": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", - "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==" - }, - "babel-eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", - "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0", - "eslint-visitor-keys": "^1.0.0", - "resolve": "^1.12.0" - } - }, - "babel-jest": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.9.0.tgz", - "integrity": "sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw==", - "dev": true, - "requires": { - "@jest/transform": "^24.9.0", - "@jest/types": "^24.9.0", - "@types/babel__core": "^7.1.0", - "babel-plugin-istanbul": "^5.1.0", - "babel-preset-jest": "^24.9.0", - "chalk": "^2.4.2", - "slash": "^2.0.0" - } - }, - "babel-plugin-dynamic-import-node": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", - "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", - "dev": true, - "requires": { - "object.assign": "^4.1.0" - } - }, - "babel-plugin-istanbul": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz", - "integrity": "sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "find-up": "^3.0.0", - "istanbul-lib-instrument": "^3.3.0", - "test-exclude": "^5.2.3" - } - }, - "babel-plugin-jest-hoist": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.9.0.tgz", - "integrity": "sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw==", - "dev": true, - "requires": { - "@types/babel__traverse": "^7.0.6" - } - }, - "babel-preset-current-node-syntax": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.3.tgz", - "integrity": "sha512-uyexu1sVwcdFnyq9o8UQYsXwXflIh8LvrF5+cKrYam93ned1CStffB3+BEcsxGSgagoA3GEyjDqO4a/58hyPYQ==", - "dev": true, - "requires": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - } - }, - "babel-preset-jest": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz", - "integrity": "sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg==", - "dev": true, - "requires": { - "@babel/plugin-syntax-object-rest-spread": "^7.0.0", - "babel-plugin-jest-hoist": "^24.9.0" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true, - "optional": true - }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "optional": true, - "requires": { - "file-uri-to-path": "1.0.0" - } - }, - "boxen": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", - "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", - "requires": { - "ansi-align": "^2.0.0", - "camelcase": "^4.0.0", - "chalk": "^2.0.1", - "cli-boxes": "^1.0.0", - "string-width": "^2.0.0", - "term-size": "^1.2.0", - "widest-line": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", - "dev": true - }, - "browser-resolve": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", - "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", - "dev": true, - "requires": { - "resolve": "1.1.7" - }, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - } - } - }, - "browserslist": { - "version": "4.14.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.5.tgz", - "integrity": "sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001135", - "electron-to-chromium": "^1.3.571", - "escalade": "^3.1.0", - "node-releases": "^1.1.61" - } - }, - "bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "requires": { - "node-int64": "^0.4.0" - } - }, - "buffer": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", - "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "requires": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" - }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "caniuse-lite": { - "version": "1.0.30001137", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001137.tgz", - "integrity": "sha512-54xKQZTqZrKVHmVz0+UvdZR6kQc7pJDgfhsMYDG19ID1BWoNnDMFm5Q3uSBSU401pBvKYMsHAt9qhEDcxmk8aw==", - "dev": true - }, - "capture-exit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", - "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", - "dev": true, - "requires": { - "rsvp": "^4.8.4" - } - }, - "capture-stack-trace": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", - "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==" - }, - "carlo": { - "version": "0.9.46", - "resolved": "https://registry.npmjs.org/carlo/-/carlo-0.9.46.tgz", - "integrity": "sha512-FwZ/wxjqe+5RgzF2SRsPSWsVB9+McAVRWW0tRkmbh7fBjrf3HFZZbcr8vr61p1K+NBaAPv57DRjxgIyfbHmd7g==", - "requires": { - "debug": "^4.1.0", - "puppeteer-core": "~1.12.0" - }, - "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "optional": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=" - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true - }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - }, - "dependencies": { - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - } - } - }, - "clone-deep": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", - "integrity": "sha1-TnPdCen7lxzDhnDF3O2cGJZIHMY=", - "dev": true, - "requires": { - "for-own": "^0.1.3", - "is-plain-object": "^2.0.1", - "kind-of": "^3.0.2", - "lazy-cache": "^1.0.3", - "shallow-clone": "^0.1.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", - "dev": true - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true - }, - "comment-parser": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-0.6.2.tgz", - "integrity": "sha512-Wdms0Q8d4vvb2Yk72OwZjwNWtMklbC5Re7lD9cjCP/AG1fhocmc0TrxGBBAXPLy8fZQPrfHGgyygwI0lA7pbzA==" - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "config": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/config/-/config-3.3.2.tgz", - "integrity": "sha512-NlGfBn2565YA44Irn7GV5KHlIGC3KJbf0062/zW5ddP9VXIuRj0m7HVyFAWvMZvaHPEglyGfwmevGz3KosIpCg==", - "dev": true, - "requires": { - "json5": "^2.1.1" - } - }, - "configstore": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.5.tgz", - "integrity": "sha512-nlOhI4+fdzoK5xmJ+NY+1gZK56bwEaWZr8fYuXohZ9Vkc1o3a4T/R3M+yE/w7x/ZVJ1zF8c+oaOvF0dztdUgmA==", - "requires": { - "dot-prop": "^4.2.1", - "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "unique-string": "^1.0.0", - "write-file-atomic": "^2.0.0", - "xdg-basedir": "^3.0.0" - }, - "dependencies": { - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - } - } - }, - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" - }, - "core-js-compat": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.5.tgz", - "integrity": "sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng==", - "dev": true, - "requires": { - "browserslist": "^4.8.5", - "semver": "7.0.0" - }, - "dependencies": { - "semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "dev": true - } - } - }, - "core-js-pure": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.5.tgz", - "integrity": "sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "requires": { - "capture-stack-trace": "^1.0.0" - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "crypto-random-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" - }, - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - }, - "cssstyle": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz", - "integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==", - "dev": true, - "requires": { - "cssom": "0.3.x" - } - }, - "cwd": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz", - "integrity": "sha1-FyQAaUBXwioTsM8WFix+S3p/5Wc=", - "dev": true, - "requires": { - "find-pkg": "^0.1.2", - "fs-exists-sync": "^0.1.0" - } - }, - "damerau-levenshtein": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz", - "integrity": "sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==" - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "data-urls": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", - "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "whatwg-mimetype": "^2.2.0", - "whatwg-url": "^7.0.0" - }, - "dependencies": { - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - }, - "whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - } - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "detect-newline": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", - "dev": true - }, - "diff-sequences": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", - "integrity": "sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==", - "dev": true - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "domexception": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", - "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", - "dev": true, - "requires": { - "webidl-conversions": "^4.0.2" - }, - "dependencies": { - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - } - } - }, - "dot-prop": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", - "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", - "requires": { - "is-obj": "^1.0.0" - } - }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "electron-to-chromium": { - "version": "1.3.572", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.572.tgz", - "integrity": "sha512-TKqdEukCCl7JC20SwEoWTbtnGt4YjfHWAv4tcNky0a9qGo0WdM+Lrd60tps+nkaJCmktKBJjr99fLtEBU1ipWQ==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "requires": { - "iconv-lite": "^0.6.2" - } - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.18.0-next.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.0.tgz", - "integrity": "sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-negative-zero": "^2.0.0", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "requires": { - "es6-promise": "^4.0.3" - } - }, - "escalade": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.0.tgz", - "integrity": "sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "dev": true, - "requires": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } - } - }, - "eslint": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.7.2.tgz", - "integrity": "sha512-qMlSWJaCSxDFr8fBPvJM9kJwbazrhNcBU3+DszDW1OlEwKBBRWsJc7NJFelvwQpanHCR14cOLD41x8Eqvo3Nng==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.3", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.2", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^7.0.0", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.3", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", - "dev": true, - "requires": { - "type-fest": "^0.8.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "eslint-config-prettier": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.12.0.tgz", - "integrity": "sha512-9jWPlFlgNwRUYVoujvWTQ1aMO8o6648r+K7qU7K5Jmkbyqav1fuEZC0COYpGBxyiAJb65Ra9hrmFx19xRGwXWw==", - "requires": { - "get-stdin": "^6.0.0" - } - }, - "eslint-plugin-jest": { - "version": "22.21.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-22.21.0.tgz", - "integrity": "sha512-OaqnSS7uBgcGiqXUiEnjoqxPNKvR4JWG5mSRkzVoR6+vDwlqqp11beeql1hYs0HTbdhiwrxWLxbX0Vx7roG3Ew==", - "requires": { - "@typescript-eslint/experimental-utils": "^1.13.0" - } - }, - "eslint-plugin-jsdoc": { - "version": "15.12.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-15.12.2.tgz", - "integrity": "sha512-QHzPc3VKTEbTn369/HpqDjl/czv3fCei/bZg5NA5tu9Od10MfpTH4kc1xnRDobhQoDs3AMz9wuaI4coHWRzMQw==", - "requires": { - "comment-parser": "^0.6.2", - "debug": "^4.1.1", - "jsdoctypeparser": "^5.1.1", - "lodash": "^4.17.15", - "object.entries-ponyfill": "^1.0.1", - "regextras": "^0.6.1" - }, - "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "eslint-plugin-jsx-a11y": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.3.1.tgz", - "integrity": "sha512-i1S+P+c3HOlBJzMFORRbC58tHa65Kbo8b52/TwCwSKLohwvpfT5rm2GjGWzOHTEuq4xxf2aRlHHTtmExDQOP+g==", - "requires": { - "@babel/runtime": "^7.10.2", - "aria-query": "^4.2.2", - "array-includes": "^3.1.1", - "ast-types-flow": "^0.0.7", - "axe-core": "^3.5.4", - "axobject-query": "^2.1.2", - "damerau-levenshtein": "^1.0.6", - "emoji-regex": "^9.0.0", - "has": "^1.0.3", - "jsx-ast-utils": "^2.4.1", - "language-tags": "^1.0.5" - }, - "dependencies": { - "emoji-regex": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.0.0.tgz", - "integrity": "sha512-6p1NII1Vm62wni/VR/cUMauVQoxmLVb9csqQlvLz+hO2gk8U2UYDfXHQSUYIBKmZwAKz867IDqG7B+u0mj+M6w==" - } - } - }, - "eslint-plugin-prettier": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz", - "integrity": "sha512-jZDa8z76klRqo+TdGDTFJSavwbnWK2ZpqGKNZ+VvweMW516pDUMmQ2koXvxEE4JhzNvTv+radye/bWGBmA6jmg==", - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-plugin-react": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.21.2.tgz", - "integrity": "sha512-j3XKvrK3rpBzveKFbgAeGsWb9uz6iUOrR0jixRfjwdFeGSRsXvVTFtHDQYCjsd1/6Z/xvb8Vy3LiI5Reo7fDrg==", - "requires": { - "array-includes": "^3.1.1", - "array.prototype.flatmap": "^1.2.3", - "doctrine": "^2.1.0", - "has": "^1.0.3", - "jsx-ast-utils": "^2.4.1", - "object.entries": "^1.1.2", - "object.fromentries": "^2.0.2", - "object.values": "^1.1.1", - "prop-types": "^15.7.2", - "resolve": "^1.17.0", - "string.prototype.matchall": "^4.0.2" - }, - "dependencies": { - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "requires": { - "esutils": "^2.0.2" - } - } - } - }, - "eslint-plugin-react-hooks": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz", - "integrity": "sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==" - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==" - }, - "espree": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", - "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", - "dev": true, - "requires": { - "acorn": "^7.1.1", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.1.0" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esquery": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", - "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "eventemitter3": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", - "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", - "dev": true - }, - "exec-sh": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", - "integrity": "sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==", - "dev": true - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expand-tilde": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", - "integrity": "sha1-C4HrqJflo9MdHD0QL48BRB5VlEk=", - "dev": true, - "requires": { - "os-homedir": "^1.0.1" - } - }, - "expect": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-24.9.0.tgz", - "integrity": "sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q==", - "dev": true, - "requires": { - "@jest/types": "^24.9.0", - "ansi-styles": "^3.2.0", - "jest-get-type": "^24.9.0", - "jest-matcher-utils": "^24.9.0", - "jest-message-util": "^24.9.0", - "jest-regex-util": "^24.9.0" - } - }, - "expect-puppeteer": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/expect-puppeteer/-/expect-puppeteer-4.4.0.tgz", - "integrity": "sha512-6Ey4Xy2xvmuQu7z7YQtMsaMV0EHJRpVxIDOd5GRrm04/I3nkTKIutELfECsLp6le+b3SSa3cXhPiw6PgqzxYWA==", - "dev": true - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "dependencies": { - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "extract-zip": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", - "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", - "requires": { - "concat-stream": "^1.6.2", - "debug": "^2.6.9", - "mkdirp": "^0.5.4", - "yauzl": "^2.10.0" - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==" - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fb-watchman": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", - "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", - "dev": true, - "requires": { - "bser": "2.1.1" - } - }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", - "requires": { - "pend": "~1.2.0" - } - }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "dev": true, - "requires": { - "flat-cache": "^2.0.1" - } - }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "optional": true - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "find-file-up": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/find-file-up/-/find-file-up-0.1.3.tgz", - "integrity": "sha1-z2gJG8+fMApA2kEbN9pczlovvqA=", - "dev": true, - "requires": { - "fs-exists-sync": "^0.1.0", - "resolve-dir": "^0.1.0" - } - }, - "find-pkg": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/find-pkg/-/find-pkg-0.1.2.tgz", - "integrity": "sha1-G9wiwG42NlUy4qJIBGhUuXiNpVc=", - "dev": true, - "requires": { - "find-file-up": "^0.1.2" - } - }, - "find-process": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.3.tgz", - "integrity": "sha512-+IA+AUsQCf3uucawyTwMWcY+2M3FXq3BRvw3S+j5Jvydjk31f/+NPWpYZOJs+JUs2GvxH4Yfr6Wham0ZtRLlPA==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "commander": "^2.11.0", - "debug": "^2.6.8" - }, - "dependencies": { - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - } - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "dev": true, - "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - } - }, - "flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", - "dev": true - }, - "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "dev": true, - "requires": { - "debug": "=3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "fs-exists-sync": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", - "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=", - "dev": true - }, - "fs-readdir-recursive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", - "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "dev": true, - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "gensync": { - "version": "1.0.0-beta.1", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", - "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", - "dev": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true - }, - "get-stdin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", - "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==" - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "gettext-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.4.0.tgz", - "integrity": "sha512-sedZYLHlHeBop/gZ1jdg59hlUEcpcZJofLq2JFwJT1zTqAU3l2wFv6IsuwFHGqbiT9DWzMUW4/em2+hspnmMMA==", - "dev": true, - "requires": { - "encoding": "^0.1.12", - "safe-buffer": "^5.1.1" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "optional": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "optional": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", - "requires": { - "ini": "^1.3.4" - } - }, - "global-modules": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", - "integrity": "sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0=", - "dev": true, - "requires": { - "global-prefix": "^0.1.4", - "is-windows": "^0.2.0" - }, - "dependencies": { - "is-windows": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", - "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", - "dev": true - } - } - }, - "global-prefix": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", - "integrity": "sha1-jTvGuNo8qBEqFg2NSW/wRiv+948=", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.0", - "ini": "^1.3.4", - "is-windows": "^0.2.0", - "which": "^1.2.12" - }, - "dependencies": { - "is-windows": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", - "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", - "dev": true - } - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" - }, - "got": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", - "requires": { - "create-error-class": "^3.0.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "unzip-response": "^2.0.1", - "url-parse-lax": "^1.0.0" - }, - "dependencies": { - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" - } - } - }, - "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" - }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "dev": true, - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "has-symbols": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "dev": true, - "requires": { - "parse-passwd": "^1.0.0" - } - }, - "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", - "dev": true - }, - "html-encoding-sniffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", - "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", - "dev": true, - "requires": { - "whatwg-encoding": "^1.0.1" - } - }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-proxy-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", - "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", - "dev": true, - "requires": { - "agent-base": "5", - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", - "dev": true - }, - "iconv-lite": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", - "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "import-fresh": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", - "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" - }, - "import-local": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", - "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", - "dev": true, - "requires": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" - }, - "inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "internal-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.2.tgz", - "integrity": "sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g==", - "requires": { - "es-abstract": "^1.17.0-next.1", - "has": "^1.0.3", - "side-channel": "^1.0.2" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", - "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-regex": "^1.1.0", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "requires": { - "loose-envify": "^1.0.0" - } - }, - "ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", - "dev": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "optional": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-callable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", - "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" - }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "requires": { - "ci-info": "^2.0.0" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-date-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-installed-globally": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", - "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", - "requires": { - "global-dirs": "^0.1.0", - "is-path-inside": "^1.0.0" - } - }, - "is-negative-zero": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", - "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=" - }, - "is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=" - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "requires": { - "path-is-inside": "^1.0.1" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" - }, - "is-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", - "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", - "requires": { - "has-symbols": "^1.0.1" - } - }, - "is-retry-allowed": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", - "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==" - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "requires": { - "has-symbols": "^1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isbinaryfile": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz", - "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", - "requires": { - "buffer-alloc": "^1.2.0" - } - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", - "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", - "dev": true, - "requires": { - "@babel/generator": "^7.4.0", - "@babel/parser": "^7.4.3", - "@babel/template": "^7.4.0", - "@babel/traverse": "^7.4.3", - "@babel/types": "^7.4.0", - "istanbul-lib-coverage": "^2.0.5", - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "istanbul-lib-report": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", - "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^2.0.5", - "make-dir": "^2.1.0", - "supports-color": "^6.1.0" - }, - "dependencies": { - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", - "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^2.0.5", - "make-dir": "^2.1.0", - "rimraf": "^2.6.3", - "source-map": "^0.6.1" - }, - "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "istanbul-reports": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz", - "integrity": "sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0" - } - }, - "jest": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest/-/jest-25.5.4.tgz", - "integrity": "sha512-hHFJROBTqZahnO+X+PMtT6G2/ztqAZJveGqz//FnWWHurizkD05PQGzRZOhF3XP6z7SJmL+5tCfW8qV06JypwQ==", - "dev": true, - "requires": { - "@jest/core": "^25.5.4", - "import-local": "^3.0.2", - "jest-cli": "^25.5.4" - }, - "dependencies": { - "@jest/console": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-25.5.0.tgz", - "integrity": "sha512-T48kZa6MK1Y6k4b89sexwmSF4YLeZS/Udqg3Jj3jG/cHH+N/sLFCEoXEDMOKugJQ9FxPN1osxIknvKkxt6MKyw==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "jest-message-util": "^25.5.0", - "jest-util": "^25.5.0", - "slash": "^3.0.0" - } - }, - "@jest/core": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-25.5.4.tgz", - "integrity": "sha512-3uSo7laYxF00Dg/DMgbn4xMJKmDdWvZnf89n8Xj/5/AeQ2dOQmn6b6Hkj/MleyzZWXpwv+WSdYWl4cLsy2JsoA==", - "dev": true, - "requires": { - "@jest/console": "^25.5.0", - "@jest/reporters": "^25.5.1", - "@jest/test-result": "^25.5.0", - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "ansi-escapes": "^4.2.1", - "chalk": "^3.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-changed-files": "^25.5.0", - "jest-config": "^25.5.4", - "jest-haste-map": "^25.5.1", - "jest-message-util": "^25.5.0", - "jest-regex-util": "^25.2.6", - "jest-resolve": "^25.5.1", - "jest-resolve-dependencies": "^25.5.4", - "jest-runner": "^25.5.4", - "jest-runtime": "^25.5.4", - "jest-snapshot": "^25.5.1", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", - "jest-watcher": "^25.5.0", - "micromatch": "^4.0.2", - "p-each-series": "^2.1.0", - "realpath-native": "^2.0.0", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "@jest/environment": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.5.0.tgz", - "integrity": "sha512-U2VXPEqL07E/V7pSZMSQCvV5Ea4lqOlT+0ZFijl/i316cRMHvZ4qC+jBdryd+lmRetjQo0YIQr6cVPNxxK87mA==", - "dev": true, - "requires": { - "@jest/fake-timers": "^25.5.0", - "@jest/types": "^25.5.0", - "jest-mock": "^25.5.0" - } - }, - "@jest/fake-timers": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.5.0.tgz", - "integrity": "sha512-9y2+uGnESw/oyOI3eww9yaxdZyHq7XvprfP/eeoCsjqKYts2yRlsHS/SgjPDV8FyMfn2nbMy8YzUk6nyvdLOpQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-util": "^25.5.0", - "lolex": "^5.0.0" - } - }, - "@jest/reporters": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-25.5.1.tgz", - "integrity": "sha512-3jbd8pPDTuhYJ7vqiHXbSwTJQNavczPs+f1kRprRDxETeE3u6srJ+f0NPuwvOmk+lmunZzPkYWIFZDLHQPkviw==", - "dev": true, - "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.2.4", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^4.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.2", - "jest-haste-map": "^25.5.1", - "jest-resolve": "^25.5.1", - "jest-util": "^25.5.0", - "jest-worker": "^25.5.0", - "node-notifier": "^6.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.0", - "string-length": "^3.1.0", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^4.1.3" - } - }, - "@jest/source-map": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-25.5.0.tgz", - "integrity": "sha512-eIGx0xN12yVpMcPaVpjXPnn3N30QGJCJQSkEDUt9x1fI1Gdvb07Ml6K5iN2hG7NmMP6FDmtPEssE3z6doOYUwQ==", - "dev": true, - "requires": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.4", - "source-map": "^0.6.0" - } - }, - "@jest/test-result": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-25.5.0.tgz", - "integrity": "sha512-oV+hPJgXN7IQf/fHWkcS99y0smKLU2czLBJ9WA0jHITLst58HpQMtzSYxzaBvYc6U5U6jfoMthqsUlUlbRXs0A==", - "dev": true, - "requires": { - "@jest/console": "^25.5.0", - "@jest/types": "^25.5.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - } - }, - "@jest/transform": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.5.1.tgz", - "integrity": "sha512-Y8CEoVwXb4QwA6Y/9uDkn0Xfz0finGkieuV0xkdF9UtZGJeLukD5nLkaVrVsODB1ojRWlaoD0AJZpVHCSnJEvg==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^25.5.0", - "babel-plugin-istanbul": "^6.0.0", - "chalk": "^3.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^25.5.1", - "jest-regex-util": "^25.2.6", - "jest-util": "^25.5.0", - "micromatch": "^4.0.2", - "pirates": "^4.0.1", - "realpath-native": "^2.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - } - }, - "@jest/types": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", - "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", - "@types/yargs": "^15.0.0", - "chalk": "^3.0.0" - } - }, - "@types/yargs": { - "version": "15.0.7", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.7.tgz", - "integrity": "sha512-Gf4u3EjaPNcC9cTu4/j2oN14nSVhr8PQ+BvBcBQHAhDZfl0bVIiLgvnRXv/dn58XhTm9UXvBpvJpDlwV65QxOA==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "babel-jest": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.5.1.tgz", - "integrity": "sha512-9dA9+GmMjIzgPnYtkhBg73gOo/RHqPmLruP3BaGL4KEX3Dwz6pI8auSN8G8+iuEG90+GSswyKvslN+JYSaacaQ==", - "dev": true, - "requires": { - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "@types/babel__core": "^7.1.7", - "babel-plugin-istanbul": "^6.0.0", - "babel-preset-jest": "^25.5.0", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "slash": "^3.0.0" - } - }, - "babel-plugin-istanbul": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", - "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^4.0.0", - "test-exclude": "^6.0.0" - } - }, - "babel-plugin-jest-hoist": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.5.0.tgz", - "integrity": "sha512-u+/W+WAjMlvoocYGTwthAiQSxDcJAyHpQ6oWlHdFZaaN+Rlk8Q7iiwDPg2lN/FyJtAYnKjFxbn7xus4HCFkg5g==", - "dev": true, - "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__traverse": "^7.0.6" - } - }, - "babel-preset-jest": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-25.5.0.tgz", - "integrity": "sha512-8ZczygctQkBU+63DtSOKGh7tFL0CeCuz+1ieud9lJ1WPQ9O6A1a/r+LGn6Y705PA6whHQ3T1XuB/PmpfNYf8Fw==", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "^25.5.0", - "babel-preset-current-node-syntax": "^0.1.2" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", - "dev": true - }, - "cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dev": true, - "requires": { - "cssom": "~0.3.6" - }, - "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - } - } - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true - }, - "diff-sequences": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", - "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", - "dev": true - }, - "execa": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz", - "integrity": "sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "p-finally": "^2.0.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, - "expect": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-25.5.0.tgz", - "integrity": "sha512-w7KAXo0+6qqZZhovCaBVPSIqQp7/UTcx4M9uKt2m6pd2VB1voyC8JizLRqeEqud3AAVP02g+hbErDu5gu64tlA==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "ansi-styles": "^4.0.0", - "jest-get-type": "^25.2.6", - "jest-matcher-utils": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-regex-util": "^25.2.6" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "dev": true, - "optional": true - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "import-local": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", - "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", - "dev": true, - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "dev": true - }, - "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "optional": true, - "requires": { - "is-docker": "^2.0.0" - } - }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", - "dev": true, - "requires": { - "@babel/core": "^7.7.5", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - } - }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", - "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - } - }, - "istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "jest-changed-files": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-25.5.0.tgz", - "integrity": "sha512-EOw9QEqapsDT7mKF162m8HFzRPbmP8qJQny6ldVOdOVBz3ACgPm/1nAn5fPQ/NDaYhX/AHkrGwwkCncpAVSXcw==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "execa": "^3.2.0", - "throat": "^5.0.0" - } - }, - "jest-cli": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-25.5.4.tgz", - "integrity": "sha512-rG8uJkIiOUpnREh1768/N3n27Cm+xPFkSNFO91tgg+8o2rXeVLStz+vkXkGr4UtzH6t1SNbjwoiswd7p4AhHTw==", - "dev": true, - "requires": { - "@jest/core": "^25.5.4", - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "import-local": "^3.0.2", - "is-ci": "^2.0.0", - "jest-config": "^25.5.4", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", - "prompts": "^2.0.1", - "realpath-native": "^2.0.0", - "yargs": "^15.3.1" - } - }, - "jest-config": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-25.5.4.tgz", - "integrity": "sha512-SZwR91SwcdK6bz7Gco8qL7YY2sx8tFJYzvg216DLihTWf+LKY/DoJXpM9nTzYakSyfblbqeU48p/p7Jzy05Atg==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/test-sequencer": "^25.5.4", - "@jest/types": "^25.5.0", - "babel-jest": "^25.5.1", - "chalk": "^3.0.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.1", - "graceful-fs": "^4.2.4", - "jest-environment-jsdom": "^25.5.0", - "jest-environment-node": "^25.5.0", - "jest-get-type": "^25.2.6", - "jest-jasmine2": "^25.5.4", - "jest-regex-util": "^25.2.6", - "jest-resolve": "^25.5.1", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", - "micromatch": "^4.0.2", - "pretty-format": "^25.5.0", - "realpath-native": "^2.0.0" - } - }, - "jest-diff": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", - "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "diff-sequences": "^25.2.6", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - } - }, - "jest-docblock": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-25.3.0.tgz", - "integrity": "sha512-aktF0kCar8+zxRHxQZwxMy70stc9R1mOmrLsT5VO3pIT0uzGRSDAXxSlz4NqQWpuLjPpuMhPRl7H+5FRsvIQAg==", - "dev": true, - "requires": { - "detect-newline": "^3.0.0" - } - }, - "jest-each": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-25.5.0.tgz", - "integrity": "sha512-QBogUxna3D8vtiItvn54xXde7+vuzqRrEeaw8r1s+1TG9eZLVJE5ZkKoSUlqFwRjnlaA4hyKGiu9OlkFIuKnjA==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "jest-get-type": "^25.2.6", - "jest-util": "^25.5.0", - "pretty-format": "^25.5.0" - } - }, - "jest-environment-jsdom": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.5.0.tgz", - "integrity": "sha512-7Jr02ydaq4jaWMZLY+Skn8wL5nVIYpWvmeatOHL3tOcV3Zw8sjnPpx+ZdeBfc457p8jCR9J6YCc+Lga0oIy62A==", - "dev": true, - "requires": { - "@jest/environment": "^25.5.0", - "@jest/fake-timers": "^25.5.0", - "@jest/types": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-util": "^25.5.0", - "jsdom": "^15.2.1" - } - }, - "jest-environment-node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-25.5.0.tgz", - "integrity": "sha512-iuxK6rQR2En9EID+2k+IBs5fCFd919gVVK5BeND82fYeLWPqvRcFNPKu9+gxTwfB5XwBGBvZ0HFQa+cHtIoslA==", - "dev": true, - "requires": { - "@jest/environment": "^25.5.0", - "@jest/fake-timers": "^25.5.0", - "@jest/types": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-util": "^25.5.0", - "semver": "^6.3.0" - } - }, - "jest-get-type": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", - "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", - "dev": true - }, - "jest-haste-map": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.5.1.tgz", - "integrity": "sha512-dddgh9UZjV7SCDQUrQ+5t9yy8iEgKc1AKqZR9YDww8xsVOtzPQSMVLDChc21+g29oTRexb9/B0bIlZL+sWmvAQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "@types/graceful-fs": "^4.1.2", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.1.2", - "graceful-fs": "^4.2.4", - "jest-serializer": "^25.5.0", - "jest-util": "^25.5.0", - "jest-worker": "^25.5.0", - "micromatch": "^4.0.2", - "sane": "^4.0.3", - "walker": "^1.0.7", - "which": "^2.0.2" - } - }, - "jest-jasmine2": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-25.5.4.tgz", - "integrity": "sha512-9acbWEfbmS8UpdcfqnDO+uBUgKa/9hcRh983IHdM+pKmJPL77G0sWAAK0V0kr5LK3a8cSBfkFSoncXwQlRZfkQ==", - "dev": true, - "requires": { - "@babel/traverse": "^7.1.0", - "@jest/environment": "^25.5.0", - "@jest/source-map": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "co": "^4.6.0", - "expect": "^25.5.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^25.5.0", - "jest-matcher-utils": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-runtime": "^25.5.4", - "jest-snapshot": "^25.5.1", - "jest-util": "^25.5.0", - "pretty-format": "^25.5.0", - "throat": "^5.0.0" - } - }, - "jest-leak-detector": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-25.5.0.tgz", - "integrity": "sha512-rV7JdLsanS8OkdDpZtgBf61L5xZ4NnYLBq72r6ldxahJWWczZjXawRsoHyXzibM5ed7C2QRjpp6ypgwGdKyoVA==", - "dev": true, - "requires": { - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - } - }, - "jest-matcher-utils": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz", - "integrity": "sha512-VWI269+9JS5cpndnpCwm7dy7JtGQT30UHfrnM3mXl22gHGt/b7NkjBqXfbhZ8V4B7ANUsjK18PlSBmG0YH7gjw==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "jest-diff": "^25.5.0", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - } - }, - "jest-message-util": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.5.0.tgz", - "integrity": "sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@jest/types": "^25.5.0", - "@types/stack-utils": "^1.0.1", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "micromatch": "^4.0.2", - "slash": "^3.0.0", - "stack-utils": "^1.0.1" - } - }, - "jest-mock": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.5.0.tgz", - "integrity": "sha512-eXWuTV8mKzp/ovHc5+3USJMYsTBhyQ+5A1Mak35dey/RG8GlM4YWVylZuGgVXinaW6tpvk/RSecmF37FKUlpXA==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0" - } - }, - "jest-regex-util": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", - "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", - "dev": true - }, - "jest-resolve": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-25.5.1.tgz", - "integrity": "sha512-Hc09hYch5aWdtejsUZhA+vSzcotf7fajSlPA6EZPE1RmPBAD39XtJhvHWFStid58iit4IPDLI/Da4cwdDmAHiQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "browser-resolve": "^1.11.3", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "jest-pnp-resolver": "^1.2.1", - "read-pkg-up": "^7.0.1", - "realpath-native": "^2.0.0", - "resolve": "^1.17.0", - "slash": "^3.0.0" - } - }, - "jest-resolve-dependencies": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-25.5.4.tgz", - "integrity": "sha512-yFmbPd+DAQjJQg88HveObcGBA32nqNZ02fjYmtL16t1xw9bAttSn5UGRRhzMHIQbsep7znWvAvnD4kDqOFM0Uw==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "jest-regex-util": "^25.2.6", - "jest-snapshot": "^25.5.1" - } - }, - "jest-runner": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-25.5.4.tgz", - "integrity": "sha512-V/2R7fKZo6blP8E9BL9vJ8aTU4TH2beuqGNxHbxi6t14XzTb+x90B3FRgdvuHm41GY8ch4xxvf0ATH4hdpjTqg==", - "dev": true, - "requires": { - "@jest/console": "^25.5.0", - "@jest/environment": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-config": "^25.5.4", - "jest-docblock": "^25.3.0", - "jest-haste-map": "^25.5.1", - "jest-jasmine2": "^25.5.4", - "jest-leak-detector": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-resolve": "^25.5.1", - "jest-runtime": "^25.5.4", - "jest-util": "^25.5.0", - "jest-worker": "^25.5.0", - "source-map-support": "^0.5.6", - "throat": "^5.0.0" - } - }, - "jest-runtime": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-25.5.4.tgz", - "integrity": "sha512-RWTt8LeWh3GvjYtASH2eezkc8AehVoWKK20udV6n3/gC87wlTbE1kIA+opCvNWyyPeBs6ptYsc6nyHUb1GlUVQ==", - "dev": true, - "requires": { - "@jest/console": "^25.5.0", - "@jest/environment": "^25.5.0", - "@jest/globals": "^25.5.2", - "@jest/source-map": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "@types/yargs": "^15.0.0", - "chalk": "^3.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.4", - "jest-config": "^25.5.4", - "jest-haste-map": "^25.5.1", - "jest-message-util": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-regex-util": "^25.2.6", - "jest-resolve": "^25.5.1", - "jest-snapshot": "^25.5.1", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", - "realpath-native": "^2.0.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0", - "yargs": "^15.3.1" - } - }, - "jest-serializer": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.5.0.tgz", - "integrity": "sha512-LxD8fY1lByomEPflwur9o4e2a5twSQ7TaVNLlFUuToIdoJuBt8tzHfCsZ42Ok6LkKXWzFWf3AGmheuLAA7LcCA==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.4" - } - }, - "jest-snapshot": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-25.5.1.tgz", - "integrity": "sha512-C02JE1TUe64p2v1auUJ2ze5vcuv32tkv9PyhEb318e8XOKF7MOyXdJ7kdjbvrp3ChPLU2usI7Rjxs97Dj5P0uQ==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0", - "@jest/types": "^25.5.0", - "@types/prettier": "^1.19.0", - "chalk": "^3.0.0", - "expect": "^25.5.0", - "graceful-fs": "^4.2.4", - "jest-diff": "^25.5.0", - "jest-get-type": "^25.2.6", - "jest-matcher-utils": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-resolve": "^25.5.1", - "make-dir": "^3.0.0", - "natural-compare": "^1.4.0", - "pretty-format": "^25.5.0", - "semver": "^6.3.0" - } - }, - "jest-util": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.5.0.tgz", - "integrity": "sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "make-dir": "^3.0.0" - } - }, - "jest-validate": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-25.5.0.tgz", - "integrity": "sha512-okUFKqhZIpo3jDdtUXUZ2LxGUZJIlfdYBvZb1aczzxrlyMlqdnnws9MOxezoLGhSaFc2XYaHNReNQfj5zPIWyQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "camelcase": "^5.3.1", - "chalk": "^3.0.0", - "jest-get-type": "^25.2.6", - "leven": "^3.1.0", - "pretty-format": "^25.5.0" - } - }, - "jest-watcher": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-25.5.0.tgz", - "integrity": "sha512-XrSfJnVASEl+5+bb51V0Q7WQx65dTSk7NL4yDdVjPnRNpM0hG+ncFmDYJo9O8jaSRcAitVbuVawyXCRoxGrT5Q==", - "dev": true, - "requires": { - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "ansi-escapes": "^4.2.1", - "chalk": "^3.0.0", - "jest-util": "^25.5.0", - "string-length": "^3.1.0" - } - }, - "jest-worker": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.5.0.tgz", - "integrity": "sha512-/dsSmUkIy5EBGfv/IjjqmFxrNAUpBERfGs1oHROyD7yxjG/w+t0GOJDX8O1k32ySmd7+a5IhnJU2qQFcJ4n1vw==", - "dev": true, - "requires": { - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - } - }, - "jsdom": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", - "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "acorn": "^7.1.0", - "acorn-globals": "^4.3.2", - "array-equal": "^1.0.0", - "cssom": "^0.4.1", - "cssstyle": "^2.0.0", - "data-urls": "^1.1.0", - "domexception": "^1.0.1", - "escodegen": "^1.11.1", - "html-encoding-sniffer": "^1.0.2", - "nwsapi": "^2.2.0", - "parse5": "5.1.0", - "pn": "^1.1.0", - "request": "^2.88.0", - "request-promise-native": "^1.0.7", - "saxes": "^3.1.9", - "symbol-tree": "^3.2.2", - "tough-cookie": "^3.0.1", - "w3c-hr-time": "^1.0.1", - "w3c-xmlserializer": "^1.1.2", - "webidl-conversions": "^4.0.2", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^7.0.0", - "ws": "^7.0.0", - "xml-name-validator": "^3.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node-notifier": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-6.0.0.tgz", - "integrity": "sha512-SVfQ/wMw+DesunOm5cKqr6yDcvUTDl/yc97ybGHMrteNEY6oekXpNpS3lZwgLlwz0FLgHoiW28ZpmBHUDg37cw==", - "dev": true, - "optional": true, - "requires": { - "growly": "^1.3.0", - "is-wsl": "^2.1.1", - "semver": "^6.3.0", - "shellwords": "^0.1.1", - "which": "^1.3.1" - }, - "dependencies": { - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "optional": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "p-each-series": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.1.0.tgz", - "integrity": "sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ==", - "dev": true - }, - "p-finally": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", - "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", - "dev": true - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "parse-json": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", - "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "parse5": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", - "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - }, - "pretty-format": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", - "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^16.12.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - } - } - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - } - }, - "realpath-native": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz", - "integrity": "sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==", - "dev": true - }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "requires": { - "resolve-from": "^5.0.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "string-length": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz", - "integrity": "sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA==", - "dev": true, - "requires": { - "astral-regex": "^1.0.0", - "strip-ansi": "^5.2.0" - }, - "dependencies": { - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - } - } - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, - "throat": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", - "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tough-cookie": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", - "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", - "dev": true, - "requires": { - "ip-regex": "^2.1.0", - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - }, - "whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "ws": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", - "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==", - "dev": true - }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "jest-changed-files": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-24.9.0.tgz", - "integrity": "sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg==", - "dev": true, - "requires": { - "@jest/types": "^24.9.0", - "execa": "^1.0.0", - "throat": "^4.0.0" - } - }, - "jest-config": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-24.9.0.tgz", - "integrity": "sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/test-sequencer": "^24.9.0", - "@jest/types": "^24.9.0", - "babel-jest": "^24.9.0", - "chalk": "^2.0.1", - "glob": "^7.1.1", - "jest-environment-jsdom": "^24.9.0", - "jest-environment-node": "^24.9.0", - "jest-get-type": "^24.9.0", - "jest-jasmine2": "^24.9.0", - "jest-regex-util": "^24.3.0", - "jest-resolve": "^24.9.0", - "jest-util": "^24.9.0", - "jest-validate": "^24.9.0", - "micromatch": "^3.1.10", - "pretty-format": "^24.9.0", - "realpath-native": "^1.1.0" - }, - "dependencies": { - "@jest/test-sequencer": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz", - "integrity": "sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A==", - "dev": true, - "requires": { - "@jest/test-result": "^24.9.0", - "jest-haste-map": "^24.9.0", - "jest-runner": "^24.9.0", - "jest-runtime": "^24.9.0" - } - } - } - }, - "jest-dev-server": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/jest-dev-server/-/jest-dev-server-4.4.0.tgz", - "integrity": "sha512-STEHJ3iPSC8HbrQ3TME0ozGX2KT28lbT4XopPxUm2WimsX3fcB3YOptRh12YphQisMhfqNSNTZUmWyT3HEXS2A==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "cwd": "^0.10.0", - "find-process": "^1.4.3", - "prompts": "^2.3.0", - "spawnd": "^4.4.0", - "tree-kill": "^1.2.2", - "wait-on": "^3.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-diff": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz", - "integrity": "sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "diff-sequences": "^24.9.0", - "jest-get-type": "^24.9.0", - "pretty-format": "^24.9.0" - } - }, - "jest-docblock": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-24.9.0.tgz", - "integrity": "sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA==", - "dev": true, - "requires": { - "detect-newline": "^2.1.0" - } - }, - "jest-each": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-24.9.0.tgz", - "integrity": "sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog==", - "dev": true, - "requires": { - "@jest/types": "^24.9.0", - "chalk": "^2.0.1", - "jest-get-type": "^24.9.0", - "jest-util": "^24.9.0", - "pretty-format": "^24.9.0" - } - }, - "jest-environment-jsdom": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz", - "integrity": "sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA==", - "dev": true, - "requires": { - "@jest/environment": "^24.9.0", - "@jest/fake-timers": "^24.9.0", - "@jest/types": "^24.9.0", - "jest-mock": "^24.9.0", - "jest-util": "^24.9.0", - "jsdom": "^11.5.1" - } - }, - "jest-environment-node": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-24.9.0.tgz", - "integrity": "sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA==", - "dev": true, - "requires": { - "@jest/environment": "^24.9.0", - "@jest/fake-timers": "^24.9.0", - "@jest/types": "^24.9.0", - "jest-mock": "^24.9.0", - "jest-util": "^24.9.0" - } - }, - "jest-environment-puppeteer": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/jest-environment-puppeteer/-/jest-environment-puppeteer-4.4.0.tgz", - "integrity": "sha512-iV8S8+6qkdTM6OBR/M9gKywEk8GDSOe05hspCs5D8qKSwtmlUfdtHfB4cakdc68lC6YfK3AUsLirpfgodCHjzQ==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "cwd": "^0.10.0", - "jest-dev-server": "^4.4.0", - "merge-deep": "^3.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-get-type": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", - "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==", - "dev": true - }, - "jest-haste-map": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-24.9.0.tgz", - "integrity": "sha512-kfVFmsuWui2Sj1Rp1AJ4D9HqJwE4uwTlS/vO+eRUaMmd54BFpli2XhMQnPC2k4cHFVbB2Q2C+jtI1AGLgEnCjQ==", - "dev": true, - "requires": { - "@jest/types": "^24.9.0", - "anymatch": "^2.0.0", - "fb-watchman": "^2.0.0", - "fsevents": "^1.2.7", - "graceful-fs": "^4.1.15", - "invariant": "^2.2.4", - "jest-serializer": "^24.9.0", - "jest-util": "^24.9.0", - "jest-worker": "^24.9.0", - "micromatch": "^3.1.10", - "sane": "^4.0.3", - "walker": "^1.0.7" - } - }, - "jest-jasmine2": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-24.9.0.tgz", - "integrity": "sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw==", - "dev": true, - "requires": { - "@babel/traverse": "^7.1.0", - "@jest/environment": "^24.9.0", - "@jest/test-result": "^24.9.0", - "@jest/types": "^24.9.0", - "chalk": "^2.0.1", - "co": "^4.6.0", - "expect": "^24.9.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^24.9.0", - "jest-matcher-utils": "^24.9.0", - "jest-message-util": "^24.9.0", - "jest-runtime": "^24.9.0", - "jest-snapshot": "^24.9.0", - "jest-util": "^24.9.0", - "pretty-format": "^24.9.0", - "throat": "^4.0.0" - } - }, - "jest-leak-detector": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-24.9.0.tgz", - "integrity": "sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA==", - "dev": true, - "requires": { - "jest-get-type": "^24.9.0", - "pretty-format": "^24.9.0" - } - }, - "jest-matcher-utils": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz", - "integrity": "sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "jest-diff": "^24.9.0", - "jest-get-type": "^24.9.0", - "pretty-format": "^24.9.0" - } - }, - "jest-message-util": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-24.9.0.tgz", - "integrity": "sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@jest/test-result": "^24.9.0", - "@jest/types": "^24.9.0", - "@types/stack-utils": "^1.0.1", - "chalk": "^2.0.1", - "micromatch": "^3.1.10", - "slash": "^2.0.0", - "stack-utils": "^1.0.1" - } - }, - "jest-mock": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-24.9.0.tgz", - "integrity": "sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w==", - "dev": true, - "requires": { - "@jest/types": "^24.9.0" - } - }, - "jest-pnp-resolver": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", - "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true - }, - "jest-puppeteer": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/jest-puppeteer/-/jest-puppeteer-4.4.0.tgz", - "integrity": "sha512-ZaiCTlPZ07B9HW0erAWNX6cyzBqbXMM7d2ugai4epBDKpKvRDpItlRQC6XjERoJELKZsPziFGS0OhhUvTvQAXA==", - "dev": true, - "requires": { - "expect-puppeteer": "^4.4.0", - "jest-environment-puppeteer": "^4.4.0" - } - }, - "jest-regex-util": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz", - "integrity": "sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA==", - "dev": true - }, - "jest-resolve": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-24.9.0.tgz", - "integrity": "sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ==", - "dev": true, - "requires": { - "@jest/types": "^24.9.0", - "browser-resolve": "^1.11.3", - "chalk": "^2.0.1", - "jest-pnp-resolver": "^1.2.1", - "realpath-native": "^1.1.0" - } - }, - "jest-resolve-dependencies": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-24.9.0.tgz", - "integrity": "sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g==", - "dev": true, - "requires": { - "@jest/types": "^24.9.0", - "jest-regex-util": "^24.3.0", - "jest-snapshot": "^24.9.0" - } - }, - "jest-runner": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-24.9.0.tgz", - "integrity": "sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg==", - "dev": true, - "requires": { - "@jest/console": "^24.7.1", - "@jest/environment": "^24.9.0", - "@jest/test-result": "^24.9.0", - "@jest/types": "^24.9.0", - "chalk": "^2.4.2", - "exit": "^0.1.2", - "graceful-fs": "^4.1.15", - "jest-config": "^24.9.0", - "jest-docblock": "^24.3.0", - "jest-haste-map": "^24.9.0", - "jest-jasmine2": "^24.9.0", - "jest-leak-detector": "^24.9.0", - "jest-message-util": "^24.9.0", - "jest-resolve": "^24.9.0", - "jest-runtime": "^24.9.0", - "jest-util": "^24.9.0", - "jest-worker": "^24.6.0", - "source-map-support": "^0.5.6", - "throat": "^4.0.0" - } - }, - "jest-runtime": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-24.9.0.tgz", - "integrity": "sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw==", - "dev": true, - "requires": { - "@jest/console": "^24.7.1", - "@jest/environment": "^24.9.0", - "@jest/source-map": "^24.3.0", - "@jest/transform": "^24.9.0", - "@jest/types": "^24.9.0", - "@types/yargs": "^13.0.0", - "chalk": "^2.0.1", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.1.15", - "jest-config": "^24.9.0", - "jest-haste-map": "^24.9.0", - "jest-message-util": "^24.9.0", - "jest-mock": "^24.9.0", - "jest-regex-util": "^24.3.0", - "jest-resolve": "^24.9.0", - "jest-snapshot": "^24.9.0", - "jest-util": "^24.9.0", - "jest-validate": "^24.9.0", - "realpath-native": "^1.1.0", - "slash": "^2.0.0", - "strip-bom": "^3.0.0", - "yargs": "^13.3.0" - } - }, - "jest-serializer": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-24.9.0.tgz", - "integrity": "sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ==", - "dev": true - }, - "jest-snapshot": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-24.9.0.tgz", - "integrity": "sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0", - "@jest/types": "^24.9.0", - "chalk": "^2.0.1", - "expect": "^24.9.0", - "jest-diff": "^24.9.0", - "jest-get-type": "^24.9.0", - "jest-matcher-utils": "^24.9.0", - "jest-message-util": "^24.9.0", - "jest-resolve": "^24.9.0", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "pretty-format": "^24.9.0", - "semver": "^6.2.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "jest-util": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-24.9.0.tgz", - "integrity": "sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg==", - "dev": true, - "requires": { - "@jest/console": "^24.9.0", - "@jest/fake-timers": "^24.9.0", - "@jest/source-map": "^24.9.0", - "@jest/test-result": "^24.9.0", - "@jest/types": "^24.9.0", - "callsites": "^3.0.0", - "chalk": "^2.0.1", - "graceful-fs": "^4.1.15", - "is-ci": "^2.0.0", - "mkdirp": "^0.5.1", - "slash": "^2.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "jest-validate": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-24.9.0.tgz", - "integrity": "sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ==", - "dev": true, - "requires": { - "@jest/types": "^24.9.0", - "camelcase": "^5.3.1", - "chalk": "^2.0.1", - "jest-get-type": "^24.9.0", - "leven": "^3.1.0", - "pretty-format": "^24.9.0" - } - }, - "jest-watcher": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-24.9.0.tgz", - "integrity": "sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw==", - "dev": true, - "requires": { - "@jest/test-result": "^24.9.0", - "@jest/types": "^24.9.0", - "@types/yargs": "^13.0.0", - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.1", - "jest-util": "^24.9.0", - "string-length": "^2.0.0" - }, - "dependencies": { - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - } - } - }, - "jest-worker": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz", - "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==", - "dev": true, - "requires": { - "merge-stream": "^2.0.0", - "supports-color": "^6.1.0" - }, - "dependencies": { - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "js-yaml": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", - "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "jsdoctypeparser": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/jsdoctypeparser/-/jsdoctypeparser-5.1.1.tgz", - "integrity": "sha512-APGygIJrT5bbz5lsVt8vyLJC0miEbQf/z9ZBfTr4RYvdia8AhWMRlYgivvwHG5zKD/VW3d6qpChCy64hpQET3A==" - }, - "jsdom": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", - "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "acorn": "^5.5.3", - "acorn-globals": "^4.1.0", - "array-equal": "^1.0.0", - "cssom": ">= 0.3.2 < 0.4.0", - "cssstyle": "^1.0.0", - "data-urls": "^1.0.0", - "domexception": "^1.0.1", - "escodegen": "^1.9.1", - "html-encoding-sniffer": "^1.0.2", - "left-pad": "^1.3.0", - "nwsapi": "^2.0.7", - "parse5": "4.0.0", - "pn": "^1.1.0", - "request": "^2.87.0", - "request-promise-native": "^1.0.5", - "sax": "^1.2.4", - "symbol-tree": "^3.2.2", - "tough-cookie": "^2.3.4", - "w3c-hr-time": "^1.0.1", - "webidl-conversions": "^4.0.2", - "whatwg-encoding": "^1.0.3", - "whatwg-mimetype": "^2.1.0", - "whatwg-url": "^6.4.1", - "ws": "^5.2.0", - "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "acorn": { - "version": "5.7.4", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", - "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", - "dev": true - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - } - } - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json5": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", - "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "jsx-ast-utils": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz", - "integrity": "sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w==", - "requires": { - "array-includes": "^3.1.1", - "object.assign": "^4.1.0" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true - }, - "language-subtag-registry": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.20.tgz", - "integrity": "sha512-KPMwROklF4tEx283Xw0pNKtfTj1gZ4UByp4EsIFWLgBavJltF4TiYPc39k06zSTsLzxTVXXDSpbwaQXaFB4Qeg==" - }, - "language-tags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", - "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", - "requires": { - "language-subtag-registry": "~0.3.2" - } - }, - "latest-version": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", - "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", - "requires": { - "package-json": "^4.0.0" - } - }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "dev": true - }, - "left-pad": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", - "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==", - "dev": true - }, - "leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true - }, - "levenary": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/levenary/-/levenary-1.1.1.tgz", - "integrity": "sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==", - "dev": true, - "requires": { - "leven": "^3.1.0" - } - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", - "dev": true - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" - }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true - }, - "lodash.unescape": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", - "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=" - }, - "lolex": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", - "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", - "dev": true, - "requires": { - "tmpl": "1.0.x" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "memize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/memize/-/memize-1.1.0.tgz", - "integrity": "sha512-K4FcPETOMTwe7KL2LK0orMhpOmWD2wRGwWWpbZy0fyArwsyIKR8YJVz8+efBAh3BO4zPqlSICu4vsLTRRqtFAg==", - "dev": true - }, - "merge-deep": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.2.tgz", - "integrity": "sha512-T7qC8kg4Zoti1cFd8Cr0M+qaZfOwjlPDEdZIIPPB2JZctjaPM4fX+i7HOId69tAti2fvO6X5ldfYUONDODsrkA==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "clone-deep": "^0.2.4", - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "mime": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", - "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" - }, - "mime-db": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", - "dev": true - }, - "mime-types": { - "version": "2.1.27", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", - "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", - "dev": true, - "requires": { - "mime-db": "1.44.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mixin-object": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", - "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=", - "dev": true, - "requires": { - "for-in": "^0.1.3", - "is-extendable": "^0.1.1" - }, - "dependencies": { - "for-in": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", - "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=", - "dev": true - } - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, - "nan": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", - "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", - "optional": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "ndb": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ndb/-/ndb-1.1.5.tgz", - "integrity": "sha512-YiK+1wRe9SnZIyZ3kLDmogV2/Z0+8uPgHNbk3ExXzJg6IEFSenfskkn82v5ajPUJKkeEpsaK+ae1eT1be8Hllw==", - "requires": { - "carlo": "^0.9.46", - "chokidar": "^3.0.2", - "debug": "^4.1.1", - "isbinaryfile": "^3.0.3", - "mime": "^2.4.4", - "node-pty": "^0.9.0-beta18", - "opn": "^5.5.0", - "update-notifier": "^2.5.0", - "which": "^1.3.1", - "ws": "^6.2.1", - "xterm": "^3.14.5" - }, - "dependencies": { - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "binary-extensions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==" - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { - "fill-range": "^7.0.1" - } - }, - "chokidar": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", - "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.1.2", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.4.0" - } - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "requires": { - "ms": "2.1.2" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "optional": true - }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "requires": { - "is-glob": "^4.0.1" - } - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "readdirp": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", - "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", - "requires": { - "picomatch": "^2.2.1" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { - "is-number": "^7.0.0" - } - }, - "ws": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", - "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", - "requires": { - "async-limiter": "~1.0.0" - } - } - } - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node-fetch": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", - "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", - "dev": true, - "requires": { - "encoding": "^0.1.11", - "is-stream": "^1.0.1" - } - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true - }, - "node-modules-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", - "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", - "dev": true - }, - "node-notifier": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.3.tgz", - "integrity": "sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q==", - "dev": true, - "requires": { - "growly": "^1.3.0", - "is-wsl": "^1.1.0", - "semver": "^5.5.0", - "shellwords": "^0.1.1", - "which": "^1.3.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "node-pty": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-0.9.0.tgz", - "integrity": "sha512-MBnCQl83FTYOu7B4xWw10AW77AAh7ThCE1VXEv+JeWj8mSpGo+0bwgsV+b23ljBFwEM9OmsOv3kM27iUPPm84g==", - "optional": true, - "requires": { - "nan": "^2.14.0" - } - }, - "node-releases": { - "version": "1.1.61", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.61.tgz", - "integrity": "sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g==", - "dev": true - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "requires": { - "path-key": "^2.0.0" - } - }, - "nwsapi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", - "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-inspect": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", - "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, - "object.assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", - "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.0", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - } - }, - "object.entries-ponyfill": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.entries-ponyfill/-/object.entries-ponyfill-1.0.1.tgz", - "integrity": "sha1-Kavfd8v70mVm3RqiTp2I9lQz0lY=" - }, - "object.getownpropertydescriptors": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", - "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", - "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-regex": "^1.1.0", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "opn": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", - "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", - "requires": { - "is-wsl": "^1.1.0" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "p-each-series": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-1.0.0.tgz", - "integrity": "sha1-kw89Et0fUOdDRFeiLNbwSsatf3E=", - "dev": true, - "requires": { - "p-reduce": "^1.0.0" - } - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-queue": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-2.4.2.tgz", - "integrity": "sha512-n8/y+yDJwBjoLQe1GSJbbaYQLTI7QHNZI2+rpmCDbe++WLf9HC3gf6iqj5yfPAV71W4UF3ql5W1+UBPXoXTxng==", - "dev": true - }, - "p-reduce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", - "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=", - "dev": true - }, - "p-retry": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.2.0.tgz", - "integrity": "sha512-jPH38/MRh263KKcq0wBNOGFJbm+U6784RilTmHjB/HM9kH9V8WlCpVUcdOmip9cjXOh6MxZ5yk1z2SjDUJfWmA==", - "dev": true, - "requires": { - "@types/retry": "^0.12.0", - "retry": "^0.12.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "package-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", - "requires": { - "got": "^6.7.1", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - } - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", - "dev": true - }, - "parse5": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", - "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true, - "optional": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "pirates": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", - "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", - "dev": true, - "requires": { - "node-modules-regexp": "^1.0.0" - } - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - }, - "pn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", - "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", - "dev": true - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" - }, - "prettier": { - "version": "npm:prettier@1.19.1", - "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-1.19.1.tgz", - "integrity": "sha512-mqAC2r1NDmRjG+z3KCJ/i61tycKlmADIjxnDhQab+KBxSAGbF/W7/zwB2guy/ypIeKrrftNsIYkNZZQKf3vJcg==" - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "requires": { - "fast-diff": "^1.1.2" - } - }, - "pretty-format": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", - "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", - "dev": true, - "requires": { - "@jest/types": "^24.9.0", - "ansi-regex": "^4.0.0", - "ansi-styles": "^3.2.0", - "react-is": "^16.8.4" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - } - } - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" - }, - "prompts": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.3.2.tgz", - "integrity": "sha512-Q06uKs2CkNYVID0VqwfAl9mipo99zkBv/n2JtWY89Yxa3ZabWSrs0e2KTudKVa3peLUvYXMefDqIleLPVUBZMA==", - "dev": true, - "requires": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.4" - } - }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", - "dev": true - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "puppeteer": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-2.1.1.tgz", - "integrity": "sha512-LWzaDVQkk1EPiuYeTOj+CZRIjda4k2s5w4MK4xoH2+kgWV/SDlkYHmxatDdtYrciHUKSXTsGgPgPP8ILVdBsxg==", - "dev": true, - "requires": { - "@types/mime-types": "^2.1.0", - "debug": "^4.1.0", - "extract-zip": "^1.6.6", - "https-proxy-agent": "^4.0.0", - "mime": "^2.0.3", - "mime-types": "^2.1.25", - "progress": "^2.0.1", - "proxy-from-env": "^1.0.0", - "rimraf": "^2.6.1", - "ws": "^6.1.0" - }, - "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "ws": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", - "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0" - } - } - } - }, - "puppeteer-core": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-1.12.2.tgz", - "integrity": "sha512-M+atMV5e+MwJdR+OwQVZ1xqAIwh3Ou4nUxNuf334GwpcLG+LDj5BwIph4J9y8YAViByRtWGL+uF8qX2Ggzb+Fg==", - "requires": { - "debug": "^4.1.0", - "extract-zip": "^1.6.6", - "https-proxy-agent": "^2.2.1", - "mime": "^2.0.3", - "progress": "^2.0.1", - "proxy-from-env": "^1.0.0", - "rimraf": "^2.6.1", - "ws": "^6.1.0" - }, - "dependencies": { - "agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", - "requires": { - "es6-promisify": "^5.0.0" - } - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "requires": { - "ms": "2.1.2" - } - }, - "https-proxy-agent": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", - "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", - "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "ws": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", - "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", - "requires": { - "async-limiter": "~1.0.0" - } - } - } - }, - "qs": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", - "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==", - "dev": true - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - } - } - }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true - }, - "react-native-url-polyfill": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-1.2.0.tgz", - "integrity": "sha512-hpLZ8RyS3oGVyTOe/HjoqVoCOSkeJvrCoEB3bJsY7t9uh7kpQDV6kgvdlECEafYpxe3RzMrKLVcmWRbPU7CuAw==", - "dev": true, - "requires": { - "whatwg-url-without-unicode": "8.0.0-3" - } - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", - "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", - "dev": true, - "requires": { - "find-up": "^3.0.0", - "read-pkg": "^3.0.0" - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "optional": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - }, - "realpath-native": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz", - "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", - "dev": true, - "requires": { - "util.promisify": "^1.0.0" - } - }, - "regenerate": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.1.tgz", - "integrity": "sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A==", - "dev": true - }, - "regenerate-unicode-properties": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz", - "integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==", - "dev": true, - "requires": { - "regenerate": "^1.4.0" - } - }, - "regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" - }, - "regenerator-transform": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", - "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.8.4" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "regexp.prototype.flags": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", - "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", - "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-regex": "^1.1.0", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true - }, - "regexpu-core": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz", - "integrity": "sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==", - "dev": true, - "requires": { - "regenerate": "^1.4.0", - "regenerate-unicode-properties": "^8.2.0", - "regjsgen": "^0.5.1", - "regjsparser": "^0.6.4", - "unicode-match-property-ecmascript": "^1.0.4", - "unicode-match-property-value-ecmascript": "^1.2.0" - } - }, - "regextras": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.6.1.tgz", - "integrity": "sha512-EzIHww9xV2Kpqx+corS/I7OBmf2rZ0pKKJPsw5Dc+l6Zq1TslDmtRIP9maVn3UH+72MIXmn8zzDgP07ihQogUA==" - }, - "registry-auth-token": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", - "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", - "requires": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" - } - }, - "registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", - "requires": { - "rc": "^1.0.1" - } - }, - "regjsgen": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", - "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", - "dev": true - }, - "regjsparser": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.4.tgz", - "integrity": "sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true - } - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - } - } - }, - "request-promise-core": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", - "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", - "dev": true, - "requires": { - "lodash": "^4.17.19" - } - }, - "request-promise-native": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", - "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", - "dev": true, - "requires": { - "request-promise-core": "1.1.4", - "stealthy-require": "^1.1.1", - "tough-cookie": "^2.3.3" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "requireindex": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", - "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==" - }, - "resolve": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", - "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - } - } - }, - "resolve-dir": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", - "integrity": "sha1-shklmlYC+sXFxJatiUpujMQwJh4=", - "dev": true, - "requires": { - "expand-tilde": "^1.2.2", - "global-modules": "^0.2.3" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", - "dev": true - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "requires": { - "glob": "^7.1.3" - } - }, - "rsvp": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", - "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", - "dev": true - }, - "run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true - }, - "rx": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", - "integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=", - "dev": true - }, - "rxjs": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", - "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sane": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", - "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", - "dev": true, - "requires": { - "@cnakazawa/watch": "^1.0.3", - "anymatch": "^2.0.0", - "capture-exit": "^2.0.0", - "exec-sh": "^0.3.2", - "execa": "^1.0.0", - "fb-watchman": "^2.0.0", - "micromatch": "^3.1.4", - "minimist": "^1.1.1", - "walker": "~1.0.5" - } - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "saxes": { - "version": "3.1.11", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", - "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", - "dev": true, - "requires": { - "xmlchars": "^2.1.1" - } - }, - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" - }, - "semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", - "requires": { - "semver": "^5.0.3" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - } - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "shallow-clone": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", - "integrity": "sha1-WQnodLp3EG1zrEFM/sH/yofZcGA=", - "dev": true, - "requires": { - "is-extendable": "^0.1.1", - "kind-of": "^2.0.1", - "lazy-cache": "^0.2.3", - "mixin-object": "^2.0.1" - }, - "dependencies": { - "kind-of": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", - "integrity": "sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU=", - "dev": true, - "requires": { - "is-buffer": "^1.0.2" - } - }, - "lazy-cache": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", - "integrity": "sha1-f+3fLctu23fRHvHRF6tf/fCrG2U=", - "dev": true - } - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" - }, - "shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true - }, - "side-channel": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.3.tgz", - "integrity": "sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==", - "requires": { - "es-abstract": "^1.18.0-next.0", - "object-inspect": "^1.8.0" - } - }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" - }, - "sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - }, - "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - } - } - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - }, - "source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "dev": true, - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "spawnd": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/spawnd/-/spawnd-4.4.0.tgz", - "integrity": "sha512-jLPOfB6QOEgMOQY15Z6+lwZEhH3F5ncXxIaZ7WHPIapwNNLyjrs61okj3VJ3K6tmP5TZ6cO0VAu9rEY4MD4YQg==", - "dev": true, - "requires": { - "exit": "^0.1.2", - "signal-exit": "^3.0.2", - "tree-kill": "^1.2.2", - "wait-port": "^0.2.7" - } - }, - "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz", - "integrity": "sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==", - "dev": true - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", - "dev": true - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "stack-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", - "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", - "dev": true - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", - "dev": true - }, - "string-length": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", - "integrity": "sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=", - "dev": true, - "requires": { - "astral-regex": "^1.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, - "string.prototype.matchall": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz", - "integrity": "sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg==", - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0", - "has-symbols": "^1.0.1", - "internal-slot": "^1.0.2", - "regexp.prototype.flags": "^1.3.0", - "side-channel": "^1.0.2" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", - "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-regex": "^1.1.0", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "string.prototype.trimend": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", - "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", - "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-regex": "^1.1.0", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "string.prototype.trimstart": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", - "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", - "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-regex": "^1.1.0", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - } - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" - }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true - }, - "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", - "dev": true, - "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - }, - "dependencies": { - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - } - } - }, - "tannin": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tannin/-/tannin-1.2.0.tgz", - "integrity": "sha512-U7GgX/RcSeUETbV7gYgoz8PD7Ni4y95pgIP/Z6ayI3CfhSujwKEBlGFTCRN+Aqnuyf4AN2yHL+L8x+TCGjb9uA==", - "dev": true, - "requires": { - "@tannin/plural-forms": "^1.1.0" - } - }, - "term-size": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", - "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", - "requires": { - "execa": "^0.7.0" - }, - "dependencies": { - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" - } - } - }, - "test-exclude": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", - "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", - "dev": true, - "requires": { - "glob": "^7.1.3", - "minimatch": "^3.0.4", - "read-pkg-up": "^4.0.0", - "require-main-filename": "^2.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "throat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", - "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true - }, - "tslib": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "unicode-canonical-property-names-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", - "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", - "dev": true - }, - "unicode-match-property-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", - "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", - "dev": true, - "requires": { - "unicode-canonical-property-names-ecmascript": "^1.0.4", - "unicode-property-aliases-ecmascript": "^1.0.4" - } - }, - "unicode-match-property-value-ecmascript": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz", - "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==", - "dev": true - }, - "unicode-property-aliases-ecmascript": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", - "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==", - "dev": true - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "unique-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", - "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", - "requires": { - "crypto-random-string": "^1.0.0" - } - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - } - } - }, - "unzip-response": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=" - }, - "upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true, - "optional": true - }, - "update-notifier": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", - "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", - "requires": { - "boxen": "^1.2.1", - "chalk": "^2.0.1", - "configstore": "^3.0.0", - "import-lazy": "^2.1.0", - "is-ci": "^1.0.10", - "is-installed-globally": "^0.1.0", - "is-npm": "^1.0.0", - "latest-version": "^3.0.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^3.0.0" - }, - "dependencies": { - "ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==" - }, - "is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", - "requires": { - "ci-info": "^1.5.0" - } - } - } - }, - "uri-js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", - "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", - "requires": { - "prepend-http": "^1.0.1" - } - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "util.promisify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", - "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.2", - "has-symbols": "^1.0.1", - "object.getownpropertydescriptors": "^2.1.0" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", - "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-regex": "^1.1.0", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - }, - "v8-compile-cache": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", - "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", - "dev": true - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "dev": true, - "requires": { - "browser-process-hrtime": "^1.0.0" - } - }, - "w3c-xmlserializer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", - "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", - "dev": true, - "requires": { - "domexception": "^1.0.1", - "webidl-conversions": "^4.0.2", - "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - } - } - }, - "wait-on": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-3.3.0.tgz", - "integrity": "sha512-97dEuUapx4+Y12aknWZn7D25kkjMk16PbWoYzpSdA8bYpVfS6hpl2a2pOWZ3c+Tyt3/i4/pglyZctG3J4V1hWQ==", - "dev": true, - "requires": { - "@hapi/joi": "^15.0.3", - "core-js": "^2.6.5", - "minimist": "^1.2.0", - "request": "^2.88.0", - "rx": "^4.1.0" - } - }, - "wait-port": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-0.2.9.tgz", - "integrity": "sha512-hQ/cVKsNqGZ/UbZB/oakOGFqic00YAMM5/PEj3Bt4vKarv2jWIWzDbqlwT94qMs/exAQAsvMOq99sZblV92zxQ==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "commander": "^3.0.2", - "debug": "^4.1.1" - }, - "dependencies": { - "commander": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", - "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", - "dev": true - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", - "dev": true, - "requires": { - "makeerror": "1.0.x" - } - }, - "webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "dev": true - }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dev": true, - "requires": { - "iconv-lite": "0.4.24" - }, - "dependencies": { - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - } - } - }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true - }, - "whatwg-url": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", - "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - }, - "dependencies": { - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - } - } - }, - "whatwg-url-without-unicode": { - "version": "8.0.0-3", - "resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz", - "integrity": "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==", - "dev": true, - "requires": { - "buffer": "^5.4.3", - "punycode": "^2.1.1", - "webidl-conversions": "^5.0.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "widest-line": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", - "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", - "requires": { - "string-width": "^2.1.1" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - }, - "dependencies": { - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, - "write-file-atomic": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.1.tgz", - "integrity": "sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==", - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "ws": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", - "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0" - } - }, - "xdg-basedir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=" - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, - "xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true - }, - "xterm": { - "version": "3.14.5", - "resolved": "https://registry.npmjs.org/xterm/-/xterm-3.14.5.tgz", - "integrity": "sha512-DVmQ8jlEtL+WbBKUZuMxHMBgK/yeIZwkXB81bH+MGaKKnJGYwA+770hzhXPfwEIokK9On9YIFPRleVp/5G7z9g==" - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - }, - "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - }, - "dependencies": { - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - } - } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", - "requires": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - } - } - }, - "@woocommerce/e2e-utils": { - "version": "file:tests/e2e/utils", - "dev": true, - "requires": { - "@wordpress/deprecated": "^2.10.0", - "@wordpress/e2e-test-utils": "^4.6.0", - "config": "3.3.3", - "faker": "^5.1.0", - "fishery": "^1.2.0" - }, - "dependencies": { - "@babel/runtime": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", - "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "@tannin/compile": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@tannin/compile/-/compile-1.1.0.tgz", - "integrity": "sha512-n8m9eNDfoNZoxdvWiTfW/hSPhehzLJ3zW7f8E7oT6mCROoMNWCB4TYtv041+2FMAxweiE0j7i1jubQU4MEC/Gg==", - "dev": true, - "requires": { - "@tannin/evaluate": "^1.2.0", - "@tannin/postfix": "^1.1.0" - } - }, - "@tannin/evaluate": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@tannin/evaluate/-/evaluate-1.2.0.tgz", - "integrity": "sha512-3ioXvNowbO/wSrxsDG5DKIMxC81P0QrQTYai8zFNY+umuoHWRPbQ/TuuDEOju9E+jQDXmj6yI5GyejNuh8I+eg==", - "dev": true - }, - "@tannin/plural-forms": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@tannin/plural-forms/-/plural-forms-1.1.0.tgz", - "integrity": "sha512-xl9R2mDZO/qiHam1AgMnAES6IKIg7OBhcXqy6eDsRCdXuxAFPcjrej9HMjyCLE0DJ/8cHf0i5OQTstuBRhpbHw==", - "dev": true, - "requires": { - "@tannin/compile": "^1.1.0" - } - }, - "@tannin/postfix": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@tannin/postfix/-/postfix-1.1.0.tgz", - "integrity": "sha512-oocsqY7g0cR+Gur5jRQLSrX2OtpMLMse1I10JQBm8CdGMrDkh1Mg2gjsiquMHRtBs4Qwu5wgEp5GgIYHk4SNPw==", - "dev": true - }, - "@wordpress/e2e-test-utils": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils/-/e2e-test-utils-4.6.0.tgz", - "integrity": "sha512-oqnFEOuWkUFwzSVGeKZOfs9YhWVyCKdsOtJKnXd6Vv5Q1quq2fmbDp6HL+dIUI2DlJZISUmOWG4B37mMVA0DLg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.9.2", - "@wordpress/keycodes": "^2.12.0", - "@wordpress/url": "^2.14.0", - "lodash": "^4.17.15", - "node-fetch": "^1.7.3" - } - }, - "@wordpress/i18n": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-3.15.0.tgz", - "integrity": "sha512-AawJgHEGPyMoPATl8a3Qa+cCZV3S6azPfvqeStbN2pSc7v0HqYhJhWaw80WToHkN4kyOsfu1PUVf1wefuoMNEA==", - "dev": true, - "requires": { - "@babel/runtime": "^7.9.2", - "gettext-parser": "^1.3.1", - "lodash": "^4.17.19", - "memize": "^1.1.0", - "sprintf-js": "^1.1.1", - "tannin": "^1.2.0" - } - }, - "@wordpress/keycodes": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-2.15.0.tgz", - "integrity": "sha512-XHyBmhzWjp0svzwiGLOwovlQHH42KkACKTfakDizB5OaaAzlmgZ34Fdl03S7pWl+HUBa7MqItRhGsd4kxdo0bQ==", - "dev": true, - "requires": { - "@babel/runtime": "^7.9.2", - "@wordpress/i18n": "^3.15.0", - "lodash": "^4.17.19" - } - }, - "@wordpress/url": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-2.18.0.tgz", - "integrity": "sha512-FX6CYVG8vYgQnxjA9SsWTDAWPHarPSBIGk2shZ3I+cq+LV31dDaAz8OhvVMD6rvUoQW0INlWe1t2JKXoHhcTcw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.9.2", - "lodash": "^4.17.19", - "qs": "^6.5.2", - "react-native-url-polyfill": "^1.1.2" - } - }, - "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", - "dev": true - }, - "buffer": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", - "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "requires": { - "iconv-lite": "^0.6.2" - } - }, - "faker": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/faker/-/faker-5.1.0.tgz", - "integrity": "sha512-RrWKFSSA/aNLP0g3o2WW1Zez7/MnMr7xkiZmoCfAGZmdkDQZ6l2KtuXHN5XjdvpRjDl8+3vf+Rrtl06Z352+Mw==", - "dev": true - }, - "gettext-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.4.0.tgz", - "integrity": "sha512-sedZYLHlHeBop/gZ1jdg59hlUEcpcZJofLq2JFwJT1zTqAU3l2wFv6IsuwFHGqbiT9DWzMUW4/em2+hspnmMMA==", - "dev": true, - "requires": { - "encoding": "^0.1.12", - "safe-buffer": "^5.1.1" - } - }, - "iconv-lite": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", - "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "dev": true - }, - "memize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/memize/-/memize-1.1.0.tgz", - "integrity": "sha512-K4FcPETOMTwe7KL2LK0orMhpOmWD2wRGwWWpbZy0fyArwsyIKR8YJVz8+efBAh3BO4zPqlSICu4vsLTRRqtFAg==", - "dev": true - }, - "node-fetch": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", - "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", - "dev": true, - "requires": { - "encoding": "^0.1.11", - "is-stream": "^1.0.1" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "qs": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", - "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==", - "dev": true - }, - "react-native-url-polyfill": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-1.2.0.tgz", - "integrity": "sha512-hpLZ8RyS3oGVyTOe/HjoqVoCOSkeJvrCoEB3bJsY7t9uh7kpQDV6kgvdlECEafYpxe3RzMrKLVcmWRbPU7CuAw==", - "dev": true, - "requires": { - "whatwg-url-without-unicode": "8.0.0-3" - } - }, - "regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", - "dev": true - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", - "dev": true - }, - "tannin": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tannin/-/tannin-1.2.0.tgz", - "integrity": "sha512-U7GgX/RcSeUETbV7gYgoz8PD7Ni4y95pgIP/Z6ayI3CfhSujwKEBlGFTCRN+Aqnuyf4AN2yHL+L8x+TCGjb9uA==", - "dev": true, - "requires": { - "@tannin/plural-forms": "^1.1.0" - } - }, - "webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "dev": true - }, - "whatwg-url-without-unicode": { - "version": "8.0.0-3", - "resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz", - "integrity": "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==", - "dev": true, - "requires": { - "buffer": "^5.4.3", - "punycode": "^2.1.1", - "webidl-conversions": "^5.0.0" - } - } - } - }, - "@wordpress/babel-plugin-import-jsx-pragma": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@wordpress/babel-plugin-import-jsx-pragma/-/babel-plugin-import-jsx-pragma-1.1.3.tgz", - "integrity": "sha512-WkVeFZpM5yuHigWe8llZDeMRa4bhMQoHu9dzs1s3cmB1do2mhk341Iw34FidWto14Dzd+383K71vxJejqjKOwQ==", - "dev": true, - "requires": { - "@babel/runtime": "^7.0.0" - } - }, - "@wordpress/babel-preset-default": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-3.0.2.tgz", - "integrity": "sha512-bsa4piS4GU02isj2XJNUgSEC7MpzdYNy9wOFySrp8G6IHAvwrlwcPEXJf5EuwE8ZqTMmFAzPyKOHFEAx/j+J1A==", - "dev": true, - "requires": { - "@babel/core": "^7.0.0", - "@babel/plugin-proposal-async-generator-functions": "^7.0.0", - "@babel/plugin-proposal-object-rest-spread": "^7.0.0", - "@babel/plugin-transform-react-jsx": "^7.0.0", - "@babel/plugin-transform-runtime": "^7.0.0", - "@babel/preset-env": "^7.0.0", - "@babel/runtime": "^7.0.0", - "@wordpress/browserslist-config": "^2.2.3", - "babel-core": "^7.0.0-bridge.0" - } - }, - "@wordpress/browserslist-config": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-2.7.0.tgz", - "integrity": "sha512-pB45JlfmHuEigNFZ1X+CTgIsOT3/TTb9iZxw1DHXge/7ytY8FNhtcNwTfF9IgnS6/xaFRZBqzw4DyH4sP1Lyxg==", - "dev": true - }, - "@wordpress/deprecated": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@wordpress/deprecated/-/deprecated-2.11.1.tgz", - "integrity": "sha512-ri5M3TSAhonRN9G67KDwu8AXthrxay/1lLwBVbRA+6Dpj6hpC4qUBxOP4Yx5VLYOJEJW2YJx3w3G3XFYiyqfFg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.12.5", - "@wordpress/hooks": "^2.11.1" - }, - "dependencies": { - "@babel/runtime": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", - "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } - } - } - }, - "@wordpress/e2e-test-utils": { - "version": "4.15.0", - "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils/-/e2e-test-utils-4.15.0.tgz", - "integrity": "sha512-mCOlNDX/yERd7hIAFB+y9x56iCQ2XyDZkWNlQNMYRH0+EdrQ5H5zE7MSxzycideIC7grxKw/j4RcuyxUdSWGDw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.11.2", - "@wordpress/keycodes": "^2.16.0", - "@wordpress/url": "^2.19.0", - "lodash": "^4.17.19", - "node-fetch": "^2.6.0" - } - }, - "@wordpress/eslint-plugin": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-7.3.0.tgz", - "integrity": "sha512-7wIFzzc14E1XuuT9haBuhoA9FRUGWlbD4Oek+XkiZlzNVqZI3slgbtIFJ6/Mfij1V18rv6Ns9a1cPJLtCU8JHQ==", - "dev": true, - "requires": { - "@wordpress/prettier-config": "^0.4.0", - "babel-eslint": "^10.1.0", - "cosmiconfig": "^7.0.0", - "eslint-config-prettier": "^6.10.1", - "eslint-plugin-jest": "^23.8.2", - "eslint-plugin-jsdoc": "^30.2.2", - "eslint-plugin-jsx-a11y": "^6.2.3", - "eslint-plugin-prettier": "^3.1.2", - "eslint-plugin-react": "^7.20.0", - "eslint-plugin-react-hooks": "^4.0.4", - "globals": "^12.0.0", - "prettier": "npm:wp-prettier@2.0.5", - "requireindex": "^1.2.0" - }, - "dependencies": { - "cosmiconfig": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", - "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", - "dev": true, - "requires": { - "type-fest": "^0.8.1" - } - } - } - }, - "@wordpress/hooks": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-2.11.1.tgz", - "integrity": "sha512-20nsvmLH5/iw9P6M7kiEBBQ7X7G3pEbqED/aN5dqkMCklDyar+OZqYBzdpGGsthXVYgomfNy6QQZWELkGJbcbw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.12.5" - }, - "dependencies": { - "@babel/runtime": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", - "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } - } - } - }, - "@wordpress/i18n": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-3.16.0.tgz", - "integrity": "sha512-ZyRWplETgD90caVaBuGBFcnYVpcogji1g9Ctbb5AO2bGFeHpmPpjvWm0NE64iQTtLFEJoaCiq6oqUvAOPIQJpw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.11.2", - "gettext-parser": "^1.3.1", - "lodash": "^4.17.19", - "memize": "^1.1.0", - "sprintf-js": "^1.1.1", - "tannin": "^1.2.0" - } - }, - "@wordpress/jest-console": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-3.9.0.tgz", - "integrity": "sha512-SJU78Gku3BC5iXrcEAAkER/sBkguYAhaA+HVI4FARklsOfmdBbElrA5wjU0lY54CGTSqWHLy0YZJkaZ5V/YWXA==", - "dev": true, - "requires": { - "@babel/runtime": "^7.11.2", - "jest-matcher-utils": "^25.3.0", - "lodash": "^4.17.19" - } - }, - "@wordpress/jest-preset-default": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-6.4.0.tgz", - "integrity": "sha512-xey6qdRFnK3apC9qOjP5zsw+CsaEIyp6DBmeEgz8QXVd3kI9lE31HarKI/eBY7RBe0hSYLxUJ8tEuTDVMkKX4g==", - "dev": true, - "requires": { - "@jest/reporters": "^25.3.0", - "@wordpress/jest-console": "^3.9.0", - "babel-jest": "^25.3.0", - "enzyme": "^3.11.0", - "enzyme-adapter-react-16": "^1.15.2", - "enzyme-to-json": "^3.4.4" - } - }, - "@wordpress/keycodes": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-2.16.0.tgz", - "integrity": "sha512-8CfxB+9f08FXMUsaO625abmbx2ZinFUz6upzXbe0Da8W3oy7+/TZz6EWsMVBEWz+alSR3Z2FUZ7xUuopHZFcow==", - "dev": true, - "requires": { - "@babel/runtime": "^7.11.2", - "@wordpress/i18n": "^3.16.0", - "lodash": "^4.17.19" - } - }, - "@wordpress/prettier-config": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-0.4.0.tgz", - "integrity": "sha512-7c4VeugkCwDkaHSD7ffxoP0VC5c///gCTEAT032OhI5Rik2dPxE3EkNAB2NhotGE8M4dMAg4g5Wj2OWZIn8TFw==", - "dev": true - }, - "@wordpress/url": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-2.19.0.tgz", - "integrity": "sha512-RizWbBxYmWBlNd+q89r3N6Y2XO8eCG3VncnXDgbGnhV4e+2z9fjzp1/9C/SORftEn+ix/qBKbqygmkmBqb+wuw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.11.2", - "lodash": "^4.17.19", - "qs": "^6.5.2", - "react-native-url-polyfill": "^1.1.2" - } - }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true - }, - "@zkochan/cmd-shim": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@zkochan/cmd-shim/-/cmd-shim-3.1.0.tgz", - "integrity": "sha512-o8l0+x7C7sMZU3v9GuJIAU10qQLtwR1dtRQIOmlNMtyaqhmpXOzx1HWiYoWfmmf9HHZoAkXpc9TM9PQYF9d4Jg==", - "dev": true, - "requires": { - "is-windows": "^1.0.0", - "mkdirp-promise": "^5.0.1", - "mz": "^2.5.0" - } - }, - "JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "dev": true, - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, - "abab": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", - "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", - "dev": true - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - }, - "acorn-globals": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", - "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", - "dev": true, - "requires": { - "acorn": "^6.0.1", - "acorn-walk": "^6.0.1" - }, - "dependencies": { - "acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", - "dev": true - } - } - }, - "acorn-jsx": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", - "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", - "dev": true - }, - "acorn-walk": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", - "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", - "dev": true - }, - "agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", - "dev": true, - "requires": { - "es6-promisify": "^5.0.0" - } - }, - "agentkeepalive": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.2.tgz", - "integrity": "sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==", - "dev": true, - "requires": { - "humanize-ms": "^1.2.1" - } - }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - }, - "airbnb-prop-types": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz", - "integrity": "sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==", - "dev": true, - "requires": { - "array.prototype.find": "^2.1.1", - "function.prototype.name": "^1.1.2", - "is-regex": "^1.1.0", - "object-is": "^1.1.2", - "object.assign": "^4.1.0", - "object.entries": "^1.1.2", - "prop-types": "^15.7.2", - "prop-types-exact": "^1.2.0", - "react-is": "^16.13.1" - } - }, - "ajv": { - "version": "6.12.5", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz", - "integrity": "sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-errors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", - "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "dev": true - }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true - }, - "ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", - "dev": true - }, - "ansi-escapes": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", - "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", - "dev": true, - "requires": { - "type-fest": "^0.11.0" - }, - "dependencies": { - "type-fest": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", - "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", - "dev": true - } - } - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "any-observable": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz", - "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==", - "dev": true - }, - "any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", - "dev": true - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "append-transform": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", - "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=", - "dev": true, - "requires": { - "default-require-extensions": "^1.0.0" - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "dev": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - }, - "dependencies": { - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - } - } - }, - "aria-query": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", - "dev": true, - "requires": { - "@babel/runtime": "^7.10.2", - "@babel/runtime-corejs3": "^7.10.2" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-differ": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-2.1.0.tgz", - "integrity": "sha512-KbUpJgx909ZscOc/7CLATBFam7P1Z1QRQInvgT0UztM9Q72aGKCunKASAl7WNW0tnPmPyEMeMhdsfWhfmW037w==", - "dev": true - }, - "array-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", - "dev": true - }, - "array-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", - "dev": true - }, - "array-filter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz", - "integrity": "sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=", - "dev": true - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true - }, - "array-ify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=", - "dev": true - }, - "array-includes": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", - "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0", - "is-string": "^1.0.5" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "array-slice": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", - "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", - "dev": true - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "array.prototype.find": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.1.1.tgz", - "integrity": "sha512-mi+MYNJYLTx2eNYy+Yh6raoQacCsNeeMUaspFPh9Y141lFSsWxxB8V9mM2ye+eqiRs917J6/pJ4M9ZPzenWckA==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.4" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "array.prototype.flat": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", - "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "array.prototype.flatmap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.3.tgz", - "integrity": "sha512-OOEk+lkePcg+ODXIpvuU9PAryCikCJyo7GlDG1upleEpQRx6mzL9puEBkozQ5iAx20KV0l3DbyQwqciJtqe5Pg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", - "dev": true - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - }, - "dependencies": { - "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", - "dev": true - } - } - }, - "assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", - "dev": true, - "requires": { - "object-assign": "^4.1.1", - "util": "0.10.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", - "dev": true - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "dev": true, - "requires": { - "inherits": "2.0.1" - } - } - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", - "dev": true - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true - }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, - "async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true, - "optional": true - }, - "async-foreach": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", - "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "atob-lite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz", - "integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=", - "dev": true - }, - "autoprefixer": { - "version": "9.8.6", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz", - "integrity": "sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==", - "dev": true, - "requires": { - "browserslist": "^4.12.0", - "caniuse-lite": "^1.0.30001109", - "colorette": "^1.2.1", - "normalize-range": "^0.1.2", - "num2fraction": "^1.2.2", - "postcss": "^7.0.32", - "postcss-value-parser": "^4.1.0" - } - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz", - "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==", - "dev": true - }, - "axe-core": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.0.2.tgz", - "integrity": "sha512-arU1h31OGFu+LPrOLGZ7nB45v940NMDMEJeNmbutu57P+UFDVnkZg3e+J1I2HJRZ9hT7gO8J91dn/PMrAiKakA==", - "dev": true - }, - "axobject-query": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", - "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", - "dev": true - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "babel-core": { - "version": "7.0.0-bridge.0", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", - "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", - "dev": true - }, - "babel-eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", - "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0", - "eslint-visitor-keys": "^1.0.0", - "resolve": "^1.12.0" - } - }, - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "dev": true, - "requires": { - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "detect-indent": "^4.0.0", - "jsesc": "^1.3.0", - "lodash": "^4.17.4", - "source-map": "^0.5.7", - "trim-right": "^1.0.1" - }, - "dependencies": { - "jsesc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", - "dev": true - } - } - }, - "babel-jest": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.5.1.tgz", - "integrity": "sha512-9dA9+GmMjIzgPnYtkhBg73gOo/RHqPmLruP3BaGL4KEX3Dwz6pI8auSN8G8+iuEG90+GSswyKvslN+JYSaacaQ==", - "dev": true, - "requires": { - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "@types/babel__core": "^7.1.7", - "babel-plugin-istanbul": "^6.0.0", - "babel-preset-jest": "^25.5.0", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-dynamic-import-node": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", - "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", - "dev": true, - "requires": { - "object.assign": "^4.1.0" - } - }, - "babel-plugin-istanbul": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", - "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^4.0.0", - "test-exclude": "^6.0.0" - }, - "dependencies": { - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", - "dev": true, - "requires": { - "@babel/core": "^7.7.5", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "babel-plugin-jest-hoist": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.5.0.tgz", - "integrity": "sha512-u+/W+WAjMlvoocYGTwthAiQSxDcJAyHpQ6oWlHdFZaaN+Rlk8Q7iiwDPg2lN/FyJtAYnKjFxbn7xus4HCFkg5g==", - "dev": true, - "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__traverse": "^7.0.6" - } - }, - "babel-preset-current-node-syntax": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.4.tgz", - "integrity": "sha512-5/INNCYhUGqw7VbVjT/hb3ucjgkVHKXY7lX3ZjlN4gm565VyFmJUrJ/h+h16ECVB38R/9SF6aACydpKMLZ/c9w==", - "dev": true, - "requires": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - } - }, - "babel-preset-jest": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-25.5.0.tgz", - "integrity": "sha512-8ZczygctQkBU+63DtSOKGh7tFL0CeCuz+1ieud9lJ1WPQ9O6A1a/r+LGn6Y705PA6whHQ3T1XuB/PmpfNYf8Fw==", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "^25.5.0", - "babel-preset-current-node-syntax": "^0.1.2" - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true - } - } - }, - "babel-template": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "lodash": "^4.17.4" - } - }, - "babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "dev": true, - "requires": { - "babel-code-frame": "^6.26.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "debug": "^2.6.8", - "globals": "^9.18.0", - "invariant": "^2.2.2", - "lodash": "^4.17.4" - }, - "dependencies": { - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true - } - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - }, - "dependencies": { - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", - "dev": true - } - } - }, - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "dev": true - }, - "bail": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", - "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "before-after-hook": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz", - "integrity": "sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A==", - "dev": true - }, - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true, - "optional": true - }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "requires": { - "file-uri-to-path": "1.0.0" - } - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "dev": true, - "requires": { - "inherits": "~2.0.0" - } - }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, - "bn.js": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz", - "integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==", - "dev": true - }, - "body": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz", - "integrity": "sha1-5LoM5BCkaTYyM2dgnstOZVMSUGk=", - "dev": true, - "requires": { - "continuable-cache": "^0.3.1", - "error": "^7.0.0", - "raw-body": "~1.1.0", - "safe-json-parse": "~1.0.1" - } - }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true - }, - "browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", - "dev": true - }, - "browser-resolve": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", - "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", - "dev": true, - "requires": { - "resolve": "1.1.7" - }, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - } - } - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "randombytes": "^2.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", - "dev": true - } - } - }, - "browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", - "dev": true, - "requires": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - } - } - }, - "browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "requires": { - "pako": "~1.0.5" - } - }, - "browserslist": { - "version": "4.14.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.5.tgz", - "integrity": "sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001135", - "electron-to-chromium": "^1.3.571", - "escalade": "^3.1.0", - "node-releases": "^1.1.61" - } - }, - "bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "requires": { - "node-int64": "^0.4.0" - } - }, - "btoa-lite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", - "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=", - "dev": true - }, - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "dev": true - }, - "builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", - "dev": true - }, - "builtins": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", - "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=", - "dev": true - }, - "byline": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", - "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=", - "dev": true - }, - "byte-size": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-5.0.1.tgz", - "integrity": "sha512-/XuKeqWocKsYa/cBY1YbSJSWWqTi4cFgr9S6OyM7PBaPbr9zvNGwWP33vt0uqGhwDdN+y3yhbXVILEUpnwEWGw==", - "dev": true - }, - "bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", - "integrity": "sha1-NWnt6Lo0MV+rmcPpLLBMciDeH6g=", - "dev": true - }, - "cacache": { - "version": "12.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", - "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", - "dev": true, - "requires": { - "bluebird": "^3.5.5", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.4", - "graceful-fs": "^4.1.15", - "infer-owner": "^1.0.3", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.3", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - } - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", - "dev": true - }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", - "dev": true, - "requires": { - "callsites": "^2.0.0" - }, - "dependencies": { - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", - "dev": true - } - } - }, - "caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", - "dev": true, - "requires": { - "caller-callsite": "^2.0.0" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - } - }, - "caniuse-lite": { - "version": "1.0.30001146", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001146.tgz", - "integrity": "sha512-VAy5RHDfTJhpxnDdp2n40GPPLp3KqNrXz1QqFv4J64HvArKs8nuNMOWkB3ICOaBTU/Aj4rYAo/ytdQDDFF/Pug==", - "dev": true - }, - "capture-exit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", - "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", - "dev": true, - "requires": { - "rsvp": "^4.8.4" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "ccount": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.0.5.tgz", - "integrity": "sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw==", - "dev": true - }, - "chai": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", - "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.0", - "type-detect": "^4.0.5" - } - }, - "chai-as-promised": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", - "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", - "dev": true, - "requires": { - "check-error": "^1.0.2" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "character-entities": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", - "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", - "dev": true - }, - "character-entities-html4": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz", - "integrity": "sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==", - "dev": true - }, - "character-entities-legacy": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", - "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", - "dev": true - }, - "character-reference-invalid": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", - "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", - "dev": true - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true - }, - "cheerio": { - "version": "1.0.0-rc.3", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.3.tgz", - "integrity": "sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==", - "dev": true, - "requires": { - "css-select": "~1.2.0", - "dom-serializer": "~0.1.1", - "entities": "~1.1.1", - "htmlparser2": "^3.9.1", - "lodash": "^4.15.0", - "parse5": "^3.0.1" - }, - "dependencies": { - "dom-serializer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", - "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", - "dev": true, - "requires": { - "domelementtype": "^1.3.0", - "entities": "^1.1.1" - } - }, - "parse5": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", - "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", - "dev": true, - "requires": { - "@types/node": "*" - } - } - } - }, - "chokidar": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", - "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==", - "dev": true, - "optional": true, - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.1.2", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.5.0" - }, - "dependencies": { - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, - "optional": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "binary-extensions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", - "dev": true, - "optional": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "optional": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "optional": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "dev": true, - "optional": true - }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, - "optional": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "optional": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "optional": true - }, - "readdirp": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", - "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", - "dev": true, - "optional": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "optional": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true - }, - "chrome-trace-event": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", - "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "clean-css": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", - "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", - "dev": true, - "requires": { - "source-map": "~0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "cli-truncate": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", - "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=", - "dev": true, - "requires": { - "slice-ansi": "0.0.4", - "string-width": "^1.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", - "dev": true - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - }, - "dependencies": { - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", - "dev": true - }, - "clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - } - }, - "clone-regexp": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz", - "integrity": "sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==", - "dev": true, - "requires": { - "is-regexp": "^2.0.0" - }, - "dependencies": { - "is-regexp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-2.1.0.tgz", - "integrity": "sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==", - "dev": true - } - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "collapse-white-space": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz", - "integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==", - "dev": true - }, - "collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", - "dev": true - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "colorette": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", - "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", - "dev": true - }, - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", - "dev": true - }, - "columnify": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.5.4.tgz", - "integrity": "sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs=", - "dev": true, - "requires": { - "strip-ansi": "^3.0.0", - "wcwidth": "^1.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true - }, - "comment-parser": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-0.7.6.tgz", - "integrity": "sha512-GKNxVA7/iuTnAqGADlTWX4tkhzxZKXp5fLJqKTlQLHkE65XDUKutZ3BHaJC5IGcper2tT3QRD1xr4o3jNpgXXg==", - "dev": true - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true - }, - "compare-func": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", - "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", - "dev": true, - "requires": { - "array-ify": "^1.0.0", - "dot-prop": "^5.1.0" - }, - "dependencies": { - "dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "requires": { - "is-obj": "^2.0.0" - } - }, - "is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true - } - } - }, - "compare-versions": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", - "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", - "dev": true - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "config": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/config/-/config-3.3.3.tgz", - "integrity": "sha512-T3RmZQEAji5KYqUQpziWtyGJFli6Khz7h0rpxDwYNjSkr5ynyTWwO7WpfjHzTXclNCDfSWQRcwMb+NwxJesCKw==", - "dev": true, - "requires": { - "json5": "^2.1.1" - } - }, - "config-chain": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", - "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", - "dev": true, - "requires": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "console-browserify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true - }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", - "dev": true - }, - "continuable-cache": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz", - "integrity": "sha1-vXJ6f67XfnH/OYWskzUakSczrQ8=", - "dev": true - }, - "conventional-changelog-angular": { - "version": "5.0.11", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.11.tgz", - "integrity": "sha512-nSLypht/1yEflhuTogC03i7DX7sOrXGsRn14g131Potqi6cbGbGEE9PSDEHKldabB6N76HiSyw9Ph+kLmC04Qw==", - "dev": true, - "requires": { - "compare-func": "^2.0.0", - "q": "^1.5.1" - } - }, - "conventional-changelog-core": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-3.2.3.tgz", - "integrity": "sha512-LMMX1JlxPIq/Ez5aYAYS5CpuwbOk6QFp8O4HLAcZxe3vxoCtABkhfjetk8IYdRB9CDQGwJFLR3Dr55Za6XKgUQ==", - "dev": true, - "requires": { - "conventional-changelog-writer": "^4.0.6", - "conventional-commits-parser": "^3.0.3", - "dateformat": "^3.0.0", - "get-pkg-repo": "^1.0.0", - "git-raw-commits": "2.0.0", - "git-remote-origin-url": "^2.0.0", - "git-semver-tags": "^2.0.3", - "lodash": "^4.2.1", - "normalize-package-data": "^2.3.5", - "q": "^1.5.1", - "read-pkg": "^3.0.0", - "read-pkg-up": "^3.0.0", - "through2": "^3.0.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "through2": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", - "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", - "dev": true, - "requires": { - "inherits": "^2.0.4", - "readable-stream": "2 || 3" - } - } - } - }, - "conventional-changelog-preset-loader": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz", - "integrity": "sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==", - "dev": true - }, - "conventional-changelog-writer": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.0.17.tgz", - "integrity": "sha512-IKQuK3bib/n032KWaSb8YlBFds+aLmzENtnKtxJy3+HqDq5kohu3g/UdNbIHeJWygfnEbZjnCKFxAW0y7ArZAw==", - "dev": true, - "requires": { - "compare-func": "^2.0.0", - "conventional-commits-filter": "^2.0.6", - "dateformat": "^3.0.0", - "handlebars": "^4.7.6", - "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.15", - "meow": "^7.0.0", - "semver": "^6.0.0", - "split": "^1.0.0", - "through2": "^3.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "through2": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", - "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", - "dev": true, - "requires": { - "inherits": "^2.0.4", - "readable-stream": "2 || 3" - } - } - } - }, - "conventional-commits-filter": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.6.tgz", - "integrity": "sha512-4g+sw8+KA50/Qwzfr0hL5k5NWxqtrOVw4DDk3/h6L85a9Gz0/Eqp3oP+CWCNfesBvZZZEFHF7OTEbRe+yYSyKw==", - "dev": true, - "requires": { - "lodash.ismatch": "^4.4.0", - "modify-values": "^1.0.0" - } - }, - "conventional-commits-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.1.0.tgz", - "integrity": "sha512-RSo5S0WIwXZiRxUGTPuYFbqvrR4vpJ1BDdTlthFgvHt5kEdnd1+pdvwWphWn57/oIl4V72NMmOocFqqJ8mFFhA==", - "dev": true, - "requires": { - "JSONStream": "^1.0.4", - "is-text-path": "^1.0.1", - "lodash": "^4.17.15", - "meow": "^7.0.0", - "split2": "^2.0.0", - "through2": "^3.0.0", - "trim-off-newlines": "^1.0.0" - }, - "dependencies": { - "through2": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", - "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", - "dev": true, - "requires": { - "inherits": "^2.0.4", - "readable-stream": "2 || 3" - } - } - } - }, - "conventional-recommended-bump": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-5.0.1.tgz", - "integrity": "sha512-RVdt0elRcCxL90IrNP0fYCpq1uGt2MALko0eyeQ+zQuDVWtMGAy9ng6yYn3kax42lCj9+XBxQ8ZN6S9bdKxDhQ==", - "dev": true, - "requires": { - "concat-stream": "^2.0.0", - "conventional-changelog-preset-loader": "^2.1.1", - "conventional-commits-filter": "^2.0.2", - "conventional-commits-parser": "^3.0.3", - "git-raw-commits": "2.0.0", - "git-semver-tags": "^2.0.3", - "meow": "^4.0.0", - "q": "^1.5.1" - }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "camelcase-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", - "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", - "dev": true, - "requires": { - "camelcase": "^4.1.0", - "map-obj": "^2.0.0", - "quick-lru": "^1.0.0" - } - }, - "concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", - "dev": true - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "map-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", - "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", - "dev": true - }, - "meow": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", - "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", - "dev": true, - "requires": { - "camelcase-keys": "^4.0.0", - "decamelize-keys": "^1.0.0", - "loud-rejection": "^1.0.0", - "minimist": "^1.1.3", - "minimist-options": "^3.0.1", - "normalize-package-data": "^2.3.4", - "read-pkg-up": "^3.0.0", - "redent": "^2.0.0", - "trim-newlines": "^2.0.0" - } - }, - "minimist-options": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", - "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "quick-lru": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", - "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", - "dev": true - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "redent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", - "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", - "dev": true, - "requires": { - "indent-string": "^3.0.0", - "strip-indent": "^2.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-indent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", - "dev": true - }, - "trim-newlines": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", - "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", - "dev": true - } - } - }, - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "copy-concurrently": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", - "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", - "dev": true, - "requires": { - "aproba": "^1.1.1", - "fs-write-stream-atomic": "^1.0.8", - "iferr": "^0.1.5", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.0" - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", - "dev": true - }, - "core-js-compat": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.0.tgz", - "integrity": "sha512-o9QKelQSxQMYWHXc/Gc4L8bx/4F7TTraE5rhuN8I7mKBt5dBIUpXpIR3omv70ebr8ST5R3PqbDQr+ZI3+Tt1FQ==", - "dev": true, - "requires": { - "browserslist": "^4.14.7", - "semver": "7.0.0" - }, - "dependencies": { - "browserslist": { - "version": "4.14.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.7.tgz", - "integrity": "sha512-BSVRLCeG3Xt/j/1cCGj1019Wbty0H+Yvu2AOuZSuoaUWn3RatbL33Cxk+Q4jRMRAbOm0p7SLravLjpnT6s0vzQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001157", - "colorette": "^1.2.1", - "electron-to-chromium": "^1.3.591", - "escalade": "^3.1.1", - "node-releases": "^1.1.66" - } - }, - "caniuse-lite": { - "version": "1.0.30001163", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001163.tgz", - "integrity": "sha512-QQbOGkHWnvhn3Dlf4scPlXTZVhGOK+2qCOP5gPxqzXHhtn3tZHwNdH9qNcQRWN0f3tDYrsyXFJCFiP/GLzI5Vg==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.3.611", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.611.tgz", - "integrity": "sha512-YhqTzCXtEO2h0foGLGS60ortd6yY/yUQhqDEp1VWG3DIyHvckFFyaRwR41M0/M3m7Yb8Exqh+nzyb2TuxaoMTw==", - "dev": true - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "node-releases": { - "version": "1.1.67", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.67.tgz", - "integrity": "sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==", - "dev": true - }, - "semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "dev": true - } - } - }, - "core-js-pure": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.7.0.tgz", - "integrity": "sha512-EZD2ckZysv8MMt4J6HSvS9K2GdtlZtdBncKAmF9lr2n0c9dJUaUN88PSTjvgwCgQPWKTkERXITgS6JJRAnljtg==", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - } - }, - "create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - }, - "dependencies": { - "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", - "dev": true - } - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "cross-env": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-6.0.3.tgz", - "integrity": "sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0" - } - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - }, - "dependencies": { - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - } - } - }, - "css-select": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", - "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", - "dev": true, - "requires": { - "boolbase": "~1.0.0", - "css-what": "2.1", - "domutils": "1.5.1", - "nth-check": "~1.0.1" - }, - "dependencies": { - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - } - } - }, - "css-what": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", - "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", - "dev": true - }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true - }, - "cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", - "dev": true - }, - "cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dev": true, - "requires": { - "cssom": "~0.3.6" - }, - "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - } - } - }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "dev": true, - "requires": { - "array-find-index": "^1.0.1" - } - }, - "cyclist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", - "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", - "dev": true - }, - "damerau-levenshtein": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz", - "integrity": "sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==", - "dev": true - }, - "dargs": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", - "integrity": "sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "data-urls": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", - "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "whatwg-mimetype": "^2.2.0", - "whatwg-url": "^7.0.0" - } - }, - "date-fns": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", - "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", - "dev": true - }, - "dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", - "dev": true - }, - "deasync": { - "version": "0.1.21", - "resolved": "https://registry.npmjs.org/deasync/-/deasync-0.1.21.tgz", - "integrity": "sha512-kUmM8Y+PZpMpQ+B4AuOW9k2Pfx/mSupJtxOsLzmnHY2WqZUYRFccFn2RhzPAqt3Xb+sorK/badW2D4zNzqZz5w==", - "dev": true, - "requires": { - "bindings": "^1.5.0", - "node-addon-api": "^1.7.1" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "debuglog": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", - "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=", - "dev": true - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decamelize-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", - "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", - "dev": true, - "requires": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "dependencies": { - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true - } - } - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", - "dev": true - }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true - }, - "default-require-extensions": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", - "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=", - "dev": true, - "requires": { - "strip-bom": "^2.0.0" - } - }, - "defaults": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", - "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", - "dev": true, - "requires": { - "clone": "^1.0.2" - } - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "del": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/del/-/del-5.1.0.tgz", - "integrity": "sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==", - "dev": true, - "requires": { - "globby": "^10.0.1", - "graceful-fs": "^4.2.2", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.1", - "p-map": "^3.0.0", - "rimraf": "^3.0.0", - "slash": "^3.0.0" - }, - "dependencies": { - "@nodelib/fs.stat": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", - "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "fast-glob": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", - "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", - "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globby": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz", - "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==", - "dev": true, - "requires": { - "@types/glob": "^7.1.1", - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.0.3", - "glob": "^7.1.3", - "ignore": "^5.1.1", - "merge2": "^1.2.3", - "slash": "^3.0.0" - } - }, - "ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true - }, - "deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", - "dev": true - }, - "des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", - "dev": true - }, - "detect-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true - }, - "dezalgo": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", - "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", - "dev": true, - "requires": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, - "diff-sequences": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", - "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", - "dev": true - }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - }, - "dependencies": { - "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", - "dev": true - } - } - }, - "dir-glob": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", - "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", - "dev": true, - "requires": { - "path-type": "^3.0.0" - }, - "dependencies": { - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, - "discontinuous-range": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", - "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=", - "dev": true - }, - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - }, - "dependencies": { - "domelementtype": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.2.tgz", - "integrity": "sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA==", - "dev": true - }, - "entities": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", - "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", - "dev": true - } - } - }, - "domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", - "dev": true - }, - "domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true - }, - "domexception": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", - "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", - "dev": true, - "requires": { - "webidl-conversions": "^4.0.2" - }, - "dependencies": { - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - } - } - }, - "domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", - "dev": true, - "requires": { - "domelementtype": "1" - } - }, - "domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "dot-prop": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", - "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", - "dev": true, - "requires": { - "is-obj": "^1.0.0" - } - }, - "duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true - }, - "duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "dev": true, - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "electron-to-chromium": { - "version": "1.3.578", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.578.tgz", - "integrity": "sha512-z4gU6dA1CbBJsAErW5swTGAaU2TBzc2mPAonJb00zqW1rOraDo2zfBMDRvaz9cVic+0JEZiYbHWPw/fTaZlG2Q==", - "dev": true - }, - "elegant-spinner": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", - "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=", - "dev": true - }, - "elliptic": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", - "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", - "dev": true, - "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" - }, - "dependencies": { - "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", - "dev": true - } - } - }, - "emoji-regex": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.0.tgz", - "integrity": "sha512-DNc3KFPK18bPdElMJnf/Pkv5TXhxFU3YFDEuGLDRtPmV4rkmCjBkCSEp22u6rBHdSN9Vlp/GK7k98prmE1Jgug==", - "dev": true - }, - "emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true - }, - "encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "requires": { - "iconv-lite": "^0.6.2" - } - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "enhanced-resolve": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", - "integrity": "sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.5.0", - "tapable": "^1.0.0" - }, - "dependencies": { - "memory-fs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", - "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - } - } - }, - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", - "dev": true - }, - "env-paths": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", - "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==", - "dev": true - }, - "envinfo": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.7.3.tgz", - "integrity": "sha512-46+j5QxbPWza0PB1i15nZx0xQ4I/EfQxg9J8Had3b408SV63nEtor2e+oiY63amTo9KTuh2a3XLObNwduxYwwA==", - "dev": true - }, - "enzyme": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.11.0.tgz", - "integrity": "sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw==", - "dev": true, - "requires": { - "array.prototype.flat": "^1.2.3", - "cheerio": "^1.0.0-rc.3", - "enzyme-shallow-equal": "^1.0.1", - "function.prototype.name": "^1.1.2", - "has": "^1.0.3", - "html-element-map": "^1.2.0", - "is-boolean-object": "^1.0.1", - "is-callable": "^1.1.5", - "is-number-object": "^1.0.4", - "is-regex": "^1.0.5", - "is-string": "^1.0.5", - "is-subset": "^0.1.1", - "lodash.escape": "^4.0.1", - "lodash.isequal": "^4.5.0", - "object-inspect": "^1.7.0", - "object-is": "^1.0.2", - "object.assign": "^4.1.0", - "object.entries": "^1.1.1", - "object.values": "^1.1.1", - "raf": "^3.4.1", - "rst-selector-parser": "^2.2.3", - "string.prototype.trim": "^1.2.1" - } - }, - "enzyme-adapter-react-16": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.5.tgz", - "integrity": "sha512-33yUJGT1nHFQlbVI5qdo5Pfqvu/h4qPwi1o0a6ZZsjpiqq92a3HjynDhwd1IeED+Su60HDWV8mxJqkTnLYdGkw==", - "dev": true, - "requires": { - "enzyme-adapter-utils": "^1.13.1", - "enzyme-shallow-equal": "^1.0.4", - "has": "^1.0.3", - "object.assign": "^4.1.0", - "object.values": "^1.1.1", - "prop-types": "^15.7.2", - "react-is": "^16.13.1", - "react-test-renderer": "^16.0.0-0", - "semver": "^5.7.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "enzyme-adapter-utils": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.13.1.tgz", - "integrity": "sha512-5A9MXXgmh/Tkvee3bL/9RCAAgleHqFnsurTYCbymecO4ohvtNO5zqIhHxV370t7nJAwaCfkgtffarKpC0GPt0g==", - "dev": true, - "requires": { - "airbnb-prop-types": "^2.16.0", - "function.prototype.name": "^1.1.2", - "object.assign": "^4.1.0", - "object.fromentries": "^2.0.2", - "prop-types": "^15.7.2", - "semver": "^5.7.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "enzyme-shallow-equal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz", - "integrity": "sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q==", - "dev": true, - "requires": { - "has": "^1.0.3", - "object-is": "^1.1.2" - } - }, - "enzyme-to-json": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.6.1.tgz", - "integrity": "sha512-15tXuONeq5ORoZjV/bUo2gbtZrN2IH+Z6DvL35QmZyKHgbY1ahn6wcnLd9Xv9OjiwbAXiiP8MRZwbZrCv1wYNg==", - "dev": true, - "requires": { - "@types/cheerio": "^0.22.22", - "lodash": "^4.17.15", - "react-is": "^16.12.0" - } - }, - "err-code": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz", - "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=", - "dev": true - }, - "errno": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", - "dev": true, - "requires": { - "prr": "~1.0.1" - } - }, - "error": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/error/-/error-7.2.1.tgz", - "integrity": "sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA==", - "dev": true, - "requires": { - "string-template": "~0.2.1" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.18.0-next.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", - "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-negative-zero": "^2.0.0", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", - "dev": true - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "dev": true, - "requires": { - "es6-promise": "^4.0.3" - } - }, - "escalade": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.0.tgz", - "integrity": "sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "dev": true, - "requires": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } - } - }, - "eslint": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", - "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.3", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.2", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^7.0.0", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.3", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", - "dev": true, - "requires": { - "type-fest": "^0.8.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "eslint-config-prettier": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz", - "integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==", - "dev": true, - "requires": { - "get-stdin": "^6.0.0" - } - }, - "eslint-config-wpcalypso": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-wpcalypso/-/eslint-config-wpcalypso-5.0.0.tgz", - "integrity": "sha512-bENkOkC7Hk2LREkj9aVqv5ELqYaUZqN2IBtmCdsQXrkJBsW8FV9mOzcBHnLm3Cvw4YYfq0rZzIFuCs3pkPbe1Q==", - "dev": true, - "requires": { - "eslint-plugin-react-hooks": "^2.0.0" - }, - "dependencies": { - "eslint-plugin-react-hooks": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.5.1.tgz", - "integrity": "sha512-Y2c4b55R+6ZzwtTppKwSmK/Kar8AdLiC2f9NADCuxbcTgPPg41Gyqa6b9GppgXSvCtkRw43ZE86CT5sejKC6/g==", - "dev": true - } - } - }, - "eslint-plugin-jest": { - "version": "23.20.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-23.20.0.tgz", - "integrity": "sha512-+6BGQt85OREevBDWCvhqj1yYA4+BFK4XnRZSGJionuEYmcglMZYLNNBBemwzbqUAckURaHdJSBcjHPyrtypZOw==", - "dev": true, - "requires": { - "@typescript-eslint/experimental-utils": "^2.5.0" - }, - "dependencies": { - "@typescript-eslint/experimental-utils": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz", - "integrity": "sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.34.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" - } - }, - "@typescript-eslint/typescript-estree": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", - "integrity": "sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "eslint-visitor-keys": "^1.1.0", - "glob": "^7.1.6", - "is-glob": "^4.0.1", - "lodash": "^4.17.15", - "semver": "^7.3.2", - "tsutils": "^3.17.1" - } - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", - "dev": true - } - } - }, - "eslint-plugin-jsdoc": { - "version": "30.7.7", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-30.7.7.tgz", - "integrity": "sha512-DmVMJC2AbpYX7X1KhnVT1a9ex1AUvG+q9G8i6hzjp3cpjW8vmKQTUmZnRS0//W+7HvMqeb+eXPANdCOzGVVZBQ==", - "dev": true, - "requires": { - "comment-parser": "^0.7.6", - "debug": "^4.2.0", - "jsdoctypeparser": "^9.0.0", - "lodash": "^4.17.20", - "regextras": "^0.7.1", - "semver": "^7.3.2", - "spdx-expression-parse": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "eslint-plugin-jsx-a11y": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz", - "integrity": "sha512-0rGPJBbwHoGNPU73/QCLP/vveMlM1b1Z9PponxO87jfr6tuH5ligXbDT6nHSSzBC8ovX2Z+BQu7Bk5D/Xgq9zg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.11.2", - "aria-query": "^4.2.2", - "array-includes": "^3.1.1", - "ast-types-flow": "^0.0.7", - "axe-core": "^4.0.2", - "axobject-query": "^2.2.0", - "damerau-levenshtein": "^1.0.6", - "emoji-regex": "^9.0.0", - "has": "^1.0.3", - "jsx-ast-utils": "^3.1.0", - "language-tags": "^1.0.5" - } - }, - "eslint-plugin-prettier": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz", - "integrity": "sha512-jZDa8z76klRqo+TdGDTFJSavwbnWK2ZpqGKNZ+VvweMW516pDUMmQ2koXvxEE4JhzNvTv+radye/bWGBmA6jmg==", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-plugin-react": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.21.5.tgz", - "integrity": "sha512-8MaEggC2et0wSF6bUeywF7qQ46ER81irOdWS4QWxnnlAEsnzeBevk1sWh7fhpCghPpXb+8Ks7hvaft6L/xsR6g==", - "dev": true, - "requires": { - "array-includes": "^3.1.1", - "array.prototype.flatmap": "^1.2.3", - "doctrine": "^2.1.0", - "has": "^1.0.3", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "object.entries": "^1.1.2", - "object.fromentries": "^2.0.2", - "object.values": "^1.1.1", - "prop-types": "^15.7.2", - "resolve": "^1.18.1", - "string.prototype.matchall": "^4.0.2" - }, - "dependencies": { - "resolve": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", - "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", - "dev": true, - "requires": { - "is-core-module": "^2.1.0", - "path-parse": "^1.0.6" - } - } - } - }, - "eslint-plugin-react-hooks": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz", - "integrity": "sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==", - "dev": true - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - }, - "espree": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", - "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", - "dev": true, - "requires": { - "acorn": "^7.1.1", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.1.0" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esquery": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", - "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "eventemitter2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", - "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", - "dev": true - }, - "eventemitter3": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", - "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", - "dev": true - }, - "events": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", - "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==", - "dev": true - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "exec-sh": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", - "integrity": "sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==", - "dev": true - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "execall": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/execall/-/execall-2.0.0.tgz", - "integrity": "sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==", - "dev": true, - "requires": { - "clone-regexp": "^2.1.0" - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "expect": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-25.5.0.tgz", - "integrity": "sha512-w7KAXo0+6qqZZhovCaBVPSIqQp7/UTcx4M9uKt2m6pd2VB1voyC8JizLRqeEqud3AAVP02g+hbErDu5gu64tlA==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "ansi-styles": "^4.0.0", - "jest-get-type": "^25.2.6", - "jest-matcher-utils": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-regex-util": "^25.2.6" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "dependencies": { - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, - "fast-glob": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", - "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", - "dev": true, - "requires": { - "@mrmlnc/readdir-enhanced": "^2.2.1", - "@nodelib/fs.stat": "^1.1.2", - "glob-parent": "^3.1.0", - "is-glob": "^4.0.0", - "merge2": "^1.2.3", - "micromatch": "^3.1.10" - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fastq": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz", - "integrity": "sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "faye-websocket": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", - "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - }, - "fb-watchman": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", - "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", - "dev": true, - "requires": { - "bser": "2.1.1" - } - }, - "figgy-pudding": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", - "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", - "dev": true - }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "dev": true, - "requires": { - "flat-cache": "^2.0.1" - } - }, - "file-sync-cmp": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz", - "integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=", - "dev": true - }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true - }, - "fileset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", - "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", - "dev": true, - "requires": { - "glob": "^7.0.3", - "minimatch": "^3.0.3" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "find-versions": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz", - "integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==", - "dev": true, - "requires": { - "semver-regex": "^2.0.0" - } - }, - "findup": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/findup/-/findup-0.1.5.tgz", - "integrity": "sha1-itkpozk7rGJ5V6fl3kYjsGsOLOs=", - "dev": true, - "requires": { - "colors": "~0.6.0-1", - "commander": "~2.1.0" - }, - "dependencies": { - "colors": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", - "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", - "dev": true - }, - "commander": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz", - "integrity": "sha1-0SG7roYNmZKj1Re6lvVliOR8Z4E=", - "dev": true - } - } - }, - "findup-sync": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", - "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", - "dev": true, - "requires": { - "glob": "~5.0.0" - }, - "dependencies": { - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "fined": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", - "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", - "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", - "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" - } - }, - "fishery": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fishery/-/fishery-1.2.0.tgz", - "integrity": "sha512-0GG029KHF3p8Q0NiAl/ZOK1fvyAprOiHdtRWUNS46x9QXuQhMwzcGLNDbZ7XIEEBowwBmMsw7StkaU0ek9dSbg==", - "dev": true, - "requires": { - "lodash.mergewith": "^4.6.2" - } - }, - "flagged-respawn": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", - "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", - "dev": true - }, - "flat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", - "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", - "dev": true, - "requires": { - "is-buffer": "~2.0.3" - }, - "dependencies": { - "is-buffer": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", - "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", - "dev": true - } - } - }, - "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "dev": true, - "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - } - }, - "flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", - "dev": true - }, - "flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "dev": true, - "requires": { - "minipass": "^2.6.0" - } - }, - "fs-readdir-recursive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", - "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", - "dev": true - }, - "fs-write-stream-atomic": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", - "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "dev": true, - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - } - }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "function.prototype.name": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.2.tgz", - "integrity": "sha512-C8A+LlHBJjB2AdcRPorc5JvJ5VUoWlXdEHLOJdCI7kjHEtGTpHQUiqMvCIKUwIsGwZX2jZJy761AXsn356bJQg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "functions-have-names": "^1.2.0" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "functions-have-names": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.1.tgz", - "integrity": "sha512-j48B/ZI7VKs3sgeI2cZp7WXWmZXu7Iq5pl5/vptV5N2mq+DGFuS/ulaDjtaoLpYzuD6u8UgrUKHfgo7fDTSiBA==", - "dev": true - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", - "dev": true, - "requires": { - "globule": "^1.0.0" - } - }, - "genfun": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/genfun/-/genfun-5.0.0.tgz", - "integrity": "sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA==", - "dev": true - }, - "gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true - }, - "get-own-enumerable-property-symbols": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", - "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", - "dev": true - }, - "get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true - }, - "get-pkg-repo": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz", - "integrity": "sha1-xztInAbYDMVTbCyFP54FIyBWly0=", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "meow": "^3.3.0", - "normalize-package-data": "^2.3.0", - "parse-github-repo-url": "^1.3.0", - "through2": "^2.0.0" - }, - "dependencies": { - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true - }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "dev": true, - "requires": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - } - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true - }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true - }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "dev": true, - "requires": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "dev": true, - "requires": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - } - }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "dev": true, - "requires": { - "get-stdin": "^4.0.1" - } - }, - "trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", - "dev": true - } - } - }, - "get-port": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz", - "integrity": "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==", - "dev": true - }, - "get-stdin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", - "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", - "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getobject": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", - "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "gettext-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.4.0.tgz", - "integrity": "sha512-sedZYLHlHeBop/gZ1jdg59hlUEcpcZJofLq2JFwJT1zTqAU3l2wFv6IsuwFHGqbiT9DWzMUW4/em2+hspnmMMA==", - "dev": true, - "requires": { - "encoding": "^0.1.12", - "safe-buffer": "^5.1.1" - } - }, - "git-raw-commits": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.0.tgz", - "integrity": "sha512-w4jFEJFgKXMQJ0H0ikBk2S+4KP2VEjhCvLCNqbNRQC8BgGWgLKNCO7a9K9LI+TVT7Gfoloje502sEnctibffgg==", - "dev": true, - "requires": { - "dargs": "^4.0.1", - "lodash.template": "^4.0.2", - "meow": "^4.0.0", - "split2": "^2.0.0", - "through2": "^2.0.0" - }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "camelcase-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", - "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", - "dev": true, - "requires": { - "camelcase": "^4.1.0", - "map-obj": "^2.0.0", - "quick-lru": "^1.0.0" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", - "dev": true - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "map-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", - "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", - "dev": true - }, - "meow": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", - "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", - "dev": true, - "requires": { - "camelcase-keys": "^4.0.0", - "decamelize-keys": "^1.0.0", - "loud-rejection": "^1.0.0", - "minimist": "^1.1.3", - "minimist-options": "^3.0.1", - "normalize-package-data": "^2.3.4", - "read-pkg-up": "^3.0.0", - "redent": "^2.0.0", - "trim-newlines": "^2.0.0" - } - }, - "minimist-options": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", - "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "quick-lru": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", - "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", - "dev": true - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } - }, - "redent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", - "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", - "dev": true, - "requires": { - "indent-string": "^3.0.0", - "strip-indent": "^2.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-indent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", - "dev": true - }, - "trim-newlines": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", - "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", - "dev": true - } - } - }, - "git-remote-origin-url": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", - "integrity": "sha1-UoJlna4hBxRaERJhEq0yFuxfpl8=", - "dev": true, - "requires": { - "gitconfiglocal": "^1.0.0", - "pify": "^2.3.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - }, - "git-semver-tags": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-2.0.3.tgz", - "integrity": "sha512-tj4FD4ww2RX2ae//jSrXZzrocla9db5h0V7ikPl1P/WwoZar9epdUhwR7XHXSgc+ZkNq72BEEerqQuicoEQfzA==", - "dev": true, - "requires": { - "meow": "^4.0.0", - "semver": "^6.0.0" - }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "camelcase-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", - "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", - "dev": true, - "requires": { - "camelcase": "^4.1.0", - "map-obj": "^2.0.0", - "quick-lru": "^1.0.0" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", - "dev": true - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "map-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", - "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", - "dev": true - }, - "meow": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", - "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", - "dev": true, - "requires": { - "camelcase-keys": "^4.0.0", - "decamelize-keys": "^1.0.0", - "loud-rejection": "^1.0.0", - "minimist": "^1.1.3", - "minimist-options": "^3.0.1", - "normalize-package-data": "^2.3.4", - "read-pkg-up": "^3.0.0", - "redent": "^2.0.0", - "trim-newlines": "^2.0.0" - } - }, - "minimist-options": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", - "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "quick-lru": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", - "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", - "dev": true - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } - }, - "redent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", - "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", - "dev": true, - "requires": { - "indent-string": "^3.0.0", - "strip-indent": "^2.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-indent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", - "dev": true - }, - "trim-newlines": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", - "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", - "dev": true - } - } - }, - "git-up": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/git-up/-/git-up-4.0.2.tgz", - "integrity": "sha512-kbuvus1dWQB2sSW4cbfTeGpCMd8ge9jx9RKnhXhuJ7tnvT+NIrTVfYZxjtflZddQYcmdOTlkAcjmx7bor+15AQ==", - "dev": true, - "requires": { - "is-ssh": "^1.3.0", - "parse-url": "^5.0.0" - } - }, - "git-url-parse": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.3.0.tgz", - "integrity": "sha512-i3XNa8IKmqnUqWBcdWBjOcnyZYfN3C1WRvnKI6ouFWwsXCZEnlgbwbm55ZpJ3OJMhfEP/ryFhqW8bBhej3C5Ug==", - "dev": true, - "requires": { - "git-up": "^4.0.0" - } - }, - "gitconfiglocal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", - "integrity": "sha1-QdBF84UaXqiPA/JMocYXgRRGS5s=", - "dev": true, - "requires": { - "ini": "^1.3.2" - } - }, - "github-contributors-list": { - "version": "https://github.com/woocommerce/github-contributors-list/tarball/master", - "integrity": "sha512-4lJ4ERWmcGlG43jDdIy7shG0v3QsfoSvSrtbdcw/cpqF69WBOoHu1iC8Cb3IQ63WU7KujtR0TEF50CEcebhRCA==", - "dev": true, - "requires": { - "marked": "~0.6.2", - "merge": "^1.2.1", - "minimist": "1.2.0", - "q": "~1.5.1", - "sprintf-js": "1.1.2" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "glob-to-regexp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", - "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", - "dev": true - }, - "global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "dev": true, - "requires": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - } - }, - "global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - }, - "dependencies": { - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "globby": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz", - "integrity": "sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==", - "dev": true, - "requires": { - "@types/glob": "^7.1.1", - "array-union": "^1.0.2", - "dir-glob": "^2.2.2", - "fast-glob": "^2.2.6", - "glob": "^7.1.3", - "ignore": "^4.0.3", - "pify": "^4.0.1", - "slash": "^2.0.0" - } - }, - "globjoin": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", - "integrity": "sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM=", - "dev": true - }, - "globule": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.2.tgz", - "integrity": "sha512-7IDTQTIu2xzXkT+6mlluidnWo+BypnbSoEVVQCGfzqnl5Ik8d3e1d4wycb8Rj9tWW+Z39uPWsdlquqiqPCd/pA==", - "dev": true, - "requires": { - "glob": "~7.1.1", - "lodash": "~4.17.10", - "minimatch": "~3.0.2" - } - }, - "gonzales-pe": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz", - "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", - "dev": true - }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true, - "optional": true - }, - "grunt": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.3.0.tgz", - "integrity": "sha512-6ILlMXv11/4cxuhSMfSU+SfvbxrPuqZrAtLN64+tZpQ3DAKfSQPQHRbTjSbdtxfyQhGZPtN0bDZJ/LdCM5WXXA==", - "dev": true, - "requires": { - "dateformat": "~3.0.3", - "eventemitter2": "~0.4.13", - "exit": "~0.1.2", - "findup-sync": "~0.3.0", - "glob": "~7.1.6", - "grunt-cli": "~1.3.2", - "grunt-known-options": "~1.1.0", - "grunt-legacy-log": "~3.0.0", - "grunt-legacy-util": "~2.0.0", - "iconv-lite": "~0.4.13", - "js-yaml": "~3.14.0", - "minimatch": "~3.0.4", - "mkdirp": "~1.0.4", - "nopt": "~3.0.6", - "rimraf": "~3.0.2" - }, - "dependencies": { - "grunt-cli": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.3.2.tgz", - "integrity": "sha512-8OHDiZZkcptxVXtMfDxJvmN7MVJNE8L/yIcPb4HB7TlyFD1kDvjHrb62uhySsU14wJx9ORMnTuhRMQ40lH/orQ==", - "dev": true, - "requires": { - "grunt-known-options": "~1.1.0", - "interpret": "~1.1.0", - "liftoff": "~2.5.0", - "nopt": "~4.0.1", - "v8flags": "~3.1.1" - }, - "dependencies": { - "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "dev": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - } - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "grunt-contrib-clean": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-2.0.0.tgz", - "integrity": "sha512-g5ZD3ORk6gMa5ugZosLDQl3dZO7cI3R14U75hTM+dVLVxdMNJCPVmwf9OUt4v4eWgpKKWWoVK9DZc1amJp4nQw==", - "dev": true, - "requires": { - "async": "^2.6.1", - "rimraf": "^2.6.2" - }, - "dependencies": { - "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - } - } - } - }, - "grunt-contrib-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/grunt-contrib-concat/-/grunt-contrib-concat-1.0.1.tgz", - "integrity": "sha1-YVCYYwhOhx1+ht5IwBUlntl3Rb0=", - "dev": true, - "requires": { - "chalk": "^1.0.0", - "source-map": "^0.5.3" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "grunt-contrib-copy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz", - "integrity": "sha1-cGDGWB6QS4qw0A8HbgqPbj58NXM=", - "dev": true, - "requires": { - "chalk": "^1.1.1", - "file-sync-cmp": "^0.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "grunt-contrib-cssmin": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-cssmin/-/grunt-contrib-cssmin-3.0.0.tgz", - "integrity": "sha512-eXpooYmVGKMs/xV7DzTLgJFPVOfMuawPD3x0JwhlH0mumq2NtH3xsxaHxp1Y3NKxp0j0tRhFS6kSBRsz6TuTGg==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "clean-css": "~4.2.1", - "maxmin": "^2.1.0" - } - }, - "grunt-contrib-uglify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-4.0.1.tgz", - "integrity": "sha512-dwf8/+4uW1+7pH72WButOEnzErPGmtUvc8p08B0eQS/6ON0WdeQu0+WFeafaPTbbY1GqtS25lsHWaDeiTQNWPg==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "maxmin": "^2.1.0", - "uglify-js": "^3.5.0", - "uri-path": "^1.0.0" - } - }, - "grunt-contrib-watch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-1.1.0.tgz", - "integrity": "sha512-yGweN+0DW5yM+oo58fRu/XIRrPcn3r4tQx+nL7eMRwjpvk+rQY6R8o94BPK0i2UhTg9FN21hS+m8vR8v9vXfeg==", - "dev": true, - "requires": { - "async": "^2.6.0", - "gaze": "^1.1.0", - "lodash": "^4.17.10", - "tiny-lr": "^1.1.1" - }, - "dependencies": { - "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - } - } - } - }, - "grunt-known-options": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-1.1.1.tgz", - "integrity": "sha512-cHwsLqoighpu7TuYj5RonnEuxGVFnztcUqTqp5rXFGYL4OuPFofwC4Ycg7n9fYwvK6F5WbYgeVOwph9Crs2fsQ==", - "dev": true - }, - "grunt-legacy-log": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz", - "integrity": "sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA==", - "dev": true, - "requires": { - "colors": "~1.1.2", - "grunt-legacy-log-utils": "~2.1.0", - "hooker": "~0.2.3", - "lodash": "~4.17.19" - } - }, - "grunt-legacy-log-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz", - "integrity": "sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==", - "dev": true, - "requires": { - "chalk": "~4.1.0", - "lodash": "~4.17.19" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "grunt-legacy-util": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.0.tgz", - "integrity": "sha512-ZEmYFB44bblwPE2oz3q3ygfF6hseQja9tx8I3UZIwbUik32FMWewA+d1qSFicMFB+8dNXDkh35HcDCWlpRsGlA==", - "dev": true, - "requires": { - "async": "~1.5.2", - "exit": "~0.1.1", - "getobject": "~0.1.0", - "hooker": "~0.2.3", - "lodash": "~4.17.20", - "underscore.string": "~3.3.5", - "which": "~1.3.0" - }, - "dependencies": { - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "grunt-newer": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/grunt-newer/-/grunt-newer-1.3.0.tgz", - "integrity": "sha1-g8y3od2ny9irI7BZAk6+YUrS80I=", - "dev": true, - "requires": { - "async": "^1.5.2", - "rimraf": "^2.5.2" - } - }, - "grunt-phpcs": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/grunt-phpcs/-/grunt-phpcs-0.4.0.tgz", - "integrity": "sha1-oI1iX8ZEZeRTsr2T+BCyqB6Uvao=", - "dev": true - }, - "grunt-postcss": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/grunt-postcss/-/grunt-postcss-0.9.0.tgz", - "integrity": "sha512-lglLcVaoOIqH0sFv7RqwUKkEFGQwnlqyAKbatxZderwZGV1nDyKHN7gZS9LUiTx1t5GOvRBx0BEalHMyVwFAIA==", - "dev": true, - "requires": { - "chalk": "^2.1.0", - "diff": "^3.0.0", - "postcss": "^6.0.11" - }, - "dependencies": { - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "grunt-rtlcss": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/grunt-rtlcss/-/grunt-rtlcss-2.0.2.tgz", - "integrity": "sha512-WbI2thnwlF04N+xvJu+NxqEaCyPuLyar196SYhEQFZ2EJHkOS8YYE+Zkh+X9cWhwAtKp7ZEpR/IKXcyQggOIsQ==", - "dev": true, - "requires": { - "chalk": "^1.0.0", - "rtlcss": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "grunt-sass": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/grunt-sass/-/grunt-sass-3.1.0.tgz", - "integrity": "sha512-90s27H7FoCDcA8C8+R0GwC+ntYD3lG6S/jqcavWm3bn9RiJTmSfOvfbFa1PXx4NbBWuiGQMLfQTj/JvvqT5w6A==", - "dev": true - }, - "grunt-stylelint": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/grunt-stylelint/-/grunt-stylelint-0.16.0.tgz", - "integrity": "sha512-ullm0h9iCdgPEDq1TNwKL5HteXA4zke6wbYoRtsO32ATCU3zfUXmDN9unhu+joEcdgJKOPcd2+7UhRNXO1rr+w==", - "dev": true, - "requires": { - "chalk": "^4.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "gruntify-eslint": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/gruntify-eslint/-/gruntify-eslint-5.0.0.tgz", - "integrity": "sha512-pa2sXHK9+U4dCGdGSIMkpJARNwRStdLBsddNxmSHSSWROUdhWMrXvFWm6pj48zJhyV3Qy068VIuF1seYIvc0cw==", - "dev": true, - "requires": { - "eslint": "^5.0.0" - }, - "dependencies": { - "acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", - "dev": true - }, - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "cli-width": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", - "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", - "dev": true - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "eslint": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", - "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.9.1", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^4.0.3", - "eslint-utils": "^1.3.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^5.0.1", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob": "^7.1.2", - "globals": "^11.7.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^6.2.2", - "js-yaml": "^3.13.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.11", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "path-is-inside": "^1.0.2", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^5.5.1", - "strip-ansi": "^4.0.0", - "strip-json-comments": "^2.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "espree": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", - "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", - "dev": true, - "requires": { - "acorn": "^6.0.7", - "acorn-jsx": "^5.0.0", - "eslint-visitor-keys": "^1.0.0" - } - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "inquirer": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", - "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", - "dev": true, - "requires": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", - "dev": true - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "gzip-size": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz", - "integrity": "sha1-VGGI6b3DN/Zzdy+BZgRks4nc5SA=", - "dev": true, - "requires": { - "duplexer": "^0.1.1" - } - }, - "handlebars": { - "version": "4.7.6", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", - "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", - "dev": true, - "requires": { - "minimist": "^1.2.5", - "neo-async": "^2.6.0", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4", - "wordwrap": "^1.0.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "dev": true, - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - } - }, - "hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - } - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-symbols": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dev": true, - "requires": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - } - } - }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dev": true, - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "dev": true, - "requires": { - "parse-passwd": "^1.0.0" - } - }, - "hooker": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", - "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", - "dev": true - }, - "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", - "dev": true - }, - "html-element-map": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.2.0.tgz", - "integrity": "sha512-0uXq8HsuG1v2TmQ8QkIhzbrqeskE4kn52Q18QJ9iAA/SnHoEKXWiUxHQtclRsCFWEUD2So34X+0+pZZu862nnw==", - "dev": true, - "requires": { - "array-filter": "^1.0.0" - } - }, - "html-encoding-sniffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", - "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", - "dev": true, - "requires": { - "whatwg-encoding": "^1.0.1" - } - }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "html-tags": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz", - "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==", - "dev": true - }, - "htmlparser2": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", - "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", - "dev": true, - "requires": { - "domelementtype": "^1.3.1", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^3.1.1" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "http-cache-semantics": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", - "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", - "dev": true - }, - "http-parser-js": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.2.tgz", - "integrity": "sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ==", - "dev": true - }, - "http-proxy-agent": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", - "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", - "dev": true, - "requires": { - "agent-base": "4", - "debug": "3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", - "dev": true - }, - "https-proxy-agent": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", - "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", - "dev": true, - "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", - "dev": true - }, - "humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", - "dev": true, - "requires": { - "ms": "^2.0.0" - } - }, - "husky": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.0.tgz", - "integrity": "sha512-tTMeLCLqSBqnflBZnlVDhpaIMucSGaYyX6855jM4AguGeWCeSzNdb1mfyWduTZ3pe3SJVvVWGL0jO1iKZVPfTA==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "ci-info": "^2.0.0", - "compare-versions": "^3.6.0", - "cosmiconfig": "^7.0.0", - "find-versions": "^3.2.0", - "opencollective-postinstall": "^2.0.2", - "pkg-dir": "^4.2.0", - "please-upgrade-node": "^3.2.0", - "slash": "^3.0.0", - "which-pm-runs": "^1.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "cosmiconfig": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", - "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "parse-json": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", - "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "iconv-lite": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", - "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true - }, - "iferr": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", - "dev": true - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "ignore-walk": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", - "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", - "dev": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "import-fresh": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", - "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "import-lazy": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", - "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", - "dev": true - }, - "import-local": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", - "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", - "dev": true, - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - } - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "in-publish": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.1.tgz", - "integrity": "sha512-oDM0kUSNFC31ShNxHKUyfZKy8ZeXZBWMjMdZHKLOk13uvT27VTL/QzRGfRUcevJhpkZAvlhPYuXkF7eNWrtyxQ==", - "dev": true - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "indexes-of": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", - "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", - "dev": true - }, - "infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true - }, - "init-package-json": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/init-package-json/-/init-package-json-1.10.3.tgz", - "integrity": "sha512-zKSiXKhQveNteyhcj1CoOP8tqp1QuxPIPBl8Bid99DGLFqA1p87M6lNgfjJHSBoWJJlidGOv5rWjyYKEB3g2Jw==", - "dev": true, - "requires": { - "glob": "^7.1.1", - "npm-package-arg": "^4.0.0 || ^5.0.0 || ^6.0.0", - "promzard": "^0.3.0", - "read": "~1.0.1", - "read-package-json": "1 || 2", - "semver": "2.x || 3.x || 4 || 5", - "validate-npm-package-license": "^3.0.1", - "validate-npm-package-name": "^3.0.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "internal-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.2.tgz", - "integrity": "sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g==", - "dev": true, - "requires": { - "es-abstract": "^1.17.0-next.1", - "has": "^1.0.3", - "side-channel": "^1.0.2" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "interpret": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", - "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", - "dev": true - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "requires": { - "loose-envify": "^1.0.0" - } - }, - "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", - "dev": true - }, - "ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", - "dev": true - }, - "is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", - "dev": true, - "requires": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-alphabetical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", - "dev": true - }, - "is-alphanumeric": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz", - "integrity": "sha1-Spzvcdr0wAHB2B1j0UDPU/1oifQ=", - "dev": true - }, - "is-alphanumerical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", - "dev": true, - "requires": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "optional": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-boolean-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.1.tgz", - "integrity": "sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==", - "dev": true - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-callable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", - "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" - }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "requires": { - "ci-info": "^2.0.0" - } - }, - "is-core-module": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz", - "integrity": "sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-date-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" - }, - "is-decimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", - "dev": true - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", - "dev": true - }, - "is-docker": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", - "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==", - "dev": true, - "optional": true - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-finite": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", - "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-hexadecimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", - "dev": true - }, - "is-negative-zero": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", - "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=" - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-number-object": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz", - "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==", - "dev": true - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true - }, - "is-observable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", - "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", - "dev": true, - "requires": { - "symbol-observable": "^1.1.0" - } - }, - "is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true - }, - "is-path-inside": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", - "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==", - "dev": true - }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true - }, - "is-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", - "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", - "requires": { - "has-symbols": "^1.0.1" - } - }, - "is-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=", - "dev": true - }, - "is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", - "dev": true, - "requires": { - "is-unc-path": "^1.0.0" - } - }, - "is-ssh": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.2.tgz", - "integrity": "sha512-elEw0/0c2UscLrNG+OAorbP539E3rhliKPg+hDMWN9VwrDXfYK+4PBEykDPfxlYYtQvl84TascnQyobfQLHEhQ==", - "dev": true, - "requires": { - "protocols": "^1.1.0" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "is-string": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", - "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==" - }, - "is-subset": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", - "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", - "dev": true - }, - "is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "requires": { - "has-symbols": "^1.0.1" - } - }, - "is-text-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", - "integrity": "sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=", - "dev": true, - "requires": { - "text-extensions": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", - "dev": true, - "requires": { - "unc-path-regex": "^0.1.2" - } - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "is-whitespace-character": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz", - "integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "is-word-character": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz", - "integrity": "sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==", - "dev": true - }, - "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "optional": true, - "requires": { - "is-docker": "^2.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "istanbul": { - "version": "1.0.0-alpha.2", - "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-1.0.0-alpha.2.tgz", - "integrity": "sha1-BglrwI6Yuq10Sq5Gli2N+frGPQg=", - "dev": true, - "requires": { - "abbrev": "1.0.x", - "async": "1.x", - "istanbul-api": "^1.0.0-alpha", - "js-yaml": "3.x", - "mkdirp": "0.5.x", - "nopt": "3.x", - "which": "^1.1.1", - "wordwrap": "^1.0.0" - }, - "dependencies": { - "abbrev": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", - "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "istanbul-api": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.7.tgz", - "integrity": "sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA==", - "dev": true, - "requires": { - "async": "^2.1.4", - "fileset": "^2.0.2", - "istanbul-lib-coverage": "^1.2.1", - "istanbul-lib-hook": "^1.2.2", - "istanbul-lib-instrument": "^1.10.2", - "istanbul-lib-report": "^1.1.5", - "istanbul-lib-source-maps": "^1.2.6", - "istanbul-reports": "^1.5.1", - "js-yaml": "^3.7.0", - "mkdirp": "^0.5.1", - "once": "^1.4.0" - }, - "dependencies": { - "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - } - } - } - }, - "istanbul-lib-coverage": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", - "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", - "dev": true - }, - "istanbul-lib-hook": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz", - "integrity": "sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw==", - "dev": true, - "requires": { - "append-transform": "^0.4.0" - } - }, - "istanbul-lib-instrument": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz", - "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", - "dev": true, - "requires": { - "babel-generator": "^6.18.0", - "babel-template": "^6.16.0", - "babel-traverse": "^6.18.0", - "babel-types": "^6.18.0", - "babylon": "^6.18.0", - "istanbul-lib-coverage": "^1.2.1", - "semver": "^5.3.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "istanbul-lib-report": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz", - "integrity": "sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^1.2.1", - "mkdirp": "^0.5.1", - "path-parse": "^1.0.5", - "supports-color": "^3.1.2" - }, - "dependencies": { - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "^1.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz", - "integrity": "sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg==", - "dev": true, - "requires": { - "debug": "^3.1.0", - "istanbul-lib-coverage": "^1.2.1", - "mkdirp": "^0.5.1", - "rimraf": "^2.6.1", - "source-map": "^0.5.3" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "istanbul-reports": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.5.1.tgz", - "integrity": "sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw==", - "dev": true, - "requires": { - "handlebars": "^4.0.3" - } - }, - "jest": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest/-/jest-25.5.4.tgz", - "integrity": "sha512-hHFJROBTqZahnO+X+PMtT6G2/ztqAZJveGqz//FnWWHurizkD05PQGzRZOhF3XP6z7SJmL+5tCfW8qV06JypwQ==", - "dev": true, - "requires": { - "@jest/core": "^25.5.4", - "import-local": "^3.0.2", - "jest-cli": "^25.5.4" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "jest-cli": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-25.5.4.tgz", - "integrity": "sha512-rG8uJkIiOUpnREh1768/N3n27Cm+xPFkSNFO91tgg+8o2rXeVLStz+vkXkGr4UtzH6t1SNbjwoiswd7p4AhHTw==", - "dev": true, - "requires": { - "@jest/core": "^25.5.4", - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "import-local": "^3.0.2", - "is-ci": "^2.0.0", - "jest-config": "^25.5.4", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", - "prompts": "^2.0.1", - "realpath-native": "^2.0.0", - "yargs": "^15.3.1" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-changed-files": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-25.5.0.tgz", - "integrity": "sha512-EOw9QEqapsDT7mKF162m8HFzRPbmP8qJQny6ldVOdOVBz3ACgPm/1nAn5fPQ/NDaYhX/AHkrGwwkCncpAVSXcw==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "execa": "^3.2.0", - "throat": "^5.0.0" - }, - "dependencies": { - "execa": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz", - "integrity": "sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "p-finally": "^2.0.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "dev": true - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "p-finally": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", - "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", - "dev": true - } - } - }, - "jest-config": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-25.5.4.tgz", - "integrity": "sha512-SZwR91SwcdK6bz7Gco8qL7YY2sx8tFJYzvg216DLihTWf+LKY/DoJXpM9nTzYakSyfblbqeU48p/p7Jzy05Atg==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/test-sequencer": "^25.5.4", - "@jest/types": "^25.5.0", - "babel-jest": "^25.5.1", - "chalk": "^3.0.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.1", - "graceful-fs": "^4.2.4", - "jest-environment-jsdom": "^25.5.0", - "jest-environment-node": "^25.5.0", - "jest-get-type": "^25.2.6", - "jest-jasmine2": "^25.5.4", - "jest-regex-util": "^25.2.6", - "jest-resolve": "^25.5.1", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", - "micromatch": "^4.0.2", - "pretty-format": "^25.5.0", - "realpath-native": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "jest-diff": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", - "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "diff-sequences": "^25.2.6", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-docblock": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-25.3.0.tgz", - "integrity": "sha512-aktF0kCar8+zxRHxQZwxMy70stc9R1mOmrLsT5VO3pIT0uzGRSDAXxSlz4NqQWpuLjPpuMhPRl7H+5FRsvIQAg==", - "dev": true, - "requires": { - "detect-newline": "^3.0.0" - } - }, - "jest-each": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-25.5.0.tgz", - "integrity": "sha512-QBogUxna3D8vtiItvn54xXde7+vuzqRrEeaw8r1s+1TG9eZLVJE5ZkKoSUlqFwRjnlaA4hyKGiu9OlkFIuKnjA==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "jest-get-type": "^25.2.6", - "jest-util": "^25.5.0", - "pretty-format": "^25.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-environment-jsdom": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.5.0.tgz", - "integrity": "sha512-7Jr02ydaq4jaWMZLY+Skn8wL5nVIYpWvmeatOHL3tOcV3Zw8sjnPpx+ZdeBfc457p8jCR9J6YCc+Lga0oIy62A==", - "dev": true, - "requires": { - "@jest/environment": "^25.5.0", - "@jest/fake-timers": "^25.5.0", - "@jest/types": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-util": "^25.5.0", - "jsdom": "^15.2.1" - } - }, - "jest-environment-node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-25.5.0.tgz", - "integrity": "sha512-iuxK6rQR2En9EID+2k+IBs5fCFd919gVVK5BeND82fYeLWPqvRcFNPKu9+gxTwfB5XwBGBvZ0HFQa+cHtIoslA==", - "dev": true, - "requires": { - "@jest/environment": "^25.5.0", - "@jest/fake-timers": "^25.5.0", - "@jest/types": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-util": "^25.5.0", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "jest-get-type": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", - "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", - "dev": true - }, - "jest-haste-map": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.5.1.tgz", - "integrity": "sha512-dddgh9UZjV7SCDQUrQ+5t9yy8iEgKc1AKqZR9YDww8xsVOtzPQSMVLDChc21+g29oTRexb9/B0bIlZL+sWmvAQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "@types/graceful-fs": "^4.1.2", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.1.2", - "graceful-fs": "^4.2.4", - "jest-serializer": "^25.5.0", - "jest-util": "^25.5.0", - "jest-worker": "^25.5.0", - "micromatch": "^4.0.2", - "sane": "^4.0.3", - "walker": "^1.0.7", - "which": "^2.0.2" - }, - "dependencies": { - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "dev": true, - "optional": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "jest-jasmine2": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-25.5.4.tgz", - "integrity": "sha512-9acbWEfbmS8UpdcfqnDO+uBUgKa/9hcRh983IHdM+pKmJPL77G0sWAAK0V0kr5LK3a8cSBfkFSoncXwQlRZfkQ==", - "dev": true, - "requires": { - "@babel/traverse": "^7.1.0", - "@jest/environment": "^25.5.0", - "@jest/source-map": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "co": "^4.6.0", - "expect": "^25.5.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^25.5.0", - "jest-matcher-utils": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-runtime": "^25.5.4", - "jest-snapshot": "^25.5.1", - "jest-util": "^25.5.0", - "pretty-format": "^25.5.0", - "throat": "^5.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-leak-detector": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-25.5.0.tgz", - "integrity": "sha512-rV7JdLsanS8OkdDpZtgBf61L5xZ4NnYLBq72r6ldxahJWWczZjXawRsoHyXzibM5ed7C2QRjpp6ypgwGdKyoVA==", - "dev": true, - "requires": { - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - } - }, - "jest-matcher-utils": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz", - "integrity": "sha512-VWI269+9JS5cpndnpCwm7dy7JtGQT30UHfrnM3mXl22gHGt/b7NkjBqXfbhZ8V4B7ANUsjK18PlSBmG0YH7gjw==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "jest-diff": "^25.5.0", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-message-util": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.5.0.tgz", - "integrity": "sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@jest/types": "^25.5.0", - "@types/stack-utils": "^1.0.1", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "micromatch": "^4.0.2", - "slash": "^3.0.0", - "stack-utils": "^1.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "jest-mock": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.5.0.tgz", - "integrity": "sha512-eXWuTV8mKzp/ovHc5+3USJMYsTBhyQ+5A1Mak35dey/RG8GlM4YWVylZuGgVXinaW6tpvk/RSecmF37FKUlpXA==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0" - } - }, - "jest-pnp-resolver": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", - "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true - }, - "jest-regex-util": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", - "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", - "dev": true - }, - "jest-resolve": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-25.5.1.tgz", - "integrity": "sha512-Hc09hYch5aWdtejsUZhA+vSzcotf7fajSlPA6EZPE1RmPBAD39XtJhvHWFStid58iit4IPDLI/Da4cwdDmAHiQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "browser-resolve": "^1.11.3", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "jest-pnp-resolver": "^1.2.1", - "read-pkg-up": "^7.0.1", - "realpath-native": "^2.0.0", - "resolve": "^1.17.0", - "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-resolve-dependencies": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-25.5.4.tgz", - "integrity": "sha512-yFmbPd+DAQjJQg88HveObcGBA32nqNZ02fjYmtL16t1xw9bAttSn5UGRRhzMHIQbsep7znWvAvnD4kDqOFM0Uw==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "jest-regex-util": "^25.2.6", - "jest-snapshot": "^25.5.1" - } - }, - "jest-runner": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-25.5.4.tgz", - "integrity": "sha512-V/2R7fKZo6blP8E9BL9vJ8aTU4TH2beuqGNxHbxi6t14XzTb+x90B3FRgdvuHm41GY8ch4xxvf0ATH4hdpjTqg==", - "dev": true, - "requires": { - "@jest/console": "^25.5.0", - "@jest/environment": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-config": "^25.5.4", - "jest-docblock": "^25.3.0", - "jest-haste-map": "^25.5.1", - "jest-jasmine2": "^25.5.4", - "jest-leak-detector": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-resolve": "^25.5.1", - "jest-runtime": "^25.5.4", - "jest-util": "^25.5.0", - "jest-worker": "^25.5.0", - "source-map-support": "^0.5.6", - "throat": "^5.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-runtime": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-25.5.4.tgz", - "integrity": "sha512-RWTt8LeWh3GvjYtASH2eezkc8AehVoWKK20udV6n3/gC87wlTbE1kIA+opCvNWyyPeBs6ptYsc6nyHUb1GlUVQ==", - "dev": true, - "requires": { - "@jest/console": "^25.5.0", - "@jest/environment": "^25.5.0", - "@jest/globals": "^25.5.2", - "@jest/source-map": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "@types/yargs": "^15.0.0", - "chalk": "^3.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.4", - "jest-config": "^25.5.4", - "jest-haste-map": "^25.5.1", - "jest-message-util": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-regex-util": "^25.2.6", - "jest-resolve": "^25.5.1", - "jest-snapshot": "^25.5.1", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", - "realpath-native": "^2.0.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0", - "yargs": "^15.3.1" - }, - "dependencies": { - "@jest/globals": { - "version": "25.5.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-25.5.2.tgz", - "integrity": "sha512-AgAS/Ny7Q2RCIj5kZ+0MuKM1wbF0WMLxbCVl/GOMoCNbODRdJ541IxJ98xnZdVSZXivKpJlNPIWa3QmY0l4CXA==", - "dev": true, - "requires": { - "@jest/environment": "^25.5.0", - "@jest/types": "^25.5.0", - "expect": "^25.5.0" - } - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-serializer": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.5.0.tgz", - "integrity": "sha512-LxD8fY1lByomEPflwur9o4e2a5twSQ7TaVNLlFUuToIdoJuBt8tzHfCsZ42Ok6LkKXWzFWf3AGmheuLAA7LcCA==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.4" - } - }, - "jest-snapshot": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-25.5.1.tgz", - "integrity": "sha512-C02JE1TUe64p2v1auUJ2ze5vcuv32tkv9PyhEb318e8XOKF7MOyXdJ7kdjbvrp3ChPLU2usI7Rjxs97Dj5P0uQ==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0", - "@jest/types": "^25.5.0", - "@types/prettier": "^1.19.0", - "chalk": "^3.0.0", - "expect": "^25.5.0", - "graceful-fs": "^4.2.4", - "jest-diff": "^25.5.0", - "jest-get-type": "^25.2.6", - "jest-matcher-utils": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-resolve": "^25.5.1", - "make-dir": "^3.0.0", - "natural-compare": "^1.4.0", - "pretty-format": "^25.5.0", - "semver": "^6.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-util": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.5.0.tgz", - "integrity": "sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "make-dir": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-validate": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-25.5.0.tgz", - "integrity": "sha512-okUFKqhZIpo3jDdtUXUZ2LxGUZJIlfdYBvZb1aczzxrlyMlqdnnws9MOxezoLGhSaFc2XYaHNReNQfj5zPIWyQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "camelcase": "^5.3.1", - "chalk": "^3.0.0", - "jest-get-type": "^25.2.6", - "leven": "^3.1.0", - "pretty-format": "^25.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-watcher": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-25.5.0.tgz", - "integrity": "sha512-XrSfJnVASEl+5+bb51V0Q7WQx65dTSk7NL4yDdVjPnRNpM0hG+ncFmDYJo9O8jaSRcAitVbuVawyXCRoxGrT5Q==", - "dev": true, - "requires": { - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "ansi-escapes": "^4.2.1", - "chalk": "^3.0.0", - "jest-util": "^25.5.0", - "string-length": "^3.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-worker": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.5.0.tgz", - "integrity": "sha512-/dsSmUkIy5EBGfv/IjjqmFxrNAUpBERfGs1oHROyD7yxjG/w+t0GOJDX8O1k32ySmd7+a5IhnJU2qQFcJ4n1vw==", - "dev": true, - "requires": { - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "js-base64": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", - "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "js-yaml": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", - "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "jsdoctypeparser": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsdoctypeparser/-/jsdoctypeparser-9.0.0.tgz", - "integrity": "sha512-jrTA2jJIL6/DAEILBEh2/w9QxCuwmvNXIry39Ay/HVfhE3o2yVV0U44blYkqdHA/OKloJEqvJy0xU+GSdE2SIw==", - "dev": true - }, - "jsdom": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", - "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "acorn": "^7.1.0", - "acorn-globals": "^4.3.2", - "array-equal": "^1.0.0", - "cssom": "^0.4.1", - "cssstyle": "^2.0.0", - "data-urls": "^1.1.0", - "domexception": "^1.0.1", - "escodegen": "^1.11.1", - "html-encoding-sniffer": "^1.0.2", - "nwsapi": "^2.2.0", - "parse5": "5.1.0", - "pn": "^1.1.0", - "request": "^2.88.0", - "request-promise-native": "^1.0.7", - "saxes": "^3.1.9", - "symbol-tree": "^3.2.2", - "tough-cookie": "^3.0.1", - "w3c-hr-time": "^1.0.1", - "w3c-xmlserializer": "^1.1.2", - "webidl-conversions": "^4.0.2", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^7.0.0", - "ws": "^7.0.0", - "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - } - } - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json5": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", - "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "jsx-ast-utils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.1.0.tgz", - "integrity": "sha512-d4/UOjg+mxAWxCiF0c5UTSwyqbchkbqCvK87aBovhnh8GtysTjWmgC63tY0cJx/HzGgm9qnA147jVBdpOiQ2RA==", - "dev": true, - "requires": { - "array-includes": "^3.1.1", - "object.assign": "^4.1.1" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true - }, - "known-css-properties": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.17.0.tgz", - "integrity": "sha512-Vi3nxDGMm/z+lAaCjvAR1u+7fiv+sG6gU/iYDj5QOF8h76ytK9EW/EKfF0NeTyiGBi8Jy6Hklty/vxISrLox3w==", - "dev": true - }, - "language-subtag-registry": { - "version": "0.3.21", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", - "integrity": "sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==", - "dev": true - }, - "language-tags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", - "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", - "dev": true, - "requires": { - "language-subtag-registry": "~0.3.2" - } - }, - "lerna": { - "version": "3.22.1", - "resolved": "https://registry.npmjs.org/lerna/-/lerna-3.22.1.tgz", - "integrity": "sha512-vk1lfVRFm+UuEFA7wkLKeSF7Iz13W+N/vFd48aW2yuS7Kv0RbNm2/qcDPV863056LMfkRlsEe+QYOw3palj5Lg==", - "dev": true, - "requires": { - "@lerna/add": "3.21.0", - "@lerna/bootstrap": "3.21.0", - "@lerna/changed": "3.21.0", - "@lerna/clean": "3.21.0", - "@lerna/cli": "3.18.5", - "@lerna/create": "3.22.0", - "@lerna/diff": "3.21.0", - "@lerna/exec": "3.21.0", - "@lerna/import": "3.22.0", - "@lerna/info": "3.21.0", - "@lerna/init": "3.21.0", - "@lerna/link": "3.21.0", - "@lerna/list": "3.21.0", - "@lerna/publish": "3.22.1", - "@lerna/run": "3.21.0", - "@lerna/version": "3.22.1", - "import-local": "^2.0.0", - "npmlog": "^4.1.2" - }, - "dependencies": { - "import-local": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", - "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", - "dev": true, - "requires": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" - } - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - } - } - }, - "leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "liftoff": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz", - "integrity": "sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew=", - "dev": true, - "requires": { - "extend": "^3.0.0", - "findup-sync": "^2.0.0", - "fined": "^1.0.1", - "flagged-respawn": "^1.0.0", - "is-plain-object": "^2.0.4", - "object.map": "^1.0.0", - "rechoir": "^0.6.2", - "resolve": "^1.1.7" - }, - "dependencies": { - "findup-sync": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", - "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", - "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^3.1.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - } - }, - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", - "dev": true - }, - "lint-staged": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-9.5.0.tgz", - "integrity": "sha512-nawMob9cb/G1J98nb8v3VC/E8rcX1rryUYXVZ69aT9kde6YWX+uvNOEHY5yf2gcWcTJGiD0kqXmCnS3oD75GIA==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "commander": "^2.20.0", - "cosmiconfig": "^5.2.1", - "debug": "^4.1.1", - "dedent": "^0.7.0", - "del": "^5.0.0", - "execa": "^2.0.3", - "listr": "^0.14.3", - "log-symbols": "^3.0.0", - "micromatch": "^4.0.2", - "normalize-path": "^3.0.0", - "please-upgrade-node": "^3.1.1", - "string-argv": "^0.3.0", - "stringify-object": "^3.3.0" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", - "dev": true, - "requires": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" - } - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "execa": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-2.1.0.tgz", - "integrity": "sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^3.0.0", - "onetime": "^5.1.0", - "p-finally": "^2.0.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", - "dev": true, - "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "npm-run-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz", - "integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "p-finally": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", - "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", - "dev": true - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "listr": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz", - "integrity": "sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==", - "dev": true, - "requires": { - "@samverschueren/stream-to-observable": "^0.3.0", - "is-observable": "^1.1.0", - "is-promise": "^2.1.0", - "is-stream": "^1.1.0", - "listr-silent-renderer": "^1.1.1", - "listr-update-renderer": "^0.5.0", - "listr-verbose-renderer": "^0.5.0", - "p-map": "^2.0.0", - "rxjs": "^6.3.3" - } - }, - "listr-silent-renderer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", - "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=", - "dev": true - }, - "listr-update-renderer": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz", - "integrity": "sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "cli-truncate": "^0.2.1", - "elegant-spinner": "^1.0.1", - "figures": "^1.7.0", - "indent-string": "^3.0.0", - "log-symbols": "^1.0.2", - "log-update": "^2.3.0", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - } - }, - "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", - "dev": true - }, - "log-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", - "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", - "dev": true, - "requires": { - "chalk": "^1.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "listr-verbose-renderer": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz", - "integrity": "sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "cli-cursor": "^2.1.0", - "date-fns": "^1.27.2", - "figures": "^2.0.0" - }, - "dependencies": { - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - } - } - }, - "livereload-js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz", - "integrity": "sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==", - "dev": true - }, - "load-json-file": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", - "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.15", - "parse-json": "^4.0.0", - "pify": "^4.0.1", - "strip-bom": "^3.0.0", - "type-fest": "^0.3.0" - }, - "dependencies": { - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "type-fest": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", - "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", - "dev": true - } - } - }, - "loader-runner": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", - "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", - "dev": true - }, - "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - } - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "dev": true - }, - "lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", - "dev": true - }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true - }, - "lodash.escape": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", - "integrity": "sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=", - "dev": true - }, - "lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", - "dev": true - }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", - "dev": true - }, - "lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", - "dev": true - }, - "lodash.ismatch": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", - "integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=", - "dev": true - }, - "lodash.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", - "dev": true - }, - "lodash.set": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", - "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", - "dev": true - }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true - }, - "lodash.template": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", - "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "lodash.templatesettings": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0" - } - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true - }, - "log-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", - "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", - "dev": true, - "requires": { - "chalk": "^2.4.2" - } - }, - "log-update": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz", - "integrity": "sha1-iDKP19HOeTiykoN0bwsbwSayRwg=", - "dev": true, - "requires": { - "ansi-escapes": "^3.0.0", - "cli-cursor": "^2.0.0", - "wrap-ansi": "^3.0.1" - }, - "dependencies": { - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "wrap-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz", - "integrity": "sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0" - } - } - } - }, - "lolex": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", - "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, - "longest-streak": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz", - "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==", - "dev": true - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - } - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "macos-release": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.4.1.tgz", - "integrity": "sha512-H/QHeBIN1fIGJX517pvK8IEK53yQOW7YcEI55oYtgjDdoCQQz7eJS94qt5kNrscReEyuD/JcdFCm2XBEcGOITg==", - "dev": true - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "make-fetch-happen": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-5.0.2.tgz", - "integrity": "sha512-07JHC0r1ykIoruKO8ifMXu+xEU8qOXDFETylktdug6vJDACnP+HKevOu3PXyNPzFyTSlz8vrBYlBO1JZRe8Cag==", - "dev": true, - "requires": { - "agentkeepalive": "^3.4.1", - "cacache": "^12.0.0", - "http-cache-semantics": "^3.8.1", - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^2.2.3", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "node-fetch-npm": "^2.0.2", - "promise-retry": "^1.1.1", - "socks-proxy-agent": "^4.0.0", - "ssri": "^6.0.0" - } - }, - "make-iterator": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", - "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - } - }, - "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", - "dev": true, - "requires": { - "tmpl": "1.0.x" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz", - "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "markdown-escapes": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz", - "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==", - "dev": true - }, - "markdown-table": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz", - "integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==", - "dev": true - }, - "marked": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.6.3.tgz", - "integrity": "sha512-Fqa7eq+UaxfMriqzYLayfqAE40WN03jf+zHjT18/uXNuzjq3TY0XTbrAoPeqSJrAmPz11VuUA+kBPYOhHt9oOQ==", - "dev": true - }, - "mathml-tag-names": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", - "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", - "dev": true - }, - "maxmin": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/maxmin/-/maxmin-2.1.0.tgz", - "integrity": "sha1-TTsiCQPZXu5+t6x/qGTnLcCaMWY=", - "dev": true, - "requires": { - "chalk": "^1.0.0", - "figures": "^1.0.1", - "gzip-size": "^3.0.0", - "pretty-bytes": "^3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "mdast-util-compact": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mdast-util-compact/-/mdast-util-compact-1.0.4.tgz", - "integrity": "sha512-3YDMQHI5vRiS2uygEFYaqckibpJtKq5Sj2c8JioeOQBU6INpKbdWzfyLqFFnDwEcEnRFIdMsguzs5pC1Jp4Isg==", - "dev": true, - "requires": { - "unist-util-visit": "^1.1.0" - } - }, - "memize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/memize/-/memize-1.1.0.tgz", - "integrity": "sha512-K4FcPETOMTwe7KL2LK0orMhpOmWD2wRGwWWpbZy0fyArwsyIKR8YJVz8+efBAh3BO4zPqlSICu4vsLTRRqtFAg==", - "dev": true - }, - "memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - }, - "meow": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-7.1.1.tgz", - "integrity": "sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA==", - "dev": true, - "requires": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^2.5.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.13.1", - "yargs-parser": "^18.1.3" - }, - "dependencies": { - "type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true - } - } - }, - "merge": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", - "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", - "dev": true - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", - "dev": true - } - } - }, - "mime-db": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", - "dev": true - }, - "mime-types": { - "version": "2.1.27", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", - "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", - "dev": true, - "requires": { - "mime-db": "1.44.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - } - }, - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "dev": true, - "requires": { - "minipass": "^2.9.0" - } - }, - "mississippi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", - "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", - "dev": true, - "requires": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^3.0.0", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - } - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "mkdirp-promise": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mkdirp-promise/-/mkdirp-promise-5.0.1.tgz", - "integrity": "sha1-6bj2jlUsaKnBcTuEiD96HdA5uKE=", - "dev": true, - "requires": { - "mkdirp": "*" - } - }, - "mocha": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz", - "integrity": "sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ==", - "dev": true, - "requires": { - "ansi-colors": "3.2.3", - "browser-stdout": "1.3.1", - "chokidar": "3.3.0", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "3.0.0", - "minimatch": "3.0.4", - "mkdirp": "0.5.5", - "ms": "2.1.1", - "node-environment-flags": "1.0.6", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", - "wide-align": "1.1.3", - "yargs": "13.3.2", - "yargs-parser": "13.1.2", - "yargs-unparser": "1.6.0" - }, - "dependencies": { - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "binary-extensions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chokidar": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", - "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", - "dev": true, - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.1.1", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.2.0" - } - }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "dev": true, - "optional": true - }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "readdirp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", - "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", - "dev": true, - "requires": { - "picomatch": "^2.0.4" - } - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "modify-values": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", - "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", - "dev": true - }, - "moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", - "dev": true - }, - "moo": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz", - "integrity": "sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==", - "dev": true - }, - "move-concurrently": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", - "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", - "dev": true, - "requires": { - "aproba": "^1.1.1", - "copy-concurrently": "^1.0.0", - "fs-write-stream-atomic": "^1.0.8", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.3" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "multimatch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-3.0.0.tgz", - "integrity": "sha512-22foS/gqQfANZ3o+W7ST2x25ueHDVNWl/b9OlGcLpy/iKxjCpvcNCM51YCenUi7Mt/jAjjqv8JwZRs8YP5sRjA==", - "dev": true, - "requires": { - "array-differ": "^2.0.3", - "array-union": "^1.0.2", - "arrify": "^1.0.1", - "minimatch": "^3.0.4" - } - }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, - "mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "requires": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "nan": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", - "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", - "dev": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "nearley": { - "version": "2.19.7", - "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.19.7.tgz", - "integrity": "sha512-Y+KNwhBPcSJKeyQCFjn8B/MIe+DDlhaaDgjVldhy5xtFewIbiQgcbZV8k2gCVwkI1ZsKCnjIYZbR+0Fim5QYgg==", - "dev": true, - "requires": { - "commander": "^2.19.0", - "moo": "^0.5.0", - "railroad-diagrams": "^1.0.0", - "randexp": "0.4.6", - "semver": "^5.4.1" - }, - "dependencies": { - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node-addon-api": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", - "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", - "dev": true - }, - "node-environment-flags": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", - "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", - "dev": true, - "requires": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "dev": true - }, - "node-fetch-npm": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.4.tgz", - "integrity": "sha512-iOuIQDWDyjhv9qSDrj9aq/klt6F9z1p2otB3AV7v3zBDcL/x+OfGsvGQZZCcMZbUf4Ujw1xGNQkjvGnVT22cKg==", - "dev": true, - "requires": { - "encoding": "^0.1.11", - "json-parse-better-errors": "^1.0.0", - "safe-buffer": "^5.1.1" - } - }, - "node-gyp": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-5.1.1.tgz", - "integrity": "sha512-WH0WKGi+a4i4DUt2mHnvocex/xPLp9pYt5R6M2JdFB7pJ7Z34hveZ4nDTGTiLXCkitA9T8HFZjhinBCiVHYcWw==", - "dev": true, - "requires": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.2", - "mkdirp": "^0.5.1", - "nopt": "^4.0.1", - "npmlog": "^4.1.2", - "request": "^2.88.0", - "rimraf": "^2.6.3", - "semver": "^5.7.1", - "tar": "^4.4.12", - "which": "^1.3.1" - }, - "dependencies": { - "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "dev": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true - }, - "node-libs-browser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", - "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", - "dev": true, - "requires": { - "assert": "^1.1.1", - "browserify-zlib": "^0.2.0", - "buffer": "^4.3.0", - "console-browserify": "^1.1.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.11.0", - "domain-browser": "^1.1.1", - "events": "^3.0.0", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "0.0.1", - "process": "^0.11.10", - "punycode": "^1.2.4", - "querystring-es3": "^0.2.0", - "readable-stream": "^2.3.3", - "stream-browserify": "^2.0.1", - "stream-http": "^2.7.2", - "string_decoder": "^1.0.0", - "timers-browserify": "^2.0.4", - "tty-browserify": "0.0.0", - "url": "^0.11.0", - "util": "^0.11.0", - "vm-browserify": "^1.0.1" - }, - "dependencies": { - "buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } - } - }, - "node-modules-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", - "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", - "dev": true - }, - "node-notifier": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-6.0.0.tgz", - "integrity": "sha512-SVfQ/wMw+DesunOm5cKqr6yDcvUTDl/yc97ybGHMrteNEY6oekXpNpS3lZwgLlwz0FLgHoiW28ZpmBHUDg37cw==", - "dev": true, - "optional": true, - "requires": { - "growly": "^1.3.0", - "is-wsl": "^2.1.1", - "semver": "^6.3.0", - "shellwords": "^0.1.1", - "which": "^1.3.1" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "optional": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "optional": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "node-releases": { - "version": "1.1.61", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.61.tgz", - "integrity": "sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g==", - "dev": true - }, - "node-sass": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.14.1.tgz", - "integrity": "sha512-sjCuOlvGyCJS40R8BscF5vhVlQjNN069NtQ1gSxyK1u9iqvn6tf7O1R4GNowVZfiZUCRt5MmMs1xd+4V/7Yr0g==", - "dev": true, - "requires": { - "async-foreach": "^0.1.3", - "chalk": "^1.1.1", - "cross-spawn": "^3.0.0", - "gaze": "^1.0.0", - "get-stdin": "^4.0.1", - "glob": "^7.0.3", - "in-publish": "^2.0.0", - "lodash": "^4.17.15", - "meow": "^3.7.0", - "mkdirp": "^0.5.1", - "nan": "^2.13.2", - "node-gyp": "^3.8.0", - "npmlog": "^4.0.0", - "request": "^2.88.0", - "sass-graph": "2.2.5", - "stdout-stream": "^1.4.0", - "true-case-path": "^1.0.2" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true - }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "dev": true, - "requires": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "cross-spawn": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", - "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" - } - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true - }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true - }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "dev": true, - "requires": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - } - }, - "node-gyp": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", - "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", - "dev": true, - "requires": { - "fstream": "^1.0.0", - "glob": "^7.0.3", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "nopt": "2 || 3", - "npmlog": "0 || 1 || 2 || 3 || 4", - "osenv": "0", - "request": "^2.87.0", - "rimraf": "2", - "semver": "~5.3.0", - "tar": "^2.0.0", - "which": "1" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "dev": true, - "requires": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - } - }, - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "dev": true, - "requires": { - "get-stdin": "^4.0.1" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "tar": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", - "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", - "dev": true, - "requires": { - "block-stream": "*", - "fstream": "^1.0.12", - "inherits": "2" - } - }, - "trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - } - } - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", - "dev": true - }, - "normalize-selector": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/normalize-selector/-/normalize-selector-0.2.0.tgz", - "integrity": "sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=", - "dev": true - }, - "normalize-url": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", - "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", - "dev": true - }, - "npm-bundled": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", - "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", - "dev": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-lifecycle": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/npm-lifecycle/-/npm-lifecycle-3.1.5.tgz", - "integrity": "sha512-lDLVkjfZmvmfvpvBzA4vzee9cn+Me4orq0QF8glbswJVEbIcSNWib7qGOffolysc3teCqbbPZZkzbr3GQZTL1g==", - "dev": true, - "requires": { - "byline": "^5.0.0", - "graceful-fs": "^4.1.15", - "node-gyp": "^5.0.2", - "resolve-from": "^4.0.0", - "slide": "^1.1.6", - "uid-number": "0.0.6", - "umask": "^1.1.0", - "which": "^1.3.1" - }, - "dependencies": { - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", - "dev": true - }, - "npm-package-arg": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz", - "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==", - "dev": true, - "requires": { - "hosted-git-info": "^2.7.1", - "osenv": "^0.1.5", - "semver": "^5.6.0", - "validate-npm-package-name": "^3.0.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "npm-packlist": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", - "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", - "dev": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-pick-manifest": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-3.0.2.tgz", - "integrity": "sha512-wNprTNg+X5nf+tDi+hbjdHhM4bX+mKqv6XmPh7B5eG+QY9VARfQPfCEH013H5GqfNj6ee8Ij2fg8yk0mzps1Vw==", - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1", - "npm-package-arg": "^6.0.0", - "semver": "^5.4.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - }, - "dependencies": { - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - } - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dev": true, - "requires": { - "boolbase": "~1.0.0" - } - }, - "num2fraction": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", - "dev": true - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "nwsapi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", - "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-inspect": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", - "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" - }, - "object-is": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.3.tgz", - "integrity": "sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" - } - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, - "object.assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", - "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.0", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - } - }, - "object.defaults": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", - "dev": true, - "requires": { - "array-each": "^1.0.1", - "array-slice": "^1.0.0", - "for-own": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "object.entries": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.2.tgz", - "integrity": "sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==", - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5", - "has": "^1.0.3" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "object.fromentries": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.2.tgz", - "integrity": "sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ==", - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1", - "has": "^1.0.3" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "object.getownpropertydescriptors": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", - "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "object.map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", - "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", - "dev": true, - "requires": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "object.values": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", - "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1", - "has": "^1.0.3" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "octokit-pagination-methods": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz", - "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "opencollective-postinstall": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", - "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", - "dev": true - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", - "dev": true - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-name": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", - "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", - "dev": true, - "requires": { - "macos-release": "^2.2.0", - "windows-release": "^3.1.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "p-each-series": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.1.0.tgz", - "integrity": "sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ==", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true - }, - "p-map-series": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-map-series/-/p-map-series-1.0.0.tgz", - "integrity": "sha1-v5j+V1cFZYqeE1G++4WuTB8Hvco=", - "dev": true, - "requires": { - "p-reduce": "^1.0.0" - } - }, - "p-pipe": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz", - "integrity": "sha1-SxoROZoRUgpneQ7loMHViB1r7+k=", - "dev": true - }, - "p-queue": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-4.0.0.tgz", - "integrity": "sha512-3cRXXn3/O0o3+eVmUroJPSj/esxoEFIm0ZOno/T+NzG/VZgPOqQ8WKmlNqubSEpZmCIngEy34unkHGg83ZIBmg==", - "dev": true, - "requires": { - "eventemitter3": "^3.1.0" - } - }, - "p-reduce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", - "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=", - "dev": true - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "p-waterfall": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-waterfall/-/p-waterfall-1.0.0.tgz", - "integrity": "sha1-ftlLPOszMngjU69qrhGqn8I1uwA=", - "dev": true, - "requires": { - "p-reduce": "^1.0.0" - } - }, - "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true - }, - "parallel-transform": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", - "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", - "dev": true, - "requires": { - "cyclist": "^1.0.1", - "inherits": "^2.0.3", - "readable-stream": "^2.1.5" - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", - "dev": true, - "requires": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, - "parse-entities": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.2.tgz", - "integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==", - "dev": true, - "requires": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - } - }, - "parse-filepath": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", - "dev": true, - "requires": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" - } - }, - "parse-github-repo-url": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz", - "integrity": "sha1-nn2LslKmy2ukJZUGC3v23z28H1A=", - "dev": true - }, - "parse-json": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", - "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", - "dev": true - }, - "parse-path": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-4.0.2.tgz", - "integrity": "sha512-HSqVz6iuXSiL8C1ku5Gl1Z5cwDd9Wo0q8CoffdAghP6bz8pJa1tcMC+m4N+z6VAS8QdksnIGq1TB6EgR4vPR6w==", - "dev": true, - "requires": { - "is-ssh": "^1.3.0", - "protocols": "^1.4.0" - } - }, - "parse-url": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-5.0.2.tgz", - "integrity": "sha512-Czj+GIit4cdWtxo3ISZCvLiUjErSo0iI3wJ+q9Oi3QuMYTI6OZu+7cewMWZ+C1YAnKhYTk6/TLuhIgCypLthPA==", - "dev": true, - "requires": { - "is-ssh": "^1.3.0", - "normalize-url": "^3.3.0", - "parse-path": "^4.0.0", - "protocols": "^1.4.0" - } - }, - "parse5": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", - "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", - "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "path-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", - "dev": true, - "requires": { - "path-root-regex": "^0.1.0" - } - }, - "path-root-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", - "dev": true - }, - "pbkdf2": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", - "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", - "dev": true, - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - }, - "dependencies": { - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - } - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "php-parser": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/php-parser/-/php-parser-3.0.2.tgz", - "integrity": "sha512-a7y1+odEGsceLDLpu7oNyspZ0pK8FMWJOoim4/yd82AtnEZNLdCLZ67arnOQZ9K0lHJiSp4/7lVUpGELVxE14w==", - "dev": true - }, - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "^2.0.0" - } - }, - "pirates": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", - "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", - "dev": true, - "requires": { - "node-modules-regexp": "^1.0.0" - } - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - }, - "please-upgrade-node": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", - "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", - "dev": true, - "requires": { - "semver-compare": "^1.0.0" - } - }, - "pn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", - "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", - "dev": true - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-html": { - "version": "0.36.0", - "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-0.36.0.tgz", - "integrity": "sha512-HeiOxGcuwID0AFsNAL0ox3mW6MHH5cstWN1Z3Y+n6H+g12ih7LHdYxWwEA/QmrebctLjo79xz9ouK3MroHwOJw==", - "dev": true, - "requires": { - "htmlparser2": "^3.10.0" - } - }, - "postcss-jsx": { - "version": "0.36.4", - "resolved": "https://registry.npmjs.org/postcss-jsx/-/postcss-jsx-0.36.4.tgz", - "integrity": "sha512-jwO/7qWUvYuWYnpOb0+4bIIgJt7003pgU3P6nETBLaOyBXuTD55ho21xnals5nBrlpTIFodyd3/jBi6UO3dHvA==", - "dev": true, - "requires": { - "@babel/core": ">=7.2.2" - } - }, - "postcss-less": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-3.1.4.tgz", - "integrity": "sha512-7TvleQWNM2QLcHqvudt3VYjULVB49uiW6XzEUFmvwHzvsOEF5MwBrIXZDJQvJNFGjJQTzSzZnDoCJ8h/ljyGXA==", - "dev": true, - "requires": { - "postcss": "^7.0.14" - } - }, - "postcss-markdown": { - "version": "0.36.0", - "resolved": "https://registry.npmjs.org/postcss-markdown/-/postcss-markdown-0.36.0.tgz", - "integrity": "sha512-rl7fs1r/LNSB2bWRhyZ+lM/0bwKv9fhl38/06gF6mKMo/NPnp55+K1dSTosSVjFZc0e1ppBlu+WT91ba0PMBfQ==", - "dev": true, - "requires": { - "remark": "^10.0.1", - "unist-util-find-all-after": "^1.0.2" - } - }, - "postcss-media-query-parser": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", - "integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=", - "dev": true - }, - "postcss-reporter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-6.0.1.tgz", - "integrity": "sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "lodash": "^4.17.11", - "log-symbols": "^2.2.0", - "postcss": "^7.0.7" - }, - "dependencies": { - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "dev": true, - "requires": { - "chalk": "^2.0.1" - } - } - } - }, - "postcss-resolve-nested-selector": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz", - "integrity": "sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4=", - "dev": true - }, - "postcss-safe-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz", - "integrity": "sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g==", - "dev": true, - "requires": { - "postcss": "^7.0.26" - } - }, - "postcss-sass": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/postcss-sass/-/postcss-sass-0.4.4.tgz", - "integrity": "sha512-BYxnVYx4mQooOhr+zer0qWbSPYnarAy8ZT7hAQtbxtgVf8gy+LSLT/hHGe35h14/pZDTw1DsxdbrwxBN++H+fg==", - "dev": true, - "requires": { - "gonzales-pe": "^4.3.0", - "postcss": "^7.0.21" - } - }, - "postcss-scss": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-2.1.1.tgz", - "integrity": "sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA==", - "dev": true, - "requires": { - "postcss": "^7.0.6" - } - }, - "postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "dev": true, - "requires": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "dependencies": { - "dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "requires": { - "is-obj": "^2.0.0" - } - }, - "is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true - } - } - }, - "postcss-syntax": { - "version": "0.36.2", - "resolved": "https://registry.npmjs.org/postcss-syntax/-/postcss-syntax-0.36.2.tgz", - "integrity": "sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==", - "dev": true - }, - "postcss-value-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "prettier": { - "version": "npm:wp-prettier@2.0.5", - "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-2.0.5.tgz", - "integrity": "sha512-5GCgdeevIXwR3cW4Qj5XWC5MO1iSCz8+IPn0mMw6awAt/PBiey8yyO7MhePRsaMqghJAhg6Q3QLYWSnUHWkG6A==", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "pretty-bytes": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-3.0.1.tgz", - "integrity": "sha1-J9AAjXeAY6C0gRuzXHnxvV1fvM8=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "pretty-format": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", - "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^16.12.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - } - } - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", - "dev": true - }, - "promise-retry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz", - "integrity": "sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=", - "dev": true, - "requires": { - "err-code": "^1.0.0", - "retry": "^0.10.0" - } - }, - "prompts": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz", - "integrity": "sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ==", - "dev": true, - "requires": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - } - }, - "promzard": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/promzard/-/promzard-0.3.0.tgz", - "integrity": "sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=", - "dev": true, - "requires": { - "read": "1" - } - }, - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, - "prop-types-exact": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/prop-types-exact/-/prop-types-exact-1.2.0.tgz", - "integrity": "sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==", - "dev": true, - "requires": { - "has": "^1.0.3", - "object.assign": "^4.1.0", - "reflect.ownkeys": "^0.2.0" - } - }, - "proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", - "dev": true - }, - "protocols": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz", - "integrity": "sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg==", - "dev": true - }, - "protoduck": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/protoduck/-/protoduck-5.0.1.tgz", - "integrity": "sha512-WxoCeDCoCBY55BMvj4cAEjdVUFGRWed9ZxPlqTKYyw1nDDTQ4pqmnIMAGfJlg7Dx35uB/M+PHJPTmGOvaCaPTg==", - "dev": true, - "requires": { - "genfun": "^5.0.0" - } - }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "dev": true - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", - "dev": true - }, - "public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - }, - "dependencies": { - "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", - "dev": true - } - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - }, - "dependencies": { - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", - "dev": true - }, - "qs": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", - "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==", - "dev": true - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "dev": true - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", - "dev": true - }, - "quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "dev": true - }, - "raf": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", - "dev": true, - "requires": { - "performance-now": "^2.1.0" - } - }, - "railroad-diagrams": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", - "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=", - "dev": true - }, - "randexp": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", - "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", - "dev": true, - "requires": { - "discontinuous-range": "1.0.0", - "ret": "~0.1.10" - } - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "raw-body": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz", - "integrity": "sha1-HQJ8K/oRasxmI7yo8AAWVyqH1CU=", - "dev": true, - "requires": { - "bytes": "1", - "string_decoder": "0.10" - }, - "dependencies": { - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "react-native-url-polyfill": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-1.2.0.tgz", - "integrity": "sha512-hpLZ8RyS3oGVyTOe/HjoqVoCOSkeJvrCoEB3bJsY7t9uh7kpQDV6kgvdlECEafYpxe3RzMrKLVcmWRbPU7CuAw==", - "dev": true, - "requires": { - "whatwg-url-without-unicode": "8.0.0-3" - } - }, - "react-test-renderer": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.14.0.tgz", - "integrity": "sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg==", - "dev": true, - "requires": { - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "react-is": "^16.8.6", - "scheduler": "^0.19.1" - } - }, - "read": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", - "dev": true, - "requires": { - "mute-stream": "~0.0.4" - } - }, - "read-cmd-shim": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz", - "integrity": "sha512-v5yCqQ/7okKoZZkBQUAfTsQ3sVJtXdNfbPnI5cceppoxEVLYA3k+VtV2omkeo8MS94JCy4fSiUwlRBAwCVRPUA==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2" - } - }, - "read-package-json": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", - "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==", - "dev": true, - "requires": { - "glob": "^7.1.1", - "json-parse-even-better-errors": "^2.3.0", - "normalize-package-data": "^2.0.0", - "npm-normalize-package-bin": "^1.0.0" - } - }, - "read-package-tree": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/read-package-tree/-/read-package-tree-5.3.1.tgz", - "integrity": "sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw==", - "dev": true, - "requires": { - "read-package-json": "^2.0.0", - "readdir-scoped-modules": "^1.0.0", - "util-promisify": "^2.1.0" - } - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readdir-scoped-modules": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", - "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", - "dev": true, - "requires": { - "debuglog": "^1.0.1", - "dezalgo": "^1.0.0", - "graceful-fs": "^4.1.2", - "once": "^1.3.0" - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "optional": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - }, - "realpath-native": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz", - "integrity": "sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==", - "dev": true - }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "dev": true, - "requires": { - "resolve": "^1.1.6" - } - }, - "redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - } - }, - "reflect.ownkeys": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz", - "integrity": "sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=", - "dev": true - }, - "regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true - }, - "regenerate-unicode-properties": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz", - "integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==", - "dev": true, - "requires": { - "regenerate": "^1.4.0" - } - }, - "regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", - "dev": true - }, - "regenerator-transform": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", - "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.8.4" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "regexp.prototype.flags": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", - "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "regexpp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", - "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", - "dev": true - }, - "regexpu-core": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz", - "integrity": "sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==", - "dev": true, - "requires": { - "regenerate": "^1.4.0", - "regenerate-unicode-properties": "^8.2.0", - "regjsgen": "^0.5.1", - "regjsparser": "^0.6.4", - "unicode-match-property-ecmascript": "^1.0.4", - "unicode-match-property-value-ecmascript": "^1.2.0" - } - }, - "regextras": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.7.1.tgz", - "integrity": "sha512-9YXf6xtW+qzQ+hcMQXx95MOvfqXFgsKDZodX3qZB0x2n5Z94ioetIITsBtvJbiOyxa/6s9AtyweBLCdPmPko/w==", - "dev": true - }, - "regjsgen": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", - "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", - "dev": true - }, - "regjsparser": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.4.tgz", - "integrity": "sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true - } - } - }, - "remark": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/remark/-/remark-10.0.1.tgz", - "integrity": "sha512-E6lMuoLIy2TyiokHprMjcWNJ5UxfGQjaMSMhV+f4idM625UjjK4j798+gPs5mfjzDE6vL0oFKVeZM6gZVSVrzQ==", - "dev": true, - "requires": { - "remark-parse": "^6.0.0", - "remark-stringify": "^6.0.0", - "unified": "^7.0.0" - } - }, - "remark-parse": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-6.0.3.tgz", - "integrity": "sha512-QbDXWN4HfKTUC0hHa4teU463KclLAnwpn/FBn87j9cKYJWWawbiLgMfP2Q4XwhxxuuuOxHlw+pSN0OKuJwyVvg==", - "dev": true, - "requires": { - "collapse-white-space": "^1.0.2", - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-whitespace-character": "^1.0.0", - "is-word-character": "^1.0.0", - "markdown-escapes": "^1.0.0", - "parse-entities": "^1.1.0", - "repeat-string": "^1.5.4", - "state-toggle": "^1.0.0", - "trim": "0.0.1", - "trim-trailing-lines": "^1.0.0", - "unherit": "^1.0.4", - "unist-util-remove-position": "^1.0.0", - "vfile-location": "^2.0.0", - "xtend": "^4.0.1" - } - }, - "remark-stringify": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-6.0.4.tgz", - "integrity": "sha512-eRWGdEPMVudijE/psbIDNcnJLRVx3xhfuEsTDGgH4GsFF91dVhw5nhmnBppafJ7+NWINW6C7ZwWbi30ImJzqWg==", - "dev": true, - "requires": { - "ccount": "^1.0.0", - "is-alphanumeric": "^1.0.0", - "is-decimal": "^1.0.0", - "is-whitespace-character": "^1.0.0", - "longest-streak": "^2.0.1", - "markdown-escapes": "^1.0.0", - "markdown-table": "^1.1.0", - "mdast-util-compact": "^1.0.0", - "parse-entities": "^1.0.2", - "repeat-string": "^1.5.4", - "state-toggle": "^1.0.0", - "stringify-entities": "^1.0.1", - "unherit": "^1.0.4", - "xtend": "^4.0.1" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, - "requires": { - "is-finite": "^1.0.0" - } - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - } - } - }, - "request-promise-core": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", - "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", - "dev": true, - "requires": { - "lodash": "^4.17.19" - } - }, - "request-promise-native": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", - "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", - "dev": true, - "requires": { - "request-promise-core": "1.1.4", - "stealthy-require": "^1.1.1", - "tough-cookie": "^2.3.3" - }, - "dependencies": { - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - } - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "requireindex": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", - "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", - "dev": true - }, - "resolve": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", - "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "requires": { - "resolve-from": "^5.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "retry": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", - "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "rst-selector-parser": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", - "integrity": "sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=", - "dev": true, - "requires": { - "lodash.flattendeep": "^4.4.0", - "nearley": "^2.7.10" - } - }, - "rsvp": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", - "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", - "dev": true - }, - "rtlcss": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-2.6.0.tgz", - "integrity": "sha512-eRctJtYmVFgCUeGMm6uzrJE0P/1ipuaKHY9TaksDt+UZ7SLMVoatQZePPFAJeSNJ8eh3QJ/ROb44bl0wgaXksQ==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "findup": "^0.1.5", - "mkdirp": "^0.5.1", - "postcss": "^6.0.23", - "strip-json-comments": "^2.0.0" - }, - "dependencies": { - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - } - } - }, - "run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true - }, - "run-parallel": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", - "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==", - "dev": true - }, - "run-queue": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", - "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", - "dev": true, - "requires": { - "aproba": "^1.1.1" - } - }, - "rxjs": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", - "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "safe-json-parse": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz", - "integrity": "sha1-PnZyPjjf3aE8mx0poeB//uSzC1c=", - "dev": true - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sane": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", - "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", - "dev": true, - "requires": { - "@cnakazawa/watch": "^1.0.3", - "anymatch": "^2.0.0", - "capture-exit": "^2.0.0", - "exec-sh": "^0.3.2", - "execa": "^1.0.0", - "fb-watchman": "^2.0.0", - "micromatch": "^3.1.4", - "minimist": "^1.1.1", - "walker": "~1.0.5" - } - }, - "sass-graph": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz", - "integrity": "sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag==", - "dev": true, - "requires": { - "glob": "^7.0.0", - "lodash": "^4.0.0", - "scss-tokenizer": "^0.2.3", - "yargs": "^13.3.2" - }, - "dependencies": { - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "saxes": { - "version": "3.1.11", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", - "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", - "dev": true, - "requires": { - "xmlchars": "^2.1.1" - } - }, - "scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", - "dev": true, - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - }, - "scss-tokenizer": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", - "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", - "dev": true, - "requires": { - "js-base64": "^2.1.8", - "source-map": "^0.4.2" - }, - "dependencies": { - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", - "dev": true - }, - "semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", - "dev": true - }, - "semver-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz", - "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==", - "dev": true - }, - "serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "dev": true - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true, - "optional": true - }, - "side-channel": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.3.tgz", - "integrity": "sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==", - "dev": true, - "requires": { - "es-abstract": "^1.18.0-next.0", - "object-inspect": "^1.8.0" - } - }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true - }, - "sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - }, - "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - } - } - }, - "slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", - "dev": true - }, - "smart-buffer": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz", - "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==", - "dev": true - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "socks": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz", - "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==", - "dev": true, - "requires": { - "ip": "1.1.5", - "smart-buffer": "^4.1.0" - } - }, - "socks-proxy-agent": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz", - "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", - "dev": true, - "requires": { - "agent-base": "~4.2.1", - "socks": "~2.3.2" - }, - "dependencies": { - "agent-base": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", - "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", - "dev": true, - "requires": { - "es6-promisify": "^5.0.0" - } - } - } - }, - "sort-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", - "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", - "dev": true, - "requires": { - "is-plain-obj": "^1.0.0" - } - }, - "source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "dev": true, - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz", - "integrity": "sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==", - "dev": true - }, - "specificity": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/specificity/-/specificity-0.4.1.tgz", - "integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==", - "dev": true - }, - "split": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", - "dev": true, - "requires": { - "through": "2" - } - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "split2": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", - "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", - "dev": true, - "requires": { - "through2": "^2.0.2" - } - }, - "sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", - "dev": true - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1" - } - }, - "stack-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", - "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", - "dev": true - }, - "state-toggle": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz", - "integrity": "sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==", - "dev": true - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "stdout-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", - "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", - "dev": true, - "requires": { - "readable-stream": "^2.0.1" - } - }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", - "dev": true - }, - "stream-browserify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", - "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", - "dev": true, - "requires": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - }, - "stream-each": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", - "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "stream-shift": "^1.0.0" - } - }, - "stream-http": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", - "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", - "dev": true, - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "to-arraybuffer": "^1.0.0", - "xtend": "^4.0.0" - } - }, - "stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", - "dev": true - }, - "string-argv": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", - "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", - "dev": true - }, - "string-length": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz", - "integrity": "sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA==", - "dev": true, - "requires": { - "astral-regex": "^1.0.0", - "strip-ansi": "^5.2.0" - } - }, - "string-template": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", - "integrity": "sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=", - "dev": true - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, - "string.prototype.matchall": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz", - "integrity": "sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0", - "has-symbols": "^1.0.1", - "internal-slot": "^1.0.2", - "regexp.prototype.flags": "^1.3.0", - "side-channel": "^1.0.2" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "string.prototype.trim": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.2.tgz", - "integrity": "sha512-b5yrbl3BXIjHau9Prk7U0RRYcUYdN4wGSVaqoBQS50CCE3KBuYU0TYRNPFCP7aVoNMX87HKThdMRVIP3giclKg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.0" - } - }, - "string.prototype.trimend": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", - "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "string.prototype.trimstart": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", - "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "stringify-entities": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz", - "integrity": "sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==", - "dev": true, - "requires": { - "character-entities-html4": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-hexadecimal": "^1.0.0" - } - }, - "stringify-object": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", - "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", - "dev": true, - "requires": { - "get-own-enumerable-property-symbols": "^3.0.0", - "is-obj": "^1.0.1", - "is-regexp": "^1.0.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - } - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true - }, - "strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "requires": { - "min-indent": "^1.0.0" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "strong-log-transformer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz", - "integrity": "sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA==", - "dev": true, - "requires": { - "duplexer": "^0.1.1", - "minimist": "^1.2.0", - "through": "^2.3.4" - } - }, - "style-search": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", - "integrity": "sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=", - "dev": true - }, - "stylelint": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-12.0.1.tgz", - "integrity": "sha512-1mn39pqZiC/e8KUPoRMc1WMM83Upb2ILaSGxkCvKxALHutEOs2txcPQocJiXdO4Zx4FY4prGqjlkwrbthAxqig==", - "dev": true, - "requires": { - "autoprefixer": "^9.7.1", - "balanced-match": "^1.0.0", - "chalk": "^3.0.0", - "cosmiconfig": "^6.0.0", - "debug": "^4.1.1", - "execall": "^2.0.0", - "file-entry-cache": "^5.0.1", - "get-stdin": "^7.0.0", - "global-modules": "^2.0.0", - "globby": "^9.2.0", - "globjoin": "^0.1.4", - "html-tags": "^3.1.0", - "ignore": "^5.1.4", - "import-lazy": "^4.0.0", - "imurmurhash": "^0.1.4", - "known-css-properties": "^0.17.0", - "leven": "^3.1.0", - "lodash": "^4.17.15", - "log-symbols": "^3.0.0", - "mathml-tag-names": "^2.1.1", - "meow": "^5.0.0", - "micromatch": "^4.0.2", - "normalize-selector": "^0.2.0", - "postcss": "^7.0.21", - "postcss-html": "^0.36.0", - "postcss-jsx": "^0.36.3", - "postcss-less": "^3.1.4", - "postcss-markdown": "^0.36.0", - "postcss-media-query-parser": "^0.2.3", - "postcss-reporter": "^6.0.1", - "postcss-resolve-nested-selector": "^0.1.1", - "postcss-safe-parser": "^4.0.1", - "postcss-sass": "^0.4.2", - "postcss-scss": "^2.0.0", - "postcss-selector-parser": "^3.1.0", - "postcss-syntax": "^0.36.2", - "postcss-value-parser": "^4.0.2", - "resolve-from": "^5.0.0", - "slash": "^3.0.0", - "specificity": "^0.4.1", - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "style-search": "^0.1.0", - "sugarss": "^2.0.0", - "svg-tags": "^1.0.0", - "table": "^5.4.6", - "v8-compile-cache": "^2.1.0", - "write-file-atomic": "^3.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "camelcase-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", - "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", - "dev": true, - "requires": { - "camelcase": "^4.1.0", - "map-obj": "^2.0.0", - "quick-lru": "^1.0.0" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "get-stdin": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", - "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==", - "dev": true - }, - "global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "dev": true, - "requires": { - "global-prefix": "^3.0.0" - } - }, - "global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "dev": true, - "requires": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", - "dev": true - }, - "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "map-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", - "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", - "dev": true - }, - "meow": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz", - "integrity": "sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==", - "dev": true, - "requires": { - "camelcase-keys": "^4.0.0", - "decamelize-keys": "^1.0.0", - "loud-rejection": "^1.0.0", - "minimist-options": "^3.0.1", - "normalize-package-data": "^2.3.4", - "read-pkg-up": "^3.0.0", - "redent": "^2.0.0", - "trim-newlines": "^2.0.0", - "yargs-parser": "^10.0.0" - } - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "minimist-options": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", - "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "quick-lru": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", - "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", - "dev": true - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } - }, - "redent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", - "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", - "dev": true, - "requires": { - "indent-string": "^3.0.0", - "strip-indent": "^2.0.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-indent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "trim-newlines": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", - "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "yargs-parser": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", - "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", - "dev": true, - "requires": { - "camelcase": "^4.1.0" - } - } - } - }, - "stylelint-config-recommended": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-3.0.0.tgz", - "integrity": "sha512-F6yTRuc06xr1h5Qw/ykb2LuFynJ2IxkKfCMf+1xqPffkxh0S09Zc902XCffcsw/XMFq/OzQ1w54fLIDtmRNHnQ==", - "dev": true - }, - "stylelint-config-recommended-scss": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-4.2.0.tgz", - "integrity": "sha512-4bI5BYbabo/GCQ6LbRZx/ZlVkK65a1jivNNsD+ix/Lw0U3iAch+jQcvliGnnAX8SUPaZ0UqzNVNNAF3urswa7g==", - "dev": true, - "requires": { - "stylelint-config-recommended": "^3.0.0" - } - }, - "stylelint-config-wordpress": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/stylelint-config-wordpress/-/stylelint-config-wordpress-16.0.0.tgz", - "integrity": "sha512-fu8F2a3DTHjo7Id4rUbua2FprieKBDQ+jQ67XVBMsys8YyBjOd/CdcCRiWQug4sA1/A41lq0JlD2gOlR0dWmpw==", - "dev": true, - "requires": { - "stylelint-config-recommended": "^3.0.0", - "stylelint-config-recommended-scss": "^4.1.0", - "stylelint-scss": "^3.13.0" - } - }, - "stylelint-scss": { - "version": "3.18.0", - "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.18.0.tgz", - "integrity": "sha512-LD7+hv/6/ApNGt7+nR/50ft7cezKP2HM5rI8avIdGaUWre3xlHfV4jKO/DRZhscfuN+Ewy9FMhcTq0CcS0C/SA==", - "dev": true, - "requires": { - "lodash": "^4.17.15", - "postcss-media-query-parser": "^0.2.3", - "postcss-resolve-nested-selector": "^0.1.1", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, - "dependencies": { - "postcss-selector-parser": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", - "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", - "dev": true, - "requires": { - "cssesc": "^3.0.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1", - "util-deprecate": "^1.0.2" - } - } - } - }, - "sugarss": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-2.0.0.tgz", - "integrity": "sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ==", - "dev": true, - "requires": { - "postcss": "^7.0.2" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "supports-hyperlinks": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz", - "integrity": "sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==", - "dev": true, - "requires": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "svg-tags": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", - "integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=", - "dev": true - }, - "symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", - "dev": true - }, - "symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true - }, - "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", - "dev": true, - "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - }, - "dependencies": { - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - } - } - }, - "tannin": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tannin/-/tannin-1.2.0.tgz", - "integrity": "sha512-U7GgX/RcSeUETbV7gYgoz8PD7Ni4y95pgIP/Z6ayI3CfhSujwKEBlGFTCRN+Aqnuyf4AN2yHL+L8x+TCGjb9uA==", - "dev": true, - "requires": { - "@tannin/plural-forms": "^1.1.0" - } - }, - "tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "dev": true - }, - "tar": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", - "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", - "dev": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - }, - "temp-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", - "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", - "dev": true - }, - "temp-write": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/temp-write/-/temp-write-3.4.0.tgz", - "integrity": "sha1-jP9jD7fp2gXwR8dM5M5NaFRX1JI=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "is-stream": "^1.1.0", - "make-dir": "^1.0.0", - "pify": "^3.0.0", - "temp-dir": "^1.0.0", - "uuid": "^3.0.1" - }, - "dependencies": { - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, - "terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - } - }, - "terser": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", - "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", - "dev": true, - "requires": { - "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" - }, - "dependencies": { - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "terser-webpack-plugin": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", - "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", - "dev": true, - "requires": { - "cacache": "^12.0.2", - "find-cache-dir": "^2.1.0", - "is-wsl": "^1.1.0", - "schema-utils": "^1.0.0", - "serialize-javascript": "^4.0.0", - "source-map": "^0.6.1", - "terser": "^4.1.2", - "webpack-sources": "^1.4.0", - "worker-farm": "^1.7.0" - }, - "dependencies": { - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, - "text-extensions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", - "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", - "dev": true - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "requires": { - "any-promise": "^1.0.0" - } - }, - "thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", - "dev": true, - "requires": { - "thenify": ">= 3.1.0 < 4" - } - }, - "throat": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", - "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "timers-browserify": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", - "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", - "dev": true, - "requires": { - "setimmediate": "^1.0.4" - } - }, - "tiny-lr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz", - "integrity": "sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==", - "dev": true, - "requires": { - "body": "^5.1.0", - "debug": "^3.1.0", - "faye-websocket": "~0.10.0", - "livereload-js": "^2.3.0", - "object-assign": "^4.1.0", - "qs": "^6.4.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", - "dev": true - }, - "to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "tough-cookie": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", - "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", - "dev": true, - "requires": { - "ip-regex": "^2.1.0", - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "trim": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", - "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=", - "dev": true - }, - "trim-newlines": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz", - "integrity": "sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==", - "dev": true - }, - "trim-off-newlines": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", - "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=", - "dev": true - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true - }, - "trim-trailing-lines": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz", - "integrity": "sha512-4ku0mmjXifQcTVfYDfR5lpgV7zVqPg6zV9rdZmwOPqq0+Zq19xDqEgagqVbc4pOOShbncuAOIs59R3+3gcF3ZA==", - "dev": true - }, - "trough": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", - "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", - "dev": true - }, - "true-case-path": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", - "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", - "dev": true, - "requires": { - "glob": "^7.1.2" - } - }, - "tslib": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz", - "integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw==", - "dev": true - }, - "tsutils": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", - "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "typescript": { - "version": "3.9.7", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", - "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", - "dev": true - }, - "uglify-js": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.11.1.tgz", - "integrity": "sha512-OApPSuJcxcnewwjSGGfWOjx3oix5XpmrK9Z2j0fTRlHGoZ49IU6kExfZTM0++fCArOOCet+vIfWwFHbvWqwp6g==", - "dev": true - }, - "uid-number": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", - "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=", - "dev": true - }, - "umask": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/umask/-/umask-1.1.0.tgz", - "integrity": "sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=", - "dev": true - }, - "unc-path-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", - "dev": true - }, - "underscore.string": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz", - "integrity": "sha512-g+dpmgn+XBneLmXXo+sGlW5xQEt4ErkS3mgeN2GFbremYeMBSJKr9Wf2KJplQVaiPY/f7FN6atosWYNm9ovrYg==", - "dev": true, - "requires": { - "sprintf-js": "^1.0.3", - "util-deprecate": "^1.0.2" - } - }, - "unherit": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", - "integrity": "sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==", - "dev": true, - "requires": { - "inherits": "^2.0.0", - "xtend": "^4.0.0" - } - }, - "unicode-canonical-property-names-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", - "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", - "dev": true - }, - "unicode-match-property-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", - "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", - "dev": true, - "requires": { - "unicode-canonical-property-names-ecmascript": "^1.0.4", - "unicode-property-aliases-ecmascript": "^1.0.4" - } - }, - "unicode-match-property-value-ecmascript": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz", - "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==", - "dev": true - }, - "unicode-property-aliases-ecmascript": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", - "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==", - "dev": true - }, - "unified": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/unified/-/unified-7.1.0.tgz", - "integrity": "sha512-lbk82UOIGuCEsZhPj8rNAkXSDXd6p0QLzIuSsCdxrqnqU56St4eyOB+AlXsVgVeRmetPTYydIuvFfpDIed8mqw==", - "dev": true, - "requires": { - "@types/unist": "^2.0.0", - "@types/vfile": "^3.0.0", - "bail": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^1.1.0", - "trough": "^1.0.0", - "vfile": "^3.0.0", - "x-is-string": "^0.1.0" - } - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "uniq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", - "dev": true - }, - "unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "dev": true, - "requires": { - "unique-slug": "^2.0.0" - } - }, - "unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4" - } - }, - "unist-util-find-all-after": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/unist-util-find-all-after/-/unist-util-find-all-after-1.0.5.tgz", - "integrity": "sha512-lWgIc3rrTMTlK1Y0hEuL+k+ApzFk78h+lsaa2gHf63Gp5Ww+mt11huDniuaoq1H+XMK2lIIjjPkncxXcDp3QDw==", - "dev": true, - "requires": { - "unist-util-is": "^3.0.0" - } - }, - "unist-util-is": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", - "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==", - "dev": true - }, - "unist-util-remove-position": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.4.tgz", - "integrity": "sha512-tLqd653ArxJIPnKII6LMZwH+mb5q+n/GtXQZo6S6csPRs5zB0u79Yw8ouR3wTw8wxvdJFhpP6Y7jorWdCgLO0A==", - "dev": true, - "requires": { - "unist-util-visit": "^1.1.0" - } - }, - "unist-util-stringify-position": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", - "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", - "dev": true, - "requires": { - "@types/unist": "^2.0.2" - } - }, - "unist-util-visit": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz", - "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", - "dev": true, - "requires": { - "unist-util-visit-parents": "^2.0.0" - } - }, - "unist-util-visit-parents": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz", - "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", - "dev": true, - "requires": { - "unist-util-is": "^3.0.0" - } - }, - "universal-user-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.1.tgz", - "integrity": "sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg==", - "dev": true, - "requires": { - "os-name": "^3.1.0" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - } - } - }, - "upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true - }, - "uri-js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", - "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "uri-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/uri-path/-/uri-path-1.0.0.tgz", - "integrity": "sha1-l0fwGDWJM8Md4PzP2C0TjmcmLjI=", - "dev": true - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - } - } - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "util": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", - "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", - "dev": true, - "requires": { - "inherits": "2.0.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "util-promisify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/util-promisify/-/util-promisify-2.1.0.tgz", - "integrity": "sha1-PCI2R2xNMsX/PEcAKt18E7moKlM=", - "dev": true, - "requires": { - "object.getownpropertydescriptors": "^2.0.3" - } - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - }, - "v8-compile-cache": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", - "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", - "dev": true - }, - "v8-to-istanbul": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-4.1.4.tgz", - "integrity": "sha512-Rw6vJHj1mbdK8edjR7+zuJrpDtKIgNdAvTSAcpYfgMIw+u2dPDntD3dgN4XQFLU2/fvFQdzj+EeSGfd/jnY5fQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0", - "source-map": "^0.7.3" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } - } - }, - "v8flags": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", - "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "validate-npm-package-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", - "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", - "dev": true, - "requires": { - "builtins": "^1.0.3" - } - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "vfile": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-3.0.1.tgz", - "integrity": "sha512-y7Y3gH9BsUSdD4KzHsuMaCzRjglXN0W2EcMf0gpvu6+SbsGhMje7xDc8AEoeXy6mIwCKMI6BkjMsRjzQbhMEjQ==", - "dev": true, - "requires": { - "is-buffer": "^2.0.0", - "replace-ext": "1.0.0", - "unist-util-stringify-position": "^1.0.0", - "vfile-message": "^1.0.0" - }, - "dependencies": { - "is-buffer": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", - "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", - "dev": true - }, - "unist-util-stringify-position": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz", - "integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==", - "dev": true - }, - "vfile-message": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-1.1.1.tgz", - "integrity": "sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==", - "dev": true, - "requires": { - "unist-util-stringify-position": "^1.1.1" - } - } - } - }, - "vfile-location": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.6.tgz", - "integrity": "sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA==", - "dev": true - }, - "vfile-message": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", - "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", - "dev": true, - "requires": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^2.0.0" - } - }, - "vm-browserify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", - "dev": true - }, - "w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "dev": true, - "requires": { - "browser-process-hrtime": "^1.0.0" - } - }, - "w3c-xmlserializer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", - "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", - "dev": true, - "requires": { - "domexception": "^1.0.1", - "webidl-conversions": "^4.0.2", - "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - } - } - }, - "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", - "dev": true, - "requires": { - "makeerror": "1.0.x" - } - }, - "watchpack": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.4.tgz", - "integrity": "sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==", - "dev": true, - "requires": { - "chokidar": "^3.4.1", - "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0", - "watchpack-chokidar2": "^2.0.0" - } - }, - "watchpack-chokidar2": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz", - "integrity": "sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA==", - "dev": true, - "optional": true, - "requires": { - "chokidar": "^2.1.8" - }, - "dependencies": { - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "optional": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - } - } - }, - "wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", - "dev": true, - "requires": { - "defaults": "^1.0.3" - } - }, - "webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "dev": true - }, - "webpack": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.44.2.tgz", - "integrity": "sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/wasm-edit": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "acorn": "^6.4.1", - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^4.3.0", - "eslint-scope": "^4.0.3", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^2.4.0", - "loader-utils": "^1.2.3", - "memory-fs": "^0.4.1", - "micromatch": "^3.1.10", - "mkdirp": "^0.5.3", - "neo-async": "^2.6.1", - "node-libs-browser": "^2.2.1", - "schema-utils": "^1.0.0", - "tapable": "^1.1.3", - "terser-webpack-plugin": "^1.4.3", - "watchpack": "^1.7.4", - "webpack-sources": "^1.4.1" - }, - "dependencies": { - "acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", - "dev": true - }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - } - } - }, - "webpack-cli": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.12.tgz", - "integrity": "sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "cross-spawn": "^6.0.5", - "enhanced-resolve": "^4.1.1", - "findup-sync": "^3.0.0", - "global-modules": "^2.0.0", - "import-local": "^2.0.0", - "interpret": "^1.4.0", - "loader-utils": "^1.4.0", - "supports-color": "^6.1.0", - "v8-compile-cache": "^2.1.1", - "yargs": "^13.3.2" - }, - "dependencies": { - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "findup-sync": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", - "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", - "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - } - }, - "global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "dev": true, - "requires": { - "global-prefix": "^3.0.0" - } - }, - "global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "dev": true, - "requires": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - } - }, - "import-local": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", - "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", - "dev": true, - "requires": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" - } - }, - "interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "dev": true, - "requires": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - } - }, - "websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true - }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dev": true, - "requires": { - "iconv-lite": "0.4.24" - }, - "dependencies": { - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - } - } - }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true - }, - "whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - }, - "dependencies": { - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - } - } - }, - "whatwg-url-without-unicode": { - "version": "8.0.0-3", - "resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz", - "integrity": "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==", - "dev": true, - "requires": { - "buffer": "^5.4.3", - "punycode": "^2.1.1", - "webidl-conversions": "^5.0.0" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "which-pm-runs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", - "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", - "dev": true - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "requires": { - "string-width": "^1.0.2 || 2" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "windows-release": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.3.3.tgz", - "integrity": "sha512-OSOGH1QYiW5yVor9TtmXKQvt2vjQqbYS+DqmsZw+r7xDwLXEeT3JGW0ZppFmHx4diyXmxt238KFR3N9jzevBRg==", - "dev": true, - "requires": { - "execa": "^1.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - }, - "worker-farm": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", - "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", - "dev": true, - "requires": { - "errno": "~0.1.7" - } - }, - "wp-textdomain": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wp-textdomain/-/wp-textdomain-1.0.1.tgz", - "integrity": "sha512-6Guapw25yCmnQHyz62TEi1OvRnIzGfyj0sVaPBhwx19QoxeD6HI2zZHWeBIUXSauJK3BIyxWPYnxlwmnqHUskg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "glob": "^7.1.3", - "moment": "^2.24.0", - "php-parser": "^3.0.0-prerelease.8", - "text-table": "^0.2.0" - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "write-json-file": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/write-json-file/-/write-json-file-3.2.0.tgz", - "integrity": "sha512-3xZqT7Byc2uORAatYiP3DHUUAVEkNOswEWNs9H5KXiicRTvzYzYqKjYc4G7p+8pltvAw641lVByKVtMpf+4sYQ==", - "dev": true, - "requires": { - "detect-indent": "^5.0.0", - "graceful-fs": "^4.1.15", - "make-dir": "^2.1.0", - "pify": "^4.0.1", - "sort-keys": "^2.0.0", - "write-file-atomic": "^2.4.2" - }, - "dependencies": { - "detect-indent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", - "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=", - "dev": true - }, - "write-file-atomic": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - } - } - }, - "write-pkg": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/write-pkg/-/write-pkg-3.2.0.tgz", - "integrity": "sha512-tX2ifZ0YqEFOF1wjRW2Pk93NLsj02+n1UP5RvO6rCs0K6R2g1padvf006cY74PQJKMGS2r42NK7FD0dG6Y6paw==", - "dev": true, - "requires": { - "sort-keys": "^2.0.0", - "write-json-file": "^2.2.0" - }, - "dependencies": { - "detect-indent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", - "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=", - "dev": true - }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "write-file-atomic": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "write-json-file": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/write-json-file/-/write-json-file-2.3.0.tgz", - "integrity": "sha1-K2TIozAE1UuGmMdtWFp3zrYdoy8=", - "dev": true, - "requires": { - "detect-indent": "^5.0.0", - "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "pify": "^3.0.0", - "sort-keys": "^2.0.0", - "write-file-atomic": "^2.0.0" - } - } - } - }, - "ws": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz", - "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==", - "dev": true - }, - "x-is-string": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", - "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=", - "dev": true - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, - "xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "yaml": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", - "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==", - "dev": true - }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "yargs-unparser": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", - "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", - "dev": true, - "requires": { - "flat": "^4.1.0", - "lodash": "^4.17.15", - "yargs": "^13.3.0" - }, - "dependencies": { - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - } - } -} diff --git a/package.json b/package.json index 210c07c286d..2ff6a3d8460 100644 --- a/package.json +++ b/package.json @@ -1,143 +1,51 @@ { - "name": "woocommerce", - "title": "WooCommerce", - "version": "5.1.0", + "name": "woocommerce-monorepo", + "title": "WooCommerce Monorepo", + "description": "Monorepo for the WooCommerce ecosystem", "homepage": "https://woocommerce.com/", + "private": true, "repository": { "type": "git", "url": "https://github.com/woocommerce/woocommerce.git" }, - "license": "GPL-3.0+", - "main": "Gruntfile.js", - "config": { - "wp_org_slug": "woocommerce" + "author": "Automattic", + "license": "GPL-3.0-or-later", + "bugs": { + "url": "https://github.com/woocommerce/woocommerce/issues" }, "scripts": { - "install": " if [ -z \"$SKIP_LERNA_BOOTSTRAP\" ]; then npx lerna bootstrap --hoist; fi", - "check:subset-installed": "npm list --depth 1 install-subset > /dev/null 2>&1", - "install:subset-only": "npm install --no-package-lock --no-save install-subset", - "install:no-e2e": "npm run check:subset-installed --silent || npm run install:subset-only && SKIP_LERNA_BOOTSTRAP=true npx install-subset i no-e2e", - "build": "./bin/build-zip.sh", - "build:core": "grunt && npm run makepot", - "build:dev": "npm run build:core && npm run build:packages", - "build-watch": "grunt watch", - "build:packages": "lerna run build", - "build:zip": "npm run build", - "build:assets": "grunt assets", - "lint:js": "eslint assets/js --ext=js", - "docker:down": "npx wc-e2e docker:down", - "docker:ssh": "npx wc-e2e docker:ssh", - "docker:up": "npx wc-e2e docker:up", - "test:e2e": "npx wc-e2e test:e2e", - "test:e2e-debug": "npx wc-e2e test:e2e-debug", - "test:e2e-dev": "npx wc-e2e test:e2e-dev", - "test:unit": "./vendor/bin/phpunit -c ./phpunit.xml", - "makepot": "composer run-script makepot", - "packages:fix:textdomain": "node ./bin/package-update-textdomain.js", - "publish-packages": "lerna publish from-package", - "git:update-hooks": "rm -r .git/hooks && mkdir -p .git/hooks && node ./node_modules/husky/husky.js install" + "preinstall": "npx only-allow pnpm", + "postinstall": "pnpm git:update-hooks", + "git:update-hooks": "rm -r .git/hooks && mkdir -p .git/hooks && husky install" }, "devDependencies": { - "@babel/cli": "7.12.8", + "@automattic/nx-composer": "^0.1.0", + "@nrwl/cli": "^13.3.4", + "@nrwl/devkit": "^13.1.4", + "@nrwl/linter": "^13.3.4", + "@nrwl/tao": "13.3.4", + "@nrwl/web": "^13.3.4", + "@nrwl/workspace": "^13.3.4", + "@types/node": "14.14.33", + "@woocommerce/eslint-plugin": "workspace:*", + "@wordpress/eslint-plugin": "^11.0.0", + "@wordpress/prettier-config": "^1.1.1", + "chalk": "^4.1.2", + "glob": "^7.2.0", + "husky": "^7.0.4", + "jest": "^27.3.1", + "lint-staged": "^12.3.7", + "mkdirp": "^1.0.4", + "node-stream-zip": "^1.15.0", + "prettier": "npm:wp-prettier@^2.2.1-beta-1", + "request": "^2.88.2", + "typescript": "4.2.4" + }, + "dependencies": { "@babel/core": "7.12.9", - "@babel/polyfill": "7.12.1", - "@babel/preset-env": "7.12.7", - "@babel/register": "7.12.1", - "@typescript-eslint/eslint-plugin": "3.10.1", - "@typescript-eslint/experimental-utils": "3.10.1", - "@typescript-eslint/parser": "3.10.1", - "@woocommerce/api": "file:tests/e2e/api", - "@woocommerce/e2e-core-tests": "file:tests/e2e/core-tests", - "@woocommerce/e2e-environment": "file:tests/e2e/env", - "@woocommerce/e2e-utils": "file:tests/e2e/utils", - "@wordpress/babel-plugin-import-jsx-pragma": "1.1.3", - "@wordpress/babel-preset-default": "3.0.2", - "@wordpress/e2e-test-utils": "^4.6.0", - "@wordpress/eslint-plugin": "7.3.0", - "autoprefixer": "9.8.6", - "babel-eslint": "10.1.0", - "chai": "4.2.0", - "chai-as-promised": "7.1.1", - "commander": "4.1.1", - "config": "3.3.3", - "cross-env": "6.0.3", - "deasync": "0.1.21", - "eslint": "6.8.0", - "eslint-config-wpcalypso": "5.0.0", - "eslint-plugin-jest": "23.20.0", - "github-contributors-list": "https://github.com/woocommerce/github-contributors-list/tarball/master", - "grunt": "1.3.0", - "grunt-contrib-clean": "2.0.0", - "grunt-contrib-concat": "1.0.1", - "grunt-contrib-copy": "1.0.0", - "grunt-contrib-cssmin": "3.0.0", - "grunt-contrib-uglify": "4.0.1", - "grunt-contrib-watch": "1.1.0", - "grunt-newer": "^1.3.0", - "grunt-phpcs": "0.4.0", - "grunt-postcss": "0.9.0", - "grunt-rtlcss": "2.0.2", - "grunt-sass": "3.1.0", - "grunt-stylelint": "0.16.0", - "gruntify-eslint": "5.0.0", - "husky": "4.3.0", - "istanbul": "1.0.0-alpha.2", - "jest": "^25.1.0", - "lerna": "3.22.1", - "lint-staged": "9.5.0", - "mocha": "7.2.0", - "node-sass": "4.14.1", - "prettier": "npm:wp-prettier@2.0.5", - "stylelint": "12.0.1", - "stylelint-config-wordpress": "16.0.0", - "typescript": "3.9.7", - "webpack": "4.44.2", - "webpack-cli": "3.3.12", + "@wordpress/babel-plugin-import-jsx-pragma": "^3.1.0", + "@wordpress/babel-preset-default": "^6.4.1", + "lodash": "^4.17.21", "wp-textdomain": "1.0.1" - }, - "engines": { - "node": "^10.22.0", - "npm": "^6.14.6" - }, - "husky": { - "hooks": { - "post-merge": "./bin/post-merge.sh", - "pre-commit": "lint-staged", - "pre-push": "./bin/pre-push.sh" - } - }, - "lint-staged": { - "*.php": [ - "php -d display_errors=1 -l", - "composer run-script phpcs-pre-commit" - ], - "*.scss": [ - "stylelint --syntax=scss --fix", - "git add" - ], - "*.js": [ - "eslint --fix", - "git add" - ], - "*.ts": [ - "eslint --fix", - "git add" - ] - }, - "browserslist": [ - "> 0.1%", - "ie 8", - "ie 9" - ], - "subsets": { - "no-e2e": { - "exclude": [ - "@woocommerce/api", - "@woocommerce/e2e-core-tests", - "@woocommerce/e2e-environment", - "@woocommerce/e2e-utils", - "@wordpress/e2e-test-utils" - ] - } } } diff --git a/packages/js/README.md b/packages/js/README.md new file mode 100644 index 00000000000..5f82f22a152 --- /dev/null +++ b/packages/js/README.md @@ -0,0 +1,85 @@ +# WooCommerce Packages + +Currently we have a small set of public-facing packages that can be dowloaded from [npm](https://www.npmjs.com/org/woocommerce) and used in external applications. + +- `@woocommerce/components`: A library of components that can be used to create pages in the WooCommerce dashboard and reports pages. +- `@woocommerce/csv-export`: A set of functions to convert data into CSV values, and enable a browser download of the CSV data. +- `@woocommerce/currency`: A class to display and work with currency values. +- `@woocommerce/date`: A collection of utilities to display and work with date values. +- `@woocommerce/navigation`: A collection of navigation-related functions for handling query parameter objects, serializing query parameters, updating query parameters, and triggering path changes. +- `@woocommerce/tracks`: User event tracking utility functions for Automattic based projects. + +## Working with existing packages + +- You can make changes to packages files as normal, and running `pnpm start` will compile and watch both app files and packages. +- :warning: Make sure any dependencies you add to a package are also added to that package's `package.json`, not just the woocommerce-admin package.json +- :warning: Make sure you're not importing from any woocommerce-admin files outside of the package (you can import from other packages, just use the `import from @woocommerce/package` syntax). +- Add your change to the CHANGELOG for that package under the next version number, creating one if necessary (we use semantic versioning for packages, [see these guidelines](https://github.com/WordPress/gutenberg/blob/master/CONTRIBUTING.md#maintaining-changelogs)). +- Don't change the version in `package.json`. +- Label your PR with the `Packages` label. +- Once merged, you can wait for the next package release roundup, or you can publish a release now (see below, "Publishing packages"). + +--- + +## Creating a new package + +Most of this is pulled [from the Gutenberg workflow](https://github.com/WordPress/gutenberg/blob/master/CONTRIBUTING.md#creating-new-package). + +To create a new package, add a new folder to `/packages`, containing… + +1. `package.json` based on the template: + ```json + { + "name": "@woocommerce/package-name", + "version": "1.0.0-beta.0", + "description": "Package description.", + "author": "Automattic", + "license": "GPL-2.0-or-later", + "keywords": [ "wordpress", "woocommerce" ], + "homepage": "https://github.com/woocommerce/woocommerce/tree/main/packages/[_YOUR_PACKAGE_]/README.md", + "repository": { + "type": "git", + "url": "https://github.com/woocommerce/woocommerce.git" + }, + "bugs": { + "url": "https://github.com/woocommerce/woocommerce/issues" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "react-native": "src/index", + "publishConfig": { + "access": "public" + } + } + ``` +2. `.npmrc` file which disables creating `package-lock.json` file for the package: + ``` + package-lock=false + ``` +3. `README.md` file containing at least: + - Package name + - Package description + - Installation details + - Usage example +4. A `src` directory for the source of your module, which will be built by default using the `pnpm run build:packages` command. Note that you'll want an `index.js` file that exports the package contents, see other packages for examples. + +5. Add the new package name to `packages/dependency-extraction-webpack-plugin/assets/packages.js` so that users of that plugin will also be able to use the new package without enqueuing it. + +--- + +## Publishing packages + +- Run `pnpm run publish-packages:check` to run pnpm publish with the `--dry-run` option +- Create a PR with a CHANGELOG for each updated package (or try to add to the CHANGELOG with any PR editing `packages/`) +- Run `pnpm run publish-packages:prod` to publish the package +- _OR_ Run `pnpm run publish-packages:dev` to publish "next" releases (installed as `pnpm i @woocommerce/package@next`). Only use `:dev` if you have a reason to. +- Both commands will run `build:packages` before the publishing task, just to catch any last updates. + +### Publishing a single package + +Sometimes, its helpful to release a singular package. This can be done directly through pnpm. Be sure versions and builds are correct. + +- Bump the version in the package's package.json as well as its CHANGELOG file. +- `pnpm install && pnpm run build:packages` to build packages. +- `cd packages/` +- `pnpm publish` diff --git a/packages/js/admin-e2e-tests/.eslintrc.js b/packages/js/admin-e2e-tests/.eslintrc.js new file mode 100644 index 00000000000..e4d185d8cd1 --- /dev/null +++ b/packages/js/admin-e2e-tests/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + extends: [ 'plugin:@woocommerce/eslint-plugin/recommended' ], + root: true, +}; diff --git a/tests/e2e/env/.npmrc b/packages/js/admin-e2e-tests/.npmrc similarity index 100% rename from tests/e2e/env/.npmrc rename to packages/js/admin-e2e-tests/.npmrc diff --git a/packages/js/admin-e2e-tests/CHANGELOG.md b/packages/js/admin-e2e-tests/CHANGELOG.md new file mode 100644 index 00000000000..5836c843674 --- /dev/null +++ b/packages/js/admin-e2e-tests/CHANGELOG.md @@ -0,0 +1,56 @@ +# Unreleased + +- Add E2E tests to disabled welcome modal #32505 + +- Update test for payment task. #32467 + +- Increase timeout threshold for payment task. #32605 + +# 1.0.0 + +- Add returned type annotations and remove unused vars. #8020 + +- Add E2E tests for checking store currency if it matches the onboarded country. #7712 + +- Make unchecking free features more robust. #7761 + +- Fix typescript type error in admin-e2e-tests package #7765 + +- Add extension deactivation util function addition. #7804 + +- Add tests to Subscriptions inclusion. #7804 + +- Add missing dependencies. #8349 + +- Update all js packages with minor/patch version changes. #8392 + +- Add E2E test for checking onboarding tab clickable after going back. #8469 +## Breaking changes + +- Update `@types/jest` to v27 +- Update the peer dependency constraint `@typescript-eslint/eslint-plugin` to ^5. + - eslint-plugin: ban-types no longer reports object by default. + + +# 0.1.2 + +- Add Customers to analytics pages tested #7573 +- Add `waitForTimeout` utility function #7572 +- Update analytics overview tests to allow re-running the tests. + +# 0.1.1 + +- Allow packages to be built in isolation. #7286 +- Add scope to BACS slotfill #7405 +- Update e2e matcher for tasklist header #7406 +- Update homescreen, utils, payment task, payments setup. #7338 +- Refactor package style builds #7531 +- Updated onboarding tests to include email prefill and move client setup checkbox to business step. +- Payment task update. #7577 +- Add test cases for the home screen tasklist and activity panels. #7509 +- Add wait for orders text on activity panel. #7550 +- Allow CBD to be optional in business details in E2E. #7675 + +# 0.1.0 + +- Released initial package diff --git a/packages/js/admin-e2e-tests/README.md b/packages/js/admin-e2e-tests/README.md new file mode 100644 index 00000000000..19f168934fa --- /dev/null +++ b/packages/js/admin-e2e-tests/README.md @@ -0,0 +1,56 @@ +# Admin E2E Tests + +An end-to-end test suite for WooCommerce setup, onboarding, home screen/task list, and analytics. + +## Installation + +Install the module + +```bash +pnpm install @woocommerce/admin-e2e-tests --save +``` + +## Usage + +Create a E2E test specification file under `/tests/e2e/specs/example.test.js`: + +```js +const { testAdminBasicSetup } = require( '@woocommerce/admin-e2e-tests' ); + +testAdminBasicSetup(); +``` + +See the [wooCommerce E2E Boilerplate](https://github.com/woocommerce/woocommerce-e2e-boilerplate) for instructions on setting up an E2E test environment. + +### Configuration + +Add the following entries to `tests/e2e/config/default.json` + +```json + "onboardingwizard": { + "industry": "Test industry", + "numberofproducts": "1 - 10", + "sellingelsewhere": "No" + }, + "settings": { + "shipping": { + "zonename": "United States", + "zoneregions": "United States (US)", + "shippingmethod": "Free shipping" + } + } +``` + +### Available tests + +The following test functions are included in the package: + +| Function | Description | +| --- | --- | +| `testAdminBasicSetup` | Test that WooCommerce can be activated with pretty permalinks | +| `testAdminOnboardingWizard` | Complete the onboarding wizard with US merchant | +| `testAdminNonUSRecommendedFeatures` | Complete the onboarding wizard with non-US merchant | +| `testSelectiveBundleWCPay` | Ensure onboarding wizard offers WC Payments in appropriate contexts | +| `testAdminAnalyticsPages` | Test that the React App is functional on Analytics pages | +| `testAdminCouponsPage` | Test that the Coupons is functional | +| `testAdminPaymentSetupTask` | Test that payment methods can be configured | diff --git a/packages/js/admin-e2e-tests/package.json b/packages/js/admin-e2e-tests/package.json new file mode 100644 index 00000000000..38c82638c0a --- /dev/null +++ b/packages/js/admin-e2e-tests/package.json @@ -0,0 +1,65 @@ +{ + "name": "@woocommerce/admin-e2e-tests", + "version": "1.0.0", + "author": "Automattic", + "description": "E2E tests for the new WooCommerce interface.", + "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/admin-e2e-tests/README.md", + "repository": { + "type": "git", + "url": "https://github.com/woocommerce/woocommerce.git" + }, + "keywords": [ + "woocommerce", + "e2e" + ], + "license": "GPL-3.0+", + "main": "build/index.js", + "types": "build/index.d.ts", + "files": [ + "/build/", + "!*.ts.map", + "!*.tsbuildinfo" + ], + "sideEffects": false, + "dependencies": { + "@jest/globals": "^26.6.2", + "@types/jest": "^27.4.1", + "config": "^3.3.7" + }, + "peerDependencies": { + "@woocommerce/e2e-environment": "^0.2.3 || ^0.3.0", + "@woocommerce/e2e-utils": "^0.2.0", + "puppeteer": "^2.0.0" + }, + "devDependencies": { + "@babel/core": "^7.17.5", + "@types/expect-puppeteer": "^4.4.7", + "@types/puppeteer": "^5.4.5", + "@typescript-eslint/eslint-plugin": "^5.14.0", + "@woocommerce/api": "^0.2.0", + "@wordpress/eslint-plugin": "^11.0.0", + "eslint": "^8.12.0", + "jest": "^27.5.1", + "jest-cli": "^27.5.1", + "jest-mock-extended": "^1.0.18", + "rimraf": "^3.0.2", + "ts-jest": "^27.1.3", + "typescript": "^4.6.2" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "prepare": "pnpm run build", + "build": "tsc --build", + "start": "tsc --build --watch", + "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", + "lint": "eslint src", + "prepack": "pnpm run clean && pnpm run build" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] + } +} diff --git a/packages/js/admin-e2e-tests/project.json b/packages/js/admin-e2e-tests/project.json new file mode 100644 index 00000000000..be3b68c506c --- /dev/null +++ b/packages/js/admin-e2e-tests/project.json @@ -0,0 +1,44 @@ +{ + "root": "packages/js/admin-e2e-tests", + "sourceRoot": "packages/js/admin-e2e-tests/src", + "projectType": "library", + "targets": { + "changelog": { + "executor": "./tools/executors/changelogger:changelog", + "options": { + "action": "add", + "cwd": "packages/js/admin-e2e-tests" + } + }, + "build": { + "executor": "@nrwl/workspace:run-script", + "options": { + "script": "build" + } + }, + "build-watch": { + "executor": "@nrwl/workspace:run-script", + "options": { + "script": "start" + } + }, + "test": { + "executor": "@nrwl/workspace:run-script", + "options": { + "script": "test" + } + }, + "clean": { + "executor": "@nrwl/workspace:run-script", + "options": { + "script": "clean" + } + }, + "prepare": { + "executor": "@nrwl/workspace:run-script", + "options": { + "script": "prepare" + } + } + } + } diff --git a/packages/js/admin-e2e-tests/src/constants/taskTitles.ts b/packages/js/admin-e2e-tests/src/constants/taskTitles.ts new file mode 100644 index 00000000000..fbc316e12a6 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/constants/taskTitles.ts @@ -0,0 +1,10 @@ +export const TaskTitles = { + storeDetails: 'Store details', + addPayments: 'Set up payments', + wooPayments: + 'Set up WooCommerce PaymentsBy setting up, you are agreeing to the Terms of Service2 minutes', + addProducts: 'Add my products', + taxSetup: 'Set up tax', + setUpShippingCosts: 'Set up shipping', + personalizeStore: 'Personalize my store', +}; diff --git a/packages/js/admin-e2e-tests/src/elements/BaseElement.ts b/packages/js/admin-e2e-tests/src/elements/BaseElement.ts new file mode 100644 index 00000000000..1d0c33dbabf --- /dev/null +++ b/packages/js/admin-e2e-tests/src/elements/BaseElement.ts @@ -0,0 +1,14 @@ +/** + * External dependencies + */ +import { Page } from 'puppeteer'; + +export abstract class BaseElement { + protected page: Page; + protected selector: string; + + constructor( page: Page, selector: string ) { + this.page = page; + this.selector = selector; + } +} diff --git a/packages/js/admin-e2e-tests/src/elements/DropdownField.ts b/packages/js/admin-e2e-tests/src/elements/DropdownField.ts new file mode 100644 index 00000000000..ec307998166 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/elements/DropdownField.ts @@ -0,0 +1,29 @@ +/** + * Internal dependencies + */ +import { getElementByText, getInputValue } from '../utils/actions'; +import { BaseElement } from './BaseElement'; + +export class DropdownField extends BaseElement { + async select( value: string ): Promise< void > { + const currentVal = await getInputValue( this.selector + ' input' ); + if ( currentVal !== value ) { + await this.page.click( + this.selector + ' .woocommerce-select-control__control' + ); + const button = await getElementByText( + 'button', + value, + this.selector + ); + await button?.click(); + await this.checkSelected( value ); + } + return undefined; + } + + async checkSelected( value: string ): Promise< void > { + const currentVal = await getInputValue( this.selector + ' input' ); + expect( currentVal ).toBe( value ); + } +} diff --git a/packages/js/admin-e2e-tests/src/elements/DropdownTypeaheadField.ts b/packages/js/admin-e2e-tests/src/elements/DropdownTypeaheadField.ts new file mode 100644 index 00000000000..ac2a990a18c --- /dev/null +++ b/packages/js/admin-e2e-tests/src/elements/DropdownTypeaheadField.ts @@ -0,0 +1,31 @@ +/** + * Internal dependencies + */ +import { BaseElement } from './BaseElement'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { clearAndFillInput } = require( '@woocommerce/e2e-utils' ); +/* eslint-enable @typescript-eslint/no-var-requires */ + +export class DropdownTypeaheadField extends BaseElement { + async search( text: string ): Promise< void > { + await clearAndFillInput( this.selector + '-0__control-input', text ); + } + async select( selector: string ): Promise< void > { + await this.page.click( this.selector + `__option-0-${ selector }` ); + } + + async checkSelected( value: string ): Promise< void > { + const selector = this.selector + '-0__control-input'; + await page.focus( selector ); + const field = await this.page.$( selector ); + const curValue = await field?.getProperty( 'value' ); + if ( curValue ) { + const fieldValue = ( await curValue.jsonValue() ) as string; + // Only compare alphanumeric characters + expect( fieldValue?.replace( /\W/g, '' ) ).toBe( + value.replace( /\W/g, '' ) + ); + } + } +} diff --git a/packages/js/admin-e2e-tests/src/elements/FormToggle.ts b/packages/js/admin-e2e-tests/src/elements/FormToggle.ts new file mode 100644 index 00000000000..ad7917f937e --- /dev/null +++ b/packages/js/admin-e2e-tests/src/elements/FormToggle.ts @@ -0,0 +1,70 @@ +/** + * External dependencies + */ +import type { ElementHandle } from 'puppeteer'; +/** + * Internal dependencies + */ +import { BaseElement } from './BaseElement'; +import { hasClass } from '../utils/actions'; + +export class FormToggle extends BaseElement { + // Represents a FormToggle input. Use `selector` to represent the container its found in. + async switchOn(): Promise< void > { + const container = await this.getCheckboxContainer(); + if ( container && ! ( await hasClass( container, 'is-checked' ) ) ) { + const input = await this.getCheckboxInput(); + + if ( ! input ) { + throw new Error( + `Could not find form toggle with selector ${ this.selector }` + ); + } + input.click(); + + // Wait for it to be checked. + await this.page.waitForSelector( + `${ this.selector } .components-form-toggle.is-checked` + ); + } + } + + async switchOff(): Promise< void > { + const container = await this.getCheckboxContainer(); + if ( container && ( await hasClass( container, 'is-checked' ) ) ) { + const input = await this.getCheckboxInput(); + + if ( ! input ) { + throw new Error( + `Could not find form toggle with selector ${ this.selector }` + ); + } + input.click(); + + // Wait for a not checked toggle to be present. + await page.waitForFunction( + ( selector ) => { + return document.querySelectorAll( selector ).length; + }, + {}, + `${ this.selector } .components-form-toggle:not(.is-checked)` + ); + } + } + + async getCheckboxContainer(): Promise< ElementHandle< Element > | null > { + return this.page.$( `${ this.selector } .components-form-toggle` ); + } + + async getCheckboxInput(): Promise< ElementHandle< Element > | null > { + return this.page.$( + `${ this.selector } .components-form-toggle__input` + ); + } + + async isEnabled(): Promise< void > { + await this.page.waitForSelector( + `${ this.selector } .components-form-toggle.is-checked` + ); + } +} diff --git a/packages/js/admin-e2e-tests/src/elements/HelpMenu.ts b/packages/js/admin-e2e-tests/src/elements/HelpMenu.ts new file mode 100644 index 00000000000..eb509dbcc0d --- /dev/null +++ b/packages/js/admin-e2e-tests/src/elements/HelpMenu.ts @@ -0,0 +1,38 @@ +/** + * External dependencies + */ +import { Page } from 'puppeteer'; +/** + * Internal dependencies + */ +import { getElementByText, waitForElementByText } from '../utils/actions'; +import { BaseElement } from './BaseElement'; + +export class HelpMenu extends BaseElement { + protected helpMenuId = '#contextual-help-columns'; + + constructor( page: Page ) { + super( page, '' ); + } + + async openHelpMenu(): Promise< void > { + const el = await getElementByText( 'button', 'Help' ); + await el?.click(); + } + + async openSetupWizardTab(): Promise< void > { + const el = await waitForElementByText( '*', 'Setup wizard' ); + await el?.click(); + } + + async enableTaskList(): Promise< void > { + await this.openSetupWizardTab(); + + const enableLink = await getElementByText( + '*', + 'Enable', + this.helpMenuId + ); + await enableLink?.click(); + } +} diff --git a/packages/js/admin-e2e-tests/src/elements/OrdersActivityPanel.ts b/packages/js/admin-e2e-tests/src/elements/OrdersActivityPanel.ts new file mode 100644 index 00000000000..b13a47353c4 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/elements/OrdersActivityPanel.ts @@ -0,0 +1,32 @@ +/** + * External dependencies + */ +import { ElementHandle, Page } from 'puppeteer'; +/** + * Internal dependencies + */ +import { BaseElement } from './BaseElement'; + +export class OrdersActivityPanel extends BaseElement { + constructor( page: Page ) { + super( page, '.woocommerce-order-activity-card' ); + } + + async getDisplayedOrders(): Promise< string[] > { + await this.page.waitForSelector( + '.woocommerce-order-activity-card h3' + ); + const list = await this.page.$$( + '.woocommerce-order-activity-card h3' + ); + return Promise.all( + list.map( async ( item: ElementHandle ) => { + const textContent = await page.evaluate( + ( el ) => el.textContent, + item + ); + return textContent.trim(); + } ) + ); + } +} diff --git a/packages/js/admin-e2e-tests/src/fixtures/action-scheduler.ts b/packages/js/admin-e2e-tests/src/fixtures/action-scheduler.ts new file mode 100644 index 00000000000..f114933d1fc --- /dev/null +++ b/packages/js/admin-e2e-tests/src/fixtures/action-scheduler.ts @@ -0,0 +1,13 @@ +/** + * Internal dependencies + */ +import { httpClient } from './http-client'; + +const actionSchedulerEndpoint = '/woocommerce-reset/v1/cron/run'; + +export async function runActionScheduler() { + const response = await httpClient.post( actionSchedulerEndpoint ); + if ( response.statusCode !== 404 ) { + expect( response.statusCode ).toEqual( 200 ); + } +} diff --git a/packages/js/admin-e2e-tests/src/fixtures/http-client.ts b/packages/js/admin-e2e-tests/src/fixtures/http-client.ts new file mode 100644 index 00000000000..37bba809dfc --- /dev/null +++ b/packages/js/admin-e2e-tests/src/fixtures/http-client.ts @@ -0,0 +1,15 @@ +/** + * External dependencies + */ +import { HTTPClientFactory } from '@woocommerce/api'; +/* eslint-disable @typescript-eslint/no-var-requires */ +const config = require( 'config' ); + +// Prepare the HTTP client that will be consumed by the repository. +// This is necessary so that it can make requests to the REST API. +const admin = config.get( 'users.admin' ); +const url = config.get( 'url' ); + +export const httpClient = HTTPClientFactory.build( url ) + .withBasicAuth( admin.username, admin.password ) + .create(); diff --git a/packages/js/admin-e2e-tests/src/fixtures/index.ts b/packages/js/admin-e2e-tests/src/fixtures/index.ts new file mode 100644 index 00000000000..d1c7b73662e --- /dev/null +++ b/packages/js/admin-e2e-tests/src/fixtures/index.ts @@ -0,0 +1,4 @@ +export * from './orders'; +export * from './options'; +export * from './reset'; +export * from './action-scheduler'; diff --git a/packages/js/admin-e2e-tests/src/fixtures/options.ts b/packages/js/admin-e2e-tests/src/fixtures/options.ts new file mode 100644 index 00000000000..dd9fcbbd555 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/fixtures/options.ts @@ -0,0 +1,23 @@ +/** + * Internal dependencies + */ +import { httpClient } from './http-client'; + +const optionsEndpoint = '/wc-admin/options'; + +export async function updateOption( + optionName: string, + optionValue: string +): Promise< void > { + const response = await httpClient.post( optionsEndpoint, { + [ optionName ]: optionValue, + } ); + expect( response.statusCode ).toEqual( 200 ); +} + +export async function unhideTaskList( id: string ): Promise< void > { + const response = await httpClient.post( + `/wc-admin/onboarding/tasks/${ id }/unhide` + ); + expect( response.statusCode ).toEqual( 200 ); +} diff --git a/packages/js/admin-e2e-tests/src/fixtures/orders.ts b/packages/js/admin-e2e-tests/src/fixtures/orders.ts new file mode 100644 index 00000000000..5606b90c0b0 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/fixtures/orders.ts @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { Order } from '@woocommerce/api'; +/** + * Internal dependencies + */ +import { httpClient } from './http-client'; + +const repository = Order.restRepository( httpClient ); + +export async function createOrder( status = 'completed' ): Promise< Order > { + // The repository can now be used to create models. + return await repository.create( { + paymentMethod: 'cod', + status, + } ); +} + +export async function removeAllOrders(): Promise< ( boolean | undefined )[] > { + const products = await repository.list(); + return await Promise.all( + products + .map( ( pr ) => ( pr.id ? repository.delete( pr.id ) : undefined ) ) + .filter( ( pr ) => !! pr ) + ); +} diff --git a/packages/js/admin-e2e-tests/src/fixtures/plugins.ts b/packages/js/admin-e2e-tests/src/fixtures/plugins.ts new file mode 100644 index 00000000000..f155aeecb06 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/fixtures/plugins.ts @@ -0,0 +1,70 @@ +/** + * Internal dependencies + */ +import { httpClient } from './http-client'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { utils } = require( '@woocommerce/e2e-utils' ); + +const wpPluginsEndpoint = '/wp/v2/plugins'; + +type Plugin = { + author: string; + name: string; + plugin: string; + plugin_uri: string; + status: 'active' | 'inactive'; + version: string; + description: { + raw: string; + rendered: string; + }; +}; + +export async function getPlugins(): Promise< Plugin[] > { + const response = await httpClient.get( wpPluginsEndpoint ); + expect( response.statusCode ).toEqual( 200 ); + return response.data; +} + +export async function deletePlugin( pluginName: string ) { + const response = await httpClient.delete( + wpPluginsEndpoint + '/' + pluginName + ); + expect( response.statusCode ).toEqual( 200 ); +} + +export async function deactivatePlugin( pluginName: string ) { + const response = await httpClient.post( + wpPluginsEndpoint + '/' + pluginName, + { + status: 'inactive', + } + ); + expect( response.statusCode ).toEqual( 200 ); +} + +async function deactivateAndDeletePlugin( pluginName: string ) { + await deactivatePlugin( pluginName ); + await deletePlugin( pluginName ); +} +export async function deactivateAndDeleteAllPlugins( except: string[] = [] ) { + let plugins = await getPlugins(); + const skippedPlugins = []; + const promises = []; + for ( const plugin of plugins ) { + const splitPluginName = plugin.plugin.split( '/' ); + const slug = splitPluginName[ 1 ] || splitPluginName[ 0 ]; + const slugFromName = utils.getSlug( + plugin.name.replace( ' &', '' ) + ); + if ( ! except.includes( slug ) && ! except.includes( slugFromName ) ) { + promises.push( deactivateAndDeletePlugin( plugin.plugin ) ); + } else { + skippedPlugins.push( slug ); + } + } + await Promise.all( promises ); + plugins = await getPlugins(); + expect( plugins.length ).toEqual( skippedPlugins.length ); +} diff --git a/packages/js/admin-e2e-tests/src/fixtures/reset.ts b/packages/js/admin-e2e-tests/src/fixtures/reset.ts new file mode 100644 index 00000000000..c7cc5d07c2b --- /dev/null +++ b/packages/js/admin-e2e-tests/src/fixtures/reset.ts @@ -0,0 +1,33 @@ +/** + * Internal dependencies + */ +import { httpClient } from './http-client'; +import { deactivateAndDeleteAllPlugins } from './plugins'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { utils } = require( '@woocommerce/e2e-utils' ); + +const { PLUGIN_NAME } = process.env; + +const resetEndpoint = '/woocommerce-reset/v1/state'; + +const pluginName = PLUGIN_NAME ? PLUGIN_NAME : 'WooCommerce'; +const pluginNameSlug = utils.getSlug( pluginName ); + +const skippedPlugins = [ + 'woocommerce', + 'woocommerce-admin', + 'woocommerce-reset', + 'basic-auth', + 'wp-mail-logging', + pluginNameSlug, +]; + +export async function resetWooCommerceState() { + const response = await httpClient.delete( resetEndpoint ); + expect( response.data.options ).toEqual( true ); + expect( response.data.transients ).toEqual( true ); + expect( response.data.notes ).toEqual( true ); + expect( response.statusCode ).toEqual( 200 ); + await deactivateAndDeleteAllPlugins( skippedPlugins ); +} diff --git a/packages/js/admin-e2e-tests/src/globalTypes.ts b/packages/js/admin-e2e-tests/src/globalTypes.ts new file mode 100644 index 00000000000..e46b34b68c8 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/globalTypes.ts @@ -0,0 +1,10 @@ +/** + * External dependencies + */ +import { Browser, Page } from 'puppeteer'; + +declare global { + const page: Page; + const browser: Browser; + const browserName: string; +} diff --git a/packages/js/admin-e2e-tests/src/index.ts b/packages/js/admin-e2e-tests/src/index.ts new file mode 100644 index 00000000000..60a182ae95b --- /dev/null +++ b/packages/js/admin-e2e-tests/src/index.ts @@ -0,0 +1 @@ +export * from './specs'; diff --git a/packages/js/admin-e2e-tests/src/pages/AllOrdersView.ts b/packages/js/admin-e2e-tests/src/pages/AllOrdersView.ts new file mode 100644 index 00000000000..48405d9273f --- /dev/null +++ b/packages/js/admin-e2e-tests/src/pages/AllOrdersView.ts @@ -0,0 +1,8 @@ +/** + * Internal dependencies + */ +import { BasePage } from './BasePage'; + +export class AllOrdersView extends BasePage { + url = 'wp-admin/edit.php?post_type=shop_order'; +} diff --git a/packages/js/admin-e2e-tests/src/pages/Analytics.ts b/packages/js/admin-e2e-tests/src/pages/Analytics.ts new file mode 100644 index 00000000000..38acae29760 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/pages/Analytics.ts @@ -0,0 +1,32 @@ +/** + * Internal dependencies + */ +import { BasePage } from './BasePage'; + +export type AnalyticsSection = + | 'overview' + | 'products' + | 'revenue' + | 'orders' + | 'variations' + | 'categories' + | 'coupons' + | 'taxes' + | 'downloads' + | 'stock' + | 'settings'; + +export class Analytics extends BasePage { + // If you need to navigate to the base analytics page you can go to the overview + url = 'wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Foverview'; + + // If you need to go to a specific single page of the analytics use `navigateToSection` + async navigateToSection( section: AnalyticsSection ): Promise< void > { + await this.goto( this.url.replace( 'overview', section ) ); + } + + async isDisplayed(): Promise< void > { + // This is a smoke test that ensures the single page was rendered without crashing + await this.page.waitForSelector( '#woocommerce-layout__primary' ); + } +} diff --git a/packages/js/admin-e2e-tests/src/pages/AnalyticsOverview.ts b/packages/js/admin-e2e-tests/src/pages/AnalyticsOverview.ts new file mode 100644 index 00000000000..1927bd50e09 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/pages/AnalyticsOverview.ts @@ -0,0 +1,154 @@ +/** + * External dependencies + */ +import { ElementHandle } from 'puppeteer'; + +/** + * Internal dependencies + */ +import { + waitForElementByText, + waitUntilElementStopsMoving, +} from '../utils/actions'; +import { Analytics } from './Analytics'; + +type Section = { + title: string; + element: ElementHandle< Element >; +}; +const isSection = ( item: Section | undefined ): item is Section => { + return !! item; +}; + +export class AnalyticsOverview extends Analytics { + async navigate(): Promise< void > { + await this.navigateToSection( 'overview' ); + } + + async getSections(): Promise< Section[] > { + const list = await this.page.$$( + '.woocommerce-dashboard-section .woocommerce-section-header' + ); + const sections = await Promise.all( + list.map( async ( item ) => { + const title = await item.evaluate( ( element ) => { + const header = element.querySelector( 'h2' ); + return header?.textContent; + } ); + if ( title ) { + return { + title, + element: item, + }; + } + return undefined; + } ) + ); + return sections.filter( isSection ); + } + + async getSectionTitles(): Promise< string[] > { + const sections = ( await this.getSections() ).map( + ( section ) => section.title + ); + return sections; + } + + async openSectionEllipsis( sectionTitle: string ): Promise< void > { + const section = ( await this.getSections() ).find( + ( thisSection ) => thisSection.title === sectionTitle + ); + if ( section ) { + const ellipsisMenu = await section.element.$( + '.woocommerce-ellipsis-menu .woocommerce-ellipsis-menu__toggle' + ); + await ellipsisMenu?.click(); + await this.page.waitForSelector( + '.woocommerce-ellipsis-menu div[role=menu]' + ); + } + } + + async closeSectionEllipsis( sectionTitle: string ): Promise< void > { + const section = ( await this.getSections() ).find( + ( thisSection ) => thisSection.title === sectionTitle + ); + if ( section ) { + const ellipsisMenu = await section.element.$( + '.woocommerce-ellipsis-menu .woocommerce-ellipsis-menu__toggle' + ); + await ellipsisMenu?.click(); + await page.waitForFunction( + () => + ! document.querySelector( + '.woocommerce-ellipsis-menu div[role=menu]' + ) + ); + } + } + + async removeSection( sectionTitle: string ): Promise< void > { + await this.openSectionEllipsis( sectionTitle ); + const item = await waitForElementByText( 'div', 'Remove section' ); + await item?.click(); + } + + async addSection( sectionTitle: string ): Promise< void > { + await this.page.waitForSelector( "button[title='Add more sections']" ); + await this.page.click( "button[title='Add more sections']" ); + const addSectionSelector = `button[title='Add ${ sectionTitle } section']`; + await this.page.waitForSelector( addSectionSelector ); + await waitUntilElementStopsMoving( addSectionSelector ); + await this.page.click( addSectionSelector ); + } + + async moveSectionDown( sectionTitle: string ): Promise< void > { + await this.openSectionEllipsis( sectionTitle ); + const item = await waitForElementByText( 'div', 'Move down' ); + await item?.click(); + } + + async moveSectionUp( sectionTitle: string ): Promise< void > { + await this.openSectionEllipsis( sectionTitle ); + const item = await waitForElementByText( 'div', 'Move up' ); + await item?.click(); + } + + async getEllipsisMenuItems( + sectionTitle: string + ): Promise< + { title: string | null; element: ElementHandle< Element > }[] + > { + await this.openSectionEllipsis( sectionTitle ); + const list = await this.page.$$( + '.woocommerce-ellipsis-menu div[role=menuitem]' + ); + return Promise.all( + list.map( async ( item ) => ( { + title: await item.evaluate( + ( element ) => element?.textContent + ), + element: item, + } ) ) + ); + } + + async getEllipsisMenuCheckboxItems( + sectionTitle: string + ): Promise< + { title: string | null; element: ElementHandle< Element > }[] + > { + await this.openSectionEllipsis( sectionTitle ); + const list = await this.page.$$( + '.woocommerce-ellipsis-menu div[role=menuitemcheckbox]' + ); + return Promise.all( + list.map( async ( item ) => ( { + title: await item.evaluate( + ( element ) => element?.textContent + ), + element: item, + } ) ) + ); + } +} diff --git a/packages/js/admin-e2e-tests/src/pages/BasePage.ts b/packages/js/admin-e2e-tests/src/pages/BasePage.ts new file mode 100644 index 00000000000..7b4ff463909 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/pages/BasePage.ts @@ -0,0 +1,162 @@ +/** + * External dependencies + */ +import { ElementHandle, Page } from 'puppeteer'; + +/** + * Internal dependencies + */ +import { DropdownField } from '../elements/DropdownField'; +import { DropdownTypeaheadField } from '../elements/DropdownTypeaheadField'; +import { FormToggle } from '../elements/FormToggle'; +import { getElementByText, waitForTimeout } from '../utils/actions'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const config = require( 'config' ); +/* eslint-enable @typescript-eslint/no-var-requires */ +const baseUrl = config.get( 'url' ); + +// Represents a page that can be navigated to +export abstract class BasePage { + protected page: Page; + protected url = ''; + protected baseUrl: string = baseUrl; + + // cache of elements that have been setup, note that they are unique "per page/per selector" + private dropDownElements: Record< string, DropdownField > = {}; + private dropDownTypeAheadElements: Record< + string, + DropdownTypeaheadField + > = {}; + private formToggleElements: Record< string, FormToggle > = {}; + + constructor( page: Page ) { + this.page = page; + } + + getDropdownField( selector: string ): DropdownField { + if ( ! this.dropDownElements[ selector ] ) { + this.dropDownElements[ selector ] = new DropdownField( + page, + selector + ); + } + + return this.dropDownElements[ selector ]; + } + + getDropdownTypeahead( selector: string ): DropdownTypeaheadField { + if ( ! this.dropDownTypeAheadElements[ selector ] ) { + this.dropDownTypeAheadElements[ + selector + ] = new DropdownTypeaheadField( page, selector ); + } + + return this.dropDownTypeAheadElements[ selector ]; + } + + getFormToggle( selector: string ): FormToggle { + if ( ! this.formToggleElements[ selector ] ) { + this.formToggleElements[ selector ] = new FormToggle( + page, + selector + ); + } + + return this.formToggleElements[ selector ]; + } + + async click( selector: string ): Promise< void > { + await this.page.waitForSelector( selector ); + await this.page.click( selector ); + } + + async clickButtonWithText( text: string ): Promise< void > { + const el = await getElementByText( 'button', text ); + await el?.click(); + } + + async clickElementWithText( + element: string, + text: string + ): Promise< void > { + const el = await getElementByText( element, text ); + await el?.click(); + } + + async setCheckboxWithText( text: string ): Promise< void > { + let checkbox = await getElementByText( 'label', text ); + + if ( ! checkbox ) { + checkbox = await getElementByText( 'span', text ); + } + + if ( checkbox ) { + const checkboxStatus = await ( + await checkbox.getProperty( 'checked' ) + ).jsonValue(); + + if ( checkboxStatus !== true ) { + await checkbox.click(); + } + } else { + throw new Error( `Could not find checkbox with text "${ text }"` ); + } + } + + async unsetAllCheckboxes( selector: string ): Promise< void > { + const checkboxes = await page.$$( selector ); + // Uncheck all checkboxes, to avoid installing plugins + for ( const checkbox of checkboxes ) { + await this.toggleCheckbox( checkbox, false ); + await waitForTimeout( 200 ); + } + } + + async setAllCheckboxes( selector: string ): Promise< void > { + const checkboxes = await page.$$( selector ); + // Uncheck all checkboxes, to avoid installing plugins + for ( const checkbox of checkboxes ) { + await this.toggleCheckbox( checkbox, true ); + await waitForTimeout( 200 ); + } + } + + // Set or unset a checkbox based on `checked` value passed. + async toggleCheckbox( + checkbox: ElementHandle< Element >, + checked: boolean + ): Promise< void > { + const checkboxStatus = await ( + await checkbox.getProperty( 'checked' ) + ).jsonValue(); + + if ( checkboxStatus !== checked ) { + await checkbox.click(); + } + } + + async navigate(): Promise< void > { + if ( ! this.url ) { + throw new Error( 'You must define a url for the page object' ); + } + + await this.goto( this.url ); + } + + protected async goto( url: string ): Promise< void > { + const fullUrl = baseUrl + url; + try { + await this.page.goto( fullUrl, { + waitUntil: 'networkidle0', + timeout: 10000, + } ); + } catch ( e ) { + if ( e instanceof Error ) { + throw new Error( + `Could not navigate to url: ${ fullUrl } with error: ${ e.message }` + ); + } + } + } +} diff --git a/packages/js/admin-e2e-tests/src/pages/Coupons.ts b/packages/js/admin-e2e-tests/src/pages/Coupons.ts new file mode 100644 index 00000000000..264ea6da2ac --- /dev/null +++ b/packages/js/admin-e2e-tests/src/pages/Coupons.ts @@ -0,0 +1,13 @@ +/** + * Internal dependencies + */ +import { BasePage } from './BasePage'; + +export class Coupons extends BasePage { + url = 'wp-admin/edit.php?post_type=shop_coupon&legacy_coupon_menu=1'; + + async isDisplayed(): Promise< void > { + // This is a smoke test that ensures the single page was rendered without crashing + await this.page.waitForSelector( '#woocommerce-layout__primary' ); + } +} diff --git a/packages/js/admin-e2e-tests/src/pages/Customers.ts b/packages/js/admin-e2e-tests/src/pages/Customers.ts new file mode 100644 index 00000000000..55cecd48a25 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/pages/Customers.ts @@ -0,0 +1,9 @@ +/** + * Internal dependencies + */ +import { Analytics } from './Analytics'; + +export class Customers extends Analytics { + // The analytics pages are `analytics-{slug}`. + url = 'wp-admin/admin.php?page=wc-admin&path=%2Fcustomers'; +} diff --git a/packages/js/admin-e2e-tests/src/pages/Dashboard.ts b/packages/js/admin-e2e-tests/src/pages/Dashboard.ts new file mode 100644 index 00000000000..a8a37264f17 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/pages/Dashboard.ts @@ -0,0 +1,8 @@ +/** + * Internal dependencies + */ +import { BasePage } from './BasePage'; + +export class Dashboard extends BasePage { + url = 'wp-admin'; +} diff --git a/packages/js/admin-e2e-tests/src/pages/Login.ts b/packages/js/admin-e2e-tests/src/pages/Login.ts new file mode 100644 index 00000000000..0c6ec4e1596 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/pages/Login.ts @@ -0,0 +1,53 @@ +/** + * Internal dependencies + */ +import { getElementByText } from '../utils/actions'; +import { BasePage } from './BasePage'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { clearAndFillInput } = require( '@woocommerce/e2e-utils' ); +const config = require( 'config' ); + +export class Login extends BasePage { + url = 'wp-login.php'; + + async login(): Promise< void > { + await this.navigate(); + + await getElementByText( 'label', 'Username or Email Address' ); + await clearAndFillInput( '#user_login', ' ' ); + + await this.page.type( + '#user_login', + config.get( 'users.admin.username' ) + ); + await this.page.type( + '#user_pass', + config.get( 'users.admin.password' ) + ); + + await Promise.all( [ + this.page.click( 'input[type=submit]' ), + this.page.waitForNavigation( { + waitUntil: 'networkidle0', + timeout: 10000, + } ), + ] ); + } + + async logout(): Promise< void > { + // Log out link in admin bar is not visible so can't be clicked directly. + const logoutLinks = await this.page.$$eval( + '#wp-admin-bar-logout a', + ( am ) => + am + .filter( ( e ) => ( e as HTMLLinkElement ).href ) + .map( ( e ) => ( e as HTMLLinkElement ).href ) + ); + + await page.goto( logoutLinks[ 0 ], { + waitUntil: 'networkidle0', + timeout: 10000, + } ); + } +} diff --git a/packages/js/admin-e2e-tests/src/pages/NewCoupon.ts b/packages/js/admin-e2e-tests/src/pages/NewCoupon.ts new file mode 100644 index 00000000000..c8d9acdeb32 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/pages/NewCoupon.ts @@ -0,0 +1,8 @@ +/** + * Internal dependencies + */ +import { BasePage } from './BasePage'; + +export class NewCoupon extends BasePage { + url = 'wp-admin/post-new.php?post_type=shop_coupon'; +} diff --git a/packages/js/admin-e2e-tests/src/pages/NewOrder.ts b/packages/js/admin-e2e-tests/src/pages/NewOrder.ts new file mode 100644 index 00000000000..07089676412 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/pages/NewOrder.ts @@ -0,0 +1,8 @@ +/** + * Internal dependencies + */ +import { BasePage } from './BasePage'; + +export class NewOrder extends BasePage { + url = 'wp-admin/post-new.php?post_type=shop_order'; +} diff --git a/packages/js/admin-e2e-tests/src/pages/NewProduct.ts b/packages/js/admin-e2e-tests/src/pages/NewProduct.ts new file mode 100644 index 00000000000..5ef75b355b4 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/pages/NewProduct.ts @@ -0,0 +1,8 @@ +/** + * Internal dependencies + */ +import { BasePage } from './BasePage'; + +export class NewProduct extends BasePage { + url = 'wp-admin/post-new.php?post_type=product'; +} diff --git a/packages/js/admin-e2e-tests/src/pages/OnboardingWizard.ts b/packages/js/admin-e2e-tests/src/pages/OnboardingWizard.ts new file mode 100644 index 00000000000..f1e3aea0260 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/pages/OnboardingWizard.ts @@ -0,0 +1,157 @@ +/** + * External dependencies + */ +import { Page } from 'puppeteer'; + +/** + * Internal dependencies + */ +import { BusinessSection } from '../sections/onboarding/BusinessSection'; +import { IndustrySection } from '../sections/onboarding/IndustrySection'; +import { ProductTypeSection } from '../sections/onboarding/ProductTypesSection'; +import { + StoreDetails, + StoreDetailsSection, +} from '../sections/onboarding/StoreDetailsSection'; +import { ThemeSection } from '../sections/onboarding/ThemeSection'; +import { BasePage } from './BasePage'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { expect } = require( '@jest/globals' ); +const config = require( 'config' ); + +export class OnboardingWizard extends BasePage { + url = 'wp-admin/admin.php?page=wc-admin&path=/setup-wizard'; + + storeDetails: StoreDetailsSection; + industry: IndustrySection; + productTypes: ProductTypeSection; + business: BusinessSection; + themes: ThemeSection; + + constructor( page: Page ) { + super( page ); + this.storeDetails = new StoreDetailsSection( page ); + this.industry = new IndustrySection( page ); + this.productTypes = new ProductTypeSection( page ); + this.business = new BusinessSection( page ); + this.themes = new ThemeSection( page ); + } + + async skipStoreSetup(): Promise< void > { + await this.clickButtonWithText( 'Skip setup store details' ); + await this.optionallySelectUsageTracking( false ); + } + + async continue(): Promise< void > { + await this.clickButtonWithText( 'Continue' ); + } + + async optionallySelectUsageTracking( select = false ): Promise< void > { + const usageTrackingHeader = await this.page.waitForSelector( + '.components-modal__header-heading', + { + timeout: 5000, + } + ); + if ( ! usageTrackingHeader ) { + return; + } + await expect( page ).toMatchElement( + '.components-modal__header-heading', + { + text: 'Build a better WooCommerce', + } + ); + + // Query for primary buttons: "Continue" and "Yes, count me in" + const primaryButtons = await this.page.$$( 'button.is-primary' ); + expect( primaryButtons ).toHaveLength( 2 ); + + if ( select ) { + await this.clickButtonWithText( 'Yes, count me in' ); + } else { + await this.clickButtonWithText( 'No thanks' ); + } + + await this.page.waitForNavigation( { + waitUntil: 'networkidle0', + timeout: 4000, + } ); + } + + async goToOBWStep( step: string ): Promise< void > { + await this.clickElementWithText( 'span', step ); + } + + async walkThroughAndCompleteOnboardingWizard( + options: { + storeDetails?: StoreDetails; + industries?: string[]; + products?: string[]; + businessDetails?: { + productNumber: string; + currentlySelling: string; + }; + themeTitle?: string; + } = {} + ): Promise< void > { + await this.navigate(); + await this.storeDetails.completeStoreDetailsSection( + options.storeDetails + ); + + // Wait for "Continue" button to become active + await this.continue(); + + // Wait for usage tracking pop-up window to appear + await this.optionallySelectUsageTracking(); + // Query for the industries checkboxes + await this.industry.isDisplayed(); + const industries = options.industries || [ 'Other' ]; + for ( const industry of industries ) { + await this.industry.selectIndustry( industry ); + } + await this.continue(); + await this.productTypes.isDisplayed( 7 ); + const products = options.products || [ + 'Physical products', + 'Downloads', + ]; + for ( const product of products ) { + await this.productTypes.selectProduct( product ); + } + + await this.continue(); + await page.waitForNavigation( { + waitUntil: 'networkidle0', + } ); + await this.business.isDisplayed(); + + const businessDetails = options.businessDetails || { + productNumber: config.get( 'onboardingwizard.numberofproducts' ), + currentlySelling: config.get( 'onboardingwizard.sellingelsewhere' ), + }; + await this.business.selectProductNumber( + businessDetails.productNumber + ); + await this.business.selectCurrentlySelling( + businessDetails.currentlySelling + ); + + await this.continue(); + await this.business.freeFeaturesIsDisplayed(); + await this.business.expandRecommendedBusinessFeatures(); + await this.business.uncheckAllRecommendedBusinessFeatures(); + + await this.continue(); + await this.themes.isDisplayed(); + + // This navigates to the home screen + if ( options.themeTitle ) { + await this.themes.continueWithTheme( options.themeTitle ); + } else { + await this.themes.continueWithActiveTheme(); + } + } +} diff --git a/packages/js/admin-e2e-tests/src/pages/PaymentsSetup.ts b/packages/js/admin-e2e-tests/src/pages/PaymentsSetup.ts new file mode 100644 index 00000000000..8efc7dc4ed4 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/pages/PaymentsSetup.ts @@ -0,0 +1,63 @@ +/** + * Internal dependencies + */ +import { waitForElementByText, getElementByText } from '../utils/actions'; +import { BasePage } from './BasePage'; + +type PaymentMethodWithSetupButton = + | 'wcpay' + | 'stripe' + | 'paypal' + | 'klarna_payments' + | 'mollie' + | 'bacs'; + +type PaymentMethod = PaymentMethodWithSetupButton | 'cod'; + +export class PaymentsSetup extends BasePage { + url = 'wp-admin/admin.php?page=wc-admin&task=payments'; + + async isDisplayed(): Promise< void > { + await waitForElementByText( 'h1', 'Set up payments' ); + } + + async possiblyCloseHelpModal(): Promise< void > { + try { + await waitForElementByText( 'div', "We're here for help", { + timeout: 2000, + } ); + await this.clickButtonWithText( 'Got it' ); + } catch ( e ) {} + } + + async showOtherPaymentMethods(): Promise< void > { + const selector = '.woocommerce-task-payments button.toggle-button'; + await this.page.waitForSelector( selector ); + const toggleButton = await this.page.$( + `${ selector }[aria-expanded=false]` + ); + await toggleButton?.click(); + await waitForElementByText( 'h2', 'Offline payment methods' ); + } + + async goToPaymentMethodSetup( + method: PaymentMethodWithSetupButton + ): Promise< void > { + const selector = `.woocommerce-task-payment-${ method } button`; + await this.page.waitForSelector( selector ); + const button = await this.page.$( selector ); + + if ( ! button ) { + throw new Error( + `Could not find button with selector: ${ selector }` + ); + } else { + await button.click(); + } + } + + async enableCashOnDelivery(): Promise< void > { + await this.page.waitForSelector( '.woocommerce-task-payment-cod' ); + await this.clickButtonWithText( 'Enable' ); + } +} diff --git a/packages/js/admin-e2e-tests/src/pages/PermalinkSettings.ts b/packages/js/admin-e2e-tests/src/pages/PermalinkSettings.ts new file mode 100644 index 00000000000..ebaf2cf9693 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/pages/PermalinkSettings.ts @@ -0,0 +1,8 @@ +/** + * Internal dependencies + */ +import { BasePage } from './BasePage'; + +export class PermalinkSettings extends BasePage { + url = 'wp-admin/options-permalink.php'; +} diff --git a/packages/js/admin-e2e-tests/src/pages/Plugins.ts b/packages/js/admin-e2e-tests/src/pages/Plugins.ts new file mode 100644 index 00000000000..ff60cdf7802 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/pages/Plugins.ts @@ -0,0 +1,8 @@ +/** + * Internal dependencies + */ +import { BasePage } from './BasePage'; + +export class Plugins extends BasePage { + url = 'wp-admin/plugins.php'; +} diff --git a/packages/js/admin-e2e-tests/src/pages/ProductsSetup.ts b/packages/js/admin-e2e-tests/src/pages/ProductsSetup.ts new file mode 100644 index 00000000000..80cfa91ff20 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/pages/ProductsSetup.ts @@ -0,0 +1,28 @@ +/** + * Internal dependencies + */ +import { waitForElementByText } from '../utils/actions'; +import { BasePage } from './BasePage'; + +export class ProductsSetup extends BasePage { + url = 'wp-admin/admin.php?page=wc-admin&task=products'; + + async isDisplayed(): Promise< void > { + await waitForElementByText( 'h1', 'Add my products' ); + } + + async isStartWithATemplateDisplayed( + templatesCount: number + ): Promise< void > { + await waitForElementByText( 'h1', 'Start with a template' ); + const length = await this.page.$$eval( + '.components-radio-control__input', + ( items ) => items.length + ); + expect( length === templatesCount ).toBeTruthy(); + } + + async clickStartWithTemplate(): Promise< void > { + await this.clickElementWithText( '*', 'Start with a template' ); + } +} diff --git a/packages/js/admin-e2e-tests/src/pages/WcHomescreen.ts b/packages/js/admin-e2e-tests/src/pages/WcHomescreen.ts new file mode 100644 index 00000000000..d1c6c90e06a --- /dev/null +++ b/packages/js/admin-e2e-tests/src/pages/WcHomescreen.ts @@ -0,0 +1,145 @@ +/** + * External dependencies + */ +import { ElementHandle } from 'puppeteer'; + +/** + * Internal dependencies + */ +import { + waitForElementByText, + getElementByAttributeAndValue, + waitForElementByTextWithoutThrow, + getElementByText, + waitForTimeout, +} from '../utils/actions'; +import { BasePage } from './BasePage'; + +export class WcHomescreen extends BasePage { + url = 'wp-admin/admin.php?page=wc-admin'; + + async isDisplayed(): Promise< void > { + // Wait for Benefits section to appear + await waitForElementByText( 'h1', 'Home' ); + } + + async possiblyDismissWelcomeModal(): Promise< void > { + const modal = await this.isWelcomeModalVisible(); + + if ( modal ) { + await this.clickButtonWithText( 'Next' ); + await waitForTimeout( 1000 ); + await this.clickButtonWithText( 'Next' ); + await waitForTimeout( 1000 ); + await this.click( '.components-guide__finish-button' ); + await waitForTimeout( 500 ); + } + } + + async isWelcomeModalVisible(): Promise< boolean > { + const modalText = 'Welcome to your WooCommerce store’s online HQ!'; + const modal = await waitForElementByTextWithoutThrow( + 'h2', + modalText, + 10 + ); + return modal; + } + + async getTaskList(): Promise< Array< string | null > > { + await page.waitForSelector( + '.woocommerce-task-card .woocommerce-task-list__item-title' + ); + await waitForElementByText( '*', 'Get ready to start selling' ); + const list = await this.page.$$eval( + '.woocommerce-task-card .woocommerce-task-list__item-title', + ( items ) => items.map( ( item ) => item.textContent ) + ); + return list.map( ( item: string | null ) => { + const match = item?.match( /(.+)[0-9] minute/ ); + if ( match && match.length > 1 ) { + return match[ 1 ]; + } + return item; + } ); + } + + async isTaskListDisplayed(): Promise< boolean > { + return !! ( await waitForElementByTextWithoutThrow( + '*', + 'Get ready to start selling' + ) ); + } + + async clickOnTaskList( taskTitle: string ): Promise< void > { + const item = await waitForElementByText( '*', taskTitle ); + + if ( ! item ) { + throw new Error( + `Could not find task list item with title: ${ taskTitle }` + ); + } else { + await item.click(); + await waitForElementByText( 'h1', taskTitle ); + } + } + + async hideTaskList(): Promise< void > { + const taskListOptions = await getElementByAttributeAndValue( + 'button', + 'title', + 'Task List Options' + ); + await taskListOptions?.click(); + await waitForElementByText( 'button', 'Hide this' ); + await waitForTimeout( 200 ); // Transition of popup. + const hideThisButton = await getElementByText( 'button', 'Hide this' ); + await hideThisButton?.click(); + await waitForTimeout( 500 ); + } + + async waitForNotesRequestToBeLoaded(): Promise< void > { + await this.page.waitForResponse( ( response ) => { + const url = encodeURIComponent( response.url() ); + return url.includes( '/wc-analytics/admin/notes' ) && response.ok(); + } ); + } + + async isActivityPanelShown(): Promise< boolean > { + return !! ( await this.page.$( '.woocommerce-activity-panel' ) ); + } + + async getActivityPanels(): Promise< + Array< { title: string; count?: number; element?: ElementHandle } > + > { + const panelContainer = await page.waitForSelector( + '.woocommerce-activity-panel' + ); + const list = await panelContainer.$$( 'h2' ); + return Promise.all( + list.map( async ( item: ElementHandle ) => { + const textContent = await page.evaluate( + ( el ) => el.textContent, + item + ); + const match = textContent?.match( /([a-zA-Z]+)([0-9]+)/ ); + if ( match && match.length > 2 ) { + return { + title: match[ 1 ], + count: parseInt( match[ 2 ], 10 ), + element: item, + }; + } + return { title: textContent }; + } ) + ); + } + + async expandActivityPanel( title: string ): Promise< void > { + const activityPanels = await this.getActivityPanels(); + const panel = activityPanels.find( ( p ) => p.title === title ); + if ( panel ) { + await panel.element?.click(); + } + } +} diff --git a/packages/js/admin-e2e-tests/src/pages/WcSettings.ts b/packages/js/admin-e2e-tests/src/pages/WcSettings.ts new file mode 100644 index 00000000000..0b5f78cf0a7 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/pages/WcSettings.ts @@ -0,0 +1,76 @@ +/** + * Internal dependencies + */ +import { getAttribute, hasClass, waitForElementByText } from '../utils/actions'; +import { BasePage } from './BasePage'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { setCheckbox } = require( '@woocommerce/e2e-utils' ); +/* eslint-enable @typescript-eslint/no-var-requires */ + +export class WcSettings extends BasePage { + url = 'wp-admin/admin.php?page=wc-settings'; + + async navigate( tab = 'general', section = '' ): Promise< void > { + let settingsUrl = this.url + `&tab=${ tab }`; + + if ( section ) { + settingsUrl += `§ion=${ section }`; + } + + await this.goto( settingsUrl ); + await waitForElementByText( 'a', 'General' ); + } + + async enableTaxRates(): Promise< void > { + await waitForElementByText( 'th', 'Enable taxes' ); + await setCheckbox( '#woocommerce_calc_taxes' ); + } + + async getTaxRateValue(): Promise< unknown > { + return await getAttribute( '#woocommerce_calc_taxes', 'checked' ); + } + + async saveSettings(): Promise< void > { + this.clickButtonWithText( 'Save changes' ); + await this.page.waitForNavigation( { + waitUntil: 'networkidle0', + } ); + await waitForElementByText( + 'strong', + 'Your settings have been saved.' + ); + } + + async paymentMethodIsEnabled( method = '' ): Promise< boolean > { + await this.navigate( 'checkout' ); + await waitForElementByText( 'h2', 'Payment methods' ); + const className = await getAttribute( + `tr[data-gateway_id=${ method }] .woocommerce-input-toggle`, + 'className' + ); + return ( + ( className as string ).indexOf( + 'woocommerce-input-toggle--disabled' + ) === -1 + ); + } + + async cleanPaymentMethods(): Promise< void > { + await this.navigate( 'checkout' ); + await waitForElementByText( 'h2', 'Payment methods' ); + const paymentMethods = await page.$$( 'span.woocommerce-input-toggle' ); + for ( const method of paymentMethods ) { + if ( + method && + ( await hasClass( + method, + 'woocommerce-input-toggle--enabled' + ) ) + ) { + await method?.click(); + } + } + await this.saveSettings(); + } +} diff --git a/packages/js/admin-e2e-tests/src/pages/WpSettings.ts b/packages/js/admin-e2e-tests/src/pages/WpSettings.ts new file mode 100644 index 00000000000..e429a2dd236 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/pages/WpSettings.ts @@ -0,0 +1,20 @@ +/** + * Internal dependencies + */ +import { waitForElementByText } from '../utils/actions'; +import { BasePage } from './BasePage'; + +export class WpSettings extends BasePage { + url = 'wp-admin/options-permalink.php'; + + async openPermalinkSettings(): Promise< void > { + await waitForElementByText( 'h1', 'Permalink Settings' ); + } + + async saveSettings(): Promise< void > { + await this.click( '#submit' ); + await this.page.waitForNavigation( { + waitUntil: 'networkidle0', + } ); + } +} diff --git a/packages/js/admin-e2e-tests/src/sections/onboarding/BusinessSection.ts b/packages/js/admin-e2e-tests/src/sections/onboarding/BusinessSection.ts new file mode 100644 index 00000000000..035a56cee21 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/sections/onboarding/BusinessSection.ts @@ -0,0 +1,114 @@ +/** + * Internal dependencies + */ +import { BasePage } from '../../pages/BasePage'; +import { waitForElementByText } from '../../utils/actions'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { + setCheckbox, + unsetCheckbox, + verifyCheckboxIsSet, + verifyCheckboxIsUnset, +} = require( '@woocommerce/e2e-utils' ); +/* eslint-enable @typescript-eslint/no-var-requires */ + +export class BusinessSection extends BasePage { + async isDisplayed(): Promise< void > { + await waitForElementByText( 'h2', 'Tell us about your business' ); + } + + async freeFeaturesIsDisplayed(): Promise< void > { + await waitForElementByText( 'h2', 'Included business features' ); + } + + async selectProductNumber( productLabel: string ): Promise< void > { + const howManyProductsDropdown = this.getDropdownField( + '.woocommerce-profile-wizard__product-count' + ); + + await howManyProductsDropdown.select( productLabel ); + } + + async selectCurrentlySelling( currentlySelling: string ): Promise< void > { + const sellingElsewhereDropdown = this.getDropdownField( + '.woocommerce-profile-wizard__selling-venues' + ); + + await sellingElsewhereDropdown.select( currentlySelling ); + } + async selectEmployeesNumber( employeesNumber: string ) { + const employeesNumberDropdown = this.getDropdownField( + '.woocommerce-profile-wizard__number-employees' + ); + + await employeesNumberDropdown.select( employeesNumber ); + } + async selectRevenue( revenue: string ) { + const revenueDropdown = this.getDropdownField( + '.woocommerce-profile-wizard__revenue' + ); + + await revenueDropdown.select( revenue ); + } + async selectOtherPlatformName( otherPlatformName: string ) { + const otherPlatformNameDropdown = this.getDropdownField( + '.woocommerce-profile-wizard__other-platform' + ); + + await otherPlatformNameDropdown.select( otherPlatformName ); + } + + async selectInstallFreeBusinessFeatures( + select: boolean + ): Promise< void > { + if ( select ) { + await setCheckbox( '#woocommerce-business-extensions__checkbox' ); + } else { + await unsetCheckbox( '#woocommerce-business-extensions__checkbox' ); + } + } + + async expandRecommendedBusinessFeatures(): Promise< void > { + const expandButtonSelector = + '.woocommerce-admin__business-details__selective-extensions-bundle__expand'; + + await this.page.waitForSelector( + expandButtonSelector + ':not([disabled])' + ); + await this.click( expandButtonSelector ); + + // Confirm that expanding the list shows all the extensions available to install. + await this.page.waitForFunction( () => { + const inputsNum = document.querySelectorAll( + '.components-checkbox-control__input' + ).length; + return inputsNum > 1; + } ); + } + + async uncheckAllRecommendedBusinessFeatures(): Promise< void > { + await this.unsetAllCheckboxes( '.components-checkbox-control__input' ); + } + + // The old list displayed on the dropdown page + async uncheckBusinessFeatures(): Promise< void > { + await this.unsetAllCheckboxes( + '.woocommerce-profile-wizard__benefit .components-form-toggle__input' + ); + } + + async selectSetupForClient(): Promise< void > { + await setCheckbox( '.components-checkbox-control__input' ); + } + + async checkClientSetupCheckbox( selected: boolean ): Promise< void > { + if ( selected ) { + await verifyCheckboxIsSet( '.components-checkbox-control__input' ); + } else { + await verifyCheckboxIsUnset( + '.components-checkbox-control__input' + ); + } + } +} diff --git a/packages/js/admin-e2e-tests/src/sections/onboarding/IndustrySection.ts b/packages/js/admin-e2e-tests/src/sections/onboarding/IndustrySection.ts new file mode 100644 index 00000000000..4764b85c53b --- /dev/null +++ b/packages/js/admin-e2e-tests/src/sections/onboarding/IndustrySection.ts @@ -0,0 +1,40 @@ +/** + * Internal dependencies + */ +import { BasePage } from '../../pages/BasePage'; +import { waitForElementByText } from '../../utils/actions'; + +export class IndustrySection extends BasePage { + async isDisplayed( + industryCount?: number, + industryCountMax?: number + ): Promise< void > { + await waitForElementByText( + 'h2', + 'In which industry does the store operate?' + ); + + if ( industryCount ) { + const length = await this.page.$$eval( + '.components-checkbox-control__input', + ( items ) => items.length + ); + + if ( industryCountMax ) { + expect( + length >= industryCount && length <= industryCountMax + ).toBeTruthy(); + } else { + expect( length === industryCount ).toBeTruthy(); + } + } + } + + async uncheckIndustries(): Promise< void > { + await this.unsetAllCheckboxes( '.components-checkbox-control__input' ); + } + + async selectIndustry( industryLabel: string ): Promise< void > { + await this.setCheckboxWithText( industryLabel ); + } +} diff --git a/packages/js/admin-e2e-tests/src/sections/onboarding/ProductTypesSection.ts b/packages/js/admin-e2e-tests/src/sections/onboarding/ProductTypesSection.ts new file mode 100644 index 00000000000..a292898fc59 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/sections/onboarding/ProductTypesSection.ts @@ -0,0 +1,27 @@ +/** + * Internal dependencies + */ +import { BasePage } from '../../pages/BasePage'; +import { waitForElementByText } from '../../utils/actions'; + +export class ProductTypeSection extends BasePage { + async isDisplayed( productCount: number ): Promise< void > { + await waitForElementByText( + 'h2', + 'What type of products will be listed?' + ); + const length = await this.page.$$eval( + '.components-checkbox-control__input', + ( items ) => items.length + ); + expect( length === productCount ).toBeTruthy(); + } + + async uncheckProducts(): Promise< void > { + await this.unsetAllCheckboxes( '.components-checkbox-control__input' ); + } + + async selectProduct( productLabel: string ): Promise< void > { + await this.setCheckboxWithText( productLabel ); + } +} diff --git a/packages/js/admin-e2e-tests/src/sections/onboarding/StoreDetailsSection.ts b/packages/js/admin-e2e-tests/src/sections/onboarding/StoreDetailsSection.ts new file mode 100644 index 00000000000..a6c89337fe9 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/sections/onboarding/StoreDetailsSection.ts @@ -0,0 +1,124 @@ +/** + * Internal dependencies + */ +import { DropdownTypeaheadField } from '../../elements/DropdownTypeaheadField'; +import { BasePage } from '../../pages/BasePage'; +import { waitForElementByText } from '../../utils/actions'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { + clearAndFillInput, + verifyCheckboxIsSet, + verifyCheckboxIsUnset, +} = require( '@woocommerce/e2e-utils' ); +const config = require( 'config' ); +/* eslint-enable @typescript-eslint/no-var-requires */ + +export interface StoreDetails { + addressLine1?: string; + addressLine2?: string; + countryRegionSubstring?: string; + countryRegionSelector?: string; + countryRegion?: string; + city?: string; + postcode?: string; + storeEmail?: string; +} + +export class StoreDetailsSection extends BasePage { + private get countryDropdown(): DropdownTypeaheadField { + return this.getDropdownTypeahead( '#woocommerce-select-control' ); + } + + async isDisplayed(): Promise< void > { + await waitForElementByText( 'h2', 'Welcome to WooCommerce' ); + } + + async completeStoreDetailsSection( + storeDetails: StoreDetails = {} + ): Promise< void > { + // const onboardingWizard = new OnboardingWizard( page ); + // Fill store's address - first line + await this.fillAddress( + storeDetails.addressLine1 || + config.get( 'addresses.admin.store.addressfirstline' ) + ); + + // Fill store's address - second line + await this.fillAddressLineTwo( + storeDetails.addressLine2 || + config.get( 'addresses.admin.store.addresssecondline' ) + ); + + // Type the requested country/region substring or 'cali' in the + // country/region select, then select the requested country/region + // substring or 'US:CA'. + await this.selectCountry( + storeDetails.countryRegionSubstring || 'cali', + storeDetails.countryRegionSelector || 'US\\:CA' + ); + + if ( storeDetails.countryRegion ) { + await this.checkCountrySelected( storeDetails.countryRegion ); + } + + // Fill the city where the store is located + await this.fillCity( + storeDetails.city || config.get( 'addresses.admin.store.city' ) + ); + + // Fill postcode of the store + await this.fillPostalCode( + storeDetails.postcode || + config.get( 'addresses.admin.store.postcode' ) + ); + + // Fill store's email address + await this.fillEmailAddress( + storeDetails.storeEmail || + config.get( 'addresses.admin.store.email' ) + ); + + // Verify that checkbox next to "Get tips, product updates and inspiration straight to your mailbox" is selected + await this.checkMarketingCheckbox( true ); + } + + async fillAddress( address: string ): Promise< void > { + await clearAndFillInput( '#inspector-text-control-0', address ); + } + + async fillAddressLineTwo( address: string ): Promise< void > { + await clearAndFillInput( '#inspector-text-control-1', address ); + } + + async selectCountry( search: string, selector: string ): Promise< void > { + await this.countryDropdown.search( search ); + await this.countryDropdown.select( selector ); + } + + async checkCountrySelected( country: string ): Promise< void > { + await this.countryDropdown.checkSelected( country ); + } + + async fillCity( city: string ): Promise< void > { + await clearAndFillInput( '#inspector-text-control-2', city ); + } + + async fillPostalCode( postalCode: string ): Promise< void > { + await clearAndFillInput( '#inspector-text-control-3', postalCode ); + } + + async fillEmailAddress( email: string ): Promise< void > { + await clearAndFillInput( '#inspector-text-control-4', email ); + } + + async checkMarketingCheckbox( selected: boolean ): Promise< void > { + if ( selected ) { + await verifyCheckboxIsSet( '.components-checkbox-control__input' ); + } else { + await verifyCheckboxIsUnset( + '.components-checkbox-control__input' + ); + } + } +} diff --git a/packages/js/admin-e2e-tests/src/sections/onboarding/ThemeSection.ts b/packages/js/admin-e2e-tests/src/sections/onboarding/ThemeSection.ts new file mode 100644 index 00000000000..8439fa01fc8 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/sections/onboarding/ThemeSection.ts @@ -0,0 +1,29 @@ +/** + * Internal dependencies + */ +import { BasePage } from '../../pages/BasePage'; +import { waitForElementByText } from '../../utils/actions'; + +export class ThemeSection extends BasePage { + async isDisplayed(): Promise< void > { + await waitForElementByText( 'h2', 'Choose a theme' ); + await waitForElementByText( 'button', 'All themes' ); + } + + async continueWithActiveTheme(): Promise< void > { + await this.clickButtonWithText( 'Continue with my active theme' ); + } + + async continueWithTheme( themeTitle: string ): Promise< void > { + const title = await waitForElementByText( 'h2', themeTitle ); + const chooseButton = await title?.evaluateHandle( ( element ) => { + const card = element.closest( '.components-card' ); + return Array.from( card?.querySelectorAll( 'button' ) || [] ).find( + ( el ) => el.textContent === 'Choose' + ); + } ); + if ( chooseButton ) { + await chooseButton.asElement()?.click(); + } + } +} diff --git a/packages/js/admin-e2e-tests/src/sections/payment-setup/BankAccountTransferSetup.ts b/packages/js/admin-e2e-tests/src/sections/payment-setup/BankAccountTransferSetup.ts new file mode 100644 index 00000000000..eb2a454b2dc --- /dev/null +++ b/packages/js/admin-e2e-tests/src/sections/payment-setup/BankAccountTransferSetup.ts @@ -0,0 +1,42 @@ +/** + * Internal dependencies + */ +import { BasePage } from '../../pages/BasePage'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { clearAndFillInput } = require( '@woocommerce/e2e-utils' ); +/* eslint-enable @typescript-eslint/no-var-requires */ + +type AccountDetails = { + accountName: string; + accountNumber: string; + bankName: string; + sortCode: string; + iban: string; + swiftCode: string; +}; + +export class BankAccountTransferSetup extends BasePage { + url = 'wp-admin/admin.php?page=wc-admin&task=payments&method=bacs'; + + async saveAccountDetails( { + accountName, + accountNumber, + bankName, + sortCode, + iban, + swiftCode, + }: AccountDetails ): Promise< void > { + await clearAndFillInput( '[placeholder="Account name"]', accountName ); + await clearAndFillInput( + '[placeholder="Account number"]', + accountNumber + ); + await clearAndFillInput( '[placeholder="Bank name"]', bankName ); + await clearAndFillInput( '[placeholder="Sort code"]', sortCode ); + await clearAndFillInput( '[placeholder="IBAN"]', iban ); + await clearAndFillInput( '[placeholder="BIC / Swift"]', swiftCode ); + + await this.clickButtonWithText( 'Save' ); + } +} diff --git a/packages/js/admin-e2e-tests/src/specs/activate-and-setup/basic-setup.ts b/packages/js/admin-e2e-tests/src/specs/activate-and-setup/basic-setup.ts new file mode 100644 index 00000000000..7dd30207fdd --- /dev/null +++ b/packages/js/admin-e2e-tests/src/specs/activate-and-setup/basic-setup.ts @@ -0,0 +1,84 @@ +/** + * Internal dependencies + */ +import { WcSettings } from '../../pages/WcSettings'; +import { WpSettings } from '../../pages/WpSettings'; +import { Login } from '../../pages/Login'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { + clearAndFillInput, + verifyValueOfInputField, +} = require( '@woocommerce/e2e-utils' ); +const { + afterAll, + beforeAll, + describe, + it, + expect, +} = require( '@jest/globals' ); +/* eslint-enable @typescript-eslint/no-var-requires */ + +const testAdminBasicSetup = () => { + describe( 'Store owner can finish initial store setup', () => { + const wcSettings = new WcSettings( page ); + const wpSettings = new WpSettings( page ); + const login = new Login( page ); + + beforeAll( async () => { + await login.login(); + } ); + afterAll( async () => { + await login.logout(); + } ); + + it( 'can enable tax rates and calculations', async () => { + // Go to general settings page + await wcSettings.navigate( 'general' ); + await wcSettings.enableTaxRates(); + await wcSettings.saveSettings(); + + // Verify that settings have been saved + const taxRate = await wcSettings.getTaxRateValue(); + expect( taxRate ).toEqual( true ); + } ); + + it( 'can configure permalink settings', async () => { + // Go to Permalink Settings page + await wpSettings.navigate(); + await wpSettings.openPermalinkSettings(); + + // Select "Post name" option in common settings section + await page.click( 'input[value="/%postname%/"]' ); + + // Select "Custom base" in product permalinks section + await page.click( '#woocommerce_custom_selection' ); + + // Fill custom base slug to use + await clearAndFillInput( '#woocommerce_permalink_structure', '' ); + await page.type( '#woocommerce_permalink_structure', '/product/' ); + + await wpSettings.saveSettings(); + + // Verify that settings have been saved + await Promise.all( [ + expect( page ).toMatchElement( + '#setting-error-settings_updated', + { + text: 'Permalink structure updated.', + } + ), + verifyValueOfInputField( + '#permalink_structure', + '/%postname%/' + ), + verifyValueOfInputField( + '#woocommerce_permalink_structure', + '/product/' + ), + ] ); + } ); + } ); +}; + +module.exports = { testAdminBasicSetup }; diff --git a/packages/js/admin-e2e-tests/src/specs/activate-and-setup/complete-onboarding-wizard.ts b/packages/js/admin-e2e-tests/src/specs/activate-and-setup/complete-onboarding-wizard.ts new file mode 100644 index 00000000000..717cacf4186 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/specs/activate-and-setup/complete-onboarding-wizard.ts @@ -0,0 +1,633 @@ +/** + * Internal dependencies + */ +import { OnboardingWizard } from '../../pages/OnboardingWizard'; +import { WcHomescreen } from '../../pages/WcHomescreen'; +import { TaskTitles } from '../../constants/taskTitles'; +import { Login } from '../../pages/Login'; +import { WcSettings } from '../../pages/WcSettings'; +import { ProductsSetup } from '../../pages/ProductsSetup'; +import { resetWooCommerceState } from '../../fixtures/reset'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { + afterAll, + beforeAll, + describe, + it, + expect, +} = require( '@jest/globals' ); +const config = require( 'config' ); + +const { verifyValueOfInputField } = require( '@woocommerce/e2e-utils' ); +/* eslint-enable @typescript-eslint/no-var-requires */ + +/** + * This tests a default, happy path for the onboarding wizard. + */ +const testAdminOnboardingWizard = () => { + describe( 'Store owner can complete onboarding wizard', () => { + const profileWizard = new OnboardingWizard( page ); + const login = new Login( page ); + + beforeAll( async () => { + await login.login(); + await resetWooCommerceState(); + } ); + afterAll( async () => { + await login.logout(); + } ); + + it( 'can start the profile wizard', async () => { + await profileWizard.navigate(); + } ); + + it( 'can complete the store details section', async () => { + await profileWizard.storeDetails.isDisplayed(); + await profileWizard.storeDetails.completeStoreDetailsSection(); + // Wait for "Continue" button to become active + await profileWizard.continue(); + + // Wait for usage tracking pop-up window to appear + await profileWizard.optionallySelectUsageTracking(); + } ); + + it( 'can complete the industry section', async () => { + // Query for the industries checkboxes + await profileWizard.industry.isDisplayed( 7, 8 ); + + // Select just "fashion" and "health/beauty" to get the single checkbox business section when + // filling out details for a US store. + await profileWizard.industry.selectIndustry( + 'Fashion, apparel, and accessories' + ); + await profileWizard.industry.selectIndustry( 'Health and beauty' ); + + await profileWizard.continue(); + } ); + + it( 'can click industry tab after going back', async () => { + await profileWizard.navigate(); + await profileWizard.goToOBWStep( 'Store Details' ); + await profileWizard.storeDetails.isDisplayed(); + + await profileWizard.goToOBWStep( 'Industry' ); + await profileWizard.industry.isDisplayed(); + + await profileWizard.continue(); + } ); + + it( 'can complete the product types section', async () => { + await profileWizard.productTypes.isDisplayed( 7 ); + + // Select Physical and Downloadable products + await profileWizard.productTypes.selectProduct( + 'Physical products' + ); + await profileWizard.productTypes.selectProduct( 'Downloads' ); + + await profileWizard.continue(); + } ); + + it( 'can complete the business section', async () => { + await profileWizard.business.isDisplayed(); + await profileWizard.business.selectProductNumber( + config.get( 'onboardingwizard.numberofproducts' ) + ); + await profileWizard.business.selectCurrentlySelling( + config.get( 'onboardingwizard.sellingelsewhere' ) + ); + await profileWizard.business.checkClientSetupCheckbox( false ); + await profileWizard.continue(); + } ); + + it( 'can unselect all business features and continue', async () => { + await profileWizard.business.freeFeaturesIsDisplayed(); + // Add WC Pay check + await profileWizard.business.expandRecommendedBusinessFeatures(); + + expect( page ).toMatchElement( 'a', { + text: 'WooCommerce Payments', + } ); + + await profileWizard.business.uncheckAllRecommendedBusinessFeatures(); + await profileWizard.continue(); + } ); + + it( 'can complete the theme selection section', async () => { + await profileWizard.themes.isDisplayed(); + await profileWizard.themes.continueWithActiveTheme(); + } ); + + it( 'can select the right currency on settings page related to the onboarding country', async () => { + const settingsScreen = new WcSettings( page ); + await settingsScreen.navigate(); + verifyValueOfInputField( '#woocommerce_currency', 'USD' ); + } ); + } ); +}; + +const testSelectiveBundleWCPay = () => { + describe( 'A japanese store can complete the selective bundle install but does not include WCPay.', () => { + const profileWizard = new OnboardingWizard( page ); + const login = new Login( page ); + + beforeAll( async () => { + await login.login(); + await resetWooCommerceState(); + } ); + afterAll( async () => { + await login.logout(); + } ); + + it( 'can start the profile wizard', async () => { + await profileWizard.navigate(); + } ); + + it( 'can complete the store details section', async () => { + await profileWizard.storeDetails.completeStoreDetailsSection( { + countryRegionSubstring: 'japan', + countryRegionSelector: 'JP\\:JP01', + countryRegion: 'Japan — Hokkaido', + } ); + + // Wait for "Continue" button to become active + await profileWizard.continue(); + + // Wait for usage tracking pop-up window to appear + await profileWizard.optionallySelectUsageTracking(); + } ); + + // JP:JP01 + it( 'can choose the "Other" industry', async () => { + // Query for the industries checkboxes + await profileWizard.industry.isDisplayed(); + await profileWizard.industry.selectIndustry( 'Other' ); + await profileWizard.continue(); + } ); + + it( 'can complete the product types section', async () => { + await profileWizard.productTypes.isDisplayed( 7 ); + + // Select Physical and Downloadable products + await profileWizard.productTypes.selectProduct( + 'Physical products' + ); + await profileWizard.productTypes.selectProduct( 'Downloads' ); + + await profileWizard.continue(); + await page.waitForNavigation( { waitUntil: 'networkidle0' } ); + } ); + + it( 'can complete the business details tab', async () => { + await profileWizard.business.isDisplayed(); + + await profileWizard.business.selectProductNumber( + config.get( 'onboardingwizard.numberofproducts' ) + ); + await profileWizard.business.selectCurrentlySelling( + config.get( 'onboardingwizard.sellingelsewhere' ) + ); + + await profileWizard.continue(); + } ); + + it( 'can choose not to install any extensions', async () => { + await profileWizard.business.freeFeaturesIsDisplayed(); + // Add WC Pay check + await profileWizard.business.expandRecommendedBusinessFeatures(); + + expect( page ).not.toMatchElement( 'a', { + text: 'WooCommerce Payments', + } ); + + await profileWizard.business.uncheckAllRecommendedBusinessFeatures(); + await profileWizard.continue(); + } ); + + it( 'can finish the rest of the wizard successfully', async () => { + await profileWizard.themes.isDisplayed(); + + // This navigates to the home screen + await profileWizard.themes.continueWithActiveTheme(); + } ); + + it( 'should display the choose payments task, and not the woocommerce payments task', async () => { + const homescreen = new WcHomescreen( page ); + await homescreen.isDisplayed(); + await homescreen.possiblyDismissWelcomeModal(); + const tasks = await homescreen.getTaskList(); + expect( tasks ).toContain( TaskTitles.addPayments ); + expect( tasks ).not.toContain( TaskTitles.wooPayments ); + } ); + + it( 'can select the right currency on settings page related to the onboarding country', async () => { + const settingsScreen = new WcSettings( page ); + await settingsScreen.navigate(); + verifyValueOfInputField( '#woocommerce_currency', 'JPY' ); + } ); + } ); +}; + +const testDifferentStoreCurrenciesWCPay = () => { + const testCountryCurrencyPairs = [ + { + countryRegionSubstring: 'australia', + countryRegionSelector: 'AU\\:QLD', + countryRegion: 'Australia — Queensland', + expectedCurrency: 'AUD', + isWCPaySupported: true, + }, + { + countryRegionSubstring: 'canada', + countryRegionSelector: 'CA\\:QC', + countryRegion: 'Canada — Quebec', + expectedCurrency: 'CAD', + isWCPaySupported: true, + }, + { + countryRegionSubstring: 'china', + countryRegionSelector: 'CN\\:CN2', + countryRegion: 'China — Beijing', + expectedCurrency: 'CNY', + isWCPaySupported: false, + }, + { + countryRegionSubstring: 'spain', + countryRegionSelector: 'ES\\:CO', + countryRegion: 'Spain — Córdoba', + expectedCurrency: 'EUR', + isWCPaySupported: true, + }, + { + countryRegionSubstring: 'india', + countryRegionSelector: 'IN\\:DL', + countryRegion: 'India — Delhi', + expectedCurrency: 'INR', + isWCPaySupported: false, + }, + { + countryRegionSubstring: 'kingd', + countryRegionSelector: 'GB', + countryRegion: 'United Kingdom (UK)', + expectedCurrency: 'GBP', + isWCPaySupported: true, + }, + ]; + + testCountryCurrencyPairs.forEach( ( spec ) => { + describe( 'A store can onboard with any country and have the correct currency selected after onboarding.', () => { + const profileWizard = new OnboardingWizard( page ); + const login = new Login( page ); + + beforeAll( async () => { + await login.login(); + await resetWooCommerceState(); + } ); + afterAll( async () => { + await login.logout(); + } ); + + it( `can complete the profile wizard with selecting ${ spec.countryRegion } as the country`, async () => { + await profileWizard.navigate(); + await profileWizard.storeDetails.completeStoreDetailsSection( { + countryRegionSubstring: spec.countryRegionSubstring, + countryRegionSelector: spec.countryRegionSelector, + countryRegion: spec.countryRegion, + } ); + + // Wait for "Continue" button to become active + await profileWizard.continue(); + + // Wait for usage tracking pop-up window to appear + await profileWizard.optionallySelectUsageTracking(); + // Query for the industries checkboxes + await profileWizard.industry.isDisplayed(); + await profileWizard.industry.selectIndustry( 'Other' ); + await profileWizard.continue(); + await profileWizard.productTypes.isDisplayed( 7 ); + await profileWizard.productTypes.selectProduct( + 'Physical products' + ); + await profileWizard.productTypes.selectProduct( 'Downloads' ); + + await profileWizard.continue(); + await page.waitForNavigation( { + waitUntil: 'networkidle0', + } ); + await profileWizard.business.isDisplayed(); + + await profileWizard.business.selectProductNumber( + config.get( 'onboardingwizard.numberofproducts' ) + ); + await profileWizard.business.selectCurrentlySelling( + config.get( 'onboardingwizard.sellingelsewhere' ) + ); + + await profileWizard.continue(); + await profileWizard.business.freeFeaturesIsDisplayed(); + // Add WC Pay check + await profileWizard.business.expandRecommendedBusinessFeatures(); + + if ( spec.isWCPaySupported ) { + expect( page ).toMatchElement( 'a', { + text: 'WooCommerce Payments', + } ); + } else { + expect( page ).not.toMatchElement( 'a', { + text: 'WooCommerce Payments', + } ); + } + + await profileWizard.business.uncheckAllRecommendedBusinessFeatures(); + await profileWizard.continue(); + await profileWizard.themes.isDisplayed(); + + // This navigates to the home screen + await profileWizard.themes.continueWithActiveTheme(); + } ); + + it( `can select ${ spec.expectedCurrency } as the currency for ${ spec.countryRegion }`, async () => { + const settingsScreen = new WcSettings( page ); + await settingsScreen.navigate(); + verifyValueOfInputField( + '#woocommerce_currency', + spec.expectedCurrency + ); + } ); + } ); + } ); +}; + +const testSubscriptionsInclusion = () => { + describe( 'A non-US store will not see the Subscriptions inclusion', () => { + const profileWizard = new OnboardingWizard( page ); + const login = new Login( page ); + + beforeAll( async () => { + await login.login(); + await resetWooCommerceState(); + } ); + + it( 'can complete the store details section', async () => { + await profileWizard.navigate(); + await profileWizard.storeDetails.completeStoreDetailsSection( { + countryRegionSubstring: 'fran', + countryRegionSelector: 'FR', + countryRegion: 'France', + } ); + + // Wait for "Continue" button to become active + await profileWizard.continue(); + + // Wait for usage tracking pop-up window to appear + await profileWizard.optionallySelectUsageTracking(); + } ); + + it( 'can complete the product types section, Subscriptions copy is not visible', async () => { + // Query for the industries checkboxes + await profileWizard.industry.isDisplayed(); + await profileWizard.industry.selectIndustry( 'Health and beauty' ); + await profileWizard.continue(); + await profileWizard.productTypes.isDisplayed( 7 ); + await profileWizard.productTypes.selectProduct( 'Subscriptions' ); + await expect( page ).not.toMatchElement( 'p', { + text: + 'The following extensions will be added to your site for free: WooCommerce Payments. An account is required to use this feature.', + } ); + + await profileWizard.continue(); + await page.waitForNavigation( { waitUntil: 'networkidle0' } ); + } ); + + it( 'can complete the business details tab', async () => { + await profileWizard.business.isDisplayed(); + + await profileWizard.business.selectProductNumber( + config.get( 'onboardingwizard.numberofproducts' ) + ); + await profileWizard.business.selectCurrentlySelling( + config.get( 'onboardingwizard.sellingelsewhere' ) + ); + + await profileWizard.continue(); + } ); + + it( 'should display the WooCommerce Payments extension after it has been installed', async () => { + await profileWizard.business.freeFeaturesIsDisplayed(); + await profileWizard.business.expandRecommendedBusinessFeatures(); + + expect( page ).toMatchElement( 'a', { + text: 'WooCommerce Payments', + } ); + } ); + + it( 'should display the task "Add Subscriptions to my store"', async () => { + await profileWizard.navigate(); + await profileWizard.goToOBWStep( 'Store Details' ); + await profileWizard.skipStoreSetup(); + const homescreen = new WcHomescreen( page ); + await homescreen.isDisplayed(); + await homescreen.possiblyDismissWelcomeModal(); + const tasks = await homescreen.getTaskList(); + expect( tasks ).toContain( 'Add Subscriptions to my store' ); + } ); + + it( 'can select the Subscription option in the "Start with a template" modal', async () => { + const productsSetup = new ProductsSetup( page ); + await productsSetup.navigate(); + await productsSetup.isDisplayed(); + await productsSetup.clickStartWithTemplate(); + await productsSetup.isStartWithATemplateDisplayed( 3 ); + } ); + } ); + describe( 'A US store will see the Subscriptions inclusion', () => { + const profileWizard = new OnboardingWizard( page ); + const login = new Login( page ); + + beforeAll( async () => { + await resetWooCommerceState(); + } ); + + it( 'can complete the store details section', async () => { + await profileWizard.navigate(); + await profileWizard.storeDetails.completeStoreDetailsSection( { + countryRegionSubstring: 'cali', + countryRegionSelector: 'US\\:CA', + countryRegion: 'United States (US) — California', + } ); + + // Wait for "Continue" button to become active + await profileWizard.continue(); + + // Wait for usage tracking pop-up window to appear + await profileWizard.optionallySelectUsageTracking(); + } ); + + it( 'can complete the product types section, the Subscriptions copy now is visible', async () => { + // Query for the industries checkboxes + await profileWizard.industry.isDisplayed(); + await profileWizard.industry.selectIndustry( 'Health and beauty' ); + await profileWizard.continue(); + await profileWizard.productTypes.isDisplayed( 7 ); + await profileWizard.productTypes.selectProduct( 'Subscriptions' ); + await expect( page ).toMatchElement( 'p', { + text: + 'The following extensions will be added to your site for free: WooCommerce Payments. An account is required to use this feature.', + } ); + + await profileWizard.continue(); + await page.waitForNavigation( { waitUntil: 'networkidle0' } ); + } ); + + it( 'can complete the business details tab', async () => { + await profileWizard.business.isDisplayed(); + + await profileWizard.business.selectProductNumber( + config.get( 'onboardingwizard.numberofproducts' ) + ); + await profileWizard.business.selectCurrentlySelling( + config.get( 'onboardingwizard.sellingelsewhere' ) + ); + + await profileWizard.continue(); + } ); + + it( 'cannot see the WooCommerce Payments extension after it has been installed', async () => { + await profileWizard.business.freeFeaturesIsDisplayed(); + await profileWizard.business.expandRecommendedBusinessFeatures(); + + expect( page ).not.toMatchElement( 'a', { + text: 'WooCommerce Payments', + } ); + } ); + + it( 'should not display the task "Add Subscriptions to my store"', async () => { + await profileWizard.navigate(); + await profileWizard.goToOBWStep( 'Store Details' ); + await profileWizard.skipStoreSetup(); + const homescreen = new WcHomescreen( page ); + await homescreen.isDisplayed(); + await homescreen.possiblyDismissWelcomeModal(); + const tasks = await homescreen.getTaskList(); + expect( tasks ).not.toContain( 'Add Subscriptions to my store' ); + } ); + + it( 'can select the Subscription option in the "Start with a template" modal', async () => { + const productsSetup = new ProductsSetup( page ); + await productsSetup.navigate(); + await productsSetup.isDisplayed(); + await productsSetup.clickStartWithTemplate(); + await productsSetup.isStartWithATemplateDisplayed( 4 ); + } ); + } ); +}; + +const testBusinessDetailsForm = () => { + describe( 'A store that is selling elsewhere will see the "Number of employees” dropdown menu', () => { + const profileWizard = new OnboardingWizard( page ); + const login = new Login( page ); + + beforeAll( async () => { + await resetWooCommerceState(); + } ); + + afterAll( async () => { + await login.logout(); + } ); + + it( 'can complete the store details and product types sections', async () => { + await profileWizard.navigate(); + await profileWizard.storeDetails.isDisplayed(); + await profileWizard.storeDetails.completeStoreDetailsSection(); + + // Wait for "Continue" button to become active + await profileWizard.continue(); + + // Wait for usage tracking pop-up window to appear + await profileWizard.optionallySelectUsageTracking(); + + // Query for the industries checkboxes + await profileWizard.industry.isDisplayed(); + await profileWizard.industry.selectIndustry( + 'Fashion, apparel, and accessories' + ); + await profileWizard.continue(); + await profileWizard.productTypes.isDisplayed( 7 ); + // Select Physical + await profileWizard.productTypes.selectProduct( + 'Physical products' + ); + await profileWizard.productTypes.selectProduct( 'Downloads' ); + + await profileWizard.continue(); + await page.waitForNavigation( { + waitUntil: 'networkidle0', + } ); + } ); + + it( 'can complete the business details tab', async () => { + await profileWizard.business.isDisplayed(); + + await profileWizard.business.selectProductNumber( + config.get( 'onboardingwizard.numberofproducts' ) + ); + await profileWizard.business.selectCurrentlySelling( + config.get( 'onboardingwizard.sellingOnAnotherPlatform' ) + ); + expect( page ).toMatchElement( 'label', { + text: 'How many employees do you have?', + } ); + await profileWizard.business.selectEmployeesNumber( + config.get( 'onboardingwizard.number_employees' ) + ); + await profileWizard.business.selectRevenue( + config.get( 'onboardingwizard.revenue' ) + ); + await profileWizard.business.selectOtherPlatformName( + config.get( 'onboardingwizard.other_platform_name' ) + ); + + await profileWizard.continue(); + await profileWizard.business.expandRecommendedBusinessFeatures(); + await profileWizard.business.uncheckAllRecommendedBusinessFeatures(); + await profileWizard.continue(); + await profileWizard.themes.isDisplayed(); + } ); + } ); +}; + +const testAdminHomescreen = () => { + describe( 'Homescreen', () => { + const profileWizard = new OnboardingWizard( page ); + const homeScreen = new WcHomescreen( page ); + const login = new Login( page ); + + beforeAll( async () => { + await login.login(); + await resetWooCommerceState(); + await profileWizard.navigate(); + await profileWizard.skipStoreSetup(); + } ); + + afterAll( async () => { + await login.logout(); + } ); + + it( 'should not show welcome modal', async () => { + await homeScreen.isDisplayed(); + await expect( homeScreen.isWelcomeModalVisible() ).resolves.toBe( + false + ); + } ); + } ); +}; + +module.exports = { + testAdminOnboardingWizard, + testSelectiveBundleWCPay, + testDifferentStoreCurrenciesWCPay, + testSubscriptionsInclusion, + testBusinessDetailsForm, + testAdminHomescreen, +}; diff --git a/packages/js/admin-e2e-tests/src/specs/analytics/analytics-overview.ts b/packages/js/admin-e2e-tests/src/specs/analytics/analytics-overview.ts new file mode 100644 index 00000000000..2e47c12f794 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/specs/analytics/analytics-overview.ts @@ -0,0 +1,98 @@ +/** + * Internal dependencies + */ +import { AnalyticsOverview } from '../../pages/AnalyticsOverview'; +import { Login } from '../../pages/Login'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { afterAll, beforeAll, describe, it } = require( '@jest/globals' ); + +const testAdminAnalyticsOverview = () => { + describe( 'Analytics pages', () => { + const analyticsPage = new AnalyticsOverview( page ); + const login = new Login( page ); + const sectionTitles = [ 'Performance', 'Charts', 'Leaderboards' ]; + const titlesString = sectionTitles.join( ', ' ); + + beforeAll( async () => { + await login.login(); + await analyticsPage.navigate(); + await analyticsPage.isDisplayed(); + // Restore original order to sections + for ( let t = 0; t < sectionTitles.length; t++ ) { + const visibleSections = await analyticsPage.getSectionTitles(); + if ( visibleSections.indexOf( sectionTitles[ t ] ) < 0 ) { + await analyticsPage.addSection( sectionTitles[ t ] ); + } + } + } ); + afterAll( async () => { + await login.logout(); + } ); + + it( `a user should see ${ sectionTitles.length } sections by default - ${ titlesString }`, async () => { + const sections = await analyticsPage.getSectionTitles(); + for ( let t = 0; t < sectionTitles.length; t++ ) { + expect( sections ).toContain( sectionTitles[ t ] ); + } + } ); + + it( 'should allow a user to remove a section', async () => { + await analyticsPage.removeSection( sectionTitles[ 0 ] ); + const sections = await analyticsPage.getSectionTitles(); + expect( sections ).not.toContain( sectionTitles[ 0 ] ); + } ); + + it( 'should allow a user to add a section back in', async () => { + let sections = await analyticsPage.getSectionTitles(); + expect( sections ).not.toContain( sectionTitles[ 0 ] ); + await analyticsPage.addSection( sectionTitles[ 0 ] ); + + sections = await analyticsPage.getSectionTitles(); + expect( sections ).toContain( sectionTitles[ 0 ] ); + } ); + + describe( 'moving sections', () => { + it( 'should not display move up for the top, or move down for the bottom section', async () => { + const sections = await analyticsPage.getSections(); + for ( const section of sections ) { + const index = sections.indexOf( section ); + const menuItems = ( + await analyticsPage.getEllipsisMenuItems( + section.title + ) + ).map( ( item ) => item.title ); + if ( index === 0 ) { + expect( menuItems ).toContain( 'Move down' ); + expect( menuItems ).not.toContain( 'Move up' ); + } else if ( index === sections.length - 1 ) { + expect( menuItems ).not.toContain( 'Move down' ); + expect( menuItems ).toContain( 'Move up' ); + } else { + expect( menuItems ).toContain( 'Move down' ); + expect( menuItems ).toContain( 'Move up' ); + } + await analyticsPage.closeSectionEllipsis( section.title ); + } + } ); + + it( 'should allow a user to move a section down', async () => { + const sections = await analyticsPage.getSectionTitles(); + await analyticsPage.moveSectionDown( sections[ 0 ] ); + const newSections = await analyticsPage.getSectionTitles(); + expect( sections[ 0 ] ).toEqual( newSections[ 1 ] ); + expect( sections[ 1 ] ).toEqual( newSections[ 0 ] ); + } ); + + it( 'should allow a user to move a section up', async () => { + const sections = await analyticsPage.getSectionTitles(); + await analyticsPage.moveSectionUp( sections[ 1 ] ); + const newSections = await analyticsPage.getSectionTitles(); + expect( sections[ 0 ] ).toEqual( newSections[ 1 ] ); + expect( sections[ 1 ] ).toEqual( newSections[ 0 ] ); + } ); + } ); + } ); +}; + +module.exports = { testAdminAnalyticsOverview }; diff --git a/packages/js/admin-e2e-tests/src/specs/analytics/analytics.ts b/packages/js/admin-e2e-tests/src/specs/analytics/analytics.ts new file mode 100644 index 00000000000..65a8a41a1e4 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/specs/analytics/analytics.ts @@ -0,0 +1,86 @@ +/** + * Internal dependencies + */ +import { Analytics } from '../../pages/Analytics'; +import { Customers } from '../../pages/Customers'; +import { Login } from '../../pages/Login'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { afterAll, beforeAll, describe, it } = require( '@jest/globals' ); + +const testAdminAnalyticsPages = () => { + describe( 'Analytics pages', () => { + const analyticsPage = new Analytics( page ); + const customersPage = new Customers( page ); + const login = new Login( page ); + + beforeAll( async () => { + await login.login(); + } ); + afterAll( async () => { + await login.logout(); + } ); + + it( 'A user can view the analytics overview without it crashing', async () => { + await analyticsPage.navigate(); + await analyticsPage.isDisplayed(); + } ); + + it( 'A user can view the analytics for products without it crashing', async () => { + await analyticsPage.navigateToSection( 'products' ); + await analyticsPage.isDisplayed(); + } ); + + it( 'A user can view the analytics for revenue without it crashing', async () => { + await analyticsPage.navigateToSection( 'revenue' ); + await analyticsPage.isDisplayed(); + } ); + + it( 'A user can view the analytics for orders without it crashing', async () => { + await analyticsPage.navigateToSection( 'orders' ); + await analyticsPage.isDisplayed(); + } ); + + it( 'A user can view the analytics for variations without it crashing', async () => { + await analyticsPage.navigateToSection( 'variations' ); + await analyticsPage.isDisplayed(); + } ); + + it( 'A user can view the analytics for categories without it crashing', async () => { + await analyticsPage.navigateToSection( 'categories' ); + await analyticsPage.isDisplayed(); + } ); + + it( 'A user can view the analytics for coupons without it crashing', async () => { + await analyticsPage.navigateToSection( 'coupons' ); + await analyticsPage.isDisplayed(); + } ); + + it( 'A user can view the analytics for taxes without it crashing', async () => { + await analyticsPage.navigateToSection( 'taxes' ); + await analyticsPage.isDisplayed(); + } ); + + it( 'A user can view the analytics for downloads without it crashing', async () => { + await analyticsPage.navigateToSection( 'downloads' ); + await analyticsPage.isDisplayed(); + } ); + + it( 'A user can view the analytics for stock without it crashing', async () => { + await analyticsPage.navigateToSection( 'stock' ); + await analyticsPage.isDisplayed(); + } ); + + it( 'A user can view the analytics for settings without it crashing', async () => { + await analyticsPage.navigateToSection( 'settings' ); + await analyticsPage.isDisplayed(); + } ); + + it( 'A user can view the customers page without it crashing', async () => { + await customersPage.navigate(); + await customersPage.isDisplayed(); + } ); + } ); +}; + +module.exports = { testAdminAnalyticsPages }; diff --git a/packages/js/admin-e2e-tests/src/specs/homescreen/activity-panel.ts b/packages/js/admin-e2e-tests/src/specs/homescreen/activity-panel.ts new file mode 100644 index 00000000000..ef234bfa37b --- /dev/null +++ b/packages/js/admin-e2e-tests/src/specs/homescreen/activity-panel.ts @@ -0,0 +1,135 @@ +/** + * External dependencies + */ +import { createSimpleProduct, withRestApi } from '@woocommerce/e2e-utils'; + +/** + * Internal dependencies + */ +import { Login } from '../../pages/Login'; +import { OnboardingWizard } from '../../pages/OnboardingWizard'; +import { WcHomescreen } from '../../pages/WcHomescreen'; +import { + createOrder, + removeAllOrders, + unhideTaskList, + runActionScheduler, + updateOption, + resetWooCommerceState, +} from '../../fixtures'; +import { OrdersActivityPanel } from '../../elements/OrdersActivityPanel'; +import { addReviewToProduct, waitForElementByText } from '../../utils/actions'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { afterAll, beforeAll, describe, it } = require( '@jest/globals' ); +/* eslint-enable @typescript-eslint/no-var-requires */ + +const simpleProductName = 'Simple order'; +const testAdminHomescreenActivityPanel = () => { + describe( 'Homescreen activity panel', () => { + const profileWizard = new OnboardingWizard( page ); + const homeScreen = new WcHomescreen( page ); + const ordersPanel = new OrdersActivityPanel( page ); + const login = new Login( page ); + + beforeAll( async () => { + await login.login(); + await resetWooCommerceState(); + await profileWizard.navigate(); + await profileWizard.skipStoreSetup(); + + await homeScreen.isDisplayed(); + await homeScreen.possiblyDismissWelcomeModal(); + } ); + + afterAll( async () => { + await withRestApi.deleteAllProducts(); + await removeAllOrders(); + await unhideTaskList( 'setup' ); + await runActionScheduler(); + await updateOption( 'woocommerce_task_list_hidden', 'no' ); + await login.logout(); + } ); + + it( 'should not show activity panel while task list is displayed', async () => { + await expect( homeScreen.isTaskListDisplayed() ).resolves.toBe( + true + ); + await expect( homeScreen.isActivityPanelShown() ).resolves.toBe( + false + ); + } ); + + it( 'should not show panels when there are no orders or products yet with task list hidden', async () => { + await homeScreen.hideTaskList(); + await expect( homeScreen.isTaskListDisplayed() ).resolves.toBe( + false + ); + await expect( homeScreen.isActivityPanelShown() ).resolves.toBe( + false + ); + } ); + + it( 'should show Reviews panel when we have at-least one product', async () => { + const productId = await createSimpleProduct( + simpleProductName, + '9.99' + ); + await addReviewToProduct( productId, simpleProductName ); + await homeScreen.navigate(); + await homeScreen.isDisplayed(); + const activityPanels = await homeScreen.getActivityPanels(); + expect( activityPanels ).toHaveLength( 1 ); + expect( activityPanels ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { title: 'Reviews' } ), + ] ) + ); + } ); + + it( 'should show Orders and Stock panels when at-least one order is added', async () => { + await createOrder(); + await page.reload( { + waitUntil: [ 'networkidle0', 'domcontentloaded' ], + } ); + const activityPanels = await homeScreen.getActivityPanels(); + expect( activityPanels ).toHaveLength( 3 ); + expect( activityPanels ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { title: 'Orders' } ), + ] ) + ); + expect( activityPanels ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { title: 'Stock' } ), + ] ) + ); + } ); + + describe( 'Orders panel', () => { + it( 'should show: "you have fullfilled all your orders" when expanding Orders panel if no actionable orders', async () => { + await homeScreen.expandActivityPanel( 'Orders' ); + await waitForElementByText( + 'h4', + 'You’ve fulfilled all your orders' + ); + await expect( page ).toMatchElement( 'h4', { + text: 'You’ve fulfilled all your orders', + } ); + } ); + + it( 'should show actionable Orders when expanding Orders panel', async () => { + const order1 = await createOrder( 'processing' ); + const order2 = await createOrder( 'on-hold' ); + await homeScreen.navigate(); + await homeScreen.expandActivityPanel( 'Orders' ); + const orders = await ordersPanel.getDisplayedOrders(); + expect( orders ).toHaveLength( 2 ); + expect( orders ).toContain( `Order #${ order1.id }` ); + expect( orders ).toContain( `Order #${ order2.id }` ); + } ); + } ); + } ); +}; + +module.exports = { testAdminHomescreenActivityPanel }; diff --git a/packages/js/admin-e2e-tests/src/specs/homescreen/task-list.ts b/packages/js/admin-e2e-tests/src/specs/homescreen/task-list.ts new file mode 100644 index 00000000000..669ac4d2493 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/specs/homescreen/task-list.ts @@ -0,0 +1,77 @@ +/** + * External dependencies + */ +import { takeScreenshotFor } from '@woocommerce/e2e-environment'; + +/** + * Internal dependencies + */ +import { Login } from '../../pages/Login'; +import { OnboardingWizard } from '../../pages/OnboardingWizard'; +import { WcHomescreen } from '../../pages/WcHomescreen'; +import { TaskTitles } from '../../constants/taskTitles'; +import { HelpMenu } from '../../elements/HelpMenu'; +import { WcSettings } from '../../pages/WcSettings'; +import { resetWooCommerceState, unhideTaskList } from '../../fixtures'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { afterAll, beforeAll, describe, it } = require( '@jest/globals' ); +/* eslint-enable @typescript-eslint/no-var-requires */ + +const testAdminHomescreenTasklist = () => { + describe( 'Homescreen task list', () => { + const profileWizard = new OnboardingWizard( page ); + const homeScreen = new WcHomescreen( page ); + const helpMenu = new HelpMenu( page ); + const settings = new WcSettings( page ); + const login = new Login( page ); + + beforeAll( async () => { + await login.login(); + await resetWooCommerceState(); + + // This makes this test more isolated, by always navigating to the + // profile wizard and skipping, this behaves the same as if the + // profile wizard had not been run yet and the user is redirected + // to it when trying to go to wc-admin. + await profileWizard.navigate(); + await profileWizard.skipStoreSetup(); + + await homeScreen.isDisplayed(); + await homeScreen.possiblyDismissWelcomeModal(); + await takeScreenshotFor( 'WooCommerce Admin Home Screen' ); + } ); + + afterAll( async () => { + await unhideTaskList( 'setup' ); + await login.logout(); + } ); + + it( 'should show 6 or more tasks on the home screen', async () => { + const tasks = await homeScreen.getTaskList(); + expect( tasks.length ).toBeGreaterThanOrEqual( 6 ); + expect( tasks ).toContain( TaskTitles.storeDetails ); + expect( tasks ).toContain( TaskTitles.addProducts ); + expect( tasks ).toContain( TaskTitles.taxSetup ); + expect( tasks ).toContain( TaskTitles.personalizeStore ); + } ); + + it( 'should be able to hide the task list', async () => { + await homeScreen.hideTaskList(); + expect( await homeScreen.isTaskListDisplayed() ).toBe( false ); + } ); + + it( 'should be able to show the task list again through the help menu', async () => { + await settings.navigate(); + await helpMenu.openHelpMenu(); + await helpMenu.enableTaskList(); + // redirects to homescreen + await homeScreen.isDisplayed(); + await expect( homeScreen.isTaskListDisplayed() ).resolves.toBe( + true + ); + } ); + } ); +}; + +module.exports = { testAdminHomescreenTasklist }; diff --git a/packages/js/admin-e2e-tests/src/specs/index.ts b/packages/js/admin-e2e-tests/src/specs/index.ts new file mode 100644 index 00000000000..dcb88e59853 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/specs/index.ts @@ -0,0 +1,9 @@ +export * from './activate-and-setup/basic-setup'; +export * from './activate-and-setup/complete-onboarding-wizard'; +export * from './analytics/analytics'; +export * from './analytics/analytics-overview'; +export * from './marketing/coupons'; +export * from './tasks/payment'; +export * from './tasks/purchase'; +export * from './homescreen/task-list'; +export * from './homescreen/activity-panel'; diff --git a/packages/js/admin-e2e-tests/src/specs/marketing/coupons.ts b/packages/js/admin-e2e-tests/src/specs/marketing/coupons.ts new file mode 100644 index 00000000000..9d19dcf0895 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/specs/marketing/coupons.ts @@ -0,0 +1,30 @@ +/** + * Internal dependencies + */ +import { Coupons } from '../../pages/Coupons'; +import { Login } from '../../pages/Login'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { afterAll, beforeAll, describe, it } = require( '@jest/globals' ); +/* eslint-enable @typescript-eslint/no-var-requires */ + +const testAdminCouponsPage = () => { + describe( 'Coupons page', () => { + const couponsPage = new Coupons( page ); + const login = new Login( page ); + + beforeAll( async () => { + await login.login(); + } ); + afterAll( async () => { + await login.logout(); + } ); + + it( 'A user can view the coupons overview without it crashing', async () => { + await couponsPage.navigate(); + await couponsPage.isDisplayed(); + } ); + } ); +}; + +module.exports = { testAdminCouponsPage }; diff --git a/packages/js/admin-e2e-tests/src/specs/tasks/payment.ts b/packages/js/admin-e2e-tests/src/specs/tasks/payment.ts new file mode 100644 index 00000000000..9d377b05db0 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/specs/tasks/payment.ts @@ -0,0 +1,90 @@ +/** + * External dependencies + */ +import { takeScreenshotFor } from '@woocommerce/e2e-environment'; + +/** + * Internal dependencies + */ +import { Login } from '../../pages/Login'; +import { OnboardingWizard } from '../../pages/OnboardingWizard'; +import { PaymentsSetup } from '../../pages/PaymentsSetup'; +import { WcHomescreen } from '../../pages/WcHomescreen'; +import { BankAccountTransferSetup } from '../../sections/payment-setup/BankAccountTransferSetup'; +import { waitForTimeout } from '../../utils/actions'; +import { WcSettings } from '../../pages/WcSettings'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { afterAll, beforeAll, describe, it } = require( '@jest/globals' ); +/* eslint-enable @typescript-eslint/no-var-requires */ + +const testAdminPaymentSetupTask = () => { + describe( 'Payment setup task', () => { + const profileWizard = new OnboardingWizard( page ); + const homeScreen = new WcHomescreen( page ); + const paymentsSetup = new PaymentsSetup( page ); + const bankTransferSetup = new BankAccountTransferSetup( page ); + const login = new Login( page ); + const settings = new WcSettings( page ); + + beforeAll( async () => { + await login.login(); + + // This makes this test more isolated, by always navigating to the + // profile wizard and skipping, this behaves the same as if the + // profile wizard had not been run yet and the user is redirected + // to it when trying to go to wc-admin. + await profileWizard.navigate(); + await profileWizard.skipStoreSetup(); + + await homeScreen.isDisplayed(); + await takeScreenshotFor( 'Payment setup task home screen' ); + await homeScreen.possiblyDismissWelcomeModal(); + } ); + + afterAll( async () => { + await login.logout(); + } ); + + it( 'Can visit the payment setup task from the homescreen if the setup wizard has been skipped', async () => { + await homeScreen.clickOnTaskList( 'Set up payments' ); + await paymentsSetup.possiblyCloseHelpModal(); + await paymentsSetup.isDisplayed(); + } ); + + it( 'Saving valid bank account transfer details enables the payment method', async () => { + await paymentsSetup.showOtherPaymentMethods(); + await paymentsSetup.goToPaymentMethodSetup( 'bacs' ); + await bankTransferSetup.saveAccountDetails( { + accountNumber: '1234', + accountName: 'Savings', + bankName: 'TestBank', + sortCode: '12', + iban: '12 3456 7890', + swiftCode: 'ABBA', + } ); + await waitForTimeout( 1500 ); + expect( await settings.paymentMethodIsEnabled( 'bacs' ) ).toBe( + true + ); + } ); + + it( 'Enabling cash on delivery enables the payment method', async () => { + await settings.cleanPaymentMethods(); + await homeScreen.navigate(); + await homeScreen.isDisplayed(); + await waitForTimeout( 1000 ); + await homeScreen.clickOnTaskList( 'Set up payments' ); + await paymentsSetup.possiblyCloseHelpModal(); + await paymentsSetup.isDisplayed(); + await paymentsSetup.showOtherPaymentMethods(); + await paymentsSetup.enableCashOnDelivery(); + await waitForTimeout( 1500 ); + expect( await settings.paymentMethodIsEnabled( 'cod' ) ).toBe( + true + ); + } ); + } ); +}; + +module.exports = { testAdminPaymentSetupTask }; diff --git a/packages/js/admin-e2e-tests/src/specs/tasks/purchase.ts b/packages/js/admin-e2e-tests/src/specs/tasks/purchase.ts new file mode 100644 index 00000000000..8c0e59eb558 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/specs/tasks/purchase.ts @@ -0,0 +1,100 @@ +/** + * Internal dependencies + */ +import { resetWooCommerceState } from '../../fixtures'; +import { Login } from '../../pages/Login'; +import { OnboardingWizard } from '../../pages/OnboardingWizard'; +import { WcHomescreen } from '../../pages/WcHomescreen'; +import { getElementByText, waitForElementByText } from '../../utils/actions'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { afterAll, beforeAll, describe, it } = require( '@jest/globals' ); +/* eslint-enable @typescript-eslint/no-var-requires */ + +const testAdminPurchaseSetupTask = () => { + describe( 'Purchase setup task', () => { + const profileWizard = new OnboardingWizard( page ); + const homeScreen = new WcHomescreen( page ); + const login = new Login( page ); + + beforeAll( async () => { + await login.login(); + } ); + + afterAll( async () => { + await login.logout(); + } ); + + describe( 'selecting paid product', () => { + beforeAll( async () => { + await resetWooCommerceState(); + + await profileWizard.navigate(); + await profileWizard.walkThroughAndCompleteOnboardingWizard( { + products: [ 'Memberships' ], + } ); + + await homeScreen.isDisplayed(); + await homeScreen.possiblyDismissWelcomeModal(); + } ); + + it( 'should display add to my store task', async () => { + expect( + await getElementByText( '*', 'Add Memberships to my store' ) + ).toBeDefined(); + } ); + + it( 'should show paid features modal with option to buy now', async () => { + const task = await getElementByText( + '*', + 'Add Memberships to my store' + ); + await task?.click(); + await waitForElementByText( + 'h1', + 'Would you like to add the following paid features to your store now?' + ); + expect( + await getElementByText( 'button', 'Buy now' ) + ).toBeDefined(); + } ); + } ); + + describe( 'selecting paid theme', () => { + beforeAll( async () => { + await resetWooCommerceState(); + + await profileWizard.navigate(); + await profileWizard.walkThroughAndCompleteOnboardingWizard( { + themeTitle: 'Blooms', + } ); + + await homeScreen.isDisplayed(); + await homeScreen.possiblyDismissWelcomeModal(); + } ); + + it( 'should display add to my store task', async () => { + expect( + await getElementByText( '*', 'Add Blooms to my store' ) + ).toBeDefined(); + } ); + + it( 'should show paid features modal with option to buy now', async () => { + const task = await getElementByText( + '*', + 'Add Blooms to my store' + ); + await task?.click(); + await waitForElementByText( + 'h1', + 'Would you like to add the following paid features to your store now?' + ); + expect( + await getElementByText( 'button', 'Buy now' ) + ).toBeDefined(); + } ); + } ); + } ); +}; + +module.exports = { testAdminPurchaseSetupTask }; diff --git a/packages/js/admin-e2e-tests/src/utils/actions.ts b/packages/js/admin-e2e-tests/src/utils/actions.ts new file mode 100644 index 00000000000..54218cbd227 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/utils/actions.ts @@ -0,0 +1,269 @@ +/** + * External dependencies + */ +import { ElementHandle } from 'puppeteer'; + +/** + * Internal dependencies + */ +import { Login } from '../pages/Login'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { expect } = require( '@jest/globals' ); +const config = require( 'config' ); +/* eslint-enable @typescript-eslint/no-var-requires */ + +/** + * Wait for UI blocking to end. + */ +const uiUnblocked = async (): Promise< void > => { + await page.waitForFunction( + () => ! Boolean( document.querySelector( '.blockUI' ) ) + ); +}; + +/** + * Suspend processing for the specified time + * + * @param {number} timeout in milliseconds + */ +const waitForTimeout = async ( timeout: number ): Promise< void > => { + await new Promise( ( resolve ) => setTimeout( resolve, timeout ) ); +}; + +/** + * Publish, verify that item was published. Trash, verify that item was trashed. + * + * @param {string} button (Publish) + * @param {string} publishNotice + * @param {string} publishVerification + */ +const verifyPublishAndTrash = async ( + button: string, + publishNotice: string, + publishVerification: string, + trashVerification: string +): Promise< void > => { + // Wait for auto save + await waitForTimeout( 2000 ); + // Publish + await page.click( button ); + + // Verify + await expect( page ).toMatchElement( publishNotice, { + text: publishVerification, + } ); + if ( button === '.order_actions li .save_order' ) { + await expect( page ).toMatchElement( + '#select2-order_status-container', + { text: 'Processing' } + ); + await expect( page ).toMatchElement( + '#woocommerce-order-notes .note_content', + { + text: + 'Order status changed from Pending payment to Processing.', + } + ); + } + + // Trash + await expect( page ).toClick( 'a', { text: 'Move to Trash' } ); + await page.waitForSelector( '#message' ); + + // Verify + await expect( page ).toMatchElement( publishNotice, { + text: trashVerification, + } ); +}; + +const hasClass = async ( + element: ElementHandle, + elementClass: string +): Promise< boolean > => { + const classNameProp = await element.getProperty( 'className' ); + const classNameValue = ( await classNameProp.jsonValue() ) as string; + + return classNameValue.includes( elementClass ); +}; + +const getInputValue = async ( selector: string ): Promise< unknown > => { + const field = await page.$( selector ); + if ( field ) { + const fieldValue = await ( + await field.getProperty( 'value' ) + ).jsonValue(); + + return fieldValue; + } + return null; +}; + +const getAttribute = async ( + selector: string, + attribute: string +): Promise< unknown > => { + await page.focus( selector ); + const field = await page.$( selector ); + if ( field ) { + const fieldValue = await ( + await field.getProperty( attribute ) + ).jsonValue(); + + return fieldValue; + } + return null; +}; + +const getElementByText = async ( + element: string, + text: string, + parentSelector?: string +): Promise< ElementHandle | null > => { + let parent: ElementHandle | null = null; + if ( parentSelector ) { + parent = await page.$( parentSelector ); + } + const els = await ( parent || page ).$x( + `//${ element }[contains(text(), "${ text }")]` + ); + return els[ 0 ]; +}; + +const getElementByAttributeAndValue = async ( + element: string, + attribute: string, + value: string, + parentSelector?: string +): Promise< ElementHandle | null > => { + let parent: ElementHandle | null = null; + if ( parentSelector ) { + parent = await page.$( parentSelector ); + } + const els = await ( parent || page ).$x( + `//${ element }[@${ attribute }="${ value }"]` + ); + return els[ 0 ]; +}; + +const waitForElementByText = async ( + element: string, + text: string, + options?: { timeout?: number } +): Promise< ElementHandle | null > => { + const els = await page.waitForXPath( + `//${ element }[contains(text(), "${ text }")]`, + options + ); + return els; +}; + +export const waitForElementByTextWithoutThrow = async ( + element: string, + text: string, + timeoutInSeconds = 5 +): Promise< boolean > => { + let selected = await getElementByText( element, text ); + for ( let s = 0; s < timeoutInSeconds; s++ ) { + if ( selected ) { + break; + } + await waitForTimeout( 1000 ); + selected = await getElementByText( element, text ); + } + return Boolean( selected ); +}; + +const waitUntilElementStopsMoving = async ( selector: string ) => { + return await page.waitForFunction( + ( elementSelector ) => { + const element = document.querySelector( elementSelector ); + const elementRect = element.getBoundingClientRect(); + const jsWindow: Window & + typeof globalThis & { + elementX?: number; + elementY?: number; + } = window; + + if ( + jsWindow.elementX !== elementRect.x.toFixed( 1 ) || + jsWindow.elementY !== elementRect.y.toFixed( 1 ) + ) { + jsWindow.elementX = elementRect.x.toFixed( 1 ); + jsWindow.elementY = elementRect.y.toFixed( 1 ); + return false; + } + + delete jsWindow.elementX; + delete jsWindow.elementY; + return true; + }, + {}, + selector + ); +}; + +const deactivateAndDeleteExtension = async ( + extension: string +): Promise< void > => { + const baseUrl = config.get( 'url' ); + const pluginsAdmin = 'wp-admin/plugins.php?plugin_status=all&paged=1&s'; + await page.goto( baseUrl + pluginsAdmin, { + waitUntil: 'networkidle0', + timeout: 10000, + } ); + await waitForElementByText( 'h1', 'Plugins' ); + // deactivate extension + const deactivateExtension = await page.$( `#deactivate-${ extension }` ); + await deactivateExtension?.click(); + await waitForElementByText( 'p', 'Plugin deactivated.' ); + // delete extension + const deleteExtension = await page.$( `#delete-${ extension }` ); + await deleteExtension?.click(); +}; + +const addReviewToProduct = async ( productId: number, productName: string ) => { + // we need a guest user + const login = new Login( page ); + await login.logout(); + + const baseUrl = config.get( 'url' ); + const productUrl = `/?p=${ productId }`; + await page.goto( baseUrl + productUrl, { + waitUntil: 'networkidle0', + timeout: 10000, + } ); + await waitForElementByText( 'h1', productName ); + + // Reviews tab + const reviewTab = await page.$( '#tab-title-reviews' ); + await reviewTab?.click(); + const fiveStars = await page.$( '.star-5' ); + await fiveStars?.click(); + + // write a comment + await page.type( '#comment', 'My comment' ); + await page.type( '#author', 'John Doe' ); + await page.type( '#email', 'john.doe@john.doe' ); + + const submit = await page.$( '#submit' ); + await submit?.click(); + // the comment was published + await waitForElementByText( 'p', 'My comment' ); + await login.login(); +}; + +export { + uiUnblocked, + verifyPublishAndTrash, + getInputValue, + getAttribute, + getElementByText, + getElementByAttributeAndValue, + waitForElementByText, + waitUntilElementStopsMoving, + hasClass, + waitForTimeout, + deactivateAndDeleteExtension, + addReviewToProduct, +}; diff --git a/packages/js/admin-e2e-tests/tsconfig.json b/packages/js/admin-e2e-tests/tsconfig.json new file mode 100644 index 00000000000..0957caa0bac --- /dev/null +++ b/packages/js/admin-e2e-tests/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "build" + } +} diff --git a/packages/js/admin-e2e-tests/typings/index.d.ts b/packages/js/admin-e2e-tests/typings/index.d.ts new file mode 100644 index 00000000000..99697491829 --- /dev/null +++ b/packages/js/admin-e2e-tests/typings/index.d.ts @@ -0,0 +1,2 @@ +declare module '@woocommerce/e2e-environment'; +declare module '@woocommerce/e2e-utils'; diff --git a/packages/js/api-core-tests/.env.example b/packages/js/api-core-tests/.env.example new file mode 100644 index 00000000000..7f70ce7a99f --- /dev/null +++ b/packages/js/api-core-tests/.env.example @@ -0,0 +1,21 @@ +# .env.exmaple + +# Your site's base URL, not including a trailing slash +# BASE_URL="https://mysite.com" +BASE_URL="" + +# The admin user's username or generated consumer key +# USER_KEY="admin" +# USER_KEY="ck_1234" +USER_KEY="" + +# The admin user's password or generated consumer secret +# USER_SECRET="password" +# USER_SECRET="cs_1234" +USER_SECRET="" + +# Optional setting to output verbose logs from Jest +# VERBOSE=true + +# Optional setting to use index permalinks +# USE_INDEX_PERMALINKS=true diff --git a/packages/js/api-core-tests/.gitignore b/packages/js/api-core-tests/.gitignore new file mode 100644 index 00000000000..80ab55bf63f --- /dev/null +++ b/packages/js/api-core-tests/.gitignore @@ -0,0 +1,6 @@ +# Collection output +collection.json + +# Allure directories +allure-report +allure-results diff --git a/packages/js/api-core-tests/CHANGELOG.md b/packages/js/api-core-tests/CHANGELOG.md new file mode 100644 index 00000000000..482fc7ceaec --- /dev/null +++ b/packages/js/api-core-tests/CHANGELOG.md @@ -0,0 +1,17 @@ +# Unreleased + +## Added +- Shipping Zones API Tests +- Shipping Methods API Tests +- Complex Order API Tests + +# 0.1.0 + +- Initial/beta release + +## Added +- Coupons API Tests +- Refunds API Tests +- Products API Tests +- CRUD tests for the Orders API +- Order Search API Tests diff --git a/packages/js/api-core-tests/NEXT_CHANGELOG.md b/packages/js/api-core-tests/NEXT_CHANGELOG.md new file mode 100644 index 00000000000..7195284dfd0 --- /dev/null +++ b/packages/js/api-core-tests/NEXT_CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +--- + +[See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob//packages/js/api-core-tests/CHANGELOG.md). diff --git a/packages/js/api-core-tests/README.md b/packages/js/api-core-tests/README.md new file mode 100644 index 00000000000..8d9f4220043 --- /dev/null +++ b/packages/js/api-core-tests/README.md @@ -0,0 +1,162 @@ +# WooCommerce Core API Test Suite + +This package contains automated API tests for WooCommerce. + +## Environment variables + +Before running the tests, the following environment variables need to be configured as shown in `.env.example`: + +``` +# Your site's base URL, not including a trailing slash +BASE_URL="https://mysite.com" + +# The admin user's username or generated consumer key +USER_KEY="" + +# The admin user's password or generated consumer secret +USER_SECRET="" +``` + +For local setup, create a `.env` file in this folder with the three required values described above. + +Alternatively, these values can be passed in via the command line. For example: + +```shell +BASE_URL=http://localhost:8084 USER_KEY=admin USER_SECRET=password npm run test:api +``` + +When using a username and password combination instead of a consumer secret and consumer key, make sure to have the [JSON Basic Authentication plugin](https://github.com/WP-API/Basic-Auth) installed and activated on the test site. + +For more information about authentication with the WooCommerce API, please see the [Authentication](https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#authentication) section in the WooCommerce REST API documentation. + +### Optional variables + +The following optional variables can be set in your local `.env` file: + +* `VERBOSE`: determine whether each individual test should be reported during the run. +* `USE_INDEX_PERMALINKS`: determine whether to use index permalinks (`?p=123`) for the API route. + +## Running tests + +### Test API connection + +To verify that everything is configured correctly, the following test script is available: + +```shell +npm run test:hello +``` + +This tests connectivity to the API by validating connection to the following: + +* A non-authenticated endpoint: [Index](https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#index) +* An endpoint requiring authentication: [System status properties](https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#system-status-properties) + +### Run all tests + +To run all of the API tests, you can use the following command: + +```shell +npm run test:api +``` + +### Running groups of tests + +To run a specific group of tests, you can use the `npm test -- --group=` command and pass in the group's name you want to run. + +For example, if you wanted to only run the orders API tests, you can use the following: + +```shell +npm test -- --group=orders +``` + +Alternatively, you can use `jest` to run test groups: + +```shell +jest --group=api +``` + +## Writing tests + +### Conventions + +1. All tests should be placed in the `tests` directory. +1. Always provide a JS doc in all tests, and tag them with `@group`. See the [Test groups](#test-groups) section for more info on grouping tests. +1. Use functions in the `data` folder when generating test data instead of constructing them from scratch within the test. +1. Use functions in the `endpoints` folder to send requests instead of directly using SuperTest's `request()` function. +1. Use `describe.each()` or `it.each()` when writing repetitive tests. +1. Always clean up all test data generated by the tests. + +### Test groups + +This package makes use of the `jest-runner-groups` package, which allows grouping tests together around semantic groups (such as `orders` API calls, or `coupons` API calls) to make running test suites more granular. + +Before the `describe()` statement, add in a doc block containing the desired groups: + +```javascript +/** + * Tests for the WooCommerce API. + * + * @group api + * @group endpoint + * + */ +describe('', () => { + it('', async () => {}); +}); +``` + +The `api` group should be included on all tests that should be run with the rest of the test suite. Groups can also contain a path, such as `orders/delete`. + +For more information on how groups work, please refer to the [`jest-runner-groups` documentation](https://www.npmjs.com/package/jest-runner-groups). + +## Using query strings + +For tests that use query strings, these can be passed into the `getRequest()` method using an object of one or more key value pairs: + +```javascript +const { getRequest } = require('./utils/request'); + +const queryString = { + dates_are_gmt: true, + after: '2021-05-13T19:00:00', + before: '2021-05-13T22:00:00' +}; + +const response = await getRequest('/orders', queryString); +``` + +## Creating test data + +Most of the time, test data would be in the form of a request payload. Instead of building them from scratch inside the test, create a test data file inside the `data` directory. Create a model of the request payload within that file, and export it as an object or a function that generates this object. + +Afterwards, make sure to add the test data file to the `data/index.js` file. + +This way, the test data would be decoupled from the test itself, allowing for easier test data management, and more readable tests. + +## Creating endpoint functions + +All functions for sending requests to endpoints should be placed in the `endpoints` directory. + +Newly created files should be added to the `endpoints/index.js` file. + +## Debugging tests + +You can make use of the REST API log plugin to see how requests are being made, and check the request payload, response, and more. + +[REST API Log](https://wordpress.org/plugins/wp-rest-api-log/) + +## Generate a Postman Collection + +This package also allows generating a `collection.json` file using the test data in this package. This file can be imported into Postman and other REST clients that support the Postman v2 collection. To generate this file, run: + +``` +npm run make:collection +``` + +This will output a `collection.json` file in this directory. + +## Resources + +This package makes use of the [SuperTest HTTP assertion package](https://www.npmjs.com/package/supertest). For more information on the `response` properties that are available can be found in the [SuperAgent documentation](https://visionmedia.github.io/superagent/#response-properties). + +For the list of WooCommerce API endpoints, expected responses, and more, please see the [WooCommerce REST API Documentation](https://woocommerce.github.io/woocommerce-rest-api-docs/). diff --git a/packages/js/api-core-tests/allure.config.js b/packages/js/api-core-tests/allure.config.js new file mode 100644 index 00000000000..340065110e1 --- /dev/null +++ b/packages/js/api-core-tests/allure.config.js @@ -0,0 +1,15 @@ +/** + * + * Use this configuration file to set certain Allure options. + * + */ + +// ALLURE_OUTPUT_DIR is the environment variable for the directory where you want the "allure-results" and "allure-report" folders to be generated in. +const { ALLURE_OUTPUT_DIR } = process.env; + +// If ALLURE_OUTPUT_DIR was specified, use it as the target for the "allure-results" directory. +if ( ALLURE_OUTPUT_DIR ) { + reporter.allure.setOptions( { + targetDir: `${ ALLURE_OUTPUT_DIR }/allure-results`, + } ); +} diff --git a/packages/js/api-core-tests/bin/wc-api-tests.sh b/packages/js/api-core-tests/bin/wc-api-tests.sh new file mode 100755 index 00000000000..24054b1bc2b --- /dev/null +++ b/packages/js/api-core-tests/bin/wc-api-tests.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# +# Run package scripts +# + +# Script help +usage() { + echo 'usage: npx wc-api-tests + 'channel', + ) + ); + + add_settings_field( + 'wc-beta-tester-auto-update', + __( 'Automatic Updates', 'woocommerce-beta-tester' ), + array( $this, 'automatic_update_checkbox_html' ), + 'wc-beta-tester', + 'wc-beta-tester-update', + array( + 'label_for' => 'auto_update', + ) + ); + } + + /** + * Update section HTML output. + * + * @param array $args Arguments. + */ + public function update_section_html( $args ) { + ?> +

    + array( + 'name' => __( 'Beta Releases', 'woocommerce-beta-tester' ), + 'description' => __( 'Beta releases contain experimental functionality for testing purposes only. This channel will also include RC and stable releases if more current.', 'woocommerce-beta-tester' ), + ), + 'rc' => array( + 'name' => __( 'Release Candidates', 'woocommerce-beta-tester' ), + 'description' => __( 'Release candidates are released to ensure any critical problems have not gone undetected. This channel will also include stable releases if more current.', 'woocommerce-beta-tester' ), + ), + 'stable' => array( + 'name' => __( 'Stable Releases', 'woocommerce-beta-tester' ), + 'description' => __( 'This is the default behavior in WordPress.', 'woocommerce-beta-tester' ), + ), + ); + echo '
    ' . esc_html__( 'Update Channel', 'woocommerce-beta-tester' ) . ''; + foreach ( $channels as $channel_id => $channel ) { + ?> + +
    + '; + } + + /** + * Auto updates checkbox markup output. + * + * @param array $args Arguments. + */ + public function automatic_update_checkbox_html( $args ) { + $settings = WC_Beta_Tester::get_settings(); + ?> + + +
    +

    +
    + +
    +
    + add_hooks(); + } + + /** + * Hook into WordPress. + */ + public function add_hooks() { + add_action( 'admin_menu', array( $this, 'add_to_menu' ), 55 ); + add_action( 'wp_ajax_' . static::AJAX_HOOK, array( $this, 'export_settings' ) ); + } + /** + * Add options page to menu + */ + public function add_to_menu() { + add_submenu_page( 'plugins.php', __( 'WC Beta Tester Import/Export', 'woocommerce-beta-tester' ), __( 'WC Import/Export', 'woocommerce-beta-tester' ), static::IMPORT_CAP, 'wc-beta-tester-settings', array( $this, 'settings_page_html' ) ); + } + + /** + * Output settings HTML + */ + public function settings_page_html() { + if ( ! current_user_can( static::IMPORT_CAP ) ) { + return; + } + + $export_url = wp_nonce_url( admin_url( 'admin-ajax.php?action=wc_beta_tester_export_settings' ), static::NONCE_ACTION ); + $this->maybe_import_settings(); + + // show error/update messages. + if ( ! empty( $this->message ) ) { + ?> +
    message['message'] ); ?>
    + +
    +

    +

    + +
    +
    + + +

    + + +
    +
    + get_settings() ); + exit; + } + + /** + * Import settings in json format if submitted. + */ + public function maybe_import_settings() { + if ( empty( $_POST ) || empty( $_POST['action'] ) || $_POST['action'] !== static::IMPORT_ACTION ) { + return; + } + + if ( ! wp_verify_nonce( $_POST['_wpnonce'], static::NONCE_ACTION ) ) { + $this->add_message( __( 'Invalid submission', 'woocommerce-beta-tester' ) ); + return; + } + + if ( empty( $_FILES[ static::IMPORT_FILENAME ] ) ) { + $this->add_message( __( 'No file uploaded.', 'woocommerce-beta-tester' ) ); + return; + } + + $tmp_file = $_FILES[ static::IMPORT_FILENAME ]['tmp_name']; + if ( empty( $tmp_file ) ) { + $this->add_message( __( 'No file uploaded.', 'woocommerce-beta-tester' ) ); + return; + } + + if ( ! is_readable( $tmp_file ) ) { + $this->add_message( __( 'File could not be read.', 'woocommerce-beta-tester' ) ); + return; + } + + $maybe_json = file_get_contents( $tmp_file ); + $settings = json_decode( $maybe_json, true ); + if ( $settings !== null ) { + foreach ( $this->get_setting_list() as $option_name ) { + if ( ! isset( $settings[ $option_name ] ) ) { + continue; + } + $setting = maybe_unserialize( $settings[ $option_name ] ); + if ( is_null( $setting ) ) { + delete_option( $option_name ); + } else { + update_option( $option_name, $setting ); + } + } + $this->add_message( __( 'Settings Imported', 'woocommerce-beta-tester' ), 'updated' ); + } else { + $this->add_message( __( 'File did not contain well formed JSON.', 'woocommerce-beta-tester' ) ); + } + } + + /** + * Get an array of the WooCommerce related settings. + */ + protected function get_settings() { + $settings = array(); + if ( current_user_can( 'manage_woocommerce' ) ) { + foreach ( $this->get_setting_list() as $option_name ) { + $setting = get_option( $option_name ); + if ( false === $setting ) { + $setting = null; + } + $settings[ $option_name ] = is_string( $setting ) ? $setting : serialize( $setting ); + } + } + return $settings; + } + + /** + * Add a settings import status message. + * + * @param string $message Message string. + * @param string $type Message type. Optional. Default 'error'. + */ + protected function add_message( $message, $type = 'error' ) { + $this->message = array( + 'message' => $message, + 'type' => $type + ); + } + + /** + * Get the WooCommerce settings list keys. + */ + private function get_setting_list() { + require_once( dirname(__FILE__ ) . '/wc-beta-tester-settings-list.php'); + return wc_beta_tester_setting_list(); + } +} diff --git a/plugins/woocommerce-beta-tester/includes/class-wc-beta-tester-plugin-upgrader.php b/plugins/woocommerce-beta-tester/includes/class-wc-beta-tester-plugin-upgrader.php new file mode 100644 index 00000000000..2e084902bc0 --- /dev/null +++ b/plugins/woocommerce-beta-tester/includes/class-wc-beta-tester-plugin-upgrader.php @@ -0,0 +1,74 @@ + true, + ); + $parsed_args = wp_parse_args( $args, $defaults ); + + $this->init(); + $this->upgrade_strings(); + + $plugin_version = $this->skin->options['version']; + + $download_url = WC_Beta_Tester::instance()->get_download_url( $plugin_version ); + + add_filter( 'upgrader_pre_install', array( $this, 'deactivate_plugin_before_upgrade' ), 10, 2 ); + add_filter( 'upgrader_clear_destination', array( $this, 'delete_old_plugin' ), 10, 4 ); + + $this->run( + array( + 'package' => $download_url, + 'destination' => WP_PLUGIN_DIR, + 'clear_destination' => true, + 'clear_working' => true, + 'hook_extra' => array( + 'plugin' => $plugin, + 'type' => 'plugin', + 'action' => 'update', + ), + ) + ); + + // Cleanup our hooks, in case something else does a upgrade on this connection. + remove_filter( 'upgrader_pre_install', array( $this, 'deactivate_plugin_before_upgrade' ) ); + remove_filter( 'upgrader_clear_destination', array( $this, 'delete_old_plugin' ) ); + + if ( ! $this->result || is_wp_error( $this->result ) ) { + return $this->result; + } + + // Force refresh of plugin update information. + wp_clean_plugins_cache( $parsed_args['clear_update_cache'] ); + + return true; + } + +} diff --git a/plugins/woocommerce-beta-tester/includes/class-wc-beta-tester-version-picker.php b/plugins/woocommerce-beta-tester/includes/class-wc-beta-tester-version-picker.php new file mode 100644 index 00000000000..5c4007aca24 --- /dev/null +++ b/plugins/woocommerce-beta-tester/includes/class-wc-beta-tester-version-picker.php @@ -0,0 +1,238 @@ + 'web', + 'url' => 'plugins.php?page=wc-beta-tester-version-picker', + 'title' => 'Version switch result', + 'plugin' => $plugin_name, + 'version' => $version, + 'nonce' => wp_unslash( $_GET['_wpnonce'] ), // WPCS: Input var ok, sanitization ok. + ); + + $skin = new Automatic_Upgrader_Skin( $skin_args ); + $upgrader = new WC_Beta_Tester_Plugin_Upgrader( $skin ); + $result = $upgrader->switch_version( $plugin ); + + // Try to reactivate. + activate_plugin( $plugin, '', is_network_admin(), true ); + + if ( is_wp_error( $skin->result ) ) { + throw new Exception( $skin->result->get_error_message() ); + } elseif ( false === $result ) { + throw new Exception( __( 'Update failed', 'woocommerce-beta-tester' ) ); + } + + wp_safe_redirect( admin_url( 'plugins.php?page=wc-beta-tester-version-picker&switched=' . rawurlencode( $version ) ) ); + exit; + } catch ( Exception $e ) { + if ( class_exists( 'WC_Admin_Notices' ) ) { + WC_Admin_Notices::add_custom_notice( + $plugin . '_update_error', + sprintf( + // translators: 1: plugin name, 2: error message. + __( '%1$s could not be updated (%2$s).', 'woocommerce-beta-tester' ), + $plugin, + $e->getMessage() + ) + ); + wp_safe_redirect( admin_url( 'plugins.php?page=wc-beta-tester-version-picker' ) ); + exit; + } else { + wp_die( esc_html( $e->getMessage() ) ); + } + } + } + + /** + * Add options page to menu. + * + * @return void + */ + public function add_to_menus() { + add_submenu_page( + 'plugins.php', + __( 'WooCommerce Version Switch', 'woocommerce-beta-tester' ), + __( 'WooCommerce Version Switch', 'woocommerce-beta-tester' ), + 'install_plugins', + 'wc-beta-tester-version-picker', + array( $this, 'select_versions_form_html' ) + ); + } + + /** + * Return HTML code representation of list of WooCommerce versions for the selected channel. + * + * @param string $channel Filter versions by channel: all|beta|rc|stable. + * @return string + */ + public function get_versions_html( $channel ) { + $tags = WC_Beta_Tester::instance()->get_tags( $channel ); + + if ( ! $tags ) { + return ''; + } + + usort( $tags, 'version_compare' ); + $tags = array_reverse( $tags ); + $versions_html = ''; + + if ( ! empty( $_GET['switched'] ) ) { // WPCS: input var ok, CSRF ok. + /* translators: %s: WooCoomerce version */ + $versions_html .= '

    ' . sprintf( esc_html__( 'Successfully switched version to %s.', 'woocommerce-beta-tester' ), esc_html( sanitize_text_field( wp_unslash( $_GET['switched'] ) ) ) ) . '

    '; // WPCS: input var ok, CSRF ok. + } + + $versions_html .= '
      '; + $plugin_data = WC_Beta_Tester::instance()->get_plugin_data(); + $this->current_version = $plugin_data['Version']; + + // Loop through versions and output in a radio list. + foreach ( $tags as $tag_version ) { + + $versions_html .= '
    • '; + $versions_html .= ''; + $versions_html .= '
    • '; + } + + $versions_html .= '
    '; + + return $versions_html; + } + + /** + * Echo HTML form to switch WooCommerce versions, filtered for the selected channel. + */ + public function select_versions_form_html() { + if ( ! current_user_can( 'install_plugins' ) ) { + return; + } + + $settings = WC_Beta_Tester::get_settings(); + $channel = $settings->channel; + + ?> +
    +
    +

    +
    +
    + get_versions_html( $channel ); // WPCS: XSS ok. ?> +
    +
    + +
    + + + + + + +
    +
    +
    + 'beta', + 'auto_update' => false, + ) + ); + + $settings->channel = $settings->channel; + $settings->auto_update = (bool) $settings->auto_update; + + return $settings; + } + + /** + * Get the plugin url. + * + * @return string + */ + public function plugin_url() { + return untrailingslashit( plugins_url( '/', WC_BETA_TESTER_FILE ) ); + } + + /** + * Constructor + */ + public function __construct() { + $this->plugin_name = plugin_basename( WC_BETA_TESTER_FILE ); + $this->plugin_config = array( + 'plugin_file' => 'woocommerce/woocommerce.php', + 'slug' => 'woocommerce', + 'proper_folder_name' => 'woocommerce', + 'api_url' => 'https://api.wordpress.org/plugins/info/1.0/woocommerce.json', + 'repo_url' => 'https://wordpress.org/plugins/woocommerce/', + ); + + add_filter( "plugin_action_links_{$this->plugin_name}", array( $this, 'plugin_action_links' ), 10, 1 ); + add_filter( 'auto_update_plugin', array( $this, 'auto_update_woocommerce' ), 100, 2 ); + + if ( 'stable' !== $this->get_settings()->channel ) { + add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'api_check' ) ); + add_filter( 'plugins_api_result', array( $this, 'plugins_api_result' ), 10, 3 ); + add_filter( 'upgrader_source_selection', array( $this, 'upgrader_source_selection' ), 10, 3 ); + } + + $this->includes(); + } + + /** + * Include any classes we need within admin. + */ + public function includes() { + include_once dirname( __FILE__ ) . '/class-wc-beta-tester-admin-menus.php'; + include_once dirname( __FILE__ ) . '/class-wc-beta-tester-admin-assets.php'; + } + + /** + * Check whether or not the transients need to be overruled and API needs to be called for every single page load + * + * @return bool overrule or not + */ + public function overrule_transients() { + return defined( 'WC_BETA_TESTER_FORCE_UPDATE' ) && WC_BETA_TESTER_FORCE_UPDATE; + } + + /** + * Checks if a given version is a pre-release. + * + * @param string $version Version to compare. + * @return bool + */ + public function is_prerelease( $version ) { + return preg_match( '/(.*)?-(beta|rc)(.*)/', $version ); + } + + /** + * Get New Version from WPorg + * + * @since 1.0 + * @return int $version the version number + */ + public function get_latest_channel_release() { + $tagged_version = get_site_transient( md5( $this->plugin_config['slug'] ) . '_latest_tag' ); + + if ( $this->overrule_transients() || empty( $tagged_version ) ) { + + $data = $this->get_wporg_data(); + + $latest_version = $data->version; + $versions = (array) $data->versions; + $channel = $this->get_settings()->channel; + + foreach ( $versions as $version => $download_url ) { + if ( 'trunk' === $version ) { + continue; + } + switch ( $channel ) { + case 'stable': + if ( $this->is_in_stable_channel( $version ) ) { + $tagged_version = $version; + } + break; + case 'rc': + if ( $this->is_in_rc_channel( $version ) ) { + $tagged_version = $version; + } + break; + case 'beta': + if ( $this->is_in_beta_channel( $version ) ) { + $tagged_version = $version; + } + break; + } + } + + // Refresh every 6 hours. + if ( ! empty( $tagged_version ) ) { + set_site_transient( md5( $this->plugin_config['slug'] ) . '_latest_tag', $tagged_version, HOUR_IN_SECONDS * 6 ); + } + } + + return $tagged_version; + } + + /** + * Get Data from .org API. + * + * @since 1.0 + * @return array $wporg_data The data. + */ + public function get_wporg_data() { + if ( ! empty( $this->wporg_data ) ) { + return $this->wporg_data; + } + + $wporg_data = get_site_transient( md5( $this->plugin_config['slug'] ) . '_wporg_data' ); + + if ( $this->overrule_transients() || ( ! isset( $wporg_data ) || ! $wporg_data || '' === $wporg_data ) ) { + $wporg_data = wp_remote_get( $this->plugin_config['api_url'] ); + + if ( is_wp_error( $wporg_data ) ) { + return false; + } + + $wporg_data = json_decode( $wporg_data['body'] ); + + // Refresh every 6 hours. + set_site_transient( md5( $this->plugin_config['slug'] ) . '_wporg_data', $wporg_data, HOUR_IN_SECONDS * 6 ); + } + + // Store the data in this class instance for future calls. + $this->wporg_data = $wporg_data; + + return $wporg_data; + } + + /** + * Get plugin download URL. + * + * @since 1.0 + * @param string $version The version. + * @return string + */ + public function get_download_url( $version ) { + $data = $this->get_wporg_data(); + + if ( empty( $data->versions->$version ) ) { + return false; + } + + return $data->versions->$version; + } + + /** + * Get Plugin data. + * + * @since 1.0 + * @return object $data The data. + */ + public function get_plugin_data() { + return get_plugin_data( WP_PLUGIN_DIR . '/' . $this->plugin_config['plugin_file'] ); + } + + /** + * Hook into the plugin update check and connect to WPorg. + * + * @since 1.0 + * @param object $transient The plugin data transient. + * @return object $transient Updated plugin data transient. + */ + public function api_check( $transient ) { + // Clear our transient. + delete_site_transient( md5( $this->plugin_config['slug'] ) . '_latest_tag' ); + + // Get version data. + $plugin_data = $this->get_plugin_data(); + $version = $plugin_data['Version']; + $new_version = $this->get_latest_channel_release(); + + // check the version and decide if it's new. + $update = version_compare( $new_version, $version, '>' ); + + if ( ! $update ) { + return $transient; + } + + // Populate response data. + if ( ! isset( $transient->response['woocommerce/woocommerce.php'] ) ) { + $transient->response['woocommerce/woocommerce.php'] = (object) $this->plugin_config; + } + + $transient->response['woocommerce/woocommerce.php']->new_version = $new_version; + $transient->response['woocommerce/woocommerce.php']->zip_url = $this->get_download_url( $new_version ); + $transient->response['woocommerce/woocommerce.php']->package = $this->get_download_url( $new_version ); + + return $transient; + } + + /** + * Filters the Plugin Installation API response results. + * + * @param object|WP_Error $response Response object or WP_Error. + * @param string $action The type of information being requested from the Plugin Installation API. + * @param object $args Plugin API arguments. + * @return object + */ + public function plugins_api_result( $response, $action, $args ) { + // Check if this call API is for the right plugin. + if ( ! isset( $response->slug ) || $response->slug !== $this->plugin_config['slug'] ) { + return $response; + } + + $new_version = $this->get_latest_channel_release(); + + if ( version_compare( $response->version, $new_version, '=' ) ) { + return $response; + } + + if ( $this->is_beta_version( $new_version ) ) { + $warning = __( '

    This is a beta release

    ', 'woocommerce-beta-tester' ); + } + + if ( $this->is_rc_version( $new_version ) ) { + $warning = __( '

    This is a pre-release version

    ', 'woocommerce-beta-tester' ); + } + + // If we are returning a different version than the stable tag on .org, manipulate the returned data. + $response->version = $new_version; + $response->download_link = $this->get_download_url( $new_version ); + + $response->sections['changelog'] = sprintf( + '

    ' . __( 'Read the changelog and find out more about the release on GitHub.', 'woocommerce-beta-tester' ) . '

    ', + 'https://github.com/woocommerce/woocommerce/blob/' . $response->version . '/readme.txt' + ); + + foreach ( $response->sections as $key => $section ) { + $response->sections[ $key ] = $warning . $section; + } + + return $response; + } + + /** + * Rename the downloaded zip + * + * @param string $source File source location. + * @param string $remote_source Remote file source location. + * @param WP_Upgrader $upgrader WordPress Upgrader instance. + * @return string + */ + public function upgrader_source_selection( $source, $remote_source, $upgrader ) { + global $wp_filesystem; + + if ( strstr( $source, '/woocommerce-woocommerce-' ) ) { + $corrected_source = trailingslashit( $remote_source ) . trailingslashit( $this->plugin_config['proper_folder_name'] ); + + if ( $wp_filesystem->move( $source, $corrected_source, true ) ) { + return $corrected_source; + } else { + return new WP_Error(); + } + } + + return $source; + } + + /** + * Enable auto updates for WooCommerce. + * + * @param bool $update Should this autoupdate. + * @param object $plugin Plugin being checked. + * @return bool + */ + public function auto_update_woocommerce( $update, $plugin ) { + if ( true === $this->get_settings()->auto_update && 'woocommerce' === $plugin->slug ) { + return true; + } else { + return $update; + } + } + + /** + * Return true if version string is a beta version. + * + * @param string $version_str Version string. + * @return bool + */ + protected static function is_beta_version( $version_str ) { + return strpos( $version_str, 'beta' ) !== false; + } + + /** + * Return true if version string is a Release Candidate. + * + * @param string $version_str Version string. + * @return bool + */ + protected static function is_rc_version( $version_str ) { + return strpos( $version_str, 'rc' ) !== false; + } + + /** + * Return true if version string is a stable version. + * + * @param string $version_str Version string. + * @return bool + */ + protected static function is_stable_version( $version_str ) { + return ! self::is_beta_version( $version_str ) && ! self::is_rc_version( $version_str ); + } + + /** + * Return true if release's version string belongs to beta channel, i.e. + * if it's beta, rc or stable release. + * + * @param string $version_str Version string of the release. + * @return bool + */ + protected static function is_in_beta_channel( $version_str ) { + return self::is_beta_version( $version_str ) || self::is_rc_version( $version_str ) || self::is_stable_version( $version_str ); + } + + /** + * Return true if release's version string belongs to release candidate channel, i.e. + * if it's rc or stable release. + * + * @param string $version_str Version string of the release. + * @return bool + */ + protected static function is_in_rc_channel( $version_str ) { + return self::is_rc_version( $version_str ) || self::is_stable_version( $version_str ); + } + + /** + * Return true if release's version string belongs to stable channel, i.e. + * if it's stable release and not a beta or rc. + * + * @param string $version_str Version string of the release. + * @return bool + */ + protected static function is_in_stable_channel( $version_str ) { + return self::is_stable_version( $version_str ); + } + + /** + * Return available versions from wp.org tags belonging to selected channel. + * + * @param string $channel Filter versions by channel: all|beta|rc|stable. + * @return array(string) + */ + public function get_tags( $channel = 'all' ) { + $data = $this->get_wporg_data(); + $releases = (array) $data->versions; + + unset( $releases['trunk'] ); + + $releases = array_keys( $releases ); + foreach ( $releases as $index => $version ) { + if ( version_compare( $version, '3.6', '<' ) ) { + unset( $releases[ $index ] ); + } + } + + if ( 'beta' === $channel ) { + $releases = array_filter( $releases, array( __CLASS__, 'is_in_beta_channel' ) ); + } elseif ( 'rc' === $channel ) { + $releases = array_filter( $releases, array( __CLASS__, 'is_in_rc_channel' ) ); + } elseif ( 'stable' === $channel ) { + $releases = array_filter( $releases, array( __CLASS__, 'is_in_stable_channel' ) ); + } + + return $releases; + } + + /** + * Show action links on the plugin screen. + * + * @param mixed $links Plugin Action links. + * @return array + */ + public function plugin_action_links( $links ) { + $action_links = array( + 'switch-version' => sprintf( + '%s', + esc_url( admin_url( 'plugins.php?page=wc-beta-tester-version-picker' ) ), + esc_html__( 'Switch versions', 'woocommerce-beta-tester' ) + ), + 'settings' => sprintf( + '%s', + esc_url( admin_url( 'plugins.php?page=wc-beta-tester' ) ), + esc_html__( 'Settings', 'woocommerce-beta-tester' ) + ), + ); + + return array_merge( $action_links, $links ); + } +} diff --git a/plugins/woocommerce-beta-tester/includes/views/html-admin-missing-woocommerce.php b/plugins/woocommerce-beta-tester/includes/views/html-admin-missing-woocommerce.php new file mode 100644 index 00000000000..605b2a30b7c --- /dev/null +++ b/plugins/woocommerce-beta-tester/includes/views/html-admin-missing-woocommerce.php @@ -0,0 +1,47 @@ + + +
    +

    + ' . esc_html__( 'WooCommerce Beta Tester', 'woocommerce-beta-tester' ) . '' ); + ?> +

    + + +

    + + + + + + +

    + + +

    + + + + +

    + +
    diff --git a/plugins/woocommerce-beta-tester/includes/wc-beta-tester-settings-list.php b/plugins/woocommerce-beta-tester/includes/wc-beta-tester-settings-list.php new file mode 100644 index 00000000000..26dd15839df --- /dev/null +++ b/plugins/woocommerce-beta-tester/includes/wc-beta-tester-settings-list.php @@ -0,0 +1,128 @@ + $file.min.js; done", + "lint:js": "eslint assets/js --ext=js" + }, + "engines": { + "node": ">=10.15.0", + "npm": ">=6.4.1" + }, + "woorelease": { + "svn_reauth": "true", + "wp_org_slug": "woocommerce-beta-tester" + }, + "lint-staged": { + "*.php": [ + "php -d display_errors=1 -l", + "composer --working-dir=./plugins/woocommerce-beta-tester run-script phpcs-pre-commit" + ], + "!(*min).js": [ + "eslint --fix" + ] + } +} diff --git a/plugins/woocommerce-beta-tester/phpcs.xml b/plugins/woocommerce-beta-tester/phpcs.xml new file mode 100644 index 00000000000..ca3d379bb17 --- /dev/null +++ b/plugins/woocommerce-beta-tester/phpcs.xml @@ -0,0 +1,51 @@ + + + + + + WooCommerce dev PHP_CodeSniffer ruleset. + + + */node_modules/* + */vendor/* + + + + + + + + + + + + tests/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce-beta-tester/project.json b/plugins/woocommerce-beta-tester/project.json new file mode 100644 index 00000000000..3d6ce0a9c21 --- /dev/null +++ b/plugins/woocommerce-beta-tester/project.json @@ -0,0 +1,47 @@ +{ + "root": "plugins/woocommerce-beta-tester/", + "sourceRoot": "plugins/woocommerce-beta-tester", + "projectType": "application", + "targets": { + "build": { + "executor": "@nrwl/workspace:run-script", + "options": { + "script": "build" + } + }, + "changelog": { + "executor": "./tools/executors/changelogger:changelog", + "options": { + "action": "add", + "cwd": "plugins/woocommerce-beta-tester" + } + }, + "composer-install": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "command": "composer install", + "cwd": "plugins/woocommerce-beta-tester" + } + }, + "composer-install-no-dev": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "command": "composer install --no-dev", + "cwd": "plugins/woocommerce-beta-tester" + } + }, + "composer-dump-autoload": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "command": "composer dump-autoload", + "cwd": "plugins/woocommerce-beta-tester" + } + }, + "lint-js": { + "executor": "@nrwl/workspace:run-script", + "options": { + "script": "lint:js" + } + } + } +} diff --git a/plugins/woocommerce-beta-tester/readme.txt b/plugins/woocommerce-beta-tester/readme.txt new file mode 100644 index 00000000000..4d4f2b19f81 --- /dev/null +++ b/plugins/woocommerce-beta-tester/readme.txt @@ -0,0 +1,95 @@ +=== WooCommerce Beta Tester === +Contributors: automattic, bor0, claudiosanches, claudiulodro, kloon, mikejolley, peterfabian1000, rodrigosprimo, wpmuguru +Tags: woocommerce, woo commerce, beta, beta tester, bleeding edge, testing +Requires at least: 4.7 +Tested up to: 5.6 +Stable tag: 2.0.5 +License: GPLv3 +License URI: https://www.gnu.org/licenses/gpl-3.0.html + +Easily update to prerelease versions of WooCommerce for testing and development purposes. + +== Description == + +**WooCommerce Beta Tester** allows you to try out new versions of WooCommerce before they are officially released. + +**Use with caution, not on production sites. Beta releases may not be stable.** + +After activation, you'll be able to choose an update channel: + +1. Beta - Update to beta releases, RC, or stable, depending on what is newest. +2. Release Candidate - Update to RC releases or stable, depending on what is newest. +3. Stable - No beta updates. Default WordPress behavior. + +These will surface pre-releases via automatic updates in WordPress. Updates will replace your installed version of WooCommerce. + +**Note**, this will not check for updates on every admin page load unless you explicitly tell it to. You can do this by clicking the "Check Again" button from the WordPress updates screen or you can set the `WC_BETA_TESTER_FORCE_UPDATE` to true in your `wp-config.php` file. + +== Frequently Asked Questions == + += Does this allow me to install multiple versions of WooCommerce at the same time? + +No; updates will replace your currently installed version of WooCommerce. You can switch to any version from this plugin via the interface however. + += Where do updates come from? = + +Updates are downloaded from the WordPress.org SVN repository where we tag prerelease versions specifically for this purpose. + += Does this rollback my data? = + +This plugin does not rollback or update data on your store automatically. + +Database updates are manually ran like after regular updates. If you downgrade, data will not be modified. We don't recommend using this in production. + += Where can I report bugs or contribute to WooCommerce Beta Tester? = + +Bugs can be reported to the [WooCommerce Beta Tester GitHub issue tracker](https://github.com/woocommerce/woocommerce-beta-tester). + += Where can I report bugs or contribute to WooCommerce? = + +Join in on our [GitHub repository](https://github.com/woocommerce/woocommerce/). + +See our [contributing guidelines here](https://github.com/woocommerce/woocommerce/blob/master/.github/CONTRIBUTING.md). + +== Changelog == + += 2.0.5 - 2021-12-17 = +* Fix: make WC version comparison case insensitive + += 2.0.4 - 2021-09-29 = +* Dev: Bump tested to version +* Fix: enqueue logic for css/js assets + += 2.0.3 - 2021-09-22 = +* Fix: Bump version to release version including admin.css. + += 2.0.2 = + +* Fix notice for undefined `item` +* Fix auto_update_plugin filter reference +* Fix including SSR in bug report +* Fix style in version modal header +* Add check for WooCommerce installed in default location + += 2.0.1 = +* Changes to make this plugin compatible with the upcoming WooCommerce 3.6 + += 2.0.0 = +* Enhancement - Re-built to pull updates from the WordPress.org repository rather than GitHub. +* Enhancement - Channel selection; choose to receive RC or beta versions. +* Enhancement - Admin bar item shows version information, and offers shortcuts to functionality. +* Enhancement - Shortcut to log GitHub issues. +* Enhancement - Version switcher; choose which release or prerelease to switch to. +* Enhancement - Setting to enable auto-updates. + += 1.0.3 = +* Fix repo URLs and directory renaming. + += 1.0.2 = +* Updated API URL. + += 1.0.1 = +* Switched to releases API to get latest release, rather than tag which are not chronological. + += 1.0 = +* First release. diff --git a/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php b/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php new file mode 100644 index 00000000000..699289e5dca --- /dev/null +++ b/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php @@ -0,0 +1,66 @@ + \ No newline at end of file diff --git a/plugins/woocommerce/assets/images/admin_notes/marketing-jetpack-2x.png b/plugins/woocommerce/assets/images/admin_notes/marketing-jetpack-2x.png new file mode 100644 index 00000000000..a81e5bfa199 Binary files /dev/null and b/plugins/woocommerce/assets/images/admin_notes/marketing-jetpack-2x.png differ diff --git a/assets/images/calendar.png b/plugins/woocommerce/assets/images/calendar.png similarity index 100% rename from assets/images/calendar.png rename to plugins/woocommerce/assets/images/calendar.png diff --git a/plugins/woocommerce/assets/images/dashboard-widget-setup.png b/plugins/woocommerce/assets/images/dashboard-widget-setup.png new file mode 100644 index 00000000000..fcba8f5532a Binary files /dev/null and b/plugins/woocommerce/assets/images/dashboard-widget-setup.png differ diff --git a/plugins/woocommerce/assets/images/empty-content.svg b/plugins/woocommerce/assets/images/empty-content.svg new file mode 100644 index 00000000000..4c61d2394c5 --- /dev/null +++ b/plugins/woocommerce/assets/images/empty-content.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/eway-logo.jpg b/plugins/woocommerce/assets/images/eway-logo.jpg similarity index 100% rename from assets/images/eway-logo.jpg rename to plugins/woocommerce/assets/images/eway-logo.jpg diff --git a/assets/images/help.png b/plugins/woocommerce/assets/images/help.png similarity index 100% rename from assets/images/help.png rename to plugins/woocommerce/assets/images/help.png diff --git a/assets/images/icons/credit-cards/amex.png b/plugins/woocommerce/assets/images/icons/credit-cards/amex.png similarity index 100% rename from assets/images/icons/credit-cards/amex.png rename to plugins/woocommerce/assets/images/icons/credit-cards/amex.png diff --git a/assets/images/icons/credit-cards/amex.svg b/plugins/woocommerce/assets/images/icons/credit-cards/amex.svg similarity index 100% rename from assets/images/icons/credit-cards/amex.svg rename to plugins/woocommerce/assets/images/icons/credit-cards/amex.svg diff --git a/assets/images/icons/credit-cards/diners.png b/plugins/woocommerce/assets/images/icons/credit-cards/diners.png similarity index 100% rename from assets/images/icons/credit-cards/diners.png rename to plugins/woocommerce/assets/images/icons/credit-cards/diners.png diff --git a/assets/images/icons/credit-cards/diners.svg b/plugins/woocommerce/assets/images/icons/credit-cards/diners.svg similarity index 100% rename from assets/images/icons/credit-cards/diners.svg rename to plugins/woocommerce/assets/images/icons/credit-cards/diners.svg diff --git a/assets/images/icons/credit-cards/discover.png b/plugins/woocommerce/assets/images/icons/credit-cards/discover.png similarity index 100% rename from assets/images/icons/credit-cards/discover.png rename to plugins/woocommerce/assets/images/icons/credit-cards/discover.png diff --git a/assets/images/icons/credit-cards/discover.svg b/plugins/woocommerce/assets/images/icons/credit-cards/discover.svg similarity index 100% rename from assets/images/icons/credit-cards/discover.svg rename to plugins/woocommerce/assets/images/icons/credit-cards/discover.svg diff --git a/assets/images/icons/credit-cards/jcb.png b/plugins/woocommerce/assets/images/icons/credit-cards/jcb.png similarity index 100% rename from assets/images/icons/credit-cards/jcb.png rename to plugins/woocommerce/assets/images/icons/credit-cards/jcb.png diff --git a/assets/images/icons/credit-cards/jcb.svg b/plugins/woocommerce/assets/images/icons/credit-cards/jcb.svg similarity index 100% rename from assets/images/icons/credit-cards/jcb.svg rename to plugins/woocommerce/assets/images/icons/credit-cards/jcb.svg diff --git a/assets/images/icons/credit-cards/laser.png b/plugins/woocommerce/assets/images/icons/credit-cards/laser.png similarity index 100% rename from assets/images/icons/credit-cards/laser.png rename to plugins/woocommerce/assets/images/icons/credit-cards/laser.png diff --git a/assets/images/icons/credit-cards/laser.svg b/plugins/woocommerce/assets/images/icons/credit-cards/laser.svg similarity index 100% rename from assets/images/icons/credit-cards/laser.svg rename to plugins/woocommerce/assets/images/icons/credit-cards/laser.svg diff --git a/assets/images/icons/credit-cards/maestro.png b/plugins/woocommerce/assets/images/icons/credit-cards/maestro.png similarity index 100% rename from assets/images/icons/credit-cards/maestro.png rename to plugins/woocommerce/assets/images/icons/credit-cards/maestro.png diff --git a/assets/images/icons/credit-cards/maestro.svg b/plugins/woocommerce/assets/images/icons/credit-cards/maestro.svg similarity index 100% rename from assets/images/icons/credit-cards/maestro.svg rename to plugins/woocommerce/assets/images/icons/credit-cards/maestro.svg diff --git a/assets/images/icons/credit-cards/mastercard.png b/plugins/woocommerce/assets/images/icons/credit-cards/mastercard.png similarity index 100% rename from assets/images/icons/credit-cards/mastercard.png rename to plugins/woocommerce/assets/images/icons/credit-cards/mastercard.png diff --git a/assets/images/icons/credit-cards/mastercard.svg b/plugins/woocommerce/assets/images/icons/credit-cards/mastercard.svg similarity index 100% rename from assets/images/icons/credit-cards/mastercard.svg rename to plugins/woocommerce/assets/images/icons/credit-cards/mastercard.svg diff --git a/assets/images/icons/credit-cards/visa.png b/plugins/woocommerce/assets/images/icons/credit-cards/visa.png similarity index 100% rename from assets/images/icons/credit-cards/visa.png rename to plugins/woocommerce/assets/images/icons/credit-cards/visa.png diff --git a/assets/images/icons/credit-cards/visa.svg b/plugins/woocommerce/assets/images/icons/credit-cards/visa.svg similarity index 100% rename from assets/images/icons/credit-cards/visa.svg rename to plugins/woocommerce/assets/images/icons/credit-cards/visa.svg diff --git a/assets/images/icons/edit.png b/plugins/woocommerce/assets/images/icons/edit.png similarity index 100% rename from assets/images/icons/edit.png rename to plugins/woocommerce/assets/images/icons/edit.png diff --git a/plugins/woocommerce/assets/images/icons/gridicons-checkmark.svg b/plugins/woocommerce/assets/images/icons/gridicons-checkmark.svg new file mode 100644 index 00000000000..6ca2bd7de7f --- /dev/null +++ b/plugins/woocommerce/assets/images/icons/gridicons-checkmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/woocommerce/assets/images/icons/gridicons-chevron-down.svg b/plugins/woocommerce/assets/images/icons/gridicons-chevron-down.svg new file mode 100644 index 00000000000..47206983e5f --- /dev/null +++ b/plugins/woocommerce/assets/images/icons/gridicons-chevron-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/icons/loader.svg b/plugins/woocommerce/assets/images/icons/loader.svg similarity index 100% rename from assets/images/icons/loader.svg rename to plugins/woocommerce/assets/images/icons/loader.svg diff --git a/plugins/woocommerce/assets/images/icons/star-golden.svg b/plugins/woocommerce/assets/images/icons/star-golden.svg new file mode 100644 index 00000000000..9f550f87964 --- /dev/null +++ b/plugins/woocommerce/assets/images/icons/star-golden.svg @@ -0,0 +1,13 @@ + + + + diff --git a/plugins/woocommerce/assets/images/icons/star-gray.svg b/plugins/woocommerce/assets/images/icons/star-gray.svg new file mode 100644 index 00000000000..a530f2298af --- /dev/null +++ b/plugins/woocommerce/assets/images/icons/star-gray.svg @@ -0,0 +1,13 @@ + + + + diff --git a/plugins/woocommerce/assets/images/icons/star-half-filled.svg b/plugins/woocommerce/assets/images/icons/star-half-filled.svg new file mode 100644 index 00000000000..33b07e8ca1b --- /dev/null +++ b/plugins/woocommerce/assets/images/icons/star-half-filled.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/assets/images/klarna-black.png b/plugins/woocommerce/assets/images/klarna-black.png similarity index 100% rename from assets/images/klarna-black.png rename to plugins/woocommerce/assets/images/klarna-black.png diff --git a/plugins/woocommerce/assets/images/marketing/amazon-ebay.svg b/plugins/woocommerce/assets/images/marketing/amazon-ebay.svg new file mode 100644 index 00000000000..8f5e111c284 --- /dev/null +++ b/plugins/woocommerce/assets/images/marketing/amazon-ebay.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/woocommerce/assets/images/marketing/automatewoo.svg b/plugins/woocommerce/assets/images/marketing/automatewoo.svg new file mode 100644 index 00000000000..5696d96c0a4 --- /dev/null +++ b/plugins/woocommerce/assets/images/marketing/automatewoo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/woocommerce/assets/images/marketing/facebook.svg b/plugins/woocommerce/assets/images/marketing/facebook.svg new file mode 100644 index 00000000000..faf32c7f45f --- /dev/null +++ b/plugins/woocommerce/assets/images/marketing/facebook.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/woocommerce/assets/images/marketing/google.svg b/plugins/woocommerce/assets/images/marketing/google.svg new file mode 100644 index 00000000000..5a021f66b8b --- /dev/null +++ b/plugins/woocommerce/assets/images/marketing/google.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/woocommerce/assets/images/marketing/hubspot.svg b/plugins/woocommerce/assets/images/marketing/hubspot.svg new file mode 100644 index 00000000000..ab3013fd5dc --- /dev/null +++ b/plugins/woocommerce/assets/images/marketing/hubspot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/woocommerce/assets/images/marketing/mailchimp.svg b/plugins/woocommerce/assets/images/marketing/mailchimp.svg new file mode 100644 index 00000000000..b2ee4a689a6 --- /dev/null +++ b/plugins/woocommerce/assets/images/marketing/mailchimp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/woocommerce/assets/images/marketing/mailpoet.svg b/plugins/woocommerce/assets/images/marketing/mailpoet.svg new file mode 100644 index 00000000000..a957e341c52 --- /dev/null +++ b/plugins/woocommerce/assets/images/marketing/mailpoet.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/woocommerce/assets/images/marketing/pinterest.svg b/plugins/woocommerce/assets/images/marketing/pinterest.svg new file mode 100644 index 00000000000..a14ac20f81a --- /dev/null +++ b/plugins/woocommerce/assets/images/marketing/pinterest.svg @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/plugins/woocommerce/assets/images/marketplace-header-bg@2x.png b/plugins/woocommerce/assets/images/marketplace-header-bg@2x.png new file mode 100644 index 00000000000..007d032d5f5 Binary files /dev/null and b/plugins/woocommerce/assets/images/marketplace-header-bg@2x.png differ diff --git a/plugins/woocommerce/assets/images/mercadopago.png b/plugins/woocommerce/assets/images/mercadopago.png new file mode 100644 index 00000000000..39a790e5bf4 Binary files /dev/null and b/plugins/woocommerce/assets/images/mercadopago.png differ diff --git a/plugins/woocommerce/assets/images/onboarding/bacs.svg b/plugins/woocommerce/assets/images/onboarding/bacs.svg new file mode 100644 index 00000000000..5a8d958278e --- /dev/null +++ b/plugins/woocommerce/assets/images/onboarding/bacs.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/woocommerce/assets/images/onboarding/cod.svg b/plugins/woocommerce/assets/images/onboarding/cod.svg new file mode 100644 index 00000000000..c03d7b98659 --- /dev/null +++ b/plugins/woocommerce/assets/images/onboarding/cod.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/woocommerce/assets/images/onboarding/creative-mail-by-constant-contact.png b/plugins/woocommerce/assets/images/onboarding/creative-mail-by-constant-contact.png new file mode 100644 index 00000000000..622c415fa1d Binary files /dev/null and b/plugins/woocommerce/assets/images/onboarding/creative-mail-by-constant-contact.png differ diff --git a/plugins/woocommerce/assets/images/onboarding/creativemail.png b/plugins/woocommerce/assets/images/onboarding/creativemail.png new file mode 100644 index 00000000000..b68fafd3506 Binary files /dev/null and b/plugins/woocommerce/assets/images/onboarding/creativemail.png differ diff --git a/plugins/woocommerce/assets/images/onboarding/eway.png b/plugins/woocommerce/assets/images/onboarding/eway.png new file mode 100644 index 00000000000..8471e81113a Binary files /dev/null and b/plugins/woocommerce/assets/images/onboarding/eway.png differ diff --git a/plugins/woocommerce/assets/images/onboarding/fb-woocommerce.png b/plugins/woocommerce/assets/images/onboarding/fb-woocommerce.png new file mode 100644 index 00000000000..5fa36ff28d8 Binary files /dev/null and b/plugins/woocommerce/assets/images/onboarding/fb-woocommerce.png differ diff --git a/plugins/woocommerce/assets/images/onboarding/g-shopping.png b/plugins/woocommerce/assets/images/onboarding/g-shopping.png new file mode 100644 index 00000000000..6686f40f6c2 Binary files /dev/null and b/plugins/woocommerce/assets/images/onboarding/g-shopping.png differ diff --git a/plugins/woocommerce/assets/images/onboarding/google-listings-and-ads.png b/plugins/woocommerce/assets/images/onboarding/google-listings-and-ads.png new file mode 100644 index 00000000000..291c91e2f73 Binary files /dev/null and b/plugins/woocommerce/assets/images/onboarding/google-listings-and-ads.png differ diff --git a/plugins/woocommerce/assets/images/onboarding/mailchimp-for-woocommerce.png b/plugins/woocommerce/assets/images/onboarding/mailchimp-for-woocommerce.png new file mode 100644 index 00000000000..647c1ede3ab Binary files /dev/null and b/plugins/woocommerce/assets/images/onboarding/mailchimp-for-woocommerce.png differ diff --git a/plugins/woocommerce/assets/images/onboarding/mailchimp.png b/plugins/woocommerce/assets/images/onboarding/mailchimp.png new file mode 100644 index 00000000000..f15448e9acb Binary files /dev/null and b/plugins/woocommerce/assets/images/onboarding/mailchimp.png differ diff --git a/plugins/woocommerce/assets/images/onboarding/mailpoet.png b/plugins/woocommerce/assets/images/onboarding/mailpoet.png new file mode 100644 index 00000000000..57db7c78e8a Binary files /dev/null and b/plugins/woocommerce/assets/images/onboarding/mailpoet.png differ diff --git a/plugins/woocommerce/assets/images/onboarding/mercadopago.png b/plugins/woocommerce/assets/images/onboarding/mercadopago.png new file mode 100644 index 00000000000..8fe97924802 Binary files /dev/null and b/plugins/woocommerce/assets/images/onboarding/mercadopago.png differ diff --git a/plugins/woocommerce/assets/images/onboarding/mollie.svg b/plugins/woocommerce/assets/images/onboarding/mollie.svg new file mode 100644 index 00000000000..35bd195fa21 --- /dev/null +++ b/plugins/woocommerce/assets/images/onboarding/mollie.svg @@ -0,0 +1 @@ + diff --git a/plugins/woocommerce/assets/images/onboarding/other-small.jpg b/plugins/woocommerce/assets/images/onboarding/other-small.jpg new file mode 100644 index 00000000000..b22554ac610 Binary files /dev/null and b/plugins/woocommerce/assets/images/onboarding/other-small.jpg differ diff --git a/plugins/woocommerce/assets/images/onboarding/paystack.png b/plugins/woocommerce/assets/images/onboarding/paystack.png new file mode 100644 index 00000000000..82d1d08c051 Binary files /dev/null and b/plugins/woocommerce/assets/images/onboarding/paystack.png differ diff --git a/plugins/woocommerce/assets/images/onboarding/payu.svg b/plugins/woocommerce/assets/images/onboarding/payu.svg new file mode 100644 index 00000000000..c8d57874ff5 --- /dev/null +++ b/plugins/woocommerce/assets/images/onboarding/payu.svg @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/plugins/woocommerce/assets/images/onboarding/pinterest.png b/plugins/woocommerce/assets/images/onboarding/pinterest.png new file mode 100644 index 00000000000..2951353d8fd Binary files /dev/null and b/plugins/woocommerce/assets/images/onboarding/pinterest.png differ diff --git a/plugins/woocommerce/assets/images/onboarding/razorpay.svg b/plugins/woocommerce/assets/images/onboarding/razorpay.svg new file mode 100644 index 00000000000..e617850c42a --- /dev/null +++ b/plugins/woocommerce/assets/images/onboarding/razorpay.svg @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/plugins/woocommerce/assets/images/onboarding/wcpay.svg b/plugins/woocommerce/assets/images/onboarding/wcpay.svg new file mode 100644 index 00000000000..aa0897def11 --- /dev/null +++ b/plugins/woocommerce/assets/images/onboarding/wcpay.svg @@ -0,0 +1,26 @@ + + + + + + diff --git a/assets/images/payfast.png b/plugins/woocommerce/assets/images/payfast.png similarity index 100% rename from assets/images/payfast.png rename to plugins/woocommerce/assets/images/payfast.png diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/affirm.png b/plugins/woocommerce/assets/images/payment_methods/72x72/affirm.png new file mode 100644 index 00000000000..9401ad1b670 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/affirm.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/afterpay.png b/plugins/woocommerce/assets/images/payment_methods/72x72/afterpay.png new file mode 100644 index 00000000000..28b89a010d1 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/afterpay.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/amazonpay.png b/plugins/woocommerce/assets/images/payment_methods/72x72/amazonpay.png new file mode 100644 index 00000000000..1116fe95b2f Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/amazonpay.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/bacs.png b/plugins/woocommerce/assets/images/payment_methods/72x72/bacs.png new file mode 100644 index 00000000000..e9b706beb8a Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/bacs.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/cod.png b/plugins/woocommerce/assets/images/payment_methods/72x72/cod.png new file mode 100644 index 00000000000..f2ba5f68888 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/cod.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/eway.png b/plugins/woocommerce/assets/images/payment_methods/72x72/eway.png new file mode 100644 index 00000000000..74884fa1b03 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/eway.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/klarna.png b/plugins/woocommerce/assets/images/payment_methods/72x72/klarna.png new file mode 100644 index 00000000000..a97abed5981 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/klarna.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/mercadopago.png b/plugins/woocommerce/assets/images/payment_methods/72x72/mercadopago.png new file mode 100644 index 00000000000..5f61e693d6e Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/mercadopago.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/mollie.png b/plugins/woocommerce/assets/images/payment_methods/72x72/mollie.png new file mode 100644 index 00000000000..3ec67896237 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/mollie.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/payfast.png b/plugins/woocommerce/assets/images/payment_methods/72x72/payfast.png new file mode 100644 index 00000000000..96440121c30 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/payfast.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/paypal.png b/plugins/woocommerce/assets/images/payment_methods/72x72/paypal.png new file mode 100644 index 00000000000..07768e74fd4 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/paypal.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/paystack.png b/plugins/woocommerce/assets/images/payment_methods/72x72/paystack.png new file mode 100644 index 00000000000..362446dfb00 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/paystack.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/payu.png b/plugins/woocommerce/assets/images/payment_methods/72x72/payu.png new file mode 100644 index 00000000000..50eaa0f8250 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/payu.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/razorpay.png b/plugins/woocommerce/assets/images/payment_methods/72x72/razorpay.png new file mode 100644 index 00000000000..01dedd7bfa6 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/razorpay.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/square.png b/plugins/woocommerce/assets/images/payment_methods/72x72/square.png new file mode 100644 index 00000000000..3ffe4877222 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/square.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/stripe.png b/plugins/woocommerce/assets/images/payment_methods/72x72/stripe.png new file mode 100644 index 00000000000..0175f2947d6 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/stripe.png differ diff --git a/assets/images/paypal-braintree.png b/plugins/woocommerce/assets/images/paypal-braintree.png similarity index 100% rename from assets/images/paypal-braintree.png rename to plugins/woocommerce/assets/images/paypal-braintree.png diff --git a/assets/images/paypal.png b/plugins/woocommerce/assets/images/paypal.png similarity index 100% rename from assets/images/paypal.png rename to plugins/woocommerce/assets/images/paypal.png diff --git a/assets/images/placeholder-attachment.png b/plugins/woocommerce/assets/images/placeholder-attachment.png similarity index 100% rename from assets/images/placeholder-attachment.png rename to plugins/woocommerce/assets/images/placeholder-attachment.png diff --git a/assets/images/placeholder.png b/plugins/woocommerce/assets/images/placeholder.png similarity index 100% rename from assets/images/placeholder.png rename to plugins/woocommerce/assets/images/placeholder.png diff --git a/assets/images/select2-spinner.gif b/plugins/woocommerce/assets/images/select2-spinner.gif similarity index 100% rename from assets/images/select2-spinner.gif rename to plugins/woocommerce/assets/images/select2-spinner.gif diff --git a/assets/images/select2.png b/plugins/woocommerce/assets/images/select2.png similarity index 100% rename from assets/images/select2.png rename to plugins/woocommerce/assets/images/select2.png diff --git a/assets/images/select2x2.png b/plugins/woocommerce/assets/images/select2x2.png similarity index 100% rename from assets/images/select2x2.png rename to plugins/woocommerce/assets/images/select2x2.png diff --git a/plugins/woocommerce/assets/images/shippingillustration.svg b/plugins/woocommerce/assets/images/shippingillustration.svg new file mode 100644 index 00000000000..fa1fd40d010 --- /dev/null +++ b/plugins/woocommerce/assets/images/shippingillustration.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/square-black.png b/plugins/woocommerce/assets/images/square-black.png similarity index 100% rename from assets/images/square-black.png rename to plugins/woocommerce/assets/images/square-black.png diff --git a/assets/images/stripe.png b/plugins/woocommerce/assets/images/stripe.png similarity index 100% rename from assets/images/stripe.png rename to plugins/woocommerce/assets/images/stripe.png diff --git a/plugins/woocommerce/assets/images/task_list/basics-section-illustration.png b/plugins/woocommerce/assets/images/task_list/basics-section-illustration.png new file mode 100644 index 00000000000..988a281048f Binary files /dev/null and b/plugins/woocommerce/assets/images/task_list/basics-section-illustration.png differ diff --git a/plugins/woocommerce/assets/images/task_list/expand-section-illustration.png b/plugins/woocommerce/assets/images/task_list/expand-section-illustration.png new file mode 100644 index 00000000000..e8bd17b154f Binary files /dev/null and b/plugins/woocommerce/assets/images/task_list/expand-section-illustration.png differ diff --git a/plugins/woocommerce/assets/images/task_list/sales-section-illustration.png b/plugins/woocommerce/assets/images/task_list/sales-section-illustration.png new file mode 100644 index 00000000000..133800215bc Binary files /dev/null and b/plugins/woocommerce/assets/images/task_list/sales-section-illustration.png differ diff --git a/assets/images/wcpayments-icon-secure.png b/plugins/woocommerce/assets/images/wcpayments-icon-secure.png similarity index 100% rename from assets/images/wcpayments-icon-secure.png rename to plugins/woocommerce/assets/images/wcpayments-icon-secure.png diff --git a/assets/images/woocommerce_logo.png b/plugins/woocommerce/assets/images/woocommerce_logo.png similarity index 100% rename from assets/images/woocommerce_logo.png rename to plugins/woocommerce/assets/images/woocommerce_logo.png diff --git a/assets/images/woocommerce_logo.svg b/plugins/woocommerce/assets/images/woocommerce_logo.svg similarity index 100% rename from assets/images/woocommerce_logo.svg rename to plugins/woocommerce/assets/images/woocommerce_logo.svg diff --git a/assets/images/wpspin-2x.gif b/plugins/woocommerce/assets/images/wpspin-2x.gif similarity index 100% rename from assets/images/wpspin-2x.gif rename to plugins/woocommerce/assets/images/wpspin-2x.gif diff --git a/assets/images/wpspin.gif b/plugins/woocommerce/assets/images/wpspin.gif similarity index 100% rename from assets/images/wpspin.gif rename to plugins/woocommerce/assets/images/wpspin.gif diff --git a/plugins/woocommerce/assets/js/.gitkeep b/plugins/woocommerce/assets/js/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/bin/build-lib.sh b/plugins/woocommerce/bin/build-lib.sh similarity index 100% rename from bin/build-lib.sh rename to plugins/woocommerce/bin/build-lib.sh diff --git a/plugins/woocommerce/bin/build-zip.sh b/plugins/woocommerce/bin/build-zip.sh new file mode 100755 index 00000000000..53d0348a0ad --- /dev/null +++ b/plugins/woocommerce/bin/build-zip.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +PLUGIN_SLUG="woocommerce" +PROJECT_PATH=$(pwd) +BUILD_PATH="${PROJECT_PATH}/build" +DEST_PATH="$BUILD_PATH/$PLUGIN_SLUG" + +echo "Generating build directory..." +rm -rf "$BUILD_PATH" +mkdir -p "$DEST_PATH" + +echo "Installing PHP and JS dependencies..." +pnpm install +composer install || exit "$?" +echo "Running JS Build..." +pnpm run build || exit "$?" +echo "Cleaning up PHP dependencies..." +composer install --no-dev || exit "$?" + +echo "Syncing files..." +rsync -rc --exclude-from="$PROJECT_PATH/.distignore" "$PROJECT_PATH/" "$DEST_PATH/" --delete --delete-excluded + +echo "Generating zip file..." +cd "$BUILD_PATH" || exit +zip -q -r "${PLUGIN_SLUG}.zip" "$PLUGIN_SLUG/" + +cd "$PROJECT_PATH" || exit +mv "$BUILD_PATH/${PLUGIN_SLUG}.zip" "$PROJECT_PATH" +echo "${PLUGIN_SLUG}.zip file generated!" + +echo "Build done!" diff --git a/bin/composer/mozart/composer.json b/plugins/woocommerce/bin/composer/mozart/composer.json similarity index 100% rename from bin/composer/mozart/composer.json rename to plugins/woocommerce/bin/composer/mozart/composer.json diff --git a/plugins/woocommerce/bin/composer/mozart/composer.lock b/plugins/woocommerce/bin/composer/mozart/composer.lock new file mode 100644 index 00000000000..c151f23629d --- /dev/null +++ b/plugins/woocommerce/bin/composer/mozart/composer.lock @@ -0,0 +1,1171 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "53c40e5764ce9bbe304e6aee0508ccb7", + "packages": [], + "packages-dev": [ + { + "name": "coenjacobs/mozart", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/coenjacobs/mozart.git", + "reference": "75ae1f91f04bbbd4b6edff282a483dfe611b2cea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/coenjacobs/mozart/zipball/75ae1f91f04bbbd4b6edff282a483dfe611b2cea", + "reference": "75ae1f91f04bbbd4b6edff282a483dfe611b2cea", + "shasum": "" + }, + "require": { + "league/flysystem": "^1.0", + "php": "^7.3|^8.0", + "symfony/console": "^4|^5", + "symfony/finder": "^4|^5" + }, + "require-dev": { + "mheap/phpunit-github-actions-printer": "^1.4", + "phpunit/phpunit": "^8.5", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.4" + }, + "default-branch": true, + "bin": [ + "bin/mozart" + ], + "type": "library", + "autoload": { + "psr-4": { + "CoenJacobs\\Mozart\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Coen Jacobs", + "email": "coenjacobs@gmail.com" + } + ], + "description": "Composes all dependencies as a package inside a WordPress plugin", + "support": { + "issues": "https://github.com/coenjacobs/mozart/issues", + "source": "https://github.com/coenjacobs/mozart/tree/master" + }, + "funding": [ + { + "url": "https://github.com/coenjacobs", + "type": "github" + } + ], + "time": "2021-08-03T18:56:55+00:00" + }, + { + "name": "league/flysystem", + "version": "1.1.9", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "094defdb4a7001845300334e7c1ee2335925ef99" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/094defdb4a7001845300334e7c1ee2335925ef99", + "reference": "094defdb4a7001845300334e7c1ee2335925ef99", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/mime-type-detection": "^1.3", + "php": "^7.2.5 || ^8.0" + }, + "conflict": { + "league/flysystem-sftp": "<1.0.6" + }, + "require-dev": { + "phpspec/prophecy": "^1.11.1", + "phpunit/phpunit": "^8.5.8" + }, + "suggest": { + "ext-ftp": "Allows you to use FTP server storage", + "ext-openssl": "Allows you to use FTPS server storage", + "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", + "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", + "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", + "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", + "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", + "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", + "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", + "league/flysystem-webdav": "Allows you to use WebDAV storage", + "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", + "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", + "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Filesystem abstraction: Many filesystems, one API.", + "keywords": [ + "Cloud Files", + "WebDAV", + "abstraction", + "aws", + "cloud", + "copy.com", + "dropbox", + "file systems", + "files", + "filesystem", + "filesystems", + "ftp", + "rackspace", + "remote", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/1.1.9" + }, + "funding": [ + { + "url": "https://offset.earth/frankdejonge", + "type": "other" + } + ], + "time": "2021-12-09T09:40:50+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "3e4a35d756eedc67096f30240a68a3149120dae7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/3e4a35d756eedc67096f30240a68a3149120dae7", + "reference": "3e4a35d756eedc67096f30240a68a3149120dae7", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.10.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2022-04-11T12:49:04+00:00" + }, + { + "name": "psr/container", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.1" + }, + "time": "2021-03-05T17:36:06+00:00" + }, + { + "name": "symfony/console", + "version": "v5.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "900275254f0a1a2afff1ab0e11abd5587a10e1d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/900275254f0a1a2afff1ab0e11abd5587a10e1d6", + "reference": "900275254f0a1a2afff1ab0e11abd5587a10e1d6", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.1|^6.0" + }, + "conflict": { + "psr/log": ">=3", + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0" + }, + "require-dev": { + "psr/log": "^1|^2", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/lock": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v5.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-03-31T17:09:19+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:53:40+00:00" + }, + { + "name": "symfony/finder", + "version": "v5.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "231313534dded84c7ecaa79d14bc5da4ccb69b7d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/231313534dded84c7ecaa79d14bc5da4ccb69b7d", + "reference": "231313534dded84c7ecaa79d14bc5da4ccb69b7d", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v5.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-26T16:34:36+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "30885182c981ab175d4d034db0f6f469898070ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", + "reference": "30885182c981ab175d4d034db0f6f469898070ab", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-10-20T20:35:02+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "81b86b50cf841a64252b439e738e97f4a34e2783" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783", + "reference": "81b86b50cf841a64252b439e738e97f4a34e2783", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-23T21:10:46+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-02-19T12:13:01+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-30T18:21:41+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/cc5db0e22b3cb4111010e48785a97f670b350ca5", + "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-06-05T21:20:04+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4407588e0d3f1f52efb65fbe92babe41f37fe50c", + "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-03-04T08:16:47+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/24d9dc654b83e91aa59f9d167b131bc3b5bea24c", + "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1", + "symfony/deprecation-contracts": "^2.1|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v2.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-03-13T20:07:29+00:00" + }, + { + "name": "symfony/string", + "version": "v5.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "92043b7d8383e48104e411bc9434b260dbeb5a10" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/92043b7d8383e48104e411bc9434b260dbeb5a10", + "reference": "92043b7d8383e48104e411bc9434b260dbeb5a10", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" + }, + "conflict": { + "symfony/translation-contracts": ">=3.0" + }, + "require-dev": { + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/http-client": "^4.4|^5.0|^6.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0|^6.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v5.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:53:40+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "coenjacobs/mozart": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "platform-overrides": { + "php": "7.3" + }, + "plugin-api-version": "2.1.0" +} diff --git a/plugins/woocommerce/bin/composer/phpcs/composer.json b/plugins/woocommerce/bin/composer/phpcs/composer.json new file mode 100644 index 00000000000..99f7cb5646a --- /dev/null +++ b/plugins/woocommerce/bin/composer/phpcs/composer.json @@ -0,0 +1,10 @@ +{ + "require-dev": { + "woocommerce/woocommerce-sniffs": "^0.1.2" + }, + "config": { + "platform": { + "php": "7.0" + } + } +} diff --git a/plugins/woocommerce/bin/composer/phpcs/composer.lock b/plugins/woocommerce/bin/composer/phpcs/composer.lock new file mode 100644 index 00000000000..6cf9a6b7cc1 --- /dev/null +++ b/plugins/woocommerce/bin/composer/phpcs/composer.lock @@ -0,0 +1,420 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "51112a9a1fd6cd39c29579a93a59cf9c", + "packages": [], + "packages-dev": [ + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v0.7.2", + "source": { + "type": "git", + "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", + "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", + "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "*", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpcompatibility/php-compatibility": "^9.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "franck.nijhof@dealerdirect.com", + "homepage": "http://www.frenck.nl", + "role": "Developer / IT Manager" + }, + { + "name": "Contributors", + "homepage": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "homepage": "http://www.dealerdirect.com", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", + "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + }, + "time": "2022-02-04T12:51:07+00:00" + }, + { + "name": "phpcompatibility/php-compatibility", + "version": "9.3.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" + }, + "conflict": { + "squizlabs/php_codesniffer": "2.6.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "homepage": "https://github.com/wimg", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + } + ], + "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", + "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "keywords": [ + "compatibility", + "phpcs", + "standards" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibility" + }, + "time": "2019-12-27T09:44:58+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-paragonie", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", + "reference": "ddabec839cc003651f2ce695c938686d1086cf43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/ddabec839cc003651f2ce695c938686d1086cf43", + "reference": "ddabec839cc003651f2ce695c938686d1086cf43", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^9.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7", + "paragonie/random_compat": "dev-master", + "paragonie/sodium_compat": "dev-master" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "paragonie", + "phpcs", + "polyfill", + "standards" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" + }, + "time": "2021-02-15T10:24:51+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-wp", + "version": "2.1.3", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", + "reference": "d55de55f88697b9cdb94bccf04f14eb3b11cf308" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/d55de55f88697b9cdb94bccf04f14eb3b11cf308", + "reference": "d55de55f88697b9cdb94bccf04f14eb3b11cf308", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^9.0", + "phpcompatibility/phpcompatibility-paragonie": "^1.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A ruleset for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by WordPress.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "phpcs", + "standards", + "wordpress" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" + }, + "time": "2021-12-30T16:37:40+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.6.2", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a", + "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards" + ], + "support": { + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, + "time": "2021-12-12T21:44:58+00:00" + }, + { + "name": "woocommerce/woocommerce-sniffs", + "version": "0.1.2", + "source": { + "type": "git", + "url": "https://github.com/woocommerce/woocommerce-sniffs.git", + "reference": "5566270d280a300bc24bd0cb055a8b9325afdd6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/woocommerce/woocommerce-sniffs/zipball/5566270d280a300bc24bd0cb055a8b9325afdd6b", + "reference": "5566270d280a300bc24bd0cb055a8b9325afdd6b", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "php": ">=7.0", + "phpcompatibility/phpcompatibility-wp": "^2.1.0", + "wp-coding-standards/wpcs": "^2.3.0" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Claudio Sanches", + "email": "claudio@automattic.com" + } + ], + "description": "WooCommerce sniffs", + "keywords": [ + "phpcs", + "standards", + "woocommerce", + "wordpress" + ], + "support": { + "issues": "https://github.com/woocommerce/woocommerce-sniffs/issues", + "source": "https://github.com/woocommerce/woocommerce-sniffs/tree/0.1.2" + }, + "time": "2022-01-21T20:13:23+00:00" + }, + { + "name": "wp-coding-standards/wpcs", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", + "reference": "7da1894633f168fe244afc6de00d141f27517b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7da1894633f168fe244afc6de00d141f27517b62", + "reference": "7da1894633f168fe244afc6de00d141f27517b62", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.3.1" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || ^0.6", + "phpcompatibility/php-compatibility": "^9.0", + "phpcsstandards/phpcsdevtools": "^1.0", + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.6 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", + "keywords": [ + "phpcs", + "standards", + "wordpress" + ], + "support": { + "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", + "source": "https://github.com/WordPress/WordPress-Coding-Standards", + "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" + }, + "time": "2020-05-13T23:57:56+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "platform-overrides": { + "php": "7.0" + }, + "plugin-api-version": "2.1.0" +} diff --git a/bin/composer/phpunit/composer.json b/plugins/woocommerce/bin/composer/phpunit/composer.json similarity index 100% rename from bin/composer/phpunit/composer.json rename to plugins/woocommerce/bin/composer/phpunit/composer.json diff --git a/plugins/woocommerce/bin/composer/phpunit/composer.lock b/plugins/woocommerce/bin/composer/phpunit/composer.lock new file mode 100644 index 00000000000..7fc2f48ec7a --- /dev/null +++ b/plugins/woocommerce/bin/composer/phpunit/composer.lock @@ -0,0 +1,1701 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "cbe696cc9c487e3027f943e82d88261f", + "packages": [], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/master" + }, + "time": "2015-06-14T21:17:01+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^4.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.x" + }, + "time": "2017-10-19T19:58:43+00:00" + }, + { + "name": "phar-io/manifest", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0", + "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^1.0.1", + "php": "^5.6 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/master" + }, + "time": "2017-03-05T18:14:27+00:00" + }, + { + "name": "phar-io/version", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df", + "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/master" + }, + "time": "2017-03-05T17:38:23+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/master" + }, + "time": "2017-09-11T18:02:19+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "4.3.4", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/da3fd972d6bafd628114f7e7e036f45944b62e9c", + "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c", + "shasum": "" + }, + "require": { + "php": "^7.0", + "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0", + "phpdocumentor/type-resolver": "~0.4 || ^1.0.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "doctrine/instantiator": "^1.0.5", + "mockery/mockery": "^1.0", + "phpdocumentor/type-resolver": "0.4.*", + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/release/4.x" + }, + "time": "2019-12-28T18:55:12+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "0.5.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "cf842904952e64e703800d094cdf34e715a8a3ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/cf842904952e64e703800d094cdf34e715a8a3ae", + "reference": "cf842904952e64e703800d094cdf34e715a8a3ae", + "shasum": "" + }, + "require": { + "php": "^7.0", + "phpdocumentor/reflection-common": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/master" + }, + "time": "2017-12-30T13:23:38+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "v1.10.3", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "451c3cd1418cf640de218914901e51b064abb093" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093", + "reference": "451c3cd1418cf640de218914901e51b064abb093", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", + "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0", + "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.5 || ^3.2", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/v1.10.3" + }, + "time": "2020-03-05T15:02:03+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "5.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "c89677919c5dd6d3b3852f230a663118762218ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac", + "reference": "c89677919c5dd6d3b3852f230a663118762218ac", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^7.0", + "phpunit/php-file-iterator": "^1.4.2", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^2.0.1", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^3.0", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-xdebug": "^2.5.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/5.3" + }, + "time": "2018-04-06T15:36:58+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "irc": "irc://irc.freenode.net/phpunit", + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/1.4.5" + }, + "time": "2017-11-27T13:52:08+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" + }, + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/master" + }, + "time": "2017-02-26T11:10:40+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "791198a2c6254db10131eecfe8c06670700904db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db", + "reference": "791198a2c6254db10131eecfe8c06670700904db", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.2.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", + "source": "https://github.com/sebastianbergmann/php-token-stream/tree/master" + }, + "abandoned": true, + "time": "2017-11-27T05:48:46+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "6.5.14", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bac23fe7ff13dbdb461481f706f0e9fe746334b7", + "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "^1.6.1", + "phar-io/manifest": "^1.0.1", + "phar-io/version": "^1.0", + "php": "^7.0", + "phpspec/prophecy": "^1.7", + "phpunit/php-code-coverage": "^5.3", + "phpunit/php-file-iterator": "^1.4.3", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^1.0.9", + "phpunit/phpunit-mock-objects": "^5.0.9", + "sebastian/comparator": "^2.1", + "sebastian/diff": "^2.0", + "sebastian/environment": "^3.1", + "sebastian/exporter": "^3.1", + "sebastian/global-state": "^2.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^1.0", + "sebastian/version": "^2.0.1" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "3.0.2", + "phpunit/dbunit": "<3.0" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-xdebug": "*", + "phpunit/php-invoker": "^1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/6.5.14" + }, + "time": "2019-02-01T05:22:47+00:00" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "5.0.10", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/cd1cf05c553ecfec36b170070573e540b67d3f1f", + "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.5", + "php": "^7.0", + "phpunit/php-text-template": "^1.2.1", + "sebastian/exporter": "^3.1" + }, + "conflict": { + "phpunit/phpunit": "<6.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.5.11" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit-mock-objects/issues", + "source": "https://github.com/sebastianbergmann/phpunit-mock-objects/tree/5.0.10" + }, + "abandoned": true, + "time": "2018-08-09T05:50:03+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619", + "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T08:15:22+00:00" + }, + { + "name": "sebastian/comparator", + "version": "2.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9", + "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/diff": "^2.0 || ^3.0", + "sebastian/exporter": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/master" + }, + "time": "2018-02-01T13:46:46+00:00" + }, + { + "name": "sebastian/diff", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/master" + }, + "time": "2017-08-03T08:09:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/master" + }, + "time": "2017-07-01T08:51:00+00:00" + }, + { + "name": "sebastian/exporter", + "version": "3.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "0c32ea2e40dbf59de29f3b49bf375176ce7dd8db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0c32ea2e40dbf59de29f3b49bf375176ce7dd8db", + "reference": "0c32ea2e40dbf59de29f3b49bf375176ce7dd8db", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-11-11T13:51:24+00:00" + }, + { + "name": "sebastian/global-state", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/2.0.0" + }, + "time": "2017-04-27T15:39:26+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", + "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:40:27+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", + "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:37:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb", + "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:34:24+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/master" + }, + "time": "2015-07-28T20:34:47+00:00" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/master" + }, + "time": "2016-10-03T07:35:21+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.19.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "aed596913b70fae57be53d86faa2e9ef85a2297b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/aed596913b70fae57be53d86faa2e9ef85a2297b", + "reference": "aed596913b70fae57be53d86faa2e9ef85a2297b", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.19-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.19.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T09:01:57+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/master" + }, + "time": "2019-06-13T22:48:21+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.9.1", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<3.9.1" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36 || ^7.5.13" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.9.1" + }, + "time": "2020-07-08T17:02:28+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "platform-overrides": { + "php": "7.0" + }, + "plugin-api-version": "2.1.0" +} diff --git a/bin/composer/wp/composer.json b/plugins/woocommerce/bin/composer/wp/composer.json similarity index 100% rename from bin/composer/wp/composer.json rename to plugins/woocommerce/bin/composer/wp/composer.json diff --git a/plugins/woocommerce/bin/composer/wp/composer.lock b/plugins/woocommerce/bin/composer/wp/composer.lock new file mode 100644 index 00000000000..8fee101ebe0 --- /dev/null +++ b/plugins/woocommerce/bin/composer/wp/composer.lock @@ -0,0 +1,688 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "4d4f2befccefe100869d30305083672b", + "packages": [], + "packages-dev": [ + { + "name": "eftec/bladeone", + "version": "3.52", + "source": { + "type": "git", + "url": "https://github.com/EFTEC/BladeOne.git", + "reference": "a19bf66917de0b29836983db87a455a4f6e32148" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/EFTEC/BladeOne/zipball/a19bf66917de0b29836983db87a455a4f6e32148", + "reference": "a19bf66917de0b29836983db87a455a4f6e32148", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=5.6" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16.1", + "phpunit/phpunit": "^5.7", + "squizlabs/php_codesniffer": "^3.5.4" + }, + "suggest": { + "eftec/bladeonehtml": "Extension to create forms", + "ext-mbstring": "This extension is used if it's active" + }, + "type": "library", + "autoload": { + "psr-4": { + "eftec\\bladeone\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jorge Patricio Castro Castillo", + "email": "jcastro@eftec.cl" + } + ], + "description": "The standalone version Blade Template Engine from Laravel in a single php file", + "homepage": "https://github.com/EFTEC/BladeOne", + "keywords": [ + "blade", + "php", + "template", + "templating", + "view" + ], + "support": { + "issues": "https://github.com/EFTEC/BladeOne/issues", + "source": "https://github.com/EFTEC/BladeOne/tree/3.52" + }, + "time": "2021-04-17T13:49:01+00:00" + }, + { + "name": "gettext/gettext", + "version": "v4.8.6", + "source": { + "type": "git", + "url": "https://github.com/php-gettext/Gettext.git", + "reference": "bbeb8f4d3077663739aecb4551b22e720c0e9efe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-gettext/Gettext/zipball/bbeb8f4d3077663739aecb4551b22e720c0e9efe", + "reference": "bbeb8f4d3077663739aecb4551b22e720c0e9efe", + "shasum": "" + }, + "require": { + "gettext/languages": "^2.3", + "php": ">=5.4.0" + }, + "require-dev": { + "illuminate/view": "^5.0.x-dev", + "phpunit/phpunit": "^4.8|^5.7|^6.5", + "squizlabs/php_codesniffer": "^3.0", + "symfony/yaml": "~2", + "twig/extensions": "*", + "twig/twig": "^1.31|^2.0" + }, + "suggest": { + "illuminate/view": "Is necessary if you want to use the Blade extractor", + "symfony/yaml": "Is necessary if you want to use the Yaml extractor/generator", + "twig/extensions": "Is necessary if you want to use the Twig extractor", + "twig/twig": "Is necessary if you want to use the Twig extractor" + }, + "type": "library", + "autoload": { + "psr-4": { + "Gettext\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oscar Otero", + "email": "oom@oscarotero.com", + "homepage": "http://oscarotero.com", + "role": "Developer" + } + ], + "description": "PHP gettext manager", + "homepage": "https://github.com/oscarotero/Gettext", + "keywords": [ + "JS", + "gettext", + "i18n", + "mo", + "po", + "translation" + ], + "support": { + "email": "oom@oscarotero.com", + "issues": "https://github.com/oscarotero/Gettext/issues", + "source": "https://github.com/php-gettext/Gettext/tree/v4.8.6" + }, + "funding": [ + { + "url": "https://paypal.me/oscarotero", + "type": "custom" + }, + { + "url": "https://github.com/oscarotero", + "type": "github" + }, + { + "url": "https://www.patreon.com/misteroom", + "type": "patreon" + } + ], + "time": "2021-10-19T10:44:53+00:00" + }, + { + "name": "gettext/languages", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/php-gettext/Languages.git", + "reference": "ed56dd2c7f4024cc953ed180d25f02f2640e3ffa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-gettext/Languages/zipball/ed56dd2c7f4024cc953ed180d25f02f2640e3ffa", + "reference": "ed56dd2c7f4024cc953ed180d25f02f2640e3ffa", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.4" + }, + "bin": [ + "bin/export-plural-rules" + ], + "type": "library", + "autoload": { + "psr-4": { + "Gettext\\Languages\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michele Locati", + "email": "mlocati@gmail.com", + "role": "Developer" + } + ], + "description": "gettext languages with plural rules", + "homepage": "https://github.com/php-gettext/Languages", + "keywords": [ + "cldr", + "i18n", + "internationalization", + "l10n", + "language", + "languages", + "localization", + "php", + "plural", + "plural rules", + "plurals", + "translate", + "translations", + "unicode" + ], + "support": { + "issues": "https://github.com/php-gettext/Languages/issues", + "source": "https://github.com/php-gettext/Languages/tree/2.9.0" + }, + "funding": [ + { + "url": "https://paypal.me/mlocati", + "type": "custom" + }, + { + "url": "https://github.com/mlocati", + "type": "github" + } + ], + "time": "2021-11-11T17:30:39+00:00" + }, + { + "name": "mck89/peast", + "version": "v1.13.11", + "source": { + "type": "git", + "url": "https://github.com/mck89/peast.git", + "reference": "78c57966f3da5f223636ea0417d71ac6ff61e47f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mck89/peast/zipball/78c57966f3da5f223636ea0417d71ac6ff61e47f", + "reference": "78c57966f3da5f223636ea0417d71ac6ff61e47f", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.13.11-dev" + } + }, + "autoload": { + "psr-4": { + "Peast\\": "lib/Peast/", + "Peast\\test\\": "test/Peast/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marco Marchiò", + "email": "marco.mm89@gmail.com" + } + ], + "description": "Peast is PHP library that generates AST for JavaScript code", + "support": { + "issues": "https://github.com/mck89/peast/issues", + "source": "https://github.com/mck89/peast/tree/v1.13.11" + }, + "time": "2022-01-11T17:58:18+00:00" + }, + { + "name": "mustache/mustache", + "version": "v2.14.1", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/mustache.php.git", + "reference": "579ffa5c96e1d292c060b3dd62811ff01ad8c24e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/579ffa5c96e1d292c060b3dd62811ff01ad8c24e", + "reference": "579ffa5c96e1d292c060b3dd62811ff01ad8c24e", + "shasum": "" + }, + "require": { + "php": ">=5.2.4" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~1.11", + "phpunit/phpunit": "~3.7|~4.0|~5.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Mustache": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info", + "homepage": "http://justinhileman.com" + } + ], + "description": "A Mustache implementation in PHP.", + "homepage": "https://github.com/bobthecow/mustache.php", + "keywords": [ + "mustache", + "templating" + ], + "support": { + "issues": "https://github.com/bobthecow/mustache.php/issues", + "source": "https://github.com/bobthecow/mustache.php/tree/v2.14.1" + }, + "time": "2022-01-21T06:08:36+00:00" + }, + { + "name": "rmccue/requests", + "version": "v1.8.1", + "source": { + "type": "git", + "url": "https://github.com/WordPress/Requests.git", + "reference": "82e6936366eac3af4d836c18b9d8c31028fe4cd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/Requests/zipball/82e6936366eac3af4d836c18b9d8c31028fe4cd5", + "reference": "82e6936366eac3af4d836c18b9d8c31028fe4cd5", + "shasum": "" + }, + "require": { + "php": ">=5.2" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7", + "php-parallel-lint/php-console-highlighter": "^0.5.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpcompatibility/php-compatibility": "^9.0", + "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5", + "requests/test-server": "dev-master", + "squizlabs/php_codesniffer": "^3.5", + "wp-coding-standards/wpcs": "^2.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Requests": "library/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Ryan McCue", + "homepage": "http://ryanmccue.info" + } + ], + "description": "A HTTP library written in PHP, for human beings.", + "homepage": "http://github.com/WordPress/Requests", + "keywords": [ + "curl", + "fsockopen", + "http", + "idna", + "ipv6", + "iri", + "sockets" + ], + "support": { + "issues": "https://github.com/WordPress/Requests/issues", + "source": "https://github.com/WordPress/Requests/tree/v1.8.1" + }, + "time": "2021-06-04T09:56:25+00:00" + }, + { + "name": "symfony/finder", + "version": "v3.3.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/baea7f66d30854ad32988c11a09d7ffd485810c4", + "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Finder Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/3.3" + }, + "time": "2017-06-01T21:01:25+00:00" + }, + { + "name": "wp-cli/i18n-command", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/wp-cli/i18n-command.git", + "reference": "bcb1a8159679cafdf1da884dbe5830122bae2c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wp-cli/i18n-command/zipball/bcb1a8159679cafdf1da884dbe5830122bae2c4d", + "reference": "bcb1a8159679cafdf1da884dbe5830122bae2c4d", + "shasum": "" + }, + "require": { + "eftec/bladeone": "3.52", + "gettext/gettext": "^4.8", + "mck89/peast": "^1.13.11", + "wp-cli/wp-cli": "^2.5" + }, + "require-dev": { + "wp-cli/scaffold-command": "^1.2 || ^2", + "wp-cli/wp-cli-tests": "^3.1" + }, + "suggest": { + "ext-mbstring": "Used for calculating include/exclude matches in code extraction" + }, + "type": "wp-cli-package", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + }, + "bundled": true, + "commands": [ + "i18n", + "i18n make-pot", + "i18n make-json" + ] + }, + "autoload": { + "files": [ + "i18n-command.php" + ], + "psr-4": { + "WP_CLI\\I18n\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Pascal Birchler", + "homepage": "https://pascalbirchler.com/" + } + ], + "description": "Provides internationalization tools for WordPress projects.", + "homepage": "https://github.com/wp-cli/i18n-command", + "support": { + "issues": "https://github.com/wp-cli/i18n-command/issues", + "source": "https://github.com/wp-cli/i18n-command/tree/v2.3.0" + }, + "time": "2022-04-06T15:32:48+00:00" + }, + { + "name": "wp-cli/mustangostang-spyc", + "version": "0.6.3", + "source": { + "type": "git", + "url": "https://github.com/wp-cli/spyc.git", + "reference": "6aa0b4da69ce9e9a2c8402dab8d43cf32c581cc7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wp-cli/spyc/zipball/6aa0b4da69ce9e9a2c8402dab8d43cf32c581cc7", + "reference": "6aa0b4da69ce9e9a2c8402dab8d43cf32c581cc7", + "shasum": "" + }, + "require": { + "php": ">=5.3.1" + }, + "require-dev": { + "phpunit/phpunit": "4.3.*@dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.5.x-dev" + } + }, + "autoload": { + "files": [ + "includes/functions.php" + ], + "psr-4": { + "Mustangostang\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "mustangostang", + "email": "vlad.andersen@gmail.com" + } + ], + "description": "A simple YAML loader/dumper class for PHP (WP-CLI fork)", + "homepage": "https://github.com/mustangostang/spyc/", + "support": { + "source": "https://github.com/wp-cli/spyc/tree/autoload" + }, + "time": "2017-04-25T11:26:20+00:00" + }, + { + "name": "wp-cli/php-cli-tools", + "version": "v0.11.13", + "source": { + "type": "git", + "url": "https://github.com/wp-cli/php-cli-tools.git", + "reference": "a2866855ac1abc53005c102e901553ad5772dc04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wp-cli/php-cli-tools/zipball/a2866855ac1abc53005c102e901553ad5772dc04", + "reference": "a2866855ac1abc53005c102e901553ad5772dc04", + "shasum": "" + }, + "require": { + "php": ">= 5.3.0" + }, + "type": "library", + "autoload": { + "files": [ + "lib/cli/cli.php" + ], + "psr-0": { + "cli": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Bachhuber", + "email": "daniel@handbuilt.co", + "role": "Maintainer" + }, + { + "name": "James Logsdon", + "email": "jlogsdon@php.net", + "role": "Developer" + } + ], + "description": "Console utilities for PHP", + "homepage": "http://github.com/wp-cli/php-cli-tools", + "keywords": [ + "cli", + "console" + ], + "support": { + "issues": "https://github.com/wp-cli/php-cli-tools/issues", + "source": "https://github.com/wp-cli/php-cli-tools/tree/v0.11.13" + }, + "time": "2021-07-01T15:08:16+00:00" + }, + { + "name": "wp-cli/wp-cli", + "version": "v2.6.0", + "source": { + "type": "git", + "url": "https://github.com/wp-cli/wp-cli.git", + "reference": "dee13c2baf6bf972484a63f8b8dab48f7220f095" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/dee13c2baf6bf972484a63f8b8dab48f7220f095", + "reference": "dee13c2baf6bf972484a63f8b8dab48f7220f095", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "mustache/mustache": "^2.14.1", + "php": "^5.6 || ^7.0 || ^8.0", + "rmccue/requests": "^1.8", + "symfony/finder": ">2.7", + "wp-cli/mustangostang-spyc": "^0.6.3", + "wp-cli/php-cli-tools": "~0.11.2" + }, + "require-dev": { + "roave/security-advisories": "dev-latest", + "wp-cli/db-command": "^1.3 || ^2", + "wp-cli/entity-command": "^1.2 || ^2", + "wp-cli/extension-command": "^1.1 || ^2", + "wp-cli/package-command": "^1 || ^2", + "wp-cli/wp-cli-tests": "^3.1.3" + }, + "suggest": { + "ext-readline": "Include for a better --prompt implementation", + "ext-zip": "Needed to support extraction of ZIP archives when doing downloads or updates" + }, + "bin": [ + "bin/wp", + "bin/wp.bat" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "WP_CLI\\": "php/" + }, + "classmap": [ + "php/class-wp-cli.php", + "php/class-wp-cli-command.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WP-CLI framework", + "homepage": "https://wp-cli.org", + "keywords": [ + "cli", + "wordpress" + ], + "support": { + "docs": "https://make.wordpress.org/cli/handbook/", + "issues": "https://github.com/wp-cli/wp-cli/issues", + "source": "https://github.com/wp-cli/wp-cli" + }, + "time": "2022-01-25T16:31:27+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "platform-overrides": { + "php": "7.0" + }, + "plugin-api-version": "2.1.0" +} diff --git a/plugins/woocommerce/bin/contributors.sh b/plugins/woocommerce/bin/contributors.sh new file mode 100755 index 00000000000..e25389fb6e2 --- /dev/null +++ b/plugins/woocommerce/bin/contributors.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +read -p 'What date (YYYY-MM-DD) should we get contributions since? (i.e. date of previous release): ' from_date +read -sp 'Provide a personal access token (you must): ' auth_token + +ignored_users="renovate-bot,apps/renovate,renovate,renovate[bot],github-actions[bot],dependabot,dependabot[bot]" +output_file="contributors.html" +common_arguments="--owner woocommerce --fromDate $from_date --authToken $auth_token --cols 6 --sortBy contributions --format html --sortOrder desc --showlogin true --filter $ignored_users" + +echo "" + +echo "

    WooCommerce core

    " > $output_file +echo "Generating contributor list for WC core since $from_date" +./node_modules/.bin/githubcontrib --repo woocommerce $common_arguments >> $output_file + +echo "

    WooCommerce Admin

    " >> $output_file +echo "Generating contributor list for WC Admin since $from_date" +./node_modules/.bin/githubcontrib --repo woocommerce-admin $common_arguments >> $output_file + +echo "

    WooCommerce Blocks

    " >> $output_file +echo "Generating contributor list for WC Blocks since $from_date" +./node_modules/.bin/githubcontrib --repo woocommerce-gutenberg-products-block $common_arguments >> $output_file + +echo "

    Action Scheduler

    " >> $output_file +echo "Generating contributor list for Action Scheduler since $from_date" +./node_modules/.bin/githubcontrib --repo action-scheduler $common_arguments >> $output_file + +echo "Output generated to $output_file." diff --git a/plugins/woocommerce/bin/generate-feature-config.php b/plugins/woocommerce/bin/generate-feature-config.php new file mode 100644 index 00000000000..ffe637c880b --- /dev/null +++ b/plugins/woocommerce/bin/generate-feature-config.php @@ -0,0 +1,38 @@ +features as $feature => $bool ) { + $write .= "\t\t\t'{$feature}' => " . ( $bool ? 'true' : 'false' ) . ",\n"; +} +$write .= "\t\t);\n"; +$write .= "\t}\n"; +$write .= "}\n"; + +$config_file = fopen( __DIR__ . '/../includes/react-admin/feature-config.php', 'w' ); + +fwrite( $config_file, $write ); +fclose( $config_file ); diff --git a/bin/package-update-textdomain.js b/plugins/woocommerce/bin/package-update-textdomain.js similarity index 100% rename from bin/package-update-textdomain.js rename to plugins/woocommerce/bin/package-update-textdomain.js diff --git a/plugins/woocommerce/bin/package-update.sh b/plugins/woocommerce/bin/package-update.sh new file mode 100755 index 00000000000..dd574adfa97 --- /dev/null +++ b/plugins/woocommerce/bin/package-update.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +# Output colorized strings +# +# Color codes: +# 0 - black +# 1 - red +# 2 - green +# 3 - yellow +# 4 - blue +# 5 - magenta +# 6 - cian +# 7 - white +output() { + echo "$(tput setaf "$1")$2$(tput sgr0)" +} + +if [ ! -d "packages/" ]; then + output 1 "./packages doesn't exist!" + output 1 "run \"composer install\" before proceed." +fi + +# Autoloader +output 3 "Updating autoloader classmaps..." +composer dump-autoload +output 2 "Done" + +if [ -z "$SKIP_UPDATE_TEXTDOMAINS" ]; then + # Convert textdomains + output 3 "Updating package PHP textdomains..." + + # Replace text domains within packages with woocommerce + pnpm run packages:fix:textdomain + output 2 "Done!" + + output 3 "Updating package JS textdomains..." + find ./packages/woocommerce-blocks -iname '*.js' -exec sed -i.bak -e "s/'woo-gutenberg-products-block'/'woocommerce'/g" -e "s/\"woo-gutenberg-products-block\"/\"woocommerce\"/g" {} \; + find ./packages/woocommerce-blocks -iname '*.js' -exec sed -i.bak -e "s/'woocommerce-admin'/'woocommerce'/g" -e "s/\"woocommerce-admin\"/\"woocommerce\"/g" {} \; +fi + +# Cleanup backup files +find ./packages -name "*.bak" -type f -delete +output 2 "Done!" diff --git a/plugins/woocommerce/changelog/.gitkeep b/plugins/woocommerce/changelog/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/plugins/woocommerce/changelog/31477-remove-coupon-js b/plugins/woocommerce/changelog/31477-remove-coupon-js new file mode 100644 index 00000000000..0771ab1e340 --- /dev/null +++ b/plugins/woocommerce/changelog/31477-remove-coupon-js @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Ensure observers of the `removed_coupon_in_checkout` event can access the coupon code successfully. diff --git a/plugins/woocommerce/changelog/add-wctracker-dropins b/plugins/woocommerce/changelog/add-wctracker-dropins new file mode 100644 index 00000000000..ef5b3fd34e3 --- /dev/null +++ b/plugins/woocommerce/changelog/add-wctracker-dropins @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Add dropins to WCTracker diff --git a/plugins/woocommerce/changelog/fix-29705-tests-install b/plugins/woocommerce/changelog/fix-29705-tests-install new file mode 100644 index 00000000000..58bfebea831 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-29705-tests-install @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Add basic pre-requisite checking to the test suite installation script. diff --git a/plugins/woocommerce/changelog/fix-32149_mark_marketing_task_as_complete b/plugins/woocommerce/changelog/fix-32149_mark_marketing_task_as_complete new file mode 100644 index 00000000000..847efafb53f --- /dev/null +++ b/plugins/woocommerce/changelog/fix-32149_mark_marketing_task_as_complete @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Mark marketing task as complete when an extension is installed #32630 diff --git a/plugins/woocommerce/changelog/fix-32600-vietnam b/plugins/woocommerce/changelog/fix-32600-vietnam new file mode 100644 index 00000000000..b6fa4972010 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-32600-vietnam @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +For Vietnam, the second street address line should be displayed but not required. diff --git a/plugins/woocommerce/changelog/fix-32705 b/plugins/woocommerce/changelog/fix-32705 new file mode 100644 index 00000000000..d3025548d9e --- /dev/null +++ b/plugins/woocommerce/changelog/fix-32705 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fixing bug on sectioned task list for tasks with actionUrl property diff --git a/plugins/woocommerce/changelog/fix-admin-payment-type b/plugins/woocommerce/changelog/fix-admin-payment-type new file mode 100644 index 00000000000..45c6988663a --- /dev/null +++ b/plugins/woocommerce/changelog/fix-admin-payment-type @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix typescript type errors in react admin `payments` & `payment-welcome` diff --git a/plugins/woocommerce/changelog/fix-admin-two-column-tasks-types b/plugins/woocommerce/changelog/fix-admin-two-column-tasks-types new file mode 100644 index 00000000000..056d3b655e8 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-admin-two-column-tasks-types @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix typescript type errors in react admin ./client/two-column/tasks diff --git a/plugins/woocommerce/changelog/fix-admin-types b/plugins/woocommerce/changelog/fix-admin-types new file mode 100644 index 00000000000..fbe0512fd64 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-admin-types @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Add @types/* & declare TS modules to fix admin TS errors diff --git a/plugins/woocommerce/changelog/fix-client-shipping-typescritp-errors b/plugins/woocommerce/changelog/fix-client-shipping-typescritp-errors new file mode 100644 index 00000000000..5cc68050080 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-client-shipping-typescritp-errors @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Fix typescript type errors in react admin ./client/shipping \ No newline at end of file diff --git a/plugins/woocommerce/changelog/fix-core-build-scripts b/plugins/woocommerce/changelog/fix-core-build-scripts new file mode 100644 index 00000000000..c5e5b19f105 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-core-build-scripts @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Standardize WooCommerce build scripts diff --git a/plugins/woocommerce/changelog/fix-exclude-drafts-in-reports b/plugins/woocommerce/changelog/fix-exclude-drafts-in-reports new file mode 100644 index 00000000000..354111e694b --- /dev/null +++ b/plugins/woocommerce/changelog/fix-exclude-drafts-in-reports @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Don't include draft orders in reports diff --git a/plugins/woocommerce/changelog/fix-wp-admin-scripts-type b/plugins/woocommerce/changelog/fix-wp-admin-scripts-type new file mode 100644 index 00000000000..5f5fb04fb5d --- /dev/null +++ b/plugins/woocommerce/changelog/fix-wp-admin-scripts-type @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Fix typescript type errors in react admin ./client/wp-admin-scripts diff --git a/plugins/woocommerce/changelog/remove-admin-tests-folder b/plugins/woocommerce/changelog/remove-admin-tests-folder new file mode 100644 index 00000000000..7d8c33d848d --- /dev/null +++ b/plugins/woocommerce/changelog/remove-admin-tests-folder @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Simply remove woocommerce-admin tests folder + + diff --git a/plugins/woocommerce/changelog/remove-post_id-from-orders-table b/plugins/woocommerce/changelog/remove-post_id-from-orders-table new file mode 100644 index 00000000000..f3c4ee19ab2 --- /dev/null +++ b/plugins/woocommerce/changelog/remove-post_id-from-orders-table @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Remove the post_id column from the orders table, and adjust the SQL queries that count/get out of sync orders accordingly. diff --git a/plugins/woocommerce/changelog/update-32508_pnpx_deprecated b/plugins/woocommerce/changelog/update-32508_pnpx_deprecated new file mode 100644 index 00000000000..e8010152980 --- /dev/null +++ b/plugins/woocommerce/changelog/update-32508_pnpx_deprecated @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Updating instance of now-deprecated pnpx to pnpm dlx/exec diff --git a/plugins/woocommerce/changelog/update-admin-tsconfig-config b/plugins/woocommerce/changelog/update-admin-tsconfig-config new file mode 100644 index 00000000000..c16f8582c8f --- /dev/null +++ b/plugins/woocommerce/changelog/update-admin-tsconfig-config @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Update woo admin ts config to have an isolated TS environment diff --git a/plugins/woocommerce/changelog/update-refactor-and-improve-tests-payments-task b/plugins/woocommerce/changelog/update-refactor-and-improve-tests-payments-task new file mode 100644 index 00000000000..740381593c8 --- /dev/null +++ b/plugins/woocommerce/changelog/update-refactor-and-improve-tests-payments-task @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Refactor and improve tests payments task diff --git a/plugins/woocommerce/changelog/update-woocommerce-blocks-7.4.1 b/plugins/woocommerce/changelog/update-woocommerce-blocks-7.4.1 new file mode 100644 index 00000000000..85231c63d41 --- /dev/null +++ b/plugins/woocommerce/changelog/update-woocommerce-blocks-7.4.1 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Woo Blocks 7.3.0 & 7.4.1 diff --git a/plugins/woocommerce/client/admin/config/core.json b/plugins/woocommerce/client/admin/config/core.json new file mode 100644 index 00000000000..9a36495bc54 --- /dev/null +++ b/plugins/woocommerce/client/admin/config/core.json @@ -0,0 +1,25 @@ +{ + "features": { + "activity-panels": true, + "analytics": true, + "coupons": true, + "customer-effort-score-tracks": true, + "homescreen": true, + "marketing": true, + "minified-js": false, + "mobile-app-banner": true, + "navigation": true, + "onboarding": true, + "onboarding-tasks": true, + "remote-inbox-notifications": true, + "remote-free-extensions": true, + "payment-gateway-suggestions": true, + "settings": false, + "shipping-label-banner": true, + "subscriptions": true, + "store-alerts": true, + "transient-notices": true, + "wc-pay-promotion": true, + "wc-pay-welcome-page": true + } +} diff --git a/plugins/woocommerce/client/admin/config/development.json b/plugins/woocommerce/client/admin/config/development.json new file mode 100644 index 00000000000..b407ceeeb20 --- /dev/null +++ b/plugins/woocommerce/client/admin/config/development.json @@ -0,0 +1,25 @@ +{ + "features": { + "activity-panels": true, + "analytics": true, + "coupons": true, + "customer-effort-score-tracks": true, + "homescreen": true, + "marketing": true, + "minified-js": true, + "mobile-app-banner": true, + "navigation": true, + "onboarding": true, + "onboarding-tasks": true, + "payment-gateway-suggestions": true, + "remote-inbox-notifications": true, + "remote-free-extensions": true, + "settings": false, + "shipping-label-banner": true, + "subscriptions": true, + "store-alerts": true, + "transient-notices": true, + "wc-pay-promotion": true, + "wc-pay-welcome-page": true + } +} diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json new file mode 100644 index 00000000000..0c4777b486a --- /dev/null +++ b/plugins/woocommerce/composer.json @@ -0,0 +1,143 @@ +{ + "name": "woocommerce/woocommerce", + "description": "An eCommerce toolkit that helps you sell anything. Beautifully.", + "homepage": "https://woocommerce.com/", + "type": "wordpress-plugin", + "license": "GPL-3.0-or-later", + "prefer-stable": true, + "minimum-stability": "dev", + "repositories": [ + { + "type": "path", + "url": "lib" + } + ], + "require": { + "php": ">=7.2", + "automattic/jetpack-autoloader": "2.10.1", + "automattic/jetpack-constants": "1.5.1", + "composer/installers": "^1.9", + "maxmind-db/reader": "^1.11", + "pelago/emogrifier": "^6.0", + "psr/container": "1.0.0", + "woocommerce/action-scheduler": "3.4.0", + "woocommerce/woocommerce-blocks": "7.4.1" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4", + "yoast/phpunit-polyfills": "^1.0", + "phpunit/phpunit": "7.5.20", + "automattic/jetpack-changelogger": "3.0.2" + }, + "config": { + "optimize-autoloader": true, + "preferred-install": { + "woocommerce/action-scheduler": "dist", + "woocommerce/woocommerce-rest-api": "dist", + "woocommerce/woocommerce-blocks": "dist" + }, + "sort-packages": true, + "platform": { + "php": "7.2" + }, + "allow-plugins": { + "automattic/jetpack-autoloader": true, + "composer/installers": true, + "bamarni/composer-bin-plugin": true + } + }, + "autoload": { + "exclude-from-classmap": [ + "includes/legacy", + "includes/libraries" + ], + "classmap": [ + "includes/rest-api" + ], + "psr-4": { + "Automattic\\WooCommerce\\": "src/", + "Automattic\\WooCommerce\\Vendor\\": "lib/packages/" + }, + "psr-0": { + "Automattic\\WooCommerce\\Vendor\\": "lib/packages/" + } + }, + "autoload-dev": { + "psr-4": { + "Automattic\\WooCommerce\\Tests\\": "tests/php/src/", + "Automattic\\WooCommerce\\Testing\\Tools\\": "tests/Tools/" + }, + "classmap": [ + "tests/legacy/unit-tests/rest-api/Helpers" + ] + }, + "scripts": { + "post-install-cmd": [ + "@composer bin all install --ansi", + "sh ./bin/package-update.sh" + ], + "post-update-cmd": [ + "@composer bin all update --ansi", + "sh ./bin/package-update.sh" + ], + "test": [ + "phpunit" + ], + "phpcs": [ + "phpcs -s -p" + ], + "phpcs-pre-commit": [ + "phpcs -s -p -n" + ], + "phpcbf": [ + "phpcbf -p" + ], + "makepot-audit": [ + "wp --allow-root i18n make-pot . --exclude=\".github,.wordpress-org,bin,sample-data,node_modules,tests\" --slug=woocommerce" + ], + "makepot": [ + "@makepot-audit --skip-audit" + ], + "bin": [ + "echo 'bin not installed'" + ], + "build-lib": [ + "sh ./bin/build-lib.sh" + ] + }, + "extra": { + "installer-paths": { + "packages/{$name}": [ + "woocommerce/action-scheduler", + "woocommerce/woocommerce-blocks", + "woocommerce/woocommerce-admin" + ] + }, + "scripts-description": { + "test": "Run unit tests", + "phpcs": "Analyze code against the WordPress coding standards with PHP_CodeSniffer", + "phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier", + "makepot-audit": "Generate i18n/languages/woocommerce.pot file and run audit", + "makepot": "Generate i18n/languages/woocommerce.pot file" + }, + "bamarni-bin": { + "target-directory": "bin/composer" + }, + "changelogger": { + "formatter": { + "filename": "../../tools/changelogger/PluginFormatter.php" + }, + "types": { + "fix": "Fixes an existing bug", + "add": "Adds functionality", + "update": "Update existing functionality", + "dev": "Development related task", + "tweak": "A minor adjustment to the codebase", + "performance": "Address performance issues", + "enhancement": "Improve existing functionality" + }, + "versioning": "wordpress", + "changelog": "NEXT_CHANGELOG.md" + } + } +} diff --git a/plugins/woocommerce/composer.lock b/plugins/woocommerce/composer.lock new file mode 100644 index 00000000000..0ff06545e2f --- /dev/null +++ b/plugins/woocommerce/composer.lock @@ -0,0 +1,3025 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "03a229b123645dbf035c87685be1043a", + "packages": [ + { + "name": "automattic/jetpack-autoloader", + "version": "2.10.1", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-autoloader.git", + "reference": "20393c4677765c3e737dcb5aee7a3f7b90dce4b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/20393c4677765c3e737dcb5aee7a3f7b90dce4b3", + "reference": "20393c4677765c3e737dcb5aee7a3f7b90dce4b3", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1 || ^2.0" + }, + "require-dev": { + "automattic/jetpack-changelogger": "^1.1", + "yoast/phpunit-polyfills": "0.2.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Automattic\\Jetpack\\Autoloader\\CustomAutoloaderPlugin", + "mirror-repo": "Automattic/jetpack-autoloader", + "changelogger": { + "link-template": "https://github.com/Automattic/jetpack-autoloader/compare/v${old}...v${new}" + }, + "branch-alias": { + "dev-master": "2.10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Automattic\\Jetpack\\Autoloader\\": "src" + }, + "classmap": [ + "src/AutoloadGenerator.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Creates a custom autoloader for a plugin or theme.", + "support": { + "source": "https://github.com/Automattic/jetpack-autoloader/tree/2.10.1" + }, + "time": "2021-03-30T15:15:59+00:00" + }, + { + "name": "automattic/jetpack-constants", + "version": "v1.5.1", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-constants.git", + "reference": "18f772daddc8be5df76c9f4a92e017a3c2569a5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/18f772daddc8be5df76c9f4a92e017a3c2569a5b", + "reference": "18f772daddc8be5df76c9f4a92e017a3c2569a5b", + "shasum": "" + }, + "require-dev": { + "php-mock/php-mock": "^2.1", + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "A wrapper for defining constants in a more testable way.", + "support": { + "source": "https://github.com/Automattic/jetpack-constants/tree/v1.5.1" + }, + "time": "2020-10-28T19:00:31+00:00" + }, + { + "name": "composer/installers", + "version": "v1.12.0", + "source": { + "type": "git", + "url": "https://github.com/composer/installers.git", + "reference": "d20a64ed3c94748397ff5973488761b22f6d3f19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/installers/zipball/d20a64ed3c94748397ff5973488761b22f6d3f19", + "reference": "d20a64ed3c94748397ff5973488761b22f6d3f19", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0" + }, + "replace": { + "roundcube/plugin-installer": "*", + "shama/baton": "*" + }, + "require-dev": { + "composer/composer": "1.6.* || ^2.0", + "composer/semver": "^1 || ^3", + "phpstan/phpstan": "^0.12.55", + "phpstan/phpstan-phpunit": "^0.12.16", + "symfony/phpunit-bridge": "^4.2 || ^5", + "symfony/process": "^2.3" + }, + "type": "composer-plugin", + "extra": { + "class": "Composer\\Installers\\Plugin", + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Installers\\": "src/Composer/Installers" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kyle Robinson Young", + "email": "kyle@dontkry.com", + "homepage": "https://github.com/shama" + } + ], + "description": "A multi-framework Composer library installer", + "homepage": "https://composer.github.io/installers/", + "keywords": [ + "Craft", + "Dolibarr", + "Eliasis", + "Hurad", + "ImageCMS", + "Kanboard", + "Lan Management System", + "MODX Evo", + "MantisBT", + "Mautic", + "Maya", + "OXID", + "Plentymarkets", + "Porto", + "RadPHP", + "SMF", + "Starbug", + "Thelia", + "Whmcs", + "WolfCMS", + "agl", + "aimeos", + "annotatecms", + "attogram", + "bitrix", + "cakephp", + "chef", + "cockpit", + "codeigniter", + "concrete5", + "croogo", + "dokuwiki", + "drupal", + "eZ Platform", + "elgg", + "expressionengine", + "fuelphp", + "grav", + "installer", + "itop", + "joomla", + "known", + "kohana", + "laravel", + "lavalite", + "lithium", + "magento", + "majima", + "mako", + "mediawiki", + "miaoxing", + "modulework", + "modx", + "moodle", + "osclass", + "pantheon", + "phpbb", + "piwik", + "ppi", + "processwire", + "puppet", + "pxcms", + "reindex", + "roundcube", + "shopware", + "silverstripe", + "sydes", + "sylius", + "symfony", + "tastyigniter", + "typo3", + "wordpress", + "yawik", + "zend", + "zikula" + ], + "support": { + "issues": "https://github.com/composer/installers/issues", + "source": "https://github.com/composer/installers/tree/v1.12.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2021-09-13T08:19:44+00:00" + }, + { + "name": "maxmind-db/reader", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git", + "reference": "b1f3c0699525336d09cc5161a2861268d9f2ae5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/b1f3c0699525336d09cc5161a2861268d9f2ae5b", + "reference": "b1f3c0699525336d09cc5161a2861268d9f2ae5b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "conflict": { + "ext-maxminddb": "<1.10.1,>=2.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.*", + "php-coveralls/php-coveralls": "^2.1", + "phpstan/phpstan": "*", + "phpunit/phpcov": ">=6.0.0", + "phpunit/phpunit": ">=8.0.0,<10.0.0", + "squizlabs/php_codesniffer": "3.*" + }, + "suggest": { + "ext-bcmath": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder", + "ext-gmp": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder", + "ext-maxminddb": "A C-based database decoder that provides significantly faster lookups" + }, + "type": "library", + "autoload": { + "psr-4": { + "MaxMind\\Db\\": "src/MaxMind/Db" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Gregory J. Oschwald", + "email": "goschwald@maxmind.com", + "homepage": "https://www.maxmind.com/" + } + ], + "description": "MaxMind DB Reader API", + "homepage": "https://github.com/maxmind/MaxMind-DB-Reader-php", + "keywords": [ + "database", + "geoip", + "geoip2", + "geolocation", + "maxmind" + ], + "support": { + "issues": "https://github.com/maxmind/MaxMind-DB-Reader-php/issues", + "source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.11.0" + }, + "time": "2021-10-18T15:23:10+00:00" + }, + { + "name": "pelago/emogrifier", + "version": "v6.0.0", + "source": { + "type": "git", + "url": "https://github.com/MyIntervals/emogrifier.git", + "reference": "aa72d5407efac118f3896bcb995a2cba793df0ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/aa72d5407efac118f3896bcb995a2cba793df0ae", + "reference": "aa72d5407efac118f3896bcb995a2cba793df0ae", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0", + "sabberworm/php-css-parser": "^8.3.1", + "symfony/css-selector": "^3.4.32 || ^4.4 || ^5.3 || ^6.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.3.0", + "phpunit/phpunit": "^8.5.16", + "rawr/cross-data-providers": "^2.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Pelago\\Emogrifier\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Klee", + "email": "github@oliverklee.de" + }, + { + "name": "Zoli Szabó", + "email": "zoli.szabo+github@gmail.com" + }, + { + "name": "John Reeve", + "email": "jreeve@pelagodesign.com" + }, + { + "name": "Jake Hotson", + "email": "jake@qzdesign.co.uk" + }, + { + "name": "Cameron Brooks" + }, + { + "name": "Jaime Prado" + } + ], + "description": "Converts CSS styles into inline style attributes in your HTML code", + "homepage": "https://www.myintervals.com/emogrifier.php", + "keywords": [ + "css", + "email", + "pre-processing" + ], + "support": { + "issues": "https://github.com/MyIntervals/emogrifier/issues", + "source": "https://github.com/MyIntervals/emogrifier" + }, + "time": "2021-09-16T16:22:04+00:00" + }, + { + "name": "psr/container", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/master" + }, + "time": "2017-02-14T16:28:37+00:00" + }, + { + "name": "sabberworm/php-css-parser", + "version": "8.4.0", + "source": { + "type": "git", + "url": "https://github.com/sabberworm/PHP-CSS-Parser.git", + "reference": "e41d2140031d533348b2192a83f02d8dd8a71d30" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabberworm/PHP-CSS-Parser/zipball/e41d2140031d533348b2192a83f02d8dd8a71d30", + "reference": "e41d2140031d533348b2192a83f02d8dd8a71d30", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=5.6.20" + }, + "require-dev": { + "codacy/coverage": "^1.4", + "phpunit/phpunit": "^4.8.36" + }, + "suggest": { + "ext-mbstring": "for parsing UTF-8 CSS" + }, + "type": "library", + "autoload": { + "psr-4": { + "Sabberworm\\CSS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Raphael Schweikert" + } + ], + "description": "Parser for CSS Files written in PHP", + "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", + "keywords": [ + "css", + "parser", + "stylesheet" + ], + "support": { + "issues": "https://github.com/sabberworm/PHP-CSS-Parser/issues", + "source": "https://github.com/sabberworm/PHP-CSS-Parser/tree/8.4.0" + }, + "time": "2021-12-11T13:40:54+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v4.4.37", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "0628e6c6d7c92f1a7bae543959bdc17347be2436" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/0628e6c6d7c92f1a7bae543959bdc17347be2436", + "reference": "0628e6c6d7c92f1a7bae543959bdc17347be2436", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v4.4.37" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:41:36+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4407588e0d3f1f52efb65fbe92babe41f37fe50c", + "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-03-04T08:16:47+00:00" + }, + { + "name": "woocommerce/action-scheduler", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/woocommerce/action-scheduler.git", + "reference": "3218a33ff14b968f8cb05de9656c2efa1eeb1330" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/woocommerce/action-scheduler/zipball/3218a33ff14b968f8cb05de9656c2efa1eeb1330", + "reference": "3218a33ff14b968f8cb05de9656c2efa1eeb1330", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "^7.5", + "woocommerce/woocommerce-sniffs": "0.1.0", + "wp-cli/wp-cli": "~2.5.0", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "wordpress-plugin", + "extra": { + "scripts-description": { + "test": "Run unit tests", + "phpcs": "Analyze code against the WordPress coding standards with PHP_CodeSniffer", + "phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-or-later" + ], + "description": "Action Scheduler for WordPress and WooCommerce", + "homepage": "https://actionscheduler.org/", + "support": { + "issues": "https://github.com/woocommerce/action-scheduler/issues", + "source": "https://github.com/woocommerce/action-scheduler/tree/3.4.0" + }, + "time": "2021-10-28T17:09:12+00:00" + }, + { + "name": "woocommerce/woocommerce-blocks", + "version": "v7.4.1", + "source": { + "type": "git", + "url": "https://github.com/woocommerce/woocommerce-gutenberg-products-block.git", + "reference": "bde2a5771ddc7970c2114da621c28b0f7b6296ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/woocommerce/woocommerce-gutenberg-products-block/zipball/bde2a5771ddc7970c2114da621c28b0f7b6296ca", + "reference": "bde2a5771ddc7970c2114da621c28b0f7b6296ca", + "shasum": "" + }, + "require": { + "automattic/jetpack-autoloader": "^2.9.1", + "composer/installers": "^1.7.0" + }, + "require-dev": { + "johnbillion/wp-hooks-generator": "^0.7.0", + "mockery/mockery": "^1.4", + "woocommerce/woocommerce-sniffs": "0.1.0", + "wp-phpunit/wp-phpunit": "^5.4", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "wordpress-plugin", + "extra": { + "scripts-description": { + "phpcs": "Analyze code against the WordPress coding standards with PHP_CodeSniffer", + "phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier" + } + }, + "autoload": { + "files": [ + "src/StoreApi/deprecated.php", + "src/StoreApi/functions.php" + ], + "psr-4": { + "Automattic\\WooCommerce\\Blocks\\": "src/", + "Automattic\\WooCommerce\\StoreApi\\": "src/StoreApi/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-or-later" + ], + "description": "WooCommerce blocks for the Gutenberg editor.", + "homepage": "https://woocommerce.com/", + "keywords": [ + "blocks", + "gutenberg", + "woocommerce" + ], + "support": { + "issues": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues", + "source": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/tree/v7.4.1" + }, + "time": "2022-04-14T16:44:52+00:00" + } + ], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", + "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", + "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "symfony/console": "^3.4 | ^5.2", + "symfony/process": "^3.4 | ^5.2", + "wikimedia/at-ease": "^1.2 | ^2.0" + }, + "require-dev": { + "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", + "yoast/phpunit-polyfills": "1.0.2" + }, + "bin": [ + "bin/changelogger" + ], + "type": "project", + "extra": { + "autotagger": true, + "branch-alias": { + "dev-master": "3.0.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { + "::VERSION": "src/Application.php" + }, + "changelogger": { + "link-template": "https://github.com/Automattic/jetpack-changelogger/compare/${old}...${new}" + } + }, + "autoload": { + "psr-4": { + "Automattic\\Jetpack\\Changelog\\": "lib", + "Automattic\\Jetpack\\Changelogger\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { + "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.0.2" + }, + "time": "2021-11-02T14:06:49+00:00" + }, + { + "name": "bamarni/composer-bin-plugin", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/bamarni/composer-bin-plugin.git", + "reference": "49934ffea764864788334c1485fbb08a4b852031" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bamarni/composer-bin-plugin/zipball/49934ffea764864788334c1485fbb08a4b852031", + "reference": "49934ffea764864788334c1485fbb08a4b852031", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": "^5.5.9 || ^7.0 || ^8.0" + }, + "require-dev": { + "composer/composer": "^1.0 || ^2.0", + "symfony/console": "^2.5 || ^3.0 || ^4.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Bamarni\\Composer\\Bin\\Plugin" + }, + "autoload": { + "psr-4": { + "Bamarni\\Composer\\Bin\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "No conflicts for your bin dependencies", + "keywords": [ + "composer", + "conflict", + "dependency", + "executable", + "isolation", + "tool" + ], + "support": { + "issues": "https://github.com/bamarni/composer-bin-plugin/issues", + "source": "https://github.com/bamarni/composer-bin-plugin/tree/v1.5.0" + }, + "time": "2022-02-22T21:01:25+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", + "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.22" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.4.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-03-03T08:28:38+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2022-03-03T13:19:32+00:00" + }, + { + "name": "phar-io/manifest", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^2.0", + "php": "^5.6 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/master" + }, + "time": "2018-07-08T19:23:20+00:00" + }, + { + "name": "phar-io/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/master" + }, + "time": "2018-07-08T19:19:57+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.2", + "psalm/phar": "^4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" + }, + "time": "2021-10-19T17:43:47+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.6.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "77a32518733312af16a44300404e945338981de3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3", + "reference": "77a32518733312af16a44300404e945338981de3", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "psalm/phar": "^4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1" + }, + "time": "2022-03-15T21:29:03+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "v1.15.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13", + "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2", + "php": "^7.2 || ~8.0, <8.2", + "phpdocumentor/reflection-docblock": "^5.2", + "sebastian/comparator": "^3.0 || ^4.0", + "sebastian/recursion-context": "^3.0 || ^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^6.0 || ^7.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/v1.15.0" + }, + "time": "2021-12-08T12:19:24+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "6.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", + "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^7.1", + "phpunit/php-file-iterator": "^2.0", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^3.0", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^3.1 || ^4.0", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "suggest": { + "ext-xdebug": "^2.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/master" + }, + "time": "2018-10-31T16:06:48+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "42c5ba5220e6904cbfe8b1a1bda7c0cfdc8c12f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/42c5ba5220e6904cbfe8b1a1bda7c0cfdc8c12f5", + "reference": "42c5ba5220e6904cbfe8b1a1bda7c0cfdc8c12f5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:42:26+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" + }, + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "2.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/2454ae1765516d20c4ffe103d85a58a9a3bd5662", + "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T08:20:02+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "3.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "9c1da83261628cb24b6a6df371b6e312b3954768" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9c1da83261628cb24b6a6df371b6e312b3954768", + "reference": "9c1da83261628cb24b6a6df371b6e312b3954768", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", + "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "abandoned": true, + "time": "2021-07-26T12:15:06+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "7.5.20", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "9467db479d1b0487c99733bb1e7944d32deded2c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9467db479d1b0487c99733bb1e7944d32deded2c", + "reference": "9467db479d1b0487c99733bb1e7944d32deded2c", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.1", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "^1.7", + "phar-io/manifest": "^1.0.2", + "phar-io/version": "^2.0", + "php": "^7.1", + "phpspec/prophecy": "^1.7", + "phpunit/php-code-coverage": "^6.0.7", + "phpunit/php-file-iterator": "^2.0.1", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^2.1", + "sebastian/comparator": "^3.0", + "sebastian/diff": "^3.0", + "sebastian/environment": "^4.0", + "sebastian/exporter": "^3.1", + "sebastian/global-state": "^2.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^2.0", + "sebastian/version": "^2.0.1" + }, + "conflict": { + "phpunit/phpunit-mock-objects": "*" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*", + "phpunit/php-invoker": "^2.0" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.5-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/7.5.20" + }, + "time": "2020-01-08T08:45:45+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619", + "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T08:15:22+00:00" + }, + { + "name": "sebastian/comparator", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "1071dfcef776a57013124ff35e1fc41ccd294758" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1071dfcef776a57013124ff35e1fc41ccd294758", + "reference": "1071dfcef776a57013124ff35e1fc41ccd294758", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "sebastian/diff": "^3.0", + "sebastian/exporter": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T08:04:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211", + "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8.0", + "symfony/process": "^2 || ^3.3 || ^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:59:04+00:00" + }, + { + "name": "sebastian/environment", + "version": "4.2.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d47bbbad83711771f167c72d4e3f25f7fcc1f8b0", + "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/4.2.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:53:42+00:00" + }, + { + "name": "sebastian/exporter", + "version": "3.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "0c32ea2e40dbf59de29f3b49bf375176ce7dd8db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0c32ea2e40dbf59de29f3b49bf375176ce7dd8db", + "reference": "0c32ea2e40dbf59de29f3b49bf375176ce7dd8db", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-11-11T13:51:24+00:00" + }, + { + "name": "sebastian/global-state", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/2.0.0" + }, + "time": "2017-04-27T15:39:26+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", + "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:40:27+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", + "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:37:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb", + "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:34:24+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/31d35ca87926450c44eae7e2611d45a7a65ea8b3", + "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:30:19+00:00" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/master" + }, + "time": "2016-10-03T07:35:21+00:00" + }, + { + "name": "symfony/console", + "version": "v3.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "a10b1da6fc93080c180bba7219b5ff5b7518fe81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/a10b1da6fc93080c180bba7219b5ff5b7518fe81", + "reference": "a10b1da6fc93080c180bba7219b5ff5b7518fe81", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/debug": "~2.8|~3.0|~4.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/process": "<3.3" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.3|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "~2.8|~3.0|~4.0", + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.3|~4.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/console/tree/v3.4.47" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T10:57:07+00:00" + }, + { + "name": "symfony/debug", + "version": "v4.4.37", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "5de6c6e7f52b364840e53851c126be4d71e60470" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/5de6c6e7f52b364840e53851c126be4d71e60470", + "reference": "5de6c6e7f52b364840e53851c126be4d71e60470", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "symfony/http-kernel": "<3.4" + }, + "require-dev": { + "symfony/http-kernel": "^3.4|^4.0|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/debug/tree/v4.4.37" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:41:36+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "30885182c981ab175d4d034db0f6f469898070ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", + "reference": "30885182c981ab175d4d034db0f6f469898070ab", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-10-20T20:35:02+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-30T18:21:41+00:00" + }, + { + "name": "symfony/process", + "version": "v3.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/b8648cf1d5af12a44a51d07ef9bf980921f15fca", + "reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v3.4.47" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T10:57:07+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2021-07-28T10:34:58+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.10.0" + }, + "time": "2021-03-09T10:59:23+00:00" + }, + { + "name": "wikimedia/at-ease", + "version": "v2.0.0", + "source": { + "type": "git", + "url": "https://github.com/wikimedia/at-ease.git", + "reference": "013ac61929797839c80a111a3f1a4710d8248e7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wikimedia/at-ease/zipball/013ac61929797839c80a111a3f1a4710d8248e7a", + "reference": "013ac61929797839c80a111a3f1a4710d8248e7a", + "shasum": "" + }, + "require": { + "php": ">=5.6.99" + }, + "require-dev": { + "jakub-onderka/php-console-highlighter": "0.3.2", + "jakub-onderka/php-parallel-lint": "1.0.0", + "mediawiki/mediawiki-codesniffer": "22.0.0", + "mediawiki/minus-x": "0.3.1", + "ockcyp/covers-validator": "0.5.1 || 0.6.1", + "phpunit/phpunit": "4.8.36 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/Wikimedia/Functions.php" + ], + "psr-4": { + "Wikimedia\\AtEase\\": "src/Wikimedia/AtEase/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Tim Starling", + "email": "tstarling@wikimedia.org" + }, + { + "name": "MediaWiki developers", + "email": "wikitech-l@lists.wikimedia.org" + } + ], + "description": "Safe replacement to @ for suppressing warnings.", + "homepage": "https://www.mediawiki.org/wiki/at-ease", + "support": { + "source": "https://github.com/wikimedia/at-ease/tree/master" + }, + "time": "2018-10-10T15:39:06+00:00" + }, + { + "name": "yoast/phpunit-polyfills", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", + "reference": "5ea3536428944955f969bc764bbe09738e151ada" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/5ea3536428944955f969bc764bbe09738e151ada", + "reference": "5ea3536428944955f969bc764bbe09738e151ada", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + }, + "require-dev": { + "yoast/yoastcs": "^2.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "autoload": { + "files": [ + "phpunitpolyfills-autoload.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Team Yoast", + "email": "support@yoast.com", + "homepage": "https://yoast.com" + }, + { + "name": "Contributors", + "homepage": "https://github.com/Yoast/PHPUnit-Polyfills/graphs/contributors" + } + ], + "description": "Set of polyfills for changed PHPUnit functionality to allow for creating PHPUnit cross-version compatible tests", + "homepage": "https://github.com/Yoast/PHPUnit-Polyfills", + "keywords": [ + "phpunit", + "polyfill", + "testing" + ], + "support": { + "issues": "https://github.com/Yoast/PHPUnit-Polyfills/issues", + "source": "https://github.com/Yoast/PHPUnit-Polyfills" + }, + "time": "2021-11-23T01:37:03+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=7.2" + }, + "platform-dev": [], + "platform-overrides": { + "php": "7.2" + }, + "plugin-api-version": "2.1.0" +} diff --git a/i18n/continents.php b/plugins/woocommerce/i18n/continents.php similarity index 100% rename from i18n/continents.php rename to plugins/woocommerce/i18n/continents.php diff --git a/plugins/woocommerce/i18n/countries.php b/plugins/woocommerce/i18n/countries.php new file mode 100644 index 00000000000..f38ed899f4d --- /dev/null +++ b/plugins/woocommerce/i18n/countries.php @@ -0,0 +1,266 @@ + __( 'Afghanistan', 'woocommerce' ), + 'AX' => __( 'Åland Islands', 'woocommerce' ), + 'AL' => __( 'Albania', 'woocommerce' ), + 'DZ' => __( 'Algeria', 'woocommerce' ), + 'AS' => __( 'American Samoa', 'woocommerce' ), + 'AD' => __( 'Andorra', 'woocommerce' ), + 'AO' => __( 'Angola', 'woocommerce' ), + 'AI' => __( 'Anguilla', 'woocommerce' ), + 'AQ' => __( 'Antarctica', 'woocommerce' ), + 'AG' => __( 'Antigua and Barbuda', 'woocommerce' ), + 'AR' => __( 'Argentina', 'woocommerce' ), + 'AM' => __( 'Armenia', 'woocommerce' ), + 'AW' => __( 'Aruba', 'woocommerce' ), + 'AU' => __( 'Australia', 'woocommerce' ), + 'AT' => __( 'Austria', 'woocommerce' ), + 'AZ' => __( 'Azerbaijan', 'woocommerce' ), + 'BS' => __( 'Bahamas', 'woocommerce' ), + 'BH' => __( 'Bahrain', 'woocommerce' ), + 'BD' => __( 'Bangladesh', 'woocommerce' ), + 'BB' => __( 'Barbados', 'woocommerce' ), + 'BY' => __( 'Belarus', 'woocommerce' ), + 'BE' => __( 'Belgium', 'woocommerce' ), + 'PW' => __( 'Belau', 'woocommerce' ), + 'BZ' => __( 'Belize', 'woocommerce' ), + 'BJ' => __( 'Benin', 'woocommerce' ), + 'BM' => __( 'Bermuda', 'woocommerce' ), + 'BT' => __( 'Bhutan', 'woocommerce' ), + 'BO' => __( 'Bolivia', 'woocommerce' ), + 'BQ' => __( 'Bonaire, Saint Eustatius and Saba', 'woocommerce' ), + 'BA' => __( 'Bosnia and Herzegovina', 'woocommerce' ), + 'BW' => __( 'Botswana', 'woocommerce' ), + 'BV' => __( 'Bouvet Island', 'woocommerce' ), + 'BR' => __( 'Brazil', 'woocommerce' ), + 'IO' => __( 'British Indian Ocean Territory', 'woocommerce' ), + 'BN' => __( 'Brunei', 'woocommerce' ), + 'BG' => __( 'Bulgaria', 'woocommerce' ), + 'BF' => __( 'Burkina Faso', 'woocommerce' ), + 'BI' => __( 'Burundi', 'woocommerce' ), + 'KH' => __( 'Cambodia', 'woocommerce' ), + 'CM' => __( 'Cameroon', 'woocommerce' ), + 'CA' => __( 'Canada', 'woocommerce' ), + 'CV' => __( 'Cape Verde', 'woocommerce' ), + 'KY' => __( 'Cayman Islands', 'woocommerce' ), + 'CF' => __( 'Central African Republic', 'woocommerce' ), + 'TD' => __( 'Chad', 'woocommerce' ), + 'CL' => __( 'Chile', 'woocommerce' ), + 'CN' => __( 'China', 'woocommerce' ), + 'CX' => __( 'Christmas Island', 'woocommerce' ), + 'CC' => __( 'Cocos (Keeling) Islands', 'woocommerce' ), + 'CO' => __( 'Colombia', 'woocommerce' ), + 'KM' => __( 'Comoros', 'woocommerce' ), + 'CG' => __( 'Congo (Brazzaville)', 'woocommerce' ), + 'CD' => __( 'Congo (Kinshasa)', 'woocommerce' ), + 'CK' => __( 'Cook Islands', 'woocommerce' ), + 'CR' => __( 'Costa Rica', 'woocommerce' ), + 'HR' => __( 'Croatia', 'woocommerce' ), + 'CU' => __( 'Cuba', 'woocommerce' ), + 'CW' => __( 'Curaçao', 'woocommerce' ), + 'CY' => __( 'Cyprus', 'woocommerce' ), + 'CZ' => __( 'Czech Republic', 'woocommerce' ), + 'DK' => __( 'Denmark', 'woocommerce' ), + 'DJ' => __( 'Djibouti', 'woocommerce' ), + 'DM' => __( 'Dominica', 'woocommerce' ), + 'DO' => __( 'Dominican Republic', 'woocommerce' ), + 'EC' => __( 'Ecuador', 'woocommerce' ), + 'EG' => __( 'Egypt', 'woocommerce' ), + 'SV' => __( 'El Salvador', 'woocommerce' ), + 'GQ' => __( 'Equatorial Guinea', 'woocommerce' ), + 'ER' => __( 'Eritrea', 'woocommerce' ), + 'EE' => __( 'Estonia', 'woocommerce' ), + 'ET' => __( 'Ethiopia', 'woocommerce' ), + 'FK' => __( 'Falkland Islands', 'woocommerce' ), + 'FO' => __( 'Faroe Islands', 'woocommerce' ), + 'FJ' => __( 'Fiji', 'woocommerce' ), + 'FI' => __( 'Finland', 'woocommerce' ), + 'FR' => __( 'France', 'woocommerce' ), + 'GF' => __( 'French Guiana', 'woocommerce' ), + 'PF' => __( 'French Polynesia', 'woocommerce' ), + 'TF' => __( 'French Southern Territories', 'woocommerce' ), + 'GA' => __( 'Gabon', 'woocommerce' ), + 'GM' => __( 'Gambia', 'woocommerce' ), + 'GE' => __( 'Georgia', 'woocommerce' ), + 'DE' => __( 'Germany', 'woocommerce' ), + 'GH' => __( 'Ghana', 'woocommerce' ), + 'GI' => __( 'Gibraltar', 'woocommerce' ), + 'GR' => __( 'Greece', 'woocommerce' ), + 'GL' => __( 'Greenland', 'woocommerce' ), + 'GD' => __( 'Grenada', 'woocommerce' ), + 'GP' => __( 'Guadeloupe', 'woocommerce' ), + 'GU' => __( 'Guam', 'woocommerce' ), + 'GT' => __( 'Guatemala', 'woocommerce' ), + 'GG' => __( 'Guernsey', 'woocommerce' ), + 'GN' => __( 'Guinea', 'woocommerce' ), + 'GW' => __( 'Guinea-Bissau', 'woocommerce' ), + 'GY' => __( 'Guyana', 'woocommerce' ), + 'HT' => __( 'Haiti', 'woocommerce' ), + 'HM' => __( 'Heard Island and McDonald Islands', 'woocommerce' ), + 'HN' => __( 'Honduras', 'woocommerce' ), + 'HK' => __( 'Hong Kong', 'woocommerce' ), + 'HU' => __( 'Hungary', 'woocommerce' ), + 'IS' => __( 'Iceland', 'woocommerce' ), + 'IN' => __( 'India', 'woocommerce' ), + 'ID' => __( 'Indonesia', 'woocommerce' ), + 'IR' => __( 'Iran', 'woocommerce' ), + 'IQ' => __( 'Iraq', 'woocommerce' ), + 'IE' => __( 'Ireland', 'woocommerce' ), + 'IM' => __( 'Isle of Man', 'woocommerce' ), + 'IL' => __( 'Israel', 'woocommerce' ), + 'IT' => __( 'Italy', 'woocommerce' ), + 'CI' => __( 'Ivory Coast', 'woocommerce' ), + 'JM' => __( 'Jamaica', 'woocommerce' ), + 'JP' => __( 'Japan', 'woocommerce' ), + 'JE' => __( 'Jersey', 'woocommerce' ), + 'JO' => __( 'Jordan', 'woocommerce' ), + 'KZ' => __( 'Kazakhstan', 'woocommerce' ), + 'KE' => __( 'Kenya', 'woocommerce' ), + 'KI' => __( 'Kiribati', 'woocommerce' ), + 'KW' => __( 'Kuwait', 'woocommerce' ), + 'KG' => __( 'Kyrgyzstan', 'woocommerce' ), + 'LA' => __( 'Laos', 'woocommerce' ), + 'LV' => __( 'Latvia', 'woocommerce' ), + 'LB' => __( 'Lebanon', 'woocommerce' ), + 'LS' => __( 'Lesotho', 'woocommerce' ), + 'LR' => __( 'Liberia', 'woocommerce' ), + 'LY' => __( 'Libya', 'woocommerce' ), + 'LI' => __( 'Liechtenstein', 'woocommerce' ), + 'LT' => __( 'Lithuania', 'woocommerce' ), + 'LU' => __( 'Luxembourg', 'woocommerce' ), + 'MO' => __( 'Macao', 'woocommerce' ), + 'MK' => __( 'North Macedonia', 'woocommerce' ), + 'MG' => __( 'Madagascar', 'woocommerce' ), + 'MW' => __( 'Malawi', 'woocommerce' ), + 'MY' => __( 'Malaysia', 'woocommerce' ), + 'MV' => __( 'Maldives', 'woocommerce' ), + 'ML' => __( 'Mali', 'woocommerce' ), + 'MT' => __( 'Malta', 'woocommerce' ), + 'MH' => __( 'Marshall Islands', 'woocommerce' ), + 'MQ' => __( 'Martinique', 'woocommerce' ), + 'MR' => __( 'Mauritania', 'woocommerce' ), + 'MU' => __( 'Mauritius', 'woocommerce' ), + 'YT' => __( 'Mayotte', 'woocommerce' ), + 'MX' => __( 'Mexico', 'woocommerce' ), + 'FM' => __( 'Micronesia', 'woocommerce' ), + 'MD' => __( 'Moldova', 'woocommerce' ), + 'MC' => __( 'Monaco', 'woocommerce' ), + 'MN' => __( 'Mongolia', 'woocommerce' ), + 'ME' => __( 'Montenegro', 'woocommerce' ), + 'MS' => __( 'Montserrat', 'woocommerce' ), + 'MA' => __( 'Morocco', 'woocommerce' ), + 'MZ' => __( 'Mozambique', 'woocommerce' ), + 'MM' => __( 'Myanmar', 'woocommerce' ), + 'NA' => __( 'Namibia', 'woocommerce' ), + 'NR' => __( 'Nauru', 'woocommerce' ), + 'NP' => __( 'Nepal', 'woocommerce' ), + 'NL' => __( 'Netherlands', 'woocommerce' ), + 'NC' => __( 'New Caledonia', 'woocommerce' ), + 'NZ' => __( 'New Zealand', 'woocommerce' ), + 'NI' => __( 'Nicaragua', 'woocommerce' ), + 'NE' => __( 'Niger', 'woocommerce' ), + 'NG' => __( 'Nigeria', 'woocommerce' ), + 'NU' => __( 'Niue', 'woocommerce' ), + 'NF' => __( 'Norfolk Island', 'woocommerce' ), + 'MP' => __( 'Northern Mariana Islands', 'woocommerce' ), + 'KP' => __( 'North Korea', 'woocommerce' ), + 'NO' => __( 'Norway', 'woocommerce' ), + 'OM' => __( 'Oman', 'woocommerce' ), + 'PK' => __( 'Pakistan', 'woocommerce' ), + 'PS' => __( 'Palestinian Territory', 'woocommerce' ), + 'PA' => __( 'Panama', 'woocommerce' ), + 'PG' => __( 'Papua New Guinea', 'woocommerce' ), + 'PY' => __( 'Paraguay', 'woocommerce' ), + 'PE' => __( 'Peru', 'woocommerce' ), + 'PH' => __( 'Philippines', 'woocommerce' ), + 'PN' => __( 'Pitcairn', 'woocommerce' ), + 'PL' => __( 'Poland', 'woocommerce' ), + 'PT' => __( 'Portugal', 'woocommerce' ), + 'PR' => __( 'Puerto Rico', 'woocommerce' ), + 'QA' => __( 'Qatar', 'woocommerce' ), + 'RE' => __( 'Reunion', 'woocommerce' ), + 'RO' => __( 'Romania', 'woocommerce' ), + 'RU' => __( 'Russia', 'woocommerce' ), + 'RW' => __( 'Rwanda', 'woocommerce' ), + 'BL' => __( 'Saint Barthélemy', 'woocommerce' ), + 'SH' => __( 'Saint Helena', 'woocommerce' ), + 'KN' => __( 'Saint Kitts and Nevis', 'woocommerce' ), + 'LC' => __( 'Saint Lucia', 'woocommerce' ), + 'MF' => __( 'Saint Martin (French part)', 'woocommerce' ), + 'SX' => __( 'Saint Martin (Dutch part)', 'woocommerce' ), + 'PM' => __( 'Saint Pierre and Miquelon', 'woocommerce' ), + 'VC' => __( 'Saint Vincent and the Grenadines', 'woocommerce' ), + 'SM' => __( 'San Marino', 'woocommerce' ), + 'ST' => __( 'São Tomé and Príncipe', 'woocommerce' ), + 'SA' => __( 'Saudi Arabia', 'woocommerce' ), + 'SN' => __( 'Senegal', 'woocommerce' ), + 'RS' => __( 'Serbia', 'woocommerce' ), + 'SC' => __( 'Seychelles', 'woocommerce' ), + 'SL' => __( 'Sierra Leone', 'woocommerce' ), + 'SG' => __( 'Singapore', 'woocommerce' ), + 'SK' => __( 'Slovakia', 'woocommerce' ), + 'SI' => __( 'Slovenia', 'woocommerce' ), + 'SB' => __( 'Solomon Islands', 'woocommerce' ), + 'SO' => __( 'Somalia', 'woocommerce' ), + 'ZA' => __( 'South Africa', 'woocommerce' ), + 'GS' => __( 'South Georgia/Sandwich Islands', 'woocommerce' ), + 'KR' => __( 'South Korea', 'woocommerce' ), + 'SS' => __( 'South Sudan', 'woocommerce' ), + 'ES' => __( 'Spain', 'woocommerce' ), + 'LK' => __( 'Sri Lanka', 'woocommerce' ), + 'SD' => __( 'Sudan', 'woocommerce' ), + 'SR' => __( 'Suriname', 'woocommerce' ), + 'SJ' => __( 'Svalbard and Jan Mayen', 'woocommerce' ), + 'SZ' => __( 'Eswatini', 'woocommerce' ), + 'SE' => __( 'Sweden', 'woocommerce' ), + 'CH' => __( 'Switzerland', 'woocommerce' ), + 'SY' => __( 'Syria', 'woocommerce' ), + 'TW' => __( 'Taiwan', 'woocommerce' ), + 'TJ' => __( 'Tajikistan', 'woocommerce' ), + 'TZ' => __( 'Tanzania', 'woocommerce' ), + 'TH' => __( 'Thailand', 'woocommerce' ), + 'TL' => __( 'Timor-Leste', 'woocommerce' ), + 'TG' => __( 'Togo', 'woocommerce' ), + 'TK' => __( 'Tokelau', 'woocommerce' ), + 'TO' => __( 'Tonga', 'woocommerce' ), + 'TT' => __( 'Trinidad and Tobago', 'woocommerce' ), + 'TN' => __( 'Tunisia', 'woocommerce' ), + 'TR' => __( 'Turkey', 'woocommerce' ), + 'TM' => __( 'Turkmenistan', 'woocommerce' ), + 'TC' => __( 'Turks and Caicos Islands', 'woocommerce' ), + 'TV' => __( 'Tuvalu', 'woocommerce' ), + 'UG' => __( 'Uganda', 'woocommerce' ), + 'UA' => __( 'Ukraine', 'woocommerce' ), + 'AE' => __( 'United Arab Emirates', 'woocommerce' ), + 'GB' => __( 'United Kingdom (UK)', 'woocommerce' ), + 'US' => __( 'United States (US)', 'woocommerce' ), + 'UM' => __( 'United States (US) Minor Outlying Islands', 'woocommerce' ), + 'UY' => __( 'Uruguay', 'woocommerce' ), + 'UZ' => __( 'Uzbekistan', 'woocommerce' ), + 'VU' => __( 'Vanuatu', 'woocommerce' ), + 'VA' => __( 'Vatican', 'woocommerce' ), + 'VE' => __( 'Venezuela', 'woocommerce' ), + 'VN' => __( 'Vietnam', 'woocommerce' ), + 'VG' => __( 'Virgin Islands (British)', 'woocommerce' ), + 'VI' => __( 'Virgin Islands (US)', 'woocommerce' ), + 'WF' => __( 'Wallis and Futuna', 'woocommerce' ), + 'EH' => __( 'Western Sahara', 'woocommerce' ), + 'WS' => __( 'Samoa', 'woocommerce' ), + 'YE' => __( 'Yemen', 'woocommerce' ), + 'ZM' => __( 'Zambia', 'woocommerce' ), + 'ZW' => __( 'Zimbabwe', 'woocommerce' ), +); diff --git a/plugins/woocommerce/i18n/currency-info.php b/plugins/woocommerce/i18n/currency-info.php new file mode 100644 index 00000000000..f0d10e9450a --- /dev/null +++ b/plugins/woocommerce/i18n/currency-info.php @@ -0,0 +1,952 @@ + array( + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'direction' => 'ltr', + 'currency_pos' => 'left_space', + ), + 'ls_comma_dot_rtl' => array( + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'direction' => 'rtl', + 'currency_pos' => 'left_space', + ), + 'ls_comma_space_ltr' => array( + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'direction' => 'ltr', + 'currency_pos' => 'left_space', + ), + 'ls_dot_apos_ltr' => array( + 'thousand_sep' => '\'', + 'decimal_sep' => '.', + 'direction' => 'ltr', + 'currency_pos' => 'left_space', + ), + 'ls_dot_comma_ltr' => array( + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'direction' => 'ltr', + 'currency_pos' => 'left_space', + ), + 'ls_dot_comma_rtl' => array( + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'direction' => 'rtl', + 'currency_pos' => 'left_space', + ), + 'lx_comma_dot_ltr' => array( + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'direction' => 'ltr', + 'currency_pos' => 'left', + ), + 'lx_comma_dot_rtl' => array( + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'direction' => 'rtl', + 'currency_pos' => 'left', + ), + 'lx_comma_space_ltr' => array( + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'direction' => 'ltr', + 'currency_pos' => 'left', + ), + 'lx_dot_comma_ltr' => array( + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'direction' => 'ltr', + 'currency_pos' => 'left', + ), + 'lx_dot_space_ltr' => array( + 'thousand_sep' => ' ', + 'decimal_sep' => '.', + 'direction' => 'ltr', + 'currency_pos' => 'left', + ), + 'rs_comma_dot_ltr' => array( + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'direction' => 'ltr', + 'currency_pos' => 'right_space', + ), + 'rs_comma_dot_rtl' => array( + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'direction' => 'rtl', + 'currency_pos' => 'right_space', + ), + 'rs_comma_space_ltr' => array( + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'direction' => 'ltr', + 'currency_pos' => 'right_space', + ), + 'rs_dot_apos_ltr' => array( + 'thousand_sep' => '\'', + 'decimal_sep' => '.', + 'direction' => 'ltr', + 'currency_pos' => 'right_space', + ), + 'rs_dot_comma_ltr' => array( + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'direction' => 'ltr', + 'currency_pos' => 'right_space', + ), + 'rs_dot_comma_rtl' => array( + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'direction' => 'rtl', + 'currency_pos' => 'right_space', + ), + 'rx_comma_dot_ltr' => array( + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'direction' => 'ltr', + 'currency_pos' => 'right', + ), + 'rx_dot_comma_ltr' => array( + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'direction' => 'ltr', + 'currency_pos' => 'right', + ), +); + +return array( + 'AED' => array( + 'ar_AE' => $global_formats['rs_comma_dot_rtl'], + 'default' => $global_formats['rs_comma_dot_rtl'], + ), + 'AFN' => array( + 'fa_AF' => $global_formats['ls_comma_dot_rtl'], + 'default' => $global_formats['ls_comma_dot_rtl'], + 'ps_AF' => $global_formats['rs_comma_dot_rtl'], + 'uz_AF' => $global_formats['rs_comma_space_ltr'], + ), + 'ALL' => array( + 'default' => $global_formats['rs_comma_space_ltr'], + 'sq_AL' => $global_formats['rs_comma_space_ltr'], + ), + 'AMD' => array( + 'default' => $global_formats['rs_comma_space_ltr'], + 'hy_AM' => $global_formats['rs_comma_space_ltr'], + ), + 'ANG' => array( + 'en_SX' => $global_formats['lx_dot_comma_ltr'], + 'nl_CW' => $global_formats['ls_comma_dot_ltr'], + 'nl_SX' => $global_formats['ls_comma_dot_ltr'], + 'default' => $global_formats['ls_comma_dot_ltr'], + ), + 'AOA' => array( + 'pt_AO' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['rs_comma_space_ltr'], + ), + 'ARS' => array( + 'es_AR' => $global_formats['ls_comma_dot_ltr'], + 'default' => $global_formats['ls_comma_dot_ltr'], + ), + 'AUD' => array( + 'en_AU' => $global_formats['lx_dot_comma_ltr'], + 'en_CC' => $global_formats['lx_dot_comma_ltr'], + 'en_CX' => $global_formats['lx_dot_comma_ltr'], + 'en_KI' => $global_formats['lx_dot_comma_ltr'], + 'en_NF' => $global_formats['lx_dot_comma_ltr'], + 'en_NR' => $global_formats['lx_dot_comma_ltr'], + 'en_TV' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'AWG' => array( + 'nl_AW' => $global_formats['ls_comma_dot_ltr'], + 'default' => $global_formats['ls_comma_dot_ltr'], + ), + 'AZN' => array( + 'default' => $global_formats['rs_comma_dot_ltr'], + 'az_AZ' => $global_formats['rs_comma_dot_ltr'], + ), + 'BAM' => array( + 'hr_BA' => $global_formats['rs_comma_dot_ltr'], + 'sr_Latn_BA' => $global_formats['rs_comma_dot_ltr'], + 'default' => $global_formats['rs_comma_dot_ltr'], + 'bs_BA' => $global_formats['rs_comma_dot_ltr'], + ), + 'BBD' => array( + 'en_BB' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'BDT' => array( + 'default' => $global_formats['rx_dot_comma_ltr'], + 'bn_BD' => $global_formats['rx_dot_comma_ltr'], + ), + 'BGN' => array( + 'default' => $global_formats['rs_comma_space_ltr'], + 'bg_BG' => $global_formats['rs_comma_space_ltr'], + ), + 'BHD' => array( + 'ar_BH' => $global_formats['rs_comma_dot_rtl'], + 'default' => $global_formats['rs_comma_dot_rtl'], + ), + 'BIF' => array( + 'en_BI' => $global_formats['lx_dot_comma_ltr'], + 'fr_BI' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + 'rn_BI' => $global_formats['rx_comma_dot_ltr'], + ), + 'BMD' => array( + 'en_BM' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'BND' => array( + 'ms_BN' => $global_formats['ls_comma_dot_ltr'], + 'default' => $global_formats['ls_comma_dot_ltr'], + ), + 'BOB' => array( + 'es_BO' => $global_formats['lx_comma_dot_ltr'], + 'qu_BO' => $global_formats['ls_comma_dot_ltr'], + 'default' => $global_formats['lx_comma_dot_ltr'], + ), + 'BRL' => array( + 'default' => $global_formats['ls_comma_dot_ltr'], + 'pt_BR' => $global_formats['ls_comma_dot_ltr'], + ), + 'BSD' => array( + 'en_BS' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'BTN' => array( + 'default' => $global_formats['lx_dot_comma_ltr'], + 'dz_BT' => $global_formats['lx_dot_comma_ltr'], + ), + 'BWP' => array( + 'en_BW' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'BYN' => array( + 'ru_BY' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['rs_comma_space_ltr'], + 'be_BY' => $global_formats['rs_comma_space_ltr'], + ), + 'BZD' => array( + 'en_BZ' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'CAD' => array( + 'en_CA' => $global_formats['lx_dot_comma_ltr'], + 'fr_CA' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'CDF' => array( + 'fr_CD' => $global_formats['rs_comma_space_ltr'], + 'sw_CD' => $global_formats['ls_comma_dot_ltr'], + 'default' => $global_formats['rs_comma_space_ltr'], + 'ln_CD' => $global_formats['rs_comma_dot_ltr'], + ), + 'CHF' => array( + 'de_CH' => $global_formats['ls_dot_apos_ltr'], + 'de_LI' => $global_formats['ls_dot_apos_ltr'], + 'fr_CH' => $global_formats['rs_comma_space_ltr'], + 'gsw_LI' => $global_formats['rs_dot_apos_ltr'], + 'it_CH' => $global_formats['ls_dot_apos_ltr'], + 'default' => $global_formats['ls_dot_apos_ltr'], + 'gsw_CH' => $global_formats['rs_dot_apos_ltr'], + 'rm_CH' => $global_formats['rs_dot_apos_ltr'], + ), + 'CLP' => array( + 'es_CL' => $global_formats['lx_comma_dot_ltr'], + 'default' => $global_formats['lx_comma_dot_ltr'], + ), + 'CNY' => array( + 'default' => $global_formats['lx_dot_comma_ltr'], + 'bo_CN' => $global_formats['ls_dot_comma_ltr'], + 'ug_CN' => $global_formats['lx_dot_comma_ltr'], + 'zh_CN' => $global_formats['lx_dot_comma_ltr'], + ), + 'COP' => array( + 'es_CO' => $global_formats['ls_comma_dot_ltr'], + 'default' => $global_formats['ls_comma_dot_ltr'], + ), + 'CRC' => array( + 'es_CR' => $global_formats['lx_comma_space_ltr'], + 'default' => $global_formats['lx_comma_space_ltr'], + ), + 'CUC' => array( + 'es_CU' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'CVE' => array( + 'pt_CV' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['rs_comma_space_ltr'], + ), + 'CZK' => array( + 'default' => $global_formats['rs_comma_space_ltr'], + 'cs_CZ' => $global_formats['rs_comma_space_ltr'], + ), + 'DJF' => array( + 'ar_DJ' => $global_formats['rs_comma_dot_rtl'], + 'fr_DJ' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['rs_comma_dot_rtl'], + ), + 'DKK' => array( + 'default' => $global_formats['rs_comma_dot_ltr'], + 'da_DK' => $global_formats['rs_comma_dot_ltr'], + 'fo_FO' => $global_formats['rs_comma_dot_ltr'], + 'kl_GL' => $global_formats['lx_comma_dot_ltr'], + ), + 'DOP' => array( + 'es_DO' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'DZD' => array( + 'ar_DZ' => $global_formats['ls_comma_dot_rtl'], + 'fr_DZ' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['ls_comma_dot_rtl'], + ), + 'EGP' => array( + 'ar_EG' => $global_formats['rs_comma_dot_rtl'], + 'default' => $global_formats['rs_comma_dot_rtl'], + ), + 'ERN' => array( + 'ar_ER' => $global_formats['rs_comma_dot_rtl'], + 'en_ER' => $global_formats['lx_dot_comma_ltr'], + 'ti_ER' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'ETB' => array( + 'default' => $global_formats['lx_dot_comma_ltr'], + 'am_ET' => $global_formats['lx_dot_comma_ltr'], + ), + 'EUR' => array( + 'ca_AD' => $global_formats['rs_comma_dot_ltr'], + 'de_AT' => $global_formats['ls_comma_space_ltr'], + 'de_BE' => $global_formats['rs_comma_dot_ltr'], + 'de_LU' => $global_formats['rs_comma_dot_ltr'], + 'el_CY' => $global_formats['rs_comma_dot_ltr'], + 'en_IE' => $global_formats['lx_dot_comma_ltr'], + 'en_MT' => $global_formats['lx_dot_comma_ltr'], + 'es_EA' => $global_formats['rs_comma_dot_ltr'], + 'es_IC' => $global_formats['rs_comma_dot_ltr'], + 'fr_BE' => $global_formats['rs_comma_space_ltr'], + 'fr_BL' => $global_formats['rs_comma_space_ltr'], + 'fr_GF' => $global_formats['rs_comma_space_ltr'], + 'fr_GP' => $global_formats['rs_comma_space_ltr'], + 'fr_LU' => $global_formats['rs_comma_dot_ltr'], + 'fr_MC' => $global_formats['rs_comma_space_ltr'], + 'fr_MF' => $global_formats['rs_comma_space_ltr'], + 'fr_MQ' => $global_formats['rs_comma_space_ltr'], + 'fr_PM' => $global_formats['rs_comma_space_ltr'], + 'fr_RE' => $global_formats['rs_comma_space_ltr'], + 'fr_YT' => $global_formats['rs_comma_space_ltr'], + 'it_SM' => $global_formats['rs_comma_dot_ltr'], + 'it_VA' => $global_formats['rs_comma_dot_ltr'], + 'nl_BE' => $global_formats['ls_comma_dot_ltr'], + 'pt_PT' => $global_formats['rs_comma_space_ltr'], + 'sq_XK' => $global_formats['rs_comma_space_ltr'], + 'sr_Latn_ME' => $global_formats['rs_comma_dot_ltr'], + 'sr_Latn_XK' => $global_formats['rs_comma_dot_ltr'], + 'sv_AX' => $global_formats['rs_comma_space_ltr'], + 'sv_FI' => $global_formats['rs_comma_space_ltr'], + 'tr_CY' => $global_formats['lx_comma_dot_ltr'], + 'default' => $global_formats['rs_comma_dot_ltr'], + 'ast_ES' => $global_formats['rs_comma_dot_ltr'], + 'ca_ES' => $global_formats['rs_comma_dot_ltr'], + 'de_DE' => $global_formats['rs_comma_dot_ltr'], + 'el_GR' => $global_formats['rs_comma_dot_ltr'], + 'es_ES' => $global_formats['rs_comma_dot_ltr'], + 'et_EE' => $global_formats['rs_comma_space_ltr'], + 'eu_ES' => $global_formats['rs_comma_dot_ltr'], + 'fi_FI' => $global_formats['rs_comma_space_ltr'], + 'fr_FR' => $global_formats['rs_comma_space_ltr'], + 'fy_NL' => $global_formats['ls_comma_dot_ltr'], + 'ga_IE' => $global_formats['lx_dot_comma_ltr'], + 'gl_ES' => $global_formats['rs_comma_dot_ltr'], + 'it_IT' => $global_formats['rs_comma_dot_ltr'], + 'lb_LU' => $global_formats['rs_comma_dot_ltr'], + 'lt_LT' => $global_formats['rs_comma_space_ltr'], + 'lv_LV' => $global_formats['rs_comma_space_ltr'], + 'mt_MT' => $global_formats['lx_dot_comma_ltr'], + 'nl_NL' => $global_formats['ls_comma_dot_ltr'], + 'sk_SK' => $global_formats['rs_comma_space_ltr'], + 'sl_SI' => $global_formats['rs_comma_dot_ltr'], + ), + 'FJD' => array( + 'en_FJ' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'FKP' => array( + 'en_FK' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'GBP' => array( + 'en_GB' => $global_formats['lx_dot_comma_ltr'], + 'en_GG' => $global_formats['lx_dot_comma_ltr'], + 'en_IM' => $global_formats['lx_dot_comma_ltr'], + 'en_JE' => $global_formats['lx_dot_comma_ltr'], + 'ga_GB' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + 'cy_GB' => $global_formats['lx_dot_comma_ltr'], + 'gd_GB' => $global_formats['lx_dot_comma_ltr'], + 'gv_IM' => $global_formats['lx_dot_comma_ltr'], + ), + 'GEL' => array( + 'default' => $global_formats['rs_comma_space_ltr'], + 'ka_GE' => $global_formats['rs_comma_space_ltr'], + 'os_GE' => $global_formats['ls_comma_space_ltr'], + ), + 'GHS' => array( + 'en_GH' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + 'ak_GH' => $global_formats['lx_dot_comma_ltr'], + 'ee_GH' => $global_formats['lx_dot_comma_ltr'], + ), + 'GIP' => array( + 'en_GI' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'GMD' => array( + 'en_GM' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'GNF' => array( + 'fr_GN' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['rs_comma_space_ltr'], + ), + 'GTQ' => array( + 'es_GT' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'GYD' => array( + 'en_GY' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'HKD' => array( + 'en_HK' => $global_formats['lx_dot_comma_ltr'], + 'zh_Hant_HK' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'HNL' => array( + 'es_HN' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'HRK' => array( + 'default' => $global_formats['rs_comma_dot_ltr'], + 'hr_HR' => $global_formats['rs_comma_dot_ltr'], + ), + 'HUF' => array( + 'default' => $global_formats['rs_comma_space_ltr'], + 'hu_HU' => $global_formats['rs_comma_space_ltr'], + ), + 'IDR' => array( + 'default' => $global_formats['lx_comma_dot_ltr'], + 'id_ID' => $global_formats['lx_comma_dot_ltr'], + ), + 'ILS' => array( + 'ar_IL' => $global_formats['rs_comma_dot_rtl'], + 'ar_PS' => $global_formats['rs_comma_dot_rtl'], + 'default' => $global_formats['rs_comma_dot_rtl'], + 'he_IL' => $global_formats['rs_dot_comma_rtl'], + ), + 'INR' => array( + 'bn_IN' => $global_formats['rx_dot_comma_ltr'], + 'en_IN' => $global_formats['lx_dot_comma_ltr'], + 'ne_IN' => $global_formats['ls_dot_comma_ltr'], + 'ur_IN' => $global_formats['ls_comma_dot_rtl'], + 'default' => $global_formats['lx_dot_comma_ltr'], + 'as_IN' => $global_formats['ls_dot_comma_ltr'], + 'dz_BT' => $global_formats['lx_dot_comma_ltr'], + 'gu_IN' => $global_formats['lx_dot_comma_ltr'], + 'hi_IN' => $global_formats['lx_dot_comma_ltr'], + 'kn_IN' => $global_formats['lx_dot_comma_ltr'], + 'kok_IN' => $global_formats['ls_dot_comma_ltr'], + 'mai_IN' => $global_formats['ls_dot_comma_ltr'], + 'ml_IN' => $global_formats['lx_dot_comma_ltr'], + 'mr_IN' => $global_formats['lx_dot_comma_ltr'], + 'or_IN' => $global_formats['lx_dot_comma_ltr'], + 'sa_IN' => $global_formats['lx_dot_comma_ltr'], + 'sd_PK' => $global_formats['rs_comma_dot_ltr'], + 'ta_IN' => $global_formats['ls_dot_comma_ltr'], + 'te_IN' => $global_formats['lx_dot_comma_ltr'], + ), + 'IQD' => array( + 'ar_IQ' => $global_formats['rs_comma_dot_rtl'], + 'default' => $global_formats['rs_comma_dot_rtl'], + 'ckb_IQ' => $global_formats['rs_comma_dot_rtl'], + ), + 'IRR' => array( + 'default' => $global_formats['lx_comma_dot_rtl'], + 'fa_IR' => $global_formats['lx_comma_dot_rtl'], + ), + 'ISK' => array( + 'default' => $global_formats['rs_comma_dot_ltr'], + 'is_IS' => $global_formats['rs_comma_dot_ltr'], + ), + 'JMD' => array( + 'en_JM' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'JOD' => array( + 'ar_JO' => $global_formats['rs_comma_dot_rtl'], + 'ar_PS' => $global_formats['rs_comma_dot_rtl'], + 'default' => $global_formats['rs_comma_dot_rtl'], + ), + 'JPY' => array( + 'default' => $global_formats['lx_dot_comma_ltr'], + 'ja_JP' => $global_formats['lx_dot_comma_ltr'], + ), + 'KES' => array( + 'en_KE' => $global_formats['lx_dot_comma_ltr'], + 'sw_KE' => $global_formats['ls_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'KGS' => array( + 'ru_KG' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['rs_comma_space_ltr'], + 'ky_KG' => $global_formats['rs_comma_space_ltr'], + ), + 'KHR' => array( + 'default' => $global_formats['rx_comma_dot_ltr'], + 'km_KH' => $global_formats['rx_comma_dot_ltr'], + ), + 'KMF' => array( + 'ar_KM' => $global_formats['rs_comma_dot_rtl'], + 'fr_KM' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['rs_comma_dot_rtl'], + ), + 'KPW' => array( + 'ko_KP' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'KRW' => array( + 'default' => $global_formats['lx_dot_comma_ltr'], + 'ko_KR' => $global_formats['lx_dot_comma_ltr'], + ), + 'KWD' => array( + 'ar_KW' => $global_formats['rs_comma_dot_rtl'], + 'default' => $global_formats['rs_comma_dot_rtl'], + ), + 'KYD' => array( + 'en_KY' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'KZT' => array( + 'ru_KZ' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['rs_comma_space_ltr'], + 'kk_KZ' => $global_formats['rs_comma_space_ltr'], + ), + 'LAK' => array( + 'default' => $global_formats['lx_comma_dot_ltr'], + 'lo_LA' => $global_formats['lx_comma_dot_ltr'], + ), + 'LBP' => array( + 'ar_LB' => $global_formats['rs_comma_dot_rtl'], + 'default' => $global_formats['rs_comma_dot_rtl'], + ), + 'LKR' => array( + 'ta_LK' => $global_formats['ls_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + 'si_LK' => $global_formats['lx_dot_comma_ltr'], + ), + 'LRD' => array( + 'en_LR' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'LSL' => array( + 'en_LS' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'LYD' => array( + 'ar_LY' => $global_formats['ls_comma_dot_rtl'], + 'default' => $global_formats['ls_comma_dot_rtl'], + ), + 'MAD' => array( + 'ar_EH' => $global_formats['ls_dot_comma_rtl'], + 'ar_MA' => $global_formats['ls_comma_dot_rtl'], + 'fr_MA' => $global_formats['rs_comma_dot_ltr'], + 'default' => $global_formats['ls_dot_comma_rtl'], + 'tzm_MA' => $global_formats['rs_comma_space_ltr'], + ), + 'MDL' => array( + 'ro_MD' => $global_formats['rs_comma_dot_ltr'], + 'default' => $global_formats['rs_comma_dot_ltr'], + ), + 'MGA' => array( + 'en_MG' => $global_formats['lx_dot_comma_ltr'], + 'fr_MG' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + 'mg_MG' => $global_formats['ls_dot_comma_ltr'], + ), + 'MKD' => array( + 'sq_MK' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['rs_comma_dot_ltr'], + 'mk_MK' => $global_formats['rs_comma_dot_ltr'], + ), + 'MMK' => array( + 'default' => $global_formats['rs_dot_comma_ltr'], + 'my_MM' => $global_formats['rs_dot_comma_ltr'], + ), + 'MNT' => array( + 'default' => $global_formats['ls_dot_comma_ltr'], + 'mn_MN' => $global_formats['ls_dot_comma_ltr'], + ), + 'MOP' => array( + 'pt_MO' => $global_formats['rs_comma_space_ltr'], + 'zh_Hant_MO' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['rs_comma_space_ltr'], + ), + 'MRU' => array( + 'ar_MR' => $global_formats['rs_comma_dot_rtl'], + 'default' => $global_formats['rs_comma_dot_rtl'], + ), + 'MUR' => array( + 'en_MU' => $global_formats['lx_dot_comma_ltr'], + 'fr_MU' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'MVR' => array( + 'default' => array(), + ), + 'MWK' => array( + 'en_MW' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'MXN' => array( + 'es_MX' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'MYR' => array( + 'default' => $global_formats['lx_dot_comma_ltr'], + 'ms_MY' => $global_formats['lx_dot_comma_ltr'], + ), + 'MZN' => array( + 'pt_MZ' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['rs_comma_space_ltr'], + ), + 'NAD' => array( + 'en_NA' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'NGN' => array( + 'en_NG' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + 'yo_NG' => $global_formats['lx_dot_comma_ltr'], + ), + 'NIO' => array( + 'es_NI' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'NOK' => array( + 'nb_SJ' => $global_formats['ls_comma_space_ltr'], + 'default' => $global_formats['ls_comma_space_ltr'], + 'nb_NO' => $global_formats['ls_comma_space_ltr'], + 'nn_NO' => $global_formats['rs_comma_space_ltr'], + 'se_NO' => $global_formats['rs_comma_space_ltr'], + ), + 'NPR' => array( + 'default' => $global_formats['ls_dot_comma_ltr'], + 'ne_NP' => $global_formats['ls_dot_comma_ltr'], + ), + 'NZD' => array( + 'en_CK' => $global_formats['lx_dot_comma_ltr'], + 'en_NU' => $global_formats['lx_dot_comma_ltr'], + 'en_NZ' => $global_formats['lx_dot_comma_ltr'], + 'en_PN' => $global_formats['lx_dot_comma_ltr'], + 'en_TK' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + 'mi_NZ' => $global_formats['ls_dot_comma_ltr'], + ), + 'OMR' => array( + 'ar_OM' => $global_formats['rs_comma_dot_rtl'], + 'default' => $global_formats['rs_comma_dot_rtl'], + ), + 'PEN' => array( + 'es_PE' => $global_formats['ls_dot_comma_ltr'], + 'default' => $global_formats['ls_dot_comma_ltr'], + 'qu_PE' => $global_formats['ls_dot_comma_ltr'], + ), + 'PGK' => array( + 'en_PG' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'PHP' => array( + 'en_PH' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + 'ceb_PH' => $global_formats['lx_dot_comma_ltr'], + 'fil_PH' => $global_formats['lx_dot_comma_ltr'], + ), + 'PKR' => array( + 'en_PK' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + 'ur_PK' => $global_formats['ls_dot_comma_rtl'], + ), + 'PLN' => array( + 'default' => $global_formats['rs_comma_space_ltr'], + 'pl_PL' => $global_formats['rs_comma_space_ltr'], + ), + 'PYG' => array( + 'es_PY' => $global_formats['ls_comma_dot_ltr'], + 'default' => $global_formats['ls_comma_dot_ltr'], + ), + 'QAR' => array( + 'ar_QA' => $global_formats['rs_comma_dot_rtl'], + 'default' => $global_formats['rs_comma_dot_rtl'], + ), + 'RON' => array( + 'default' => $global_formats['rs_comma_dot_ltr'], + 'ro_RO' => $global_formats['rs_comma_dot_ltr'], + ), + 'RSD' => array( + 'default' => $global_formats['rs_comma_dot_ltr'], + 'sr_RS' => $global_formats['rs_comma_dot_ltr'], + ), + 'RUB' => array( + 'default' => $global_formats['rs_comma_space_ltr'], + 'ce_RU' => $global_formats['rs_dot_comma_ltr'], + 'ru_RU' => $global_formats['rs_comma_space_ltr'], + 'sah_RU' => $global_formats['rs_comma_space_ltr'], + 'tt_RU' => $global_formats['rs_comma_space_ltr'], + ), + 'RWF' => array( + 'en_RW' => $global_formats['lx_dot_comma_ltr'], + 'fr_RW' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + 'rw_RW' => $global_formats['ls_comma_dot_ltr'], + ), + 'SAR' => array( + 'ar_SA' => $global_formats['rs_comma_dot_rtl'], + 'default' => $global_formats['rs_comma_dot_rtl'], + ), + 'SBD' => array( + 'en_SB' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'SCR' => array( + 'en_SC' => $global_formats['lx_dot_comma_ltr'], + 'fr_SC' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'SDG' => array( + 'ar_SD' => $global_formats['rs_comma_dot_rtl'], + 'en_SD' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['rs_comma_dot_rtl'], + ), + 'SEK' => array( + 'default' => $global_formats['rs_comma_space_ltr'], + 'sv_SE' => $global_formats['rs_comma_space_ltr'], + ), + 'SGD' => array( + 'en_SG' => $global_formats['lx_dot_comma_ltr'], + 'ms_SG' => $global_formats['lx_dot_comma_ltr'], + 'ta_SG' => $global_formats['ls_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'SHP' => array( + 'en_SH' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'SLL' => array( + 'en_SL' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'SOS' => array( + 'ar_SO' => $global_formats['rs_comma_dot_rtl'], + 'default' => $global_formats['rs_comma_dot_rtl'], + 'so_SO' => $global_formats['lx_dot_comma_ltr'], + ), + 'SRD' => array( + 'nl_SR' => $global_formats['ls_comma_dot_ltr'], + 'default' => $global_formats['ls_comma_dot_ltr'], + ), + 'SSP' => array( + 'en_SS' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'STN' => array( + 'pt_ST' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['rs_comma_space_ltr'], + ), + 'SYP' => array( + 'ar_SY' => $global_formats['rs_comma_dot_rtl'], + 'fr_SY' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['rs_comma_dot_rtl'], + ), + 'SZL' => array( + 'en_SZ' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'THB' => array( + 'default' => $global_formats['lx_dot_comma_ltr'], + 'th_TH' => $global_formats['lx_dot_comma_ltr'], + ), + 'TJS' => array( + 'default' => $global_formats['rs_comma_space_ltr'], + 'tg_TJ' => $global_formats['rs_comma_space_ltr'], + ), + 'TMT' => array( + 'default' => $global_formats['rs_comma_space_ltr'], + 'tk_TM' => $global_formats['rs_comma_space_ltr'], + ), + 'TND' => array( + 'ar_TN' => $global_formats['ls_comma_dot_rtl'], + 'fr_TN' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['ls_comma_dot_rtl'], + ), + 'TOP' => array( + 'en_TO' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + 'to_TO' => $global_formats['ls_dot_comma_ltr'], + ), + 'TRY' => array( + 'default' => $global_formats['lx_comma_dot_ltr'], + 'tr_TR' => $global_formats['lx_comma_dot_ltr'], + ), + 'TTD' => array( + 'en_TT' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'TWD' => array( + 'zh_Hant' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'TZS' => array( + 'en_TZ' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + 'sw_TZ' => $global_formats['ls_dot_comma_ltr'], + ), + 'UAH' => array( + 'ru_UA' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['rs_comma_space_ltr'], + 'uk_UA' => $global_formats['rs_comma_space_ltr'], + ), + 'UGX' => array( + 'en_UG' => $global_formats['lx_dot_comma_ltr'], + 'sw_UG' => $global_formats['ls_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'USD' => array( + 'en_AS' => $global_formats['lx_dot_comma_ltr'], + 'en_DG' => $global_formats['lx_dot_comma_ltr'], + 'en_FM' => $global_formats['lx_dot_comma_ltr'], + 'en_GU' => $global_formats['lx_dot_comma_ltr'], + 'en_IO' => $global_formats['lx_dot_comma_ltr'], + 'en_MH' => $global_formats['lx_dot_comma_ltr'], + 'en_MP' => $global_formats['lx_dot_comma_ltr'], + 'en_PR' => $global_formats['lx_dot_comma_ltr'], + 'en_PW' => $global_formats['lx_dot_comma_ltr'], + 'en_TC' => $global_formats['lx_dot_comma_ltr'], + 'en_UM' => $global_formats['lx_dot_comma_ltr'], + 'en_VG' => $global_formats['lx_dot_comma_ltr'], + 'en_VI' => $global_formats['lx_dot_comma_ltr'], + 'en_ZW' => $global_formats['lx_dot_comma_ltr'], + 'es_EC' => $global_formats['lx_comma_dot_ltr'], + 'es_PA' => $global_formats['lx_dot_comma_ltr'], + 'es_PR' => $global_formats['lx_dot_comma_ltr'], + 'es_SV' => $global_formats['lx_dot_comma_ltr'], + 'es_US' => $global_formats['lx_dot_comma_ltr'], + 'fr_HT' => $global_formats['rs_comma_space_ltr'], + 'nl_BQ' => $global_formats['ls_comma_dot_ltr'], + 'pt_TL' => $global_formats['rs_comma_space_ltr'], + 'qu_EC' => $global_formats['ls_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + 'en_US' => $global_formats['lx_dot_comma_ltr'], + 'haw_US' => $global_formats['lx_dot_comma_ltr'], + 'nd_ZW' => $global_formats['lx_dot_comma_ltr'], + 'sn_ZW' => $global_formats['ls_dot_comma_ltr'], + ), + 'UYU' => array( + 'es_UY' => $global_formats['ls_comma_dot_ltr'], + 'default' => $global_formats['ls_comma_dot_ltr'], + ), + 'UZS' => array( + 'default' => $global_formats['rs_comma_space_ltr'], + 'uz_AF' => $global_formats['rs_comma_space_ltr'], + ), + 'VES' => array( + 'es_VE' => $global_formats['lx_comma_dot_ltr'], + 'default' => $global_formats['lx_comma_dot_ltr'], + ), + 'VND' => array( + 'default' => $global_formats['rs_comma_dot_ltr'], + 'vi_VN' => $global_formats['rs_comma_dot_ltr'], + ), + 'VUV' => array( + 'en_VU' => $global_formats['lx_dot_comma_ltr'], + 'fr_VU' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'WST' => array( + 'en_WS' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'XAF' => array( + 'ar_TD' => $global_formats['rs_comma_dot_rtl'], + 'en_CM' => $global_formats['lx_dot_comma_ltr'], + 'es_GQ' => $global_formats['lx_comma_dot_ltr'], + 'fr_CF' => $global_formats['rs_comma_space_ltr'], + 'fr_CG' => $global_formats['rs_comma_space_ltr'], + 'fr_CM' => $global_formats['rs_comma_space_ltr'], + 'fr_GA' => $global_formats['rs_comma_space_ltr'], + 'fr_GQ' => $global_formats['rs_comma_space_ltr'], + 'fr_TD' => $global_formats['rs_comma_space_ltr'], + 'pt_GQ' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['rs_comma_space_ltr'], + 'sg_CF' => $global_formats['lx_comma_dot_ltr'], + ), + 'XCD' => array( + 'en_AG' => $global_formats['lx_dot_comma_ltr'], + 'en_AI' => $global_formats['lx_dot_comma_ltr'], + 'en_DM' => $global_formats['lx_dot_comma_ltr'], + 'en_GD' => $global_formats['lx_dot_comma_ltr'], + 'en_KN' => $global_formats['lx_dot_comma_ltr'], + 'en_LC' => $global_formats['lx_dot_comma_ltr'], + 'en_MS' => $global_formats['lx_dot_comma_ltr'], + 'en_VC' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), + 'XOF' => array( + 'fr_BF' => $global_formats['rs_comma_space_ltr'], + 'fr_BJ' => $global_formats['rs_comma_space_ltr'], + 'fr_CI' => $global_formats['rs_comma_space_ltr'], + 'fr_ML' => $global_formats['rs_comma_space_ltr'], + 'fr_NE' => $global_formats['rs_comma_space_ltr'], + 'fr_SN' => $global_formats['rs_comma_space_ltr'], + 'fr_TG' => $global_formats['rs_comma_space_ltr'], + 'pt_GW' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['rs_comma_space_ltr'], + 'dyo_SN' => $global_formats['rs_comma_space_ltr'], + 'wo_SN' => $global_formats['ls_comma_dot_ltr'], + ), + 'XPF' => array( + 'fr_NC' => $global_formats['rs_comma_space_ltr'], + 'fr_PF' => $global_formats['rs_comma_space_ltr'], + 'fr_WF' => $global_formats['rs_comma_space_ltr'], + 'default' => $global_formats['rs_comma_space_ltr'], + ), + 'YER' => array( + 'ar_YE' => $global_formats['rs_comma_dot_rtl'], + 'default' => $global_formats['rs_comma_dot_rtl'], + ), + 'ZAR' => array( + 'en_LS' => $global_formats['lx_dot_comma_ltr'], + 'en_NA' => $global_formats['lx_dot_comma_ltr'], + 'en_ZA' => $global_formats['lx_comma_space_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + 'af_ZA' => $global_formats['lx_comma_space_ltr'], + 'xh_ZA' => $global_formats['lx_dot_space_ltr'], + 'zu_ZA' => $global_formats['lx_dot_comma_ltr'], + ), + 'ZMW' => array( + 'en_ZM' => $global_formats['lx_dot_comma_ltr'], + 'default' => $global_formats['lx_dot_comma_ltr'], + ), +); diff --git a/i18n/languages/README.md b/plugins/woocommerce/i18n/languages/README.md similarity index 100% rename from i18n/languages/README.md rename to plugins/woocommerce/i18n/languages/README.md diff --git a/plugins/woocommerce/i18n/locale-info.php b/plugins/woocommerce/i18n/locale-info.php new file mode 100644 index 00000000000..59d0ea4892d --- /dev/null +++ b/plugins/woocommerce/i18n/locale-info.php @@ -0,0 +1,3982 @@ + array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'ca_AD', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'AE' => array( + 'currency_code' => 'AED', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_AE', + 'name' => 'United Arab Emirates dirham', + 'singular' => 'UAE dirham', + 'plural' => 'UAE dirhams', + 'short_symbol' => null, + 'locales' => $locales['AED'], + ), + 'AF' => array( + 'currency_code' => 'AFN', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'fa_AF', + 'name' => 'Afghan afghani', + 'singular' => 'Afghan Afghani', + 'plural' => 'Afghan Afghanis', + 'short_symbol' => '؋', + 'locales' => $locales['AFN'], + ), + 'AG' => array( + 'currency_code' => 'XCD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_AG', + 'name' => 'East Caribbean dollar', + 'singular' => 'East Caribbean dollar', + 'plural' => 'East Caribbean dollars', + 'short_symbol' => '$', + 'locales' => $locales['XCD'], + ), + 'AI' => array( + 'currency_code' => 'XCD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_AI', + 'name' => 'East Caribbean dollar', + 'singular' => 'East Caribbean dollar', + 'plural' => 'East Caribbean dollars', + 'short_symbol' => '$', + 'locales' => $locales['XCD'], + ), + 'AL' => array( + 'currency_code' => 'ALL', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'sq_AL', + 'name' => 'Albanian lek', + 'singular' => 'Albanian lek', + 'plural' => 'Albanian lekë', + 'short_symbol' => null, + 'locales' => $locales['ALL'], + ), + 'AM' => array( + 'currency_code' => 'AMD', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'hy_AM', + 'name' => 'Armenian dram', + 'singular' => 'Armenian dram', + 'plural' => 'Armenian drams', + 'short_symbol' => '֏', + 'locales' => $locales['AMD'], + ), + 'AO' => array( + 'currency_code' => 'AOA', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'pt_AO', + 'name' => 'Angolan kwanza', + 'singular' => 'Angolan kwanza', + 'plural' => 'Angolan kwanzas', + 'short_symbol' => 'Kz', + 'locales' => $locales['AOA'], + ), + 'AR' => array( + 'currency_code' => 'ARS', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_AR', + 'name' => 'Argentine peso', + 'singular' => 'Argentine peso', + 'plural' => 'Argentine pesos', + 'short_symbol' => '$', + 'locales' => $locales['ARS'], + ), + 'AS' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_AS', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), + 'AT' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'left_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'de_AT', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'AU' => array( + 'currency_code' => 'AUD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_AU', + 'name' => 'Australian dollar', + 'singular' => 'Australian dollar', + 'plural' => 'Australian dollars', + 'short_symbol' => '$', + 'locales' => $locales['AUD'], + ), + 'AW' => array( + 'currency_code' => 'AWG', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'nl_AW', + 'name' => 'Aruban florin', + 'singular' => 'Aruban florin', + 'plural' => 'Aruban florin', + 'short_symbol' => null, + 'locales' => $locales['AWG'], + ), + 'AX' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'sv_AX', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'AZ' => array( + 'currency_code' => 'AZN', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'az_AZ', + 'name' => 'Azerbaijani manat', + 'singular' => 'Azerbaijani manat', + 'plural' => 'Azerbaijani manats', + 'short_symbol' => '₼', + 'locales' => $locales['AZN'], + ), + 'BA' => array( + 'currency_code' => 'BAM', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'bs_BA', + 'name' => 'Bosnia and Herzegovina convertible mark', + 'singular' => 'Bosnia-Herzegovina convertible mark', + 'plural' => 'Bosnia-Herzegovina convertible marks', + 'short_symbol' => 'KM', + 'locales' => $locales['BAM'], + ), + 'BB' => array( + 'currency_code' => 'BBD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_BB', + 'name' => 'Barbadian dollar', + 'singular' => 'Barbadian dollar', + 'plural' => 'Barbadian dollars', + 'short_symbol' => '$', + 'locales' => $locales['BBD'], + ), + 'BD' => array( + 'currency_code' => 'BDT', + 'currency_pos' => 'right', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'bn_BD', + 'name' => 'Bangladeshi taka', + 'singular' => 'Bangladeshi taka', + 'plural' => 'Bangladeshi takas', + 'short_symbol' => '৳', + 'locales' => $locales['BDT'], + ), + 'BE' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'nl_BE', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'BF' => array( + 'currency_code' => 'XOF', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_BF', + 'name' => 'West African CFA franc', + 'singular' => 'West African CFA franc', + 'plural' => 'West African CFA francs', + 'short_symbol' => null, + 'locales' => $locales['XOF'], + ), + 'BG' => array( + 'currency_code' => 'BGN', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'bg_BG', + 'name' => 'Bulgarian lev', + 'singular' => 'Bulgarian lev', + 'plural' => 'Bulgarian leva', + 'short_symbol' => null, + 'locales' => $locales['BGN'], + ), + 'BH' => array( + 'currency_code' => 'BHD', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 3, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_BH', + 'name' => 'Bahraini dinar', + 'singular' => 'Bahraini dinar', + 'plural' => 'Bahraini dinars', + 'short_symbol' => null, + 'locales' => $locales['BHD'], + ), + 'BI' => array( + 'currency_code' => 'BIF', + 'currency_pos' => 'right', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'rn_BI', + 'name' => 'Burundian franc', + 'singular' => 'Burundian franc', + 'plural' => 'Burundian francs', + 'short_symbol' => null, + 'locales' => $locales['BIF'], + ), + 'BJ' => array( + 'currency_code' => 'XOF', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_BJ', + 'name' => 'West African CFA franc', + 'singular' => 'West African CFA franc', + 'plural' => 'West African CFA francs', + 'short_symbol' => null, + 'locales' => $locales['XOF'], + ), + 'BL' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_BL', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'BM' => array( + 'currency_code' => 'BMD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_BM', + 'name' => 'Bermudian dollar', + 'singular' => 'Bermudan dollar', + 'plural' => 'Bermudan dollars', + 'short_symbol' => '$', + 'locales' => $locales['BMD'], + ), + 'BN' => array( + 'currency_code' => 'BND', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'ms_BN', + 'name' => 'Brunei dollar', + 'singular' => 'Brunei dollar', + 'plural' => 'Brunei dollars', + 'short_symbol' => '$', + 'locales' => $locales['BND'], + ), + 'BO' => array( + 'currency_code' => 'BOB', + 'currency_pos' => 'left', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_BO', + 'name' => 'Bolivian boliviano', + 'singular' => 'Bolivian boliviano', + 'plural' => 'Bolivian bolivianos', + 'short_symbol' => 'Bs', + 'locales' => $locales['BOB'], + ), + 'BQ' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'nl_BQ', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), + 'BR' => array( + 'currency_code' => 'BRL', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'pt_BR', + 'name' => 'Brazilian real', + 'singular' => 'Brazilian real', + 'plural' => 'Brazilian reals', + 'short_symbol' => 'R$', + 'locales' => $locales['BRL'], + ), + 'BS' => array( + 'currency_code' => 'BSD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_BS', + 'name' => 'Bahamian dollar', + 'singular' => 'Bahamian dollar', + 'plural' => 'Bahamian dollars', + 'short_symbol' => '$', + 'locales' => $locales['BSD'], + ), + 'BT' => array( + 'currency_code' => 'BTN', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'dz_BT', + 'name' => 'Bhutanese ngultrum', + 'singular' => 'Bhutanese ngultrum', + 'plural' => 'Bhutanese ngultrums', + 'short_symbol' => null, + 'locales' => $locales['BTN'], + ), + 'BW' => array( + 'currency_code' => 'BWP', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_BW', + 'name' => 'Botswana pula', + 'singular' => 'Botswanan pula', + 'plural' => 'Botswanan pulas', + 'short_symbol' => 'P', + 'locales' => $locales['BWP'], + ), + 'BY' => array( + 'currency_code' => 'BYN', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'be_BY', + 'name' => 'Belarusian ruble', + 'singular' => 'Belarusian ruble', + 'plural' => 'Belarusian rubles', + 'short_symbol' => 'р.', + 'locales' => $locales['BYN'], + ), + 'BZ' => array( + 'currency_code' => 'BZD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_BZ', + 'name' => 'Belize dollar', + 'singular' => 'Belize dollar', + 'plural' => 'Belize dollars', + 'short_symbol' => '$', + 'locales' => $locales['BZD'], + ), + 'CA' => array( + 'currency_code' => 'CAD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_CA', + 'name' => 'Canadian dollar', + 'singular' => 'Canadian dollar', + 'plural' => 'Canadian dollars', + 'short_symbol' => '$', + 'locales' => $locales['CAD'], + ), + 'CC' => array( + 'currency_code' => 'AUD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_CC', + 'name' => 'Australian dollar', + 'singular' => 'Australian dollar', + 'plural' => 'Australian dollars', + 'short_symbol' => '$', + 'locales' => $locales['AUD'], + ), + 'CD' => array( + 'currency_code' => 'CDF', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'sw_CD', + 'name' => 'Congolese franc', + 'singular' => 'Congolese franc', + 'plural' => 'Congolese francs', + 'short_symbol' => null, + 'locales' => $locales['CDF'], + ), + 'CF' => array( + 'currency_code' => 'XAF', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_CF', + 'name' => 'Central African CFA franc', + 'singular' => 'Central African CFA franc', + 'plural' => 'Central African CFA francs', + 'short_symbol' => null, + 'locales' => $locales['XAF'], + ), + 'CG' => array( + 'currency_code' => 'XAF', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_CG', + 'name' => 'Central African CFA franc', + 'singular' => 'Central African CFA franc', + 'plural' => 'Central African CFA francs', + 'short_symbol' => null, + 'locales' => $locales['XAF'], + ), + 'CH' => array( + 'currency_code' => 'CHF', + 'currency_pos' => 'left_space', + 'thousand_sep' => '\'', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'de_CH', + 'name' => 'Swiss franc', + 'singular' => 'Swiss franc', + 'plural' => 'Swiss francs', + 'short_symbol' => null, + 'locales' => $locales['CHF'], + ), + 'CI' => array( + 'currency_code' => 'XOF', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_CI', + 'name' => 'West African CFA franc', + 'singular' => 'West African CFA franc', + 'plural' => 'West African CFA francs', + 'short_symbol' => null, + 'locales' => $locales['XOF'], + ), + 'CK' => array( + 'currency_code' => 'NZD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_CK', + 'name' => 'New Zealand dollar', + 'singular' => 'New Zealand dollar', + 'plural' => 'New Zealand dollars', + 'short_symbol' => '$', + 'locales' => $locales['NZD'], + ), + 'CL' => array( + 'currency_code' => 'CLP', + 'currency_pos' => 'left', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_CL', + 'name' => 'Chilean peso', + 'singular' => 'Chilean peso', + 'plural' => 'Chilean pesos', + 'short_symbol' => '$', + 'locales' => $locales['CLP'], + ), + 'CM' => array( + 'currency_code' => 'XAF', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_CM', + 'name' => 'Central African CFA franc', + 'singular' => 'Central African CFA franc', + 'plural' => 'Central African CFA francs', + 'short_symbol' => null, + 'locales' => $locales['XAF'], + ), + 'CN' => array( + 'currency_code' => 'CNY', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'zh_CN', + 'name' => 'Chinese yuan', + 'singular' => 'Chinese yuan', + 'plural' => 'Chinese yuan', + 'short_symbol' => '¥', + 'locales' => $locales['CNY'], + ), + 'CO' => array( + 'currency_code' => 'COP', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_CO', + 'name' => 'Colombian peso', + 'singular' => 'Colombian peso', + 'plural' => 'Colombian pesos', + 'short_symbol' => '$', + 'locales' => $locales['COP'], + ), + 'CR' => array( + 'currency_code' => 'CRC', + 'currency_pos' => 'left', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_CR', + 'name' => 'Costa Rican colón', + 'singular' => 'Costa Rican colón', + 'plural' => 'Costa Rican colóns', + 'short_symbol' => '₡', + 'locales' => $locales['CRC'], + ), + 'CU' => array( + 'currency_code' => 'CUC', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_CU', + 'name' => 'Cuban convertible peso', + 'singular' => 'Cuban convertible peso', + 'plural' => 'Cuban convertible pesos', + 'short_symbol' => '$', + 'locales' => $locales['CUC'], + ), + 'CV' => array( + 'currency_code' => 'CVE', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'pt_CV', + 'name' => 'Cape Verdean escudo', + 'singular' => 'Cape Verdean escudo', + 'plural' => 'Cape Verdean escudos', + 'short_symbol' => null, + 'locales' => $locales['CVE'], + ), + 'CW' => array( + 'currency_code' => 'ANG', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'nl_CW', + 'name' => 'Netherlands Antillean guilder', + 'singular' => 'Netherlands Antillean guilder', + 'plural' => 'Netherlands Antillean guilders', + 'short_symbol' => null, + 'locales' => $locales['ANG'], + ), + 'CX' => array( + 'currency_code' => 'AUD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_CX', + 'name' => 'Australian dollar', + 'singular' => 'Australian dollar', + 'plural' => 'Australian dollars', + 'short_symbol' => '$', + 'locales' => $locales['AUD'], + ), + 'CY' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'el_CY', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'CZ' => array( + 'currency_code' => 'CZK', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'cs_CZ', + 'name' => 'Czech koruna', + 'singular' => 'Czech koruna', + 'plural' => 'Czech korunas', + 'short_symbol' => 'Kč', + 'locales' => $locales['CZK'], + ), + 'DE' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'de_DE', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'DG' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_DG', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), + 'DJ' => array( + 'currency_code' => 'DJF', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_DJ', + 'name' => 'Djiboutian franc', + 'singular' => 'Djiboutian franc', + 'plural' => 'Djiboutian francs', + 'short_symbol' => null, + 'locales' => $locales['DJF'], + ), + 'DK' => array( + 'currency_code' => 'DKK', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'da_DK', + 'name' => 'Danish krone', + 'singular' => 'Danish krone', + 'plural' => 'Danish kroner', + 'short_symbol' => 'kr', + 'locales' => $locales['DKK'], + ), + 'DM' => array( + 'currency_code' => 'XCD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_DM', + 'name' => 'East Caribbean dollar', + 'singular' => 'East Caribbean dollar', + 'plural' => 'East Caribbean dollars', + 'short_symbol' => '$', + 'locales' => $locales['XCD'], + ), + 'DO' => array( + 'currency_code' => 'DOP', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_DO', + 'name' => 'Dominican peso', + 'singular' => 'Dominican peso', + 'plural' => 'Dominican pesos', + 'short_symbol' => '$', + 'locales' => $locales['DOP'], + ), + 'DZ' => array( + 'currency_code' => 'DZD', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_DZ', + 'name' => 'Algerian dinar', + 'singular' => 'Algerian dinar', + 'plural' => 'Algerian dinars', + 'short_symbol' => null, + 'locales' => $locales['DZD'], + ), + 'EA' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_EA', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'EC' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_EC', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), + 'EE' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'et_EE', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'EG' => array( + 'currency_code' => 'EGP', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_EG', + 'name' => 'Egyptian pound', + 'singular' => 'Egyptian pound', + 'plural' => 'Egyptian pounds', + 'short_symbol' => 'E£', + 'locales' => $locales['EGP'], + ), + 'EH' => array( + 'currency_code' => 'MAD', + 'currency_pos' => 'left_space', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_EH', + 'name' => 'Moroccan dirham', + 'singular' => 'Moroccan dirham', + 'plural' => 'Moroccan dirhams', + 'short_symbol' => null, + 'locales' => $locales['MAD'], + ), + 'ER' => array( + 'currency_code' => 'ERN', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'ti_ER', + 'name' => 'Eritrean nakfa', + 'singular' => 'Eritrean nakfa', + 'plural' => 'Eritrean nakfas', + 'short_symbol' => null, + 'locales' => $locales['ERN'], + ), + 'ES' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_ES', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'ET' => array( + 'currency_code' => 'ETB', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'am_ET', + 'name' => 'Ethiopian birr', + 'singular' => 'Ethiopian birr', + 'plural' => 'Ethiopian birrs', + 'short_symbol' => null, + 'locales' => $locales['ETB'], + ), + 'FI' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fi_FI', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'FJ' => array( + 'currency_code' => 'FJD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_FJ', + 'name' => 'Fijian dollar', + 'singular' => 'Fijian dollar', + 'plural' => 'Fijian dollars', + 'short_symbol' => '$', + 'locales' => $locales['FJD'], + ), + 'FK' => array( + 'currency_code' => 'FKP', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_FK', + 'name' => 'Falkland Islands pound', + 'singular' => 'Falkland Islands pound', + 'plural' => 'Falkland Islands pounds', + 'short_symbol' => '£', + 'locales' => $locales['FKP'], + ), + 'FM' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_FM', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), + 'FO' => array( + 'currency_code' => 'DKK', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fo_FO', + 'name' => 'Danish krone', + 'singular' => 'Danish krone', + 'plural' => 'Danish kroner', + 'short_symbol' => 'kr', + 'locales' => $locales['DKK'], + ), + 'FR' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_FR', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'GA' => array( + 'currency_code' => 'XAF', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_GA', + 'name' => 'Central African CFA franc', + 'singular' => 'Central African CFA franc', + 'plural' => 'Central African CFA francs', + 'short_symbol' => null, + 'locales' => $locales['XAF'], + ), + 'GB' => array( + 'currency_code' => 'GBP', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'oz', + 'dimension_unit' => 'foot', + 'direction' => 'ltr', + 'default_locale' => 'en_GB', + 'name' => 'Pound sterling', + 'singular' => 'British pound', + 'plural' => 'British pounds', + 'short_symbol' => '£', + 'locales' => $locales['GBP'], + ), + 'GD' => array( + 'currency_code' => 'XCD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_GD', + 'name' => 'East Caribbean dollar', + 'singular' => 'East Caribbean dollar', + 'plural' => 'East Caribbean dollars', + 'short_symbol' => '$', + 'locales' => $locales['XCD'], + ), + 'GE' => array( + 'currency_code' => 'GEL', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'ka_GE', + 'name' => 'Georgian lari', + 'singular' => 'Georgian lari', + 'plural' => 'Georgian laris', + 'short_symbol' => '₾', + 'locales' => $locales['GEL'], + ), + 'GF' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_GF', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'GG' => array( + 'currency_code' => 'GBP', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_GG', + 'name' => 'Pound sterling', + 'singular' => 'British pound', + 'plural' => 'British pounds', + 'short_symbol' => '£', + 'locales' => $locales['GBP'], + ), + 'GH' => array( + 'currency_code' => 'GHS', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'ak_GH', + 'name' => 'Ghana cedi', + 'singular' => 'Ghanaian cedi', + 'plural' => 'Ghanaian cedis', + 'short_symbol' => 'GH₵', + 'locales' => $locales['GHS'], + ), + 'GI' => array( + 'currency_code' => 'GIP', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_GI', + 'name' => 'Gibraltar pound', + 'singular' => 'Gibraltar pound', + 'plural' => 'Gibraltar pounds', + 'short_symbol' => '£', + 'locales' => $locales['GIP'], + ), + 'GL' => array( + 'currency_code' => 'DKK', + 'currency_pos' => 'left', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'kl_GL', + 'name' => 'Danish krone', + 'singular' => 'Danish krone', + 'plural' => 'Danish kroner', + 'short_symbol' => 'kr', + 'locales' => $locales['DKK'], + ), + 'GM' => array( + 'currency_code' => 'GMD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_GM', + 'name' => 'Gambian dalasi', + 'singular' => 'Gambian dalasi', + 'plural' => 'Gambian dalasis', + 'short_symbol' => null, + 'locales' => $locales['GMD'], + ), + 'GN' => array( + 'currency_code' => 'GNF', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_GN', + 'name' => 'Guinean franc', + 'singular' => 'Guinean franc', + 'plural' => 'Guinean francs', + 'short_symbol' => 'FG', + 'locales' => $locales['GNF'], + ), + 'GP' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_GP', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'GQ' => array( + 'currency_code' => 'XAF', + 'currency_pos' => 'left', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_GQ', + 'name' => 'Central African CFA franc', + 'singular' => 'Central African CFA franc', + 'plural' => 'Central African CFA francs', + 'short_symbol' => null, + 'locales' => $locales['XAF'], + ), + 'GR' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'el_GR', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'GT' => array( + 'currency_code' => 'GTQ', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_GT', + 'name' => 'Guatemalan quetzal', + 'singular' => 'Guatemalan quetzal', + 'plural' => 'Guatemalan quetzals', + 'short_symbol' => 'Q', + 'locales' => $locales['GTQ'], + ), + 'GU' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_GU', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), + 'GW' => array( + 'currency_code' => 'XOF', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'pt_GW', + 'name' => 'West African CFA franc', + 'singular' => 'West African CFA franc', + 'plural' => 'West African CFA francs', + 'short_symbol' => null, + 'locales' => $locales['XOF'], + ), + 'GY' => array( + 'currency_code' => 'GYD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_GY', + 'name' => 'Guyanese dollar', + 'singular' => 'Guyanaese dollar', + 'plural' => 'Guyanaese dollars', + 'short_symbol' => '$', + 'locales' => $locales['GYD'], + ), + 'HK' => array( + 'currency_code' => 'HKD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'zh_Hant_HK', + 'name' => 'Hong Kong dollar', + 'singular' => 'Hong Kong dollar', + 'plural' => 'Hong Kong dollars', + 'short_symbol' => '$', + 'locales' => $locales['HKD'], + ), + 'HN' => array( + 'currency_code' => 'HNL', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_HN', + 'name' => 'Honduran lempira', + 'singular' => 'Honduran lempira', + 'plural' => 'Honduran lempiras', + 'short_symbol' => 'L', + 'locales' => $locales['HNL'], + ), + 'HR' => array( + 'currency_code' => 'HRK', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'hr_HR', + 'name' => 'Croatian kuna', + 'singular' => 'Croatian kuna', + 'plural' => 'Croatian kunas', + 'short_symbol' => 'kn', + 'locales' => $locales['HRK'], + ), + 'HT' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_HT', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), + 'HU' => array( + 'currency_code' => 'HUF', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'hu_HU', + 'name' => 'Hungarian forint', + 'singular' => 'Hungarian forint', + 'plural' => 'Hungarian forints', + 'short_symbol' => 'Ft', + 'locales' => $locales['HUF'], + ), + 'IC' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_IC', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'ID' => array( + 'currency_code' => 'IDR', + 'currency_pos' => 'left', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'id_ID', + 'name' => 'Indonesian rupiah', + 'singular' => 'Indonesian rupiah', + 'plural' => 'Indonesian rupiahs', + 'short_symbol' => 'Rp', + 'locales' => $locales['IDR'], + ), + 'IE' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_IE', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'IL' => array( + 'currency_code' => 'ILS', + 'currency_pos' => 'right_space', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'he_IL', + 'name' => 'Israeli new shekel', + 'singular' => 'Israeli new shekel', + 'plural' => 'Israeli new shekels', + 'short_symbol' => '₪', + 'locales' => $locales['ILS'], + ), + 'IM' => array( + 'currency_code' => 'GBP', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_IM', + 'name' => 'Pound sterling', + 'singular' => 'British pound', + 'plural' => 'British pounds', + 'short_symbol' => '£', + 'locales' => $locales['GBP'], + ), + 'IN' => array( + 'currency_code' => 'INR', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'hi_IN', + 'name' => 'Indian rupee', + 'singular' => 'Indian rupee', + 'plural' => 'Indian rupees', + 'short_symbol' => '₹', + 'locales' => $locales['INR'], + ), + 'IO' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_IO', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), + 'IQ' => array( + 'currency_code' => 'IQD', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_IQ', + 'name' => 'Iraqi dinar', + 'singular' => 'Iraqi dinar', + 'plural' => 'Iraqi dinars', + 'short_symbol' => null, + 'locales' => $locales['IQD'], + ), + 'IR' => array( + 'currency_code' => 'IRR', + 'currency_pos' => 'left', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'fa_IR', + 'name' => 'Iranian rial', + 'singular' => 'Iranian rial', + 'plural' => 'Iranian rials', + 'short_symbol' => null, + 'locales' => $locales['IRR'], + ), + 'IS' => array( + 'currency_code' => 'ISK', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'is_IS', + 'name' => 'Icelandic króna', + 'singular' => 'Icelandic króna', + 'plural' => 'Icelandic krónur', + 'short_symbol' => 'kr', + 'locales' => $locales['ISK'], + ), + 'IT' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'it_IT', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'JE' => array( + 'currency_code' => 'GBP', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_JE', + 'name' => 'Pound sterling', + 'singular' => 'British pound', + 'plural' => 'British pounds', + 'short_symbol' => '£', + 'locales' => $locales['GBP'], + ), + 'JM' => array( + 'currency_code' => 'JMD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_JM', + 'name' => 'Jamaican dollar', + 'singular' => 'Jamaican dollar', + 'plural' => 'Jamaican dollars', + 'short_symbol' => '$', + 'locales' => $locales['JMD'], + ), + 'JO' => array( + 'currency_code' => 'JOD', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 3, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_JO', + 'name' => 'Jordanian dinar', + 'singular' => 'Jordanian dinar', + 'plural' => 'Jordanian dinars', + 'short_symbol' => null, + 'locales' => $locales['JOD'], + ), + 'JP' => array( + 'currency_code' => 'JPY', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'ja_JP', + 'name' => 'Japanese yen', + 'singular' => 'Japanese yen', + 'plural' => 'Japanese yen', + 'short_symbol' => '¥', + 'locales' => $locales['JPY'], + ), + 'KE' => array( + 'currency_code' => 'KES', + 'currency_pos' => 'left_space', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'sw_KE', + 'name' => 'Kenyan shilling', + 'singular' => 'Kenyan shilling', + 'plural' => 'Kenyan shillings', + 'short_symbol' => null, + 'locales' => $locales['KES'], + ), + 'KG' => array( + 'currency_code' => 'KGS', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'ky_KG', + 'name' => 'Kyrgyzstani som', + 'singular' => 'Kyrgystani som', + 'plural' => 'Kyrgystani soms', + 'short_symbol' => null, + 'locales' => $locales['KGS'], + ), + 'KH' => array( + 'currency_code' => 'KHR', + 'currency_pos' => 'right', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'km_KH', + 'name' => 'Cambodian riel', + 'singular' => 'Cambodian riel', + 'plural' => 'Cambodian riels', + 'short_symbol' => '៛', + 'locales' => $locales['KHR'], + ), + 'KI' => array( + 'currency_code' => 'AUD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_KI', + 'name' => 'Australian dollar', + 'singular' => 'Australian dollar', + 'plural' => 'Australian dollars', + 'short_symbol' => '$', + 'locales' => $locales['AUD'], + ), + 'KM' => array( + 'currency_code' => 'KMF', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_KM', + 'name' => 'Comorian franc', + 'singular' => 'Comorian franc', + 'plural' => 'Comorian francs', + 'short_symbol' => 'CF', + 'locales' => $locales['KMF'], + ), + 'KN' => array( + 'currency_code' => 'XCD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_KN', + 'name' => 'East Caribbean dollar', + 'singular' => 'East Caribbean dollar', + 'plural' => 'East Caribbean dollars', + 'short_symbol' => '$', + 'locales' => $locales['XCD'], + ), + 'KP' => array( + 'currency_code' => 'KPW', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'ko_KP', + 'name' => 'North Korean won', + 'singular' => 'North Korean won', + 'plural' => 'North Korean won', + 'short_symbol' => '₩', + 'locales' => $locales['KPW'], + ), + 'KR' => array( + 'currency_code' => 'KRW', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'ko_KR', + 'name' => 'South Korean won', + 'singular' => 'South Korean won', + 'plural' => 'South Korean won', + 'short_symbol' => '₩', + 'locales' => $locales['KRW'], + ), + 'KW' => array( + 'currency_code' => 'KWD', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 3, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_KW', + 'name' => 'Kuwaiti dinar', + 'singular' => 'Kuwaiti dinar', + 'plural' => 'Kuwaiti dinars', + 'short_symbol' => null, + 'locales' => $locales['KWD'], + ), + 'KY' => array( + 'currency_code' => 'KYD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_KY', + 'name' => 'Cayman Islands dollar', + 'singular' => 'Cayman Islands dollar', + 'plural' => 'Cayman Islands dollars', + 'short_symbol' => '$', + 'locales' => $locales['KYD'], + ), + 'KZ' => array( + 'currency_code' => 'KZT', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'ru_KZ', + 'name' => 'Kazakhstani tenge', + 'singular' => 'Kazakhstani tenge', + 'plural' => 'Kazakhstani tenges', + 'short_symbol' => '₸', + 'locales' => $locales['KZT'], + ), + 'LA' => array( + 'currency_code' => 'LAK', + 'currency_pos' => 'left', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'lo_LA', + 'name' => 'Lao kip', + 'singular' => 'Laotian kip', + 'plural' => 'Laotian kips', + 'short_symbol' => '₭', + 'locales' => $locales['LAK'], + ), + 'LB' => array( + 'currency_code' => 'LBP', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_LB', + 'name' => 'Lebanese pound', + 'singular' => 'Lebanese pound', + 'plural' => 'Lebanese pounds', + 'short_symbol' => 'L£', + 'locales' => $locales['LBP'], + ), + 'LC' => array( + 'currency_code' => 'XCD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_LC', + 'name' => 'East Caribbean dollar', + 'singular' => 'East Caribbean dollar', + 'plural' => 'East Caribbean dollars', + 'short_symbol' => '$', + 'locales' => $locales['XCD'], + ), + 'LI' => array( + 'currency_code' => 'CHF', + 'currency_pos' => 'left_space', + 'thousand_sep' => '\'', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'de_LI', + 'name' => 'Swiss franc', + 'singular' => 'Swiss franc', + 'plural' => 'Swiss francs', + 'short_symbol' => null, + 'locales' => $locales['CHF'], + ), + 'LK' => array( + 'currency_code' => 'LKR', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'si_LK', + 'name' => 'Sri Lankan rupee', + 'singular' => 'Sri Lankan rupee', + 'plural' => 'Sri Lankan rupees', + 'short_symbol' => 'Rs', + 'locales' => $locales['LKR'], + ), + 'LR' => array( + 'currency_code' => 'LRD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_LR', + 'name' => 'Liberian dollar', + 'singular' => 'Liberian dollar', + 'plural' => 'Liberian dollars', + 'short_symbol' => '$', + 'locales' => $locales['LRD'], + ), + 'LS' => array( + 'currency_code' => 'LSL', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_LS', + 'name' => 'Lesotho loti', + 'singular' => 'Lesotho loti', + 'plural' => 'Lesotho lotis', + 'short_symbol' => null, + 'locales' => $locales['LSL'], + ), + 'LT' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'lt_LT', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'LU' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_LU', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'LV' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'lv_LV', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'LY' => array( + 'currency_code' => 'LYD', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 3, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_LY', + 'name' => 'Libyan dinar', + 'singular' => 'Libyan dinar', + 'plural' => 'Libyan dinars', + 'short_symbol' => null, + 'locales' => $locales['LYD'], + ), + 'MA' => array( + 'currency_code' => 'MAD', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_MA', + 'name' => 'Moroccan dirham', + 'singular' => 'Moroccan dirham', + 'plural' => 'Moroccan dirhams', + 'short_symbol' => null, + 'locales' => $locales['MAD'], + ), + 'MC' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_MC', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'MD' => array( + 'currency_code' => 'MDL', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'ro_MD', + 'name' => 'Moldovan leu', + 'singular' => 'Moldovan leu', + 'plural' => 'Moldovan lei', + 'short_symbol' => null, + 'locales' => $locales['MDL'], + ), + 'ME' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'sr_Latn_ME', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'MF' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_MF', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'MG' => array( + 'currency_code' => 'MGA', + 'currency_pos' => 'left_space', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'mg_MG', + 'name' => 'Malagasy ariary', + 'singular' => 'Malagasy ariary', + 'plural' => 'Malagasy ariaries', + 'short_symbol' => 'Ar', + 'locales' => $locales['MGA'], + ), + 'MH' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_MH', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), + 'MK' => array( + 'currency_code' => 'MKD', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'mk_MK', + 'name' => 'Macedonian denar', + 'singular' => 'Macedonian denar', + 'plural' => 'Macedonian denari', + 'short_symbol' => null, + 'locales' => $locales['MKD'], + ), + 'ML' => array( + 'currency_code' => 'XOF', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_ML', + 'name' => 'West African CFA franc', + 'singular' => 'West African CFA franc', + 'plural' => 'West African CFA francs', + 'short_symbol' => null, + 'locales' => $locales['XOF'], + ), + 'MM' => array( + 'currency_code' => 'MMK', + 'currency_pos' => 'right_space', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'my_MM', + 'name' => 'Burmese kyat', + 'singular' => 'Myanmar kyat', + 'plural' => 'Myanmar kyats', + 'short_symbol' => 'K', + 'locales' => $locales['MMK'], + ), + 'MN' => array( + 'currency_code' => 'MNT', + 'currency_pos' => 'left_space', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'mn_MN', + 'name' => 'Mongolian tögrög', + 'singular' => 'Mongolian tugrik', + 'plural' => 'Mongolian tugriks', + 'short_symbol' => '₮', + 'locales' => $locales['MNT'], + ), + 'MO' => array( + 'currency_code' => 'MOP', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'zh_Hant_MO', + 'name' => 'Macanese pataca', + 'singular' => 'Macanese pataca', + 'plural' => 'Macanese patacas', + 'short_symbol' => null, + 'locales' => $locales['MOP'], + ), + 'MP' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_MP', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), + 'MQ' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_MQ', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'MR' => array( + 'currency_code' => 'MRU', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_MR', + 'name' => 'Mauritanian ouguiya', + 'singular' => 'Mauritanian ouguiya', + 'plural' => 'Mauritanian ouguiyas', + 'short_symbol' => null, + 'locales' => $locales['MRU'], + ), + 'MS' => array( + 'currency_code' => 'XCD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_MS', + 'name' => 'East Caribbean dollar', + 'singular' => 'East Caribbean dollar', + 'plural' => 'East Caribbean dollars', + 'short_symbol' => '$', + 'locales' => $locales['XCD'], + ), + 'MT' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'mt_MT', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'MU' => array( + 'currency_code' => 'MUR', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_MU', + 'name' => 'Mauritian rupee', + 'singular' => 'Mauritian rupee', + 'plural' => 'Mauritian rupees', + 'short_symbol' => 'Rs', + 'locales' => $locales['MUR'], + ), + 'MV' => array( + 'currency_code' => 'MVR', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => '', + 'name' => 'Maldivian rufiyaa', + 'singular' => 'Maldivian rufiyaa', + 'plural' => 'Maldivian rufiyaas', + 'short_symbol' => null, + 'locales' => $locales['MVR'], + ), + 'MW' => array( + 'currency_code' => 'MWK', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_MW', + 'name' => 'Malawian kwacha', + 'singular' => 'Malawian kwacha', + 'plural' => 'Malawian kwachas', + 'short_symbol' => null, + 'locales' => $locales['MWK'], + ), + 'MX' => array( + 'currency_code' => 'MXN', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_MX', + 'name' => 'Mexican peso', + 'singular' => 'Mexican peso', + 'plural' => 'Mexican pesos', + 'short_symbol' => '$', + 'locales' => $locales['MXN'], + ), + 'MY' => array( + 'currency_code' => 'MYR', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'ms_MY', + 'name' => 'Malaysian ringgit', + 'singular' => 'Malaysian ringgit', + 'plural' => 'Malaysian ringgits', + 'short_symbol' => 'RM', + 'locales' => $locales['MYR'], + ), + 'MZ' => array( + 'currency_code' => 'MZN', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'pt_MZ', + 'name' => 'Mozambican metical', + 'singular' => 'Mozambican metical', + 'plural' => 'Mozambican meticals', + 'short_symbol' => null, + 'locales' => $locales['MZN'], + ), + 'NA' => array( + 'currency_code' => 'NAD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_NA', + 'name' => 'Namibian dollar', + 'singular' => 'Namibian dollar', + 'plural' => 'Namibian dollars', + 'short_symbol' => '$', + 'locales' => $locales['NAD'], + ), + 'NC' => array( + 'currency_code' => 'XPF', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_NC', + 'name' => 'CFP franc', + 'singular' => 'CFP franc', + 'plural' => 'CFP francs', + 'short_symbol' => null, + 'locales' => $locales['XPF'], + ), + 'NE' => array( + 'currency_code' => 'XOF', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_NE', + 'name' => 'West African CFA franc', + 'singular' => 'West African CFA franc', + 'plural' => 'West African CFA francs', + 'short_symbol' => null, + 'locales' => $locales['XOF'], + ), + 'NF' => array( + 'currency_code' => 'AUD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_NF', + 'name' => 'Australian dollar', + 'singular' => 'Australian dollar', + 'plural' => 'Australian dollars', + 'short_symbol' => '$', + 'locales' => $locales['AUD'], + ), + 'NG' => array( + 'currency_code' => 'NGN', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_NG', + 'name' => 'Nigerian naira', + 'singular' => 'Nigerian naira', + 'plural' => 'Nigerian nairas', + 'short_symbol' => '₦', + 'locales' => $locales['NGN'], + ), + 'NI' => array( + 'currency_code' => 'NIO', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_NI', + 'name' => 'Nicaraguan córdoba', + 'singular' => 'Nicaraguan córdoba', + 'plural' => 'Nicaraguan córdobas', + 'short_symbol' => 'C$', + 'locales' => $locales['NIO'], + ), + 'NL' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'nl_NL', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'NO' => array( + 'currency_code' => 'NOK', + 'currency_pos' => 'left_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'nb_NO', + 'name' => 'Norwegian krone', + 'singular' => 'Norwegian krone', + 'plural' => 'Norwegian kroner', + 'short_symbol' => 'kr', + 'locales' => $locales['NOK'], + ), + 'NP' => array( + 'currency_code' => 'NPR', + 'currency_pos' => 'left_space', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'ne_NP', + 'name' => 'Nepalese rupee', + 'singular' => 'Nepalese rupee', + 'plural' => 'Nepalese rupees', + 'short_symbol' => 'Rs', + 'locales' => $locales['NPR'], + ), + 'NR' => array( + 'currency_code' => 'AUD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_NR', + 'name' => 'Australian dollar', + 'singular' => 'Australian dollar', + 'plural' => 'Australian dollars', + 'short_symbol' => '$', + 'locales' => $locales['AUD'], + ), + 'NU' => array( + 'currency_code' => 'NZD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_NU', + 'name' => 'New Zealand dollar', + 'singular' => 'New Zealand dollar', + 'plural' => 'New Zealand dollars', + 'short_symbol' => '$', + 'locales' => $locales['NZD'], + ), + 'NZ' => array( + 'currency_code' => 'NZD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_NZ', + 'name' => 'New Zealand dollar', + 'singular' => 'New Zealand dollar', + 'plural' => 'New Zealand dollars', + 'short_symbol' => '$', + 'locales' => $locales['NZD'], + ), + 'OM' => array( + 'currency_code' => 'OMR', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 3, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_OM', + 'name' => 'Omani rial', + 'singular' => 'Omani rial', + 'plural' => 'Omani rials', + 'short_symbol' => null, + 'locales' => $locales['OMR'], + ), + 'PA' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_PA', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), + 'PE' => array( + 'currency_code' => 'PEN', + 'currency_pos' => 'left_space', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_PE', + 'name' => 'Sol', + 'singular' => 'Peruvian sol', + 'plural' => 'Peruvian soles', + 'short_symbol' => null, + 'locales' => $locales['PEN'], + ), + 'PF' => array( + 'currency_code' => 'XPF', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_PF', + 'name' => 'CFP franc', + 'singular' => 'CFP franc', + 'plural' => 'CFP francs', + 'short_symbol' => null, + 'locales' => $locales['XPF'], + ), + 'PG' => array( + 'currency_code' => 'PGK', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_PG', + 'name' => 'Papua New Guinean kina', + 'singular' => 'Papua New Guinean kina', + 'plural' => 'Papua New Guinean kina', + 'short_symbol' => null, + 'locales' => $locales['PGK'], + ), + 'PH' => array( + 'currency_code' => 'PHP', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_PH', + 'name' => 'Philippine peso', + 'singular' => 'Philippine piso', + 'plural' => 'Philippine pisos', + 'short_symbol' => '₱', + 'locales' => $locales['PHP'], + ), + 'PK' => array( + 'currency_code' => 'PKR', + 'currency_pos' => 'left_space', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ur_PK', + 'name' => 'Pakistani rupee', + 'singular' => 'Pakistani rupee', + 'plural' => 'Pakistani rupees', + 'short_symbol' => 'Rs', + 'locales' => $locales['PKR'], + ), + 'PL' => array( + 'currency_code' => 'PLN', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'pl_PL', + 'name' => 'Polish złoty', + 'singular' => 'Polish zloty', + 'plural' => 'Polish zlotys', + 'short_symbol' => 'zł', + 'locales' => $locales['PLN'], + ), + 'PM' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_PM', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'PN' => array( + 'currency_code' => 'NZD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_PN', + 'name' => 'New Zealand dollar', + 'singular' => 'New Zealand dollar', + 'plural' => 'New Zealand dollars', + 'short_symbol' => '$', + 'locales' => $locales['NZD'], + ), + 'PR' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_PR', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), + 'PS' => array( + 'currency_code' => 'JOD', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 3, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_PS', + 'name' => 'Jordanian dinar', + 'singular' => 'Jordanian dinar', + 'plural' => 'Jordanian dinars', + 'short_symbol' => null, + 'locales' => $locales['JOD'], + ), + 'PT' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'pt_PT', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'PW' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_PW', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), + 'PY' => array( + 'currency_code' => 'PYG', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_PY', + 'name' => 'Paraguayan guaraní', + 'singular' => 'Paraguayan guarani', + 'plural' => 'Paraguayan guaranis', + 'short_symbol' => '₲', + 'locales' => $locales['PYG'], + ), + 'QA' => array( + 'currency_code' => 'QAR', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_QA', + 'name' => 'Qatari riyal', + 'singular' => 'Qatari rial', + 'plural' => 'Qatari rials', + 'short_symbol' => null, + 'locales' => $locales['QAR'], + ), + 'RE' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_RE', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'RO' => array( + 'currency_code' => 'RON', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'ro_RO', + 'name' => 'Romanian leu', + 'singular' => 'Romanian leu', + 'plural' => 'Romanian lei', + 'short_symbol' => 'lei', + 'locales' => $locales['RON'], + ), + 'RS' => array( + 'currency_code' => 'RSD', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'sr_RS', + 'name' => 'Serbian dinar', + 'singular' => 'Serbian dinar', + 'plural' => 'Serbian dinars', + 'short_symbol' => null, + 'locales' => $locales['RSD'], + ), + 'RU' => array( + 'currency_code' => 'RUB', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'ru_RU', + 'name' => 'Russian ruble', + 'singular' => 'Russian ruble', + 'plural' => 'Russian rubles', + 'short_symbol' => '₽', + 'locales' => $locales['RUB'], + ), + 'RW' => array( + 'currency_code' => 'RWF', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'rw_RW', + 'name' => 'Rwandan franc', + 'singular' => 'Rwandan franc', + 'plural' => 'Rwandan francs', + 'short_symbol' => 'RF', + 'locales' => $locales['RWF'], + ), + 'SA' => array( + 'currency_code' => 'SAR', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_SA', + 'name' => 'Saudi riyal', + 'singular' => 'Saudi riyal', + 'plural' => 'Saudi riyals', + 'short_symbol' => null, + 'locales' => $locales['SAR'], + ), + 'SB' => array( + 'currency_code' => 'SBD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_SB', + 'name' => 'Solomon Islands dollar', + 'singular' => 'Solomon Islands dollar', + 'plural' => 'Solomon Islands dollars', + 'short_symbol' => '$', + 'locales' => $locales['SBD'], + ), + 'SC' => array( + 'currency_code' => 'SCR', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_SC', + 'name' => 'Seychellois rupee', + 'singular' => 'Seychellois rupee', + 'plural' => 'Seychellois rupees', + 'short_symbol' => null, + 'locales' => $locales['SCR'], + ), + 'SD' => array( + 'currency_code' => 'SDG', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_SD', + 'name' => 'Sudanese pound', + 'singular' => 'Sudanese pound', + 'plural' => 'Sudanese pounds', + 'short_symbol' => null, + 'locales' => $locales['SDG'], + ), + 'SE' => array( + 'currency_code' => 'SEK', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'sv_SE', + 'name' => 'Swedish krona', + 'singular' => 'Swedish krona', + 'plural' => 'Swedish kronor', + 'short_symbol' => 'kr', + 'locales' => $locales['SEK'], + ), + 'SG' => array( + 'currency_code' => 'SGD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_SG', + 'name' => 'Singapore dollar', + 'singular' => 'Singapore dollar', + 'plural' => 'Singapore dollars', + 'short_symbol' => '$', + 'locales' => $locales['SGD'], + ), + 'SH' => array( + 'currency_code' => 'SHP', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_SH', + 'name' => 'Saint Helena pound', + 'singular' => 'St. Helena pound', + 'plural' => 'St. Helena pounds', + 'short_symbol' => '£', + 'locales' => $locales['SHP'], + ), + 'SI' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'sl_SI', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'SJ' => array( + 'currency_code' => 'NOK', + 'currency_pos' => 'left_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'nb_SJ', + 'name' => 'Norwegian krone', + 'singular' => 'Norwegian krone', + 'plural' => 'Norwegian kroner', + 'short_symbol' => 'kr', + 'locales' => $locales['NOK'], + ), + 'SK' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'sk_SK', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'SL' => array( + 'currency_code' => 'SLL', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_SL', + 'name' => 'Sierra Leonean leone', + 'singular' => 'Sierra Leonean leone', + 'plural' => 'Sierra Leonean leones', + 'short_symbol' => null, + 'locales' => $locales['SLL'], + ), + 'SM' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'it_SM', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'SN' => array( + 'currency_code' => 'XOF', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'wo_SN', + 'name' => 'West African CFA franc', + 'singular' => 'West African CFA franc', + 'plural' => 'West African CFA francs', + 'short_symbol' => null, + 'locales' => $locales['XOF'], + ), + 'SO' => array( + 'currency_code' => 'SOS', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'so_SO', + 'name' => 'Somali shilling', + 'singular' => 'Somali shilling', + 'plural' => 'Somali shillings', + 'short_symbol' => null, + 'locales' => $locales['SOS'], + ), + 'SR' => array( + 'currency_code' => 'SRD', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'nl_SR', + 'name' => 'Surinamese dollar', + 'singular' => 'Surinamese dollar', + 'plural' => 'Surinamese dollars', + 'short_symbol' => '$', + 'locales' => $locales['SRD'], + ), + 'SS' => array( + 'currency_code' => 'SSP', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_SS', + 'name' => 'South Sudanese pound', + 'singular' => 'South Sudanese pound', + 'plural' => 'South Sudanese pounds', + 'short_symbol' => '£', + 'locales' => $locales['SSP'], + ), + 'ST' => array( + 'currency_code' => 'STN', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'pt_ST', + 'name' => 'São Tomé and Príncipe dobra', + 'singular' => 'São Tomé & Príncipe dobra', + 'plural' => 'São Tomé & Príncipe dobras', + 'short_symbol' => 'Db', + 'locales' => $locales['STN'], + ), + 'SV' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_SV', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), + 'SX' => array( + 'currency_code' => 'ANG', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_SX', + 'name' => 'Netherlands Antillean guilder', + 'singular' => 'Netherlands Antillean guilder', + 'plural' => 'Netherlands Antillean guilders', + 'short_symbol' => null, + 'locales' => $locales['ANG'], + ), + 'SY' => array( + 'currency_code' => 'SYP', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_SY', + 'name' => 'Syrian pound', + 'singular' => 'Syrian pound', + 'plural' => 'Syrian pounds', + 'short_symbol' => '£', + 'locales' => $locales['SYP'], + ), + 'SZ' => array( + 'currency_code' => 'SZL', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_SZ', + 'name' => 'Swazi lilangeni', + 'singular' => 'Swazi lilangeni', + 'plural' => 'Swazi emalangeni', + 'short_symbol' => null, + 'locales' => $locales['SZL'], + ), + 'TC' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_TC', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), + 'TD' => array( + 'currency_code' => 'XAF', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_TD', + 'name' => 'Central African CFA franc', + 'singular' => 'Central African CFA franc', + 'plural' => 'Central African CFA francs', + 'short_symbol' => null, + 'locales' => $locales['XAF'], + ), + 'TG' => array( + 'currency_code' => 'XOF', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_TG', + 'name' => 'West African CFA franc', + 'singular' => 'West African CFA franc', + 'plural' => 'West African CFA francs', + 'short_symbol' => null, + 'locales' => $locales['XOF'], + ), + 'TH' => array( + 'currency_code' => 'THB', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'th_TH', + 'name' => 'Thai baht', + 'singular' => 'Thai baht', + 'plural' => 'Thai baht', + 'short_symbol' => '฿', + 'locales' => $locales['THB'], + ), + 'TJ' => array( + 'currency_code' => 'TJS', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'tg_TJ', + 'name' => 'Tajikistani somoni', + 'singular' => 'Tajikistani somoni', + 'plural' => 'Tajikistani somonis', + 'short_symbol' => null, + 'locales' => $locales['TJS'], + ), + 'TK' => array( + 'currency_code' => 'NZD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_TK', + 'name' => 'New Zealand dollar', + 'singular' => 'New Zealand dollar', + 'plural' => 'New Zealand dollars', + 'short_symbol' => '$', + 'locales' => $locales['NZD'], + ), + 'TL' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'pt_TL', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), + 'TM' => array( + 'currency_code' => 'TMT', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'tk_TM', + 'name' => 'Turkmenistan manat', + 'singular' => 'Turkmenistani manat', + 'plural' => 'Turkmenistani manat', + 'short_symbol' => null, + 'locales' => $locales['TMT'], + ), + 'TN' => array( + 'currency_code' => 'TND', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 3, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_TN', + 'name' => 'Tunisian dinar', + 'singular' => 'Tunisian dinar', + 'plural' => 'Tunisian dinars', + 'short_symbol' => null, + 'locales' => $locales['TND'], + ), + 'TO' => array( + 'currency_code' => 'TOP', + 'currency_pos' => 'left_space', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'to_TO', + 'name' => 'Tongan paʻanga', + 'singular' => 'Tongan paʻanga', + 'plural' => 'Tongan paʻanga', + 'short_symbol' => 'T$', + 'locales' => $locales['TOP'], + ), + 'TR' => array( + 'currency_code' => 'TRY', + 'currency_pos' => 'left', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'tr_TR', + 'name' => 'Turkish lira', + 'singular' => 'Turkish lira', + 'plural' => 'Turkish Lira', + 'short_symbol' => '₺', + 'locales' => $locales['TRY'], + ), + 'TT' => array( + 'currency_code' => 'TTD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_TT', + 'name' => 'Trinidad and Tobago dollar', + 'singular' => 'Trinidad & Tobago dollar', + 'plural' => 'Trinidad & Tobago dollars', + 'short_symbol' => '$', + 'locales' => $locales['TTD'], + ), + 'TV' => array( + 'currency_code' => 'AUD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_TV', + 'name' => 'Australian dollar', + 'singular' => 'Australian dollar', + 'plural' => 'Australian dollars', + 'short_symbol' => '$', + 'locales' => $locales['AUD'], + ), + 'TW' => array( + 'currency_code' => 'TWD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'zh_Hant', + 'name' => 'New Taiwan dollar', + 'singular' => 'New Taiwan dollar', + 'plural' => 'New Taiwan dollars', + 'short_symbol' => '$', + 'locales' => $locales['TWD'], + ), + 'TZ' => array( + 'currency_code' => 'TZS', + 'currency_pos' => 'left_space', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'sw_TZ', + 'name' => 'Tanzanian shilling', + 'singular' => 'Tanzanian shilling', + 'plural' => 'Tanzanian shillings', + 'short_symbol' => null, + 'locales' => $locales['TZS'], + ), + 'UA' => array( + 'currency_code' => 'UAH', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'uk_UA', + 'name' => 'Ukrainian hryvnia', + 'singular' => 'Ukrainian hryvnia', + 'plural' => 'Ukrainian hryvnias', + 'short_symbol' => '₴', + 'locales' => $locales['UAH'], + ), + 'UG' => array( + 'currency_code' => 'UGX', + 'currency_pos' => 'left_space', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'sw_UG', + 'name' => 'Ugandan shilling', + 'singular' => 'Ugandan shilling', + 'plural' => 'Ugandan shillings', + 'short_symbol' => null, + 'locales' => $locales['UGX'], + ), + 'UM' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_UM', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), + 'US' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'oz', + 'dimension_unit' => 'foot', + 'direction' => 'ltr', + 'default_locale' => 'en_US', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), + 'UY' => array( + 'currency_code' => 'UYU', + 'currency_pos' => 'left_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_UY', + 'name' => 'Uruguayan peso', + 'singular' => 'Uruguayan peso', + 'plural' => 'Uruguayan pesos', + 'short_symbol' => '$', + 'locales' => $locales['UYU'], + ), + 'UZ' => array( + 'currency_code' => 'UZS', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'uz_AF', + 'name' => 'Uzbekistani som', + 'singular' => 'Uzbekistani som', + 'plural' => 'Uzbekistani som', + 'short_symbol' => null, + 'locales' => $locales['UZS'], + ), + 'VA' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'it_VA', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'VC' => array( + 'currency_code' => 'XCD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_VC', + 'name' => 'East Caribbean dollar', + 'singular' => 'East Caribbean dollar', + 'plural' => 'East Caribbean dollars', + 'short_symbol' => '$', + 'locales' => $locales['XCD'], + ), + 'VE' => array( + 'currency_code' => 'VES', + 'currency_pos' => 'left', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'es_VE', + 'name' => 'Bolívar soberano', + 'singular' => 'Venezuelan bolívar', + 'plural' => 'Venezuelan bolívars', + 'short_symbol' => null, + 'locales' => $locales['VES'], + ), + 'VG' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_VG', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), + 'VI' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_VI', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), + 'VN' => array( + 'currency_code' => 'VND', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'vi_VN', + 'name' => 'Vietnamese đồng', + 'singular' => 'Vietnamese dong', + 'plural' => 'Vietnamese dong', + 'short_symbol' => '₫', + 'locales' => $locales['VND'], + ), + 'VU' => array( + 'currency_code' => 'VUV', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_VU', + 'name' => 'Vanuatu vatu', + 'singular' => 'Vanuatu vatu', + 'plural' => 'Vanuatu vatus', + 'short_symbol' => null, + 'locales' => $locales['VUV'], + ), + 'WF' => array( + 'currency_code' => 'XPF', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_WF', + 'name' => 'CFP franc', + 'singular' => 'CFP franc', + 'plural' => 'CFP francs', + 'short_symbol' => null, + 'locales' => $locales['XPF'], + ), + 'WS' => array( + 'currency_code' => 'WST', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_WS', + 'name' => 'Samoan tālā', + 'singular' => 'Samoan tala', + 'plural' => 'Samoan tala', + 'short_symbol' => null, + 'locales' => $locales['WST'], + ), + 'XK' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'sq_XK', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'YE' => array( + 'currency_code' => 'YER', + 'currency_pos' => 'right_space', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 0, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'rtl', + 'default_locale' => 'ar_YE', + 'name' => 'Yemeni rial', + 'singular' => 'Yemeni rial', + 'plural' => 'Yemeni rials', + 'short_symbol' => null, + 'locales' => $locales['YER'], + ), + 'YT' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'right_space', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'fr_YT', + 'name' => 'Euro', + 'singular' => 'euro', + 'plural' => 'euros', + 'short_symbol' => '€', + 'locales' => $locales['EUR'], + ), + 'ZA' => array( + 'currency_code' => 'ZAR', + 'currency_pos' => 'left', + 'thousand_sep' => ' ', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_ZA', + 'name' => 'South African rand', + 'singular' => 'South African rand', + 'plural' => 'South African rand', + 'short_symbol' => 'R', + 'locales' => $locales['ZAR'], + ), + 'ZM' => array( + 'currency_code' => 'ZMW', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'en_ZM', + 'name' => 'Zambian kwacha', + 'singular' => 'Zambian kwacha', + 'plural' => 'Zambian kwachas', + 'short_symbol' => 'ZK', + 'locales' => $locales['ZMW'], + ), + 'ZW' => array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'thousand_sep' => ',', + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + 'direction' => 'ltr', + 'default_locale' => 'sn_ZW', + 'name' => 'United States (US) dollar', + 'singular' => 'US dollar', + 'plural' => 'US dollars', + 'short_symbol' => '$', + 'locales' => $locales['USD'], + ), +); diff --git a/i18n/phone.php b/plugins/woocommerce/i18n/phone.php similarity index 100% rename from i18n/phone.php rename to plugins/woocommerce/i18n/phone.php diff --git a/plugins/woocommerce/i18n/states.php b/plugins/woocommerce/i18n/states.php new file mode 100644 index 00000000000..764c42de999 --- /dev/null +++ b/plugins/woocommerce/i18n/states.php @@ -0,0 +1,2089 @@ + array(), + 'AL' => array( // Albanian states. + 'AL-01' => __( 'Berat', 'woocommerce' ), + 'AL-09' => __( 'Dibër', 'woocommerce' ), + 'AL-02' => __( 'Durrës', 'woocommerce' ), + 'AL-03' => __( 'Elbasan', 'woocommerce' ), + 'AL-04' => __( 'Fier', 'woocommerce' ), + 'AL-05' => __( 'Gjirokastër', 'woocommerce' ), + 'AL-06' => __( 'Korçë', 'woocommerce' ), + 'AL-07' => __( 'Kukës', 'woocommerce' ), + 'AL-08' => __( 'Lezhë', 'woocommerce' ), + 'AL-10' => __( 'Shkodër', 'woocommerce' ), + 'AL-11' => __( 'Tirana', 'woocommerce' ), + 'AL-12' => __( 'Vlorë', 'woocommerce' ), + ), + 'AO' => array( // Angolan states. + 'BGO' => __( 'Bengo', 'woocommerce' ), + 'BLU' => __( 'Benguela', 'woocommerce' ), + 'BIE' => __( 'Bié', 'woocommerce' ), + 'CAB' => __( 'Cabinda', 'woocommerce' ), + 'CNN' => __( 'Cunene', 'woocommerce' ), + 'HUA' => __( 'Huambo', 'woocommerce' ), + 'HUI' => __( 'Huíla', 'woocommerce' ), + 'CCU' => __( 'Kuando Kubango', 'woocommerce' ), + 'CNO' => __( 'Kwanza-Norte', 'woocommerce' ), + 'CUS' => __( 'Kwanza-Sul', 'woocommerce' ), + 'LUA' => __( 'Luanda', 'woocommerce' ), + 'LNO' => __( 'Lunda-Norte', 'woocommerce' ), + 'LSU' => __( 'Lunda-Sul', 'woocommerce' ), + 'MAL' => __( 'Malanje', 'woocommerce' ), + 'MOX' => __( 'Moxico', 'woocommerce' ), + 'NAM' => __( 'Namibe', 'woocommerce' ), + 'UIG' => __( 'Uíge', 'woocommerce' ), + 'ZAI' => __( 'Zaire', 'woocommerce' ), + ), + 'AR' => array( // Argentinian provinces. + 'C' => __( 'Ciudad Autónoma de Buenos Aires', 'woocommerce' ), + 'B' => __( 'Buenos Aires', 'woocommerce' ), + 'K' => __( 'Catamarca', 'woocommerce' ), + 'H' => __( 'Chaco', 'woocommerce' ), + 'U' => __( 'Chubut', 'woocommerce' ), + 'X' => __( 'Córdoba', 'woocommerce' ), + 'W' => __( 'Corrientes', 'woocommerce' ), + 'E' => __( 'Entre Ríos', 'woocommerce' ), + 'P' => __( 'Formosa', 'woocommerce' ), + 'Y' => __( 'Jujuy', 'woocommerce' ), + 'L' => __( 'La Pampa', 'woocommerce' ), + 'F' => __( 'La Rioja', 'woocommerce' ), + 'M' => __( 'Mendoza', 'woocommerce' ), + 'N' => __( 'Misiones', 'woocommerce' ), + 'Q' => __( 'Neuquén', 'woocommerce' ), + 'R' => __( 'Río Negro', 'woocommerce' ), + 'A' => __( 'Salta', 'woocommerce' ), + 'J' => __( 'San Juan', 'woocommerce' ), + 'D' => __( 'San Luis', 'woocommerce' ), + 'Z' => __( 'Santa Cruz', 'woocommerce' ), + 'S' => __( 'Santa Fe', 'woocommerce' ), + 'G' => __( 'Santiago del Estero', 'woocommerce' ), + 'V' => __( 'Tierra del Fuego', 'woocommerce' ), + 'T' => __( 'Tucumán', 'woocommerce' ), + ), + 'AT' => array(), + 'AU' => array( // Australian states. + 'ACT' => __( 'Australian Capital Territory', 'woocommerce' ), + 'NSW' => __( 'New South Wales', 'woocommerce' ), + 'NT' => __( 'Northern Territory', 'woocommerce' ), + 'QLD' => __( 'Queensland', 'woocommerce' ), + 'SA' => __( 'South Australia', 'woocommerce' ), + 'TAS' => __( 'Tasmania', 'woocommerce' ), + 'VIC' => __( 'Victoria', 'woocommerce' ), + 'WA' => __( 'Western Australia', 'woocommerce' ), + ), + 'AX' => array(), + 'BD' => array( // Bangladeshi districts. + 'BD-05' => __( 'Bagerhat', 'woocommerce' ), + 'BD-01' => __( 'Bandarban', 'woocommerce' ), + 'BD-02' => __( 'Barguna', 'woocommerce' ), + 'BD-06' => __( 'Barishal', 'woocommerce' ), + 'BD-07' => __( 'Bhola', 'woocommerce' ), + 'BD-03' => __( 'Bogura', 'woocommerce' ), + 'BD-04' => __( 'Brahmanbaria', 'woocommerce' ), + 'BD-09' => __( 'Chandpur', 'woocommerce' ), + 'BD-10' => __( 'Chattogram', 'woocommerce' ), + 'BD-12' => __( 'Chuadanga', 'woocommerce' ), + 'BD-11' => __( "Cox's Bazar", 'woocommerce' ), + 'BD-08' => __( 'Cumilla', 'woocommerce' ), + 'BD-13' => __( 'Dhaka', 'woocommerce' ), + 'BD-14' => __( 'Dinajpur', 'woocommerce' ), + 'BD-15' => __( 'Faridpur ', 'woocommerce' ), + 'BD-16' => __( 'Feni', 'woocommerce' ), + 'BD-19' => __( 'Gaibandha', 'woocommerce' ), + 'BD-18' => __( 'Gazipur', 'woocommerce' ), + 'BD-17' => __( 'Gopalganj', 'woocommerce' ), + 'BD-20' => __( 'Habiganj', 'woocommerce' ), + 'BD-21' => __( 'Jamalpur', 'woocommerce' ), + 'BD-22' => __( 'Jashore', 'woocommerce' ), + 'BD-25' => __( 'Jhalokati', 'woocommerce' ), + 'BD-23' => __( 'Jhenaidah', 'woocommerce' ), + 'BD-24' => __( 'Joypurhat', 'woocommerce' ), + 'BD-29' => __( 'Khagrachhari', 'woocommerce' ), + 'BD-27' => __( 'Khulna', 'woocommerce' ), + 'BD-26' => __( 'Kishoreganj', 'woocommerce' ), + 'BD-28' => __( 'Kurigram', 'woocommerce' ), + 'BD-30' => __( 'Kushtia', 'woocommerce' ), + 'BD-31' => __( 'Lakshmipur', 'woocommerce' ), + 'BD-32' => __( 'Lalmonirhat', 'woocommerce' ), + 'BD-36' => __( 'Madaripur', 'woocommerce' ), + 'BD-37' => __( 'Magura', 'woocommerce' ), + 'BD-33' => __( 'Manikganj ', 'woocommerce' ), + 'BD-39' => __( 'Meherpur', 'woocommerce' ), + 'BD-38' => __( 'Moulvibazar', 'woocommerce' ), + 'BD-35' => __( 'Munshiganj', 'woocommerce' ), + 'BD-34' => __( 'Mymensingh', 'woocommerce' ), + 'BD-48' => __( 'Naogaon', 'woocommerce' ), + 'BD-43' => __( 'Narail', 'woocommerce' ), + 'BD-40' => __( 'Narayanganj', 'woocommerce' ), + 'BD-42' => __( 'Narsingdi', 'woocommerce' ), + 'BD-44' => __( 'Natore', 'woocommerce' ), + 'BD-45' => __( 'Nawabganj', 'woocommerce' ), + 'BD-41' => __( 'Netrakona', 'woocommerce' ), + 'BD-46' => __( 'Nilphamari', 'woocommerce' ), + 'BD-47' => __( 'Noakhali', 'woocommerce' ), + 'BD-49' => __( 'Pabna', 'woocommerce' ), + 'BD-52' => __( 'Panchagarh', 'woocommerce' ), + 'BD-51' => __( 'Patuakhali', 'woocommerce' ), + 'BD-50' => __( 'Pirojpur', 'woocommerce' ), + 'BD-53' => __( 'Rajbari', 'woocommerce' ), + 'BD-54' => __( 'Rajshahi', 'woocommerce' ), + 'BD-56' => __( 'Rangamati', 'woocommerce' ), + 'BD-55' => __( 'Rangpur', 'woocommerce' ), + 'BD-58' => __( 'Satkhira', 'woocommerce' ), + 'BD-62' => __( 'Shariatpur', 'woocommerce' ), + 'BD-57' => __( 'Sherpur', 'woocommerce' ), + 'BD-59' => __( 'Sirajganj', 'woocommerce' ), + 'BD-61' => __( 'Sunamganj', 'woocommerce' ), + 'BD-60' => __( 'Sylhet', 'woocommerce' ), + 'BD-63' => __( 'Tangail', 'woocommerce' ), + 'BD-64' => __( 'Thakurgaon', 'woocommerce' ), + ), + 'BE' => array(), + 'BG' => array( // Bulgarian states. + 'BG-01' => __( 'Blagoevgrad', 'woocommerce' ), + 'BG-02' => __( 'Burgas', 'woocommerce' ), + 'BG-08' => __( 'Dobrich', 'woocommerce' ), + 'BG-07' => __( 'Gabrovo', 'woocommerce' ), + 'BG-26' => __( 'Haskovo', 'woocommerce' ), + 'BG-09' => __( 'Kardzhali', 'woocommerce' ), + 'BG-10' => __( 'Kyustendil', 'woocommerce' ), + 'BG-11' => __( 'Lovech', 'woocommerce' ), + 'BG-12' => __( 'Montana', 'woocommerce' ), + 'BG-13' => __( 'Pazardzhik', 'woocommerce' ), + 'BG-14' => __( 'Pernik', 'woocommerce' ), + 'BG-15' => __( 'Pleven', 'woocommerce' ), + 'BG-16' => __( 'Plovdiv', 'woocommerce' ), + 'BG-17' => __( 'Razgrad', 'woocommerce' ), + 'BG-18' => __( 'Ruse', 'woocommerce' ), + 'BG-27' => __( 'Shumen', 'woocommerce' ), + 'BG-19' => __( 'Silistra', 'woocommerce' ), + 'BG-20' => __( 'Sliven', 'woocommerce' ), + 'BG-21' => __( 'Smolyan', 'woocommerce' ), + 'BG-23' => __( 'Sofia', 'woocommerce' ), + 'BG-22' => __( 'Sofia-Grad', 'woocommerce' ), + 'BG-24' => __( 'Stara Zagora', 'woocommerce' ), + 'BG-25' => __( 'Targovishte', 'woocommerce' ), + 'BG-03' => __( 'Varna', 'woocommerce' ), + 'BG-04' => __( 'Veliko Tarnovo', 'woocommerce' ), + 'BG-05' => __( 'Vidin', 'woocommerce' ), + 'BG-06' => __( 'Vratsa', 'woocommerce' ), + 'BG-28' => __( 'Yambol', 'woocommerce' ), + ), + 'BH' => array(), + 'BI' => array(), + 'BJ' => array( // Beninese states. + 'AL' => __( 'Alibori', 'woocommerce' ), + 'AK' => __( 'Atakora', 'woocommerce' ), + 'AQ' => __( 'Atlantique', 'woocommerce' ), + 'BO' => __( 'Borgou', 'woocommerce' ), + 'CO' => __( 'Collines', 'woocommerce' ), + 'KO' => __( 'Kouffo', 'woocommerce' ), + 'DO' => __( 'Donga', 'woocommerce' ), + 'LI' => __( 'Littoral', 'woocommerce' ), + 'MO' => __( 'Mono', 'woocommerce' ), + 'OU' => __( 'Ouémé', 'woocommerce' ), + 'PL' => __( 'Plateau', 'woocommerce' ), + 'ZO' => __( 'Zou', 'woocommerce' ), + ), + 'BO' => array( // Bolivian states. + 'BO-B' => __( 'Beni', 'woocommerce' ), + 'BO-H' => __( 'Chuquisaca', 'woocommerce' ), + 'BO-C' => __( 'Cochabamba', 'woocommerce' ), + 'BO-L' => __( 'La Paz', 'woocommerce' ), + 'BO-O' => __( 'Oruro', 'woocommerce' ), + 'BO-N' => __( 'Pando', 'woocommerce' ), + 'BO-P' => __( 'Potosí', 'woocommerce' ), + 'BO-S' => __( 'Santa Cruz', 'woocommerce' ), + 'BO-T' => __( 'Tarija', 'woocommerce' ), + ), + 'BR' => array( // Brazilian states. + 'AC' => __( 'Acre', 'woocommerce' ), + 'AL' => __( 'Alagoas', 'woocommerce' ), + 'AP' => __( 'Amapá', 'woocommerce' ), + 'AM' => __( 'Amazonas', 'woocommerce' ), + 'BA' => __( 'Bahia', 'woocommerce' ), + 'CE' => __( 'Ceará', 'woocommerce' ), + 'DF' => __( 'Distrito Federal', 'woocommerce' ), + 'ES' => __( 'Espírito Santo', 'woocommerce' ), + 'GO' => __( 'Goiás', 'woocommerce' ), + 'MA' => __( 'Maranhão', 'woocommerce' ), + 'MT' => __( 'Mato Grosso', 'woocommerce' ), + 'MS' => __( 'Mato Grosso do Sul', 'woocommerce' ), + 'MG' => __( 'Minas Gerais', 'woocommerce' ), + 'PA' => __( 'Pará', 'woocommerce' ), + 'PB' => __( 'Paraíba', 'woocommerce' ), + 'PR' => __( 'Paraná', 'woocommerce' ), + 'PE' => __( 'Pernambuco', 'woocommerce' ), + 'PI' => __( 'Piauí', 'woocommerce' ), + 'RJ' => __( 'Rio de Janeiro', 'woocommerce' ), + 'RN' => __( 'Rio Grande do Norte', 'woocommerce' ), + 'RS' => __( 'Rio Grande do Sul', 'woocommerce' ), + 'RO' => __( 'Rondônia', 'woocommerce' ), + 'RR' => __( 'Roraima', 'woocommerce' ), + 'SC' => __( 'Santa Catarina', 'woocommerce' ), + 'SP' => __( 'São Paulo', 'woocommerce' ), + 'SE' => __( 'Sergipe', 'woocommerce' ), + 'TO' => __( 'Tocantins', 'woocommerce' ), + ), + 'CA' => array( // Canadian states. + 'AB' => __( 'Alberta', 'woocommerce' ), + 'BC' => __( 'British Columbia', 'woocommerce' ), + 'MB' => __( 'Manitoba', 'woocommerce' ), + 'NB' => __( 'New Brunswick', 'woocommerce' ), + 'NL' => __( 'Newfoundland and Labrador', 'woocommerce' ), + 'NT' => __( 'Northwest Territories', 'woocommerce' ), + 'NS' => __( 'Nova Scotia', 'woocommerce' ), + 'NU' => __( 'Nunavut', 'woocommerce' ), + 'ON' => __( 'Ontario', 'woocommerce' ), + 'PE' => __( 'Prince Edward Island', 'woocommerce' ), + 'QC' => __( 'Quebec', 'woocommerce' ), + 'SK' => __( 'Saskatchewan', 'woocommerce' ), + 'YT' => __( 'Yukon Territory', 'woocommerce' ), + ), + 'CH' => array( // Swiss cantons. + 'AG' => __( 'Aargau', 'woocommerce' ), + 'AR' => __( 'Appenzell Ausserrhoden', 'woocommerce' ), + 'AI' => __( 'Appenzell Innerrhoden', 'woocommerce' ), + 'BL' => __( 'Basel-Landschaft', 'woocommerce' ), + 'BS' => __( 'Basel-Stadt', 'woocommerce' ), + 'BE' => __( 'Bern', 'woocommerce' ), + 'FR' => __( 'Fribourg', 'woocommerce' ), + 'GE' => __( 'Geneva', 'woocommerce' ), + 'GL' => __( 'Glarus', 'woocommerce' ), + 'GR' => __( 'Graubünden', 'woocommerce' ), + 'JU' => __( 'Jura', 'woocommerce' ), + 'LU' => __( 'Luzern', 'woocommerce' ), + 'NE' => __( 'Neuchâtel', 'woocommerce' ), + 'NW' => __( 'Nidwalden', 'woocommerce' ), + 'OW' => __( 'Obwalden', 'woocommerce' ), + 'SH' => __( 'Schaffhausen', 'woocommerce' ), + 'SZ' => __( 'Schwyz', 'woocommerce' ), + 'SO' => __( 'Solothurn', 'woocommerce' ), + 'SG' => __( 'St. Gallen', 'woocommerce' ), + 'TG' => __( 'Thurgau', 'woocommerce' ), + 'TI' => __( 'Ticino', 'woocommerce' ), + 'UR' => __( 'Uri', 'woocommerce' ), + 'VS' => __( 'Valais', 'woocommerce' ), + 'VD' => __( 'Vaud', 'woocommerce' ), + 'ZG' => __( 'Zug', 'woocommerce' ), + 'ZH' => __( 'Zürich', 'woocommerce' ), + ), + 'CL' => array( // Chilean states. + 'CL-AI' => __( 'Aisén del General Carlos Ibañez del Campo', 'woocommerce' ), + 'CL-AN' => __( 'Antofagasta', 'woocommerce' ), + 'CL-AP' => __( 'Arica y Parinacota', 'woocommerce' ), + 'CL-AR' => __( 'La Araucanía', 'woocommerce' ), + 'CL-AT' => __( 'Atacama', 'woocommerce' ), + 'CL-BI' => __( 'Biobío', 'woocommerce' ), + 'CL-CO' => __( 'Coquimbo', 'woocommerce' ), + 'CL-LI' => __( 'Libertador General Bernardo O\'Higgins', 'woocommerce' ), + 'CL-LL' => __( 'Los Lagos', 'woocommerce' ), + 'CL-LR' => __( 'Los Ríos', 'woocommerce' ), + 'CL-MA' => __( 'Magallanes', 'woocommerce' ), + 'CL-ML' => __( 'Maule', 'woocommerce' ), + 'CL-NB' => __( 'Ñuble', 'woocommerce' ), + 'CL-RM' => __( 'Región Metropolitana de Santiago', 'woocommerce' ), + 'CL-TA' => __( 'Tarapacá', 'woocommerce' ), + 'CL-VS' => __( 'Valparaíso', 'woocommerce' ), + ), + 'CN' => array( // Chinese states. + 'CN1' => __( 'Yunnan / 云南', 'woocommerce' ), + 'CN2' => __( 'Beijing / 北京', 'woocommerce' ), + 'CN3' => __( 'Tianjin / 天津', 'woocommerce' ), + 'CN4' => __( 'Hebei / 河北', 'woocommerce' ), + 'CN5' => __( 'Shanxi / 山西', 'woocommerce' ), + 'CN6' => __( 'Inner Mongolia / 內蒙古', 'woocommerce' ), + 'CN7' => __( 'Liaoning / 辽宁', 'woocommerce' ), + 'CN8' => __( 'Jilin / 吉林', 'woocommerce' ), + 'CN9' => __( 'Heilongjiang / 黑龙江', 'woocommerce' ), + 'CN10' => __( 'Shanghai / 上海', 'woocommerce' ), + 'CN11' => __( 'Jiangsu / 江苏', 'woocommerce' ), + 'CN12' => __( 'Zhejiang / 浙江', 'woocommerce' ), + 'CN13' => __( 'Anhui / 安徽', 'woocommerce' ), + 'CN14' => __( 'Fujian / 福建', 'woocommerce' ), + 'CN15' => __( 'Jiangxi / 江西', 'woocommerce' ), + 'CN16' => __( 'Shandong / 山东', 'woocommerce' ), + 'CN17' => __( 'Henan / 河南', 'woocommerce' ), + 'CN18' => __( 'Hubei / 湖北', 'woocommerce' ), + 'CN19' => __( 'Hunan / 湖南', 'woocommerce' ), + 'CN20' => __( 'Guangdong / 广东', 'woocommerce' ), + 'CN21' => __( 'Guangxi Zhuang / 广西壮族', 'woocommerce' ), + 'CN22' => __( 'Hainan / 海南', 'woocommerce' ), + 'CN23' => __( 'Chongqing / 重庆', 'woocommerce' ), + 'CN24' => __( 'Sichuan / 四川', 'woocommerce' ), + 'CN25' => __( 'Guizhou / 贵州', 'woocommerce' ), + 'CN26' => __( 'Shaanxi / 陕西', 'woocommerce' ), + 'CN27' => __( 'Gansu / 甘肃', 'woocommerce' ), + 'CN28' => __( 'Qinghai / 青海', 'woocommerce' ), + 'CN29' => __( 'Ningxia Hui / 宁夏', 'woocommerce' ), + 'CN30' => __( 'Macao / 澳门', 'woocommerce' ), + 'CN31' => __( 'Tibet / 西藏', 'woocommerce' ), + 'CN32' => __( 'Xinjiang / 新疆', 'woocommerce' ), + ), + 'CO' => array( // Colombian states. + 'CO-AMA' => __( 'Amazonas', 'woocommerce' ), + 'CO-ANT' => __( 'Antioquia', 'woocommerce' ), + 'CO-ARA' => __( 'Arauca', 'woocommerce' ), + 'CO-ATL' => __( 'Atlántico', 'woocommerce' ), + 'CO-BOL' => __( 'Bolívar', 'woocommerce' ), + 'CO-BOY' => __( 'Boyacá', 'woocommerce' ), + 'CO-CAL' => __( 'Caldas', 'woocommerce' ), + 'CO-CAQ' => __( 'Caquetá', 'woocommerce' ), + 'CO-CAS' => __( 'Casanare', 'woocommerce' ), + 'CO-CAU' => __( 'Cauca', 'woocommerce' ), + 'CO-CES' => __( 'Cesar', 'woocommerce' ), + 'CO-CHO' => __( 'Chocó', 'woocommerce' ), + 'CO-COR' => __( 'Córdoba', 'woocommerce' ), + 'CO-CUN' => __( 'Cundinamarca', 'woocommerce' ), + 'CO-DC' => __( 'Capital District', 'woocommerce' ), + 'CO-GUA' => __( 'Guainía', 'woocommerce' ), + 'CO-GUV' => __( 'Guaviare', 'woocommerce' ), + 'CO-HUI' => __( 'Huila', 'woocommerce' ), + 'CO-LAG' => __( 'La Guajira', 'woocommerce' ), + 'CO-MAG' => __( 'Magdalena', 'woocommerce' ), + 'CO-MET' => __( 'Meta', 'woocommerce' ), + 'CO-NAR' => __( 'Nariño', 'woocommerce' ), + 'CO-NSA' => __( 'Norte de Santander', 'woocommerce' ), + 'CO-PUT' => __( 'Putumayo', 'woocommerce' ), + 'CO-QUI' => __( 'Quindío', 'woocommerce' ), + 'CO-RIS' => __( 'Risaralda', 'woocommerce' ), + 'CO-SAN' => __( 'Santander', 'woocommerce' ), + 'CO-SAP' => __( 'San Andrés & Providencia', 'woocommerce' ), + 'CO-SUC' => __( 'Sucre', 'woocommerce' ), + 'CO-TOL' => __( 'Tolima', 'woocommerce' ), + 'CO-VAC' => __( 'Valle del Cauca', 'woocommerce' ), + 'CO-VAU' => __( 'Vaupés', 'woocommerce' ), + 'CO-VID' => __( 'Vichada', 'woocommerce' ), + ), + 'CR' => array( // Costa Rican states. + 'CR-A' => __( 'Alajuela', 'woocommerce' ), + 'CR-C' => __( 'Cartago', 'woocommerce' ), + 'CR-G' => __( 'Guanacaste', 'woocommerce' ), + 'CR-H' => __( 'Heredia', 'woocommerce' ), + 'CR-L' => __( 'Limón', 'woocommerce' ), + 'CR-P' => __( 'Puntarenas', 'woocommerce' ), + 'CR-SJ' => __( 'San José', 'woocommerce' ), + ), + 'CZ' => array(), + 'DE' => array( // German states. + 'DE-BW' => __( 'Baden-Württemberg', 'woocommerce' ), + 'DE-BY' => __( 'Bavaria', 'woocommerce' ), + 'DE-BE' => __( 'Berlin', 'woocommerce' ), + 'DE-BB' => __( 'Brandenburg', 'woocommerce' ), + 'DE-HB' => __( 'Bremen', 'woocommerce' ), + 'DE-HH' => __( 'Hamburg', 'woocommerce' ), + 'DE-HE' => __( 'Hesse', 'woocommerce' ), + 'DE-MV' => __( 'Mecklenburg-Vorpommern', 'woocommerce' ), + 'DE-NI' => __( 'Lower Saxony', 'woocommerce' ), + 'DE-NW' => __( 'North Rhine-Westphalia', 'woocommerce' ), + 'DE-RP' => __( 'Rhineland-Palatinate', 'woocommerce' ), + 'DE-SL' => __( 'Saarland', 'woocommerce' ), + 'DE-SN' => __( 'Saxony', 'woocommerce' ), + 'DE-ST' => __( 'Saxony-Anhalt', 'woocommerce' ), + 'DE-SH' => __( 'Schleswig-Holstein', 'woocommerce' ), + 'DE-TH' => __( 'Thuringia', 'woocommerce' ), + ), + 'DK' => array(), + 'DO' => array( // Dominican states. + 'DO-01' => __( 'Distrito Nacional', 'woocommerce' ), + 'DO-02' => __( 'Azua', 'woocommerce' ), + 'DO-03' => __( 'Baoruco', 'woocommerce' ), + 'DO-04' => __( 'Barahona', 'woocommerce' ), + 'DO-33' => __( 'Cibao Nordeste', 'woocommerce' ), + 'DO-34' => __( 'Cibao Noroeste', 'woocommerce' ), + 'DO-35' => __( 'Cibao Norte', 'woocommerce' ), + 'DO-36' => __( 'Cibao Sur', 'woocommerce' ), + 'DO-05' => __( 'Dajabón', 'woocommerce' ), + 'DO-06' => __( 'Duarte', 'woocommerce' ), + 'DO-08' => __( 'El Seibo', 'woocommerce' ), + 'DO-37' => __( 'El Valle', 'woocommerce' ), + 'DO-07' => __( 'Elías Piña', 'woocommerce' ), + 'DO-38' => __( 'Enriquillo', 'woocommerce' ), + 'DO-09' => __( 'Espaillat', 'woocommerce' ), + 'DO-30' => __( 'Hato Mayor', 'woocommerce' ), + 'DO-19' => __( 'Hermanas Mirabal', 'woocommerce' ), + 'DO-39' => __( 'Higüamo', 'woocommerce' ), + 'DO-10' => __( 'Independencia', 'woocommerce' ), + 'DO-11' => __( 'La Altagracia', 'woocommerce' ), + 'DO-12' => __( 'La Romana', 'woocommerce' ), + 'DO-13' => __( 'La Vega', 'woocommerce' ), + 'DO-14' => __( 'María Trinidad Sánchez', 'woocommerce' ), + 'DO-28' => __( 'Monseñor Nouel', 'woocommerce' ), + 'DO-15' => __( 'Monte Cristi', 'woocommerce' ), + 'DO-29' => __( 'Monte Plata', 'woocommerce' ), + 'DO-40' => __( 'Ozama', 'woocommerce' ), + 'DO-16' => __( 'Pedernales', 'woocommerce' ), + 'DO-17' => __( 'Peravia', 'woocommerce' ), + 'DO-18' => __( 'Puerto Plata', 'woocommerce' ), + 'DO-20' => __( 'Samaná', 'woocommerce' ), + 'DO-21' => __( 'San Cristóbal', 'woocommerce' ), + 'DO-31' => __( 'San José de Ocoa', 'woocommerce' ), + 'DO-22' => __( 'San Juan', 'woocommerce' ), + 'DO-23' => __( 'San Pedro de Macorís', 'woocommerce' ), + 'DO-24' => __( 'Sánchez Ramírez', 'woocommerce' ), + 'DO-25' => __( 'Santiago', 'woocommerce' ), + 'DO-26' => __( 'Santiago Rodríguez', 'woocommerce' ), + 'DO-32' => __( 'Santo Domingo', 'woocommerce' ), + 'DO-41' => __( 'Valdesia', 'woocommerce' ), + 'DO-27' => __( 'Valverde', 'woocommerce' ), + 'DO-42' => __( 'Yuma', 'woocommerce' ), + ), + 'DZ' => array( // Algerian states. + 'DZ-01' => __( 'Adrar', 'woocommerce' ), + 'DZ-02' => __( 'Chlef', 'woocommerce' ), + 'DZ-03' => __( 'Laghouat', 'woocommerce' ), + 'DZ-04' => __( 'Oum El Bouaghi', 'woocommerce' ), + 'DZ-05' => __( 'Batna', 'woocommerce' ), + 'DZ-06' => __( 'Béjaïa', 'woocommerce' ), + 'DZ-07' => __( 'Biskra', 'woocommerce' ), + 'DZ-08' => __( 'Béchar', 'woocommerce' ), + 'DZ-09' => __( 'Blida', 'woocommerce' ), + 'DZ-10' => __( 'Bouira', 'woocommerce' ), + 'DZ-11' => __( 'Tamanghasset', 'woocommerce' ), + 'DZ-12' => __( 'Tébessa', 'woocommerce' ), + 'DZ-13' => __( 'Tlemcen', 'woocommerce' ), + 'DZ-14' => __( 'Tiaret', 'woocommerce' ), + 'DZ-15' => __( 'Tizi Ouzou', 'woocommerce' ), + 'DZ-16' => __( 'Algiers', 'woocommerce' ), + 'DZ-17' => __( 'Djelfa', 'woocommerce' ), + 'DZ-18' => __( 'Jijel', 'woocommerce' ), + 'DZ-19' => __( 'Sétif', 'woocommerce' ), + 'DZ-20' => __( 'Saïda', 'woocommerce' ), + 'DZ-21' => __( 'Skikda', 'woocommerce' ), + 'DZ-22' => __( 'Sidi Bel Abbès', 'woocommerce' ), + 'DZ-23' => __( 'Annaba', 'woocommerce' ), + 'DZ-24' => __( 'Guelma', 'woocommerce' ), + 'DZ-25' => __( 'Constantine', 'woocommerce' ), + 'DZ-26' => __( 'Médéa', 'woocommerce' ), + 'DZ-27' => __( 'Mostaganem', 'woocommerce' ), + 'DZ-28' => __( 'M’Sila', 'woocommerce' ), + 'DZ-29' => __( 'Mascara', 'woocommerce' ), + 'DZ-30' => __( 'Ouargla', 'woocommerce' ), + 'DZ-31' => __( 'Oran', 'woocommerce' ), + 'DZ-32' => __( 'El Bayadh', 'woocommerce' ), + 'DZ-33' => __( 'Illizi', 'woocommerce' ), + 'DZ-34' => __( 'Bordj Bou Arréridj', 'woocommerce' ), + 'DZ-35' => __( 'Boumerdès', 'woocommerce' ), + 'DZ-36' => __( 'El Tarf', 'woocommerce' ), + 'DZ-37' => __( 'Tindouf', 'woocommerce' ), + 'DZ-38' => __( 'Tissemsilt', 'woocommerce' ), + 'DZ-39' => __( 'El Oued', 'woocommerce' ), + 'DZ-40' => __( 'Khenchela', 'woocommerce' ), + 'DZ-41' => __( 'Souk Ahras', 'woocommerce' ), + 'DZ-42' => __( 'Tipasa', 'woocommerce' ), + 'DZ-43' => __( 'Mila', 'woocommerce' ), + 'DZ-44' => __( 'Aïn Defla', 'woocommerce' ), + 'DZ-45' => __( 'Naama', 'woocommerce' ), + 'DZ-46' => __( 'Aïn Témouchent', 'woocommerce' ), + 'DZ-47' => __( 'Ghardaïa', 'woocommerce' ), + 'DZ-48' => __( 'Relizane', 'woocommerce' ), + ), + 'EE' => array(), + 'EC' => array( // Ecuadorian states. + 'EC-A' => __( 'Azuay', 'woocommerce' ), + 'EC-B' => __( 'Bolívar', 'woocommerce' ), + 'EC-F' => __( 'Cañar', 'woocommerce' ), + 'EC-C' => __( 'Carchi', 'woocommerce' ), + 'EC-H' => __( 'Chimborazo', 'woocommerce' ), + 'EC-X' => __( 'Cotopaxi', 'woocommerce' ), + 'EC-O' => __( 'El Oro', 'woocommerce' ), + 'EC-E' => __( 'Esmeraldas', 'woocommerce' ), + 'EC-W' => __( 'Galápagos', 'woocommerce' ), + 'EC-G' => __( 'Guayas', 'woocommerce' ), + 'EC-I' => __( 'Imbabura', 'woocommerce' ), + 'EC-L' => __( 'Loja', 'woocommerce' ), + 'EC-R' => __( 'Los Ríos', 'woocommerce' ), + 'EC-M' => __( 'Manabí', 'woocommerce' ), + 'EC-S' => __( 'Morona-Santiago', 'woocommerce' ), + 'EC-N' => __( 'Napo', 'woocommerce' ), + 'EC-D' => __( 'Orellana', 'woocommerce' ), + 'EC-Y' => __( 'Pastaza', 'woocommerce' ), + 'EC-P' => __( 'Pichincha', 'woocommerce' ), + 'EC-SE' => __( 'Santa Elena', 'woocommerce' ), + 'EC-SD' => __( 'Santo Domingo de los Tsáchilas', 'woocommerce' ), + 'EC-U' => __( 'Sucumbíos', 'woocommerce' ), + 'EC-T' => __( 'Tungurahua', 'woocommerce' ), + 'EC-Z' => __( 'Zamora-Chinchipe', 'woocommerce' ), + ), + 'EG' => array( // Egyptian states. + 'EGALX' => __( 'Alexandria', 'woocommerce' ), + 'EGASN' => __( 'Aswan', 'woocommerce' ), + 'EGAST' => __( 'Asyut', 'woocommerce' ), + 'EGBA' => __( 'Red Sea', 'woocommerce' ), + 'EGBH' => __( 'Beheira', 'woocommerce' ), + 'EGBNS' => __( 'Beni Suef', 'woocommerce' ), + 'EGC' => __( 'Cairo', 'woocommerce' ), + 'EGDK' => __( 'Dakahlia', 'woocommerce' ), + 'EGDT' => __( 'Damietta', 'woocommerce' ), + 'EGFYM' => __( 'Faiyum', 'woocommerce' ), + 'EGGH' => __( 'Gharbia', 'woocommerce' ), + 'EGGZ' => __( 'Giza', 'woocommerce' ), + 'EGIS' => __( 'Ismailia', 'woocommerce' ), + 'EGJS' => __( 'South Sinai', 'woocommerce' ), + 'EGKB' => __( 'Qalyubia', 'woocommerce' ), + 'EGKFS' => __( 'Kafr el-Sheikh', 'woocommerce' ), + 'EGKN' => __( 'Qena', 'woocommerce' ), + 'EGLX' => __( 'Luxor', 'woocommerce' ), + 'EGMN' => __( 'Minya', 'woocommerce' ), + 'EGMNF' => __( 'Monufia', 'woocommerce' ), + 'EGMT' => __( 'Matrouh', 'woocommerce' ), + 'EGPTS' => __( 'Port Said', 'woocommerce' ), + 'EGSHG' => __( 'Sohag', 'woocommerce' ), + 'EGSHR' => __( 'Al Sharqia', 'woocommerce' ), + 'EGSIN' => __( 'North Sinai', 'woocommerce' ), + 'EGSUZ' => __( 'Suez', 'woocommerce' ), + 'EGWAD' => __( 'New Valley', 'woocommerce' ), + ), + 'ES' => array( // Spanish states. + 'C' => __( 'A Coruña', 'woocommerce' ), + 'VI' => __( 'Araba/Álava', 'woocommerce' ), + 'AB' => __( 'Albacete', 'woocommerce' ), + 'A' => __( 'Alicante', 'woocommerce' ), + 'AL' => __( 'Almería', 'woocommerce' ), + 'O' => __( 'Asturias', 'woocommerce' ), + 'AV' => __( 'Ávila', 'woocommerce' ), + 'BA' => __( 'Badajoz', 'woocommerce' ), + 'PM' => __( 'Baleares', 'woocommerce' ), + 'B' => __( 'Barcelona', 'woocommerce' ), + 'BU' => __( 'Burgos', 'woocommerce' ), + 'CC' => __( 'Cáceres', 'woocommerce' ), + 'CA' => __( 'Cádiz', 'woocommerce' ), + 'S' => __( 'Cantabria', 'woocommerce' ), + 'CS' => __( 'Castellón', 'woocommerce' ), + 'CE' => __( 'Ceuta', 'woocommerce' ), + 'CR' => __( 'Ciudad Real', 'woocommerce' ), + 'CO' => __( 'Córdoba', 'woocommerce' ), + 'CU' => __( 'Cuenca', 'woocommerce' ), + 'GI' => __( 'Girona', 'woocommerce' ), + 'GR' => __( 'Granada', 'woocommerce' ), + 'GU' => __( 'Guadalajara', 'woocommerce' ), + 'SS' => __( 'Gipuzkoa', 'woocommerce' ), + 'H' => __( 'Huelva', 'woocommerce' ), + 'HU' => __( 'Huesca', 'woocommerce' ), + 'J' => __( 'Jaén', 'woocommerce' ), + 'LO' => __( 'La Rioja', 'woocommerce' ), + 'GC' => __( 'Las Palmas', 'woocommerce' ), + 'LE' => __( 'León', 'woocommerce' ), + 'L' => __( 'Lleida', 'woocommerce' ), + 'LU' => __( 'Lugo', 'woocommerce' ), + 'M' => __( 'Madrid', 'woocommerce' ), + 'MA' => __( 'Málaga', 'woocommerce' ), + 'ML' => __( 'Melilla', 'woocommerce' ), + 'MU' => __( 'Murcia', 'woocommerce' ), + 'NA' => __( 'Navarra', 'woocommerce' ), + 'OR' => __( 'Ourense', 'woocommerce' ), + 'P' => __( 'Palencia', 'woocommerce' ), + 'PO' => __( 'Pontevedra', 'woocommerce' ), + 'SA' => __( 'Salamanca', 'woocommerce' ), + 'TF' => __( 'Santa Cruz de Tenerife', 'woocommerce' ), + 'SG' => __( 'Segovia', 'woocommerce' ), + 'SE' => __( 'Sevilla', 'woocommerce' ), + 'SO' => __( 'Soria', 'woocommerce' ), + 'T' => __( 'Tarragona', 'woocommerce' ), + 'TE' => __( 'Teruel', 'woocommerce' ), + 'TO' => __( 'Toledo', 'woocommerce' ), + 'V' => __( 'Valencia', 'woocommerce' ), + 'VA' => __( 'Valladolid', 'woocommerce' ), + 'BI' => __( 'Biscay', 'woocommerce' ), + 'ZA' => __( 'Zamora', 'woocommerce' ), + 'Z' => __( 'Zaragoza', 'woocommerce' ), + ), + 'FI' => array(), + 'FR' => array(), + 'GF' => array(), + 'GH' => array( // Ghanaian regions. + 'AF' => __( 'Ahafo', 'woocommerce' ), + 'AH' => __( 'Ashanti', 'woocommerce' ), + 'BA' => __( 'Brong-Ahafo', 'woocommerce' ), + 'BO' => __( 'Bono', 'woocommerce' ), + 'BE' => __( 'Bono East', 'woocommerce' ), + 'CP' => __( 'Central', 'woocommerce' ), + 'EP' => __( 'Eastern', 'woocommerce' ), + 'AA' => __( 'Greater Accra', 'woocommerce' ), + 'NE' => __( 'North East', 'woocommerce' ), + 'NP' => __( 'Northern', 'woocommerce' ), + 'OT' => __( 'Oti', 'woocommerce' ), + 'SV' => __( 'Savannah', 'woocommerce' ), + 'UE' => __( 'Upper East', 'woocommerce' ), + 'UW' => __( 'Upper West', 'woocommerce' ), + 'TV' => __( 'Volta', 'woocommerce' ), + 'WP' => __( 'Western', 'woocommerce' ), + 'WN' => __( 'Western North', 'woocommerce' ), + ), + 'GP' => array(), + 'GR' => array( // Greek regions. + 'I' => __( 'Attica', 'woocommerce' ), + 'A' => __( 'East Macedonia and Thrace', 'woocommerce' ), + 'B' => __( 'Central Macedonia', 'woocommerce' ), + 'C' => __( 'West Macedonia', 'woocommerce' ), + 'D' => __( 'Epirus', 'woocommerce' ), + 'E' => __( 'Thessaly', 'woocommerce' ), + 'F' => __( 'Ionian Islands', 'woocommerce' ), + 'G' => __( 'West Greece', 'woocommerce' ), + 'H' => __( 'Central Greece', 'woocommerce' ), + 'J' => __( 'Peloponnese', 'woocommerce' ), + 'K' => __( 'North Aegean', 'woocommerce' ), + 'L' => __( 'South Aegean', 'woocommerce' ), + 'M' => __( 'Crete', 'woocommerce' ), + ), + 'GT' => array( // Guatemalan states. + 'GT-AV' => __( 'Alta Verapaz', 'woocommerce' ), + 'GT-BV' => __( 'Baja Verapaz', 'woocommerce' ), + 'GT-CM' => __( 'Chimaltenango', 'woocommerce' ), + 'GT-CQ' => __( 'Chiquimula', 'woocommerce' ), + 'GT-PR' => __( 'El Progreso', 'woocommerce' ), + 'GT-ES' => __( 'Escuintla', 'woocommerce' ), + 'GT-GU' => __( 'Guatemala', 'woocommerce' ), + 'GT-HU' => __( 'Huehuetenango', 'woocommerce' ), + 'GT-IZ' => __( 'Izabal', 'woocommerce' ), + 'GT-JA' => __( 'Jalapa', 'woocommerce' ), + 'GT-JU' => __( 'Jutiapa', 'woocommerce' ), + 'GT-PE' => __( 'Petén', 'woocommerce' ), + 'GT-QZ' => __( 'Quetzaltenango', 'woocommerce' ), + 'GT-QC' => __( 'Quiché', 'woocommerce' ), + 'GT-RE' => __( 'Retalhuleu', 'woocommerce' ), + 'GT-SA' => __( 'Sacatepéquez', 'woocommerce' ), + 'GT-SM' => __( 'San Marcos', 'woocommerce' ), + 'GT-SR' => __( 'Santa Rosa', 'woocommerce' ), + 'GT-SO' => __( 'Sololá', 'woocommerce' ), + 'GT-SU' => __( 'Suchitepéquez', 'woocommerce' ), + 'GT-TO' => __( 'Totonicapán', 'woocommerce' ), + 'GT-ZA' => __( 'Zacapa', 'woocommerce' ), + ), + 'HK' => array( // Hong Kong states. + 'HONG KONG' => __( 'Hong Kong Island', 'woocommerce' ), + 'KOWLOON' => __( 'Kowloon', 'woocommerce' ), + 'NEW TERRITORIES' => __( 'New Territories', 'woocommerce' ), + ), + 'HN' => array( // Honduran states. + 'HN-AT' => __( 'Atlántida', 'woocommerce' ), + 'HN-IB' => __( 'Bay Islands', 'woocommerce' ), + 'HN-CH' => __( 'Choluteca', 'woocommerce' ), + 'HN-CL' => __( 'Colón', 'woocommerce' ), + 'HN-CM' => __( 'Comayagua', 'woocommerce' ), + 'HN-CP' => __( 'Copán', 'woocommerce' ), + 'HN-CR' => __( 'Cortés', 'woocommerce' ), + 'HN-EP' => __( 'El Paraíso', 'woocommerce' ), + 'HN-FM' => __( 'Francisco Morazán', 'woocommerce' ), + 'HN-GD' => __( 'Gracias a Dios', 'woocommerce' ), + 'HN-IN' => __( 'Intibucá', 'woocommerce' ), + 'HN-LE' => __( 'Lempira', 'woocommerce' ), + 'HN-LP' => __( 'La Paz', 'woocommerce' ), + 'HN-OC' => __( 'Ocotepeque', 'woocommerce' ), + 'HN-OL' => __( 'Olancho', 'woocommerce' ), + 'HN-SB' => __( 'Santa Bárbara', 'woocommerce' ), + 'HN-VA' => __( 'Valle', 'woocommerce' ), + 'HN-YO' => __( 'Yoro', 'woocommerce' ), + ), + 'HU' => array( // Hungarian states. + 'BK' => __( 'Bács-Kiskun', 'woocommerce' ), + 'BE' => __( 'Békés', 'woocommerce' ), + 'BA' => __( 'Baranya', 'woocommerce' ), + 'BZ' => __( 'Borsod-Abaúj-Zemplén', 'woocommerce' ), + 'BU' => __( 'Budapest', 'woocommerce' ), + 'CS' => __( 'Csongrád-Csanád', 'woocommerce' ), + 'FE' => __( 'Fejér', 'woocommerce' ), + 'GS' => __( 'Győr-Moson-Sopron', 'woocommerce' ), + 'HB' => __( 'Hajdú-Bihar', 'woocommerce' ), + 'HE' => __( 'Heves', 'woocommerce' ), + 'JN' => __( 'Jász-Nagykun-Szolnok', 'woocommerce' ), + 'KE' => __( 'Komárom-Esztergom', 'woocommerce' ), + 'NO' => __( 'Nógrád', 'woocommerce' ), + 'PE' => __( 'Pest', 'woocommerce' ), + 'SO' => __( 'Somogy', 'woocommerce' ), + 'SZ' => __( 'Szabolcs-Szatmár-Bereg', 'woocommerce' ), + 'TO' => __( 'Tolna', 'woocommerce' ), + 'VA' => __( 'Vas', 'woocommerce' ), + 'VE' => __( 'Veszprém', 'woocommerce' ), + 'ZA' => __( 'Zala', 'woocommerce' ), + ), + 'ID' => array( // Indonesian provinces. + 'AC' => __( 'Daerah Istimewa Aceh', 'woocommerce' ), + 'SU' => __( 'Sumatera Utara', 'woocommerce' ), + 'SB' => __( 'Sumatera Barat', 'woocommerce' ), + 'RI' => __( 'Riau', 'woocommerce' ), + 'KR' => __( 'Kepulauan Riau', 'woocommerce' ), + 'JA' => __( 'Jambi', 'woocommerce' ), + 'SS' => __( 'Sumatera Selatan', 'woocommerce' ), + 'BB' => __( 'Bangka Belitung', 'woocommerce' ), + 'BE' => __( 'Bengkulu', 'woocommerce' ), + 'LA' => __( 'Lampung', 'woocommerce' ), + 'JK' => __( 'DKI Jakarta', 'woocommerce' ), + 'JB' => __( 'Jawa Barat', 'woocommerce' ), + 'BT' => __( 'Banten', 'woocommerce' ), + 'JT' => __( 'Jawa Tengah', 'woocommerce' ), + 'JI' => __( 'Jawa Timur', 'woocommerce' ), + 'YO' => __( 'Daerah Istimewa Yogyakarta', 'woocommerce' ), + 'BA' => __( 'Bali', 'woocommerce' ), + 'NB' => __( 'Nusa Tenggara Barat', 'woocommerce' ), + 'NT' => __( 'Nusa Tenggara Timur', 'woocommerce' ), + 'KB' => __( 'Kalimantan Barat', 'woocommerce' ), + 'KT' => __( 'Kalimantan Tengah', 'woocommerce' ), + 'KI' => __( 'Kalimantan Timur', 'woocommerce' ), + 'KS' => __( 'Kalimantan Selatan', 'woocommerce' ), + 'KU' => __( 'Kalimantan Utara', 'woocommerce' ), + 'SA' => __( 'Sulawesi Utara', 'woocommerce' ), + 'ST' => __( 'Sulawesi Tengah', 'woocommerce' ), + 'SG' => __( 'Sulawesi Tenggara', 'woocommerce' ), + 'SR' => __( 'Sulawesi Barat', 'woocommerce' ), + 'SN' => __( 'Sulawesi Selatan', 'woocommerce' ), + 'GO' => __( 'Gorontalo', 'woocommerce' ), + 'MA' => __( 'Maluku', 'woocommerce' ), + 'MU' => __( 'Maluku Utara', 'woocommerce' ), + 'PA' => __( 'Papua', 'woocommerce' ), + 'PB' => __( 'Papua Barat', 'woocommerce' ), + ), + 'IE' => array( // Irish states. + 'CW' => __( 'Carlow', 'woocommerce' ), + 'CN' => __( 'Cavan', 'woocommerce' ), + 'CE' => __( 'Clare', 'woocommerce' ), + 'CO' => __( 'Cork', 'woocommerce' ), + 'DL' => __( 'Donegal', 'woocommerce' ), + 'D' => __( 'Dublin', 'woocommerce' ), + 'G' => __( 'Galway', 'woocommerce' ), + 'KY' => __( 'Kerry', 'woocommerce' ), + 'KE' => __( 'Kildare', 'woocommerce' ), + 'KK' => __( 'Kilkenny', 'woocommerce' ), + 'LS' => __( 'Laois', 'woocommerce' ), + 'LM' => __( 'Leitrim', 'woocommerce' ), + 'LK' => __( 'Limerick', 'woocommerce' ), + 'LD' => __( 'Longford', 'woocommerce' ), + 'LH' => __( 'Louth', 'woocommerce' ), + 'MO' => __( 'Mayo', 'woocommerce' ), + 'MH' => __( 'Meath', 'woocommerce' ), + 'MN' => __( 'Monaghan', 'woocommerce' ), + 'OY' => __( 'Offaly', 'woocommerce' ), + 'RN' => __( 'Roscommon', 'woocommerce' ), + 'SO' => __( 'Sligo', 'woocommerce' ), + 'TA' => __( 'Tipperary', 'woocommerce' ), + 'WD' => __( 'Waterford', 'woocommerce' ), + 'WH' => __( 'Westmeath', 'woocommerce' ), + 'WX' => __( 'Wexford', 'woocommerce' ), + 'WW' => __( 'Wicklow', 'woocommerce' ), + ), + 'IN' => array( // Indian states. + 'AP' => __( 'Andhra Pradesh', 'woocommerce' ), + 'AR' => __( 'Arunachal Pradesh', 'woocommerce' ), + 'AS' => __( 'Assam', 'woocommerce' ), + 'BR' => __( 'Bihar', 'woocommerce' ), + 'CT' => __( 'Chhattisgarh', 'woocommerce' ), + 'GA' => __( 'Goa', 'woocommerce' ), + 'GJ' => __( 'Gujarat', 'woocommerce' ), + 'HR' => __( 'Haryana', 'woocommerce' ), + 'HP' => __( 'Himachal Pradesh', 'woocommerce' ), + 'JK' => __( 'Jammu and Kashmir', 'woocommerce' ), + 'JH' => __( 'Jharkhand', 'woocommerce' ), + 'KA' => __( 'Karnataka', 'woocommerce' ), + 'KL' => __( 'Kerala', 'woocommerce' ), + 'LA' => __( 'Ladakh', 'woocommerce' ), + 'MP' => __( 'Madhya Pradesh', 'woocommerce' ), + 'MH' => __( 'Maharashtra', 'woocommerce' ), + 'MN' => __( 'Manipur', 'woocommerce' ), + 'ML' => __( 'Meghalaya', 'woocommerce' ), + 'MZ' => __( 'Mizoram', 'woocommerce' ), + 'NL' => __( 'Nagaland', 'woocommerce' ), + 'OR' => __( 'Odisha', 'woocommerce' ), + 'PB' => __( 'Punjab', 'woocommerce' ), + 'RJ' => __( 'Rajasthan', 'woocommerce' ), + 'SK' => __( 'Sikkim', 'woocommerce' ), + 'TN' => __( 'Tamil Nadu', 'woocommerce' ), + 'TS' => __( 'Telangana', 'woocommerce' ), + 'TR' => __( 'Tripura', 'woocommerce' ), + 'UK' => __( 'Uttarakhand', 'woocommerce' ), + 'UP' => __( 'Uttar Pradesh', 'woocommerce' ), + 'WB' => __( 'West Bengal', 'woocommerce' ), + 'AN' => __( 'Andaman and Nicobar Islands', 'woocommerce' ), + 'CH' => __( 'Chandigarh', 'woocommerce' ), + 'DN' => __( 'Dadra and Nagar Haveli', 'woocommerce' ), + 'DD' => __( 'Daman and Diu', 'woocommerce' ), + 'DL' => __( 'Delhi', 'woocommerce' ), + 'LD' => __( 'Lakshadeep', 'woocommerce' ), + 'PY' => __( 'Pondicherry (Puducherry)', 'woocommerce' ), + ), + 'IR' => array( // Irania states. + 'KHZ' => __( 'Khuzestan (خوزستان)', 'woocommerce' ), + 'THR' => __( 'Tehran (تهران)', 'woocommerce' ), + 'ILM' => __( 'Ilaam (ایلام)', 'woocommerce' ), + 'BHR' => __( 'Bushehr (بوشهر)', 'woocommerce' ), + 'ADL' => __( 'Ardabil (اردبیل)', 'woocommerce' ), + 'ESF' => __( 'Isfahan (اصفهان)', 'woocommerce' ), + 'YZD' => __( 'Yazd (یزد)', 'woocommerce' ), + 'KRH' => __( 'Kermanshah (کرمانشاه)', 'woocommerce' ), + 'KRN' => __( 'Kerman (کرمان)', 'woocommerce' ), + 'HDN' => __( 'Hamadan (همدان)', 'woocommerce' ), + 'GZN' => __( 'Ghazvin (قزوین)', 'woocommerce' ), + 'ZJN' => __( 'Zanjan (زنجان)', 'woocommerce' ), + 'LRS' => __( 'Luristan (لرستان)', 'woocommerce' ), + 'ABZ' => __( 'Alborz (البرز)', 'woocommerce' ), + 'EAZ' => __( 'East Azarbaijan (آذربایجان شرقی)', 'woocommerce' ), + 'WAZ' => __( 'West Azarbaijan (آذربایجان غربی)', 'woocommerce' ), + 'CHB' => __( 'Chaharmahal and Bakhtiari (چهارمحال و بختیاری)', 'woocommerce' ), + 'SKH' => __( 'South Khorasan (خراسان جنوبی)', 'woocommerce' ), + 'RKH' => __( 'Razavi Khorasan (خراسان رضوی)', 'woocommerce' ), + 'NKH' => __( 'North Khorasan (خراسان شمالی)', 'woocommerce' ), + 'SMN' => __( 'Semnan (سمنان)', 'woocommerce' ), + 'FRS' => __( 'Fars (فارس)', 'woocommerce' ), + 'QHM' => __( 'Qom (قم)', 'woocommerce' ), + 'KRD' => __( 'Kurdistan / کردستان)', 'woocommerce' ), + 'KBD' => __( 'Kohgiluyeh and BoyerAhmad (کهگیلوییه و بویراحمد)', 'woocommerce' ), + 'GLS' => __( 'Golestan (گلستان)', 'woocommerce' ), + 'GIL' => __( 'Gilan (گیلان)', 'woocommerce' ), + 'MZN' => __( 'Mazandaran (مازندران)', 'woocommerce' ), + 'MKZ' => __( 'Markazi (مرکزی)', 'woocommerce' ), + 'HRZ' => __( 'Hormozgan (هرمزگان)', 'woocommerce' ), + 'SBN' => __( 'Sistan and Baluchestan (سیستان و بلوچستان)', 'woocommerce' ), + ), + 'IS' => array(), + 'IT' => array( // Italian provinces. + 'AG' => __( 'Agrigento', 'woocommerce' ), + 'AL' => __( 'Alessandria', 'woocommerce' ), + 'AN' => __( 'Ancona', 'woocommerce' ), + 'AO' => __( 'Aosta', 'woocommerce' ), + 'AR' => __( 'Arezzo', 'woocommerce' ), + 'AP' => __( 'Ascoli Piceno', 'woocommerce' ), + 'AT' => __( 'Asti', 'woocommerce' ), + 'AV' => __( 'Avellino', 'woocommerce' ), + 'BA' => __( 'Bari', 'woocommerce' ), + 'BT' => __( 'Barletta-Andria-Trani', 'woocommerce' ), + 'BL' => __( 'Belluno', 'woocommerce' ), + 'BN' => __( 'Benevento', 'woocommerce' ), + 'BG' => __( 'Bergamo', 'woocommerce' ), + 'BI' => __( 'Biella', 'woocommerce' ), + 'BO' => __( 'Bologna', 'woocommerce' ), + 'BZ' => __( 'Bolzano', 'woocommerce' ), + 'BS' => __( 'Brescia', 'woocommerce' ), + 'BR' => __( 'Brindisi', 'woocommerce' ), + 'CA' => __( 'Cagliari', 'woocommerce' ), + 'CL' => __( 'Caltanissetta', 'woocommerce' ), + 'CB' => __( 'Campobasso', 'woocommerce' ), + 'CE' => __( 'Caserta', 'woocommerce' ), + 'CT' => __( 'Catania', 'woocommerce' ), + 'CZ' => __( 'Catanzaro', 'woocommerce' ), + 'CH' => __( 'Chieti', 'woocommerce' ), + 'CO' => __( 'Como', 'woocommerce' ), + 'CS' => __( 'Cosenza', 'woocommerce' ), + 'CR' => __( 'Cremona', 'woocommerce' ), + 'KR' => __( 'Crotone', 'woocommerce' ), + 'CN' => __( 'Cuneo', 'woocommerce' ), + 'EN' => __( 'Enna', 'woocommerce' ), + 'FM' => __( 'Fermo', 'woocommerce' ), + 'FE' => __( 'Ferrara', 'woocommerce' ), + 'FI' => __( 'Firenze', 'woocommerce' ), + 'FG' => __( 'Foggia', 'woocommerce' ), + 'FC' => __( 'Forlì-Cesena', 'woocommerce' ), + 'FR' => __( 'Frosinone', 'woocommerce' ), + 'GE' => __( 'Genova', 'woocommerce' ), + 'GO' => __( 'Gorizia', 'woocommerce' ), + 'GR' => __( 'Grosseto', 'woocommerce' ), + 'IM' => __( 'Imperia', 'woocommerce' ), + 'IS' => __( 'Isernia', 'woocommerce' ), + 'SP' => __( 'La Spezia', 'woocommerce' ), + 'AQ' => __( "L'Aquila", 'woocommerce' ), + 'LT' => __( 'Latina', 'woocommerce' ), + 'LE' => __( 'Lecce', 'woocommerce' ), + 'LC' => __( 'Lecco', 'woocommerce' ), + 'LI' => __( 'Livorno', 'woocommerce' ), + 'LO' => __( 'Lodi', 'woocommerce' ), + 'LU' => __( 'Lucca', 'woocommerce' ), + 'MC' => __( 'Macerata', 'woocommerce' ), + 'MN' => __( 'Mantova', 'woocommerce' ), + 'MS' => __( 'Massa-Carrara', 'woocommerce' ), + 'MT' => __( 'Matera', 'woocommerce' ), + 'ME' => __( 'Messina', 'woocommerce' ), + 'MI' => __( 'Milano', 'woocommerce' ), + 'MO' => __( 'Modena', 'woocommerce' ), + 'MB' => __( 'Monza e della Brianza', 'woocommerce' ), + 'NA' => __( 'Napoli', 'woocommerce' ), + 'NO' => __( 'Novara', 'woocommerce' ), + 'NU' => __( 'Nuoro', 'woocommerce' ), + 'OR' => __( 'Oristano', 'woocommerce' ), + 'PD' => __( 'Padova', 'woocommerce' ), + 'PA' => __( 'Palermo', 'woocommerce' ), + 'PR' => __( 'Parma', 'woocommerce' ), + 'PV' => __( 'Pavia', 'woocommerce' ), + 'PG' => __( 'Perugia', 'woocommerce' ), + 'PU' => __( 'Pesaro e Urbino', 'woocommerce' ), + 'PE' => __( 'Pescara', 'woocommerce' ), + 'PC' => __( 'Piacenza', 'woocommerce' ), + 'PI' => __( 'Pisa', 'woocommerce' ), + 'PT' => __( 'Pistoia', 'woocommerce' ), + 'PN' => __( 'Pordenone', 'woocommerce' ), + 'PZ' => __( 'Potenza', 'woocommerce' ), + 'PO' => __( 'Prato', 'woocommerce' ), + 'RG' => __( 'Ragusa', 'woocommerce' ), + 'RA' => __( 'Ravenna', 'woocommerce' ), + 'RC' => __( 'Reggio Calabria', 'woocommerce' ), + 'RE' => __( 'Reggio Emilia', 'woocommerce' ), + 'RI' => __( 'Rieti', 'woocommerce' ), + 'RN' => __( 'Rimini', 'woocommerce' ), + 'RM' => __( 'Roma', 'woocommerce' ), + 'RO' => __( 'Rovigo', 'woocommerce' ), + 'SA' => __( 'Salerno', 'woocommerce' ), + 'SS' => __( 'Sassari', 'woocommerce' ), + 'SV' => __( 'Savona', 'woocommerce' ), + 'SI' => __( 'Siena', 'woocommerce' ), + 'SR' => __( 'Siracusa', 'woocommerce' ), + 'SO' => __( 'Sondrio', 'woocommerce' ), + 'SU' => __( 'Sud Sardegna', 'woocommerce' ), + 'TA' => __( 'Taranto', 'woocommerce' ), + 'TE' => __( 'Teramo', 'woocommerce' ), + 'TR' => __( 'Terni', 'woocommerce' ), + 'TO' => __( 'Torino', 'woocommerce' ), + 'TP' => __( 'Trapani', 'woocommerce' ), + 'TN' => __( 'Trento', 'woocommerce' ), + 'TV' => __( 'Treviso', 'woocommerce' ), + 'TS' => __( 'Trieste', 'woocommerce' ), + 'UD' => __( 'Udine', 'woocommerce' ), + 'VA' => __( 'Varese', 'woocommerce' ), + 'VE' => __( 'Venezia', 'woocommerce' ), + 'VB' => __( 'Verbano-Cusio-Ossola', 'woocommerce' ), + 'VC' => __( 'Vercelli', 'woocommerce' ), + 'VR' => __( 'Verona', 'woocommerce' ), + 'VV' => __( 'Vibo Valentia', 'woocommerce' ), + 'VI' => __( 'Vicenza', 'woocommerce' ), + 'VT' => __( 'Viterbo', 'woocommerce' ), + ), + 'IL' => array(), + 'IM' => array(), + 'JM' => array( // Jamaican parishes. + 'JM-01' => __( 'Kingston', 'woocommerce' ), + 'JM-02' => __( 'Saint Andrew', 'woocommerce' ), + 'JM-03' => __( 'Saint Thomas', 'woocommerce' ), + 'JM-04' => __( 'Portland', 'woocommerce' ), + 'JM-05' => __( 'Saint Mary', 'woocommerce' ), + 'JM-06' => __( 'Saint Ann', 'woocommerce' ), + 'JM-07' => __( 'Trelawny', 'woocommerce' ), + 'JM-08' => __( 'Saint James', 'woocommerce' ), + 'JM-09' => __( 'Hanover', 'woocommerce' ), + 'JM-10' => __( 'Westmoreland', 'woocommerce' ), + 'JM-11' => __( 'Saint Elizabeth', 'woocommerce' ), + 'JM-12' => __( 'Manchester', 'woocommerce' ), + 'JM-13' => __( 'Clarendon', 'woocommerce' ), + 'JM-14' => __( 'Saint Catherine', 'woocommerce' ), + ), + + /** + * Japanese states. + * + * English notation of prefectures conform to the notation of Japan Post. + * The suffix corresponds with the Japanese translation file. + */ + 'JP' => array( + 'JP01' => __( 'Hokkaido', 'woocommerce' ), + 'JP02' => __( 'Aomori', 'woocommerce' ), + 'JP03' => __( 'Iwate', 'woocommerce' ), + 'JP04' => __( 'Miyagi', 'woocommerce' ), + 'JP05' => __( 'Akita', 'woocommerce' ), + 'JP06' => __( 'Yamagata', 'woocommerce' ), + 'JP07' => __( 'Fukushima', 'woocommerce' ), + 'JP08' => __( 'Ibaraki', 'woocommerce' ), + 'JP09' => __( 'Tochigi', 'woocommerce' ), + 'JP10' => __( 'Gunma', 'woocommerce' ), + 'JP11' => __( 'Saitama', 'woocommerce' ), + 'JP12' => __( 'Chiba', 'woocommerce' ), + 'JP13' => __( 'Tokyo', 'woocommerce' ), + 'JP14' => __( 'Kanagawa', 'woocommerce' ), + 'JP15' => __( 'Niigata', 'woocommerce' ), + 'JP16' => __( 'Toyama', 'woocommerce' ), + 'JP17' => __( 'Ishikawa', 'woocommerce' ), + 'JP18' => __( 'Fukui', 'woocommerce' ), + 'JP19' => __( 'Yamanashi', 'woocommerce' ), + 'JP20' => __( 'Nagano', 'woocommerce' ), + 'JP21' => __( 'Gifu', 'woocommerce' ), + 'JP22' => __( 'Shizuoka', 'woocommerce' ), + 'JP23' => __( 'Aichi', 'woocommerce' ), + 'JP24' => __( 'Mie', 'woocommerce' ), + 'JP25' => __( 'Shiga', 'woocommerce' ), + 'JP26' => __( 'Kyoto', 'woocommerce' ), + 'JP27' => __( 'Osaka', 'woocommerce' ), + 'JP28' => __( 'Hyogo', 'woocommerce' ), + 'JP29' => __( 'Nara', 'woocommerce' ), + 'JP30' => __( 'Wakayama', 'woocommerce' ), + 'JP31' => __( 'Tottori', 'woocommerce' ), + 'JP32' => __( 'Shimane', 'woocommerce' ), + 'JP33' => __( 'Okayama', 'woocommerce' ), + 'JP34' => __( 'Hiroshima', 'woocommerce' ), + 'JP35' => __( 'Yamaguchi', 'woocommerce' ), + 'JP36' => __( 'Tokushima', 'woocommerce' ), + 'JP37' => __( 'Kagawa', 'woocommerce' ), + 'JP38' => __( 'Ehime', 'woocommerce' ), + 'JP39' => __( 'Kochi', 'woocommerce' ), + 'JP40' => __( 'Fukuoka', 'woocommerce' ), + 'JP41' => __( 'Saga', 'woocommerce' ), + 'JP42' => __( 'Nagasaki', 'woocommerce' ), + 'JP43' => __( 'Kumamoto', 'woocommerce' ), + 'JP44' => __( 'Oita', 'woocommerce' ), + 'JP45' => __( 'Miyazaki', 'woocommerce' ), + 'JP46' => __( 'Kagoshima', 'woocommerce' ), + 'JP47' => __( 'Okinawa', 'woocommerce' ), + ), + 'KE' => array( // Kenyan counties. + 'KE01' => __( 'Baringo', 'woocommerce' ), + 'KE02' => __( 'Bomet', 'woocommerce' ), + 'KE03' => __( 'Bungoma', 'woocommerce' ), + 'KE04' => __( 'Busia', 'woocommerce' ), + 'KE05' => __( 'Elgeyo-Marakwet', 'woocommerce' ), + 'KE06' => __( 'Embu', 'woocommerce' ), + 'KE07' => __( 'Garissa', 'woocommerce' ), + 'KE08' => __( 'Homa Bay', 'woocommerce' ), + 'KE09' => __( 'Isiolo', 'woocommerce' ), + 'KE10' => __( 'Kajiado', 'woocommerce' ), + 'KE11' => __( 'Kakamega', 'woocommerce' ), + 'KE12' => __( 'Kericho', 'woocommerce' ), + 'KE13' => __( 'Kiambu', 'woocommerce' ), + 'KE14' => __( 'Kilifi', 'woocommerce' ), + 'KE15' => __( 'Kirinyaga', 'woocommerce' ), + 'KE16' => __( 'Kisii', 'woocommerce' ), + 'KE17' => __( 'Kisumu', 'woocommerce' ), + 'KE18' => __( 'Kitui', 'woocommerce' ), + 'KE19' => __( 'Kwale', 'woocommerce' ), + 'KE20' => __( 'Laikipia', 'woocommerce' ), + 'KE21' => __( 'Lamu', 'woocommerce' ), + 'KE22' => __( 'Machakos', 'woocommerce' ), + 'KE23' => __( 'Makueni', 'woocommerce' ), + 'KE24' => __( 'Mandera', 'woocommerce' ), + 'KE25' => __( 'Marsabit', 'woocommerce' ), + 'KE26' => __( 'Meru', 'woocommerce' ), + 'KE27' => __( 'Migori', 'woocommerce' ), + 'KE28' => __( 'Mombasa', 'woocommerce' ), + 'KE29' => __( 'Murang’a', 'woocommerce' ), + 'KE30' => __( 'Nairobi County', 'woocommerce' ), + 'KE31' => __( 'Nakuru', 'woocommerce' ), + 'KE32' => __( 'Nandi', 'woocommerce' ), + 'KE33' => __( 'Narok', 'woocommerce' ), + 'KE34' => __( 'Nyamira', 'woocommerce' ), + 'KE35' => __( 'Nyandarua', 'woocommerce' ), + 'KE36' => __( 'Nyeri', 'woocommerce' ), + 'KE37' => __( 'Samburu', 'woocommerce' ), + 'KE38' => __( 'Siaya', 'woocommerce' ), + 'KE39' => __( 'Taita-Taveta', 'woocommerce' ), + 'KE40' => __( 'Tana River', 'woocommerce' ), + 'KE41' => __( 'Tharaka-Nithi', 'woocommerce' ), + 'KE42' => __( 'Trans Nzoia', 'woocommerce' ), + 'KE43' => __( 'Turkana', 'woocommerce' ), + 'KE44' => __( 'Uasin Gishu', 'woocommerce' ), + 'KE45' => __( 'Vihiga', 'woocommerce' ), + 'KE46' => __( 'Wajir', 'woocommerce' ), + 'KE47' => __( 'West Pokot', 'woocommerce' ), + ), + 'KR' => array(), + 'KW' => array(), + 'LA' => array( // Laotian provinces. + 'AT' => __( 'Attapeu', 'woocommerce' ), + 'BK' => __( 'Bokeo', 'woocommerce' ), + 'BL' => __( 'Bolikhamsai', 'woocommerce' ), + 'CH' => __( 'Champasak', 'woocommerce' ), + 'HO' => __( 'Houaphanh', 'woocommerce' ), + 'KH' => __( 'Khammouane', 'woocommerce' ), + 'LM' => __( 'Luang Namtha', 'woocommerce' ), + 'LP' => __( 'Luang Prabang', 'woocommerce' ), + 'OU' => __( 'Oudomxay', 'woocommerce' ), + 'PH' => __( 'Phongsaly', 'woocommerce' ), + 'SL' => __( 'Salavan', 'woocommerce' ), + 'SV' => __( 'Savannakhet', 'woocommerce' ), + 'VI' => __( 'Vientiane Province', 'woocommerce' ), + 'VT' => __( 'Vientiane', 'woocommerce' ), + 'XA' => __( 'Sainyabuli', 'woocommerce' ), + 'XE' => __( 'Sekong', 'woocommerce' ), + 'XI' => __( 'Xiangkhouang', 'woocommerce' ), + 'XS' => __( 'Xaisomboun', 'woocommerce' ), + ), + 'LB' => array(), + 'LR' => array( // Liberian provinces. + 'BM' => __( 'Bomi', 'woocommerce' ), + 'BN' => __( 'Bong', 'woocommerce' ), + 'GA' => __( 'Gbarpolu', 'woocommerce' ), + 'GB' => __( 'Grand Bassa', 'woocommerce' ), + 'GC' => __( 'Grand Cape Mount', 'woocommerce' ), + 'GG' => __( 'Grand Gedeh', 'woocommerce' ), + 'GK' => __( 'Grand Kru', 'woocommerce' ), + 'LO' => __( 'Lofa', 'woocommerce' ), + 'MA' => __( 'Margibi', 'woocommerce' ), + 'MY' => __( 'Maryland', 'woocommerce' ), + 'MO' => __( 'Montserrado', 'woocommerce' ), + 'NM' => __( 'Nimba', 'woocommerce' ), + 'RV' => __( 'Rivercess', 'woocommerce' ), + 'RG' => __( 'River Gee', 'woocommerce' ), + 'SN' => __( 'Sinoe', 'woocommerce' ), + ), + 'LU' => array(), + 'MD' => array( // Moldovan states. + 'C' => __( 'Chișinău', 'woocommerce' ), + 'BL' => __( 'Bălți', 'woocommerce' ), + 'AN' => __( 'Anenii Noi', 'woocommerce' ), + 'BS' => __( 'Basarabeasca', 'woocommerce' ), + 'BR' => __( 'Briceni', 'woocommerce' ), + 'CH' => __( 'Cahul', 'woocommerce' ), + 'CT' => __( 'Cantemir', 'woocommerce' ), + 'CL' => __( 'Călărași', 'woocommerce' ), + 'CS' => __( 'Căușeni', 'woocommerce' ), + 'CM' => __( 'Cimișlia', 'woocommerce' ), + 'CR' => __( 'Criuleni', 'woocommerce' ), + 'DN' => __( 'Dondușeni', 'woocommerce' ), + 'DR' => __( 'Drochia', 'woocommerce' ), + 'DB' => __( 'Dubăsari', 'woocommerce' ), + 'ED' => __( 'Edineț', 'woocommerce' ), + 'FL' => __( 'Fălești', 'woocommerce' ), + 'FR' => __( 'Florești', 'woocommerce' ), + 'GE' => __( 'UTA Găgăuzia', 'woocommerce' ), + 'GL' => __( 'Glodeni', 'woocommerce' ), + 'HN' => __( 'Hîncești', 'woocommerce' ), + 'IL' => __( 'Ialoveni', 'woocommerce' ), + 'LV' => __( 'Leova', 'woocommerce' ), + 'NS' => __( 'Nisporeni', 'woocommerce' ), + 'OC' => __( 'Ocnița', 'woocommerce' ), + 'OR' => __( 'Orhei', 'woocommerce' ), + 'RZ' => __( 'Rezina', 'woocommerce' ), + 'RS' => __( 'Rîșcani', 'woocommerce' ), + 'SG' => __( 'Sîngerei', 'woocommerce' ), + 'SR' => __( 'Soroca', 'woocommerce' ), + 'ST' => __( 'Strășeni', 'woocommerce' ), + 'SD' => __( 'Șoldănești', 'woocommerce' ), + 'SV' => __( 'Ștefan Vodă', 'woocommerce' ), + 'TR' => __( 'Taraclia', 'woocommerce' ), + 'TL' => __( 'Telenești', 'woocommerce' ), + 'UN' => __( 'Ungheni', 'woocommerce' ), + ), + 'MQ' => array(), + 'MT' => array(), + 'MX' => array( // Mexican states. + 'DF' => __( 'Ciudad de México', 'woocommerce' ), + 'JA' => __( 'Jalisco', 'woocommerce' ), + 'NL' => __( 'Nuevo León', 'woocommerce' ), + 'AG' => __( 'Aguascalientes', 'woocommerce' ), + 'BC' => __( 'Baja California', 'woocommerce' ), + 'BS' => __( 'Baja California Sur', 'woocommerce' ), + 'CM' => __( 'Campeche', 'woocommerce' ), + 'CS' => __( 'Chiapas', 'woocommerce' ), + 'CH' => __( 'Chihuahua', 'woocommerce' ), + 'CO' => __( 'Coahuila', 'woocommerce' ), + 'CL' => __( 'Colima', 'woocommerce' ), + 'DG' => __( 'Durango', 'woocommerce' ), + 'GT' => __( 'Guanajuato', 'woocommerce' ), + 'GR' => __( 'Guerrero', 'woocommerce' ), + 'HG' => __( 'Hidalgo', 'woocommerce' ), + 'MX' => __( 'Estado de México', 'woocommerce' ), + 'MI' => __( 'Michoacán', 'woocommerce' ), + 'MO' => __( 'Morelos', 'woocommerce' ), + 'NA' => __( 'Nayarit', 'woocommerce' ), + 'OA' => __( 'Oaxaca', 'woocommerce' ), + 'PU' => __( 'Puebla', 'woocommerce' ), + 'QT' => __( 'Querétaro', 'woocommerce' ), + 'QR' => __( 'Quintana Roo', 'woocommerce' ), + 'SL' => __( 'San Luis Potosí', 'woocommerce' ), + 'SI' => __( 'Sinaloa', 'woocommerce' ), + 'SO' => __( 'Sonora', 'woocommerce' ), + 'TB' => __( 'Tabasco', 'woocommerce' ), + 'TM' => __( 'Tamaulipas', 'woocommerce' ), + 'TL' => __( 'Tlaxcala', 'woocommerce' ), + 'VE' => __( 'Veracruz', 'woocommerce' ), + 'YU' => __( 'Yucatán', 'woocommerce' ), + 'ZA' => __( 'Zacatecas', 'woocommerce' ), + ), + 'MY' => array( // Malaysian states. + 'JHR' => __( 'Johor', 'woocommerce' ), + 'KDH' => __( 'Kedah', 'woocommerce' ), + 'KTN' => __( 'Kelantan', 'woocommerce' ), + 'LBN' => __( 'Labuan', 'woocommerce' ), + 'MLK' => __( 'Malacca (Melaka)', 'woocommerce' ), + 'NSN' => __( 'Negeri Sembilan', 'woocommerce' ), + 'PHG' => __( 'Pahang', 'woocommerce' ), + 'PNG' => __( 'Penang (Pulau Pinang)', 'woocommerce' ), + 'PRK' => __( 'Perak', 'woocommerce' ), + 'PLS' => __( 'Perlis', 'woocommerce' ), + 'SBH' => __( 'Sabah', 'woocommerce' ), + 'SWK' => __( 'Sarawak', 'woocommerce' ), + 'SGR' => __( 'Selangor', 'woocommerce' ), + 'TRG' => __( 'Terengganu', 'woocommerce' ), + 'PJY' => __( 'Putrajaya', 'woocommerce' ), + 'KUL' => __( 'Kuala Lumpur', 'woocommerce' ), + ), + 'MZ' => array( // Mozambican provinces. + 'MZP' => __( 'Cabo Delgado', 'woocommerce' ), + 'MZG' => __( 'Gaza', 'woocommerce' ), + 'MZI' => __( 'Inhambane', 'woocommerce' ), + 'MZB' => __( 'Manica', 'woocommerce' ), + 'MZL' => __( 'Maputo Province', 'woocommerce' ), + 'MZMPM' => __( 'Maputo', 'woocommerce' ), + 'MZN' => __( 'Nampula', 'woocommerce' ), + 'MZA' => __( 'Niassa', 'woocommerce' ), + 'MZS' => __( 'Sofala', 'woocommerce' ), + 'MZT' => __( 'Tete', 'woocommerce' ), + 'MZQ' => __( 'Zambézia', 'woocommerce' ), + ), + 'NA' => array( // Namibian regions. + 'ER' => __( 'Erongo', 'woocommerce' ), + 'HA' => __( 'Hardap', 'woocommerce' ), + 'KA' => __( 'Karas', 'woocommerce' ), + 'KE' => __( 'Kavango East', 'woocommerce' ), + 'KW' => __( 'Kavango West', 'woocommerce' ), + 'KH' => __( 'Khomas', 'woocommerce' ), + 'KU' => __( 'Kunene', 'woocommerce' ), + 'OW' => __( 'Ohangwena', 'woocommerce' ), + 'OH' => __( 'Omaheke', 'woocommerce' ), + 'OS' => __( 'Omusati', 'woocommerce' ), + 'ON' => __( 'Oshana', 'woocommerce' ), + 'OT' => __( 'Oshikoto', 'woocommerce' ), + 'OD' => __( 'Otjozondjupa', 'woocommerce' ), + 'CA' => __( 'Zambezi', 'woocommerce' ), + ), + 'NG' => array( // Nigerian provinces. + 'AB' => __( 'Abia', 'woocommerce' ), + 'FC' => __( 'Abuja', 'woocommerce' ), + 'AD' => __( 'Adamawa', 'woocommerce' ), + 'AK' => __( 'Akwa Ibom', 'woocommerce' ), + 'AN' => __( 'Anambra', 'woocommerce' ), + 'BA' => __( 'Bauchi', 'woocommerce' ), + 'BY' => __( 'Bayelsa', 'woocommerce' ), + 'BE' => __( 'Benue', 'woocommerce' ), + 'BO' => __( 'Borno', 'woocommerce' ), + 'CR' => __( 'Cross River', 'woocommerce' ), + 'DE' => __( 'Delta', 'woocommerce' ), + 'EB' => __( 'Ebonyi', 'woocommerce' ), + 'ED' => __( 'Edo', 'woocommerce' ), + 'EK' => __( 'Ekiti', 'woocommerce' ), + 'EN' => __( 'Enugu', 'woocommerce' ), + 'GO' => __( 'Gombe', 'woocommerce' ), + 'IM' => __( 'Imo', 'woocommerce' ), + 'JI' => __( 'Jigawa', 'woocommerce' ), + 'KD' => __( 'Kaduna', 'woocommerce' ), + 'KN' => __( 'Kano', 'woocommerce' ), + 'KT' => __( 'Katsina', 'woocommerce' ), + 'KE' => __( 'Kebbi', 'woocommerce' ), + 'KO' => __( 'Kogi', 'woocommerce' ), + 'KW' => __( 'Kwara', 'woocommerce' ), + 'LA' => __( 'Lagos', 'woocommerce' ), + 'NA' => __( 'Nasarawa', 'woocommerce' ), + 'NI' => __( 'Niger', 'woocommerce' ), + 'OG' => __( 'Ogun', 'woocommerce' ), + 'ON' => __( 'Ondo', 'woocommerce' ), + 'OS' => __( 'Osun', 'woocommerce' ), + 'OY' => __( 'Oyo', 'woocommerce' ), + 'PL' => __( 'Plateau', 'woocommerce' ), + 'RI' => __( 'Rivers', 'woocommerce' ), + 'SO' => __( 'Sokoto', 'woocommerce' ), + 'TA' => __( 'Taraba', 'woocommerce' ), + 'YO' => __( 'Yobe', 'woocommerce' ), + 'ZA' => __( 'Zamfara', 'woocommerce' ), + ), + 'NL' => array(), + 'NO' => array(), + 'NP' => array( // Nepalese zones. + 'BAG' => __( 'Bagmati', 'woocommerce' ), + 'BHE' => __( 'Bheri', 'woocommerce' ), + 'DHA' => __( 'Dhaulagiri', 'woocommerce' ), + 'GAN' => __( 'Gandaki', 'woocommerce' ), + 'JAN' => __( 'Janakpur', 'woocommerce' ), + 'KAR' => __( 'Karnali', 'woocommerce' ), + 'KOS' => __( 'Koshi', 'woocommerce' ), + 'LUM' => __( 'Lumbini', 'woocommerce' ), + 'MAH' => __( 'Mahakali', 'woocommerce' ), + 'MEC' => __( 'Mechi', 'woocommerce' ), + 'NAR' => __( 'Narayani', 'woocommerce' ), + 'RAP' => __( 'Rapti', 'woocommerce' ), + 'SAG' => __( 'Sagarmatha', 'woocommerce' ), + 'SET' => __( 'Seti', 'woocommerce' ), + ), + 'NI' => array( // Nicaraguan states. + 'NI-AN' => __( 'Atlántico Norte', 'woocommerce' ), + 'NI-AS' => __( 'Atlántico Sur', 'woocommerce' ), + 'NI-BO' => __( 'Boaco', 'woocommerce' ), + 'NI-CA' => __( 'Carazo', 'woocommerce' ), + 'NI-CI' => __( 'Chinandega', 'woocommerce' ), + 'NI-CO' => __( 'Chontales', 'woocommerce' ), + 'NI-ES' => __( 'Estelí', 'woocommerce' ), + 'NI-GR' => __( 'Granada', 'woocommerce' ), + 'NI-JI' => __( 'Jinotega', 'woocommerce' ), + 'NI-LE' => __( 'León', 'woocommerce' ), + 'NI-MD' => __( 'Madriz', 'woocommerce' ), + 'NI-MN' => __( 'Managua', 'woocommerce' ), + 'NI-MS' => __( 'Masaya', 'woocommerce' ), + 'NI-MT' => __( 'Matagalpa', 'woocommerce' ), + 'NI-NS' => __( 'Nueva Segovia', 'woocommerce' ), + 'NI-RI' => __( 'Rivas', 'woocommerce' ), + 'NI-SJ' => __( 'Río San Juan', 'woocommerce' ), + ), + 'NZ' => array( // New Zealand states. + 'NL' => __( 'Northland', 'woocommerce' ), + 'AK' => __( 'Auckland', 'woocommerce' ), + 'WA' => __( 'Waikato', 'woocommerce' ), + 'BP' => __( 'Bay of Plenty', 'woocommerce' ), + 'TK' => __( 'Taranaki', 'woocommerce' ), + 'GI' => __( 'Gisborne', 'woocommerce' ), + 'HB' => __( 'Hawke’s Bay', 'woocommerce' ), + 'MW' => __( 'Manawatu-Wanganui', 'woocommerce' ), + 'WE' => __( 'Wellington', 'woocommerce' ), + 'NS' => __( 'Nelson', 'woocommerce' ), + 'MB' => __( 'Marlborough', 'woocommerce' ), + 'TM' => __( 'Tasman', 'woocommerce' ), + 'WC' => __( 'West Coast', 'woocommerce' ), + 'CT' => __( 'Canterbury', 'woocommerce' ), + 'OT' => __( 'Otago', 'woocommerce' ), + 'SL' => __( 'Southland', 'woocommerce' ), + ), + 'PA' => array( // Panamanian states. + 'PA-1' => __( 'Bocas del Toro', 'woocommerce' ), + 'PA-2' => __( 'Coclé', 'woocommerce' ), + 'PA-3' => __( 'Colón', 'woocommerce' ), + 'PA-4' => __( 'Chiriquí', 'woocommerce' ), + 'PA-5' => __( 'Darién', 'woocommerce' ), + 'PA-6' => __( 'Herrera', 'woocommerce' ), + 'PA-7' => __( 'Los Santos', 'woocommerce' ), + 'PA-8' => __( 'Panamá', 'woocommerce' ), + 'PA-9' => __( 'Veraguas', 'woocommerce' ), + 'PA-10' => __( 'West Panamá', 'woocommerce' ), + 'PA-EM' => __( 'Emberá', 'woocommerce' ), + 'PA-KY' => __( 'Guna Yala', 'woocommerce' ), + 'PA-NB' => __( 'Ngöbe-Buglé', 'woocommerce' ), + ), + 'PE' => array( // Peruvian states. + 'CAL' => __( 'El Callao', 'woocommerce' ), + 'LMA' => __( 'Municipalidad Metropolitana de Lima', 'woocommerce' ), + 'AMA' => __( 'Amazonas', 'woocommerce' ), + 'ANC' => __( 'Ancash', 'woocommerce' ), + 'APU' => __( 'Apurímac', 'woocommerce' ), + 'ARE' => __( 'Arequipa', 'woocommerce' ), + 'AYA' => __( 'Ayacucho', 'woocommerce' ), + 'CAJ' => __( 'Cajamarca', 'woocommerce' ), + 'CUS' => __( 'Cusco', 'woocommerce' ), + 'HUV' => __( 'Huancavelica', 'woocommerce' ), + 'HUC' => __( 'Huánuco', 'woocommerce' ), + 'ICA' => __( 'Ica', 'woocommerce' ), + 'JUN' => __( 'Junín', 'woocommerce' ), + 'LAL' => __( 'La Libertad', 'woocommerce' ), + 'LAM' => __( 'Lambayeque', 'woocommerce' ), + 'LIM' => __( 'Lima', 'woocommerce' ), + 'LOR' => __( 'Loreto', 'woocommerce' ), + 'MDD' => __( 'Madre de Dios', 'woocommerce' ), + 'MOQ' => __( 'Moquegua', 'woocommerce' ), + 'PAS' => __( 'Pasco', 'woocommerce' ), + 'PIU' => __( 'Piura', 'woocommerce' ), + 'PUN' => __( 'Puno', 'woocommerce' ), + 'SAM' => __( 'San Martín', 'woocommerce' ), + 'TAC' => __( 'Tacna', 'woocommerce' ), + 'TUM' => __( 'Tumbes', 'woocommerce' ), + 'UCA' => __( 'Ucayali', 'woocommerce' ), + ), + 'PH' => array( // Philippine provinces. + 'ABR' => __( 'Abra', 'woocommerce' ), + 'AGN' => __( 'Agusan del Norte', 'woocommerce' ), + 'AGS' => __( 'Agusan del Sur', 'woocommerce' ), + 'AKL' => __( 'Aklan', 'woocommerce' ), + 'ALB' => __( 'Albay', 'woocommerce' ), + 'ANT' => __( 'Antique', 'woocommerce' ), + 'APA' => __( 'Apayao', 'woocommerce' ), + 'AUR' => __( 'Aurora', 'woocommerce' ), + 'BAS' => __( 'Basilan', 'woocommerce' ), + 'BAN' => __( 'Bataan', 'woocommerce' ), + 'BTN' => __( 'Batanes', 'woocommerce' ), + 'BTG' => __( 'Batangas', 'woocommerce' ), + 'BEN' => __( 'Benguet', 'woocommerce' ), + 'BIL' => __( 'Biliran', 'woocommerce' ), + 'BOH' => __( 'Bohol', 'woocommerce' ), + 'BUK' => __( 'Bukidnon', 'woocommerce' ), + 'BUL' => __( 'Bulacan', 'woocommerce' ), + 'CAG' => __( 'Cagayan', 'woocommerce' ), + 'CAN' => __( 'Camarines Norte', 'woocommerce' ), + 'CAS' => __( 'Camarines Sur', 'woocommerce' ), + 'CAM' => __( 'Camiguin', 'woocommerce' ), + 'CAP' => __( 'Capiz', 'woocommerce' ), + 'CAT' => __( 'Catanduanes', 'woocommerce' ), + 'CAV' => __( 'Cavite', 'woocommerce' ), + 'CEB' => __( 'Cebu', 'woocommerce' ), + 'COM' => __( 'Compostela Valley', 'woocommerce' ), + 'NCO' => __( 'Cotabato', 'woocommerce' ), + 'DAV' => __( 'Davao del Norte', 'woocommerce' ), + 'DAS' => __( 'Davao del Sur', 'woocommerce' ), + 'DAC' => __( 'Davao Occidental', 'woocommerce' ), + 'DAO' => __( 'Davao Oriental', 'woocommerce' ), + 'DIN' => __( 'Dinagat Islands', 'woocommerce' ), + 'EAS' => __( 'Eastern Samar', 'woocommerce' ), + 'GUI' => __( 'Guimaras', 'woocommerce' ), + 'IFU' => __( 'Ifugao', 'woocommerce' ), + 'ILN' => __( 'Ilocos Norte', 'woocommerce' ), + 'ILS' => __( 'Ilocos Sur', 'woocommerce' ), + 'ILI' => __( 'Iloilo', 'woocommerce' ), + 'ISA' => __( 'Isabela', 'woocommerce' ), + 'KAL' => __( 'Kalinga', 'woocommerce' ), + 'LUN' => __( 'La Union', 'woocommerce' ), + 'LAG' => __( 'Laguna', 'woocommerce' ), + 'LAN' => __( 'Lanao del Norte', 'woocommerce' ), + 'LAS' => __( 'Lanao del Sur', 'woocommerce' ), + 'LEY' => __( 'Leyte', 'woocommerce' ), + 'MAG' => __( 'Maguindanao', 'woocommerce' ), + 'MAD' => __( 'Marinduque', 'woocommerce' ), + 'MAS' => __( 'Masbate', 'woocommerce' ), + 'MSC' => __( 'Misamis Occidental', 'woocommerce' ), + 'MSR' => __( 'Misamis Oriental', 'woocommerce' ), + 'MOU' => __( 'Mountain Province', 'woocommerce' ), + 'NEC' => __( 'Negros Occidental', 'woocommerce' ), + 'NER' => __( 'Negros Oriental', 'woocommerce' ), + 'NSA' => __( 'Northern Samar', 'woocommerce' ), + 'NUE' => __( 'Nueva Ecija', 'woocommerce' ), + 'NUV' => __( 'Nueva Vizcaya', 'woocommerce' ), + 'MDC' => __( 'Occidental Mindoro', 'woocommerce' ), + 'MDR' => __( 'Oriental Mindoro', 'woocommerce' ), + 'PLW' => __( 'Palawan', 'woocommerce' ), + 'PAM' => __( 'Pampanga', 'woocommerce' ), + 'PAN' => __( 'Pangasinan', 'woocommerce' ), + 'QUE' => __( 'Quezon', 'woocommerce' ), + 'QUI' => __( 'Quirino', 'woocommerce' ), + 'RIZ' => __( 'Rizal', 'woocommerce' ), + 'ROM' => __( 'Romblon', 'woocommerce' ), + 'WSA' => __( 'Samar', 'woocommerce' ), + 'SAR' => __( 'Sarangani', 'woocommerce' ), + 'SIQ' => __( 'Siquijor', 'woocommerce' ), + 'SOR' => __( 'Sorsogon', 'woocommerce' ), + 'SCO' => __( 'South Cotabato', 'woocommerce' ), + 'SLE' => __( 'Southern Leyte', 'woocommerce' ), + 'SUK' => __( 'Sultan Kudarat', 'woocommerce' ), + 'SLU' => __( 'Sulu', 'woocommerce' ), + 'SUN' => __( 'Surigao del Norte', 'woocommerce' ), + 'SUR' => __( 'Surigao del Sur', 'woocommerce' ), + 'TAR' => __( 'Tarlac', 'woocommerce' ), + 'TAW' => __( 'Tawi-Tawi', 'woocommerce' ), + 'ZMB' => __( 'Zambales', 'woocommerce' ), + 'ZAN' => __( 'Zamboanga del Norte', 'woocommerce' ), + 'ZAS' => __( 'Zamboanga del Sur', 'woocommerce' ), + 'ZSI' => __( 'Zamboanga Sibugay', 'woocommerce' ), + '00' => __( 'Metro Manila', 'woocommerce' ), + ), + 'PK' => array( // Pakistani states. + 'JK' => __( 'Azad Kashmir', 'woocommerce' ), + 'BA' => __( 'Balochistan', 'woocommerce' ), + 'TA' => __( 'FATA', 'woocommerce' ), + 'GB' => __( 'Gilgit Baltistan', 'woocommerce' ), + 'IS' => __( 'Islamabad Capital Territory', 'woocommerce' ), + 'KP' => __( 'Khyber Pakhtunkhwa', 'woocommerce' ), + 'PB' => __( 'Punjab', 'woocommerce' ), + 'SD' => __( 'Sindh', 'woocommerce' ), + ), + 'PL' => array(), + 'PR' => array(), + 'PT' => array(), + 'PY' => array( // Paraguayan states. + 'PY-ASU' => __( 'Asunción', 'woocommerce' ), + 'PY-1' => __( 'Concepción', 'woocommerce' ), + 'PY-2' => __( 'San Pedro', 'woocommerce' ), + 'PY-3' => __( 'Cordillera', 'woocommerce' ), + 'PY-4' => __( 'Guairá', 'woocommerce' ), + 'PY-5' => __( 'Caaguazú', 'woocommerce' ), + 'PY-6' => __( 'Caazapá', 'woocommerce' ), + 'PY-7' => __( 'Itapúa', 'woocommerce' ), + 'PY-8' => __( 'Misiones', 'woocommerce' ), + 'PY-9' => __( 'Paraguarí', 'woocommerce' ), + 'PY-10' => __( 'Alto Paraná', 'woocommerce' ), + 'PY-11' => __( 'Central', 'woocommerce' ), + 'PY-12' => __( 'Ñeembucú', 'woocommerce' ), + 'PY-13' => __( 'Amambay', 'woocommerce' ), + 'PY-14' => __( 'Canindeyú', 'woocommerce' ), + 'PY-15' => __( 'Presidente Hayes', 'woocommerce' ), + 'PY-16' => __( 'Alto Paraguay', 'woocommerce' ), + 'PY-17' => __( 'Boquerón', 'woocommerce' ), + ), + 'RE' => array(), + 'RO' => array( // Romanian states. + 'AB' => __( 'Alba', 'woocommerce' ), + 'AR' => __( 'Arad', 'woocommerce' ), + 'AG' => __( 'Argeș', 'woocommerce' ), + 'BC' => __( 'Bacău', 'woocommerce' ), + 'BH' => __( 'Bihor', 'woocommerce' ), + 'BN' => __( 'Bistrița-Năsăud', 'woocommerce' ), + 'BT' => __( 'Botoșani', 'woocommerce' ), + 'BR' => __( 'Brăila', 'woocommerce' ), + 'BV' => __( 'Brașov', 'woocommerce' ), + 'B' => __( 'București', 'woocommerce' ), + 'BZ' => __( 'Buzău', 'woocommerce' ), + 'CL' => __( 'Călărași', 'woocommerce' ), + 'CS' => __( 'Caraș-Severin', 'woocommerce' ), + 'CJ' => __( 'Cluj', 'woocommerce' ), + 'CT' => __( 'Constanța', 'woocommerce' ), + 'CV' => __( 'Covasna', 'woocommerce' ), + 'DB' => __( 'Dâmbovița', 'woocommerce' ), + 'DJ' => __( 'Dolj', 'woocommerce' ), + 'GL' => __( 'Galați', 'woocommerce' ), + 'GR' => __( 'Giurgiu', 'woocommerce' ), + 'GJ' => __( 'Gorj', 'woocommerce' ), + 'HR' => __( 'Harghita', 'woocommerce' ), + 'HD' => __( 'Hunedoara', 'woocommerce' ), + 'IL' => __( 'Ialomița', 'woocommerce' ), + 'IS' => __( 'Iași', 'woocommerce' ), + 'IF' => __( 'Ilfov', 'woocommerce' ), + 'MM' => __( 'Maramureș', 'woocommerce' ), + 'MH' => __( 'Mehedinți', 'woocommerce' ), + 'MS' => __( 'Mureș', 'woocommerce' ), + 'NT' => __( 'Neamț', 'woocommerce' ), + 'OT' => __( 'Olt', 'woocommerce' ), + 'PH' => __( 'Prahova', 'woocommerce' ), + 'SJ' => __( 'Sălaj', 'woocommerce' ), + 'SM' => __( 'Satu Mare', 'woocommerce' ), + 'SB' => __( 'Sibiu', 'woocommerce' ), + 'SV' => __( 'Suceava', 'woocommerce' ), + 'TR' => __( 'Teleorman', 'woocommerce' ), + 'TM' => __( 'Timiș', 'woocommerce' ), + 'TL' => __( 'Tulcea', 'woocommerce' ), + 'VL' => __( 'Vâlcea', 'woocommerce' ), + 'VS' => __( 'Vaslui', 'woocommerce' ), + 'VN' => __( 'Vrancea', 'woocommerce' ), + ), + 'SG' => array(), + 'SK' => array(), + 'SI' => array(), + 'SV' => array( // Salvadoran states. + 'SV-AH' => __( 'Ahuachapán', 'woocommerce' ), + 'SV-CA' => __( 'Cabañas', 'woocommerce' ), + 'SV-CH' => __( 'Chalatenango', 'woocommerce' ), + 'SV-CU' => __( 'Cuscatlán', 'woocommerce' ), + 'SV-LI' => __( 'La Libertad', 'woocommerce' ), + 'SV-MO' => __( 'Morazán', 'woocommerce' ), + 'SV-PA' => __( 'La Paz', 'woocommerce' ), + 'SV-SA' => __( 'Santa Ana', 'woocommerce' ), + 'SV-SM' => __( 'San Miguel', 'woocommerce' ), + 'SV-SO' => __( 'Sonsonate', 'woocommerce' ), + 'SV-SS' => __( 'San Salvador', 'woocommerce' ), + 'SV-SV' => __( 'San Vicente', 'woocommerce' ), + 'SV-UN' => __( 'La Unión', 'woocommerce' ), + 'SV-US' => __( 'Usulután', 'woocommerce' ), + ), + 'TH' => array( // Thai states. + 'TH-37' => __( 'Amnat Charoen', 'woocommerce' ), + 'TH-15' => __( 'Ang Thong', 'woocommerce' ), + 'TH-14' => __( 'Ayutthaya', 'woocommerce' ), + 'TH-10' => __( 'Bangkok', 'woocommerce' ), + 'TH-38' => __( 'Bueng Kan', 'woocommerce' ), + 'TH-31' => __( 'Buri Ram', 'woocommerce' ), + 'TH-24' => __( 'Chachoengsao', 'woocommerce' ), + 'TH-18' => __( 'Chai Nat', 'woocommerce' ), + 'TH-36' => __( 'Chaiyaphum', 'woocommerce' ), + 'TH-22' => __( 'Chanthaburi', 'woocommerce' ), + 'TH-50' => __( 'Chiang Mai', 'woocommerce' ), + 'TH-57' => __( 'Chiang Rai', 'woocommerce' ), + 'TH-20' => __( 'Chonburi', 'woocommerce' ), + 'TH-86' => __( 'Chumphon', 'woocommerce' ), + 'TH-46' => __( 'Kalasin', 'woocommerce' ), + 'TH-62' => __( 'Kamphaeng Phet', 'woocommerce' ), + 'TH-71' => __( 'Kanchanaburi', 'woocommerce' ), + 'TH-40' => __( 'Khon Kaen', 'woocommerce' ), + 'TH-81' => __( 'Krabi', 'woocommerce' ), + 'TH-52' => __( 'Lampang', 'woocommerce' ), + 'TH-51' => __( 'Lamphun', 'woocommerce' ), + 'TH-42' => __( 'Loei', 'woocommerce' ), + 'TH-16' => __( 'Lopburi', 'woocommerce' ), + 'TH-58' => __( 'Mae Hong Son', 'woocommerce' ), + 'TH-44' => __( 'Maha Sarakham', 'woocommerce' ), + 'TH-49' => __( 'Mukdahan', 'woocommerce' ), + 'TH-26' => __( 'Nakhon Nayok', 'woocommerce' ), + 'TH-73' => __( 'Nakhon Pathom', 'woocommerce' ), + 'TH-48' => __( 'Nakhon Phanom', 'woocommerce' ), + 'TH-30' => __( 'Nakhon Ratchasima', 'woocommerce' ), + 'TH-60' => __( 'Nakhon Sawan', 'woocommerce' ), + 'TH-80' => __( 'Nakhon Si Thammarat', 'woocommerce' ), + 'TH-55' => __( 'Nan', 'woocommerce' ), + 'TH-96' => __( 'Narathiwat', 'woocommerce' ), + 'TH-39' => __( 'Nong Bua Lam Phu', 'woocommerce' ), + 'TH-43' => __( 'Nong Khai', 'woocommerce' ), + 'TH-12' => __( 'Nonthaburi', 'woocommerce' ), + 'TH-13' => __( 'Pathum Thani', 'woocommerce' ), + 'TH-94' => __( 'Pattani', 'woocommerce' ), + 'TH-82' => __( 'Phang Nga', 'woocommerce' ), + 'TH-93' => __( 'Phatthalung', 'woocommerce' ), + 'TH-56' => __( 'Phayao', 'woocommerce' ), + 'TH-67' => __( 'Phetchabun', 'woocommerce' ), + 'TH-76' => __( 'Phetchaburi', 'woocommerce' ), + 'TH-66' => __( 'Phichit', 'woocommerce' ), + 'TH-65' => __( 'Phitsanulok', 'woocommerce' ), + 'TH-54' => __( 'Phrae', 'woocommerce' ), + 'TH-83' => __( 'Phuket', 'woocommerce' ), + 'TH-25' => __( 'Prachin Buri', 'woocommerce' ), + 'TH-77' => __( 'Prachuap Khiri Khan', 'woocommerce' ), + 'TH-85' => __( 'Ranong', 'woocommerce' ), + 'TH-70' => __( 'Ratchaburi', 'woocommerce' ), + 'TH-21' => __( 'Rayong', 'woocommerce' ), + 'TH-45' => __( 'Roi Et', 'woocommerce' ), + 'TH-27' => __( 'Sa Kaeo', 'woocommerce' ), + 'TH-47' => __( 'Sakon Nakhon', 'woocommerce' ), + 'TH-11' => __( 'Samut Prakan', 'woocommerce' ), + 'TH-74' => __( 'Samut Sakhon', 'woocommerce' ), + 'TH-75' => __( 'Samut Songkhram', 'woocommerce' ), + 'TH-19' => __( 'Saraburi', 'woocommerce' ), + 'TH-91' => __( 'Satun', 'woocommerce' ), + 'TH-17' => __( 'Sing Buri', 'woocommerce' ), + 'TH-33' => __( 'Sisaket', 'woocommerce' ), + 'TH-90' => __( 'Songkhla', 'woocommerce' ), + 'TH-64' => __( 'Sukhothai', 'woocommerce' ), + 'TH-72' => __( 'Suphan Buri', 'woocommerce' ), + 'TH-84' => __( 'Surat Thani', 'woocommerce' ), + 'TH-32' => __( 'Surin', 'woocommerce' ), + 'TH-63' => __( 'Tak', 'woocommerce' ), + 'TH-92' => __( 'Trang', 'woocommerce' ), + 'TH-23' => __( 'Trat', 'woocommerce' ), + 'TH-34' => __( 'Ubon Ratchathani', 'woocommerce' ), + 'TH-41' => __( 'Udon Thani', 'woocommerce' ), + 'TH-61' => __( 'Uthai Thani', 'woocommerce' ), + 'TH-53' => __( 'Uttaradit', 'woocommerce' ), + 'TH-95' => __( 'Yala', 'woocommerce' ), + 'TH-35' => __( 'Yasothon', 'woocommerce' ), + ), + 'TR' => array( // Turkish states. + 'TR01' => __( 'Adana', 'woocommerce' ), + 'TR02' => __( 'Adıyaman', 'woocommerce' ), + 'TR03' => __( 'Afyon', 'woocommerce' ), + 'TR04' => __( 'Ağrı', 'woocommerce' ), + 'TR05' => __( 'Amasya', 'woocommerce' ), + 'TR06' => __( 'Ankara', 'woocommerce' ), + 'TR07' => __( 'Antalya', 'woocommerce' ), + 'TR08' => __( 'Artvin', 'woocommerce' ), + 'TR09' => __( 'Aydın', 'woocommerce' ), + 'TR10' => __( 'Balıkesir', 'woocommerce' ), + 'TR11' => __( 'Bilecik', 'woocommerce' ), + 'TR12' => __( 'Bingöl', 'woocommerce' ), + 'TR13' => __( 'Bitlis', 'woocommerce' ), + 'TR14' => __( 'Bolu', 'woocommerce' ), + 'TR15' => __( 'Burdur', 'woocommerce' ), + 'TR16' => __( 'Bursa', 'woocommerce' ), + 'TR17' => __( 'Çanakkale', 'woocommerce' ), + 'TR18' => __( 'Çankırı', 'woocommerce' ), + 'TR19' => __( 'Çorum', 'woocommerce' ), + 'TR20' => __( 'Denizli', 'woocommerce' ), + 'TR21' => __( 'Diyarbakır', 'woocommerce' ), + 'TR22' => __( 'Edirne', 'woocommerce' ), + 'TR23' => __( 'Elazığ', 'woocommerce' ), + 'TR24' => __( 'Erzincan', 'woocommerce' ), + 'TR25' => __( 'Erzurum', 'woocommerce' ), + 'TR26' => __( 'Eskişehir', 'woocommerce' ), + 'TR27' => __( 'Gaziantep', 'woocommerce' ), + 'TR28' => __( 'Giresun', 'woocommerce' ), + 'TR29' => __( 'Gümüşhane', 'woocommerce' ), + 'TR30' => __( 'Hakkari', 'woocommerce' ), + 'TR31' => __( 'Hatay', 'woocommerce' ), + 'TR32' => __( 'Isparta', 'woocommerce' ), + 'TR33' => __( 'İçel', 'woocommerce' ), + 'TR34' => __( 'İstanbul', 'woocommerce' ), + 'TR35' => __( 'İzmir', 'woocommerce' ), + 'TR36' => __( 'Kars', 'woocommerce' ), + 'TR37' => __( 'Kastamonu', 'woocommerce' ), + 'TR38' => __( 'Kayseri', 'woocommerce' ), + 'TR39' => __( 'Kırklareli', 'woocommerce' ), + 'TR40' => __( 'Kırşehir', 'woocommerce' ), + 'TR41' => __( 'Kocaeli', 'woocommerce' ), + 'TR42' => __( 'Konya', 'woocommerce' ), + 'TR43' => __( 'Kütahya', 'woocommerce' ), + 'TR44' => __( 'Malatya', 'woocommerce' ), + 'TR45' => __( 'Manisa', 'woocommerce' ), + 'TR46' => __( 'Kahramanmaraş', 'woocommerce' ), + 'TR47' => __( 'Mardin', 'woocommerce' ), + 'TR48' => __( 'Muğla', 'woocommerce' ), + 'TR49' => __( 'Muş', 'woocommerce' ), + 'TR50' => __( 'Nevşehir', 'woocommerce' ), + 'TR51' => __( 'Niğde', 'woocommerce' ), + 'TR52' => __( 'Ordu', 'woocommerce' ), + 'TR53' => __( 'Rize', 'woocommerce' ), + 'TR54' => __( 'Sakarya', 'woocommerce' ), + 'TR55' => __( 'Samsun', 'woocommerce' ), + 'TR56' => __( 'Siirt', 'woocommerce' ), + 'TR57' => __( 'Sinop', 'woocommerce' ), + 'TR58' => __( 'Sivas', 'woocommerce' ), + 'TR59' => __( 'Tekirdağ', 'woocommerce' ), + 'TR60' => __( 'Tokat', 'woocommerce' ), + 'TR61' => __( 'Trabzon', 'woocommerce' ), + 'TR62' => __( 'Tunceli', 'woocommerce' ), + 'TR63' => __( 'Şanlıurfa', 'woocommerce' ), + 'TR64' => __( 'Uşak', 'woocommerce' ), + 'TR65' => __( 'Van', 'woocommerce' ), + 'TR66' => __( 'Yozgat', 'woocommerce' ), + 'TR67' => __( 'Zonguldak', 'woocommerce' ), + 'TR68' => __( 'Aksaray', 'woocommerce' ), + 'TR69' => __( 'Bayburt', 'woocommerce' ), + 'TR70' => __( 'Karaman', 'woocommerce' ), + 'TR71' => __( 'Kırıkkale', 'woocommerce' ), + 'TR72' => __( 'Batman', 'woocommerce' ), + 'TR73' => __( 'Şırnak', 'woocommerce' ), + 'TR74' => __( 'Bartın', 'woocommerce' ), + 'TR75' => __( 'Ardahan', 'woocommerce' ), + 'TR76' => __( 'Iğdır', 'woocommerce' ), + 'TR77' => __( 'Yalova', 'woocommerce' ), + 'TR78' => __( 'Karabük', 'woocommerce' ), + 'TR79' => __( 'Kilis', 'woocommerce' ), + 'TR80' => __( 'Osmaniye', 'woocommerce' ), + 'TR81' => __( 'Düzce', 'woocommerce' ), + ), + 'TZ' => array( // Tanzanian states. + 'TZ01' => __( 'Arusha', 'woocommerce' ), + 'TZ02' => __( 'Dar es Salaam', 'woocommerce' ), + 'TZ03' => __( 'Dodoma', 'woocommerce' ), + 'TZ04' => __( 'Iringa', 'woocommerce' ), + 'TZ05' => __( 'Kagera', 'woocommerce' ), + 'TZ06' => __( 'Pemba North', 'woocommerce' ), + 'TZ07' => __( 'Zanzibar North', 'woocommerce' ), + 'TZ08' => __( 'Kigoma', 'woocommerce' ), + 'TZ09' => __( 'Kilimanjaro', 'woocommerce' ), + 'TZ10' => __( 'Pemba South', 'woocommerce' ), + 'TZ11' => __( 'Zanzibar South', 'woocommerce' ), + 'TZ12' => __( 'Lindi', 'woocommerce' ), + 'TZ13' => __( 'Mara', 'woocommerce' ), + 'TZ14' => __( 'Mbeya', 'woocommerce' ), + 'TZ15' => __( 'Zanzibar West', 'woocommerce' ), + 'TZ16' => __( 'Morogoro', 'woocommerce' ), + 'TZ17' => __( 'Mtwara', 'woocommerce' ), + 'TZ18' => __( 'Mwanza', 'woocommerce' ), + 'TZ19' => __( 'Coast', 'woocommerce' ), + 'TZ20' => __( 'Rukwa', 'woocommerce' ), + 'TZ21' => __( 'Ruvuma', 'woocommerce' ), + 'TZ22' => __( 'Shinyanga', 'woocommerce' ), + 'TZ23' => __( 'Singida', 'woocommerce' ), + 'TZ24' => __( 'Tabora', 'woocommerce' ), + 'TZ25' => __( 'Tanga', 'woocommerce' ), + 'TZ26' => __( 'Manyara', 'woocommerce' ), + 'TZ27' => __( 'Geita', 'woocommerce' ), + 'TZ28' => __( 'Katavi', 'woocommerce' ), + 'TZ29' => __( 'Njombe', 'woocommerce' ), + 'TZ30' => __( 'Simiyu', 'woocommerce' ), + ), + 'LK' => array(), + 'RS' => array( // Serbian districts. + 'RS00' => _x( 'Belgrade', 'district', 'woocommerce' ), + 'RS14' => _x( 'Bor', 'district', 'woocommerce' ), + 'RS11' => _x( 'Braničevo', 'district', 'woocommerce' ), + 'RS02' => _x( 'Central Banat', 'district', 'woocommerce' ), + 'RS10' => _x( 'Danube', 'district', 'woocommerce' ), + 'RS23' => _x( 'Jablanica', 'district', 'woocommerce' ), + 'RS09' => _x( 'Kolubara', 'district', 'woocommerce' ), + 'RS08' => _x( 'Mačva', 'district', 'woocommerce' ), + 'RS17' => _x( 'Morava', 'district', 'woocommerce' ), + 'RS20' => _x( 'Nišava', 'district', 'woocommerce' ), + 'RS01' => _x( 'North Bačka', 'district', 'woocommerce' ), + 'RS03' => _x( 'North Banat', 'district', 'woocommerce' ), + 'RS24' => _x( 'Pčinja', 'district', 'woocommerce' ), + 'RS22' => _x( 'Pirot', 'district', 'woocommerce' ), + 'RS13' => _x( 'Pomoravlje', 'district', 'woocommerce' ), + 'RS19' => _x( 'Rasina', 'district', 'woocommerce' ), + 'RS18' => _x( 'Raška', 'district', 'woocommerce' ), + 'RS06' => _x( 'South Bačka', 'district', 'woocommerce' ), + 'RS04' => _x( 'South Banat', 'district', 'woocommerce' ), + 'RS07' => _x( 'Srem', 'district', 'woocommerce' ), + 'RS12' => _x( 'Šumadija', 'district', 'woocommerce' ), + 'RS21' => _x( 'Toplica', 'district', 'woocommerce' ), + 'RS05' => _x( 'West Bačka', 'district', 'woocommerce' ), + 'RS15' => _x( 'Zaječar', 'district', 'woocommerce' ), + 'RS16' => _x( 'Zlatibor', 'district', 'woocommerce' ), + 'RS25' => _x( 'Kosovo', 'district', 'woocommerce' ), + 'RS26' => _x( 'Peć', 'district', 'woocommerce' ), + 'RS27' => _x( 'Prizren', 'district', 'woocommerce' ), + 'RS28' => _x( 'Kosovska Mitrovica', 'district', 'woocommerce' ), + 'RS29' => _x( 'Kosovo-Pomoravlje', 'district', 'woocommerce' ), + 'RSKM' => _x( 'Kosovo-Metohija', 'district', 'woocommerce' ), + 'RSVO' => _x( 'Vojvodina', 'district', 'woocommerce' ), + ), + 'SE' => array(), + 'UA' => array( // Ukrainian oblasts. + 'VN' => __( 'Vinnytsia Oblast', 'woocommerce' ), + 'VL' => __( 'Volyn Oblast', 'woocommerce' ), + 'DP' => __( 'Dnipropetrovsk Oblast', 'woocommerce' ), + 'DT' => __( 'Donetsk Oblast', 'woocommerce' ), + 'ZT' => __( 'Zhytomyr Oblast', 'woocommerce' ), + 'ZK' => __( 'Zakarpattia Oblast', 'woocommerce' ), + 'ZP' => __( 'Zaporizhzhia Oblast', 'woocommerce' ), + 'IF' => __( 'Ivano-Frankivsk Oblast', 'woocommerce' ), + 'KV' => __( 'Kyiv Oblast', 'woocommerce' ), + 'KH' => __( 'Kirovohrad Oblast', 'woocommerce' ), + 'LH' => __( 'Luhansk Oblast', 'woocommerce' ), + 'LV' => __( 'Lviv Oblast', 'woocommerce' ), + 'MY' => __( 'Mykolaiv Oblast', 'woocommerce' ), + 'OD' => __( 'Odessa Oblast', 'woocommerce' ), + 'PL' => __( 'Poltava Oblast', 'woocommerce' ), + 'RV' => __( 'Rivne Oblast', 'woocommerce' ), + 'SM' => __( 'Sumy Oblast', 'woocommerce' ), + 'TP' => __( 'Ternopil Oblast', 'woocommerce' ), + 'KK' => __( 'Kharkiv Oblast', 'woocommerce' ), + 'KS' => __( 'Kherson Oblast', 'woocommerce' ), + 'KM' => __( 'Khmelnytskyi Oblast', 'woocommerce' ), + 'CK' => __( 'Cherkasy Oblast', 'woocommerce' ), + 'CH' => __( 'Chernihiv Oblast', 'woocommerce' ), + 'CV' => __( 'Chernivtsi Oblast', 'woocommerce' ), + ), + 'UG' => array( // Ugandan districts. + 'UG314' => __( 'Abim', 'woocommerce' ), + 'UG301' => __( 'Adjumani', 'woocommerce' ), + 'UG322' => __( 'Agago', 'woocommerce' ), + 'UG323' => __( 'Alebtong', 'woocommerce' ), + 'UG315' => __( 'Amolatar', 'woocommerce' ), + 'UG324' => __( 'Amudat', 'woocommerce' ), + 'UG216' => __( 'Amuria', 'woocommerce' ), + 'UG316' => __( 'Amuru', 'woocommerce' ), + 'UG302' => __( 'Apac', 'woocommerce' ), + 'UG303' => __( 'Arua', 'woocommerce' ), + 'UG217' => __( 'Budaka', 'woocommerce' ), + 'UG218' => __( 'Bududa', 'woocommerce' ), + 'UG201' => __( 'Bugiri', 'woocommerce' ), + 'UG235' => __( 'Bugweri', 'woocommerce' ), + 'UG420' => __( 'Buhweju', 'woocommerce' ), + 'UG117' => __( 'Buikwe', 'woocommerce' ), + 'UG219' => __( 'Bukedea', 'woocommerce' ), + 'UG118' => __( 'Bukomansimbi', 'woocommerce' ), + 'UG220' => __( 'Bukwa', 'woocommerce' ), + 'UG225' => __( 'Bulambuli', 'woocommerce' ), + 'UG416' => __( 'Buliisa', 'woocommerce' ), + 'UG401' => __( 'Bundibugyo', 'woocommerce' ), + 'UG430' => __( 'Bunyangabu', 'woocommerce' ), + 'UG402' => __( 'Bushenyi', 'woocommerce' ), + 'UG202' => __( 'Busia', 'woocommerce' ), + 'UG221' => __( 'Butaleja', 'woocommerce' ), + 'UG119' => __( 'Butambala', 'woocommerce' ), + 'UG233' => __( 'Butebo', 'woocommerce' ), + 'UG120' => __( 'Buvuma', 'woocommerce' ), + 'UG226' => __( 'Buyende', 'woocommerce' ), + 'UG317' => __( 'Dokolo', 'woocommerce' ), + 'UG121' => __( 'Gomba', 'woocommerce' ), + 'UG304' => __( 'Gulu', 'woocommerce' ), + 'UG403' => __( 'Hoima', 'woocommerce' ), + 'UG417' => __( 'Ibanda', 'woocommerce' ), + 'UG203' => __( 'Iganga', 'woocommerce' ), + 'UG418' => __( 'Isingiro', 'woocommerce' ), + 'UG204' => __( 'Jinja', 'woocommerce' ), + 'UG318' => __( 'Kaabong', 'woocommerce' ), + 'UG404' => __( 'Kabale', 'woocommerce' ), + 'UG405' => __( 'Kabarole', 'woocommerce' ), + 'UG213' => __( 'Kaberamaido', 'woocommerce' ), + 'UG427' => __( 'Kagadi', 'woocommerce' ), + 'UG428' => __( 'Kakumiro', 'woocommerce' ), + 'UG101' => __( 'Kalangala', 'woocommerce' ), + 'UG222' => __( 'Kaliro', 'woocommerce' ), + 'UG122' => __( 'Kalungu', 'woocommerce' ), + 'UG102' => __( 'Kampala', 'woocommerce' ), + 'UG205' => __( 'Kamuli', 'woocommerce' ), + 'UG413' => __( 'Kamwenge', 'woocommerce' ), + 'UG414' => __( 'Kanungu', 'woocommerce' ), + 'UG206' => __( 'Kapchorwa', 'woocommerce' ), + 'UG236' => __( 'Kapelebyong', 'woocommerce' ), + 'UG126' => __( 'Kasanda', 'woocommerce' ), + 'UG406' => __( 'Kasese', 'woocommerce' ), + 'UG207' => __( 'Katakwi', 'woocommerce' ), + 'UG112' => __( 'Kayunga', 'woocommerce' ), + 'UG407' => __( 'Kibaale', 'woocommerce' ), + 'UG103' => __( 'Kiboga', 'woocommerce' ), + 'UG227' => __( 'Kibuku', 'woocommerce' ), + 'UG432' => __( 'Kikuube', 'woocommerce' ), + 'UG419' => __( 'Kiruhura', 'woocommerce' ), + 'UG421' => __( 'Kiryandongo', 'woocommerce' ), + 'UG408' => __( 'Kisoro', 'woocommerce' ), + 'UG305' => __( 'Kitgum', 'woocommerce' ), + 'UG319' => __( 'Koboko', 'woocommerce' ), + 'UG325' => __( 'Kole', 'woocommerce' ), + 'UG306' => __( 'Kotido', 'woocommerce' ), + 'UG208' => __( 'Kumi', 'woocommerce' ), + 'UG333' => __( 'Kwania', 'woocommerce' ), + 'UG228' => __( 'Kween', 'woocommerce' ), + 'UG123' => __( 'Kyankwanzi', 'woocommerce' ), + 'UG422' => __( 'Kyegegwa', 'woocommerce' ), + 'UG415' => __( 'Kyenjojo', 'woocommerce' ), + 'UG125' => __( 'Kyotera', 'woocommerce' ), + 'UG326' => __( 'Lamwo', 'woocommerce' ), + 'UG307' => __( 'Lira', 'woocommerce' ), + 'UG229' => __( 'Luuka', 'woocommerce' ), + 'UG104' => __( 'Luwero', 'woocommerce' ), + 'UG124' => __( 'Lwengo', 'woocommerce' ), + 'UG114' => __( 'Lyantonde', 'woocommerce' ), + 'UG223' => __( 'Manafwa', 'woocommerce' ), + 'UG320' => __( 'Maracha', 'woocommerce' ), + 'UG105' => __( 'Masaka', 'woocommerce' ), + 'UG409' => __( 'Masindi', 'woocommerce' ), + 'UG214' => __( 'Mayuge', 'woocommerce' ), + 'UG209' => __( 'Mbale', 'woocommerce' ), + 'UG410' => __( 'Mbarara', 'woocommerce' ), + 'UG423' => __( 'Mitooma', 'woocommerce' ), + 'UG115' => __( 'Mityana', 'woocommerce' ), + 'UG308' => __( 'Moroto', 'woocommerce' ), + 'UG309' => __( 'Moyo', 'woocommerce' ), + 'UG106' => __( 'Mpigi', 'woocommerce' ), + 'UG107' => __( 'Mubende', 'woocommerce' ), + 'UG108' => __( 'Mukono', 'woocommerce' ), + 'UG334' => __( 'Nabilatuk', 'woocommerce' ), + 'UG311' => __( 'Nakapiripirit', 'woocommerce' ), + 'UG116' => __( 'Nakaseke', 'woocommerce' ), + 'UG109' => __( 'Nakasongola', 'woocommerce' ), + 'UG230' => __( 'Namayingo', 'woocommerce' ), + 'UG234' => __( 'Namisindwa', 'woocommerce' ), + 'UG224' => __( 'Namutumba', 'woocommerce' ), + 'UG327' => __( 'Napak', 'woocommerce' ), + 'UG310' => __( 'Nebbi', 'woocommerce' ), + 'UG231' => __( 'Ngora', 'woocommerce' ), + 'UG424' => __( 'Ntoroko', 'woocommerce' ), + 'UG411' => __( 'Ntungamo', 'woocommerce' ), + 'UG328' => __( 'Nwoya', 'woocommerce' ), + 'UG331' => __( 'Omoro', 'woocommerce' ), + 'UG329' => __( 'Otuke', 'woocommerce' ), + 'UG321' => __( 'Oyam', 'woocommerce' ), + 'UG312' => __( 'Pader', 'woocommerce' ), + 'UG332' => __( 'Pakwach', 'woocommerce' ), + 'UG210' => __( 'Pallisa', 'woocommerce' ), + 'UG110' => __( 'Rakai', 'woocommerce' ), + 'UG429' => __( 'Rubanda', 'woocommerce' ), + 'UG425' => __( 'Rubirizi', 'woocommerce' ), + 'UG431' => __( 'Rukiga', 'woocommerce' ), + 'UG412' => __( 'Rukungiri', 'woocommerce' ), + 'UG111' => __( 'Sembabule', 'woocommerce' ), + 'UG232' => __( 'Serere', 'woocommerce' ), + 'UG426' => __( 'Sheema', 'woocommerce' ), + 'UG215' => __( 'Sironko', 'woocommerce' ), + 'UG211' => __( 'Soroti', 'woocommerce' ), + 'UG212' => __( 'Tororo', 'woocommerce' ), + 'UG113' => __( 'Wakiso', 'woocommerce' ), + 'UG313' => __( 'Yumbe', 'woocommerce' ), + 'UG330' => __( 'Zombo', 'woocommerce' ), + ), + 'UM' => array( + '81' => __( 'Baker Island', 'woocommerce' ), + '84' => __( 'Howland Island', 'woocommerce' ), + '86' => __( 'Jarvis Island', 'woocommerce' ), + '67' => __( 'Johnston Atoll', 'woocommerce' ), + '89' => __( 'Kingman Reef', 'woocommerce' ), + '71' => __( 'Midway Atoll', 'woocommerce' ), + '76' => __( 'Navassa Island', 'woocommerce' ), + '95' => __( 'Palmyra Atoll', 'woocommerce' ), + '79' => __( 'Wake Island', 'woocommerce' ), + ), + 'US' => array( // U.S. states. + 'AL' => __( 'Alabama', 'woocommerce' ), + 'AK' => __( 'Alaska', 'woocommerce' ), + 'AZ' => __( 'Arizona', 'woocommerce' ), + 'AR' => __( 'Arkansas', 'woocommerce' ), + 'CA' => __( 'California', 'woocommerce' ), + 'CO' => __( 'Colorado', 'woocommerce' ), + 'CT' => __( 'Connecticut', 'woocommerce' ), + 'DE' => __( 'Delaware', 'woocommerce' ), + 'DC' => __( 'District Of Columbia', 'woocommerce' ), + 'FL' => __( 'Florida', 'woocommerce' ), + 'GA' => _x( 'Georgia', 'US state of Georgia', 'woocommerce' ), + 'HI' => __( 'Hawaii', 'woocommerce' ), + 'ID' => __( 'Idaho', 'woocommerce' ), + 'IL' => __( 'Illinois', 'woocommerce' ), + 'IN' => __( 'Indiana', 'woocommerce' ), + 'IA' => __( 'Iowa', 'woocommerce' ), + 'KS' => __( 'Kansas', 'woocommerce' ), + 'KY' => __( 'Kentucky', 'woocommerce' ), + 'LA' => __( 'Louisiana', 'woocommerce' ), + 'ME' => __( 'Maine', 'woocommerce' ), + 'MD' => __( 'Maryland', 'woocommerce' ), + 'MA' => __( 'Massachusetts', 'woocommerce' ), + 'MI' => __( 'Michigan', 'woocommerce' ), + 'MN' => __( 'Minnesota', 'woocommerce' ), + 'MS' => __( 'Mississippi', 'woocommerce' ), + 'MO' => __( 'Missouri', 'woocommerce' ), + 'MT' => __( 'Montana', 'woocommerce' ), + 'NE' => __( 'Nebraska', 'woocommerce' ), + 'NV' => __( 'Nevada', 'woocommerce' ), + 'NH' => __( 'New Hampshire', 'woocommerce' ), + 'NJ' => __( 'New Jersey', 'woocommerce' ), + 'NM' => __( 'New Mexico', 'woocommerce' ), + 'NY' => __( 'New York', 'woocommerce' ), + 'NC' => __( 'North Carolina', 'woocommerce' ), + 'ND' => __( 'North Dakota', 'woocommerce' ), + 'OH' => __( 'Ohio', 'woocommerce' ), + 'OK' => __( 'Oklahoma', 'woocommerce' ), + 'OR' => __( 'Oregon', 'woocommerce' ), + 'PA' => __( 'Pennsylvania', 'woocommerce' ), + 'RI' => __( 'Rhode Island', 'woocommerce' ), + 'SC' => __( 'South Carolina', 'woocommerce' ), + 'SD' => __( 'South Dakota', 'woocommerce' ), + 'TN' => __( 'Tennessee', 'woocommerce' ), + 'TX' => __( 'Texas', 'woocommerce' ), + 'UT' => __( 'Utah', 'woocommerce' ), + 'VT' => __( 'Vermont', 'woocommerce' ), + 'VA' => __( 'Virginia', 'woocommerce' ), + 'WA' => __( 'Washington', 'woocommerce' ), + 'WV' => __( 'West Virginia', 'woocommerce' ), + 'WI' => __( 'Wisconsin', 'woocommerce' ), + 'WY' => __( 'Wyoming', 'woocommerce' ), + 'AA' => __( 'Armed Forces (AA)', 'woocommerce' ), + 'AE' => __( 'Armed Forces (AE)', 'woocommerce' ), + 'AP' => __( 'Armed Forces (AP)', 'woocommerce' ), + ), + 'UY' => array( // Uruguayan states. + 'UY-AR' => __( 'Artigas', 'woocommerce' ), + 'UY-CA' => __( 'Canelones', 'woocommerce' ), + 'UY-CL' => __( 'Cerro Largo', 'woocommerce' ), + 'UY-CO' => __( 'Colonia', 'woocommerce' ), + 'UY-DU' => __( 'Durazno', 'woocommerce' ), + 'UY-FS' => __( 'Flores', 'woocommerce' ), + 'UY-FD' => __( 'Florida', 'woocommerce' ), + 'UY-LA' => __( 'Lavalleja', 'woocommerce' ), + 'UY-MA' => __( 'Maldonado', 'woocommerce' ), + 'UY-MO' => __( 'Montevideo', 'woocommerce' ), + 'UY-PA' => __( 'Paysandú', 'woocommerce' ), + 'UY-RN' => __( 'Río Negro', 'woocommerce' ), + 'UY-RV' => __( 'Rivera', 'woocommerce' ), + 'UY-RO' => __( 'Rocha', 'woocommerce' ), + 'UY-SA' => __( 'Salto', 'woocommerce' ), + 'UY-SJ' => __( 'San José', 'woocommerce' ), + 'UY-SO' => __( 'Soriano', 'woocommerce' ), + 'UY-TA' => __( 'Tacuarembó', 'woocommerce' ), + 'UY-TT' => __( 'Treinta y Tres', 'woocommerce' ), + ), + 'VE' => array( // Venezuelan states. + 'VE-A' => __( 'Capital', 'woocommerce' ), + 'VE-B' => __( 'Anzoátegui', 'woocommerce' ), + 'VE-C' => __( 'Apure', 'woocommerce' ), + 'VE-D' => __( 'Aragua', 'woocommerce' ), + 'VE-E' => __( 'Barinas', 'woocommerce' ), + 'VE-F' => __( 'Bolívar', 'woocommerce' ), + 'VE-G' => __( 'Carabobo', 'woocommerce' ), + 'VE-H' => __( 'Cojedes', 'woocommerce' ), + 'VE-I' => __( 'Falcón', 'woocommerce' ), + 'VE-J' => __( 'Guárico', 'woocommerce' ), + 'VE-K' => __( 'Lara', 'woocommerce' ), + 'VE-L' => __( 'Mérida', 'woocommerce' ), + 'VE-M' => __( 'Miranda', 'woocommerce' ), + 'VE-N' => __( 'Monagas', 'woocommerce' ), + 'VE-O' => __( 'Nueva Esparta', 'woocommerce' ), + 'VE-P' => __( 'Portuguesa', 'woocommerce' ), + 'VE-R' => __( 'Sucre', 'woocommerce' ), + 'VE-S' => __( 'Táchira', 'woocommerce' ), + 'VE-T' => __( 'Trujillo', 'woocommerce' ), + 'VE-U' => __( 'Yaracuy', 'woocommerce' ), + 'VE-V' => __( 'Zulia', 'woocommerce' ), + 'VE-W' => __( 'Federal Dependencies', 'woocommerce' ), + 'VE-X' => __( 'La Guaira (Vargas)', 'woocommerce' ), + 'VE-Y' => __( 'Delta Amacuro', 'woocommerce' ), + 'VE-Z' => __( 'Amazonas', 'woocommerce' ), + ), + 'VN' => array(), + 'YT' => array(), + 'ZA' => array( // South African states. + 'EC' => __( 'Eastern Cape', 'woocommerce' ), + 'FS' => __( 'Free State', 'woocommerce' ), + 'GP' => __( 'Gauteng', 'woocommerce' ), + 'KZN' => __( 'KwaZulu-Natal', 'woocommerce' ), + 'LP' => __( 'Limpopo', 'woocommerce' ), + 'MP' => __( 'Mpumalanga', 'woocommerce' ), + 'NC' => __( 'Northern Cape', 'woocommerce' ), + 'NW' => __( 'North West', 'woocommerce' ), + 'WC' => __( 'Western Cape', 'woocommerce' ), + ), + 'ZM' => array( // Zambian provinces. + 'ZM-01' => __( 'Western', 'woocommerce' ), + 'ZM-02' => __( 'Central', 'woocommerce' ), + 'ZM-03' => __( 'Eastern', 'woocommerce' ), + 'ZM-04' => __( 'Luapula', 'woocommerce' ), + 'ZM-05' => __( 'Northern', 'woocommerce' ), + 'ZM-06' => __( 'North-Western', 'woocommerce' ), + 'ZM-07' => __( 'Southern', 'woocommerce' ), + 'ZM-08' => __( 'Copperbelt', 'woocommerce' ), + 'ZM-09' => __( 'Lusaka', 'woocommerce' ), + 'ZM-10' => __( 'Muchinga', 'woocommerce' ), + ), +); diff --git a/plugins/woocommerce/includes/README.md b/plugins/woocommerce/includes/README.md new file mode 100644 index 00000000000..923d9b0b41a --- /dev/null +++ b/plugins/woocommerce/includes/README.md @@ -0,0 +1,34 @@ +# WooCommerce `includes` files + +This directory contains WooCommerce legacy code. Ideally, the code in this folder should only get the minimum required changes for bug fixing, and any new code should go in [the `src` directory](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/src/README.md) instead. + + +## Interacting with the `src` folder + +Whenever you need to get an instance of a class from the `src` directory, please don't instantiate it directly, but instead use [the container](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/src/README.md#the-container). To get an instance of the container itself you can use the `wc_get_container` function, for example: + +```php +$container = wc_get_container(); +$service = $container->get( \Automattic\WooCommerce\TheNamespace\TheService::class ); +$service->do_something(); +``` +The exception to this rule might be data-only classes that could be created the old way (using a plain `new` statement); but in general, all classes in the `src` directory are registered in the container and should be resolved using it. + + +## Adding new actions and filters + +Please take a look at [the considerations for creation new hooks in `src` code](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/src/README.md#defining-new-actions-and-filters), as they apply for `includes` code as well. The short version is that **new hooks should be introduced only if they provide a valuable extension point for plugins**, and not with the purpose of driving WooCommerce's internal logic. + + +## Writing unit tests + +[As it's the case for the `src` folder](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/src/README.md#writing-unit-tests), writing unit tests is generally mandatory if you are a WooCommerce team member or a contributor from another Automattic team, and encouraged if you are an external contributor. Tests should cover any new code (although as mentioned, adding new code in `includes` should be rare) and any modifications to existing code. + +In order to make it easier to write unit tests, there are a couple of mechanisms in place that you can use: + +* [The code hacker](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/Tools/CodeHacking/README.md). Pros: you don't need to do any special changes to your code to make it testable. Cons: it's a hack, the tested code is being actually modified while being loaded by the PHP engine, so not an ideal solution. + +* [The legacy proxy and the related helper methods in WC_Unit_Test_case](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/src/README.md#interacting-with-legacy-code): although these are intended in principle for writing tests for code in the `src` directory, they can be used for `includes` code as well. Pros: a clean approach, no hacks involved. Cons: you need to modify your code to use the proxy whenever you need to call a function or static method that makes the code difficult to test. + +It's up to you as a contributor to decide which mechanism to use in each case. Choose wisely. + diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-data.php b/plugins/woocommerce/includes/abstracts/abstract-wc-data.php new file mode 100644 index 00000000000..a9642de97c4 --- /dev/null +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-data.php @@ -0,0 +1,858 @@ +data = array_merge( $this->data, $this->extra_data ); + $this->default_data = $this->data; + } + + /** + * Only store the object ID to avoid serializing the data object instance. + * + * @return array + */ + public function __sleep() { + return array( 'id' ); + } + + /** + * Re-run the constructor with the object ID. + * + * If the object no longer exists, remove the ID. + */ + public function __wakeup() { + try { + $this->__construct( absint( $this->id ) ); + } catch ( Exception $e ) { + $this->set_id( 0 ); + $this->set_object_read( true ); + } + } + + /** + * When the object is cloned, make sure meta is duplicated correctly. + * + * @since 3.0.2 + */ + public function __clone() { + $this->maybe_read_meta_data(); + if ( ! empty( $this->meta_data ) ) { + foreach ( $this->meta_data as $array_key => $meta ) { + $this->meta_data[ $array_key ] = clone $meta; + if ( ! empty( $meta->id ) ) { + $this->meta_data[ $array_key ]->id = null; + } + } + } + } + + /** + * Get the data store. + * + * @since 3.0.0 + * @return object + */ + public function get_data_store() { + return $this->data_store; + } + + /** + * Returns the unique ID for this object. + * + * @since 2.6.0 + * @return int + */ + public function get_id() { + return $this->id; + } + + /** + * Delete an object, set the ID to 0, and return result. + * + * @since 2.6.0 + * @param bool $force_delete Should the date be deleted permanently. + * @return bool result + */ + public function delete( $force_delete = false ) { + if ( $this->data_store ) { + $this->data_store->delete( $this, array( 'force_delete' => $force_delete ) ); + $this->set_id( 0 ); + return true; + } + return false; + } + + /** + * Save should create or update based on object existence. + * + * @since 2.6.0 + * @return int + */ + public function save() { + if ( ! $this->data_store ) { + return $this->get_id(); + } + + /** + * Trigger action before saving to the DB. Allows you to adjust object props before save. + * + * @param WC_Data $this The object being saved. + * @param WC_Data_Store_WP $data_store THe data store persisting the data. + */ + do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store ); + + if ( $this->get_id() ) { + $this->data_store->update( $this ); + } else { + $this->data_store->create( $this ); + } + + /** + * Trigger action after saving to the DB. + * + * @param WC_Data $this The object being saved. + * @param WC_Data_Store_WP $data_store THe data store persisting the data. + */ + do_action( 'woocommerce_after_' . $this->object_type . '_object_save', $this, $this->data_store ); + + return $this->get_id(); + } + + /** + * Change data to JSON format. + * + * @since 2.6.0 + * @return string Data in JSON format. + */ + public function __toString() { + return wp_json_encode( $this->get_data() ); + } + + /** + * Returns all data for this object. + * + * @since 2.6.0 + * @return array + */ + public function get_data() { + return array_merge( array( 'id' => $this->get_id() ), $this->data, array( 'meta_data' => $this->get_meta_data() ) ); + } + + /** + * Returns array of expected data keys for this object. + * + * @since 3.0.0 + * @return array + */ + public function get_data_keys() { + return array_keys( $this->data ); + } + + /** + * Returns all "extra" data keys for an object (for sub objects like product types). + * + * @since 3.0.0 + * @return array + */ + public function get_extra_data_keys() { + return array_keys( $this->extra_data ); + } + + /** + * Filter null meta values from array. + * + * @since 3.0.0 + * @param mixed $meta Meta value to check. + * @return bool + */ + protected function filter_null_meta( $meta ) { + return ! is_null( $meta->value ); + } + + /** + * Get All Meta Data. + * + * @since 2.6.0 + * @return array of objects. + */ + public function get_meta_data() { + $this->maybe_read_meta_data(); + return array_values( array_filter( $this->meta_data, array( $this, 'filter_null_meta' ) ) ); + } + + /** + * Check if the key is an internal one. + * + * @since 3.2.0 + * @param string $key Key to check. + * @return bool true if it's an internal key, false otherwise + */ + protected function is_internal_meta_key( $key ) { + $internal_meta_key = ! empty( $key ) && $this->data_store && in_array( $key, $this->data_store->get_internal_meta_keys(), true ); + + if ( ! $internal_meta_key ) { + return false; + } + + $has_setter_or_getter = is_callable( array( $this, 'set_' . ltrim( $key, '_' ) ) ) || is_callable( array( $this, 'get_' . ltrim( $key, '_' ) ) ); + + if ( ! $has_setter_or_getter ) { + return false; + } + /* translators: %s: $key Key to check */ + wc_doing_it_wrong( __FUNCTION__, sprintf( __( 'Generic add/update/get meta methods should not be used for internal meta data, including "%s". Use getters and setters.', 'woocommerce' ), $key ), '3.2.0' ); + + return true; + } + + /** + * Get Meta Data by Key. + * + * @since 2.6.0 + * @param string $key Meta Key. + * @param bool $single return first found meta with key, or all with $key. + * @param string $context What the value is for. Valid values are view and edit. + * @return mixed + */ + public function get_meta( $key = '', $single = true, $context = 'view' ) { + if ( $this->is_internal_meta_key( $key ) ) { + $function = 'get_' . ltrim( $key, '_' ); + + if ( is_callable( array( $this, $function ) ) ) { + return $this->{$function}(); + } + } + + $this->maybe_read_meta_data(); + $meta_data = $this->get_meta_data(); + $array_keys = array_keys( wp_list_pluck( $meta_data, 'key' ), $key, true ); + $value = $single ? '' : array(); + + if ( ! empty( $array_keys ) ) { + // We don't use the $this->meta_data property directly here because we don't want meta with a null value (i.e. meta which has been deleted via $this->delete_meta_data()). + if ( $single ) { + $value = $meta_data[ current( $array_keys ) ]->value; + } else { + $value = array_intersect_key( $meta_data, array_flip( $array_keys ) ); + } + } + + if ( 'view' === $context ) { + $value = apply_filters( $this->get_hook_prefix() . $key, $value, $this ); + } + + return $value; + } + + /** + * See if meta data exists, since get_meta always returns a '' or array(). + * + * @since 3.0.0 + * @param string $key Meta Key. + * @return boolean + */ + public function meta_exists( $key = '' ) { + $this->maybe_read_meta_data(); + $array_keys = wp_list_pluck( $this->get_meta_data(), 'key' ); + return in_array( $key, $array_keys, true ); + } + + /** + * Set all meta data from array. + * + * @since 2.6.0 + * @param array $data Key/Value pairs. + */ + public function set_meta_data( $data ) { + if ( ! empty( $data ) && is_array( $data ) ) { + $this->maybe_read_meta_data(); + foreach ( $data as $meta ) { + $meta = (array) $meta; + if ( isset( $meta['key'], $meta['value'], $meta['id'] ) ) { + $this->meta_data[] = new WC_Meta_Data( + array( + 'id' => $meta['id'], + 'key' => $meta['key'], + 'value' => $meta['value'], + ) + ); + } + } + } + } + + /** + * Add meta data. + * + * @since 2.6.0 + * + * @param string $key Meta key. + * @param string|array $value Meta value. + * @param bool $unique Should this be a unique key?. + */ + public function add_meta_data( $key, $value, $unique = false ) { + if ( $this->is_internal_meta_key( $key ) ) { + $function = 'set_' . ltrim( $key, '_' ); + + if ( is_callable( array( $this, $function ) ) ) { + return $this->{$function}( $value ); + } + } + + $this->maybe_read_meta_data(); + if ( $unique ) { + $this->delete_meta_data( $key ); + } + $this->meta_data[] = new WC_Meta_Data( + array( + 'key' => $key, + 'value' => $value, + ) + ); + } + + /** + * Update meta data by key or ID, if provided. + * + * @since 2.6.0 + * + * @param string $key Meta key. + * @param string|array $value Meta value. + * @param int $meta_id Meta ID. + */ + public function update_meta_data( $key, $value, $meta_id = 0 ) { + if ( $this->is_internal_meta_key( $key ) ) { + $function = 'set_' . ltrim( $key, '_' ); + + if ( is_callable( array( $this, $function ) ) ) { + return $this->{$function}( $value ); + } + } + + $this->maybe_read_meta_data(); + + $array_key = false; + + if ( $meta_id ) { + $array_keys = array_keys( wp_list_pluck( $this->meta_data, 'id' ), $meta_id, true ); + $array_key = $array_keys ? current( $array_keys ) : false; + } else { + // Find matches by key. + $matches = array(); + foreach ( $this->meta_data as $meta_data_array_key => $meta ) { + if ( $meta->key === $key ) { + $matches[] = $meta_data_array_key; + } + } + + if ( ! empty( $matches ) ) { + // Set matches to null so only one key gets the new value. + foreach ( $matches as $meta_data_array_key ) { + $this->meta_data[ $meta_data_array_key ]->value = null; + } + $array_key = current( $matches ); + } + } + + if ( false !== $array_key ) { + $meta = $this->meta_data[ $array_key ]; + $meta->key = $key; + $meta->value = $value; + } else { + $this->add_meta_data( $key, $value, true ); + } + } + + /** + * Delete meta data. + * + * @since 2.6.0 + * @param string $key Meta key. + */ + public function delete_meta_data( $key ) { + $this->maybe_read_meta_data(); + $array_keys = array_keys( wp_list_pluck( $this->meta_data, 'key' ), $key, true ); + + if ( $array_keys ) { + foreach ( $array_keys as $array_key ) { + $this->meta_data[ $array_key ]->value = null; + } + } + } + + /** + * Delete meta data. + * + * @since 2.6.0 + * @param int $mid Meta ID. + */ + public function delete_meta_data_by_mid( $mid ) { + $this->maybe_read_meta_data(); + $array_keys = array_keys( wp_list_pluck( $this->meta_data, 'id' ), (int) $mid, true ); + + if ( $array_keys ) { + foreach ( $array_keys as $array_key ) { + $this->meta_data[ $array_key ]->value = null; + } + } + } + + /** + * Read meta data if null. + * + * @since 3.0.0 + */ + protected function maybe_read_meta_data() { + if ( is_null( $this->meta_data ) ) { + $this->read_meta_data(); + } + } + + /** + * Helper method to compute meta cache key. Different from WP Meta cache key in that meta data cached using this key also contains meta_id column. + * + * @since 4.7.0 + * + * @return string + */ + public function get_meta_cache_key() { + if ( ! $this->get_id() ) { + wc_doing_it_wrong( 'get_meta_cache_key', 'ID needs to be set before fetching a cache key.', '4.7.0' ); + return false; + } + return self::generate_meta_cache_key( $this->get_id(), $this->cache_group ); + } + + /** + * Generate cache key from id and group. + * + * @since 4.7.0 + * + * @param int|string $id Object ID. + * @param string $cache_group Group name use to store cache. Whole group cache can be invalidated in one go. + * + * @return string Meta cache key. + */ + public static function generate_meta_cache_key( $id, $cache_group ) { + return WC_Cache_Helper::get_cache_prefix( $cache_group ) . WC_Cache_Helper::get_cache_prefix( 'object_' . $id ) . 'object_meta_' . $id; + } + + /** + * Prime caches for raw meta data. This includes meta_id column as well, which is not included by default in WP meta data. + * + * @since 4.7.0 + * + * @param array $raw_meta_data_collection Array of objects of { object_id => array( meta_row_1, meta_row_2, ... }. + * @param string $cache_group Name of cache group. + */ + public static function prime_raw_meta_data_cache( $raw_meta_data_collection, $cache_group ) { + foreach ( $raw_meta_data_collection as $object_id => $raw_meta_data_array ) { + $cache_key = self::generate_meta_cache_key( $object_id, $cache_group ); + wp_cache_set( $cache_key, $raw_meta_data_array, $cache_group ); + } + } + + /** + * Read Meta Data from the database. Ignore any internal properties. + * Uses it's own caches because get_metadata does not provide meta_ids. + * + * @since 2.6.0 + * @param bool $force_read True to force a new DB read (and update cache). + */ + public function read_meta_data( $force_read = false ) { + $this->meta_data = array(); + $cache_loaded = false; + + if ( ! $this->get_id() ) { + return; + } + + if ( ! $this->data_store ) { + return; + } + + if ( ! empty( $this->cache_group ) ) { + // Prefix by group allows invalidation by group until https://core.trac.wordpress.org/ticket/4476 is implemented. + $cache_key = $this->get_meta_cache_key(); + } + + if ( ! $force_read ) { + if ( ! empty( $this->cache_group ) ) { + $cached_meta = wp_cache_get( $cache_key, $this->cache_group ); + $cache_loaded = is_array( $cached_meta ); + } + } + + // We filter the raw meta data again when loading from cache, in case we cached in an earlier version where filter conditions were different. + $raw_meta_data = $cache_loaded ? $this->data_store->filter_raw_meta_data( $this, $cached_meta ) : $this->data_store->read_meta( $this ); + + if ( is_array( $raw_meta_data ) ) { + foreach ( $raw_meta_data as $meta ) { + $this->meta_data[] = new WC_Meta_Data( + array( + 'id' => (int) $meta->meta_id, + 'key' => $meta->meta_key, + 'value' => maybe_unserialize( $meta->meta_value ), + ) + ); + } + + if ( ! $cache_loaded && ! empty( $this->cache_group ) ) { + wp_cache_set( $cache_key, $raw_meta_data, $this->cache_group ); + } + } + } + + /** + * Update Meta Data in the database. + * + * @since 2.6.0 + */ + public function save_meta_data() { + if ( ! $this->data_store || is_null( $this->meta_data ) ) { + return; + } + foreach ( $this->meta_data as $array_key => $meta ) { + if ( is_null( $meta->value ) ) { + if ( ! empty( $meta->id ) ) { + $this->data_store->delete_meta( $this, $meta ); + unset( $this->meta_data[ $array_key ] ); + } + } elseif ( empty( $meta->id ) ) { + $meta->id = $this->data_store->add_meta( $this, $meta ); + $meta->apply_changes(); + } else { + if ( $meta->get_changes() ) { + $this->data_store->update_meta( $this, $meta ); + $meta->apply_changes(); + } + } + } + if ( ! empty( $this->cache_group ) ) { + $cache_key = self::generate_meta_cache_key( $this->get_id(), $this->cache_group ); + wp_cache_delete( $cache_key, $this->cache_group ); + } + } + + /** + * Set ID. + * + * @since 3.0.0 + * @param int $id ID. + */ + public function set_id( $id ) { + $this->id = absint( $id ); + } + + /** + * Set all props to default values. + * + * @since 3.0.0 + */ + public function set_defaults() { + $this->data = $this->default_data; + $this->changes = array(); + $this->set_object_read( false ); + } + + /** + * Set object read property. + * + * @since 3.0.0 + * @param boolean $read Should read?. + */ + public function set_object_read( $read = true ) { + $this->object_read = (bool) $read; + } + + /** + * Get object read property. + * + * @since 3.0.0 + * @return boolean + */ + public function get_object_read() { + return (bool) $this->object_read; + } + + /** + * Set a collection of props in one go, collect any errors, and return the result. + * Only sets using public methods. + * + * @since 3.0.0 + * + * @param array $props Key value pairs to set. Key is the prop and should map to a setter function name. + * @param string $context In what context to run this. + * + * @return bool|WP_Error + */ + public function set_props( $props, $context = 'set' ) { + $errors = false; + + foreach ( $props as $prop => $value ) { + try { + /** + * Checks if the prop being set is allowed, and the value is not null. + */ + if ( is_null( $value ) || in_array( $prop, array( 'prop', 'date_prop', 'meta_data' ), true ) ) { + continue; + } + $setter = "set_$prop"; + + if ( is_callable( array( $this, $setter ) ) ) { + $this->{$setter}( $value ); + } + } catch ( WC_Data_Exception $e ) { + if ( ! $errors ) { + $errors = new WP_Error(); + } + $errors->add( $e->getErrorCode(), $e->getMessage() ); + } + } + + return $errors && count( $errors->get_error_codes() ) ? $errors : true; + } + + /** + * Sets a prop for a setter method. + * + * This stores changes in a special array so we can track what needs saving + * the the DB later. + * + * @since 3.0.0 + * @param string $prop Name of prop to set. + * @param mixed $value Value of the prop. + */ + protected function set_prop( $prop, $value ) { + if ( array_key_exists( $prop, $this->data ) ) { + if ( true === $this->object_read ) { + if ( $value !== $this->data[ $prop ] || array_key_exists( $prop, $this->changes ) ) { + $this->changes[ $prop ] = $value; + } + } else { + $this->data[ $prop ] = $value; + } + } + } + + /** + * Return data changes only. + * + * @since 3.0.0 + * @return array + */ + public function get_changes() { + return $this->changes; + } + + /** + * Merge changes with data and clear. + * + * @since 3.0.0 + */ + public function apply_changes() { + $this->data = array_replace_recursive( $this->data, $this->changes ); // @codingStandardsIgnoreLine + $this->changes = array(); + } + + /** + * Prefix for action and filter hooks on data. + * + * @since 3.0.0 + * @return string + */ + protected function get_hook_prefix() { + return 'woocommerce_' . $this->object_type . '_get_'; + } + + /** + * Gets a prop for a getter method. + * + * Gets the value from either current pending changes, or the data itself. + * Context controls what happens to the value before it's returned. + * + * @since 3.0.0 + * @param string $prop Name of prop to get. + * @param string $context What the value is for. Valid values are view and edit. + * @return mixed + */ + protected function get_prop( $prop, $context = 'view' ) { + $value = null; + + if ( array_key_exists( $prop, $this->data ) ) { + $value = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : $this->data[ $prop ]; + + if ( 'view' === $context ) { + $value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this ); + } + } + + return $value; + } + + /** + * Sets a date prop whilst handling formatting and datetime objects. + * + * @since 3.0.0 + * @param string $prop Name of prop to set. + * @param string|integer $value Value of the prop. + */ + protected function set_date_prop( $prop, $value ) { + try { + if ( empty( $value ) ) { + $this->set_prop( $prop, null ); + return; + } + + if ( is_a( $value, 'WC_DateTime' ) ) { + $datetime = $value; + } elseif ( is_numeric( $value ) ) { + // Timestamps are handled as UTC timestamps in all cases. + $datetime = new WC_DateTime( "@{$value}", new DateTimeZone( 'UTC' ) ); + } else { + // Strings are defined in local WP timezone. Convert to UTC. + if ( 1 === preg_match( '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(Z|((-|\+)\d{2}:\d{2}))$/', $value, $date_bits ) ) { + $offset = ! empty( $date_bits[7] ) ? iso8601_timezone_to_offset( $date_bits[7] ) : wc_timezone_offset(); + $timestamp = gmmktime( $date_bits[4], $date_bits[5], $date_bits[6], $date_bits[2], $date_bits[3], $date_bits[1] ) - $offset; + } else { + $timestamp = wc_string_to_timestamp( get_gmt_from_date( gmdate( 'Y-m-d H:i:s', wc_string_to_timestamp( $value ) ) ) ); + } + $datetime = new WC_DateTime( "@{$timestamp}", new DateTimeZone( 'UTC' ) ); + } + + // Set local timezone or offset. + if ( get_option( 'timezone_string' ) ) { + $datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) ); + } else { + $datetime->set_utc_offset( wc_timezone_offset() ); + } + + $this->set_prop( $prop, $datetime ); + } catch ( Exception $e ) {} // @codingStandardsIgnoreLine. + } + + /** + * When invalid data is found, throw an exception unless reading from the DB. + * + * @throws WC_Data_Exception Data Exception. + * @since 3.0.0 + * @param string $code Error code. + * @param string $message Error message. + * @param int $http_status_code HTTP status code. + * @param array $data Extra error data. + */ + protected function error( $code, $message, $http_status_code = 400, $data = array() ) { + throw new WC_Data_Exception( $code, $message, $http_status_code, $data ); + } +} diff --git a/includes/abstracts/abstract-wc-deprecated-hooks.php b/plugins/woocommerce/includes/abstracts/abstract-wc-deprecated-hooks.php similarity index 100% rename from includes/abstracts/abstract-wc-deprecated-hooks.php rename to plugins/woocommerce/includes/abstracts/abstract-wc-deprecated-hooks.php diff --git a/includes/abstracts/abstract-wc-integration.php b/plugins/woocommerce/includes/abstracts/abstract-wc-integration.php similarity index 100% rename from includes/abstracts/abstract-wc-integration.php rename to plugins/woocommerce/includes/abstracts/abstract-wc-integration.php diff --git a/includes/abstracts/abstract-wc-log-handler.php b/plugins/woocommerce/includes/abstracts/abstract-wc-log-handler.php similarity index 100% rename from includes/abstracts/abstract-wc-log-handler.php rename to plugins/woocommerce/includes/abstracts/abstract-wc-log-handler.php diff --git a/includes/abstracts/abstract-wc-object-query.php b/plugins/woocommerce/includes/abstracts/abstract-wc-object-query.php similarity index 100% rename from includes/abstracts/abstract-wc-object-query.php rename to plugins/woocommerce/includes/abstracts/abstract-wc-object-query.php diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php new file mode 100644 index 00000000000..83d59992a42 --- /dev/null +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php @@ -0,0 +1,2230 @@ + 0, + 'status' => '', + 'currency' => '', + 'version' => '', + 'prices_include_tax' => false, + 'date_created' => null, + 'date_modified' => null, + 'discount_total' => 0, + 'discount_tax' => 0, + 'shipping_total' => 0, + 'shipping_tax' => 0, + 'cart_tax' => 0, + 'total' => 0, + 'total_tax' => 0, + ); + + /** + * Order items will be stored here, sometimes before they persist in the DB. + * + * @since 3.0.0 + * @var array + */ + protected $items = array(); + + /** + * Order items that need deleting are stored here. + * + * @since 3.0.0 + * @var array + */ + protected $items_to_delete = array(); + + /** + * Stores meta in cache for future reads. + * + * A group must be set to to enable caching. + * + * @var string + */ + protected $cache_group = 'orders'; + + /** + * Which data store to load. + * + * @var string + */ + protected $data_store_name = 'order'; + + /** + * This is the name of this object type. + * + * @var string + */ + protected $object_type = 'order'; + + /** + * Get the order if ID is passed, otherwise the order is new and empty. + * This class should NOT be instantiated, but the wc_get_order function or new WC_Order_Factory + * should be used. It is possible, but the aforementioned are preferred and are the only + * methods that will be maintained going forward. + * + * @param int|object|WC_Order $order Order to read. + */ + public function __construct( $order = 0 ) { + parent::__construct( $order ); + + if ( is_numeric( $order ) && $order > 0 ) { + $this->set_id( $order ); + } elseif ( $order instanceof self ) { + $this->set_id( $order->get_id() ); + } elseif ( ! empty( $order->ID ) ) { + $this->set_id( $order->ID ); + } else { + $this->set_object_read( true ); + } + + $this->data_store = WC_Data_Store::load( $this->data_store_name ); + + if ( $this->get_id() > 0 ) { + $this->data_store->read( $this ); + } + } + + /** + * Get internal type. + * + * @return string + */ + public function get_type() { + return 'shop_order'; + } + + /** + * Get all class data in array format. + * + * @since 3.0.0 + * @return array + */ + public function get_data() { + return array_merge( + array( + 'id' => $this->get_id(), + ), + $this->data, + array( + 'meta_data' => $this->get_meta_data(), + 'line_items' => $this->get_items( 'line_item' ), + 'tax_lines' => $this->get_items( 'tax' ), + 'shipping_lines' => $this->get_items( 'shipping' ), + 'fee_lines' => $this->get_items( 'fee' ), + 'coupon_lines' => $this->get_items( 'coupon' ), + ) + ); + } + + /* + |-------------------------------------------------------------------------- + | CRUD methods + |-------------------------------------------------------------------------- + | + | Methods which create, read, update and delete orders from the database. + | Written in abstract fashion so that the way orders are stored can be + | changed more easily in the future. + | + | A save method is included for convenience (chooses update or create based + | on if the order exists yet). + | + */ + + /** + * Save data to the database. + * + * @since 3.0.0 + * @return int order ID + */ + public function save() { + if ( ! $this->data_store ) { + return $this->get_id(); + } + + try { + /** + * Trigger action before saving to the DB. Allows you to adjust object props before save. + * + * @param WC_Data $this The object being saved. + * @param WC_Data_Store_WP $data_store THe data store persisting the data. + */ + do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store ); + + if ( $this->get_id() ) { + $this->data_store->update( $this ); + } else { + $this->data_store->create( $this ); + } + + $this->save_items(); + + /** + * Trigger action after saving to the DB. + * + * @param WC_Data $this The object being saved. + * @param WC_Data_Store_WP $data_store THe data store persisting the data. + */ + do_action( 'woocommerce_after_' . $this->object_type . '_object_save', $this, $this->data_store ); + + } catch ( Exception $e ) { + $message_id = $this->get_id() ? $this->get_id() : __( '(no ID)', 'woocommerce' ); + $this->handle_exception( + $e, + wp_kses_post( + sprintf( + /* translators: 1: Order ID or "(no ID)" if not known. */ + __( 'Error saving order ID %1$s.', 'woocommerce' ), + $message_id + ) + ) + ); + } + + return $this->get_id(); + } + + /** + * Log an error about this order is exception is encountered. + * + * @param Exception $e Exception object. + * @param string $message Message regarding exception thrown. + * @since 3.7.0 + */ + protected function handle_exception( $e, $message = 'Error' ) { + wc_get_logger()->error( + $message, + array( + 'order' => $this, + 'error' => $e, + ) + ); + } + + /** + * Save all order items which are part of this order. + */ + protected function save_items() { + $items_changed = false; + + foreach ( $this->items_to_delete as $item ) { + $item->delete(); + $items_changed = true; + } + $this->items_to_delete = array(); + + // Add/save items. + foreach ( $this->items as $item_group => $items ) { + if ( is_array( $items ) ) { + $items = array_filter( $items ); + foreach ( $items as $item_key => $item ) { + $item->set_order_id( $this->get_id() ); + + $item_id = $item->save(); + + // If ID changed (new item saved to DB)... + if ( $item_id !== $item_key ) { + $this->items[ $item_group ][ $item_id ] = $item; + + unset( $this->items[ $item_group ][ $item_key ] ); + + $items_changed = true; + } + } + } + } + + if ( $items_changed ) { + delete_transient( 'wc_order_' . $this->get_id() . '_needs_processing' ); + } + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get parent order ID. + * + * @since 3.0.0 + * @param string $context View or edit context. + * @return integer + */ + public function get_parent_id( $context = 'view' ) { + return $this->get_prop( 'parent_id', $context ); + } + + /** + * Gets order currency. + * + * @param string $context View or edit context. + * @return string + */ + public function get_currency( $context = 'view' ) { + return $this->get_prop( 'currency', $context ); + } + + /** + * Get order_version. + * + * @param string $context View or edit context. + * @return string + */ + public function get_version( $context = 'view' ) { + return $this->get_prop( 'version', $context ); + } + + /** + * Get prices_include_tax. + * + * @param string $context View or edit context. + * @return bool + */ + public function get_prices_include_tax( $context = 'view' ) { + return $this->get_prop( 'prices_include_tax', $context ); + } + + /** + * Get date_created. + * + * @param string $context View or edit context. + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_created( $context = 'view' ) { + return $this->get_prop( 'date_created', $context ); + } + + /** + * Get date_modified. + * + * @param string $context View or edit context. + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_modified( $context = 'view' ) { + return $this->get_prop( 'date_modified', $context ); + } + + /** + * Return the order statuses without wc- internal prefix. + * + * @param string $context View or edit context. + * @return string + */ + public function get_status( $context = 'view' ) { + $status = $this->get_prop( 'status', $context ); + + if ( empty( $status ) && 'view' === $context ) { + // In view context, return the default status if no status has been set. + $status = apply_filters( 'woocommerce_default_order_status', 'pending' ); + } + return $status; + } + + /** + * Get discount_total. + * + * @param string $context View or edit context. + * @return string + */ + public function get_discount_total( $context = 'view' ) { + return $this->get_prop( 'discount_total', $context ); + } + + /** + * Get discount_tax. + * + * @param string $context View or edit context. + * @return string + */ + public function get_discount_tax( $context = 'view' ) { + return $this->get_prop( 'discount_tax', $context ); + } + + /** + * Get shipping_total. + * + * @param string $context View or edit context. + * @return string + */ + public function get_shipping_total( $context = 'view' ) { + return $this->get_prop( 'shipping_total', $context ); + } + + /** + * Get shipping_tax. + * + * @param string $context View or edit context. + * @return string + */ + public function get_shipping_tax( $context = 'view' ) { + return $this->get_prop( 'shipping_tax', $context ); + } + + /** + * Gets cart tax amount. + * + * @param string $context View or edit context. + * @return float + */ + public function get_cart_tax( $context = 'view' ) { + return $this->get_prop( 'cart_tax', $context ); + } + + /** + * Gets order grand total. incl. taxes. Used in gateways. + * + * @param string $context View or edit context. + * @return float + */ + public function get_total( $context = 'view' ) { + return $this->get_prop( 'total', $context ); + } + + /** + * Get total tax amount. Alias for get_order_tax(). + * + * @param string $context View or edit context. + * @return float + */ + public function get_total_tax( $context = 'view' ) { + return $this->get_prop( 'total_tax', $context ); + } + + /* + |-------------------------------------------------------------------------- + | Non-CRUD Getters + |-------------------------------------------------------------------------- + */ + + /** + * Gets the total discount amount. + * + * @param bool $ex_tax Show discount excl any tax. + * @return float + */ + public function get_total_discount( $ex_tax = true ) { + if ( $ex_tax ) { + $total_discount = $this->get_discount_total(); + } else { + $total_discount = $this->get_discount_total() + $this->get_discount_tax(); + } + return apply_filters( 'woocommerce_order_get_total_discount', NumberUtil::round( $total_discount, WC_ROUNDING_PRECISION ), $this ); + } + + /** + * Gets order subtotal. + * + * @return float + */ + public function get_subtotal() { + $subtotal = NumberUtil::round( $this->get_cart_subtotal_for_order(), wc_get_price_decimals() ); + return apply_filters( 'woocommerce_order_get_subtotal', (float) $subtotal, $this ); + } + + /** + * Get taxes, merged by code, formatted ready for output. + * + * @return array + */ + public function get_tax_totals() { + $tax_totals = array(); + + foreach ( $this->get_items( 'tax' ) as $key => $tax ) { + $code = $tax->get_rate_code(); + + if ( ! isset( $tax_totals[ $code ] ) ) { + $tax_totals[ $code ] = new stdClass(); + $tax_totals[ $code ]->amount = 0; + } + + $tax_totals[ $code ]->id = $key; + $tax_totals[ $code ]->rate_id = $tax->get_rate_id(); + $tax_totals[ $code ]->is_compound = $tax->is_compound(); + $tax_totals[ $code ]->label = $tax->get_label(); + $tax_totals[ $code ]->amount += (float) $tax->get_tax_total() + (float) $tax->get_shipping_tax_total(); + $tax_totals[ $code ]->formatted_amount = wc_price( $tax_totals[ $code ]->amount, array( 'currency' => $this->get_currency() ) ); + } + + if ( apply_filters( 'woocommerce_order_hide_zero_taxes', true ) ) { + $amounts = array_filter( wp_list_pluck( $tax_totals, 'amount' ) ); + $tax_totals = array_intersect_key( $tax_totals, $amounts ); + } + + return apply_filters( 'woocommerce_order_get_tax_totals', $tax_totals, $this ); + } + + /** + * Get all valid statuses for this order + * + * @since 3.0.0 + * @return array Internal status keys e.g. 'wc-processing' + */ + protected function get_valid_statuses() { + return array_keys( wc_get_order_statuses() ); + } + + /** + * Get user ID. Used by orders, not other order types like refunds. + * + * @param string $context View or edit context. + * @return int + */ + public function get_user_id( $context = 'view' ) { + return 0; + } + + /** + * Get user. Used by orders, not other order types like refunds. + * + * @return WP_User|false + */ + public function get_user() { + return false; + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + | + | Functions for setting order data. These should not update anything in the + | database itself and should only change what is stored in the class + | object. However, for backwards compatibility pre 3.0.0 some of these + | setters may handle both. + */ + + /** + * Set parent order ID. + * + * @since 3.0.0 + * @param int $value Value to set. + * @throws WC_Data_Exception Exception thrown if parent ID does not exist or is invalid. + */ + public function set_parent_id( $value ) { + if ( $value && ( $value === $this->get_id() || ! wc_get_order( $value ) ) ) { + $this->error( 'order_invalid_parent_id', __( 'Invalid parent ID', 'woocommerce' ) ); + } + $this->set_prop( 'parent_id', absint( $value ) ); + } + + /** + * Set order status. + * + * @since 3.0.0 + * @param string $new_status Status to change the order to. No internal wc- prefix is required. + * @return array details of change + */ + public function set_status( $new_status ) { + $old_status = $this->get_status(); + $new_status = 'wc-' === substr( $new_status, 0, 3 ) ? substr( $new_status, 3 ) : $new_status; + + $status_exceptions = array( 'auto-draft', 'trash' ); + + // If setting the status, ensure it's set to a valid status. + if ( true === $this->object_read ) { + // Only allow valid new status. + if ( ! in_array( 'wc-' . $new_status, $this->get_valid_statuses(), true ) && ! in_array( $new_status, $status_exceptions, true ) ) { + $new_status = 'pending'; + } + + // If the old status is set but unknown (e.g. draft) assume its pending for action usage. + if ( $old_status && ! in_array( 'wc-' . $old_status, $this->get_valid_statuses(), true ) && ! in_array( $old_status, $status_exceptions, true ) ) { + $old_status = 'pending'; + } + } + + $this->set_prop( 'status', $new_status ); + + return array( + 'from' => $old_status, + 'to' => $new_status, + ); + } + + /** + * Set order_version. + * + * @param string $value Value to set. + * @throws WC_Data_Exception Exception may be thrown if value is invalid. + */ + public function set_version( $value ) { + $this->set_prop( 'version', $value ); + } + + /** + * Set order_currency. + * + * @param string $value Value to set. + * @throws WC_Data_Exception Exception may be thrown if value is invalid. + */ + public function set_currency( $value ) { + if ( $value && ! in_array( $value, array_keys( get_woocommerce_currencies() ), true ) ) { + $this->error( 'order_invalid_currency', __( 'Invalid currency code', 'woocommerce' ) ); + } + $this->set_prop( 'currency', $value ? $value : get_woocommerce_currency() ); + } + + /** + * Set prices_include_tax. + * + * @param bool $value Value to set. + * @throws WC_Data_Exception Exception may be thrown if value is invalid. + */ + public function set_prices_include_tax( $value ) { + $this->set_prop( 'prices_include_tax', (bool) $value ); + } + + /** + * Set date_created. + * + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. + * @throws WC_Data_Exception Exception may be thrown if value is invalid. + */ + public function set_date_created( $date = null ) { + $this->set_date_prop( 'date_created', $date ); + } + + /** + * Set date_modified. + * + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. + * @throws WC_Data_Exception Exception may be thrown if value is invalid. + */ + public function set_date_modified( $date = null ) { + $this->set_date_prop( 'date_modified', $date ); + } + + /** + * Set discount_total. + * + * @param string $value Value to set. + * @throws WC_Data_Exception Exception may be thrown if value is invalid. + */ + public function set_discount_total( $value ) { + $this->set_prop( 'discount_total', wc_format_decimal( $value ) ); + } + + /** + * Set discount_tax. + * + * @param string $value Value to set. + * @throws WC_Data_Exception Exception may be thrown if value is invalid. + */ + public function set_discount_tax( $value ) { + $this->set_prop( 'discount_tax', wc_format_decimal( $value ) ); + } + + /** + * Set shipping_total. + * + * @param string $value Value to set. + * @throws WC_Data_Exception Exception may be thrown if value is invalid. + */ + public function set_shipping_total( $value ) { + $this->set_prop( 'shipping_total', wc_format_decimal( $value ) ); + } + + /** + * Set shipping_tax. + * + * @param string $value Value to set. + * @throws WC_Data_Exception Exception may be thrown if value is invalid. + */ + public function set_shipping_tax( $value ) { + $this->set_prop( 'shipping_tax', wc_format_decimal( $value ) ); + $this->set_total_tax( (float) $this->get_cart_tax() + (float) $this->get_shipping_tax() ); + } + + /** + * Set cart tax. + * + * @param string $value Value to set. + * @throws WC_Data_Exception Exception may be thrown if value is invalid. + */ + public function set_cart_tax( $value ) { + $this->set_prop( 'cart_tax', wc_format_decimal( $value ) ); + $this->set_total_tax( (float) $this->get_cart_tax() + (float) $this->get_shipping_tax() ); + } + + /** + * Sets order tax (sum of cart and shipping tax). Used internally only. + * + * @param string $value Value to set. + * @throws WC_Data_Exception Exception may be thrown if value is invalid. + */ + protected function set_total_tax( $value ) { + // We round here because this is a total entry, as opposed to line items in other setters. + $this->set_prop( 'total_tax', wc_format_decimal( NumberUtil::round( $value, wc_get_price_decimals() ) ) ); + } + + /** + * Set total. + * + * @param string $value Value to set. + * @param string $deprecated Function used to set different totals based on this. + * + * @return bool|void + * @throws WC_Data_Exception Exception may be thrown if value is invalid. + */ + public function set_total( $value, $deprecated = '' ) { + if ( $deprecated ) { + wc_deprecated_argument( 'total_type', '3.0', 'Use dedicated total setter methods instead.' ); + return $this->legacy_set_total( $value, $deprecated ); + } + $this->set_prop( 'total', wc_format_decimal( $value, wc_get_price_decimals() ) ); + } + + /* + |-------------------------------------------------------------------------- + | Order Item Handling + |-------------------------------------------------------------------------- + | + | Order items are used for products, taxes, shipping, and fees within + | each order. + */ + + /** + * Remove all line items (products, coupons, shipping, taxes) from the order. + * + * @param string $type Order item type. Default null. + */ + public function remove_order_items( $type = null ) { + if ( ! empty( $type ) ) { + $this->data_store->delete_items( $this, $type ); + + $group = $this->type_to_group( $type ); + + if ( $group ) { + unset( $this->items[ $group ] ); + } + } else { + $this->data_store->delete_items( $this ); + $this->items = array(); + } + } + + /** + * Convert a type to a types group. + * + * @param string $type type to lookup. + * @return string + */ + protected function type_to_group( $type ) { + $type_to_group = apply_filters( + 'woocommerce_order_type_to_group', + array( + 'line_item' => 'line_items', + 'tax' => 'tax_lines', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ) + ); + return isset( $type_to_group[ $type ] ) ? $type_to_group[ $type ] : ''; + } + + /** + * Return an array of items/products within this order. + * + * @param string|array $types Types of line items to get (array or string). + * @return WC_Order_Item[] + */ + public function get_items( $types = 'line_item' ) { + $items = array(); + $types = array_filter( (array) $types ); + + foreach ( $types as $type ) { + $group = $this->type_to_group( $type ); + + if ( $group ) { + if ( ! isset( $this->items[ $group ] ) ) { + $this->items[ $group ] = array_filter( $this->data_store->read_items( $this, $type ) ); + } + // Don't use array_merge here because keys are numeric. + $items = $items + $this->items[ $group ]; + } + } + + return apply_filters( 'woocommerce_order_get_items', $items, $this, $types ); + } + + /** + * Return array of values for calculations. + * + * @param string $field Field name to return. + * + * @return array Array of values. + */ + protected function get_values_for_total( $field ) { + $items = array_map( + function ( $item ) use ( $field ) { + return wc_add_number_precision( $item[ $field ], false ); + }, + array_values( $this->get_items() ) + ); + return $items; + } + + /** + * Return an array of coupons within this order. + * + * @since 3.7.0 + * @return WC_Order_Item_Coupon[] + */ + public function get_coupons() { + return $this->get_items( 'coupon' ); + } + + /** + * Return an array of fees within this order. + * + * @return WC_Order_item_Fee[] + */ + public function get_fees() { + return $this->get_items( 'fee' ); + } + + /** + * Return an array of taxes within this order. + * + * @return WC_Order_Item_Tax[] + */ + public function get_taxes() { + return $this->get_items( 'tax' ); + } + + /** + * Return an array of shipping costs within this order. + * + * @return WC_Order_Item_Shipping[] + */ + public function get_shipping_methods() { + return $this->get_items( 'shipping' ); + } + + /** + * Gets formatted shipping method title. + * + * @return string + */ + public function get_shipping_method() { + $names = array(); + foreach ( $this->get_shipping_methods() as $shipping_method ) { + $names[] = $shipping_method->get_name(); + } + return apply_filters( 'woocommerce_order_shipping_method', implode( ', ', $names ), $this ); + } + + /** + * Get used coupon codes only. + * + * @since 3.7.0 + * @return array + */ + public function get_coupon_codes() { + $coupon_codes = array(); + $coupons = $this->get_items( 'coupon' ); + + if ( $coupons ) { + foreach ( $coupons as $coupon ) { + $coupon_codes[] = $coupon->get_code(); + } + } + return $coupon_codes; + } + + /** + * Gets the count of order items of a certain type. + * + * @param string $item_type Item type to lookup. + * @return int|string + */ + public function get_item_count( $item_type = '' ) { + $items = $this->get_items( empty( $item_type ) ? 'line_item' : $item_type ); + $count = 0; + + foreach ( $items as $item ) { + $count += $item->get_quantity(); + } + + return apply_filters( 'woocommerce_get_item_count', $count, $item_type, $this ); + } + + /** + * Get an order item object, based on its type. + * + * @since 3.0.0 + * @param int $item_id ID of item to get. + * @param bool $load_from_db Prior to 3.2 this item was loaded direct from WC_Order_Factory, not this object. This param is here for backwards compatibility with that. If false, uses the local items variable instead. + * @return WC_Order_Item|false + */ + public function get_item( $item_id, $load_from_db = true ) { + if ( $load_from_db ) { + return WC_Order_Factory::get_order_item( $item_id ); + } + + // Search for item id. + if ( $this->items ) { + foreach ( $this->items as $group => $items ) { + if ( isset( $items[ $item_id ] ) ) { + return $items[ $item_id ]; + } + } + } + + // Load all items of type and cache. + $type = $this->data_store->get_order_item_type( $this, $item_id ); + + if ( ! $type ) { + return false; + } + + $items = $this->get_items( $type ); + + return ! empty( $items[ $item_id ] ) ? $items[ $item_id ] : false; + } + + /** + * Get key for where a certain item type is stored in _items. + * + * @since 3.0.0 + * @param string $item object Order item (product, shipping, fee, coupon, tax). + * @return string + */ + protected function get_items_key( $item ) { + if ( is_a( $item, 'WC_Order_Item_Product' ) ) { + return 'line_items'; + } elseif ( is_a( $item, 'WC_Order_Item_Fee' ) ) { + return 'fee_lines'; + } elseif ( is_a( $item, 'WC_Order_Item_Shipping' ) ) { + return 'shipping_lines'; + } elseif ( is_a( $item, 'WC_Order_Item_Tax' ) ) { + return 'tax_lines'; + } elseif ( is_a( $item, 'WC_Order_Item_Coupon' ) ) { + return 'coupon_lines'; + } + return apply_filters( 'woocommerce_get_items_key', '', $item ); + } + + /** + * Remove item from the order. + * + * @param int $item_id Item ID to delete. + * @return false|void + */ + public function remove_item( $item_id ) { + $item = $this->get_item( $item_id, false ); + $items_key = $item ? $this->get_items_key( $item ) : false; + + if ( ! $items_key ) { + return false; + } + + // Unset and remove later. + $this->items_to_delete[] = $item; + unset( $this->items[ $items_key ][ $item->get_id() ] ); + } + + /** + * Adds an order item to this order. The order item will not persist until save. + * + * @since 3.0.0 + * @param WC_Order_Item $item Order item object (product, shipping, fee, coupon, tax). + * @return false|void + */ + public function add_item( $item ) { + $items_key = $this->get_items_key( $item ); + + if ( ! $items_key ) { + return false; + } + + // Make sure existing items are loaded so we can append this new one. + if ( ! isset( $this->items[ $items_key ] ) ) { + $this->items[ $items_key ] = $this->get_items( $item->get_type() ); + } + + // Set parent. + $item->set_order_id( $this->get_id() ); + + // Append new row with generated temporary ID. + $item_id = $item->get_id(); + + if ( $item_id ) { + $this->items[ $items_key ][ $item_id ] = $item; + } else { + $this->items[ $items_key ][ 'new:' . $items_key . count( $this->items[ $items_key ] ) ] = $item; + } + } + + /** + * Check and records coupon usage tentatively so that counts validation is correct. Display an error if coupon usage limit has been reached. + * + * If you are using this method, make sure to `release_held_coupons` in case an Exception is thrown. + * + * @throws Exception When not able to apply coupon. + * + * @param string $billing_email Billing email of order. + */ + public function hold_applied_coupons( $billing_email ) { + $held_keys = array(); + $held_keys_for_user = array(); + $error = null; + + try { + foreach ( WC()->cart->get_applied_coupons() as $code ) { + $coupon = new WC_Coupon( $code ); + if ( ! $coupon->get_data_store() ) { + continue; + } + + // Hold coupon for when global coupon usage limit is present. + if ( 0 < $coupon->get_usage_limit() ) { + $held_key = $this->hold_coupon( $coupon ); + if ( $held_key ) { + $held_keys[ $coupon->get_id() ] = $held_key; + } + } + + // Hold coupon for when usage limit per customer is enabled. + if ( 0 < $coupon->get_usage_limit_per_user() ) { + + if ( ! isset( $user_ids_and_emails ) ) { + $user_alias = get_current_user_id() ? wp_get_current_user()->ID : sanitize_email( $billing_email ); + $user_ids_and_emails = $this->get_billing_and_current_user_aliases( $billing_email ); + } + + $held_key_for_user = $this->hold_coupon_for_users( $coupon, $user_ids_and_emails, $user_alias ); + + if ( $held_key_for_user ) { + $held_keys_for_user[ $coupon->get_id() ] = $held_key_for_user; + } + } + } + } catch ( Exception $e ) { + $error = $e; + } finally { + // Even in case of error, we will save keys for whatever coupons that were held so our data remains accurate. + // We save them in bulk instead of one by one for performance reasons. + if ( 0 < count( $held_keys_for_user ) || 0 < count( $held_keys ) ) { + $this->get_data_store()->set_coupon_held_keys( $this, $held_keys, $held_keys_for_user ); + } + if ( $error instanceof Exception ) { + throw $error; + } + } + } + + + /** + * Hold coupon if a global usage limit is defined. + * + * @param WC_Coupon $coupon Coupon object. + * + * @return string Meta key which indicates held coupon. + * @throws Exception When can't be held. + */ + private function hold_coupon( $coupon ) { + $result = $coupon->get_data_store()->check_and_hold_coupon( $coupon ); + if ( false === $result ) { + // translators: Actual coupon code. + throw new Exception( sprintf( __( 'An unexpected error happened while applying the Coupon %s.', 'woocommerce' ), esc_html( $coupon->get_code() ) ) ); + } elseif ( 0 === $result ) { + // translators: Actual coupon code. + throw new Exception( sprintf( __( 'Coupon %s was used in another transaction during this checkout, and coupon usage limit is reached. Please remove the coupon and try again.', 'woocommerce' ), esc_html( $coupon->get_code() ) ) ); + } + return $result; + } + + /** + * Hold coupon if usage limit per customer is defined. + * + * @param WC_Coupon $coupon Coupon object. + * @param array $user_ids_and_emails Array of user Id and emails to check for usage limit. + * @param string $user_alias User ID or email to use to record current usage. + * + * @return string Meta key which indicates held coupon. + * @throws Exception When coupon can't be held. + */ + private function hold_coupon_for_users( $coupon, $user_ids_and_emails, $user_alias ) { + $result = $coupon->get_data_store()->check_and_hold_coupon_for_user( $coupon, $user_ids_and_emails, $user_alias ); + if ( false === $result ) { + // translators: Actual coupon code. + throw new Exception( sprintf( __( 'An unexpected error happened while applying the Coupon %s.', 'woocommerce' ), esc_html( $coupon->get_code() ) ) ); + } elseif ( 0 === $result ) { + // translators: Actual coupon code. + throw new Exception( sprintf( __( 'You have used this coupon %s in another transaction during this checkout, and coupon usage limit is reached. Please remove the coupon and try again.', 'woocommerce' ), esc_html( $coupon->get_code() ) ) ); + } + return $result; + } + + /** + * Helper method to get all aliases for current user and provide billing email. + * + * @param string $billing_email Billing email provided in form. + * + * @return array Array of all aliases. + * @throws Exception When validation fails. + */ + private function get_billing_and_current_user_aliases( $billing_email ) { + $emails = array( $billing_email ); + if ( get_current_user_id() ) { + $emails[] = wp_get_current_user()->user_email; + } + $emails = array_unique( + array_map( 'strtolower', array_map( 'sanitize_email', $emails ) ) + ); + $customer_data_store = WC_Data_Store::load( 'customer' ); + $user_ids = $customer_data_store->get_user_ids_for_billing_email( $emails ); + return array_merge( $user_ids, $emails ); + } + + /** + * Apply a coupon to the order and recalculate totals. + * + * @since 3.2.0 + * @param string|WC_Coupon $raw_coupon Coupon code or object. + * @return true|WP_Error True if applied, error if not. + */ + public function apply_coupon( $raw_coupon ) { + if ( is_a( $raw_coupon, 'WC_Coupon' ) ) { + $coupon = $raw_coupon; + } elseif ( is_string( $raw_coupon ) ) { + $code = wc_format_coupon_code( $raw_coupon ); + $coupon = new WC_Coupon( $code ); + + if ( $coupon->get_code() !== $code ) { + return new WP_Error( 'invalid_coupon', __( 'Invalid coupon code', 'woocommerce' ) ); + } + } else { + return new WP_Error( 'invalid_coupon', __( 'Invalid coupon', 'woocommerce' ) ); + } + + // Check to make sure coupon is not already applied. + $applied_coupons = $this->get_items( 'coupon' ); + foreach ( $applied_coupons as $applied_coupon ) { + if ( $applied_coupon->get_code() === $coupon->get_code() ) { + return new WP_Error( 'invalid_coupon', __( 'Coupon code already applied!', 'woocommerce' ) ); + } + } + + $discounts = new WC_Discounts( $this ); + $applied = $discounts->apply_coupon( $coupon ); + + if ( is_wp_error( $applied ) ) { + return $applied; + } + + $data_store = $coupon->get_data_store(); + + // Check specific for guest checkouts here as well since WC_Cart handles that separately in check_customer_coupons. + if ( $data_store && 0 === $this->get_customer_id() ) { + $usage_count = $data_store->get_usage_by_email( $coupon, $this->get_billing_email() ); + if ( 0 < $coupon->get_usage_limit_per_user() && $usage_count >= $coupon->get_usage_limit_per_user() ) { + return new WP_Error( + 'invalid_coupon', + $coupon->get_coupon_error( 106 ), + array( + 'status' => 400, + ) + ); + } + } + + $this->set_coupon_discount_amounts( $discounts ); + $this->save(); + + // Recalculate totals and taxes. + $this->recalculate_coupons(); + + // Record usage so counts and validation is correct. + $used_by = $this->get_user_id(); + + if ( ! $used_by ) { + $used_by = $this->get_billing_email(); + } + + $order_data_store = $this->get_data_store(); + if ( $order_data_store->get_recorded_coupon_usage_counts( $this ) ) { + $coupon->increase_usage_count( $used_by ); + } + + wc_update_coupon_usage_counts( $this->get_id() ); + + return true; + } + + /** + * Remove a coupon from the order and recalculate totals. + * + * Coupons affect line item totals, but there is no relationship between + * coupon and line total, so to remove a coupon we need to work from the + * line subtotal (price before discount) and re-apply all coupons in this + * order. + * + * Manual discounts are not affected; those are separate and do not affect + * stored line totals. + * + * @since 3.2.0 + * @param string $code Coupon code. + * @return void + */ + public function remove_coupon( $code ) { + $coupons = $this->get_items( 'coupon' ); + + // Remove the coupon line. + foreach ( $coupons as $item_id => $coupon ) { + if ( $coupon->get_code() === $code ) { + $this->remove_item( $item_id ); + $coupon_object = new WC_Coupon( $code ); + $coupon_object->decrease_usage_count( $this->get_user_id() ); + $this->recalculate_coupons(); + break; + } + } + } + + /** + * Apply all coupons in this order again to all line items. + * This method is public since WooCommerce 3.8.0. + * + * @since 3.2.0 + */ + public function recalculate_coupons() { + // Reset line item totals. + foreach ( $this->get_items() as $item ) { + $item->set_total( $item->get_subtotal() ); + $item->set_total_tax( $item->get_subtotal_tax() ); + } + + $discounts = new WC_Discounts( $this ); + + foreach ( $this->get_items( 'coupon' ) as $coupon_item ) { + $coupon_code = $coupon_item->get_code(); + $coupon_id = wc_get_coupon_id_by_code( $coupon_code ); + + // If we have a coupon ID (loaded via wc_get_coupon_id_by_code) we can simply load the new coupon object using the ID. + if ( $coupon_id ) { + $coupon_object = new WC_Coupon( $coupon_id ); + + } else { + + // If we do not have a coupon ID (was it virtual? has it been deleted?) we must create a temporary coupon using what data we have stored during checkout. + $coupon_object = new WC_Coupon(); + $coupon_object->set_props( (array) $coupon_item->get_meta( 'coupon_data', true ) ); + $coupon_object->set_code( $coupon_code ); + $coupon_object->set_virtual( true ); + + // If there is no coupon amount (maybe dynamic?), set it to the given **discount** amount so the coupon's same value is applied. + if ( ! $coupon_object->get_amount() ) { + + // If the order originally had prices including tax, remove the discount + discount tax. + if ( $this->get_prices_include_tax() ) { + $coupon_object->set_amount( $coupon_item->get_discount() + $coupon_item->get_discount_tax() ); + } else { + $coupon_object->set_amount( $coupon_item->get_discount() ); + } + $coupon_object->set_discount_type( 'fixed_cart' ); + } + } + + /** + * Allow developers to filter this coupon before it gets re-applied to the order. + * + * @since 3.2.0 + */ + $coupon_object = apply_filters( 'woocommerce_order_recalculate_coupons_coupon_object', $coupon_object, $coupon_code, $coupon_item, $this ); + + if ( $coupon_object ) { + $discounts->apply_coupon( $coupon_object, false ); + } + } + + $this->set_coupon_discount_amounts( $discounts ); + $this->set_item_discount_amounts( $discounts ); + + // Recalculate totals and taxes. + $this->calculate_totals( true ); + } + + /** + * After applying coupons via the WC_Discounts class, update line items. + * + * @since 3.2.0 + * @param WC_Discounts $discounts Discounts class. + */ + protected function set_item_discount_amounts( $discounts ) { + $item_discounts = $discounts->get_discounts_by_item(); + $tax_location = $this->get_tax_location(); + $tax_location = array( $tax_location['country'], $tax_location['state'], $tax_location['postcode'], $tax_location['city'] ); + + if ( $item_discounts ) { + foreach ( $item_discounts as $item_id => $amount ) { + $item = $this->get_item( $item_id, false ); + + // If the prices include tax, discounts should be taken off the tax inclusive prices like in the cart. + if ( $this->get_prices_include_tax() && wc_tax_enabled() && 'taxable' === $item->get_tax_status() ) { + $taxes = WC_Tax::calc_tax( $amount, $this->get_tax_rates( $item->get_tax_class(), $tax_location ), true ); + + // Use unrounded taxes so totals will be re-calculated accurately, like in cart. + $amount = $amount - array_sum( $taxes ); + } + + $item->set_total( max( 0, $item->get_total() - $amount ) ); + } + } + } + + /** + * After applying coupons via the WC_Discounts class, update or create coupon items. + * + * @since 3.2.0 + * @param WC_Discounts $discounts Discounts class. + */ + protected function set_coupon_discount_amounts( $discounts ) { + $coupons = $this->get_items( 'coupon' ); + $coupon_code_to_id = wc_list_pluck( $coupons, 'get_id', 'get_code' ); + $all_discounts = $discounts->get_discounts(); + $coupon_discounts = $discounts->get_discounts_by_coupon(); + $tax_location = $this->get_tax_location(); + $tax_location = array( + $tax_location['country'], + $tax_location['state'], + $tax_location['postcode'], + $tax_location['city'], + ); + + if ( $coupon_discounts ) { + foreach ( $coupon_discounts as $coupon_code => $amount ) { + $item_id = isset( $coupon_code_to_id[ $coupon_code ] ) ? $coupon_code_to_id[ $coupon_code ] : 0; + + if ( ! $item_id ) { + $coupon_item = new WC_Order_Item_Coupon(); + $coupon_item->set_code( $coupon_code ); + + // Add coupon data. + $coupon_id = wc_get_coupon_id_by_code( $coupon_code ); + $coupon = new WC_Coupon( $coupon_id ); + + // Avoid storing used_by - it's not needed and can get large. + $coupon_data = $coupon->get_data(); + unset( $coupon_data['used_by'] ); + + $coupon_item->add_meta_data( 'coupon_data', $coupon_data ); + } else { + $coupon_item = $this->get_item( $item_id, false ); + } + + $discount_tax = 0; + + // Work out how much tax has been removed as a result of the discount from this coupon. + foreach ( $all_discounts[ $coupon_code ] as $item_id => $item_discount_amount ) { + $item = $this->get_item( $item_id, false ); + + if ( 'taxable' !== $item->get_tax_status() || ! wc_tax_enabled() ) { + continue; + } + + $taxes = array_sum( WC_Tax::calc_tax( $item_discount_amount, $this->get_tax_rates( $item->get_tax_class(), $tax_location ), $this->get_prices_include_tax() ) ); + if ( 'yes' !== get_option( 'woocommerce_tax_round_at_subtotal' ) ) { + $taxes = wc_round_tax_total( $taxes ); + } + + $discount_tax += $taxes; + + if ( $this->get_prices_include_tax() ) { + $amount = $amount - $taxes; + } + } + + $coupon_item->set_discount( $amount ); + $coupon_item->set_discount_tax( $discount_tax ); + + $this->add_item( $coupon_item ); + } + } + } + + /** + * Add a product line item to the order. This is the only line item type with + * its own method because it saves looking up order amounts (costs are added up for you). + * + * @param WC_Product $product Product object. + * @param int $qty Quantity to add. + * @param array $args Args for the added product. + * @return int + */ + public function add_product( $product, $qty = 1, $args = array() ) { + if ( $product ) { + $order = ArrayUtil::get_value_or_default( $args, 'order' ); + $total = wc_get_price_excluding_tax( + $product, + array( + 'qty' => $qty, + 'order' => $order, + ) + ); + + $default_args = array( + 'name' => $product->get_name(), + 'tax_class' => $product->get_tax_class(), + 'product_id' => $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id(), + 'variation_id' => $product->is_type( 'variation' ) ? $product->get_id() : 0, + 'variation' => $product->is_type( 'variation' ) ? $product->get_attributes() : array(), + 'subtotal' => $total, + 'total' => $total, + 'quantity' => $qty, + ); + } else { + $default_args = array( + 'quantity' => $qty, + ); + } + + $args = wp_parse_args( $args, $default_args ); + + // BW compatibility with old args. + if ( isset( $args['totals'] ) ) { + foreach ( $args['totals'] as $key => $value ) { + if ( 'tax' === $key ) { + $args['total_tax'] = $value; + } elseif ( 'tax_data' === $key ) { + $args['taxes'] = $value; + } else { + $args[ $key ] = $value; + } + } + } + + $item = wc_get_container()->get( LegacyProxy::class )->get_instance_of( WC_Order_Item_Product::class ); + $item->set_props( $args ); + $item->set_backorder_meta(); + $item->set_order_id( $this->get_id() ); + $item->save(); + $this->add_item( $item ); + wc_do_deprecated_action( 'woocommerce_order_add_product', array( $this->get_id(), $item->get_id(), $product, $qty, $args ), '3.0', 'woocommerce_new_order_item action instead' ); + delete_transient( 'wc_order_' . $this->get_id() . '_needs_processing' ); + return $item->get_id(); + } + + /* + |-------------------------------------------------------------------------- + | Payment Token Handling + |-------------------------------------------------------------------------- + | + | Payment tokens are hashes used to take payments by certain gateways. + | + */ + + /** + * Add a payment token to an order + * + * @since 2.6 + * @param WC_Payment_Token $token Payment token object. + * @return boolean|int The new token ID or false if it failed. + */ + public function add_payment_token( $token ) { + if ( empty( $token ) || ! ( $token instanceof WC_Payment_Token ) ) { + return false; + } + + $token_ids = $this->data_store->get_payment_token_ids( $this ); + $token_ids[] = $token->get_id(); + $this->data_store->update_payment_token_ids( $this, $token_ids ); + + do_action( 'woocommerce_payment_token_added_to_order', $this->get_id(), $token->get_id(), $token, $token_ids ); + return $token->get_id(); + } + + /** + * Returns a list of all payment tokens associated with the current order + * + * @since 2.6 + * @return array An array of payment token objects + */ + public function get_payment_tokens() { + return $this->data_store->get_payment_token_ids( $this ); + } + + /* + |-------------------------------------------------------------------------- + | Calculations. + |-------------------------------------------------------------------------- + | + | These methods calculate order totals and taxes based on the current data. + | + */ + + /** + * Calculate shipping total. + * + * @since 2.2 + * @return float + */ + public function calculate_shipping() { + $shipping_total = 0; + + foreach ( $this->get_shipping_methods() as $shipping ) { + $shipping_total += $shipping->get_total(); + } + + $this->set_shipping_total( $shipping_total ); + $this->save(); + + return $this->get_shipping_total(); + } + + /** + * Get all tax classes for items in the order. + * + * @since 2.6.3 + * @return array + */ + public function get_items_tax_classes() { + $found_tax_classes = array(); + + foreach ( $this->get_items() as $item ) { + if ( is_callable( array( $item, 'get_tax_status' ) ) && in_array( $item->get_tax_status(), array( 'taxable', 'shipping' ), true ) ) { + $found_tax_classes[] = $item->get_tax_class(); + } + } + + return array_unique( $found_tax_classes ); + } + + /** + * Get tax location for this order. + * + * @since 3.2.0 + * @param array $args array Override the location. + * @return array + */ + protected function get_tax_location( $args = array() ) { + $tax_based_on = get_option( 'woocommerce_tax_based_on' ); + + if ( 'shipping' === $tax_based_on && ! $this->get_shipping_country() ) { + $tax_based_on = 'billing'; + } + + $args = wp_parse_args( + $args, + array( + 'country' => 'billing' === $tax_based_on ? $this->get_billing_country() : $this->get_shipping_country(), + 'state' => 'billing' === $tax_based_on ? $this->get_billing_state() : $this->get_shipping_state(), + 'postcode' => 'billing' === $tax_based_on ? $this->get_billing_postcode() : $this->get_shipping_postcode(), + 'city' => 'billing' === $tax_based_on ? $this->get_billing_city() : $this->get_shipping_city(), + ) + ); + + // Default to base. + if ( 'base' === $tax_based_on || empty( $args['country'] ) ) { + $args['country'] = WC()->countries->get_base_country(); + $args['state'] = WC()->countries->get_base_state(); + $args['postcode'] = WC()->countries->get_base_postcode(); + $args['city'] = WC()->countries->get_base_city(); + } + + return apply_filters( 'woocommerce_order_get_tax_location', $args, $this ); + } + + /** + * Get tax rates for an order. Use order's shipping or billing address, defaults to base location. + * + * @param string $tax_class Tax class to get rates for. + * @param array $location_args Location to compute rates for. Should be in form: array( country, state, postcode, city). + * @param object $customer Only used to maintain backward compatibility for filter `woocommerce-matched_rates`. + * + * @return mixed|void Tax rates. + */ + protected function get_tax_rates( $tax_class, $location_args = array(), $customer = null ) { + $tax_location = $this->get_tax_location( $location_args ); + $tax_location = array( $tax_location['country'], $tax_location['state'], $tax_location['postcode'], $tax_location['city'] ); + return WC_Tax::get_rates_from_location( $tax_class, $tax_location, $customer ); + } + + /** + * Calculate taxes for all line items and shipping, and store the totals and tax rows. + * + * If by default the taxes are based on the shipping address and the current order doesn't + * have any, it would use the billing address rather than using the Shopping base location. + * + * Will use the base country unless customer addresses are set. + * + * @param array $args Added in 3.0.0 to pass things like location. + */ + public function calculate_taxes( $args = array() ) { + do_action( 'woocommerce_order_before_calculate_taxes', $args, $this ); + + $calculate_tax_for = $this->get_tax_location( $args ); + $shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' ); + + if ( 'inherit' === $shipping_tax_class ) { + $found_classes = array_intersect( array_merge( array( '' ), WC_Tax::get_tax_class_slugs() ), $this->get_items_tax_classes() ); + $shipping_tax_class = count( $found_classes ) ? current( $found_classes ) : false; + } + + $is_vat_exempt = apply_filters( 'woocommerce_order_is_vat_exempt', 'yes' === $this->get_meta( 'is_vat_exempt' ), $this ); + + // Trigger tax recalculation for all items. + foreach ( $this->get_items( array( 'line_item', 'fee' ) ) as $item_id => $item ) { + if ( ! $is_vat_exempt ) { + $item->calculate_taxes( $calculate_tax_for ); + } else { + $item->set_taxes( false ); + } + } + + foreach ( $this->get_shipping_methods() as $item_id => $item ) { + if ( false !== $shipping_tax_class && ! $is_vat_exempt ) { + $item->calculate_taxes( array_merge( $calculate_tax_for, array( 'tax_class' => $shipping_tax_class ) ) ); + } else { + $item->set_taxes( false ); + } + } + + $this->update_taxes(); + } + + /** + * Calculate fees for all line items. + * + * @return float Fee total. + */ + public function get_total_fees() { + return array_reduce( + $this->get_fees(), + function( $carry, $item ) { + return $carry + $item->get_total(); + } + ); + } + + /** + * Update tax lines for the order based on the line item taxes themselves. + */ + public function update_taxes() { + $cart_taxes = array(); + $shipping_taxes = array(); + $existing_taxes = $this->get_taxes(); + $saved_rate_ids = array(); + + foreach ( $this->get_items( array( 'line_item', 'fee' ) ) as $item_id => $item ) { + $taxes = $item->get_taxes(); + foreach ( $taxes['total'] as $tax_rate_id => $tax ) { + $tax_amount = (float) $this->round_line_tax( $tax, false ); + + $cart_taxes[ $tax_rate_id ] = isset( $cart_taxes[ $tax_rate_id ] ) ? (float) $cart_taxes[ $tax_rate_id ] + $tax_amount : $tax_amount; + } + } + + foreach ( $this->get_shipping_methods() as $item_id => $item ) { + $taxes = $item->get_taxes(); + foreach ( $taxes['total'] as $tax_rate_id => $tax ) { + $tax_amount = (float) $tax; + + if ( 'yes' !== get_option( 'woocommerce_tax_round_at_subtotal' ) ) { + $tax_amount = wc_round_tax_total( $tax_amount ); + } + + $shipping_taxes[ $tax_rate_id ] = isset( $shipping_taxes[ $tax_rate_id ] ) ? $shipping_taxes[ $tax_rate_id ] + $tax_amount : $tax_amount; + } + } + + foreach ( $existing_taxes as $tax ) { + // Remove taxes which no longer exist for cart/shipping. + if ( ( ! array_key_exists( $tax->get_rate_id(), $cart_taxes ) && ! array_key_exists( $tax->get_rate_id(), $shipping_taxes ) ) || in_array( $tax->get_rate_id(), $saved_rate_ids, true ) ) { + $this->remove_item( $tax->get_id() ); + continue; + } + $saved_rate_ids[] = $tax->get_rate_id(); + $tax->set_rate( $tax->get_rate_id() ); + $tax->set_tax_total( isset( $cart_taxes[ $tax->get_rate_id() ] ) ? $cart_taxes[ $tax->get_rate_id() ] : 0 ); + $tax->set_label( WC_Tax::get_rate_label( $tax->get_rate_id() ) ); + $tax->set_shipping_tax_total( ! empty( $shipping_taxes[ $tax->get_rate_id() ] ) ? $shipping_taxes[ $tax->get_rate_id() ] : 0 ); + $tax->save(); + } + + $new_rate_ids = wp_parse_id_list( array_diff( array_keys( $cart_taxes + $shipping_taxes ), $saved_rate_ids ) ); + + // New taxes. + foreach ( $new_rate_ids as $tax_rate_id ) { + $item = new WC_Order_Item_Tax(); + $item->set_rate( $tax_rate_id ); + $item->set_tax_total( isset( $cart_taxes[ $tax_rate_id ] ) ? $cart_taxes[ $tax_rate_id ] : 0 ); + $item->set_shipping_tax_total( ! empty( $shipping_taxes[ $tax_rate_id ] ) ? $shipping_taxes[ $tax_rate_id ] : 0 ); + $this->add_item( $item ); + } + + $this->set_shipping_tax( array_sum( $shipping_taxes ) ); + $this->set_cart_tax( array_sum( $cart_taxes ) ); + $this->save(); + } + + /** + * Helper function. + * If you add all items in this order in cart again, this would be the cart subtotal (assuming all other settings are same). + * + * @return float Cart subtotal. + */ + protected function get_cart_subtotal_for_order() { + return wc_remove_number_precision( + $this->get_rounded_items_total( + $this->get_values_for_total( 'subtotal' ) + ) + ); + } + + /** + * Helper function. + * If you add all items in this order in cart again, this would be the cart total (assuming all other settings are same). + * + * @return float Cart total. + */ + protected function get_cart_total_for_order() { + return wc_remove_number_precision( + $this->get_rounded_items_total( + $this->get_values_for_total( 'total' ) + ) + ); + } + + /** + * Calculate totals by looking at the contents of the order. Stores the totals and returns the orders final total. + * + * @since 2.2 + * @param bool $and_taxes Calc taxes if true. + * @return float calculated grand total. + */ + public function calculate_totals( $and_taxes = true ) { + do_action( 'woocommerce_order_before_calculate_totals', $and_taxes, $this ); + + $fees_total = 0; + $shipping_total = 0; + $cart_subtotal_tax = 0; + $cart_total_tax = 0; + + $cart_subtotal = $this->get_cart_subtotal_for_order(); + $cart_total = $this->get_cart_total_for_order(); + + // Sum shipping costs. + foreach ( $this->get_shipping_methods() as $shipping ) { + $shipping_total += NumberUtil::round( $shipping->get_total(), wc_get_price_decimals() ); + } + + $this->set_shipping_total( $shipping_total ); + + // Sum fee costs. + foreach ( $this->get_fees() as $item ) { + $fee_total = $item->get_total(); + + if ( 0 > $fee_total ) { + $max_discount = NumberUtil::round( $cart_total + $fees_total + $shipping_total, wc_get_price_decimals() ) * -1; + + if ( $fee_total < $max_discount && 0 > $max_discount ) { + $item->set_total( $max_discount ); + } + } + $fees_total += $item->get_total(); + } + + // Calculate taxes for items, shipping, discounts. Note; this also triggers save(). + if ( $and_taxes ) { + $this->calculate_taxes(); + } + + // Sum taxes again so we can work out how much tax was discounted. This uses original values, not those possibly rounded to 2dp. + foreach ( $this->get_items() as $item ) { + $taxes = $item->get_taxes(); + + foreach ( $taxes['total'] as $tax_rate_id => $tax ) { + $cart_total_tax += (float) $tax; + } + + foreach ( $taxes['subtotal'] as $tax_rate_id => $tax ) { + $cart_subtotal_tax += (float) $tax; + } + } + + $this->set_discount_total( NumberUtil::round( $cart_subtotal - $cart_total, wc_get_price_decimals() ) ); + $this->set_discount_tax( wc_round_tax_total( $cart_subtotal_tax - $cart_total_tax ) ); + $this->set_total( NumberUtil::round( $cart_total + $fees_total + $this->get_shipping_total() + $this->get_cart_tax() + $this->get_shipping_tax(), wc_get_price_decimals() ) ); + + do_action( 'woocommerce_order_after_calculate_totals', $and_taxes, $this ); + + $this->save(); + + return $this->get_total(); + } + + /** + * Get item subtotal - this is the cost before discount. + * + * @param object $item Item to get total from. + * @param bool $inc_tax (default: false). + * @param bool $round (default: true). + * @return float + */ + public function get_item_subtotal( $item, $inc_tax = false, $round = true ) { + $subtotal = 0; + + if ( is_callable( array( $item, 'get_subtotal' ) ) && $item->get_quantity() ) { + if ( $inc_tax ) { + $subtotal = ( $item->get_subtotal() + $item->get_subtotal_tax() ) / $item->get_quantity(); + } else { + $subtotal = floatval( $item->get_subtotal() ) / $item->get_quantity(); + } + + $subtotal = $round ? number_format( (float) $subtotal, wc_get_price_decimals(), '.', '' ) : $subtotal; + } + + return apply_filters( 'woocommerce_order_amount_item_subtotal', $subtotal, $this, $item, $inc_tax, $round ); + } + + /** + * Get line subtotal - this is the cost before discount. + * + * @param object $item Item to get total from. + * @param bool $inc_tax (default: false). + * @param bool $round (default: true). + * @return float + */ + public function get_line_subtotal( $item, $inc_tax = false, $round = true ) { + $subtotal = 0; + + if ( is_callable( array( $item, 'get_subtotal' ) ) ) { + if ( $inc_tax ) { + $subtotal = $item->get_subtotal() + $item->get_subtotal_tax(); + } else { + $subtotal = $item->get_subtotal(); + } + + $subtotal = $round ? NumberUtil::round( $subtotal, wc_get_price_decimals() ) : $subtotal; + } + + return apply_filters( 'woocommerce_order_amount_line_subtotal', $subtotal, $this, $item, $inc_tax, $round ); + } + + /** + * Calculate item cost - useful for gateways. + * + * @param object $item Item to get total from. + * @param bool $inc_tax (default: false). + * @param bool $round (default: true). + * @return float + */ + public function get_item_total( $item, $inc_tax = false, $round = true ) { + $total = 0; + + if ( is_callable( array( $item, 'get_total' ) ) && $item->get_quantity() ) { + if ( $inc_tax ) { + $total = ( $item->get_total() + $item->get_total_tax() ) / $item->get_quantity(); + } else { + $total = floatval( $item->get_total() ) / $item->get_quantity(); + } + + $total = $round ? NumberUtil::round( $total, wc_get_price_decimals() ) : $total; + } + + return apply_filters( 'woocommerce_order_amount_item_total', $total, $this, $item, $inc_tax, $round ); + } + + /** + * Calculate line total - useful for gateways. + * + * @param object $item Item to get total from. + * @param bool $inc_tax (default: false). + * @param bool $round (default: true). + * @return float + */ + public function get_line_total( $item, $inc_tax = false, $round = true ) { + $total = 0; + + if ( is_callable( array( $item, 'get_total' ) ) ) { + // Check if we need to add line tax to the line total. + $total = $inc_tax ? $item->get_total() + $item->get_total_tax() : $item->get_total(); + + // Check if we need to round. + $total = $round ? NumberUtil::round( $total, wc_get_price_decimals() ) : $total; + } + + return apply_filters( 'woocommerce_order_amount_line_total', $total, $this, $item, $inc_tax, $round ); + } + + /** + * Get item tax - useful for gateways. + * + * @param mixed $item Item to get total from. + * @param bool $round (default: true). + * @return float + */ + public function get_item_tax( $item, $round = true ) { + $tax = 0; + + if ( is_callable( array( $item, 'get_total_tax' ) ) && $item->get_quantity() ) { + $tax = $item->get_total_tax() / $item->get_quantity(); + $tax = $round ? wc_round_tax_total( $tax ) : $tax; + } + + return apply_filters( 'woocommerce_order_amount_item_tax', $tax, $item, $round, $this ); + } + + /** + * Get line tax - useful for gateways. + * + * @param mixed $item Item to get total from. + * @return float + */ + public function get_line_tax( $item ) { + return apply_filters( 'woocommerce_order_amount_line_tax', is_callable( array( $item, 'get_total_tax' ) ) ? wc_round_tax_total( $item->get_total_tax() ) : 0, $item, $this ); + } + + /** + * Gets line subtotal - formatted for display. + * + * @param object $item Item to get total from. + * @param string $tax_display Incl or excl tax display mode. + * @return string + */ + public function get_formatted_line_subtotal( $item, $tax_display = '' ) { + $tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' ); + + if ( 'excl' === $tax_display ) { + $ex_tax_label = $this->get_prices_include_tax() ? 1 : 0; + + $subtotal = wc_price( + $this->get_line_subtotal( $item ), + array( + 'ex_tax_label' => $ex_tax_label, + 'currency' => $this->get_currency(), + ) + ); + } else { + $subtotal = wc_price( $this->get_line_subtotal( $item, true ), array( 'currency' => $this->get_currency() ) ); + } + + return apply_filters( 'woocommerce_order_formatted_line_subtotal', $subtotal, $item, $this ); + } + + /** + * Gets order total - formatted for display. + * + * @return string + */ + public function get_formatted_order_total() { + $formatted_total = wc_price( $this->get_total(), array( 'currency' => $this->get_currency() ) ); + return apply_filters( 'woocommerce_get_formatted_order_total', $formatted_total, $this ); + } + + /** + * Gets subtotal - subtotal is shown before discounts, but with localised taxes. + * + * @param bool $compound (default: false). + * @param string $tax_display (default: the tax_display_cart value). + * @return string + */ + public function get_subtotal_to_display( $compound = false, $tax_display = '' ) { + $tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' ); + $subtotal = $this->get_cart_subtotal_for_order(); + + if ( ! $compound ) { + + if ( 'incl' === $tax_display ) { + $subtotal_taxes = 0; + foreach ( $this->get_items() as $item ) { + $subtotal_taxes += self::round_line_tax( $item->get_subtotal_tax(), false ); + } + $subtotal += wc_round_tax_total( $subtotal_taxes ); + } + + $subtotal = wc_price( $subtotal, array( 'currency' => $this->get_currency() ) ); + + if ( 'excl' === $tax_display && $this->get_prices_include_tax() && wc_tax_enabled() ) { + $subtotal .= ' ' . WC()->countries->ex_tax_or_vat() . ''; + } + } else { + if ( 'incl' === $tax_display ) { + return ''; + } + + // Add Shipping Costs. + $subtotal += $this->get_shipping_total(); + + // Remove non-compound taxes. + foreach ( $this->get_taxes() as $tax ) { + if ( $tax->is_compound() ) { + continue; + } + $subtotal = $subtotal + $tax->get_tax_total() + $tax->get_shipping_tax_total(); + } + + // Remove discounts. + $subtotal = $subtotal - $this->get_total_discount(); + $subtotal = wc_price( $subtotal, array( 'currency' => $this->get_currency() ) ); + } + + return apply_filters( 'woocommerce_order_subtotal_to_display', $subtotal, $compound, $this ); + } + + /** + * Gets shipping (formatted). + * + * @param string $tax_display Excl or incl tax display mode. + * @return string + */ + public function get_shipping_to_display( $tax_display = '' ) { + $tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' ); + + if ( 0 < abs( (float) $this->get_shipping_total() ) ) { + + if ( 'excl' === $tax_display ) { + + // Show shipping excluding tax. + $shipping = wc_price( $this->get_shipping_total(), array( 'currency' => $this->get_currency() ) ); + + if ( (float) $this->get_shipping_tax() > 0 && $this->get_prices_include_tax() ) { + $shipping .= apply_filters( 'woocommerce_order_shipping_to_display_tax_label', ' ' . WC()->countries->ex_tax_or_vat() . '', $this, $tax_display ); + } + } else { + + // Show shipping including tax. + $shipping = wc_price( $this->get_shipping_total() + $this->get_shipping_tax(), array( 'currency' => $this->get_currency() ) ); + + if ( (float) $this->get_shipping_tax() > 0 && ! $this->get_prices_include_tax() ) { + $shipping .= apply_filters( 'woocommerce_order_shipping_to_display_tax_label', ' ' . WC()->countries->inc_tax_or_vat() . '', $this, $tax_display ); + } + } + + /* translators: %s: method */ + $shipping .= apply_filters( 'woocommerce_order_shipping_to_display_shipped_via', ' ' . sprintf( __( 'via %s', 'woocommerce' ), $this->get_shipping_method() ) . '', $this ); + + } elseif ( $this->get_shipping_method() ) { + $shipping = $this->get_shipping_method(); + } else { + $shipping = __( 'Free!', 'woocommerce' ); + } + + return apply_filters( 'woocommerce_order_shipping_to_display', $shipping, $this, $tax_display ); + } + + /** + * Get the discount amount (formatted). + * + * @since 2.3.0 + * @param string $tax_display Excl or incl tax display mode. + * @return string + */ + public function get_discount_to_display( $tax_display = '' ) { + $tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' ); + return apply_filters( 'woocommerce_order_discount_to_display', wc_price( $this->get_total_discount( 'excl' === $tax_display && 'excl' === get_option( 'woocommerce_tax_display_cart' ) ), array( 'currency' => $this->get_currency() ) ), $this ); + } + + /** + * Add total row for subtotal. + * + * @param array $total_rows Reference to total rows array. + * @param string $tax_display Excl or incl tax display mode. + */ + protected function add_order_item_totals_subtotal_row( &$total_rows, $tax_display ) { + $subtotal = $this->get_subtotal_to_display( false, $tax_display ); + + if ( $subtotal ) { + $total_rows['cart_subtotal'] = array( + 'label' => __( 'Subtotal:', 'woocommerce' ), + 'value' => $subtotal, + ); + } + } + + /** + * Add total row for discounts. + * + * @param array $total_rows Reference to total rows array. + * @param string $tax_display Excl or incl tax display mode. + */ + protected function add_order_item_totals_discount_row( &$total_rows, $tax_display ) { + if ( $this->get_total_discount() > 0 ) { + $total_rows['discount'] = array( + 'label' => __( 'Discount:', 'woocommerce' ), + 'value' => '-' . $this->get_discount_to_display( $tax_display ), + ); + } + } + + /** + * Add total row for shipping. + * + * @param array $total_rows Reference to total rows array. + * @param string $tax_display Excl or incl tax display mode. + */ + protected function add_order_item_totals_shipping_row( &$total_rows, $tax_display ) { + if ( $this->get_shipping_method() ) { + $total_rows['shipping'] = array( + 'label' => __( 'Shipping:', 'woocommerce' ), + 'value' => $this->get_shipping_to_display( $tax_display ), + ); + } + } + + /** + * Add total row for fees. + * + * @param array $total_rows Reference to total rows array. + * @param string $tax_display Excl or incl tax display mode. + */ + protected function add_order_item_totals_fee_rows( &$total_rows, $tax_display ) { + $fees = $this->get_fees(); + + if ( $fees ) { + foreach ( $fees as $id => $fee ) { + if ( apply_filters( 'woocommerce_get_order_item_totals_excl_free_fees', empty( $fee['line_total'] ) && empty( $fee['line_tax'] ), $id ) ) { + continue; + } + $total_rows[ 'fee_' . $fee->get_id() ] = array( + 'label' => $fee->get_name() . ':', + 'value' => wc_price( 'excl' === $tax_display ? $fee->get_total() : $fee->get_total() + $fee->get_total_tax(), array( 'currency' => $this->get_currency() ) ), + ); + } + } + } + + /** + * Add total row for taxes. + * + * @param array $total_rows Reference to total rows array. + * @param string $tax_display Excl or incl tax display mode. + */ + protected function add_order_item_totals_tax_rows( &$total_rows, $tax_display ) { + // Tax for tax exclusive prices. + if ( 'excl' === $tax_display && wc_tax_enabled() ) { + if ( 'itemized' === get_option( 'woocommerce_tax_total_display' ) ) { + foreach ( $this->get_tax_totals() as $code => $tax ) { + $total_rows[ sanitize_title( $code ) ] = array( + 'label' => $tax->label . ':', + 'value' => $tax->formatted_amount, + ); + } + } else { + $total_rows['tax'] = array( + 'label' => WC()->countries->tax_or_vat() . ':', + 'value' => wc_price( $this->get_total_tax(), array( 'currency' => $this->get_currency() ) ), + ); + } + } + } + + /** + * Add total row for grand total. + * + * @param array $total_rows Reference to total rows array. + * @param string $tax_display Excl or incl tax display mode. + */ + protected function add_order_item_totals_total_row( &$total_rows, $tax_display ) { + $total_rows['order_total'] = array( + 'label' => __( 'Total:', 'woocommerce' ), + 'value' => $this->get_formatted_order_total( $tax_display ), + ); + } + + /** + * Get totals for display on pages and in emails. + * + * @param mixed $tax_display Excl or incl tax display mode. + * @return array + */ + public function get_order_item_totals( $tax_display = '' ) { + $tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' ); + $total_rows = array(); + + $this->add_order_item_totals_subtotal_row( $total_rows, $tax_display ); + $this->add_order_item_totals_discount_row( $total_rows, $tax_display ); + $this->add_order_item_totals_shipping_row( $total_rows, $tax_display ); + $this->add_order_item_totals_fee_rows( $total_rows, $tax_display ); + $this->add_order_item_totals_tax_rows( $total_rows, $tax_display ); + $this->add_order_item_totals_total_row( $total_rows, $tax_display ); + + return apply_filters( 'woocommerce_get_order_item_totals', $total_rows, $this, $tax_display ); + } + + /* + |-------------------------------------------------------------------------- + | Conditionals + |-------------------------------------------------------------------------- + | + | Checks if a condition is true or false. + | + */ + + /** + * Checks the order status against a passed in status. + * + * @param array|string $status Status to check. + * @return bool + */ + public function has_status( $status ) { + return apply_filters( 'woocommerce_order_has_status', ( is_array( $status ) && in_array( $this->get_status(), $status, true ) ) || $this->get_status() === $status, $this, $status ); + } + + /** + * Check whether this order has a specific shipping method or not. + * + * @param string $method_id Method ID to check. + * @return bool + */ + public function has_shipping_method( $method_id ) { + foreach ( $this->get_shipping_methods() as $shipping_method ) { + if ( strpos( $shipping_method->get_method_id(), $method_id ) === 0 ) { + return true; + } + } + return false; + } + + /** + * Returns true if the order contains a free product. + * + * @since 2.5.0 + * @return bool + */ + public function has_free_item() { + foreach ( $this->get_items() as $item ) { + if ( ! $item->get_total() ) { + return true; + } + } + return false; + } +} diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-payment-gateway.php b/plugins/woocommerce/includes/abstracts/abstract-wc-payment-gateway.php new file mode 100644 index 00000000000..065d8d5bedd --- /dev/null +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-payment-gateway.php @@ -0,0 +1,562 @@ +tokens ) > 0 ) { + return $this->tokens; + } + + if ( is_user_logged_in() && $this->supports( 'tokenization' ) ) { + $this->tokens = WC_Payment_Tokens::get_customer_tokens( get_current_user_id(), $this->id ); + } + + return $this->tokens; + } + + /** + * Return the title for admin screens. + * + * @return string + */ + public function get_method_title() { + return apply_filters( 'woocommerce_gateway_method_title', $this->method_title, $this ); + } + + /** + * Return the description for admin screens. + * + * @return string + */ + public function get_method_description() { + return apply_filters( 'woocommerce_gateway_method_description', $this->method_description, $this ); + } + + /** + * Output the gateway settings screen. + */ + public function admin_options() { + echo '

    ' . esc_html( $this->get_method_title() ); + wc_back_link( __( 'Return to payments', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=checkout' ) ); + echo '

    '; + echo wp_kses_post( wpautop( $this->get_method_description() ) ); + parent::admin_options(); + } + + /** + * Init settings for gateways. + */ + public function init_settings() { + parent::init_settings(); + $this->enabled = ! empty( $this->settings['enabled'] ) && 'yes' === $this->settings['enabled'] ? 'yes' : 'no'; + } + + /** + * Return whether or not this gateway still requires setup to function. + * + * When this gateway is toggled on via AJAX, if this returns true a + * redirect will occur to the settings page instead. + * + * @since 3.4.0 + * @return bool + */ + public function needs_setup() { + return false; + } + + /** + * Get the return url (thank you page). + * + * @param WC_Order|null $order Order object. + * @return string + */ + public function get_return_url( $order = null ) { + if ( $order ) { + $return_url = $order->get_checkout_order_received_url(); + } else { + $return_url = wc_get_endpoint_url( 'order-received', '', wc_get_checkout_url() ); + } + + return apply_filters( 'woocommerce_get_return_url', $return_url, $order ); + } + + /** + * Get a link to the transaction on the 3rd party gateway site (if applicable). + * + * @param WC_Order $order the order object. + * @return string transaction URL, or empty string. + */ + public function get_transaction_url( $order ) { + + $return_url = ''; + $transaction_id = $order->get_transaction_id(); + + if ( ! empty( $this->view_transaction_url ) && ! empty( $transaction_id ) ) { + $return_url = sprintf( $this->view_transaction_url, $transaction_id ); + } + + return apply_filters( 'woocommerce_get_transaction_url', $return_url, $order, $this ); + } + + /** + * Get the order total in checkout and pay_for_order. + * + * @return float + */ + protected function get_order_total() { + + $total = 0; + $order_id = absint( get_query_var( 'order-pay' ) ); + + // Gets order total from "pay for order" page. + if ( 0 < $order_id ) { + $order = wc_get_order( $order_id ); + if ( $order ) { + $total = (float) $order->get_total(); + } + + // Gets order total from cart/checkout. + } elseif ( 0 < WC()->cart->total ) { + $total = (float) WC()->cart->total; + } + + return $total; + } + + /** + * Check if the gateway is available for use. + * + * @return bool + */ + public function is_available() { + $is_available = ( 'yes' === $this->enabled ); + + if ( WC()->cart && 0 < $this->get_order_total() && 0 < $this->max_amount && $this->max_amount < $this->get_order_total() ) { + $is_available = false; + } + + return $is_available; + } + + /** + * Check if the gateway has fields on the checkout. + * + * @return bool + */ + public function has_fields() { + return (bool) $this->has_fields; + } + + /** + * Return the gateway's title. + * + * @return string + */ + public function get_title() { + return apply_filters( 'woocommerce_gateway_title', $this->title, $this->id ); + } + + /** + * Return the gateway's description. + * + * @return string + */ + public function get_description() { + return apply_filters( 'woocommerce_gateway_description', $this->description, $this->id ); + } + + /** + * Return the gateway's icon. + * + * @return string + */ + public function get_icon() { + + $icon = $this->icon ? '' . esc_attr( $this->get_title() ) . '' : ''; + + return apply_filters( 'woocommerce_gateway_icon', $icon, $this->id ); + } + + /** + * Return the gateway's pay button ID. + * + * @since 3.9.0 + * @return string + */ + public function get_pay_button_id() { + return sanitize_html_class( $this->pay_button_id ); + } + + /** + * Set as current gateway. + * + * Set this as the current gateway. + */ + public function set_current() { + $this->chosen = true; + } + + /** + * Process Payment. + * + * Process the payment. Override this in your gateway. When implemented, this should. + * return the success and redirect in an array. e.g: + * + * return array( + * 'result' => 'success', + * 'redirect' => $this->get_return_url( $order ) + * ); + * + * @param int $order_id Order ID. + * @return array + */ + public function process_payment( $order_id ) { + return array(); + } + + /** + * Process refund. + * + * If the gateway declares 'refunds' support, this will allow it to refund. + * a passed in amount. + * + * @param int $order_id Order ID. + * @param float|null $amount Refund amount. + * @param string $reason Refund reason. + * @return boolean True or false based on success, or a WP_Error object. + */ + public function process_refund( $order_id, $amount = null, $reason = '' ) { + return false; + } + + /** + * Validate frontend fields. + * + * Validate payment fields on the frontend. + * + * @return bool + */ + public function validate_fields() { + return true; + } + + /** + * If There are no payment fields show the description if set. + * Override this in your gateway if you have some. + */ + public function payment_fields() { + $description = $this->get_description(); + if ( $description ) { + echo wpautop( wptexturize( $description ) ); // @codingStandardsIgnoreLine. + } + + if ( $this->supports( 'default_credit_card_form' ) ) { + $this->credit_card_form(); // Deprecated, will be removed in a future version. + } + } + + /** + * Check if a gateway supports a given feature. + * + * Gateways should override this to declare support (or lack of support) for a feature. + * For backward compatibility, gateways support 'products' by default, but nothing else. + * + * @param string $feature string The name of a feature to test support for. + * @return bool True if the gateway supports the feature, false otherwise. + * @since 1.5.7 + */ + public function supports( $feature ) { + return apply_filters( 'woocommerce_payment_gateway_supports', in_array( $feature, $this->supports ), $feature, $this ); + } + + /** + * Can the order be refunded via this gateway? + * + * Should be extended by gateways to do their own checks. + * + * @param WC_Order $order Order object. + * @return bool If false, the automatic refund button is hidden in the UI. + */ + public function can_refund_order( $order ) { + return $order && $this->supports( 'refunds' ); + } + + /** + * Core credit card form which gateways can use if needed. Deprecated - inherit WC_Payment_Gateway_CC instead. + * + * @param array $args Arguments. + * @param array $fields Fields. + */ + public function credit_card_form( $args = array(), $fields = array() ) { + wc_deprecated_function( 'credit_card_form', '2.6', 'WC_Payment_Gateway_CC->form' ); + $cc_form = new WC_Payment_Gateway_CC(); + $cc_form->id = $this->id; + $cc_form->supports = $this->supports; + $cc_form->form(); + } + + /** + * Enqueues our tokenization script to handle some of the new form options. + * + * @since 2.6.0 + */ + public function tokenization_script() { + wp_enqueue_script( + 'woocommerce-tokenization-form', + plugins_url( '/assets/js/frontend/tokenization-form' . ( Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min' ) . '.js', WC_PLUGIN_FILE ), + array( 'jquery' ), + WC()->version + ); + + wp_localize_script( + 'woocommerce-tokenization-form', + 'wc_tokenization_form_params', + array( + 'is_registration_required' => WC()->checkout()->is_registration_required(), + 'is_logged_in' => is_user_logged_in(), + ) + ); + } + + /** + * Grab and display our saved payment methods. + * + * @since 2.6.0 + */ + public function saved_payment_methods() { + $html = '
      '; + + foreach ( $this->get_tokens() as $token ) { + $html .= $this->get_saved_payment_method_option_html( $token ); + } + + $html .= $this->get_new_payment_method_option_html(); + $html .= '
    '; + + echo apply_filters( 'wc_payment_gateway_form_saved_payment_methods_html', $html, $this ); // @codingStandardsIgnoreLine + } + + /** + * Gets saved payment method HTML from a token. + * + * @since 2.6.0 + * @param WC_Payment_Token $token Payment Token. + * @return string Generated payment method HTML + */ + public function get_saved_payment_method_option_html( $token ) { + $html = sprintf( + '
  • + + +
  • ', + esc_attr( $this->id ), + esc_attr( $token->get_id() ), + esc_html( $token->get_display_name() ), + checked( $token->is_default(), true, false ) + ); + + return apply_filters( 'woocommerce_payment_gateway_get_saved_payment_method_option_html', $html, $token, $this ); + } + + /** + * Displays a radio button for entering a new payment method (new CC details) instead of using a saved method. + * Only displayed when a gateway supports tokenization. + * + * @since 2.6.0 + */ + public function get_new_payment_method_option_html() { + $label = apply_filters( 'woocommerce_payment_gateway_get_new_payment_method_option_html_label', $this->new_method_label ? $this->new_method_label : __( 'Use a new payment method', 'woocommerce' ), $this ); + $html = sprintf( + '
  • + + +
  • ', + esc_attr( $this->id ), + esc_html( $label ) + ); + + return apply_filters( 'woocommerce_payment_gateway_get_new_payment_method_option_html', $html, $this ); + } + + /** + * Outputs a checkbox for saving a new payment method to the database. + * + * @since 2.6.0 + */ + public function save_payment_method_checkbox() { + $html = sprintf( + '

    + + +

    ', + esc_attr( $this->id ), + esc_html__( 'Save to account', 'woocommerce' ) + ); + + echo apply_filters( 'woocommerce_payment_gateway_save_new_payment_method_option_html', $html, $this ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * Add payment method via account screen. This should be extended by gateway plugins. + * + * @since 3.2.0 Included here from 3.2.0, but supported from 3.0.0. + * @return array + */ + public function add_payment_method() { + return array( + 'result' => 'failure', + 'redirect' => wc_get_endpoint_url( 'payment-methods' ), + ); + } +} diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-payment-token.php b/plugins/woocommerce/includes/abstracts/abstract-wc-payment-token.php new file mode 100644 index 00000000000..6fc6f634d29 --- /dev/null +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-payment-token.php @@ -0,0 +1,233 @@ + '', + 'token' => '', + 'is_default' => false, + 'user_id' => 0, + 'type' => '', + ); + + /** + * Token Type (CC, eCheck, or a custom type added by an extension). + * Set by child classes. + * + * @var string + */ + protected $type = ''; + + /** + * Initialize a payment token. + * + * These fields are accepted by all payment tokens: + * is_default - boolean Optional - Indicates this is the default payment token for a user + * token - string Required - The actual token to store + * gateway_id - string Required - Identifier for the gateway this token is associated with + * user_id - int Optional - ID for the user this token is associated with. 0 if this token is not associated with a user + * + * @since 2.6.0 + * @param mixed $token Token. + */ + public function __construct( $token = '' ) { + parent::__construct( $token ); + + if ( is_numeric( $token ) ) { + $this->set_id( $token ); + } elseif ( is_object( $token ) ) { + $token_id = $token->get_id(); + if ( ! empty( $token_id ) ) { + $this->set_id( $token->get_id() ); + } + } else { + $this->set_object_read( true ); + } + + $this->data_store = WC_Data_Store::load( 'payment-token' ); + if ( $this->get_id() > 0 ) { + $this->data_store->read( $this ); + } + } + + /* + *-------------------------------------------------------------------------- + * Getters + *-------------------------------------------------------------------------- + */ + + /** + * Returns the raw payment token. + * + * @since 2.6.0 + * @param string $context Context in which to call this. + * @return string Raw token + */ + public function get_token( $context = 'view' ) { + return $this->get_prop( 'token', $context ); + } + + /** + * Returns the type of this payment token (CC, eCheck, or something else). + * Overwritten by child classes. + * + * @since 2.6.0 + * @param string $deprecated Deprecated since WooCommerce 3.0. + * @return string Payment Token Type (CC, eCheck) + */ + public function get_type( $deprecated = '' ) { + return $this->type; + } + + /** + * Get type to display to user. + * Get's overwritten by child classes. + * + * @since 2.6.0 + * @param string $deprecated Deprecated since WooCommerce 3.0. + * @return string + */ + public function get_display_name( $deprecated = '' ) { + return $this->get_type(); + } + + /** + * Returns the user ID associated with the token or false if this token is not associated. + * + * @since 2.6.0 + * @param string $context In what context to execute this. + * @return int User ID if this token is associated with a user or 0 if no user is associated + */ + public function get_user_id( $context = 'view' ) { + return $this->get_prop( 'user_id', $context ); + } + + /** + * Returns the ID of the gateway associated with this payment token. + * + * @since 2.6.0 + * @param string $context In what context to execute this. + * @return string Gateway ID + */ + public function get_gateway_id( $context = 'view' ) { + return $this->get_prop( 'gateway_id', $context ); + } + + /** + * Returns the ID of the gateway associated with this payment token. + * + * @since 2.6.0 + * @param string $context In what context to execute this. + * @return string Gateway ID + */ + public function get_is_default( $context = 'view' ) { + return $this->get_prop( 'is_default', $context ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set the raw payment token. + * + * @since 2.6.0 + * @param string $token Payment token. + */ + public function set_token( $token ) { + $this->set_prop( 'token', $token ); + } + + /** + * Set the user ID for the user associated with this order. + * + * @since 2.6.0 + * @param int $user_id User ID. + */ + public function set_user_id( $user_id ) { + $this->set_prop( 'user_id', absint( $user_id ) ); + } + + /** + * Set the gateway ID. + * + * @since 2.6.0 + * @param string $gateway_id Gateway ID. + */ + public function set_gateway_id( $gateway_id ) { + $this->set_prop( 'gateway_id', $gateway_id ); + } + + /** + * Marks the payment as default or non-default. + * + * @since 2.6.0 + * @param boolean $is_default True or false. + */ + public function set_default( $is_default ) { + $this->set_prop( 'is_default', (bool) $is_default ); + } + + /* + |-------------------------------------------------------------------------- + | Other Methods + |-------------------------------------------------------------------------- + */ + + /** + * Returns if the token is marked as default. + * + * @since 2.6.0 + * @return boolean True if the token is default + */ + public function is_default() { + return (bool) $this->get_prop( 'is_default', 'view' ); + } + + /** + * Validate basic token info (token and type are required). + * + * @since 2.6.0 + * @return boolean True if the passed data is valid + */ + public function validate() { + $token = $this->get_prop( 'token', 'edit' ); + if ( empty( $token ) ) { + return false; + } + return true; + } + +} diff --git a/includes/abstracts/abstract-wc-privacy.php b/plugins/woocommerce/includes/abstracts/abstract-wc-privacy.php similarity index 100% rename from includes/abstracts/abstract-wc-privacy.php rename to plugins/woocommerce/includes/abstracts/abstract-wc-privacy.php diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-product.php b/plugins/woocommerce/includes/abstracts/abstract-wc-product.php new file mode 100644 index 00000000000..71ffba8a229 --- /dev/null +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-product.php @@ -0,0 +1,2136 @@ + '', + 'slug' => '', + 'date_created' => null, + 'date_modified' => null, + 'status' => false, + 'featured' => false, + 'catalog_visibility' => 'visible', + 'description' => '', + 'short_description' => '', + 'sku' => '', + 'price' => '', + 'regular_price' => '', + 'sale_price' => '', + 'date_on_sale_from' => null, + 'date_on_sale_to' => null, + 'total_sales' => '0', + 'tax_status' => 'taxable', + 'tax_class' => '', + 'manage_stock' => false, + 'stock_quantity' => null, + 'stock_status' => 'instock', + 'backorders' => 'no', + 'low_stock_amount' => '', + 'sold_individually' => false, + 'weight' => '', + 'length' => '', + 'width' => '', + 'height' => '', + 'upsell_ids' => array(), + 'cross_sell_ids' => array(), + 'parent_id' => 0, + 'reviews_allowed' => true, + 'purchase_note' => '', + 'attributes' => array(), + 'default_attributes' => array(), + 'menu_order' => 0, + 'post_password' => '', + 'virtual' => false, + 'downloadable' => false, + 'category_ids' => array(), + 'tag_ids' => array(), + 'shipping_class_id' => 0, + 'downloads' => array(), + 'image_id' => '', + 'gallery_image_ids' => array(), + 'download_limit' => -1, + 'download_expiry' => -1, + 'rating_counts' => array(), + 'average_rating' => 0, + 'review_count' => 0, + ); + + /** + * Supported features such as 'ajax_add_to_cart'. + * + * @var array + */ + protected $supports = array(); + + /** + * Get the product if ID is passed, otherwise the product is new and empty. + * This class should NOT be instantiated, but the wc_get_product() function + * should be used. It is possible, but the wc_get_product() is preferred. + * + * @param int|WC_Product|object $product Product to init. + */ + public function __construct( $product = 0 ) { + parent::__construct( $product ); + if ( is_numeric( $product ) && $product > 0 ) { + $this->set_id( $product ); + } elseif ( $product instanceof self ) { + $this->set_id( absint( $product->get_id() ) ); + } elseif ( ! empty( $product->ID ) ) { + $this->set_id( absint( $product->ID ) ); + } else { + $this->set_object_read( true ); + } + + $this->data_store = WC_Data_Store::load( 'product-' . $this->get_type() ); + if ( $this->get_id() > 0 ) { + $this->data_store->read( $this ); + } + } + + /** + * Get internal type. Should return string and *should be overridden* by child classes. + * + * The product_type property is deprecated but is used here for BW compatibility with child classes which may be defining product_type and not have a get_type method. + * + * @since 3.0.0 + * @return string + */ + public function get_type() { + return isset( $this->product_type ) ? $this->product_type : 'simple'; + } + + /** + * Get product name. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_name( $context = 'view' ) { + return $this->get_prop( 'name', $context ); + } + + /** + * Get product slug. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_slug( $context = 'view' ) { + return $this->get_prop( 'slug', $context ); + } + + /** + * Get product created date. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_created( $context = 'view' ) { + return $this->get_prop( 'date_created', $context ); + } + + /** + * Get product modified date. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_modified( $context = 'view' ) { + return $this->get_prop( 'date_modified', $context ); + } + + /** + * Get product status. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_status( $context = 'view' ) { + return $this->get_prop( 'status', $context ); + } + + /** + * If the product is featured. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return boolean + */ + public function get_featured( $context = 'view' ) { + return $this->get_prop( 'featured', $context ); + } + + /** + * Get catalog visibility. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_catalog_visibility( $context = 'view' ) { + return $this->get_prop( 'catalog_visibility', $context ); + } + + /** + * Get product description. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_description( $context = 'view' ) { + return $this->get_prop( 'description', $context ); + } + + /** + * Get product short description. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_short_description( $context = 'view' ) { + return $this->get_prop( 'short_description', $context ); + } + + /** + * Get SKU (Stock-keeping unit) - product unique ID. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_sku( $context = 'view' ) { + return $this->get_prop( 'sku', $context ); + } + + /** + * Returns the product's active price. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string price + */ + public function get_price( $context = 'view' ) { + return $this->get_prop( 'price', $context ); + } + + /** + * Returns the product's regular price. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string price + */ + public function get_regular_price( $context = 'view' ) { + return $this->get_prop( 'regular_price', $context ); + } + + /** + * Returns the product's sale price. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string price + */ + public function get_sale_price( $context = 'view' ) { + return $this->get_prop( 'sale_price', $context ); + } + + /** + * Get date on sale from. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_on_sale_from( $context = 'view' ) { + return $this->get_prop( 'date_on_sale_from', $context ); + } + + /** + * Get date on sale to. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_on_sale_to( $context = 'view' ) { + return $this->get_prop( 'date_on_sale_to', $context ); + } + + /** + * Get number total of sales. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return int + */ + public function get_total_sales( $context = 'view' ) { + return $this->get_prop( 'total_sales', $context ); + } + + /** + * Returns the tax status. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_tax_status( $context = 'view' ) { + return $this->get_prop( 'tax_status', $context ); + } + + /** + * Returns the tax class. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_tax_class( $context = 'view' ) { + return $this->get_prop( 'tax_class', $context ); + } + + /** + * Return if product manage stock. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return boolean + */ + public function get_manage_stock( $context = 'view' ) { + return $this->get_prop( 'manage_stock', $context ); + } + + /** + * Returns number of items available for sale. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return int|null + */ + public function get_stock_quantity( $context = 'view' ) { + return $this->get_prop( 'stock_quantity', $context ); + } + + /** + * Return the stock status. + * + * @param string $context What the value is for. Valid values are view and edit. + * @since 3.0.0 + * @return string + */ + public function get_stock_status( $context = 'view' ) { + return $this->get_prop( 'stock_status', $context ); + } + + /** + * Get backorders. + * + * @param string $context What the value is for. Valid values are view and edit. + * @since 3.0.0 + * @return string yes no or notify + */ + public function get_backorders( $context = 'view' ) { + return $this->get_prop( 'backorders', $context ); + } + + /** + * Get low stock amount. + * + * @param string $context What the value is for. Valid values are view and edit. + * @since 3.5.0 + * @return int|string Returns empty string if value not set + */ + public function get_low_stock_amount( $context = 'view' ) { + return $this->get_prop( 'low_stock_amount', $context ); + } + + /** + * Return if should be sold individually. + * + * @param string $context What the value is for. Valid values are view and edit. + * @since 3.0.0 + * @return boolean + */ + public function get_sold_individually( $context = 'view' ) { + return $this->get_prop( 'sold_individually', $context ); + } + + /** + * Returns the product's weight. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_weight( $context = 'view' ) { + return $this->get_prop( 'weight', $context ); + } + + /** + * Returns the product length. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_length( $context = 'view' ) { + return $this->get_prop( 'length', $context ); + } + + /** + * Returns the product width. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_width( $context = 'view' ) { + return $this->get_prop( 'width', $context ); + } + + /** + * Returns the product height. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_height( $context = 'view' ) { + return $this->get_prop( 'height', $context ); + } + + /** + * Returns formatted dimensions. + * + * @param bool $formatted True by default for legacy support - will be false/not set in future versions to return the array only. Use wc_format_dimensions for formatted versions instead. + * @return string|array + */ + public function get_dimensions( $formatted = true ) { + if ( $formatted ) { + wc_deprecated_argument( 'WC_Product::get_dimensions', '3.0', 'By default, get_dimensions has an argument set to true so that HTML is returned. This is to support the legacy version of the method. To get HTML dimensions, instead use wc_format_dimensions() function. Pass false to this method to return an array of dimensions. This will be the new default behavior in future versions.' ); + return apply_filters( 'woocommerce_product_dimensions', wc_format_dimensions( $this->get_dimensions( false ) ), $this ); + } + return array( + 'length' => $this->get_length(), + 'width' => $this->get_width(), + 'height' => $this->get_height(), + ); + } + + /** + * Get upsell IDs. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return array + */ + public function get_upsell_ids( $context = 'view' ) { + return $this->get_prop( 'upsell_ids', $context ); + } + + /** + * Get cross sell IDs. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return array + */ + public function get_cross_sell_ids( $context = 'view' ) { + return $this->get_prop( 'cross_sell_ids', $context ); + } + + /** + * Get parent ID. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return int + */ + public function get_parent_id( $context = 'view' ) { + return $this->get_prop( 'parent_id', $context ); + } + + /** + * Return if reviews is allowed. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return bool + */ + public function get_reviews_allowed( $context = 'view' ) { + return $this->get_prop( 'reviews_allowed', $context ); + } + + /** + * Get purchase note. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_purchase_note( $context = 'view' ) { + return $this->get_prop( 'purchase_note', $context ); + } + + /** + * Returns product attributes. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return array + */ + public function get_attributes( $context = 'view' ) { + return $this->get_prop( 'attributes', $context ); + } + + /** + * Get default attributes. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return array + */ + public function get_default_attributes( $context = 'view' ) { + return $this->get_prop( 'default_attributes', $context ); + } + + /** + * Get menu order. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return int + */ + public function get_menu_order( $context = 'view' ) { + return $this->get_prop( 'menu_order', $context ); + } + + /** + * Get post password. + * + * @since 3.6.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return int + */ + public function get_post_password( $context = 'view' ) { + return $this->get_prop( 'post_password', $context ); + } + + /** + * Get category ids. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return array + */ + public function get_category_ids( $context = 'view' ) { + return $this->get_prop( 'category_ids', $context ); + } + + /** + * Get tag ids. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return array + */ + public function get_tag_ids( $context = 'view' ) { + return $this->get_prop( 'tag_ids', $context ); + } + + /** + * Get virtual. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return bool + */ + public function get_virtual( $context = 'view' ) { + return $this->get_prop( 'virtual', $context ); + } + + /** + * Returns the gallery attachment ids. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return array + */ + public function get_gallery_image_ids( $context = 'view' ) { + return $this->get_prop( 'gallery_image_ids', $context ); + } + + /** + * Get shipping class ID. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return int + */ + public function get_shipping_class_id( $context = 'view' ) { + return $this->get_prop( 'shipping_class_id', $context ); + } + + /** + * Get downloads. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return array + */ + public function get_downloads( $context = 'view' ) { + return $this->get_prop( 'downloads', $context ); + } + + /** + * Get download expiry. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return int + */ + public function get_download_expiry( $context = 'view' ) { + return $this->get_prop( 'download_expiry', $context ); + } + + /** + * Get downloadable. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return bool + */ + public function get_downloadable( $context = 'view' ) { + return $this->get_prop( 'downloadable', $context ); + } + + /** + * Get download limit. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return int + */ + public function get_download_limit( $context = 'view' ) { + return $this->get_prop( 'download_limit', $context ); + } + + /** + * Get main image ID. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_image_id( $context = 'view' ) { + return $this->get_prop( 'image_id', $context ); + } + + /** + * Get rating count. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return array of counts + */ + public function get_rating_counts( $context = 'view' ) { + return $this->get_prop( 'rating_counts', $context ); + } + + /** + * Get average rating. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return float + */ + public function get_average_rating( $context = 'view' ) { + return $this->get_prop( 'average_rating', $context ); + } + + /** + * Get review count. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return int + */ + public function get_review_count( $context = 'view' ) { + return $this->get_prop( 'review_count', $context ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + | + | Functions for setting product data. These should not update anything in the + | database itself and should only change what is stored in the class + | object. + */ + + /** + * Set product name. + * + * @since 3.0.0 + * @param string $name Product name. + */ + public function set_name( $name ) { + $this->set_prop( 'name', $name ); + } + + /** + * Set product slug. + * + * @since 3.0.0 + * @param string $slug Product slug. + */ + public function set_slug( $slug ) { + $this->set_prop( 'slug', $slug ); + } + + /** + * Set product created date. + * + * @since 3.0.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. + */ + public function set_date_created( $date = null ) { + $this->set_date_prop( 'date_created', $date ); + } + + /** + * Set product modified date. + * + * @since 3.0.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. + */ + public function set_date_modified( $date = null ) { + $this->set_date_prop( 'date_modified', $date ); + } + + /** + * Set product status. + * + * @since 3.0.0 + * @param string $status Product status. + */ + public function set_status( $status ) { + $this->set_prop( 'status', $status ); + } + + /** + * Set if the product is featured. + * + * @since 3.0.0 + * @param bool|string $featured Whether the product is featured or not. + */ + public function set_featured( $featured ) { + $this->set_prop( 'featured', wc_string_to_bool( $featured ) ); + } + + /** + * Set catalog visibility. + * + * @since 3.0.0 + * @throws WC_Data_Exception Throws exception when invalid data is found. + * @param string $visibility Options: 'hidden', 'visible', 'search' and 'catalog'. + */ + public function set_catalog_visibility( $visibility ) { + $options = array_keys( wc_get_product_visibility_options() ); + if ( ! in_array( $visibility, $options, true ) ) { + $this->error( 'product_invalid_catalog_visibility', __( 'Invalid catalog visibility option.', 'woocommerce' ) ); + } + $this->set_prop( 'catalog_visibility', $visibility ); + } + + /** + * Set product description. + * + * @since 3.0.0 + * @param string $description Product description. + */ + public function set_description( $description ) { + $this->set_prop( 'description', $description ); + } + + /** + * Set product short description. + * + * @since 3.0.0 + * @param string $short_description Product short description. + */ + public function set_short_description( $short_description ) { + $this->set_prop( 'short_description', $short_description ); + } + + /** + * Set SKU. + * + * @since 3.0.0 + * @throws WC_Data_Exception Throws exception when invalid data is found. + * @param string $sku Product SKU. + */ + public function set_sku( $sku ) { + $sku = (string) $sku; + if ( $this->get_object_read() && ! empty( $sku ) && ! wc_product_has_unique_sku( $this->get_id(), $sku ) ) { + $sku_found = wc_get_product_id_by_sku( $sku ); + + $this->error( 'product_invalid_sku', __( 'Invalid or duplicated SKU.', 'woocommerce' ), 400, array( 'resource_id' => $sku_found ) ); + } + $this->set_prop( 'sku', $sku ); + } + + /** + * Set the product's active price. + * + * @param string $price Price. + */ + public function set_price( $price ) { + $this->set_prop( 'price', wc_format_decimal( $price ) ); + } + + /** + * Set the product's regular price. + * + * @since 3.0.0 + * @param string $price Regular price. + */ + public function set_regular_price( $price ) { + $this->set_prop( 'regular_price', wc_format_decimal( $price ) ); + } + + /** + * Set the product's sale price. + * + * @since 3.0.0 + * @param string $price sale price. + */ + public function set_sale_price( $price ) { + $this->set_prop( 'sale_price', wc_format_decimal( $price ) ); + } + + /** + * Set date on sale from. + * + * @since 3.0.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. + */ + public function set_date_on_sale_from( $date = null ) { + $this->set_date_prop( 'date_on_sale_from', $date ); + } + + /** + * Set date on sale to. + * + * @since 3.0.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. + */ + public function set_date_on_sale_to( $date = null ) { + $this->set_date_prop( 'date_on_sale_to', $date ); + } + + /** + * Set number total of sales. + * + * @since 3.0.0 + * @param int $total Total of sales. + */ + public function set_total_sales( $total ) { + $this->set_prop( 'total_sales', absint( $total ) ); + } + + /** + * Set the tax status. + * + * @since 3.0.0 + * @throws WC_Data_Exception Throws exception when invalid data is found. + * @param string $status Tax status. + */ + public function set_tax_status( $status ) { + $options = array( + 'taxable', + 'shipping', + 'none', + ); + + // Set default if empty. + if ( empty( $status ) ) { + $status = 'taxable'; + } + + if ( ! in_array( $status, $options, true ) ) { + $this->error( 'product_invalid_tax_status', __( 'Invalid product tax status.', 'woocommerce' ) ); + } + + $this->set_prop( 'tax_status', $status ); + } + + /** + * Set the tax class. + * + * @since 3.0.0 + * @param string $class Tax class. + */ + public function set_tax_class( $class ) { + $class = sanitize_title( $class ); + $class = 'standard' === $class ? '' : $class; + $valid_classes = $this->get_valid_tax_classes(); + + if ( ! in_array( $class, $valid_classes, true ) ) { + $class = ''; + } + + $this->set_prop( 'tax_class', $class ); + } + + /** + * Return an array of valid tax classes + * + * @return array valid tax classes + */ + protected function get_valid_tax_classes() { + return WC_Tax::get_tax_class_slugs(); + } + + /** + * Set if product manage stock. + * + * @since 3.0.0 + * @param bool $manage_stock Whether or not manage stock is enabled. + */ + public function set_manage_stock( $manage_stock ) { + $this->set_prop( 'manage_stock', wc_string_to_bool( $manage_stock ) ); + } + + /** + * Set number of items available for sale. + * + * @since 3.0.0 + * @param float|null $quantity Stock quantity. + */ + public function set_stock_quantity( $quantity ) { + $this->set_prop( 'stock_quantity', '' !== $quantity ? wc_stock_amount( $quantity ) : null ); + } + + /** + * Set stock status. + * + * @param string $status New status. + */ + public function set_stock_status( $status = 'instock' ) { + $valid_statuses = wc_get_product_stock_status_options(); + + if ( isset( $valid_statuses[ $status ] ) ) { + $this->set_prop( 'stock_status', $status ); + } else { + $this->set_prop( 'stock_status', 'instock' ); + } + } + + /** + * Set backorders. + * + * @since 3.0.0 + * @param string $backorders Options: 'yes', 'no' or 'notify'. + */ + public function set_backorders( $backorders ) { + $this->set_prop( 'backorders', $backorders ); + } + + /** + * Set low stock amount. + * + * @param int|string $amount Empty string if value not set. + * @since 3.5.0 + */ + public function set_low_stock_amount( $amount ) { + $this->set_prop( 'low_stock_amount', '' === $amount ? '' : absint( $amount ) ); + } + + /** + * Set if should be sold individually. + * + * @since 3.0.0 + * @param bool $sold_individually Whether or not product is sold individually. + */ + public function set_sold_individually( $sold_individually ) { + $this->set_prop( 'sold_individually', wc_string_to_bool( $sold_individually ) ); + } + + /** + * Set the product's weight. + * + * @since 3.0.0 + * @param float|string $weight Total weight. + */ + public function set_weight( $weight ) { + $this->set_prop( 'weight', '' === $weight ? '' : wc_format_decimal( $weight ) ); + } + + /** + * Set the product length. + * + * @since 3.0.0 + * @param float|string $length Total length. + */ + public function set_length( $length ) { + $this->set_prop( 'length', '' === $length ? '' : wc_format_decimal( $length ) ); + } + + /** + * Set the product width. + * + * @since 3.0.0 + * @param float|string $width Total width. + */ + public function set_width( $width ) { + $this->set_prop( 'width', '' === $width ? '' : wc_format_decimal( $width ) ); + } + + /** + * Set the product height. + * + * @since 3.0.0 + * @param float|string $height Total height. + */ + public function set_height( $height ) { + $this->set_prop( 'height', '' === $height ? '' : wc_format_decimal( $height ) ); + } + + /** + * Set upsell IDs. + * + * @since 3.0.0 + * @param array $upsell_ids IDs from the up-sell products. + */ + public function set_upsell_ids( $upsell_ids ) { + $this->set_prop( 'upsell_ids', array_filter( (array) $upsell_ids ) ); + } + + /** + * Set crosssell IDs. + * + * @since 3.0.0 + * @param array $cross_sell_ids IDs from the cross-sell products. + */ + public function set_cross_sell_ids( $cross_sell_ids ) { + $this->set_prop( 'cross_sell_ids', array_filter( (array) $cross_sell_ids ) ); + } + + /** + * Set parent ID. + * + * @since 3.0.0 + * @param int $parent_id Product parent ID. + */ + public function set_parent_id( $parent_id ) { + $this->set_prop( 'parent_id', absint( $parent_id ) ); + } + + /** + * Set if reviews is allowed. + * + * @since 3.0.0 + * @param bool $reviews_allowed Reviews allowed or not. + */ + public function set_reviews_allowed( $reviews_allowed ) { + $this->set_prop( 'reviews_allowed', wc_string_to_bool( $reviews_allowed ) ); + } + + /** + * Set purchase note. + * + * @since 3.0.0 + * @param string $purchase_note Purchase note. + */ + public function set_purchase_note( $purchase_note ) { + $this->set_prop( 'purchase_note', $purchase_note ); + } + + /** + * Set product attributes. + * + * Attributes are made up of: + * id - 0 for product level attributes. ID for global attributes. + * name - Attribute name. + * options - attribute value or array of term ids/names. + * position - integer sort order. + * visible - If visible on frontend. + * variation - If used for variations. + * Indexed by unqiue key to allow clearing old ones after a set. + * + * @since 3.0.0 + * @param array $raw_attributes Array of WC_Product_Attribute objects. + */ + public function set_attributes( $raw_attributes ) { + $attributes = array_fill_keys( array_keys( $this->get_attributes( 'edit' ) ), null ); + foreach ( $raw_attributes as $attribute ) { + if ( is_a( $attribute, 'WC_Product_Attribute' ) ) { + $attributes[ sanitize_title( $attribute->get_name() ) ] = $attribute; + } + } + + uasort( $attributes, 'wc_product_attribute_uasort_comparison' ); + $this->set_prop( 'attributes', $attributes ); + } + + /** + * Set default attributes. These will be saved as strings and should map to attribute values. + * + * @since 3.0.0 + * @param array $default_attributes List of default attributes. + */ + public function set_default_attributes( $default_attributes ) { + $this->set_prop( 'default_attributes', array_map( 'strval', array_filter( (array) $default_attributes, 'wc_array_filter_default_attributes' ) ) ); + } + + /** + * Set menu order. + * + * @since 3.0.0 + * @param int $menu_order Menu order. + */ + public function set_menu_order( $menu_order ) { + $this->set_prop( 'menu_order', intval( $menu_order ) ); + } + + /** + * Set post password. + * + * @since 3.6.0 + * @param int $post_password Post password. + */ + public function set_post_password( $post_password ) { + $this->set_prop( 'post_password', $post_password ); + } + + /** + * Set the product categories. + * + * @since 3.0.0 + * @param array $term_ids List of terms IDs. + */ + public function set_category_ids( $term_ids ) { + $this->set_prop( 'category_ids', array_unique( array_map( 'intval', $term_ids ) ) ); + } + + /** + * Set the product tags. + * + * @since 3.0.0 + * @param array $term_ids List of terms IDs. + */ + public function set_tag_ids( $term_ids ) { + $this->set_prop( 'tag_ids', array_unique( array_map( 'intval', $term_ids ) ) ); + } + + /** + * Set if the product is virtual. + * + * @since 3.0.0 + * @param bool|string $virtual Whether product is virtual or not. + */ + public function set_virtual( $virtual ) { + $this->set_prop( 'virtual', wc_string_to_bool( $virtual ) ); + } + + /** + * Set shipping class ID. + * + * @since 3.0.0 + * @param int $id Product shipping class id. + */ + public function set_shipping_class_id( $id ) { + $this->set_prop( 'shipping_class_id', absint( $id ) ); + } + + /** + * Set if the product is downloadable. + * + * @since 3.0.0 + * @param bool|string $downloadable Whether product is downloadable or not. + */ + public function set_downloadable( $downloadable ) { + $this->set_prop( 'downloadable', wc_string_to_bool( $downloadable ) ); + } + + /** + * Set downloads. + * + * @throws WC_Data_Exception If an error relating to one of the downloads is encountered. + * + * @param array $downloads_array Array of WC_Product_Download objects or arrays. + * + * @since 3.0.0 + */ + public function set_downloads( $downloads_array ) { + // When the object is first hydrated, only the previously persisted downloads will be passed in. + $existing_downloads = $this->get_object_read() ? (array) $this->get_prop( 'downloads' ) : $downloads_array; + $downloads = array(); + $errors = array(); + + $downloads_array = $this->build_downloads_map( $downloads_array ); + $existing_downloads = $this->build_downloads_map( $existing_downloads ); + + foreach ( $downloads_array as $download ) { + $download_id = $download->get_id(); + $is_new = ! isset( $existing_downloads[ $download_id ] ); + + try { + $download->check_is_valid( $this->get_object_read() ); + $downloads[ $download_id ] = $download; + } catch ( Exception $e ) { + // We only add error messages for newly added downloads (let's not overwhelm the user if there are + // multiple existing files which are problematic). + if ( $is_new ) { + $errors[] = $e->getMessage(); + } + + // If the problem is with an existing download, disable it. + if ( ! $is_new ) { + $download->set_enabled( false ); + $downloads[ $download_id ] = $download; + } + } + } + + $this->set_prop( 'downloads', $downloads ); + + if ( $errors && $this->get_object_read() ) { + $this->error( 'product_invalid_download', $errors[0] ); + } + } + + /** + * Takes an array of downloadable file representations and converts it into an array of + * WC_Product_Download objects, indexed by download ID. + * + * @param array[]|WC_Product_Download[] $downloads Download data to be re-mapped. + * + * @return WC_Product_Download[] + */ + private function build_downloads_map( array $downloads ): array { + $downloads_map = array(); + + foreach ( $downloads as $download_data ) { + // If the item is already a WC_Product_Download we can add it to the map and move on. + if ( is_a( $download_data, 'WC_Product_Download' ) ) { + $downloads_map[ $download_data->get_id() ] = $download_data; + continue; + } + + // If the item is not an array, there is nothing else we can do (bad data). + if ( ! is_array( $download_data ) ) { + continue; + } + + // Otherwise, transform the array to a WC_Product_Download and add to the map. + $download_object = new WC_Product_Download(); + + // If we don't have a previous hash, generate UUID for download. + if ( empty( $download_data['download_id'] ) ) { + $download_data['download_id'] = wp_generate_uuid4(); + } + + $download_object->set_id( $download_data['download_id'] ); + $download_object->set_name( $download_data['name'] ); + $download_object->set_file( $download_data['file'] ); + $download_object->set_enabled( isset( $download_data['enabled'] ) ? $download_data['enabled'] : true ); + + $downloads_map[ $download_object->get_id() ] = $download_object; + } + + return $downloads_map; + } + + /** + * Set download limit. + * + * @since 3.0.0 + * @param int|string $download_limit Product download limit. + */ + public function set_download_limit( $download_limit ) { + $this->set_prop( 'download_limit', -1 === (int) $download_limit || '' === $download_limit ? -1 : absint( $download_limit ) ); + } + + /** + * Set download expiry. + * + * @since 3.0.0 + * @param int|string $download_expiry Product download expiry. + */ + public function set_download_expiry( $download_expiry ) { + $this->set_prop( 'download_expiry', -1 === (int) $download_expiry || '' === $download_expiry ? -1 : absint( $download_expiry ) ); + } + + /** + * Set gallery attachment ids. + * + * @since 3.0.0 + * @param array $image_ids List of image ids. + */ + public function set_gallery_image_ids( $image_ids ) { + $image_ids = wp_parse_id_list( $image_ids ); + + $this->set_prop( 'gallery_image_ids', $image_ids ); + } + + /** + * Set main image ID. + * + * @since 3.0.0 + * @param int|string $image_id Product image id. + */ + public function set_image_id( $image_id = '' ) { + $this->set_prop( 'image_id', $image_id ); + } + + /** + * Set rating counts. Read only. + * + * @param array $counts Product rating counts. + */ + public function set_rating_counts( $counts ) { + $this->set_prop( 'rating_counts', array_filter( array_map( 'absint', (array) $counts ) ) ); + } + + /** + * Set average rating. Read only. + * + * @param float $average Product average rating. + */ + public function set_average_rating( $average ) { + $this->set_prop( 'average_rating', wc_format_decimal( $average ) ); + } + + /** + * Set review count. Read only. + * + * @param int $count Product review count. + */ + public function set_review_count( $count ) { + $this->set_prop( 'review_count', absint( $count ) ); + } + + /* + |-------------------------------------------------------------------------- + | Other Methods + |-------------------------------------------------------------------------- + */ + + /** + * Ensure properties are set correctly before save. + * + * @since 3.0.0 + */ + public function validate_props() { + // Before updating, ensure stock props are all aligned. Qty, backorders and low stock amount are not needed if not stock managed. + if ( ! $this->get_manage_stock() ) { + $this->set_stock_quantity( '' ); + $this->set_backorders( 'no' ); + $this->set_low_stock_amount( '' ); + return; + } + + $stock_is_above_notification_threshold = ( $this->get_stock_quantity() > get_option( 'woocommerce_notify_no_stock_amount', 0 ) ); + $backorders_are_allowed = ( 'no' !== $this->get_backorders() ); + + if ( $stock_is_above_notification_threshold ) { + $new_stock_status = 'instock'; + } elseif ( $backorders_are_allowed ) { + $new_stock_status = 'onbackorder'; + } else { + $new_stock_status = 'outofstock'; + } + + $this->set_stock_status( $new_stock_status ); + } + + /** + * Save data (either create or update depending on if we are working on an existing product). + * + * @since 3.0.0 + * @return int + */ + public function save() { + $this->validate_props(); + + if ( ! $this->data_store ) { + return $this->get_id(); + } + + /** + * Trigger action before saving to the DB. Allows you to adjust object props before save. + * + * @param WC_Data $this The object being saved. + * @param WC_Data_Store_WP $data_store THe data store persisting the data. + */ + do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store ); + + $state = $this->before_data_store_save_or_update(); + + if ( $this->get_id() ) { + $changeset = $this->get_changes(); + $this->data_store->update( $this ); + } else { + $changeset = null; + $this->data_store->create( $this ); + } + + $this->after_data_store_save_or_update( $state ); + + // Update attributes lookup table if the product is new OR it's not but there are actually any changes. + if ( is_null( $changeset ) || ! empty( $changeset ) ) { + wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_changed( $this, $changeset ); + } + + /** + * Trigger action after saving to the DB. + * + * @param WC_Data $this The object being saved. + * @param WC_Data_Store_WP $data_store THe data store persisting the data. + */ + do_action( 'woocommerce_after_' . $this->object_type . '_object_save', $this, $this->data_store ); + + return $this->get_id(); + } + + /** + * Do any extra processing needed before the actual product save + * (but after triggering the 'woocommerce_before_..._object_save' action) + * + * @return mixed A state value that will be passed to after_data_store_save_or_update. + */ + protected function before_data_store_save_or_update() { + } + + /** + * Do any extra processing needed after the actual product save + * (but before triggering the 'woocommerce_after_..._object_save' action) + * + * @param mixed $state The state object that was returned by before_data_store_save_or_update. + */ + protected function after_data_store_save_or_update( $state ) { + $this->maybe_defer_product_sync(); + } + + /** + * Delete the product, set its ID to 0, and return result. + * + * @param bool $force_delete Should the product be deleted permanently. + * @return bool result + */ + public function delete( $force_delete = false ) { + $product_id = $this->get_id(); + $deleted = parent::delete( $force_delete ); + + if ( $deleted ) { + $this->maybe_defer_product_sync(); + wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_deleted( $product_id ); + } + + return $deleted; + } + + /** + * If this is a child product, queue its parent for syncing at the end of the request. + */ + protected function maybe_defer_product_sync() { + $parent_id = $this->get_parent_id(); + if ( $parent_id ) { + wc_deferred_product_sync( $parent_id ); + } + } + + /* + |-------------------------------------------------------------------------- + | Conditionals + |-------------------------------------------------------------------------- + */ + + /** + * Check if a product supports a given feature. + * + * Product classes should override this to declare support (or lack of support) for a feature. + * + * @param string $feature string The name of a feature to test support for. + * @return bool True if the product supports the feature, false otherwise. + * @since 2.5.0 + */ + public function supports( $feature ) { + return apply_filters( 'woocommerce_product_supports', in_array( $feature, $this->supports, true ), $feature, $this ); + } + + /** + * Returns whether or not the product post exists. + * + * @return bool + */ + public function exists() { + return false !== $this->get_status(); + } + + /** + * Checks the product type. + * + * Backwards compatibility with downloadable/virtual. + * + * @param string|array $type Array or string of types. + * @return bool + */ + public function is_type( $type ) { + return ( $this->get_type() === $type || ( is_array( $type ) && in_array( $this->get_type(), $type, true ) ) ); + } + + /** + * Checks if a product is downloadable. + * + * @return bool + */ + public function is_downloadable() { + return apply_filters( 'woocommerce_is_downloadable', true === $this->get_downloadable(), $this ); + } + + /** + * Checks if a product is virtual (has no shipping). + * + * @return bool + */ + public function is_virtual() { + return apply_filters( 'woocommerce_is_virtual', true === $this->get_virtual(), $this ); + } + + /** + * Returns whether or not the product is featured. + * + * @return bool + */ + public function is_featured() { + return true === $this->get_featured(); + } + + /** + * Check if a product is sold individually (no quantities). + * + * @return bool + */ + public function is_sold_individually() { + return apply_filters( 'woocommerce_is_sold_individually', true === $this->get_sold_individually(), $this ); + } + + /** + * Returns whether or not the product is visible in the catalog. + * + * @return bool + */ + public function is_visible() { + $visible = $this->is_visible_core(); + return apply_filters( 'woocommerce_product_is_visible', $visible, $this->get_id() ); + } + + /** + * Returns whether or not the product is visible in the catalog (doesn't trigger filters). + * + * @return bool + */ + protected function is_visible_core() { + $visible = 'visible' === $this->get_catalog_visibility() || ( is_search() && 'search' === $this->get_catalog_visibility() ) || ( ! is_search() && 'catalog' === $this->get_catalog_visibility() ); + + if ( 'trash' === $this->get_status() ) { + $visible = false; + } elseif ( 'publish' !== $this->get_status() && ! current_user_can( 'edit_post', $this->get_id() ) ) { + $visible = false; + } + + if ( $this->get_parent_id() ) { + $parent_product = wc_get_product( $this->get_parent_id() ); + + if ( $parent_product && 'publish' !== $parent_product->get_status() ) { + $visible = false; + } + } + + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $this->is_in_stock() ) { + $visible = false; + } + + return $visible; + } + + /** + * Returns false if the product cannot be bought. + * + * @return bool + */ + public function is_purchasable() { + return apply_filters( 'woocommerce_is_purchasable', $this->exists() && ( 'publish' === $this->get_status() || current_user_can( 'edit_post', $this->get_id() ) ) && '' !== $this->get_price(), $this ); + } + + /** + * Returns whether or not the product is on sale. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return bool + */ + public function is_on_sale( $context = 'view' ) { + if ( '' !== (string) $this->get_sale_price( $context ) && $this->get_regular_price( $context ) > $this->get_sale_price( $context ) ) { + $on_sale = true; + + if ( $this->get_date_on_sale_from( $context ) && $this->get_date_on_sale_from( $context )->getTimestamp() > time() ) { + $on_sale = false; + } + + if ( $this->get_date_on_sale_to( $context ) && $this->get_date_on_sale_to( $context )->getTimestamp() < time() ) { + $on_sale = false; + } + } else { + $on_sale = false; + } + return 'view' === $context ? apply_filters( 'woocommerce_product_is_on_sale', $on_sale, $this ) : $on_sale; + } + + /** + * Returns whether or not the product has dimensions set. + * + * @return bool + */ + public function has_dimensions() { + return ( $this->get_length() || $this->get_height() || $this->get_width() ) && ! $this->get_virtual(); + } + + /** + * Returns whether or not the product has weight set. + * + * @return bool + */ + public function has_weight() { + return $this->get_weight() && ! $this->get_virtual(); + } + + /** + * Returns whether or not the product can be purchased. + * This returns true for 'instock' and 'onbackorder' stock statuses. + * + * @return bool + */ + public function is_in_stock() { + return apply_filters( 'woocommerce_product_is_in_stock', 'outofstock' !== $this->get_stock_status(), $this ); + } + + /** + * Checks if a product needs shipping. + * + * @return bool + */ + public function needs_shipping() { + return apply_filters( 'woocommerce_product_needs_shipping', ! $this->is_virtual(), $this ); + } + + /** + * Returns whether or not the product is taxable. + * + * @return bool + */ + public function is_taxable() { + return apply_filters( 'woocommerce_product_is_taxable', $this->get_tax_status() === 'taxable' && wc_tax_enabled(), $this ); + } + + /** + * Returns whether or not the product shipping is taxable. + * + * @return bool + */ + public function is_shipping_taxable() { + return $this->needs_shipping() && ( $this->get_tax_status() === 'taxable' || $this->get_tax_status() === 'shipping' ); + } + + /** + * Returns whether or not the product is stock managed. + * + * @return bool + */ + public function managing_stock() { + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + return $this->get_manage_stock(); + } + return false; + } + + /** + * Returns whether or not the product can be backordered. + * + * @return bool + */ + public function backorders_allowed() { + return apply_filters( 'woocommerce_product_backorders_allowed', ( 'yes' === $this->get_backorders() || 'notify' === $this->get_backorders() ), $this->get_id(), $this ); + } + + /** + * Returns whether or not the product needs to notify the customer on backorder. + * + * @return bool + */ + public function backorders_require_notification() { + return apply_filters( 'woocommerce_product_backorders_require_notification', ( $this->managing_stock() && 'notify' === $this->get_backorders() ), $this ); + } + + /** + * Check if a product is on backorder. + * + * @param int $qty_in_cart (default: 0). + * @return bool + */ + public function is_on_backorder( $qty_in_cart = 0 ) { + if ( 'onbackorder' === $this->get_stock_status() ) { + return true; + } + + return $this->managing_stock() && $this->backorders_allowed() && ( $this->get_stock_quantity() - $qty_in_cart ) < 0; + } + + /** + * Returns whether or not the product has enough stock for the order. + * + * @param mixed $quantity Quantity of a product added to an order. + * @return bool + */ + public function has_enough_stock( $quantity ) { + return ! $this->managing_stock() || $this->backorders_allowed() || $this->get_stock_quantity() >= $quantity; + } + + /** + * Returns whether or not the product has any visible attributes. + * + * @return boolean + */ + public function has_attributes() { + foreach ( $this->get_attributes() as $attribute ) { + if ( $attribute->get_visible() ) { + return true; + } + } + return false; + } + + /** + * Returns whether or not the product has any child product. + * + * @return bool + */ + public function has_child() { + return 0 < count( $this->get_children() ); + } + + /** + * Does a child have dimensions? + * + * @since 3.0.0 + * @return bool + */ + public function child_has_dimensions() { + return false; + } + + /** + * Does a child have a weight? + * + * @since 3.0.0 + * @return boolean + */ + public function child_has_weight() { + return false; + } + + /** + * Check if downloadable product has a file attached. + * + * @since 1.6.2 + * + * @param string $download_id file identifier. + * @return bool Whether downloadable product has a file attached. + */ + public function has_file( $download_id = '' ) { + return $this->is_downloadable() && $this->get_file( $download_id ); + } + + /** + * Returns whether or not the product has additional options that need + * selecting before adding to cart. + * + * @since 3.0.0 + * @return boolean + */ + public function has_options() { + return apply_filters( 'woocommerce_product_has_options', false, $this ); + } + + /* + |-------------------------------------------------------------------------- + | Non-CRUD Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get the product's title. For products this is the product name. + * + * @return string + */ + public function get_title() { + return apply_filters( 'woocommerce_product_title', $this->get_name(), $this ); + } + + /** + * Product permalink. + * + * @return string + */ + public function get_permalink() { + return get_permalink( $this->get_id() ); + } + + /** + * Returns the children IDs if applicable. Overridden by child classes. + * + * @return array of IDs + */ + public function get_children() { + return array(); + } + + /** + * If the stock level comes from another product ID, this should be modified. + * + * @since 3.0.0 + * @return int + */ + public function get_stock_managed_by_id() { + return $this->get_id(); + } + + /** + * Returns the price in html format. + * + * @param string $deprecated Deprecated param. + * + * @return string + */ + public function get_price_html( $deprecated = '' ) { + if ( '' === $this->get_price() ) { + $price = apply_filters( 'woocommerce_empty_price_html', '', $this ); + } elseif ( $this->is_on_sale() ) { + $price = wc_format_sale_price( wc_get_price_to_display( $this, array( 'price' => $this->get_regular_price() ) ), wc_get_price_to_display( $this ) ) . $this->get_price_suffix(); + } else { + $price = wc_price( wc_get_price_to_display( $this ) ) . $this->get_price_suffix(); + } + + return apply_filters( 'woocommerce_get_price_html', $price, $this ); + } + + /** + * Get product name with SKU or ID. Used within admin. + * + * @return string Formatted product name + */ + public function get_formatted_name() { + if ( $this->get_sku() ) { + $identifier = $this->get_sku(); + } else { + $identifier = '#' . $this->get_id(); + } + return sprintf( '%2$s (%1$s)', $identifier, $this->get_name() ); + } + + /** + * Get min quantity which can be purchased at once. + * + * @since 3.0.0 + * @return int + */ + public function get_min_purchase_quantity() { + return 1; + } + + /** + * Get max quantity which can be purchased at once. + * + * @since 3.0.0 + * @return int Quantity or -1 if unlimited. + */ + public function get_max_purchase_quantity() { + return $this->is_sold_individually() ? 1 : ( $this->backorders_allowed() || ! $this->managing_stock() ? -1 : $this->get_stock_quantity() ); + } + + /** + * Get the add to url used mainly in loops. + * + * @return string + */ + public function add_to_cart_url() { + return apply_filters( 'woocommerce_product_add_to_cart_url', $this->get_permalink(), $this ); + } + + /** + * Get the add to cart button text for the single page. + * + * @return string + */ + public function single_add_to_cart_text() { + return apply_filters( 'woocommerce_product_single_add_to_cart_text', __( 'Add to cart', 'woocommerce' ), $this ); + } + + /** + * Get the add to cart button text. + * + * @return string + */ + public function add_to_cart_text() { + return apply_filters( 'woocommerce_product_add_to_cart_text', __( 'Read more', 'woocommerce' ), $this ); + } + + /** + * Get the add to cart button text description - used in aria tags. + * + * @since 3.3.0 + * @return string + */ + public function add_to_cart_description() { + /* translators: %s: Product title */ + return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( __( 'Read more about “%s”', 'woocommerce' ), $this->get_name() ), $this ); + } + + /** + * Returns the main product image. + * + * @param string $size (default: 'woocommerce_thumbnail'). + * @param array $attr Image attributes. + * @param bool $placeholder True to return $placeholder if no image is found, or false to return an empty string. + * @return string + */ + public function get_image( $size = 'woocommerce_thumbnail', $attr = array(), $placeholder = true ) { + $image = ''; + if ( $this->get_image_id() ) { + $image = wp_get_attachment_image( $this->get_image_id(), $size, false, $attr ); + } elseif ( $this->get_parent_id() ) { + $parent_product = wc_get_product( $this->get_parent_id() ); + if ( $parent_product ) { + $image = $parent_product->get_image( $size, $attr, $placeholder ); + } + } + + if ( ! $image && $placeholder ) { + $image = wc_placeholder_img( $size, $attr ); + } + + return apply_filters( 'woocommerce_product_get_image', $image, $this, $size, $attr, $placeholder, $image ); + } + + /** + * Returns the product shipping class SLUG. + * + * @return string + */ + public function get_shipping_class() { + $class_id = $this->get_shipping_class_id(); + if ( $class_id ) { + $term = get_term_by( 'id', $class_id, 'product_shipping_class' ); + + if ( $term && ! is_wp_error( $term ) ) { + return $term->slug; + } + } + return ''; + } + + /** + * Returns a single product attribute as a string. + * + * @param string $attribute to get. + * @return string + */ + public function get_attribute( $attribute ) { + $attributes = $this->get_attributes(); + $attribute = sanitize_title( $attribute ); + + if ( isset( $attributes[ $attribute ] ) ) { + $attribute_object = $attributes[ $attribute ]; + } elseif ( isset( $attributes[ 'pa_' . $attribute ] ) ) { + $attribute_object = $attributes[ 'pa_' . $attribute ]; + } else { + return ''; + } + return $attribute_object->is_taxonomy() ? implode( ', ', wc_get_product_terms( $this->get_id(), $attribute_object->get_name(), array( 'fields' => 'names' ) ) ) : wc_implode_text_attributes( $attribute_object->get_options() ); + } + + /** + * Get the total amount (COUNT) of ratings, or just the count for one rating e.g. number of 5 star ratings. + * + * @param int $value Optional. Rating value to get the count for. By default returns the count of all rating values. + * @return int + */ + public function get_rating_count( $value = null ) { + $counts = $this->get_rating_counts(); + + if ( is_null( $value ) ) { + return array_sum( $counts ); + } elseif ( isset( $counts[ $value ] ) ) { + return absint( $counts[ $value ] ); + } else { + return 0; + } + } + + /** + * Get a file by $download_id. + * + * @param string $download_id file identifier. + * @return array|false if not found + */ + public function get_file( $download_id = '' ) { + $files = $this->get_downloads(); + + if ( '' === $download_id ) { + $file = count( $files ) ? current( $files ) : false; + } elseif ( isset( $files[ $download_id ] ) ) { + $file = $files[ $download_id ]; + } else { + $file = false; + } + + return apply_filters( 'woocommerce_product_file', $file, $this, $download_id ); + } + + /** + * Get file download path identified by $download_id. + * + * @param string $download_id file identifier. + * @return string + */ + public function get_file_download_path( $download_id ) { + $files = $this->get_downloads(); + $file_path = isset( $files[ $download_id ] ) ? $files[ $download_id ]->get_file() : ''; + + // allow overriding based on the particular file being requested. + return apply_filters( 'woocommerce_product_file_download_path', $file_path, $this, $download_id ); + } + + /** + * Get the suffix to display after prices > 0. + * + * @param string $price to calculate, left blank to just use get_price(). + * @param integer $qty passed on to get_price_including_tax() or get_price_excluding_tax(). + * @return string + */ + public function get_price_suffix( $price = '', $qty = 1 ) { + $html = ''; + + $suffix = get_option( 'woocommerce_price_display_suffix' ); + if ( $suffix && wc_tax_enabled() && 'taxable' === $this->get_tax_status() ) { + if ( '' === $price ) { + $price = $this->get_price(); + } + $replacements = array( + '{price_including_tax}' => wc_price( wc_get_price_including_tax( $this, array( 'qty' => $qty, 'price' => $price ) ) ), // @phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine, WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound + '{price_excluding_tax}' => wc_price( wc_get_price_excluding_tax( $this, array( 'qty' => $qty, 'price' => $price ) ) ), // @phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound + ); + $html = str_replace( array_keys( $replacements ), array_values( $replacements ), ' ' . wp_kses_post( $suffix ) . '' ); + } + return apply_filters( 'woocommerce_get_price_suffix', $html, $this, $price, $qty ); + } + + /** + * Returns the availability of the product. + * + * @return string[] + */ + public function get_availability() { + return apply_filters( + 'woocommerce_get_availability', + array( + 'availability' => $this->get_availability_text(), + 'class' => $this->get_availability_class(), + ), + $this + ); + } + + /** + * Get availability text based on stock status. + * + * @return string + */ + protected function get_availability_text() { + if ( ! $this->is_in_stock() ) { + $availability = __( 'Out of stock', 'woocommerce' ); + } elseif ( $this->managing_stock() && $this->is_on_backorder( 1 ) ) { + $availability = $this->backorders_require_notification() ? __( 'Available on backorder', 'woocommerce' ) : ''; + } elseif ( ! $this->managing_stock() && $this->is_on_backorder( 1 ) ) { + $availability = __( 'Available on backorder', 'woocommerce' ); + } elseif ( $this->managing_stock() ) { + $availability = wc_format_stock_for_display( $this ); + } else { + $availability = ''; + } + return apply_filters( 'woocommerce_get_availability_text', $availability, $this ); + } + + /** + * Get availability classname based on stock status. + * + * @return string + */ + protected function get_availability_class() { + if ( ! $this->is_in_stock() ) { + $class = 'out-of-stock'; + } elseif ( ( $this->managing_stock() && $this->is_on_backorder( 1 ) ) || ( ! $this->managing_stock() && $this->is_on_backorder( 1 ) ) ) { + $class = 'available-on-backorder'; + } else { + $class = 'in-stock'; + } + return apply_filters( 'woocommerce_get_availability_class', $class, $this ); + } +} diff --git a/includes/abstracts/abstract-wc-session.php b/plugins/woocommerce/includes/abstracts/abstract-wc-session.php similarity index 100% rename from includes/abstracts/abstract-wc-session.php rename to plugins/woocommerce/includes/abstracts/abstract-wc-session.php diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-settings-api.php b/plugins/woocommerce/includes/abstracts/abstract-wc-settings-api.php new file mode 100644 index 00000000000..6aa83bc744e --- /dev/null +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-settings-api.php @@ -0,0 +1,977 @@ +id, array_map( array( $this, 'set_defaults' ), $this->form_fields ) ); + } + + /** + * Set default required properties for each field. + * + * @param array $field Setting field array. + * @return array + */ + protected function set_defaults( $field ) { + if ( ! isset( $field['default'] ) ) { + $field['default'] = ''; + } + return $field; + } + + /** + * Output the admin options table. + */ + public function admin_options() { + echo '' . $this->generate_settings_html( $this->get_form_fields(), false ) . '
    '; // WPCS: XSS ok. + } + + /** + * Initialise settings form fields. + * + * Add an array of fields to be displayed on the gateway's settings screen. + * + * @since 1.0.0 + */ + public function init_form_fields() {} + + /** + * Return the name of the option in the WP DB. + * + * @since 2.6.0 + * @return string + */ + public function get_option_key() { + return $this->plugin_id . $this->id . '_settings'; + } + + /** + * Get a fields type. Defaults to "text" if not set. + * + * @param array $field Field key. + * @return string + */ + public function get_field_type( $field ) { + return empty( $field['type'] ) ? 'text' : $field['type']; + } + + /** + * Get a fields default value. Defaults to "" if not set. + * + * @param array $field Field key. + * @return string + */ + public function get_field_default( $field ) { + return empty( $field['default'] ) ? '' : $field['default']; + } + + /** + * Get a field's posted and validated value. + * + * @param string $key Field key. + * @param array $field Field array. + * @param array $post_data Posted data. + * @return string + */ + public function get_field_value( $key, $field, $post_data = array() ) { + $type = $this->get_field_type( $field ); + $field_key = $this->get_field_key( $key ); + $post_data = empty( $post_data ) ? $_POST : $post_data; // WPCS: CSRF ok, input var ok. + $value = isset( $post_data[ $field_key ] ) ? $post_data[ $field_key ] : null; + + if ( isset( $field['sanitize_callback'] ) && is_callable( $field['sanitize_callback'] ) ) { + return call_user_func( $field['sanitize_callback'], $value ); + } + + // Look for a validate_FIELDID_field method for special handling. + if ( is_callable( array( $this, 'validate_' . $key . '_field' ) ) ) { + return $this->{'validate_' . $key . '_field'}( $key, $value ); + } + + // Look for a validate_FIELDTYPE_field method. + if ( is_callable( array( $this, 'validate_' . $type . '_field' ) ) ) { + return $this->{'validate_' . $type . '_field'}( $key, $value ); + } + + // Fallback to text. + return $this->validate_text_field( $key, $value ); + } + + /** + * Sets the POSTed data. This method can be used to set specific data, instead of taking it from the $_POST array. + * + * @param array $data Posted data. + */ + public function set_post_data( $data = array() ) { + $this->data = $data; + } + + /** + * Returns the POSTed data, to be used to save the settings. + * + * @return array + */ + public function get_post_data() { + if ( ! empty( $this->data ) && is_array( $this->data ) ) { + return $this->data; + } + return $_POST; // WPCS: CSRF ok, input var ok. + } + + /** + * Update a single option. + * + * @since 3.4.0 + * @param string $key Option key. + * @param mixed $value Value to set. + * @return bool was anything saved? + */ + public function update_option( $key, $value = '' ) { + if ( empty( $this->settings ) ) { + $this->init_settings(); + } + + $this->settings[ $key ] = $value; + + return update_option( $this->get_option_key(), apply_filters( 'woocommerce_settings_api_sanitized_fields_' . $this->id, $this->settings ), 'yes' ); + } + + /** + * Processes and saves options. + * If there is an error thrown, will continue to save and validate fields, but will leave the erroring field out. + * + * @return bool was anything saved? + */ + public function process_admin_options() { + $this->init_settings(); + + $post_data = $this->get_post_data(); + + foreach ( $this->get_form_fields() as $key => $field ) { + if ( 'title' !== $this->get_field_type( $field ) ) { + try { + $this->settings[ $key ] = $this->get_field_value( $key, $field, $post_data ); + } catch ( Exception $e ) { + $this->add_error( $e->getMessage() ); + } + } + } + + $option_key = $this->get_option_key(); + do_action( 'woocommerce_update_option', array( 'id' => $option_key ) ); + return update_option( $option_key, apply_filters( 'woocommerce_settings_api_sanitized_fields_' . $this->id, $this->settings ), 'yes' ); + } + + /** + * Add an error message for display in admin on save. + * + * @param string $error Error message. + */ + public function add_error( $error ) { + $this->errors[] = $error; + } + + /** + * Get admin error messages. + */ + public function get_errors() { + return $this->errors; + } + + /** + * Display admin error messages. + */ + public function display_errors() { + if ( $this->get_errors() ) { + echo '
    '; + foreach ( $this->get_errors() as $error ) { + echo '

    ' . wp_kses_post( $error ) . '

    '; + } + echo '
    '; + } + } + + /** + * Initialise Settings. + * + * Store all settings in a single database entry + * and make sure the $settings array is either the default + * or the settings stored in the database. + * + * @since 1.0.0 + * @uses get_option(), add_option() + */ + public function init_settings() { + $this->settings = get_option( $this->get_option_key(), null ); + + // If there are no settings defined, use defaults. + if ( ! is_array( $this->settings ) ) { + $form_fields = $this->get_form_fields(); + $this->settings = array_merge( array_fill_keys( array_keys( $form_fields ), '' ), wp_list_pluck( $form_fields, 'default' ) ); + } + } + + /** + * Get option from DB. + * + * Gets an option from the settings API, using defaults if necessary to prevent undefined notices. + * + * @param string $key Option key. + * @param mixed $empty_value Value when empty. + * @return string The value specified for the option or a default value for the option. + */ + public function get_option( $key, $empty_value = null ) { + if ( empty( $this->settings ) ) { + $this->init_settings(); + } + + // Get option default if unset. + if ( ! isset( $this->settings[ $key ] ) ) { + $form_fields = $this->get_form_fields(); + $this->settings[ $key ] = isset( $form_fields[ $key ] ) ? $this->get_field_default( $form_fields[ $key ] ) : ''; + } + + if ( ! is_null( $empty_value ) && '' === $this->settings[ $key ] ) { + $this->settings[ $key ] = $empty_value; + } + + return $this->settings[ $key ]; + } + + /** + * Prefix key for settings. + * + * @param string $key Field key. + * @return string + */ + public function get_field_key( $key ) { + return $this->plugin_id . $this->id . '_' . $key; + } + + /** + * Generate Settings HTML. + * + * Generate the HTML for the fields on the "settings" screen. + * + * @param array $form_fields (default: array()) Array of form fields. + * @param bool $echo Echo or return. + * @return string the html for the settings + * @since 1.0.0 + * @uses method_exists() + */ + public function generate_settings_html( $form_fields = array(), $echo = true ) { + if ( empty( $form_fields ) ) { + $form_fields = $this->get_form_fields(); + } + + $html = ''; + foreach ( $form_fields as $k => $v ) { + $type = $this->get_field_type( $v ); + + if ( method_exists( $this, 'generate_' . $type . '_html' ) ) { + $html .= $this->{'generate_' . $type . '_html'}( $k, $v ); + } elseif ( has_filter( 'woocommerce_generate_' . $type . '_html' ) ) { + /** + * Allow the generation of custom field types on the settings screen. + * + * The dynamic portion of the hook name refers to the slug of the custom field type. + * For instance, to introduce a new field type `fancy_lazy_dropdown` you would use + * the hook `woocommerce_generate_fancy_lazy_dropdown_html`. + * + * @since 6.5.0 + * + * @param string $field_html The markup of the field being generated (initiated as an empty string). + * @param string $key The key of the field. + * @param array $data The attributes of the field as an associative array. + * @param object $wc_settings The current WC_Settings_API object. + */ + $html .= apply_filters( 'woocommerce_generate_' . $type . '_html', '', $k, $v, $this ); + } else { + $html .= $this->generate_text_html( $k, $v ); + } + } + + if ( $echo ) { + echo $html; // WPCS: XSS ok. + } else { + return $html; + } + } + + /** + * Get HTML for tooltips. + * + * @param array $data Data for the tooltip. + * @return string + */ + public function get_tooltip_html( $data ) { + if ( true === $data['desc_tip'] ) { + $tip = $data['description']; + } elseif ( ! empty( $data['desc_tip'] ) ) { + $tip = $data['desc_tip']; + } else { + $tip = ''; + } + + return $tip ? wc_help_tip( $tip, true ) : ''; + } + + /** + * Get HTML for descriptions. + * + * @param array $data Data for the description. + * @return string + */ + public function get_description_html( $data ) { + if ( true === $data['desc_tip'] ) { + $description = ''; + } elseif ( ! empty( $data['desc_tip'] ) ) { + $description = $data['description']; + } elseif ( ! empty( $data['description'] ) ) { + $description = $data['description']; + } else { + $description = ''; + } + + return $description ? '

    ' . wp_kses_post( $description ) . '

    ' . "\n" : ''; + } + + /** + * Get custom attributes. + * + * @param array $data Field data. + * @return string + */ + public function get_custom_attribute_html( $data ) { + $custom_attributes = array(); + + if ( ! empty( $data['custom_attributes'] ) && is_array( $data['custom_attributes'] ) ) { + foreach ( $data['custom_attributes'] as $attribute => $attribute_value ) { + $custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $attribute_value ) . '"'; + } + } + + return implode( ' ', $custom_attributes ); + } + + /** + * Generate Text Input HTML. + * + * @param string $key Field key. + * @param array $data Field data. + * @since 1.0.0 + * @return string + */ + public function generate_text_html( $key, $data ) { + $field_key = $this->get_field_key( $key ); + $defaults = array( + 'title' => '', + 'disabled' => false, + 'class' => '', + 'css' => '', + 'placeholder' => '', + 'type' => 'text', + 'desc_tip' => false, + 'description' => '', + 'custom_attributes' => array(), + ); + + $data = wp_parse_args( $data, $defaults ); + + ob_start(); + ?> + + + + + +
    + + get_custom_attribute_html( $data ); // WPCS: XSS ok. ?> /> + get_description_html( $data ); // WPCS: XSS ok. ?> +
    + + + get_field_key( $key ); + $defaults = array( + 'title' => '', + 'disabled' => false, + 'class' => '', + 'css' => '', + 'placeholder' => '', + 'type' => 'text', + 'desc_tip' => false, + 'description' => '', + 'custom_attributes' => array(), + ); + + $data = wp_parse_args( $data, $defaults ); + + ob_start(); + ?> + + + + + +
    + + get_custom_attribute_html( $data ); // WPCS: XSS ok. ?> /> + get_description_html( $data ); // WPCS: XSS ok. ?> +
    + + + get_field_key( $key ); + $defaults = array( + 'title' => '', + 'disabled' => false, + 'class' => '', + 'css' => '', + 'placeholder' => '', + 'type' => 'text', + 'desc_tip' => false, + 'description' => '', + 'custom_attributes' => array(), + ); + + $data = wp_parse_args( $data, $defaults ); + + ob_start(); + ?> + + + + + +
    + + get_custom_attribute_html( $data ); // WPCS: XSS ok. ?> /> + get_description_html( $data ); // WPCS: XSS ok. ?> +
    + + + generate_text_html( $key, $data ); + } + + /** + * Generate Color Picker Input HTML. + * + * @param string $key Field key. + * @param array $data Field data. + * @since 1.0.0 + * @return string + */ + public function generate_color_html( $key, $data ) { + $field_key = $this->get_field_key( $key ); + $defaults = array( + 'title' => '', + 'disabled' => false, + 'class' => '', + 'css' => '', + 'placeholder' => '', + 'desc_tip' => false, + 'description' => '', + 'custom_attributes' => array(), + ); + + $data = wp_parse_args( $data, $defaults ); + + ob_start(); + ?> + + + + + +
    + +   + get_custom_attribute_html( $data ); // WPCS: XSS ok. ?> /> + + get_description_html( $data ); // WPCS: XSS ok. ?> +
    + + + get_field_key( $key ); + $defaults = array( + 'title' => '', + 'disabled' => false, + 'class' => '', + 'css' => '', + 'placeholder' => '', + 'type' => 'text', + 'desc_tip' => false, + 'description' => '', + 'custom_attributes' => array(), + ); + + $data = wp_parse_args( $data, $defaults ); + + ob_start(); + ?> + + + + + +
    + + + get_description_html( $data ); // WPCS: XSS ok. ?> +
    + + + get_field_key( $key ); + $defaults = array( + 'title' => '', + 'label' => '', + 'disabled' => false, + 'class' => '', + 'css' => '', + 'type' => 'text', + 'desc_tip' => false, + 'description' => '', + 'custom_attributes' => array(), + ); + + $data = wp_parse_args( $data, $defaults ); + + if ( ! $data['label'] ) { + $data['label'] = $data['title']; + } + + ob_start(); + ?> + + + + + +
    + +
    + get_description_html( $data ); // WPCS: XSS ok. ?> +
    + + + get_field_key( $key ); + $defaults = array( + 'title' => '', + 'disabled' => false, + 'class' => '', + 'css' => '', + 'placeholder' => '', + 'type' => 'text', + 'desc_tip' => false, + 'description' => '', + 'custom_attributes' => array(), + 'options' => array(), + ); + + $data = wp_parse_args( $data, $defaults ); + $value = $this->get_option( $key ); + + ob_start(); + ?> + + + + + +
    + + + get_description_html( $data ); // WPCS: XSS ok. ?> +
    + + + get_field_key( $key ); + $defaults = array( + 'title' => '', + 'disabled' => false, + 'class' => '', + 'css' => '', + 'placeholder' => '', + 'type' => 'text', + 'desc_tip' => false, + 'description' => '', + 'custom_attributes' => array(), + 'select_buttons' => false, + 'options' => array(), + ); + + $data = wp_parse_args( $data, $defaults ); + $value = (array) $this->get_option( $key, array() ); + + ob_start(); + ?> + + + + + +
    + + + get_description_html( $data ); // WPCS: XSS ok. ?> + +
    + +
    + + + get_field_key( $key ); + $defaults = array( + 'title' => '', + 'class' => '', + ); + + $data = wp_parse_args( $data, $defaults ); + + ob_start(); + ?> + +

    + +

    + + + array( + 'src' => true, + 'style' => true, + 'id' => true, + 'class' => true, + ), + ), + wp_kses_allowed_html( 'post' ) + ) + ); + } + + /** + * Validate Checkbox Field. + * + * If not set, return "no", otherwise return "yes". + * + * @param string $key Field key. + * @param string $value Posted Value. + * @return string + */ + public function validate_checkbox_field( $key, $value ) { + return ! is_null( $value ) ? 'yes' : 'no'; + } + + /** + * Validate Select Field. + * + * @param string $key Field key. + * @param string $value Posted Value. + * @return string + */ + public function validate_select_field( $key, $value ) { + $value = is_null( $value ) ? '' : $value; + return wc_clean( stripslashes( $value ) ); + } + + /** + * Validate Multiselect Field. + * + * @param string $key Field key. + * @param string $value Posted Value. + * @return string|array + */ + public function validate_multiselect_field( $key, $value ) { + return is_array( $value ) ? array_map( 'wc_clean', array_map( 'stripslashes', $value ) ) : ''; + } + + /** + * Validate the data on the "Settings" form. + * + * @deprecated 2.6.0 No longer used. + * @param array $form_fields Array of fields. + */ + public function validate_settings_fields( $form_fields = array() ) { + wc_deprecated_function( 'validate_settings_fields', '2.6' ); + } + + /** + * Format settings if needed. + * + * @deprecated 2.6.0 Unused. + * @param array $value Value to format. + * @return array + */ + public function format_settings( $value ) { + wc_deprecated_function( 'format_settings', '2.6' ); + return $value; + } +} diff --git a/includes/abstracts/abstract-wc-shipping-method.php b/plugins/woocommerce/includes/abstracts/abstract-wc-shipping-method.php similarity index 100% rename from includes/abstracts/abstract-wc-shipping-method.php rename to plugins/woocommerce/includes/abstracts/abstract-wc-shipping-method.php diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-widget.php b/plugins/woocommerce/includes/abstracts/abstract-wc-widget.php new file mode 100644 index 00000000000..bb7222ba4f4 --- /dev/null +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-widget.php @@ -0,0 +1,408 @@ + $this->widget_cssclass, + 'description' => $this->widget_description, + 'customize_selective_refresh' => true, + 'show_instance_in_rest' => true, + ); + + parent::__construct( $this->widget_id, $this->widget_name, $widget_ops ); + + add_action( 'save_post', array( $this, 'flush_widget_cache' ) ); + add_action( 'deleted_post', array( $this, 'flush_widget_cache' ) ); + add_action( 'switch_theme', array( $this, 'flush_widget_cache' ) ); + } + + /** + * Get cached widget. + * + * @param array $args Arguments. + * @return bool true if the widget is cached otherwise false + */ + public function get_cached_widget( $args ) { + // Don't get cache if widget_id doesn't exists. + if ( empty( $args['widget_id'] ) ) { + return false; + } + + $cache = wp_cache_get( $this->get_widget_id_for_cache( $this->widget_id ), 'widget' ); + + if ( ! is_array( $cache ) ) { + $cache = array(); + } + + if ( isset( $cache[ $this->get_widget_id_for_cache( $args['widget_id'] ) ] ) ) { + echo $cache[ $this->get_widget_id_for_cache( $args['widget_id'] ) ]; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped + return true; + } + + return false; + } + + /** + * Cache the widget. + * + * @param array $args Arguments. + * @param string $content Content. + * @return string the content that was cached + */ + public function cache_widget( $args, $content ) { + // Don't set any cache if widget_id doesn't exist. + if ( empty( $args['widget_id'] ) ) { + return $content; + } + + $cache = wp_cache_get( $this->get_widget_id_for_cache( $this->widget_id ), 'widget' ); + + if ( ! is_array( $cache ) ) { + $cache = array(); + } + + $cache[ $this->get_widget_id_for_cache( $args['widget_id'] ) ] = $content; + + wp_cache_set( $this->get_widget_id_for_cache( $this->widget_id ), $cache, 'widget' ); + + return $content; + } + + /** + * Flush the cache. + */ + public function flush_widget_cache() { + foreach ( array( 'https', 'http' ) as $scheme ) { + wp_cache_delete( $this->get_widget_id_for_cache( $this->widget_id, $scheme ), 'widget' ); + } + } + + /** + * Get this widgets title. + * + * @param array $instance Array of instance options. + * @return string + */ + protected function get_instance_title( $instance ) { + if ( isset( $instance['title'] ) ) { + return $instance['title']; + } + + if ( isset( $this->settings, $this->settings['title'], $this->settings['title']['std'] ) ) { + return $this->settings['title']['std']; + } + + return ''; + } + + /** + * Output the html at the start of a widget. + * + * @param array $args Arguments. + * @param array $instance Instance. + */ + public function widget_start( $args, $instance ) { + echo $args['before_widget']; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped + + $title = apply_filters( 'widget_title', $this->get_instance_title( $instance ), $instance, $this->id_base ); + + if ( $title ) { + echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped + } + } + + /** + * Output the html at the end of a widget. + * + * @param array $args Arguments. + */ + public function widget_end( $args ) { + echo $args['after_widget']; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped + } + + /** + * Updates a particular instance of a widget. + * + * @see WP_Widget->update + * @param array $new_instance New instance. + * @param array $old_instance Old instance. + * @return array + */ + public function update( $new_instance, $old_instance ) { + + $instance = $old_instance; + + if ( empty( $this->settings ) ) { + return $instance; + } + + // Loop settings and get values to save. + foreach ( $this->settings as $key => $setting ) { + if ( ! isset( $setting['type'] ) ) { + continue; + } + + // Format the value based on settings type. + switch ( $setting['type'] ) { + case 'number': + $instance[ $key ] = absint( $new_instance[ $key ] ); + + if ( isset( $setting['min'] ) && '' !== $setting['min'] ) { + $instance[ $key ] = max( $instance[ $key ], $setting['min'] ); + } + + if ( isset( $setting['max'] ) && '' !== $setting['max'] ) { + $instance[ $key ] = min( $instance[ $key ], $setting['max'] ); + } + break; + case 'textarea': + $instance[ $key ] = wp_kses( trim( wp_unslash( $new_instance[ $key ] ) ), wp_kses_allowed_html( 'post' ) ); + break; + case 'checkbox': + $instance[ $key ] = empty( $new_instance[ $key ] ) ? 0 : 1; + break; + default: + $instance[ $key ] = isset( $new_instance[ $key ] ) ? sanitize_text_field( $new_instance[ $key ] ) : $setting['std']; + break; + } + + /** + * Sanitize the value of a setting. + */ + $instance[ $key ] = apply_filters( 'woocommerce_widget_settings_sanitize_option', $instance[ $key ], $new_instance, $key, $setting ); + } + + $this->flush_widget_cache(); + + return $instance; + } + + /** + * Outputs the settings update form. + * + * @see WP_Widget->form + * + * @param array $instance Instance. + */ + public function form( $instance ) { + + if ( empty( $this->settings ) ) { + return; + } + + foreach ( $this->settings as $key => $setting ) { + + $class = isset( $setting['class'] ) ? $setting['class'] : ''; + $value = isset( $instance[ $key ] ) ? $instance[ $key ] : $setting['std']; + + switch ( $setting['type'] ) { + + case 'text': + ?> +

    + + +

    + +

    + + +

    + +

    + + +

    + +

    + + + + + +

    + +

    + /> + +

    + slug, $queried_object->taxonomy ); + } + + // Min/Max. + if ( isset( $_GET['min_price'] ) ) { + $link = add_query_arg( 'min_price', wc_clean( wp_unslash( $_GET['min_price'] ) ), $link ); + } + + if ( isset( $_GET['max_price'] ) ) { + $link = add_query_arg( 'max_price', wc_clean( wp_unslash( $_GET['max_price'] ) ), $link ); + } + + // Order by. + if ( isset( $_GET['orderby'] ) ) { + $link = add_query_arg( 'orderby', wc_clean( wp_unslash( $_GET['orderby'] ) ), $link ); + } + + /** + * Search Arg. + * To support quote characters, first they are decoded from " entities, then URL encoded. + */ + if ( get_search_query() ) { + $link = add_query_arg( 's', rawurlencode( htmlspecialchars_decode( get_search_query() ) ), $link ); + } + + // Post Type Arg. + if ( isset( $_GET['post_type'] ) ) { + $link = add_query_arg( 'post_type', wc_clean( wp_unslash( $_GET['post_type'] ) ), $link ); + + // Prevent post type and page id when pretty permalinks are disabled. + if ( is_shop() ) { + $link = remove_query_arg( 'page_id', $link ); + } + } + + // Min Rating Arg. + if ( isset( $_GET['rating_filter'] ) ) { + $link = add_query_arg( 'rating_filter', wc_clean( wp_unslash( $_GET['rating_filter'] ) ), $link ); + } + + // All current filters. + if ( $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes() ) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure, WordPress.CodeAnalysis.AssignmentInCondition.Found + foreach ( $_chosen_attributes as $name => $data ) { + $filter_name = wc_attribute_taxonomy_slug( $name ); + if ( ! empty( $data['terms'] ) ) { + $link = add_query_arg( 'filter_' . $filter_name, implode( ',', $data['terms'] ), $link ); + } + if ( 'or' === $data['query_type'] ) { + $link = add_query_arg( 'query_type_' . $filter_name, 'or', $link ); + } + } + } + + return apply_filters( 'woocommerce_widget_get_current_page_url', $link, $this ); + } + + /** + * Get widget id plus scheme/protocol to prevent serving mixed content from (persistently) cached widgets. + * + * @since 3.4.0 + * @param string $widget_id Id of the cached widget. + * @param string $scheme Scheme for the widget id. + * @return string Widget id including scheme/protocol. + */ + protected function get_widget_id_for_cache( $widget_id, $scheme = '' ) { + if ( $scheme ) { + $widget_id_for_cache = $widget_id . '-' . $scheme; + } else { + $widget_id_for_cache = $widget_id . '-' . ( is_ssl() ? 'https' : 'http' ); + } + + return apply_filters( 'woocommerce_cached_widget_id', $widget_id_for_cache ); + } +} diff --git a/includes/abstracts/class-wc-background-process.php b/plugins/woocommerce/includes/abstracts/class-wc-background-process.php similarity index 100% rename from includes/abstracts/class-wc-background-process.php rename to plugins/woocommerce/includes/abstracts/class-wc-background-process.php diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-addons.php b/plugins/woocommerce/includes/admin/class-wc-admin-addons.php new file mode 100644 index 00000000000..b47f5ecd3e1 --- /dev/null +++ b/plugins/woocommerce/includes/admin/class-wc-admin-addons.php @@ -0,0 +1,1485 @@ + $headers, + ) + ); + + if ( ! is_wp_error( $raw_featured ) ) { + $featured = json_decode( wp_remote_retrieve_body( $raw_featured ) ); + if ( $featured ) { + set_transient( 'wc_addons_featured_2', $featured, DAY_IN_SECONDS ); + } + } + } + + if ( is_object( $featured ) ) { + self::output_featured_sections( $featured->sections ); + return $featured; + } + } + + /** + * Render featured products and banners using WCCOM's the Featured 2.0 Endpoint + * + * @return void + */ + public static function render_featured() { + $featured = get_transient( 'wc_addons_featured' ); + if ( false === $featured ) { + $headers = array(); + $auth = WC_Helper_Options::get( 'auth' ); + + if ( ! empty( $auth['access_token'] ) ) { + $headers['Authorization'] = 'Bearer ' . $auth['access_token']; + } + + $parameter_string = ''; + $country = WC()->countries->get_base_country(); + if ( ! empty( $country ) ) { + $parameter_string = '?' . http_build_query( array( 'country' => $country ) ); + } + + // Important: WCCOM Extensions API v2.0 is used. + $raw_featured = wp_safe_remote_get( + 'https://woocommerce.com/wp-json/wccom-extensions/2.0/featured' . $parameter_string, + array( + 'headers' => $headers, + ) + ); + + if ( is_wp_error( $raw_featured ) ) { + do_action( 'woocommerce_page_wc-addons_connection_error', $raw_featured->get_error_message() ); + + $message = self::is_ssl_error( $raw_featured->get_error_message() ) + ? __( 'We encountered an SSL error. Please ensure your site supports TLS version 1.2 or above.', 'woocommerce' ) + : $raw_featured->get_error_message(); + + self::output_empty( $message ); + + return; + } + + $response_code = (int) wp_remote_retrieve_response_code( $raw_featured ); + if ( 200 !== $response_code ) { + do_action( 'woocommerce_page_wc-addons_connection_error', $response_code ); + + /* translators: %d: HTTP error code. */ + $message = sprintf( + esc_html( + /* translators: Error code */ + __( + 'Our request to the featured API got error code %d.', + 'woocommerce' + ) + ), + $response_code + ); + + self::output_empty( $message ); + + return; + } + + $featured = json_decode( wp_remote_retrieve_body( $raw_featured ) ); + if ( empty( $featured ) || ! is_array( $featured ) ) { + do_action( 'woocommerce_page_wc-addons_connection_error', 'Empty or malformed response' ); + $message = __( 'Our request to the featured API got a malformed response.', 'woocommerce' ); + self::output_empty( $message ); + + return; + } + + if ( $featured ) { + set_transient( 'wc_addons_featured', $featured, DAY_IN_SECONDS ); + } + } + + self::output_featured( $featured ); + } + + /** + * Check if the error is due to an SSL error + * + * @param string $error_message Error message. + * + * @return bool True if SSL error, false otherwise + */ + public static function is_ssl_error( $error_message ) { + return false !== stripos( $error_message, 'cURL error 35' ); + } + + /** + * Build url parameter string + * + * @param string $category Addon (sub) category. + * @param string $term Search terms. + * @param string $country Store country. + * + * @return string url parameter string + */ + public static function build_parameter_string( $category, $term, $country ) { + + $parameters = array( + 'category' => $category, + 'term' => $term, + 'country' => $country, + ); + + return '?' . http_build_query( $parameters ); + } + + /** + * Call API to get extensions + * + * @param string $category Addon (sub) category. + * @param string $term Search terms. + * @param string $country Store country. + * + * @return object|WP_Error Object with products and promotions properties, or WP_Error + */ + public static function get_extension_data( $category, $term, $country ) { + $parameters = self::build_parameter_string( $category, $term, $country ); + + $headers = array(); + $auth = WC_Helper_Options::get( 'auth' ); + + if ( ! empty( $auth['access_token'] ) ) { + $headers['Authorization'] = 'Bearer ' . $auth['access_token']; + } + + $raw_extensions = wp_safe_remote_get( + 'https://woocommerce.com/wp-json/wccom-extensions/1.0/search' . $parameters, + array( 'headers' => $headers ) + ); + + if ( is_wp_error( $raw_extensions ) ) { + do_action( 'woocommerce_page_wc-addons_connection_error', $raw_extensions->get_error_message() ); + return $raw_extensions; + } + + $response_code = (int) wp_remote_retrieve_response_code( $raw_extensions ); + if ( 200 !== $response_code ) { + do_action( 'woocommerce_page_wc-addons_connection_error', $response_code ); + return new WP_Error( + 'error', + sprintf( + esc_html( + /* translators: Error code */ + __( 'Our request to the search API got response code %s.', 'woocommerce' ) + ), + $response_code + ) + ); + } + + $addons = json_decode( wp_remote_retrieve_body( $raw_extensions ) ); + + if ( ! is_object( $addons ) || ! isset( $addons->products ) ) { + do_action( 'woocommerce_page_wc-addons_connection_error', 'Empty or malformed response' ); + return new WP_Error( 'error', __( 'Our request to the search API got a malformed response.', 'woocommerce' ) ); + } + + return $addons; + } + + /** + * Get sections for the addons screen + * + * @return array of objects + */ + public static function get_sections() { + $addon_sections = get_transient( 'wc_addons_sections' ); + if ( false === ( $addon_sections ) ) { + $raw_sections = wp_safe_remote_get( + 'https://woocommerce.com/wp-json/wccom-extensions/1.0/categories' + ); + if ( ! is_wp_error( $raw_sections ) ) { + $addon_sections = json_decode( wp_remote_retrieve_body( $raw_sections ) ); + if ( $addon_sections ) { + set_transient( 'wc_addons_sections', $addon_sections, WEEK_IN_SECONDS ); + } + } + } + return apply_filters( 'woocommerce_addons_sections', $addon_sections ); + } + + /** + * Get section for the addons screen. + * + * @param string $section_id Required section ID. + * + * @return object|bool + */ + public static function get_section( $section_id ) { + $sections = self::get_sections(); + if ( isset( $sections[ $section_id ] ) ) { + return $sections[ $section_id ]; + } + return false; + } + + + /** + * Get section content for the addons screen. + * + * @deprecated 5.9.0 No longer used in In-App Marketplace + * + * @param string $section_id Required section ID. + * + * @return array + */ + public static function get_section_data( $section_id ) { + $section = self::get_section( $section_id ); + $section_data = ''; + + if ( ! empty( $section->endpoint ) ) { + $section_data = get_transient( 'wc_addons_section_' . $section_id ); + if ( false === $section_data ) { + $raw_section = wp_safe_remote_get( esc_url_raw( $section->endpoint ) ); + + if ( ! is_wp_error( $raw_section ) ) { + $section_data = json_decode( wp_remote_retrieve_body( $raw_section ) ); + + if ( ! empty( $section_data->products ) ) { + set_transient( 'wc_addons_section_' . $section_id, $section_data, WEEK_IN_SECONDS ); + } + } + } + } + + return apply_filters( 'woocommerce_addons_section_data', $section_data->products, $section_id ); + } + + /** + * Handles the outputting of a contextually aware Storefront link (points to child themes if Storefront is already active). + * + * @deprecated 5.9.0 No longer used in In-App Marketplace + */ + public static function output_storefront_button() { + $template = get_option( 'template' ); + $stylesheet = get_option( 'stylesheet' ); + + if ( 'storefront' === $template ) { + if ( 'storefront' === $stylesheet ) { + $url = 'https://woocommerce.com/product-category/themes/storefront-child-theme-themes/'; + $text = __( 'Need a fresh look? Try Storefront child themes', 'woocommerce' ); + $utm_content = 'nostorefrontchildtheme'; + } else { + $url = 'https://woocommerce.com/product-category/themes/storefront-child-theme-themes/'; + $text = __( 'View more Storefront child themes', 'woocommerce' ); + $utm_content = 'hasstorefrontchildtheme'; + } + } else { + $url = 'https://woocommerce.com/storefront/'; + $text = __( 'Need a theme? Try Storefront', 'woocommerce' ); + $utm_content = 'nostorefront'; + } + + $url = add_query_arg( + array( + 'utm_source' => 'addons', + 'utm_medium' => 'product', + 'utm_campaign' => 'woocommerceplugin', + 'utm_content' => $utm_content, + ), + $url + ); + + echo '' . esc_html( $text ) . '' . "\n"; + } + + /** + * Handles the outputting of a banner block. + * + * @deprecated 5.9.0 No longer used in In-App Marketplace + * + * @param object $block Banner data. + */ + public static function output_banner_block( $block ) { + ?> +
    +

    title ); ?>

    +

    description ); ?>

    +
    + items as $item ) : ?> + +
    +
    + +
    +
    +

    title ); ?>

    +

    description ); ?>

    + href, + $item->button, + 'addons-button-solid', + $item->plugin + ); + ?> +
    +
    + + +
    +
    + container ) && 'column_container_start' === $block->container ) { + ?> +
    + module ) { + ?> +
    + +
    + container ) && 'column_container_end' === $block->container ) { + ?> +
    + +
    +

    title ); ?>

    +

    description ); ?>

    + items as $item ) : ?> + +
    +
    + +
    +
    +

    title ); ?>

    + href, + $item->button, + 'addons-button-solid', + $item->plugin + ); + ?> +

    description ); ?>

    +
    +
    + + +
    + + +
    + +
    +

    title ); ?>

    +

    description ); ?>

    +
    + buttons as $button ) : ?> + href, + $button->text, + 'addons-button-solid' + ); + ?> + +
    +
    +
    + +
    +

    title ); ?>

    +

    description ); ?>

    +
    + items as $item ) : ?> +
    + image ) ) : ?> +
    + +
    + + href, + $item->button, + 'addons-button-outline-white' + ); + ?> +
    + +
    +
    + 'woocommerce-services', + ) + ), + 'install-addon_woocommerce-services' + ); + + $defaults = array( + 'image' => WC()->plugin_url() . '/assets/images/wcs-extensions-banner-3x.jpg', + 'image_alt' => __( 'WooCommerce Shipping', 'woocommerce' ), + 'title' => __( 'Save time and money with WooCommerce Shipping', 'woocommerce' ), + 'description' => __( 'Print discounted USPS and DHL labels straight from your WooCommerce dashboard and save on shipping.', 'woocommerce' ), + 'button' => __( 'Free - Install now', 'woocommerce' ), + 'href' => $button_url, + 'logos' => array(), + ); + + switch ( $location['country'] ) { + case 'US': + $local_defaults = array( + 'logos' => array_merge( + $defaults['logos'], + array( + array( + 'link' => WC()->plugin_url() . '/assets/images/wcs-usps-logo.png', + 'alt' => 'USPS logo', + ), + array( + 'link' => WC()->plugin_url() . '/assets/images/wcs-dhlexpress-logo.png', + 'alt' => 'DHL Express logo', + ), + ) + ), + ); + break; + default: + $local_defaults = array(); + } + + $block_data = array_merge( $defaults, $local_defaults, $block ); + ?> +
    +
    + <?php echo esc_attr( $block_data['image_alt'] ); ?> +
    +
    +

    +

    +
      + +
    • + +
    • + +
    + +
    +
    + 'woocommerce-payments', + ) + ), + 'install-addon_woocommerce-payments' + ); + + $defaults = array( + 'image' => WC()->plugin_url() . '/assets/images/wcpayments-icon-secure.png', + 'image_alt' => __( 'WooCommerce Payments', 'woocommerce' ), + 'title' => __( 'Payments made simple, with no monthly fees — exclusively for WooCommerce stores.', 'woocommerce' ), + 'description' => __( 'Securely accept cards in your store. See payments, track cash flow into your bank account, and stay on top of disputes – right from your dashboard.', 'woocommerce' ), + 'button' => __( 'Free - Install now', 'woocommerce' ), + 'href' => $button_url, + 'logos' => array(), + ); + + $block_data = array_merge( $defaults, $block ); + ?> +
    +
    + <?php echo esc_attr( $block_data['image_alt'] ); ?> +
    +
    +

    +

    + +
    +
    + +
    +
    + <?php echo esc_attr( $promotion['image_alt'] ); ?> +
    +
    +

    +

    + +
    +
    + geowhitelist ) ) { + $section_object->geowhitelist = explode( ',', $section_object->geowhitelist ); + } + + if ( ! empty( $section_object->geoblacklist ) ) { + $section_object->geoblacklist = explode( ',', $section_object->geoblacklist ); + } + + if ( ! self::show_extension( $section_object ) ) { + return; + } + + ?> +
    + <?php echo esc_attr( $section['image_alt'] ); ?> +
    +

    +
    + +
    +
    + +
    +
    +
    + module ) { + case 'banner_block': + self::output_banner_block( $section ); + break; + case 'column_start': + self::output_column( $section ); + break; + case 'column_end': + self::output_column( $section ); + break; + case 'column_block': + self::output_column_block( $section ); + break; + case 'small_light_block': + self::output_small_light_block( $section ); + break; + case 'small_dark_block': + self::output_small_dark_block( $section ); + break; + case 'wcs_banner_block': + self::output_wcs_banner_block( (array) $section ); + break; + case 'wcpay_banner_block': + self::output_wcpay_banner_block( (array) $section ); + break; + case 'promotion_block': + self::output_promotion_block( (array) $section ); + break; + } + } + } + + /** + * Handles the outputting of featured page + * + * @param array $blocks Featured page's blocks. + */ + private static function output_featured( $blocks ) { + foreach ( $blocks as $block ) { + $block_type = $block->type ?? null; + switch ( $block_type ) { + case 'group': + self::output_group( $block ); + break; + case 'banner': + self::output_banner( $block ); + break; + } + } + } + + /** + * Render a group block including products + * + * @param mixed $block Block of the page for rendering. + * + * @return void + */ + private static function output_group( $block ) { + $capacity = $block->capacity ?? 3; + $product_list_classes = 3 === $capacity ? 'three-column' : 'two-column'; + $product_list_classes = 'products addons-products-' . $product_list_classes; + ?> +
    +

    title ); ?>

    +
    + description ) ) : ?> +
    + description ); ?> +
    + + url ) : ?> + + + + +
    +
    +
      + items, 0, $capacity ); + foreach ( $products as $item ) { + self::render_product_card( $item ); + } + ?> +
    +
    +
    + buttons ) ) { + // Render a product-like banner. + ?> +
      + type ); ?> +
    + +
      +
    • +
      +
      +
      +

      title ); ?>

      +

      description, array() ); ?>

      +
      +
      + buttons as $button ) { + $button_classes = array( 'button', 'addons-buttons-banner-button' ); + $type = $button->type ?? null; + if ( 'primary' === $type ) { + $button_classes[] = 'addons-buttons-banner-button-primary'; + } + ?> + + title ); ?> + + +
      +
      +
    • +
    + site_url(), + 'wccom-back' => rawurlencode( $back_admin_path ), + 'wccom-woo-version' => Constants::get_constant( 'WC_VERSION' ), + 'wccom-connect-nonce' => wp_create_nonce( 'connect' ), + ); + } + + /** + * Add in-app-purchase URL params to link. + * + * Adds various url parameters to a url to support a streamlined + * flow for obtaining and setting up WooCommerce extensons. + * + * @param string $url Destination URL. + */ + public static function add_in_app_purchase_url_params( $url ) { + return add_query_arg( + self::get_in_app_purchase_url_params(), + $url + ); + } + + /** + * Outputs a button. + * + * @param string $url Destination URL. + * @param string $text Button label text. + * @param string $style Button style class. + * @param string $plugin The plugin the button is promoting. + */ + public static function output_button( $url, $text, $style, $plugin = '' ) { + $style = __( 'Free', 'woocommerce' ) === $text ? 'addons-button-outline-purple' : $style; + $style = is_plugin_active( $plugin ) ? 'addons-button-installed' : $style; + $text = is_plugin_active( $plugin ) ? __( 'Installed', 'woocommerce' ) : $text; + $url = self::add_in_app_purchase_url_params( $url ); + ?> + + + + + + + + +
    +

    + +

    + +

    + WooCommerce.com, where you\'ll find the most popular WooCommerce extensions.', + 'woocommerce' + ) + ), + 'https://woocommerce.com/products/?utm_source=extensionsscreen&utm_medium=product&utm_campaign=connectionerror' + ); + ?> +

    +
    + countries->get_base_country(); + $extension_data = self::get_extension_data( $category, $term, $country ); + $addons = is_wp_error( $extension_data ) ? $extension_data : $extension_data->products; + $promotions = ! empty( $extension_data->promotions ) ? $extension_data->promotions : array(); + } + + // We need Automattic\WooCommerce\Admin\RemoteInboxNotifications for the next part, if not remove all promotions. + if ( ! WC()->is_wc_admin_active() ) { + $promotions = array(); + } + // Check for existence of promotions and evaluate out if we should show them. + if ( ! empty( $promotions ) ) { + foreach ( $promotions as $promo_id => $promotion ) { + $evaluator = new PromotionRuleEngine\RuleEvaluator(); + $passed = $evaluator->evaluate( $promotion->rules ); + if ( ! $passed ) { + unset( $promotions[ $promo_id ] ); + } + } + // Transform promotions to the correct format ready for output. + $promotions = self::format_promotions( $promotions ); + } + + /** + * Addon page view. + * + * @uses $addons + * @uses $search + * @uses $sections + * @uses $theme + * @uses $current_section + */ + include_once dirname( __FILE__ ) . '/views/html-admin-page-addons.php'; + } + + /** + * Install WooCommerce Services from Extensions screens. + */ + public static function install_woocommerce_services_addon() { + check_admin_referer( 'install-addon_woocommerce-services' ); + + $services_plugin_id = 'woocommerce-services'; + $services_plugin = array( + 'name' => __( 'WooCommerce Services', 'woocommerce' ), + 'repo-slug' => 'woocommerce-services', + ); + + WC_Install::background_installer( $services_plugin_id, $services_plugin ); + + wp_safe_redirect( remove_query_arg( array( 'install-addon', '_wpnonce' ) ) ); + exit; + } + + /** + * Install WooCommerce Payments from the Extensions screens. + * + * @param string $section Optional. Extenstions tab. + * + * @return void + */ + public static function install_woocommerce_payments_addon( $section = '_featured' ) { + check_admin_referer( 'install-addon_woocommerce-payments' ); + + $wcpay_plugin_id = 'woocommerce-payments'; + $wcpay_plugin = array( + 'name' => __( 'WooCommerce Payments', 'woocommerce' ), + 'repo-slug' => 'woocommerce-payments', + ); + + WC_Install::background_installer( $wcpay_plugin_id, $wcpay_plugin ); + + do_action( 'woocommerce_addon_installed', $wcpay_plugin_id, $section ); + + wp_safe_redirect( remove_query_arg( array( 'install-addon', '_wpnonce' ) ) ); + exit; + } + + /** + * We're displaying page=wc-addons and page=wc-addons§ion=helper as two separate pages. + * When we're on those pages, add body classes to distinguishe them. + * + * @param string $admin_body_class Unfiltered body class. + * + * @return string Body class with added class for Marketplace or My Subscriptions page. + */ + public static function filter_admin_body_classes( string $admin_body_class = '' ): string { + if ( isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) { + return " $admin_body_class woocommerce-page-wc-subscriptions "; + } + + return " $admin_body_class woocommerce-page-wc-marketplace "; + } + + /** + * Determine which class should be used for a rating star: + * - golden + * - half-filled (50/50 golden and gray) + * - gray + * + * Consider ratings from 3.0 to 4.0 as an example + * 3.0 will produce 3 stars + * 3.1 to 3.5 will produce 3 stars and a half star + * 3.6 to 4.0 will product 4 stars + * + * @param float $rating Rating of a product. + * @param int $index Index of a star in a row. + * + * @return string CSS class to use. + */ + public static function get_star_class( $rating, $index ) { + if ( $rating >= $index ) { + // Rating more that current star to show. + return 'fill'; + } elseif ( + abs( $index - 1 - floor( $rating ) ) < 0.0000001 && + 0 < ( $rating - floor( $rating ) ) + ) { + // For rating more than x.0 and less than x.5 or equal it will show a half star. + return 50 >= floor( ( $rating - floor( $rating ) ) * 100 ) + ? 'half-fill' + : 'fill'; + } + + // Don't show a golden star otherwise. + return 'no-fill'; + } + + /** + * Take an action object and return the URL based on properties of the action. + * + * @param object $action Action object. + * @return string URL. + */ + public static function get_action_url( $action ): string { + if ( ! isset( $action->url ) ) { + return ''; + } + + if ( isset( $action->url_is_admin_query ) && $action->url_is_admin_query ) { + return wc_admin_url( $action->url ); + } + + if ( isset( $action->url_is_admin_nonce_query ) && $action->url_is_admin_nonce_query ) { + if ( empty( $action->nonce ) ) { + return ''; + } + return wp_nonce_url( + admin_url( $action->url ), + $action->nonce + ); + } + + return $action->url; + } + + /** + * Format the promotion data ready for display, ie fetch locales and actions. + * + * @param array $promotions Array of promotoin objects. + * @return array Array of formatted promotions ready for output. + */ + public static function format_promotions( array $promotions ): array { + $formatted_promotions = array(); + foreach ( $promotions as $promotion ) { + // Get the matching locale or fall back to en-US. + $locale = PromotionRuleEngine\SpecRunner::get_locale( $promotion->locales ); + if ( null === $locale ) { + continue; + } + + $promotion_actions = array(); + if ( ! empty( $promotion->actions ) ) { + foreach ( $promotion->actions as $action ) { + $action_locale = PromotionRuleEngine\SpecRunner::get_action_locale( $action->locales ); + $url = self::get_action_url( $action ); + + $promotion_actions[] = array( + 'name' => $action->name, + 'label' => $action_locale->label, + 'url' => $url, + 'primary' => isset( $action->is_primary ) ? $action->is_primary : false, + ); + } + } + + $formatted_promotions[] = array( + 'title' => $locale->title, + 'description' => $locale->description, + 'image' => ( 'http' === substr( $locale->image, 0, 4 ) ) ? $locale->image : WC()->plugin_url() . $locale->image, + 'image_alt' => $locale->image_alt, + 'actions' => $promotion_actions, + ); + } + return $formatted_promotions; + } + + /** + * Map data from different endpoints to a universal format + * + * Search and featured products has a slightly different products' field names. + * Mapping converts different data structures into a universal one for further processing. + * + * @param mixed $data Product Card Data. + * + * @return object Converted data. + */ + public static function map_product_card_data( $data ) { + $mapped = (object) null; + + $type = $data->type ?? null; + + // Icon. + $mapped->icon = $data->icon ?? null; + if ( null === $mapped->icon && 'banner' === $type ) { + // For product-related banners icon is a product's image. + $mapped->icon = $data->image ?? null; + } + + // URL. + $mapped->url = $data->link ?? null; + if ( empty( $mapped->url ) ) { + $mapped->url = $data->url ?? null; + } + + // Title. + $mapped->title = $data->title ?? null; + + // Vendor Name. + $mapped->vendor_name = $data->vendor_name ?? null; + if ( empty( $mapped->vendor_name ) ) { + $mapped->vendor_name = $data->vendorName ?? null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + } + + // Vendor URL. + $mapped->vendor_url = $data->vendor_url ?? null; + if ( empty( $mapped->vendor_url ) ) { + $mapped->vendor_url = $data->vendorUrl ?? null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + } + + // Description. + $mapped->description = $data->excerpt ?? null; + if ( empty( $mapped->description ) ) { + $mapped->description = $data->description ?? null; + } + + $has_currency = ! empty( $data->currency ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + + // Is Free. + if ( $has_currency ) { + $mapped->is_free = 0 === (int) $data->price; + } else { + $mapped->is_free = '$0.00' === $data->price; + } + + // Price. + if ( $has_currency ) { + $mapped->price = wc_price( $data->price, array( 'currency' => $data->currency ) ); + } else { + $mapped->price = $data->price; + } + + // Price suffix, e.g. "per month". + $mapped->price_suffix = $data->price_suffix ?? null; + + // Rating. + $mapped->rating = $data->rating ?? null; + if ( null === $mapped->rating ) { + $mapped->rating = $data->averageRating ?? null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + } + + // Reviews Count. + $mapped->reviews_count = $data->reviews_count ?? null; + if ( null === $mapped->reviews_count ) { + $mapped->reviews_count = $data->reviewsCount ?? null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + } + // Featured & Promoted product card. + // Label. + $mapped->label = $data->label ?? null; + // Primary color. + $mapped->primary_color = $data->primary_color ?? null; + // Text color. + $mapped->text_color = $data->text_color ?? null; + // Button text. + $mapped->button = $data->button ?? null; + + return $mapped; + } + + /** + * Render a product card + * + * There's difference in data structure (e.g. field names) between endpoints such as search and + * featured. Inner mapping helps to use universal field names for further work. + * + * @param mixed $data Product data. + * @param string $block_type Block type that's different from the default product card, e.g. a banner. + * + * @return void + */ + public static function render_product_card( $data, $block_type = null ) { + $mapped = self::map_product_card_data( $data ); + $product_url = self::add_in_app_purchase_url_params( $mapped->url ); + $class_names = array( 'product' ); + // Specify a class name according to $block_type (if it's specified). + if ( null !== $block_type ) { + $class_names[] = 'addons-product-' . $block_type; + } + + $product_details_classes = 'product-details'; + if ( 'banner' === $block_type ) { + $product_details_classes .= ' addon-product-banner-details'; + } + + if ( isset( $mapped->label ) && 'promoted' === $mapped->label ) { + $product_details_classes .= ' promoted'; + } elseif ( isset( $mapped->label ) && 'featured' === $mapped->label ) { + $product_details_classes .= ' featured'; + } + + if ( 'promoted' === $mapped->label + && ! empty( $mapped->primary_color ) + && ! empty( $mapped->text_color ) + && ! empty( $mapped->button ) ) { + // Promoted product card. + ?> +
  • +
    + + +

    title ); ?>

    +
    +

    description ); ?>

    +
    + +
  • + +
  • +
    +
    + label ) && 'featured' === $mapped->label ) { ?> + + + +

    title ); ?>

    +
    + vendor_name ) && ! empty( $mapped->vendor_url ) ) : ?> +
    + 'extensionsscreen', + 'utm_medium' => 'product', + 'utm_campaign' => 'wcaddons', + 'utm_content' => 'devpartner', + ), + $mapped->vendor_url + ); + + printf( + /* translators: %s vendor link */ + esc_html__( 'Developed by %s', 'woocommerce' ), + sprintf( + '%2$s', + esc_url_raw( $vendor_url ), + esc_html( $mapped->vendor_name ) + ) + ); + ?> +
    + +

    description ); ?>

    +
    + icon ) ) : ?> + + + + + +
    + +
  • + id : ''; + + // Register admin styles. + wp_register_style( 'woocommerce_admin_menu_styles', WC()->plugin_url() . '/assets/css/menu.css', array(), $version ); + wp_register_style( 'woocommerce_admin_styles', WC()->plugin_url() . '/assets/css/admin.css', array(), $version ); + wp_register_style( 'jquery-ui-style', WC()->plugin_url() . '/assets/css/jquery-ui/jquery-ui.min.css', array(), $version ); + wp_register_style( 'woocommerce_admin_dashboard_styles', WC()->plugin_url() . '/assets/css/dashboard.css', array(), $version ); + wp_register_style( 'woocommerce_admin_print_reports_styles', WC()->plugin_url() . '/assets/css/reports-print.css', array(), $version, 'print' ); + wp_register_style( 'woocommerce_admin_marketplace_styles', WC()->plugin_url() . '/assets/css/marketplace-suggestions.css', array(), $version ); + wp_register_style( 'woocommerce_admin_privacy_styles', WC()->plugin_url() . '/assets/css/privacy.css', array(), $version ); + + // Add RTL support for admin styles. + wp_style_add_data( 'woocommerce_admin_menu_styles', 'rtl', 'replace' ); + wp_style_add_data( 'woocommerce_admin_styles', 'rtl', 'replace' ); + wp_style_add_data( 'woocommerce_admin_dashboard_styles', 'rtl', 'replace' ); + wp_style_add_data( 'woocommerce_admin_print_reports_styles', 'rtl', 'replace' ); + wp_style_add_data( 'woocommerce_admin_marketplace_styles', 'rtl', 'replace' ); + wp_style_add_data( 'woocommerce_admin_privacy_styles', 'rtl', 'replace' ); + + if ( $screen && $screen->is_block_editor() ) { + wp_register_style( 'woocommerce-general', WC()->plugin_url() . '/assets/css/woocommerce.css', array(), $version ); + wp_style_add_data( 'woocommerce-general', 'rtl', 'replace' ); + } + + // Sitewide menu CSS. + wp_enqueue_style( 'woocommerce_admin_menu_styles' ); + + // Admin styles for WC pages only. + if ( in_array( $screen_id, wc_get_screen_ids() ) ) { + wp_enqueue_style( 'woocommerce_admin_styles' ); + wp_enqueue_style( 'jquery-ui-style' ); + wp_enqueue_style( 'wp-color-picker' ); + } + + if ( in_array( $screen_id, array( 'dashboard' ) ) ) { + wp_enqueue_style( 'woocommerce_admin_dashboard_styles' ); + } + + if ( in_array( $screen_id, array( 'woocommerce_page_wc-reports', 'toplevel_page_wc-reports' ) ) ) { + wp_enqueue_style( 'woocommerce_admin_print_reports_styles' ); + } + + // Privacy Policy Guide css for back-compat. + if ( isset( $_GET['wp-privacy-policy-guide'] ) || in_array( $screen_id, array( 'privacy-policy-guide' ) ) ) { + wp_enqueue_style( 'woocommerce_admin_privacy_styles' ); + } + + // @deprecated 2.3. + if ( has_action( 'woocommerce_admin_css' ) ) { + do_action( 'woocommerce_admin_css' ); + wc_deprecated_function( 'The woocommerce_admin_css action', '2.3', 'admin_enqueue_scripts' ); + } + + if ( WC_Marketplace_Suggestions::show_suggestions_for_screen( $screen_id ) ) { + wp_enqueue_style( 'woocommerce_admin_marketplace_styles' ); + } + } + + + /** + * Enqueue scripts. + */ + public function admin_scripts() { + global $wp_query, $post; + + $screen = get_current_screen(); + $screen_id = $screen ? $screen->id : ''; + $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) ); + $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; + $version = Constants::get_constant( 'WC_VERSION' ); + + // Register scripts. + wp_register_script( 'woocommerce_admin', WC()->plugin_url() . '/assets/js/admin/woocommerce_admin' . $suffix . '.js', array( 'jquery', 'jquery-blockui', 'jquery-ui-sortable', 'jquery-ui-widget', 'jquery-ui-core', 'jquery-tiptip' ), $version ); + wp_register_script( 'jquery-blockui', WC()->plugin_url() . '/assets/js/jquery-blockui/jquery.blockUI' . $suffix . '.js', array( 'jquery' ), '2.70', true ); + wp_register_script( 'jquery-tiptip', WC()->plugin_url() . '/assets/js/jquery-tiptip/jquery.tipTip' . $suffix . '.js', array( 'jquery' ), $version, true ); + wp_register_script( 'round', WC()->plugin_url() . '/assets/js/round/round' . $suffix . '.js', array( 'jquery' ), $version ); + wp_register_script( 'wc-admin-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes' . $suffix . '.js', array( 'jquery', 'jquery-ui-datepicker', 'jquery-ui-sortable', 'accounting', 'round', 'wc-enhanced-select', 'plupload-all', 'stupidtable', 'jquery-tiptip' ), $version ); + wp_register_script( 'qrcode', WC()->plugin_url() . '/assets/js/jquery-qrcode/jquery.qrcode' . $suffix . '.js', array( 'jquery' ), $version ); + wp_register_script( 'stupidtable', WC()->plugin_url() . '/assets/js/stupidtable/stupidtable' . $suffix . '.js', array( 'jquery' ), $version ); + wp_register_script( 'serializejson', WC()->plugin_url() . '/assets/js/jquery-serializejson/jquery.serializejson' . $suffix . '.js', array( 'jquery' ), '2.8.1' ); + wp_register_script( 'flot', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot' . $suffix . '.js', array( 'jquery' ), $version ); + wp_register_script( 'flot-resize', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot.resize' . $suffix . '.js', array( 'jquery', 'flot' ), $version ); + wp_register_script( 'flot-time', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot.time' . $suffix . '.js', array( 'jquery', 'flot' ), $version ); + wp_register_script( 'flot-pie', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot.pie' . $suffix . '.js', array( 'jquery', 'flot' ), $version ); + wp_register_script( 'flot-stack', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot.stack' . $suffix . '.js', array( 'jquery', 'flot' ), $version ); + wp_register_script( 'wc-settings-tax', WC()->plugin_url() . '/assets/js/admin/settings-views-html-settings-tax' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone', 'jquery-blockui' ), $version ); + wp_register_script( 'wc-backbone-modal', WC()->plugin_url() . '/assets/js/admin/backbone-modal' . $suffix . '.js', array( 'underscore', 'backbone', 'wp-util' ), $version ); + wp_register_script( 'wc-shipping-zones', WC()->plugin_url() . '/assets/js/admin/wc-shipping-zones' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone', 'jquery-ui-sortable', 'wc-enhanced-select', 'wc-backbone-modal' ), $version ); + wp_register_script( 'wc-shipping-zone-methods', WC()->plugin_url() . '/assets/js/admin/wc-shipping-zone-methods' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone', 'jquery-ui-sortable', 'wc-backbone-modal' ), $version ); + wp_register_script( 'wc-shipping-classes', WC()->plugin_url() . '/assets/js/admin/wc-shipping-classes' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone' ), $version ); + wp_register_script( 'wc-clipboard', WC()->plugin_url() . '/assets/js/admin/wc-clipboard' . $suffix . '.js', array( 'jquery' ), $version ); + wp_register_script( 'select2', WC()->plugin_url() . '/assets/js/select2/select2.full' . $suffix . '.js', array( 'jquery' ), '4.0.3' ); + wp_register_script( 'selectWoo', WC()->plugin_url() . '/assets/js/selectWoo/selectWoo.full' . $suffix . '.js', array( 'jquery' ), '1.0.6' ); + wp_register_script( 'wc-enhanced-select', WC()->plugin_url() . '/assets/js/admin/wc-enhanced-select' . $suffix . '.js', array( 'jquery', 'selectWoo' ), $version ); + wp_register_script( 'js-cookie', WC()->plugin_url() . '/assets/js/js-cookie/js.cookie' . $suffix . '.js', array(), '2.1.4', true ); + + wp_localize_script( + 'wc-enhanced-select', + 'wc_enhanced_select_params', + array( + 'i18n_no_matches' => _x( 'No matches found', 'enhanced select', 'woocommerce' ), + 'i18n_ajax_error' => _x( 'Loading failed', 'enhanced select', 'woocommerce' ), + 'i18n_input_too_short_1' => _x( 'Please enter 1 or more characters', 'enhanced select', 'woocommerce' ), + 'i18n_input_too_short_n' => _x( 'Please enter %qty% or more characters', 'enhanced select', 'woocommerce' ), + 'i18n_input_too_long_1' => _x( 'Please delete 1 character', 'enhanced select', 'woocommerce' ), + 'i18n_input_too_long_n' => _x( 'Please delete %qty% characters', 'enhanced select', 'woocommerce' ), + 'i18n_selection_too_long_1' => _x( 'You can only select 1 item', 'enhanced select', 'woocommerce' ), + 'i18n_selection_too_long_n' => _x( 'You can only select %qty% items', 'enhanced select', 'woocommerce' ), + 'i18n_load_more' => _x( 'Loading more results…', 'enhanced select', 'woocommerce' ), + 'i18n_searching' => _x( 'Searching…', 'enhanced select', 'woocommerce' ), + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'search_products_nonce' => wp_create_nonce( 'search-products' ), + 'search_customers_nonce' => wp_create_nonce( 'search-customers' ), + 'search_categories_nonce' => wp_create_nonce( 'search-categories' ), + 'search_pages_nonce' => wp_create_nonce( 'search-pages' ), + ) + ); + + wp_register_script( 'accounting', WC()->plugin_url() . '/assets/js/accounting/accounting' . $suffix . '.js', array( 'jquery' ), '0.4.2' ); + wp_localize_script( + 'accounting', + 'accounting_params', + array( + 'mon_decimal_point' => wc_get_price_decimal_separator(), + ) + ); + + wp_register_script( 'wc-orders', WC()->plugin_url() . '/assets/js/admin/wc-orders' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone', 'jquery-blockui' ), $version ); + wp_localize_script( + 'wc-orders', + 'wc_orders_params', + array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'preview_nonce' => wp_create_nonce( 'woocommerce-preview-order' ), + ) + ); + + // WooCommerce admin pages. + if ( in_array( $screen_id, wc_get_screen_ids() ) ) { + wp_enqueue_script( 'iris' ); + wp_enqueue_script( 'woocommerce_admin' ); + wp_enqueue_script( 'wc-enhanced-select' ); + wp_enqueue_script( 'jquery-ui-sortable' ); + wp_enqueue_script( 'jquery-ui-autocomplete' ); + + $locale = localeconv(); + $decimal = isset( $locale['decimal_point'] ) ? $locale['decimal_point'] : '.'; + + $params = array( + /* translators: %s: decimal */ + 'i18n_decimal_error' => sprintf( __( 'Please enter with one decimal point (%s) without thousand separators.', 'woocommerce' ), $decimal ), + /* translators: %s: price decimal separator */ + 'i18n_mon_decimal_error' => sprintf( __( 'Please enter with one monetary decimal point (%s) without thousand separators and currency symbols.', 'woocommerce' ), wc_get_price_decimal_separator() ), + 'i18n_country_iso_error' => __( 'Please enter in country code with two capital letters.', 'woocommerce' ), + 'i18n_sale_less_than_regular_error' => __( 'Please enter in a value less than the regular price.', 'woocommerce' ), + 'i18n_delete_product_notice' => __( 'This product has produced sales and may be linked to existing orders. Are you sure you want to delete it?', 'woocommerce' ), + 'i18n_remove_personal_data_notice' => __( 'This action cannot be reversed. Are you sure you wish to erase personal data from the selected orders?', 'woocommerce' ), + 'i18n_confirm_delete' => __( 'Are you sure you wish to delete this item?', 'woocommerce' ), + 'decimal_point' => $decimal, + 'mon_decimal_point' => wc_get_price_decimal_separator(), + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'strings' => array( + 'import_products' => __( 'Import', 'woocommerce' ), + 'export_products' => __( 'Export', 'woocommerce' ), + ), + 'nonces' => array( + 'gateway_toggle' => wp_create_nonce( 'woocommerce-toggle-payment-gateway-enabled' ), + ), + 'urls' => array( + 'import_products' => current_user_can( 'import' ) ? esc_url_raw( admin_url( 'edit.php?post_type=product&page=product_importer' ) ) : null, + 'export_products' => current_user_can( 'export' ) ? esc_url_raw( admin_url( 'edit.php?post_type=product&page=product_exporter' ) ) : null, + ), + ); + + wp_localize_script( 'woocommerce_admin', 'woocommerce_admin', $params ); + } + + // Edit product category pages. + if ( in_array( $screen_id, array( 'edit-product_cat' ) ) ) { + wp_enqueue_media(); + } + + // Products. + if ( in_array( $screen_id, array( 'edit-product' ) ) ) { + wp_enqueue_script( 'woocommerce_quick-edit', WC()->plugin_url() . '/assets/js/admin/quick-edit' . $suffix . '.js', array( 'jquery', 'woocommerce_admin' ), $version ); + + $params = array( + 'strings' => array( + 'allow_reviews' => esc_js( __( 'Enable reviews', 'woocommerce' ) ), + ), + ); + + wp_localize_script( 'woocommerce_quick-edit', 'woocommerce_quick_edit', $params ); + } + + // Meta boxes. + if ( in_array( $screen_id, array( 'product', 'edit-product' ) ) ) { + wp_enqueue_media(); + wp_register_script( 'wc-admin-product-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'media-models' ), $version ); + wp_register_script( 'wc-admin-variation-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product-variation' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'serializejson', 'media-models' ), $version ); + + wp_enqueue_script( 'wc-admin-product-meta-boxes' ); + wp_enqueue_script( 'wc-admin-variation-meta-boxes' ); + + $params = array( + 'post_id' => isset( $post->ID ) ? $post->ID : '', + 'plugin_url' => WC()->plugin_url(), + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'woocommerce_placeholder_img_src' => wc_placeholder_img_src(), + 'add_variation_nonce' => wp_create_nonce( 'add-variation' ), + 'link_variation_nonce' => wp_create_nonce( 'link-variations' ), + 'delete_variations_nonce' => wp_create_nonce( 'delete-variations' ), + 'load_variations_nonce' => wp_create_nonce( 'load-variations' ), + 'save_variations_nonce' => wp_create_nonce( 'save-variations' ), + 'bulk_edit_variations_nonce' => wp_create_nonce( 'bulk-edit-variations' ), + /* translators: %d: Number of variations */ + 'i18n_link_all_variations' => esc_js( sprintf( __( 'Are you sure you want to link all variations? This will create a new variation for each and every possible combination of variation attributes (max %d per run).', 'woocommerce' ), Constants::is_defined( 'WC_MAX_LINKED_VARIATIONS' ) ? Constants::get_constant( 'WC_MAX_LINKED_VARIATIONS' ) : 50 ) ), + 'i18n_enter_a_value' => esc_js( __( 'Enter a value', 'woocommerce' ) ), + 'i18n_enter_menu_order' => esc_js( __( 'Variation menu order (determines position in the list of variations)', 'woocommerce' ) ), + 'i18n_enter_a_value_fixed_or_percent' => esc_js( __( 'Enter a value (fixed or %)', 'woocommerce' ) ), + 'i18n_delete_all_variations' => esc_js( __( 'Are you sure you want to delete all variations? This cannot be undone.', 'woocommerce' ) ), + 'i18n_last_warning' => esc_js( __( 'Last warning, are you sure?', 'woocommerce' ) ), + 'i18n_choose_image' => esc_js( __( 'Choose an image', 'woocommerce' ) ), + 'i18n_set_image' => esc_js( __( 'Set variation image', 'woocommerce' ) ), + 'i18n_variation_added' => esc_js( __( 'variation added', 'woocommerce' ) ), + 'i18n_variations_added' => esc_js( __( 'variations added', 'woocommerce' ) ), + 'i18n_no_variations_added' => esc_js( __( 'No variations added', 'woocommerce' ) ), + 'i18n_remove_variation' => esc_js( __( 'Are you sure you want to remove this variation?', 'woocommerce' ) ), + 'i18n_scheduled_sale_start' => esc_js( __( 'Sale start date (YYYY-MM-DD format or leave blank)', 'woocommerce' ) ), + 'i18n_scheduled_sale_end' => esc_js( __( 'Sale end date (YYYY-MM-DD format or leave blank)', 'woocommerce' ) ), + 'i18n_edited_variations' => esc_js( __( 'Save changes before changing page?', 'woocommerce' ) ), + 'i18n_variation_count_single' => esc_js( __( '%qty% variation', 'woocommerce' ) ), + 'i18n_variation_count_plural' => esc_js( __( '%qty% variations', 'woocommerce' ) ), + 'variations_per_page' => absint( apply_filters( 'woocommerce_admin_meta_boxes_variations_per_page', 15 ) ), + ); + + wp_localize_script( 'wc-admin-variation-meta-boxes', 'woocommerce_admin_meta_boxes_variations', $params ); + } + if ( in_array( str_replace( 'edit-', '', $screen_id ), wc_get_order_types( 'order-meta-boxes' ) ) ) { + $default_location = wc_get_customer_default_location(); + + wp_enqueue_script( 'wc-admin-order-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-order' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'wc-backbone-modal', 'selectWoo', 'wc-clipboard' ), $version ); + wp_localize_script( + 'wc-admin-order-meta-boxes', + 'woocommerce_admin_meta_boxes_order', + array( + 'countries' => wp_json_encode( array_merge( WC()->countries->get_allowed_country_states(), WC()->countries->get_shipping_country_states() ) ), + 'i18n_select_state_text' => esc_attr__( 'Select an option…', 'woocommerce' ), + 'default_country' => isset( $default_location['country'] ) ? $default_location['country'] : '', + 'default_state' => isset( $default_location['state'] ) ? $default_location['state'] : '', + 'placeholder_name' => esc_attr__( 'Name (required)', 'woocommerce' ), + 'placeholder_value' => esc_attr__( 'Value (required)', 'woocommerce' ), + ) + ); + } + if ( in_array( $screen_id, array( 'shop_coupon', 'edit-shop_coupon' ) ) ) { + wp_enqueue_script( 'wc-admin-coupon-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-coupon' . $suffix . '.js', array( 'wc-admin-meta-boxes' ), $version ); + wp_localize_script( + 'wc-admin-coupon-meta-boxes', + 'woocommerce_admin_meta_boxes_coupon', + array( + 'generate_button_text' => esc_html__( 'Generate coupon code', 'woocommerce' ), + 'characters' => apply_filters( 'woocommerce_coupon_code_generator_characters', 'ABCDEFGHJKMNPQRSTUVWXYZ23456789' ), + 'char_length' => apply_filters( 'woocommerce_coupon_code_generator_character_length', 8 ), + 'prefix' => apply_filters( 'woocommerce_coupon_code_generator_prefix', '' ), + 'suffix' => apply_filters( 'woocommerce_coupon_code_generator_suffix', '' ), + ) + ); + } + if ( in_array( str_replace( 'edit-', '', $screen_id ), array_merge( array( 'shop_coupon', 'product' ), wc_get_order_types( 'order-meta-boxes' ) ) ) ) { + $post_id = isset( $post->ID ) ? $post->ID : ''; + $currency = ''; + $remove_item_notice = __( 'Are you sure you want to remove the selected items?', 'woocommerce' ); + $remove_fee_notice = __( 'Are you sure you want to remove the selected fees?', 'woocommerce' ); + $remove_shipping_notice = __( 'Are you sure you want to remove the selected shipping?', 'woocommerce' ); + + if ( $post_id && in_array( get_post_type( $post_id ), wc_get_order_types( 'order-meta-boxes' ) ) ) { + $order = wc_get_order( $post_id ); + if ( $order ) { + $currency = $order->get_currency(); + + if ( ! $order->has_status( array( 'pending', 'failed', 'cancelled' ) ) ) { + $remove_item_notice = $remove_item_notice . ' ' . __( "You may need to manually restore the item's stock.", 'woocommerce' ); + } + } + } + + $params = array( + 'remove_item_notice' => $remove_item_notice, + 'remove_fee_notice' => $remove_fee_notice, + 'remove_shipping_notice' => $remove_shipping_notice, + 'i18n_select_items' => __( 'Please select some items.', 'woocommerce' ), + 'i18n_do_refund' => __( 'Are you sure you wish to process this refund? This action cannot be undone.', 'woocommerce' ), + 'i18n_delete_refund' => __( 'Are you sure you wish to delete this refund? This action cannot be undone.', 'woocommerce' ), + 'i18n_delete_tax' => __( 'Are you sure you wish to delete this tax column? This action cannot be undone.', 'woocommerce' ), + 'remove_item_meta' => __( 'Remove this item meta?', 'woocommerce' ), + 'remove_attribute' => __( 'Remove this attribute?', 'woocommerce' ), + 'name_label' => __( 'Name', 'woocommerce' ), + 'remove_label' => __( 'Remove', 'woocommerce' ), + 'click_to_toggle' => __( 'Click to toggle', 'woocommerce' ), + 'values_label' => __( 'Value(s)', 'woocommerce' ), + 'text_attribute_tip' => __( 'Enter some text, or some attributes by pipe (|) separating values.', 'woocommerce' ), + 'visible_label' => __( 'Visible on the product page', 'woocommerce' ), + 'used_for_variations_label' => __( 'Used for variations', 'woocommerce' ), + 'new_attribute_prompt' => __( 'Enter a name for the new attribute term:', 'woocommerce' ), + 'calc_totals' => __( 'Recalculate totals? This will calculate taxes based on the customers country (or the store base country) and update totals.', 'woocommerce' ), + 'copy_billing' => __( 'Copy billing information to shipping information? This will remove any currently entered shipping information.', 'woocommerce' ), + 'load_billing' => __( "Load the customer's billing information? This will remove any currently entered billing information.", 'woocommerce' ), + 'load_shipping' => __( "Load the customer's shipping information? This will remove any currently entered shipping information.", 'woocommerce' ), + 'featured_label' => __( 'Featured', 'woocommerce' ), + 'prices_include_tax' => esc_attr( get_option( 'woocommerce_prices_include_tax' ) ), + 'tax_based_on' => esc_attr( get_option( 'woocommerce_tax_based_on' ) ), + 'round_at_subtotal' => esc_attr( get_option( 'woocommerce_tax_round_at_subtotal' ) ), + 'no_customer_selected' => __( 'No customer selected', 'woocommerce' ), + 'plugin_url' => WC()->plugin_url(), + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'order_item_nonce' => wp_create_nonce( 'order-item' ), + 'add_attribute_nonce' => wp_create_nonce( 'add-attribute' ), + 'save_attributes_nonce' => wp_create_nonce( 'save-attributes' ), + 'calc_totals_nonce' => wp_create_nonce( 'calc-totals' ), + 'get_customer_details_nonce' => wp_create_nonce( 'get-customer-details' ), + 'search_products_nonce' => wp_create_nonce( 'search-products' ), + 'grant_access_nonce' => wp_create_nonce( 'grant-access' ), + 'revoke_access_nonce' => wp_create_nonce( 'revoke-access' ), + 'add_order_note_nonce' => wp_create_nonce( 'add-order-note' ), + 'delete_order_note_nonce' => wp_create_nonce( 'delete-order-note' ), + 'calendar_image' => WC()->plugin_url() . '/assets/images/calendar.png', + 'post_id' => isset( $post->ID ) ? $post->ID : '', + 'base_country' => WC()->countries->get_base_country(), + 'currency_format_num_decimals' => wc_get_price_decimals(), + 'currency_format_symbol' => get_woocommerce_currency_symbol( $currency ), + 'currency_format_decimal_sep' => esc_attr( wc_get_price_decimal_separator() ), + 'currency_format_thousand_sep' => esc_attr( wc_get_price_thousand_separator() ), + 'currency_format' => esc_attr( str_replace( array( '%1$s', '%2$s' ), array( '%s', '%v' ), get_woocommerce_price_format() ) ), // For accounting JS. + 'rounding_precision' => wc_get_rounding_precision(), + 'tax_rounding_mode' => wc_get_tax_rounding_mode(), + 'product_types' => array_unique( array_merge( array( 'simple', 'grouped', 'variable', 'external' ), array_keys( wc_get_product_types() ) ) ), + 'i18n_download_permission_fail' => __( 'Could not grant access - the user may already have permission for this file or billing email is not set. Ensure the billing email is set, and the order has been saved.', 'woocommerce' ), + 'i18n_permission_revoke' => __( 'Are you sure you want to revoke access to this download?', 'woocommerce' ), + 'i18n_tax_rate_already_exists' => __( 'You cannot add the same tax rate twice!', 'woocommerce' ), + 'i18n_delete_note' => __( 'Are you sure you wish to delete this note? This action cannot be undone.', 'woocommerce' ), + 'i18n_apply_coupon' => __( 'Enter a coupon code to apply. Discounts are applied to line totals, before taxes.', 'woocommerce' ), + 'i18n_add_fee' => __( 'Enter a fixed amount or percentage to apply as a fee.', 'woocommerce' ), + ); + + wp_localize_script( 'wc-admin-meta-boxes', 'woocommerce_admin_meta_boxes', $params ); + } + + // Term ordering - only when sorting by term_order. + if ( ( strstr( $screen_id, 'edit-pa_' ) || ( ! empty( $_GET['taxonomy'] ) && in_array( wp_unslash( $_GET['taxonomy'] ), apply_filters( 'woocommerce_sortable_taxonomies', array( 'product_cat' ) ) ) ) ) && ! isset( $_GET['orderby'] ) ) { + + wp_register_script( 'woocommerce_term_ordering', WC()->plugin_url() . '/assets/js/admin/term-ordering' . $suffix . '.js', array( 'jquery-ui-sortable' ), $version ); + wp_enqueue_script( 'woocommerce_term_ordering' ); + + $taxonomy = isset( $_GET['taxonomy'] ) ? wc_clean( wp_unslash( $_GET['taxonomy'] ) ) : ''; + + $woocommerce_term_order_params = array( + 'taxonomy' => $taxonomy, + ); + + wp_localize_script( 'woocommerce_term_ordering', 'woocommerce_term_ordering_params', $woocommerce_term_order_params ); + } + + // Product sorting - only when sorting by menu order on the products page. + if ( current_user_can( 'edit_others_pages' ) && 'edit-product' === $screen_id && isset( $wp_query->query['orderby'] ) && 'menu_order title' === $wp_query->query['orderby'] ) { + wp_register_script( 'woocommerce_product_ordering', WC()->plugin_url() . '/assets/js/admin/product-ordering' . $suffix . '.js', array( 'jquery-ui-sortable' ), $version, true ); + wp_enqueue_script( 'woocommerce_product_ordering' ); + } + + // Reports Pages. + if ( in_array( $screen_id, apply_filters( 'woocommerce_reports_screen_ids', array( $wc_screen_id . '_page_wc-reports', 'toplevel_page_wc-reports', 'dashboard' ) ) ) ) { + wp_register_script( 'wc-reports', WC()->plugin_url() . '/assets/js/admin/reports' . $suffix . '.js', array( 'jquery', 'jquery-ui-datepicker' ), $version ); + + wp_enqueue_script( 'wc-reports' ); + wp_enqueue_script( 'flot' ); + wp_enqueue_script( 'flot-resize' ); + wp_enqueue_script( 'flot-time' ); + wp_enqueue_script( 'flot-pie' ); + wp_enqueue_script( 'flot-stack' ); + } + + // API settings. + if ( $wc_screen_id . '_page_wc-settings' === $screen_id && isset( $_GET['section'] ) && 'keys' == $_GET['section'] ) { + wp_register_script( 'wc-api-keys', WC()->plugin_url() . '/assets/js/admin/api-keys' . $suffix . '.js', array( 'jquery', 'woocommerce_admin', 'underscore', 'backbone', 'wp-util', 'qrcode', 'wc-clipboard' ), $version, true ); + wp_enqueue_script( 'wc-api-keys' ); + wp_localize_script( + 'wc-api-keys', + 'woocommerce_admin_api_keys', + array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'update_api_nonce' => wp_create_nonce( 'update-api-key' ), + 'clipboard_failed' => esc_html__( 'Copying to clipboard failed. Please press Ctrl/Cmd+C to copy.', 'woocommerce' ), + ) + ); + } + + // System status. + if ( $wc_screen_id . '_page_wc-status' === $screen_id ) { + wp_register_script( 'wc-admin-system-status', WC()->plugin_url() . '/assets/js/admin/system-status' . $suffix . '.js', array( 'wc-clipboard' ), $version ); + wp_enqueue_script( 'wc-admin-system-status' ); + wp_localize_script( + 'wc-admin-system-status', + 'woocommerce_admin_system_status', + array( + 'delete_log_confirmation' => esc_js( __( 'Are you sure you want to delete this log?', 'woocommerce' ) ), + 'run_tool_confirmation' => esc_js( __( 'Are you sure you want to run this tool?', 'woocommerce' ) ), + ) + ); + } + + if ( in_array( $screen_id, array( 'user-edit', 'profile' ) ) ) { + wp_register_script( 'wc-users', WC()->plugin_url() . '/assets/js/admin/users' . $suffix . '.js', array( 'jquery', 'wc-enhanced-select', 'selectWoo' ), $version, true ); + wp_enqueue_script( 'wc-users' ); + wp_localize_script( + 'wc-users', + 'wc_users_params', + array( + 'countries' => wp_json_encode( array_merge( WC()->countries->get_allowed_country_states(), WC()->countries->get_shipping_country_states() ) ), + 'i18n_select_state_text' => esc_attr__( 'Select an option…', 'woocommerce' ), + ) + ); + } + + if ( WC_Marketplace_Suggestions::show_suggestions_for_screen( $screen_id ) ) { + $active_plugin_slugs = array_map( 'dirname', get_option( 'active_plugins' ) ); + wp_register_script( + 'marketplace-suggestions', + WC()->plugin_url() . '/assets/js/admin/marketplace-suggestions' . $suffix . '.js', + array( 'jquery', 'underscore', 'js-cookie' ), + $version, + true + ); + wp_localize_script( + 'marketplace-suggestions', + 'marketplace_suggestions', + array( + 'dismiss_suggestion_nonce' => wp_create_nonce( 'add_dismissed_marketplace_suggestion' ), + 'active_plugins' => $active_plugin_slugs, + 'dismissed_suggestions' => WC_Marketplace_Suggestions::get_dismissed_suggestions(), + 'suggestions_data' => WC_Marketplace_Suggestions::get_suggestions_api_data(), + 'manage_suggestions_url' => admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=woocommerce_com' ), + 'in_app_purchase_params' => WC_Admin_Addons::get_in_app_purchase_url_params(), + 'i18n_marketplace_suggestions_default_cta' + => esc_html__( 'Learn More', 'woocommerce' ), + 'i18n_marketplace_suggestions_dismiss_tooltip' + => esc_attr__( 'Dismiss this suggestion', 'woocommerce' ), + 'i18n_marketplace_suggestions_manage_suggestions' + => esc_html__( 'Manage suggestions', 'woocommerce' ), + ) + ); + wp_enqueue_script( 'marketplace-suggestions' ); + } + + } + + } + +endif; + +return new WC_Admin_Assets(); diff --git a/includes/admin/class-wc-admin-attributes.php b/plugins/woocommerce/includes/admin/class-wc-admin-attributes.php similarity index 100% rename from includes/admin/class-wc-admin-attributes.php rename to plugins/woocommerce/includes/admin/class-wc-admin-attributes.php diff --git a/includes/admin/class-wc-admin-customize.php b/plugins/woocommerce/includes/admin/class-wc-admin-customize.php similarity index 100% rename from includes/admin/class-wc-admin-customize.php rename to plugins/woocommerce/includes/admin/class-wc-admin-customize.php diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-dashboard-setup.php b/plugins/woocommerce/includes/admin/class-wc-admin-dashboard-setup.php new file mode 100644 index 00000000000..f6023c2f35b --- /dev/null +++ b/plugins/woocommerce/includes/admin/class-wc-admin-dashboard-setup.php @@ -0,0 +1,201 @@ +should_display_widget() ) { + add_meta_box( + 'wc_admin_dashboard_setup', + __( 'WooCommerce Setup', 'woocommerce' ), + array( $this, 'render' ), + 'dashboard', + 'normal', + 'high' + ); + } + } + + /** + * Render meta box output. + */ + public function render() { + $version = Constants::get_constant( 'WC_VERSION' ); + wp_enqueue_style( 'wc-dashboard-setup', WC()->plugin_url() . '/assets/css/dashboard-setup.css', array(), $version ); + + $task = $this->get_next_task(); + if ( ! $task ) { + return; + } + + $button_link = $this->get_button_link( $task ); + $completed_tasks_count = $this->get_completed_tasks_count(); + $step_number = $this->get_completed_tasks_count() + 1; + $tasks_count = count( $this->get_tasks() ); + + // Given 'r' (circle element's r attr), dashoffset = ((100-$desired_percentage)/100) * PI * (r*2). + $progress_percentage = ( $completed_tasks_count / $tasks_count ) * 100; + $circle_r = 6.5; + $circle_dashoffset = ( ( 100 - $progress_percentage ) / 100 ) * ( pi() * ( $circle_r * 2 ) ); + + include __DIR__ . '/views/html-admin-dashboard-setup.php'; + } + + /** + * Get the button link for a given task. + * + * @param Task $task Task. + * @return string + */ + public function get_button_link( $task ) { + $url = $task->get_json()['actionUrl']; + + if ( substr( $url, 0, 4 ) === 'http' ) { + return $url; + } elseif ( $url ) { + return wc_admin_url( '&path=' . $url ); + } + + return admin_url( 'admin.php?page=wc-admin&task=' . $task->get_id() ); + } + + /** + * Get the task list. + * + * @return array + */ + public function get_task_list() { + if ( $this->task_list || $this->initalized ) { + return $this->task_list; + } + + $this->set_task_list( TaskLists::get_list( 'setup' ) ); + $this->initalized = true; + return $this->task_list; + } + + /** + * Set the task list. + */ + public function set_task_list( $task_list ) { + return $this->task_list = $task_list; + } + + /** + * Get the tasks. + * + * @return array + */ + public function get_tasks() { + if ( $this->tasks ) { + return $this->tasks; + } + + $this->tasks = $this->get_task_list()->get_viewable_tasks(); + return $this->tasks; + } + + /** + * Return # of completed tasks + * + * @return integer + */ + public function get_completed_tasks_count() { + $completed_tasks = array_filter( + $this->get_tasks(), + function( $task ) { + return $task->is_complete(); + } + ); + + return count( $completed_tasks ); + } + + /** + * Get the next task. + * + * @return array|null + */ + private function get_next_task() { + foreach ( $this->get_tasks() as $task ) { + if ( false === $task->is_complete() ) { + return $task; + } + } + + return null; + } + + /** + * Check to see if we should display the widget + * + * @return bool + */ + public function should_display_widget() { + if ( ! class_exists( 'Automattic\WooCommerce\Admin\Features\Features' ) || ! class_exists( 'Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists' ) ) { + return false; + } + + if ( ! Features::is_enabled( 'onboarding' ) || ! WC()->is_wc_admin_active() ) { + return false; + } + + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return false; + } + + if ( ! $this->get_task_list() || $this->get_task_list()->is_complete() || $this->get_task_list()->is_hidden() ) { + return false; + } + + return true; + } + + } + +endif; + +return new WC_Admin_Dashboard_Setup(); diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-dashboard.php b/plugins/woocommerce/includes/admin/class-wc-admin-dashboard.php new file mode 100644 index 00000000000..6357b46e8bd --- /dev/null +++ b/plugins/woocommerce/includes/admin/class-wc-admin-dashboard.php @@ -0,0 +1,562 @@ +should_display_widget() ) { + // If on network admin, only load the widget that works in that context and skip the rest. + if ( is_multisite() && is_network_admin() ) { + add_action( 'wp_network_dashboard_setup', array( $this, 'register_network_order_widget' ) ); + } else { + add_action( 'wp_dashboard_setup', array( $this, 'init' ) ); + } + } + } + + /** + * Init dashboard widgets. + */ + public function init() { + // Reviews Widget. + if ( current_user_can( 'publish_shop_orders' ) && post_type_supports( 'product', 'comments' ) ) { + wp_add_dashboard_widget( 'woocommerce_dashboard_recent_reviews', __( 'WooCommerce Recent Reviews', 'woocommerce' ), array( $this, 'recent_reviews' ) ); + } + wp_add_dashboard_widget( 'woocommerce_dashboard_status', __( 'WooCommerce Status', 'woocommerce' ), array( $this, 'status_widget' ) ); + + // Network Order Widget. + if ( is_multisite() && is_main_site() ) { + $this->register_network_order_widget(); + } + } + + /** + * Register the network order dashboard widget. + */ + public function register_network_order_widget() { + wp_add_dashboard_widget( 'woocommerce_network_orders', __( 'WooCommerce Network Orders', 'woocommerce' ), array( $this, 'network_orders' ) ); + } + + /** + * Check to see if we should display the widget. + * + * @return bool + */ + private function should_display_widget() { + if ( ! WC()->is_wc_admin_active() ) { + return false; + } + + $has_permission = current_user_can( 'view_woocommerce_reports' ) || current_user_can( 'manage_woocommerce' ) || current_user_can( 'publish_shop_orders' ); + $task_completed_or_hidden = 'yes' === get_option( 'woocommerce_task_list_complete' ) || 'yes' === get_option( 'woocommerce_task_list_hidden' ); + return $task_completed_or_hidden && $has_permission; + } + + /** + * Get top seller from DB. + * + * @return object + */ + private function get_top_seller() { + global $wpdb; + + $query = array(); + $query['fields'] = "SELECT SUM( order_item_meta.meta_value ) as qty, order_item_meta_2.meta_value as product_id + FROM {$wpdb->posts} as posts"; + $query['join'] = "INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_id "; + $query['join'] .= "INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta ON order_items.order_item_id = order_item_meta.order_item_id "; + $query['join'] .= "INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_2 ON order_items.order_item_id = order_item_meta_2.order_item_id "; + $query['where'] = "WHERE posts.post_type IN ( '" . implode( "','", wc_get_order_types( 'order-count' ) ) . "' ) "; + $query['where'] .= "AND posts.post_status IN ( 'wc-" . implode( "','wc-", apply_filters( 'woocommerce_reports_order_statuses', array( 'completed', 'processing', 'on-hold' ) ) ) . "' ) "; + $query['where'] .= "AND order_item_meta.meta_key = '_qty' "; + $query['where'] .= "AND order_item_meta_2.meta_key = '_product_id' "; + $query['where'] .= "AND posts.post_date >= '" . gmdate( 'Y-m-01', current_time( 'timestamp' ) ) . "' "; + $query['where'] .= "AND posts.post_date <= '" . gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) ) . "' "; + $query['groupby'] = 'GROUP BY product_id'; + $query['orderby'] = 'ORDER BY qty DESC'; + $query['limits'] = 'LIMIT 1'; + + return $wpdb->get_row( implode( ' ', apply_filters( 'woocommerce_dashboard_status_widget_top_seller_query', $query ) ) ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + + /** + * Get sales report data. + * + * @return object + */ + private function get_sales_report_data() { + include_once dirname( __FILE__ ) . '/reports/class-wc-report-sales-by-date.php'; + + $sales_by_date = new WC_Report_Sales_By_Date(); + $sales_by_date->start_date = strtotime( gmdate( 'Y-m-01', current_time( 'timestamp' ) ) ); + $sales_by_date->end_date = strtotime( gmdate( 'Y-m-d', current_time( 'timestamp' ) ) ); + $sales_by_date->chart_groupby = 'day'; + $sales_by_date->group_by_query = 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)'; + + return $sales_by_date->get_report_data(); + } + + /** + * Show status widget. + */ + public function status_widget() { + $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; + $version = Constants::get_constant( 'WC_VERSION' ); + + wp_enqueue_script( 'wc-status-widget', WC()->plugin_url() . '/assets/js/admin/wc-status-widget' . $suffix . '.js', array( 'jquery' ), $version, true ); + + include_once dirname( __FILE__ ) . '/reports/class-wc-admin-report.php'; + + $is_wc_admin_disabled = apply_filters( 'woocommerce_admin_disabled', false ); + + $reports = new WC_Admin_Report(); + + $net_sales_link = 'admin.php?page=wc-reports&tab=orders&range=month'; + $top_seller_link = 'admin.php?page=wc-reports&tab=orders&report=sales_by_product&range=month&product_ids='; + $report_data = $is_wc_admin_disabled ? $this->get_sales_report_data() : $this->get_wc_admin_performance_data(); + if ( ! $is_wc_admin_disabled ) { + $net_sales_link = 'admin.php?page=wc-admin&path=%2Fanalytics%2Frevenue&chart=net_revenue&orderby=net_revenue&period=month&compare=previous_period'; + $top_seller_link = 'admin.php?page=wc-admin&filter=single_product&path=%2Fanalytics%2Fproducts&products='; + } + + echo ''; + } + + /** + * Show order data is status widget. + */ + private function status_widget_order_rows() { + if ( ! current_user_can( 'edit_shop_orders' ) ) { + return; + } + $on_hold_count = 0; + $processing_count = 0; + + foreach ( wc_get_order_types( 'order-count' ) as $type ) { + $counts = (array) wp_count_posts( $type ); + $on_hold_count += isset( $counts['wc-on-hold'] ) ? $counts['wc-on-hold'] : 0; + $processing_count += isset( $counts['wc-processing'] ) ? $counts['wc-processing'] : 0; + } + ?> +
  • + + %s order awaiting processing', '%s orders awaiting processing', $processing_count, 'woocommerce' ), + $processing_count + ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped + ?> + +
  • +
  • + + %s order on-hold', '%s orders on-hold', $on_hold_count, 'woocommerce' ), + $on_hold_count + ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped + ?> + +
  • + get_var( + $wpdb->prepare( + "SELECT COUNT( product_id ) + FROM {$wpdb->wc_product_meta_lookup} AS lookup + INNER JOIN {$wpdb->posts} as posts ON lookup.product_id = posts.ID + WHERE stock_quantity <= %d + AND stock_quantity > %d + AND posts.post_status = 'publish'", + $stock, + $nostock + ) + ); + } + + set_transient( $transient_name, (int) $lowinstock_count, DAY_IN_SECONDS * 30 ); + } + + $transient_name = 'wc_outofstock_count'; + $outofstock_count = get_transient( $transient_name ); + $lowstock_link = 'admin.php?page=wc-reports&tab=stock&report=low_in_stock'; + $outofstock_link = 'admin.php?page=wc-reports&tab=stock&report=out_of_stock'; + + if ( false === $is_wc_admin_disabled ) { + $lowstock_link = 'admin.php?page=wc-admin&type=lowstock&path=%2Fanalytics%2Fstock'; + $outofstock_link = 'admin.php?page=wc-admin&type=outofstock&path=%2Fanalytics%2Fstock'; + } + + if ( false === $outofstock_count ) { + /** + * Status widget out of stock count pre query. + * + * @since 4.3.0 + * @param null|string $outofstock_count Out of stock count, by default null. + * @param int $nostock No stock amount + */ + $outofstock_count = apply_filters( 'woocommerce_status_widget_out_of_stock_count_pre_query', null, $nostock ); + + if ( is_null( $outofstock_count ) ) { + $outofstock_count = (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT( product_id ) + FROM {$wpdb->wc_product_meta_lookup} AS lookup + INNER JOIN {$wpdb->posts} as posts ON lookup.product_id = posts.ID + WHERE stock_quantity <= %d + AND posts.post_status = 'publish'", + $nostock + ) + ); + } + + set_transient( $transient_name, (int) $outofstock_count, DAY_IN_SECONDS * 30 ); + } + ?> +
  • + + %s product low in stock', '%s products low in stock', $lowinstock_count, 'woocommerce' ), + $lowinstock_count + ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped + ?> + +
  • +
  • + + %s product out of stock', '%s products out of stock', $outofstock_count, 'woocommerce' ), + $outofstock_count + ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped + ?> + +
  • + comments} comments + LEFT JOIN {$wpdb->posts} posts ON (comments.comment_post_ID = posts.ID) + WHERE comments.comment_approved = '1' + AND comments.comment_type = 'review' + AND posts.post_password = '' + AND posts.post_type = 'product' + AND comments.comment_parent = 0 + ORDER BY comments.comment_date_gmt DESC + LIMIT 5" + ); + + $comments = $wpdb->get_results( + "SELECT posts.ID, posts.post_title, comments.comment_author, comments.comment_author_email, comments.comment_ID, comments.comment_content {$query_from};" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + ); + + if ( $comments ) { + echo '
      '; + foreach ( $comments as $comment ) { + + echo '
    • '; + + echo get_avatar( $comment->comment_author_email, '32' ); + + $rating = intval( get_comment_meta( $comment->comment_ID, 'rating', true ) ); + + /* translators: %s: rating */ + echo '
      ' . sprintf( esc_html__( '%s out of 5', 'woocommerce' ), esc_html( $rating ) ) . '
      '; + + /* translators: %s: review author */ + echo '

      ' . esc_html( apply_filters( 'woocommerce_admin_dashboard_recent_reviews', $comment->post_title, $comment ) ) . ' ' . sprintf( esc_html__( 'reviewed by %s', 'woocommerce' ), esc_html( $comment->comment_author ) ) . '

      '; + echo '
      ' . wp_kses_data( $comment->comment_content ) . '
    • '; + + } + echo '
    '; + } else { + echo '

    ' . esc_html__( 'There are no product reviews yet.', 'woocommerce' ) . '

    '; + } + } + + /** + * Network orders widget. + */ + public function network_orders() { + $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; + $version = Constants::get_constant( 'WC_VERSION' ); + + wp_enqueue_style( 'wc-network-orders', WC()->plugin_url() . '/assets/css/network-order-widget.css', array(), $version ); + + wp_enqueue_script( 'wc-network-orders', WC()->plugin_url() . '/assets/js/admin/network-orders' . $suffix . '.js', array( 'jquery', 'underscore' ), $version, true ); + + $user = wp_get_current_user(); + $blogs = get_blogs_of_user( $user->ID ); + $blog_ids = wp_list_pluck( $blogs, 'userblog_id' ); + + wp_localize_script( + 'wc-network-orders', + 'woocommerce_network_orders', + array( + 'nonce' => wp_create_nonce( 'wp_rest' ), + 'sites' => array_values( $blog_ids ), + 'order_endpoint' => get_rest_url( null, 'wc/v3/orders/network' ), + ) + ); + ?> +
    +
    +

    + +

    + +
    +
    + + + + + + + + + + +
    +
    +

    + +

    +
    + + + + + set_query_params( + array( + 'before' => $end_date, + 'after' => $start_date, + 'stats' => 'revenue/total_sales,revenue/net_revenue,orders/orders_count,products/items_sold,variations/items_sold', + ) + ); + $response = rest_do_request( $request ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + if ( 200 !== $response->get_status() ) { + return new \WP_Error( 'woocommerce_analytics_performance_indicators_result_failed', __( 'Sorry, fetching performance indicators failed.', 'woocommerce' ) ); + } + $report_keys = array( + 'net_revenue' => 'net_sales', + ); + $performance_data = new stdClass(); + foreach ( $response->get_data() as $indicator ) { + if ( isset( $indicator['chart'] ) && isset( $indicator['value'] ) ) { + $key = isset( $report_keys[ $indicator['chart'] ] ) ? $report_keys[ $indicator['chart'] ] : $indicator['chart']; + $performance_data->$key = $indicator['value']; + } + } + return $performance_data; + } + + /** + * Overwrites the original sparkline to use the new reports data if WooAdmin is enabled. + * Prepares a sparkline to show sales in the last X days. + * + * @param WC_Admin_Report $reports old class for getting reports. + * @param bool $is_wc_admin_disabled If WC Admin is disabled or not. + * @param int $id ID of the product to show. Blank to get all orders. + * @param string $type Type of sparkline to get. Ignored if ID is not set. + * @return string + */ + private function sales_sparkline( $reports, $is_wc_admin_disabled = false, $id = '', $type = 'sales' ) { + $days = max( 7, gmdate( 'd', current_time( 'timestamp' ) ) ); + if ( $is_wc_admin_disabled ) { + return $reports->sales_sparkline( $id, $days, $type ); + } + $sales_endpoint = '/wc-analytics/reports/revenue/stats'; + $start_date = gmdate( 'Y-m-d 00:00:00', current_time( 'timestamp' ) - ( ( $days - 1 ) * DAY_IN_SECONDS ) ); + $end_date = gmdate( 'Y-m-d 23:59:59', current_time( 'timestamp' ) ); + $meta_key = 'net_revenue'; + $params = array( + 'order' => 'asc', + 'interval' => 'day', + 'per_page' => 100, + 'before' => $end_date, + 'after' => $start_date, + ); + if ( $id ) { + $sales_endpoint = '/wc-analytics/reports/products/stats'; + $meta_key = ( 'sales' === $type ) ? 'net_revenue' : 'items_sold'; + $params['products'] = $id; + } + $request = new \WP_REST_Request( 'GET', $sales_endpoint ); + $params['fields'] = array( $meta_key ); + $request->set_query_params( $params ); + + $response = rest_do_request( $request ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $resp_data = $response->get_data(); + $data = $resp_data['intervals']; + + $sparkline_data = array(); + $total = 0; + foreach ( $data as $d ) { + $total += $d['subtotals']->$meta_key; + array_push( $sparkline_data, array( strval( strtotime( $d['interval'] ) * 1000 ), $d['subtotals']->$meta_key ) ); + } + + if ( 'sales' === $type ) { + /* translators: 1: total income 2: days */ + $tooltip = sprintf( __( 'Sold %1$s worth in the last %2$d days', 'woocommerce' ), strip_tags( wc_price( $total ) ), $days ); + } else { + /* translators: 1: total items sold 2: days */ + $tooltip = sprintf( _n( 'Sold %1$d item in the last %2$d days', 'Sold %1$d items in the last %2$d days', $total, 'woocommerce' ), $total, $days ); + } + + return ''; + } + } + +endif; + +return new WC_Admin_Dashboard(); diff --git a/includes/admin/class-wc-admin-duplicate-product.php b/plugins/woocommerce/includes/admin/class-wc-admin-duplicate-product.php similarity index 100% rename from includes/admin/class-wc-admin-duplicate-product.php rename to plugins/woocommerce/includes/admin/class-wc-admin-duplicate-product.php diff --git a/includes/admin/class-wc-admin-exporters.php b/plugins/woocommerce/includes/admin/class-wc-admin-exporters.php similarity index 100% rename from includes/admin/class-wc-admin-exporters.php rename to plugins/woocommerce/includes/admin/class-wc-admin-exporters.php diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-help.php b/plugins/woocommerce/includes/admin/class-wc-admin-help.php new file mode 100644 index 00000000000..c5afccda4d3 --- /dev/null +++ b/plugins/woocommerce/includes/admin/class-wc-admin-help.php @@ -0,0 +1,85 @@ +id, wc_get_screen_ids() ) ) { + return; + } + + $screen->add_help_tab( + array( + 'id' => 'woocommerce_support_tab', + 'title' => __( 'Help & Support', 'woocommerce' ), + 'content' => + '

    ' . __( 'Help & Support', 'woocommerce' ) . '

    ' . + '

    ' . sprintf( + /* translators: %s: Documentation URL */ + __( 'Should you need help understanding, using, or extending WooCommerce, please read our documentation. You will find all kinds of resources including snippets, tutorials and much more.', 'woocommerce' ), + 'https://docs.woocommerce.com/documentation/plugins/woocommerce/?utm_source=helptab&utm_medium=product&utm_content=docs&utm_campaign=woocommerceplugin' + ) . '

    ' . + '

    ' . sprintf( + /* translators: %s: Forum URL */ + __( 'For further assistance with WooCommerce core, use the community forum. For help with premium extensions sold on WooCommerce.com, open a support request at WooCommerce.com.', 'woocommerce' ), + 'https://wordpress.org/support/plugin/woocommerce', + 'https://woocommerce.com/my-account/create-a-ticket/?utm_source=helptab&utm_medium=product&utm_content=tickets&utm_campaign=woocommerceplugin' + ) . '

    ' . + '

    ' . __( 'Before asking for help, we recommend checking the system status page to identify any problems with your configuration.', 'woocommerce' ) . '

    ' . + '

    ' . __( 'System status', 'woocommerce' ) . ' ' . __( 'Community forum', 'woocommerce' ) . ' ' . __( 'WooCommerce.com support', 'woocommerce' ) . '

    ', + ) + ); + + $screen->add_help_tab( + array( + 'id' => 'woocommerce_bugs_tab', + 'title' => __( 'Found a bug?', 'woocommerce' ), + 'content' => + '

    ' . __( 'Found a bug?', 'woocommerce' ) . '

    ' . + /* translators: 1: GitHub issues URL 2: GitHub contribution guide URL 3: System status report URL */ + '

    ' . sprintf( __( 'If you find a bug within WooCommerce core you can create a ticket via GitHub issues. Ensure you read the contribution guide prior to submitting your report. To help us solve your issue, please be as descriptive as possible and include your system status report.', 'woocommerce' ), 'https://github.com/woocommerce/woocommerce/issues?state=open', 'https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md', admin_url( 'admin.php?page=wc-status' ) ) . '

    ' . + '

    ' . __( 'Report a bug', 'woocommerce' ) . ' ' . __( 'System status', 'woocommerce' ) . '

    ', + + ) + ); + + $screen->set_help_sidebar( + '

    ' . __( 'For more information:', 'woocommerce' ) . '

    ' . + '

    ' . __( 'About WooCommerce', 'woocommerce' ) . '

    ' . + '

    ' . __( 'WordPress.org project', 'woocommerce' ) . '

    ' . + '

    ' . __( 'GitHub project', 'woocommerce' ) . '

    ' . + '

    ' . __( 'Official theme', 'woocommerce' ) . '

    ' . + '

    ' . __( 'Official extensions', 'woocommerce' ) . '

    ' + ); + } +} + +return new WC_Admin_Help(); diff --git a/includes/admin/class-wc-admin-importers.php b/plugins/woocommerce/includes/admin/class-wc-admin-importers.php similarity index 100% rename from includes/admin/class-wc-admin-importers.php rename to plugins/woocommerce/includes/admin/class-wc-admin-importers.php diff --git a/includes/admin/class-wc-admin-log-table-list.php b/plugins/woocommerce/includes/admin/class-wc-admin-log-table-list.php similarity index 100% rename from includes/admin/class-wc-admin-log-table-list.php rename to plugins/woocommerce/includes/admin/class-wc-admin-log-table-list.php diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-menus.php b/plugins/woocommerce/includes/admin/class-wc-admin-menus.php new file mode 100644 index 00000000000..1f6542918b8 --- /dev/null +++ b/plugins/woocommerce/includes/admin/class-wc-admin-menus.php @@ -0,0 +1,400 @@ + Menus > Pages. + add_action( 'admin_head-nav-menus.php', array( $this, 'add_nav_menu_meta_boxes' ) ); + + // Admin bar menus. + if ( apply_filters( 'woocommerce_show_admin_bar_visit_store', true ) ) { + add_action( 'admin_bar_menu', array( $this, 'admin_bar_menus' ), 31 ); + } + + // Handle saving settings earlier than load-{page} hook to avoid race conditions in conditional menus. + add_action( 'wp_loaded', array( $this, 'save_settings' ) ); + } + + /** + * Add menu items. + */ + public function admin_menu() { + global $menu; + + $woocommerce_icon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiPjxwYXRoIGZpbGw9IiNhMmFhYjIiIGQ9Ik02MTIuMTkyIDQyNi4zMzZjMC02Ljg5Ni0zLjEzNi01MS42LTI4LTUxLjYtMzcuMzYgMC00Ni43MDQgNzIuMjU2LTQ2LjcwNCA4Mi42MjQgMCAzLjQwOCAzLjE1MiA1OC40OTYgMjguMDMyIDU4LjQ5NiAzNC4xOTItLjAzMiA0Ni42NzItNzIuMjg4IDQ2LjY3Mi04OS41MnptMjAyLjE5MiAwYzAtNi44OTYtMy4xNTItNTEuNi0yOC4wMzItNTEuNi0zNy4yOCAwLTQ2LjYwOCA3Mi4yNTYtNDYuNjA4IDgyLjYyNCAwIDMuNDA4IDMuMDcyIDU4LjQ5NiAyNy45NTIgNTguNDk2IDM0LjE5Mi0uMDMyIDQ2LjY4OC03Mi4yODggNDYuNjg4LTg5LjUyek0xNDEuMjk2Ljc2OGMtNjguMjI0IDAtMTIzLjUwNCA1NS40ODgtMTIzLjUwNCAxMjMuOTJ2NjUwLjcyYzAgNjguNDMyIDU1LjI5NiAxMjMuOTIgMTIzLjUwNCAxMjMuOTJoMzM5LjgwOGwxMjMuNTA0IDEyMy45MzZWODk5LjMyOGgyNzguMDQ4YzY4LjIyNCAwIDEyMy41Mi01NS40NzIgMTIzLjUyLTEyMy45MnYtNjUwLjcyYzAtNjguNDMyLTU1LjI5Ni0xMjMuOTItMTIzLjUyLTEyMy45MmgtNzQxLjM2em01MjYuODY0IDQyMi4xNmMwIDU1LjA4OC0zMS4wODggMTU0Ljg4LTEwMi42NCAxNTQuODgtNi4yMDggMC0xOC40OTYtMy42MTYtMjUuNDI0LTYuMDE2LTMyLjUxMi0xMS4xNjgtNTAuMTkyLTQ5LjY5Ni01Mi4zNTItNjYuMjU2IDAgMC0zLjA3Mi0xNy43OTItMy4wNzItNDAuNzUyIDAtMjIuOTkyIDMuMDcyLTQ1LjMyOCAzLjA3Mi00NS4zMjggMTUuNTUyLTc1LjcyOCA0My41NTItMTA2LjczNiA5Ni40NDgtMTA2LjczNiA1OS4wNzItLjAzMiA4My45NjggNTguNTI4IDgzLjk2OCAxMTAuMjA4ek00ODYuNDk2IDMwMi40YzAgMy4zOTItNDMuNTUyIDE0MS4xNjgtNDMuNTUyIDIxMy40MjR2NzUuNzEyYy0yLjU5MiAxMi4wOC00LjE2IDI0LjE0NC0yMS44MjQgMjQuMTQ0LTQ2LjYwOCAwLTg4Ljg4LTE1MS40NzItOTIuMDE2LTE2MS44NC02LjIwOCA2Ljg5Ni02Mi4yNCAxNjEuODQtOTYuNDQ4IDE2MS44NC0yNC44NjQgMC00My41NTItMTEzLjY0OC00Ni42MDgtMTIzLjkzNkMxNzYuNzA0IDQzNi42NzIgMTYwIDMzNC4yMjQgMTYwIDMyNy4zMjhjMC0yMC42NzIgMS4xNTItMzguNzM2IDI2LjA0OC0zOC43MzYgNi4yMDggMCAyMS42IDYuMDY0IDIzLjcxMiAxNy4xNjggMTEuNjQ4IDYyLjAzMiAxNi42ODggMTIwLjUxMiAyOS4xNjggMTg1Ljk2OCAxLjg1NiAyLjkyOCAxLjUwNCA3LjAwOCA0LjU2IDEwLjQzMiAzLjE1Mi0xMC4yODggNjYuOTI4LTE2OC43ODQgOTQuOTYtMTY4Ljc4NCAyMi41NDQgMCAzMC40IDQ0LjU5MiAzMy41MzYgNjEuODI0IDYuMjA4IDIwLjY1NiAxMy4wODggNTUuMjE2IDIyLjQxNiA4Mi43NTIgMC0xMy43NzYgMTIuNDgtMjAzLjEyIDY1LjM5Mi0yMDMuMTIgMTguNTkyLjAzMiAyNi43MDQgNi45MjggMjYuNzA0IDI3LjU2OHpNODcwLjMyIDQyMi45MjhjMCA1NS4wODgtMzEuMDg4IDE1NC44OC0xMDIuNjQgMTU0Ljg4LTYuMTkyIDAtMTguNDQ4LTMuNjE2LTI1LjQyNC02LjAxNi0zMi40MzItMTEuMTY4LTUwLjE3Ni00OS42OTYtNTIuMjg4LTY2LjI1NiAwIDAtMy44ODgtMTcuOTItMy44ODgtNDAuODk2czMuODg4LTQ1LjE4NCAzLjg4OC00NS4xODRjMTUuNTUyLTc1LjcyOCA0My40ODgtMTA2LjczNiA5Ni4zODQtMTA2LjczNiA1OS4xMDQtLjAzMiA4My45NjggNTguNTI4IDgzLjk2OCAxMTAuMjA4eiIvPjwvc3ZnPg=='; + + if ( self::can_view_woocommerce_menu_item() ) { + $menu[] = array( '', 'read', 'separator-woocommerce', '', 'wp-menu-separator woocommerce' ); // WPCS: override ok. + } + + add_menu_page( __( 'WooCommerce', 'woocommerce' ), __( 'WooCommerce', 'woocommerce' ), 'edit_others_shop_orders', 'woocommerce', null, $woocommerce_icon, '55.5' ); + + add_submenu_page( 'edit.php?post_type=product', __( 'Attributes', 'woocommerce' ), __( 'Attributes', 'woocommerce' ), 'manage_product_terms', 'product_attributes', array( $this, 'attributes_page' ) ); + } + + /** + * Add menu item. + */ + public function reports_menu() { + if ( self::can_view_woocommerce_menu_item() ) { + add_submenu_page( 'woocommerce', __( 'Reports', 'woocommerce' ), __( 'Reports', 'woocommerce' ), 'view_woocommerce_reports', 'wc-reports', array( $this, 'reports_page' ) ); + } else { + add_menu_page( __( 'Sales reports', 'woocommerce' ), __( 'Sales reports', 'woocommerce' ), 'view_woocommerce_reports', 'wc-reports', array( $this, 'reports_page' ), 'dashicons-chart-bar', '55.6' ); + } + } + + /** + * Add menu item. + */ + public function settings_menu() { + $settings_page = add_submenu_page( 'woocommerce', __( 'WooCommerce settings', 'woocommerce' ), __( 'Settings', 'woocommerce' ), 'manage_woocommerce', 'wc-settings', array( $this, 'settings_page' ) ); + + add_action( 'load-' . $settings_page, array( $this, 'settings_page_init' ) ); + } + + /** + * Check if the user can access the top-level WooCommerce item. + */ + public static function can_view_woocommerce_menu_item() { + return current_user_can( 'edit_others_shop_orders' ); + } + + /** + * Loads gateways and shipping methods into memory for use within settings. + */ + public function settings_page_init() { + WC()->payment_gateways(); + WC()->shipping(); + + // Include settings pages. + WC_Admin_Settings::get_settings_pages(); + + // Add any posted messages. + if ( ! empty( $_GET['wc_error'] ) ) { // WPCS: input var okay, CSRF ok. + WC_Admin_Settings::add_error( wp_kses_post( wp_unslash( $_GET['wc_error'] ) ) ); // WPCS: input var okay, CSRF ok. + } + + if ( ! empty( $_GET['wc_message'] ) ) { // WPCS: input var okay, CSRF ok. + WC_Admin_Settings::add_message( wp_kses_post( wp_unslash( $_GET['wc_message'] ) ) ); // WPCS: input var okay, CSRF ok. + } + + do_action( 'woocommerce_settings_page_init' ); + } + + /** + * Handle saving of settings. + * + * @return void + */ + public function save_settings() { + global $current_tab, $current_section; + + // We should only save on the settings page. + if ( ! is_admin() || ! isset( $_GET['page'] ) || 'wc-settings' !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return; + } + + // Include settings pages. + WC_Admin_Settings::get_settings_pages(); + + // Get current tab/section. + $current_tab = empty( $_GET['tab'] ) ? 'general' : sanitize_title( wp_unslash( $_GET['tab'] ) ); // WPCS: input var okay, CSRF ok. + $current_section = empty( $_REQUEST['section'] ) ? '' : sanitize_title( wp_unslash( $_REQUEST['section'] ) ); // WPCS: input var okay, CSRF ok. + + // Save settings if data has been posted. + if ( '' !== $current_section && apply_filters( "woocommerce_save_settings_{$current_tab}_{$current_section}", ! empty( $_POST['save'] ) ) ) { // WPCS: input var okay, CSRF ok. + WC_Admin_Settings::save(); + } elseif ( '' === $current_section && apply_filters( "woocommerce_save_settings_{$current_tab}", ! empty( $_POST['save'] ) ) ) { // WPCS: input var okay, CSRF ok. + WC_Admin_Settings::save(); + } + } + + /** + * Add menu item. + */ + public function status_menu() { + add_submenu_page( 'woocommerce', __( 'WooCommerce status', 'woocommerce' ), __( 'Status', 'woocommerce' ), 'manage_woocommerce', 'wc-status', array( $this, 'status_page' ) ); + } + + /** + * Addons menu item. + */ + public function addons_menu() { + $count_html = WC_Helper_Updater::get_updates_count_html(); + /* translators: %s: extensions count */ + $menu_title = sprintf( __( 'Extensions %s', 'woocommerce' ), $count_html ); + add_submenu_page( 'woocommerce', __( 'WooCommerce extensions', 'woocommerce' ), $menu_title, 'manage_woocommerce', 'wc-addons', array( $this, 'addons_page' ) ); + } + + /** + * Highlights the correct top level admin menu item for post type add screens. + */ + public function menu_highlight() { + global $parent_file, $submenu_file, $post_type; + + switch ( $post_type ) { + case 'shop_order': + case 'shop_coupon': + $parent_file = 'woocommerce'; // WPCS: override ok. + break; + case 'product': + $screen = get_current_screen(); + if ( $screen && taxonomy_is_product_attribute( $screen->taxonomy ) ) { + $submenu_file = 'product_attributes'; // WPCS: override ok. + $parent_file = 'edit.php?post_type=product'; // WPCS: override ok. + } + break; + } + } + + /** + * Adds the order processing count to the menu. + */ + public function menu_order_count() { + global $submenu; + + if ( isset( $submenu['woocommerce'] ) ) { + // Remove 'WooCommerce' sub menu item. + unset( $submenu['woocommerce'][0] ); + + // Add count if user has access. + if ( apply_filters( 'woocommerce_include_processing_order_count_in_menu', true ) && current_user_can( 'edit_others_shop_orders' ) ) { + $order_count = apply_filters( 'woocommerce_menu_order_count', wc_processing_order_count() ); + + if ( $order_count ) { + foreach ( $submenu['woocommerce'] as $key => $menu_item ) { + if ( 0 === strpos( $menu_item[0], _x( 'Orders', 'Admin menu name', 'woocommerce' ) ) ) { + $submenu['woocommerce'][ $key ][0] .= ' ' . number_format_i18n( $order_count ) . ''; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + break; + } + } + } + } + } + } + + /** + * Reorder the WC menu items in admin. + * + * @param int $menu_order Menu order. + * @return array + */ + public function menu_order( $menu_order ) { + // Initialize our custom order array. + $woocommerce_menu_order = array(); + + // Get the index of our custom separator. + $woocommerce_separator = array_search( 'separator-woocommerce', $menu_order, true ); + + // Get index of product menu. + $woocommerce_product = array_search( 'edit.php?post_type=product', $menu_order, true ); + + // Loop through menu order and do some rearranging. + foreach ( $menu_order as $index => $item ) { + + if ( 'woocommerce' === $item ) { + $woocommerce_menu_order[] = 'separator-woocommerce'; + $woocommerce_menu_order[] = $item; + $woocommerce_menu_order[] = 'edit.php?post_type=product'; + unset( $menu_order[ $woocommerce_separator ] ); + unset( $menu_order[ $woocommerce_product ] ); + } elseif ( ! in_array( $item, array( 'separator-woocommerce' ), true ) ) { + $woocommerce_menu_order[] = $item; + } + } + + // Return order. + return $woocommerce_menu_order; + } + + /** + * Custom menu order. + * + * @param bool $enabled Whether custom menu ordering is already enabled. + * @return bool + */ + public function custom_menu_order( $enabled ) { + return $enabled || self::can_view_woocommerce_menu_item(); + } + + /** + * Validate screen options on update. + * + * @param bool|int $status Screen option value. Default false to skip. + * @param string $option The option name. + * @param int $value The number of rows to use. + */ + public function set_screen_option( $status, $option, $value ) { + if ( in_array( $option, array( 'woocommerce_keys_per_page', 'woocommerce_webhooks_per_page' ), true ) ) { + return $value; + } + + return $status; + } + + /** + * Init the reports page. + */ + public function reports_page() { + WC_Admin_Reports::output(); + } + + /** + * Init the settings page. + */ + public function settings_page() { + WC_Admin_Settings::output(); + } + + /** + * Init the attributes page. + */ + public function attributes_page() { + WC_Admin_Attributes::output(); + } + + /** + * Init the status page. + */ + public function status_page() { + WC_Admin_Status::output(); + } + + /** + * Init the addons page. + */ + public function addons_page() { + WC_Admin_Addons::output(); + } + + /** + * Add custom nav meta box. + * + * Adapted from http://www.johnmorrisonline.com/how-to-add-a-fully-functional-custom-meta-box-to-wordpress-navigation-menus/. + */ + public function add_nav_menu_meta_boxes() { + add_meta_box( 'woocommerce_endpoints_nav_link', __( 'WooCommerce endpoints', 'woocommerce' ), array( $this, 'nav_menu_links' ), 'nav-menus', 'side', 'low' ); + } + + /** + * Output menu links. + */ + public function nav_menu_links() { + // Get items from account menu. + $endpoints = wc_get_account_menu_items(); + + // Remove dashboard item. + if ( isset( $endpoints['dashboard'] ) ) { + unset( $endpoints['dashboard'] ); + } + + // Include missing lost password. + $endpoints['lost-password'] = __( 'Lost password', 'woocommerce' ); + + $endpoints = apply_filters( 'woocommerce_custom_nav_menu_items', $endpoints ); + + ?> +
    +
    +
      + $value ) : + ?> +
    • + + + + + +
    • + +
    +
    +

    + + + + + + + +

    +
    + add_node( + array( + 'parent' => 'site-name', + 'id' => 'view-store', + 'title' => __( 'Visit Store', 'woocommerce' ), + 'href' => wc_get_page_permalink( 'shop' ), + ) + ); + } + +} + +return new WC_Admin_Menus(); diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-meta-boxes.php b/plugins/woocommerce/includes/admin/class-wc-admin-meta-boxes.php new file mode 100644 index 00000000000..b8c39253cf3 --- /dev/null +++ b/plugins/woocommerce/includes/admin/class-wc-admin-meta-boxes.php @@ -0,0 +1,264 @@ +'; + + foreach ( $errors as $error ) { + echo '

    ' . wp_kses_post( $error ) . '

    '; + } + + echo ''; + + // Clear. + delete_option( 'woocommerce_meta_box_errors' ); + } + } + + /** + * Add WC Meta boxes. + */ + public function add_meta_boxes() { + $screen = get_current_screen(); + $screen_id = $screen ? $screen->id : ''; + + // Products. + add_meta_box( 'postexcerpt', __( 'Product short description', 'woocommerce' ), 'WC_Meta_Box_Product_Short_Description::output', 'product', 'normal' ); + add_meta_box( 'woocommerce-product-data', __( 'Product data', 'woocommerce' ), 'WC_Meta_Box_Product_Data::output', 'product', 'normal', 'high' ); + add_meta_box( 'woocommerce-product-images', __( 'Product gallery', 'woocommerce' ), 'WC_Meta_Box_Product_Images::output', 'product', 'side', 'low' ); + + // Orders. + foreach ( wc_get_order_types( 'order-meta-boxes' ) as $type ) { + $order_type_object = get_post_type_object( $type ); + /* Translators: %s order type name. */ + add_meta_box( 'woocommerce-order-data', sprintf( __( '%s data', 'woocommerce' ), $order_type_object->labels->singular_name ), 'WC_Meta_Box_Order_Data::output', $type, 'normal', 'high' ); + add_meta_box( 'woocommerce-order-items', __( 'Items', 'woocommerce' ), 'WC_Meta_Box_Order_Items::output', $type, 'normal', 'high' ); + /* Translators: %s order type name. */ + add_meta_box( 'woocommerce-order-notes', sprintf( __( '%s notes', 'woocommerce' ), $order_type_object->labels->singular_name ), 'WC_Meta_Box_Order_Notes::output', $type, 'side', 'default' ); + add_meta_box( 'woocommerce-order-downloads', __( 'Downloadable product permissions', 'woocommerce' ) . wc_help_tip( __( 'Note: Permissions for order items will automatically be granted when the order status changes to processing/completed.', 'woocommerce' ) ), 'WC_Meta_Box_Order_Downloads::output', $type, 'normal', 'default' ); + /* Translators: %s order type name. */ + add_meta_box( 'woocommerce-order-actions', sprintf( __( '%s actions', 'woocommerce' ), $order_type_object->labels->singular_name ), 'WC_Meta_Box_Order_Actions::output', $type, 'side', 'high' ); + } + + // Coupons. + add_meta_box( 'woocommerce-coupon-data', __( 'Coupon data', 'woocommerce' ), 'WC_Meta_Box_Coupon_Data::output', 'shop_coupon', 'normal', 'high' ); + + // Comment rating. + if ( 'comment' === $screen_id && isset( $_GET['c'] ) && metadata_exists( 'comment', wc_clean( wp_unslash( $_GET['c'] ) ), 'rating' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + add_meta_box( 'woocommerce-rating', __( 'Rating', 'woocommerce' ), 'WC_Meta_Box_Product_Reviews::output', 'comment', 'normal', 'high' ); + } + } + + /** + * Remove bloat. + */ + public function remove_meta_boxes() { + remove_meta_box( 'postexcerpt', 'product', 'normal' ); + remove_meta_box( 'product_shipping_classdiv', 'product', 'side' ); + remove_meta_box( 'commentsdiv', 'product', 'normal' ); + remove_meta_box( 'commentstatusdiv', 'product', 'side' ); + remove_meta_box( 'commentstatusdiv', 'product', 'normal' ); + remove_meta_box( 'woothemes-settings', 'shop_coupon', 'normal' ); + remove_meta_box( 'commentstatusdiv', 'shop_coupon', 'normal' ); + remove_meta_box( 'slugdiv', 'shop_coupon', 'normal' ); + + foreach ( wc_get_order_types( 'order-meta-boxes' ) as $type ) { + remove_meta_box( 'commentsdiv', $type, 'normal' ); + remove_meta_box( 'woothemes-settings', $type, 'normal' ); + remove_meta_box( 'commentstatusdiv', $type, 'normal' ); + remove_meta_box( 'slugdiv', $type, 'normal' ); + remove_meta_box( 'submitdiv', $type, 'side' ); + } + } + + /** + * Rename core meta boxes. + */ + public function rename_meta_boxes() { + global $post; + + // Comments/Reviews. + if ( isset( $post ) && ( 'publish' === $post->post_status || 'private' === $post->post_status ) && post_type_supports( 'product', 'comments' ) ) { + remove_meta_box( 'commentsdiv', 'product', 'normal' ); + add_meta_box( 'commentsdiv', __( 'Reviews', 'woocommerce' ), 'post_comment_meta_box', 'product', 'normal' ); + } + } + + /** + * Check if we're saving, the trigger an action based on the post type. + * + * @param int $post_id Post ID. + * @param object $post Post object. + */ + public function save_meta_boxes( $post_id, $post ) { + $post_id = absint( $post_id ); + + // $post_id and $post are required + if ( empty( $post_id ) || empty( $post ) || self::$saved_meta_boxes ) { + return; + } + + // Dont' save meta boxes for revisions or autosaves. + if ( Constants::is_true( 'DOING_AUTOSAVE' ) || is_int( wp_is_post_revision( $post ) ) || is_int( wp_is_post_autosave( $post ) ) ) { + return; + } + + // Check the nonce. + if ( empty( $_POST['woocommerce_meta_nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['woocommerce_meta_nonce'] ), 'woocommerce_save_data' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + return; + } + + // Check the post being saved == the $post_id to prevent triggering this call for other save_post events. + if ( empty( $_POST['post_ID'] ) || absint( $_POST['post_ID'] ) !== $post_id ) { + return; + } + + // Check user has permission to edit. + if ( ! current_user_can( 'edit_post', $post_id ) ) { + return; + } + + // We need this save event to run once to avoid potential endless loops. This would have been perfect: + // remove_action( current_filter(), __METHOD__ ); + // But cannot be used due to https://github.com/woocommerce/woocommerce/issues/6485 + // When that is patched in core we can use the above. + self::$saved_meta_boxes = true; + + // Check the post type. + if ( in_array( $post->post_type, wc_get_order_types( 'order-meta-boxes' ), true ) ) { + do_action( 'woocommerce_process_shop_order_meta', $post_id, $post ); + } elseif ( in_array( $post->post_type, array( 'product', 'shop_coupon' ), true ) ) { + do_action( 'woocommerce_process_' . $post->post_type . '_meta', $post_id, $post ); + } + } + + /** + * Remove irrelevant block templates from the list of available templates for products. + * This will also remove custom created templates. + * + * @param string[] $templates Array of template header names keyed by the template file name. + * + * @return string[] Templates array excluding block-based templates. + */ + public function remove_block_templates( $templates ) { + if ( count( $templates ) === 0 || ! wc_current_theme_is_fse_theme() || ( ! function_exists( 'gutenberg_get_block_template' ) && ! function_exists( 'get_block_template' ) ) ) { + return $templates; + } + + $theme = wp_get_theme()->get_stylesheet(); + $filtered_templates = array(); + + foreach ( $templates as $template_key => $template_name ) { + // Filter out the single-product.html template as this is a duplicate of "Default Template". + if ( 'single-product' === $template_key ) { + continue; + } + + $block_template = function_exists( 'gutenberg_get_block_template' ) ? + gutenberg_get_block_template( $theme . '//' . $template_key ) : + get_block_template( $theme . '//' . $template_key ); + + // If the block template has the product post type specified, include it. + if ( $block_template && in_array( 'product', $block_template->post_types ) ) { + $filtered_templates[ $template_key ] = $template_name; + } + } + + return $filtered_templates; + } +} + +new WC_Admin_Meta_Boxes(); diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-notices.php b/plugins/woocommerce/includes/admin/class-wc-admin-notices.php new file mode 100644 index 00000000000..3677c9ef5cb --- /dev/null +++ b/plugins/woocommerce/includes/admin/class-wc-admin-notices.php @@ -0,0 +1,648 @@ + callback. + * + * @var array + */ + private static $core_notices = array( + 'update' => 'update_notice', + 'template_files' => 'template_file_check_notice', + 'legacy_shipping' => 'legacy_shipping_notice', + 'no_shipping_methods' => 'no_shipping_methods_notice', + 'regenerating_thumbnails' => 'regenerating_thumbnails_notice', + 'regenerating_lookup_table' => 'regenerating_lookup_table_notice', + 'no_secure_connection' => 'secure_connection_notice', + WC_PHP_MIN_REQUIREMENTS_NOTICE => 'wp_php_min_requirements_notice', + 'maxmind_license_key' => 'maxmind_missing_license_key_notice', + 'redirect_download_method' => 'redirect_download_method_notice', + 'uploads_directory_is_unprotected' => 'uploads_directory_is_unprotected_notice', + 'base_tables_missing' => 'base_tables_missing_notice', + 'download_directories_sync_complete' => 'download_directories_sync_complete', + ); + + /** + * Constructor. + */ + public static function init() { + self::$notices = get_option( 'woocommerce_admin_notices', array() ); + + add_action( 'switch_theme', array( __CLASS__, 'reset_admin_notices' ) ); + add_action( 'woocommerce_installed', array( __CLASS__, 'reset_admin_notices' ) ); + add_action( 'wp_loaded', array( __CLASS__, 'add_redirect_download_method_notice' ) ); + add_action( 'admin_init', array( __CLASS__, 'hide_notices' ), 20 ); + + // @TODO: This prevents Action Scheduler async jobs from storing empty list of notices during WC installation. + // That could lead to OBW not starting and 'Run setup wizard' notice not appearing in WP admin, which we want + // to avoid. + if ( ! WC_Install::is_new_install() || ! wc_is_running_from_async_action_scheduler() ) { + add_action( 'shutdown', array( __CLASS__, 'store_notices' ) ); + } + + if ( current_user_can( 'manage_woocommerce' ) ) { + add_action( 'admin_print_styles', array( __CLASS__, 'add_notices' ) ); + } + } + + /** + * Parses query to create nonces when available. + * + * @deprecated 5.4.0 + * @param object $response The WP_REST_Response we're working with. + * @return object $response The prepared WP_REST_Response object. + */ + public static function prepare_note_with_nonce( $response ) { + wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '5.4.0' ); + + return $response; + } + + /** + * Store notices to DB + */ + public static function store_notices() { + update_option( 'woocommerce_admin_notices', self::get_notices() ); + } + + /** + * Get notices + * + * @return array + */ + public static function get_notices() { + return self::$notices; + } + + /** + * Remove all notices. + */ + public static function remove_all_notices() { + self::$notices = array(); + } + + /** + * Reset notices for themes when switched or a new version of WC is installed. + */ + public static function reset_admin_notices() { + if ( ! self::is_ssl() ) { + self::add_notice( 'no_secure_connection' ); + } + if ( ! self::is_uploads_directory_protected() ) { + self::add_notice( 'uploads_directory_is_unprotected' ); + } + self::add_notice( 'template_files' ); + self::add_min_version_notice(); + self::add_maxmind_missing_license_key_notice(); + } + + /** + * Show a notice. + * + * @param string $name Notice name. + * @param bool $force_save Force saving inside this method instead of at the 'shutdown'. + */ + public static function add_notice( $name, $force_save = false ) { + self::$notices = array_unique( array_merge( self::get_notices(), array( $name ) ) ); + + if ( $force_save ) { + // Adding early save to prevent more race conditions with notices. + self::store_notices(); + } + } + + /** + * Remove a notice from being displayed. + * + * @param string $name Notice name. + * @param bool $force_save Force saving inside this method instead of at the 'shutdown'. + */ + public static function remove_notice( $name, $force_save = false ) { + self::$notices = array_diff( self::get_notices(), array( $name ) ); + delete_option( 'woocommerce_admin_notice_' . $name ); + + if ( $force_save ) { + // Adding early save to prevent more race conditions with notices. + self::store_notices(); + } + } + + /** + * See if a notice is being shown. + * + * @param string $name Notice name. + * + * @return boolean + */ + public static function has_notice( $name ) { + return in_array( $name, self::get_notices(), true ); + } + + /** + * Hide a notice if the GET variable is set. + */ + public static function hide_notices() { + if ( isset( $_GET['wc-hide-notice'] ) && isset( $_GET['_wc_notice_nonce'] ) ) { // WPCS: input var ok, CSRF ok. + if ( ! wp_verify_nonce( sanitize_key( wp_unslash( $_GET['_wc_notice_nonce'] ) ), 'woocommerce_hide_notices_nonce' ) ) { // WPCS: input var ok, CSRF ok. + wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); + } + + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_die( esc_html__( 'You don’t have permission to do this.', 'woocommerce' ) ); + } + + $hide_notice = sanitize_text_field( wp_unslash( $_GET['wc-hide-notice'] ) ); // WPCS: input var ok, CSRF ok. + + self::hide_notice( $hide_notice ); + } + } + + /** + * Hide a single notice. + * + * @param $name Notice name. + */ + private static function hide_notice( $name ) { + self::remove_notice( $name ); + + update_user_meta( get_current_user_id(), 'dismissed_' . $name . '_notice', true ); + + do_action( 'woocommerce_hide_' . $name . '_notice' ); + } + + /** + * Add notices + styles if needed. + */ + public static function add_notices() { + $notices = self::get_notices(); + + if ( empty( $notices ) ) { + return; + } + + $screen = get_current_screen(); + $screen_id = $screen ? $screen->id : ''; + $show_on_screens = array( + 'dashboard', + 'plugins', + ); + + // Notices should only show on WooCommerce screens, the main dashboard, and on the plugins screen. + if ( ! in_array( $screen_id, wc_get_screen_ids(), true ) && ! in_array( $screen_id, $show_on_screens, true ) ) { + return; + } + + wp_enqueue_style( 'woocommerce-activation', plugins_url( '/assets/css/activation.css', WC_PLUGIN_FILE ), array(), Constants::get_constant( 'WC_VERSION' ) ); + + // Add RTL support. + wp_style_add_data( 'woocommerce-activation', 'rtl', 'replace' ); + + foreach ( $notices as $notice ) { + if ( ! empty( self::$core_notices[ $notice ] ) && apply_filters( 'woocommerce_show_admin_notice', true, $notice ) ) { + add_action( 'admin_notices', array( __CLASS__, self::$core_notices[ $notice ] ) ); + } else { + add_action( 'admin_notices', array( __CLASS__, 'output_custom_notices' ) ); + } + } + } + + /** + * Add a custom notice. + * + * @param string $name Notice name. + * @param string $notice_html Notice HTML. + */ + public static function add_custom_notice( $name, $notice_html ) { + self::add_notice( $name ); + update_option( 'woocommerce_admin_notice_' . $name, wp_kses_post( $notice_html ) ); + } + + /** + * Output any stored custom notices. + */ + public static function output_custom_notices() { + $notices = self::get_notices(); + + if ( ! empty( $notices ) ) { + foreach ( $notices as $notice ) { + if ( empty( self::$core_notices[ $notice ] ) ) { + $notice_html = get_option( 'woocommerce_admin_notice_' . $notice ); + + if ( $notice_html ) { + include dirname( __FILE__ ) . '/views/html-notice-custom.php'; + } + } + } + } + } + + /** + * If we need to update the database, include a message with the DB update button. + */ + public static function update_notice() { + $screen = get_current_screen(); + $screen_id = $screen ? $screen->id : ''; + if ( WC()->is_wc_admin_active() && in_array( $screen_id, wc_get_screen_ids(), true ) ) { + return; + } + + if ( WC_Install::needs_db_update() ) { + $next_scheduled_date = WC()->queue()->get_next( 'woocommerce_run_update_callback', null, 'woocommerce-db-updates' ); + + if ( $next_scheduled_date || ! empty( $_GET['do_update_woocommerce'] ) ) { // WPCS: input var ok, CSRF ok. + include dirname( __FILE__ ) . '/views/html-notice-updating.php'; + } else { + include dirname( __FILE__ ) . '/views/html-notice-update.php'; + } + } else { + include dirname( __FILE__ ) . '/views/html-notice-updated.php'; + } + } + + /** + * If we have just installed, show a message with the install pages button. + * + * @deprecated 4.6.0 + */ + public static function install_notice() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', esc_html__( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); + } + + /** + * Show a notice highlighting bad template files. + */ + public static function template_file_check_notice() { + $core_templates = WC_Admin_Status::scan_template_files( WC()->plugin_path() . '/templates' ); + $outdated = false; + + foreach ( $core_templates as $file ) { + + $theme_file = false; + if ( file_exists( get_stylesheet_directory() . '/' . $file ) ) { + $theme_file = get_stylesheet_directory() . '/' . $file; + } elseif ( file_exists( get_stylesheet_directory() . '/' . WC()->template_path() . $file ) ) { + $theme_file = get_stylesheet_directory() . '/' . WC()->template_path() . $file; + } elseif ( file_exists( get_template_directory() . '/' . $file ) ) { + $theme_file = get_template_directory() . '/' . $file; + } elseif ( file_exists( get_template_directory() . '/' . WC()->template_path() . $file ) ) { + $theme_file = get_template_directory() . '/' . WC()->template_path() . $file; + } + + if ( false !== $theme_file ) { + $core_version = WC_Admin_Status::get_file_version( WC()->plugin_path() . '/templates/' . $file ); + $theme_version = WC_Admin_Status::get_file_version( $theme_file ); + + if ( $core_version && $theme_version && version_compare( $theme_version, $core_version, '<' ) ) { + $outdated = true; + break; + } + } + } + + if ( $outdated ) { + include dirname( __FILE__ ) . '/views/html-notice-template-check.php'; + } else { + self::remove_notice( 'template_files' ); + } + } + + /** + * Show a notice asking users to convert to shipping zones. + * + * @todo remove in 4.0.0 + */ + public static function legacy_shipping_notice() { + $maybe_load_legacy_methods = array( 'flat_rate', 'free_shipping', 'international_delivery', 'local_delivery', 'local_pickup' ); + $enabled = false; + + foreach ( $maybe_load_legacy_methods as $method ) { + $options = get_option( 'woocommerce_' . $method . '_settings' ); + if ( $options && isset( $options['enabled'] ) && 'yes' === $options['enabled'] ) { + $enabled = true; + } + } + + if ( $enabled ) { + include dirname( __FILE__ ) . '/views/html-notice-legacy-shipping.php'; + } else { + self::remove_notice( 'template_files' ); + } + } + + /** + * No shipping methods. + */ + public static function no_shipping_methods_notice() { + if ( wc_shipping_enabled() && ( empty( $_GET['page'] ) || empty( $_GET['tab'] ) || 'wc-settings' !== $_GET['page'] || 'shipping' !== $_GET['tab'] ) ) { // WPCS: input var ok, CSRF ok. + $product_count = wp_count_posts( 'product' ); + $method_count = wc_get_shipping_method_count(); + + if ( $product_count->publish > 0 && 0 === $method_count ) { + include dirname( __FILE__ ) . '/views/html-notice-no-shipping-methods.php'; + } + + if ( $method_count > 0 ) { + self::remove_notice( 'no_shipping_methods' ); + } + } + } + + /** + * Notice shown when regenerating thumbnails background process is running. + */ + public static function regenerating_thumbnails_notice() { + include dirname( __FILE__ ) . '/views/html-notice-regenerating-thumbnails.php'; + } + + /** + * Notice about secure connection. + */ + public static function secure_connection_notice() { + if ( self::is_ssl() || get_user_meta( get_current_user_id(), 'dismissed_no_secure_connection_notice', true ) ) { + return; + } + + include dirname( __FILE__ ) . '/views/html-notice-secure-connection.php'; + } + + /** + * Notice shown when regenerating thumbnails background process is running. + * + * @since 3.6.0 + */ + public static function regenerating_lookup_table_notice() { + // See if this is still relevant. + if ( ! wc_update_product_lookup_tables_is_running() ) { + self::remove_notice( 'regenerating_lookup_table' ); + return; + } + + include dirname( __FILE__ ) . '/views/html-notice-regenerating-lookup-table.php'; + } + + /** + * Add notice about minimum PHP and WordPress requirement. + * + * @since 3.6.5 + */ + public static function add_min_version_notice() { + if ( version_compare( phpversion(), WC_NOTICE_MIN_PHP_VERSION, '<' ) || version_compare( get_bloginfo( 'version' ), WC_NOTICE_MIN_WP_VERSION, '<' ) ) { + self::add_notice( WC_PHP_MIN_REQUIREMENTS_NOTICE ); + } + } + + /** + * Notice about WordPress and PHP minimum requirements. + * + * @since 3.6.5 + * @return void + */ + public static function wp_php_min_requirements_notice() { + if ( apply_filters( 'woocommerce_hide_php_wp_nag', get_user_meta( get_current_user_id(), 'dismissed_' . WC_PHP_MIN_REQUIREMENTS_NOTICE . '_notice', true ) ) ) { + self::remove_notice( WC_PHP_MIN_REQUIREMENTS_NOTICE ); + return; + } + + $old_php = version_compare( phpversion(), WC_NOTICE_MIN_PHP_VERSION, '<' ); + $old_wp = version_compare( get_bloginfo( 'version' ), WC_NOTICE_MIN_WP_VERSION, '<' ); + + // Both PHP and WordPress up to date version => no notice. + if ( ! $old_php && ! $old_wp ) { + return; + } + + if ( $old_php && $old_wp ) { + $msg = sprintf( + /* translators: 1: Minimum PHP version 2: Minimum WordPress version */ + __( 'Update required: WooCommerce will soon require PHP version %1$s and WordPress version %2$s or newer.', 'woocommerce' ), + WC_NOTICE_MIN_PHP_VERSION, + WC_NOTICE_MIN_WP_VERSION + ); + } elseif ( $old_php ) { + $msg = sprintf( + /* translators: %s: Minimum PHP version */ + __( 'Update required: WooCommerce will soon require PHP version %s or newer.', 'woocommerce' ), + WC_NOTICE_MIN_PHP_VERSION + ); + } elseif ( $old_wp ) { + $msg = sprintf( + /* translators: %s: Minimum WordPress version */ + __( 'Update required: WooCommerce will soon require WordPress version %s or newer.', 'woocommerce' ), + WC_NOTICE_MIN_WP_VERSION + ); + } + + include dirname( __FILE__ ) . '/views/html-notice-wp-php-minimum-requirements.php'; + } + + /** + * Add MaxMind missing license key notice. + * + * @since 3.9.0 + */ + public static function add_maxmind_missing_license_key_notice() { + $default_address = get_option( 'woocommerce_default_customer_address' ); + + if ( ! in_array( $default_address, array( 'geolocation', 'geolocation_ajax' ), true ) ) { + return; + } + + $integration_options = get_option( 'woocommerce_maxmind_geolocation_settings' ); + if ( empty( $integration_options['license_key'] ) ) { + self::add_notice( 'maxmind_license_key' ); + + } + } + + /** + * Add notice about Redirect-only download method, nudging user to switch to a different method instead. + */ + public static function add_redirect_download_method_notice() { + if ( 'redirect' === get_option( 'woocommerce_file_download_method' ) ) { + self::add_notice( 'redirect_download_method' ); + } else { + self::remove_notice( 'redirect_download_method' ); + } + } + + /** + * Notice about the completion of the product downloads sync, with further advice for the site operator. + */ + public static function download_directories_sync_complete() { + $notice_dismissed = apply_filters( + 'woocommerce_hide_download_directories_sync_complete', + get_user_meta( get_current_user_id(), 'download_directories_sync_complete', true ) + ); + + if ( $notice_dismissed ) { + self::remove_notice( 'download_directories_sync_complete' ); + } + + if ( Users::is_site_administrator() ) { + include __DIR__ . '/views/html-notice-download-dir-sync-complete.php'; + } + } + + /** + * Display MaxMind missing license key notice. + * + * @since 3.9.0 + */ + public static function maxmind_missing_license_key_notice() { + $user_dismissed_notice = get_user_meta( get_current_user_id(), 'dismissed_maxmind_license_key_notice', true ); + $filter_dismissed_notice = ! apply_filters( 'woocommerce_maxmind_geolocation_display_notices', true ); + + if ( $user_dismissed_notice || $filter_dismissed_notice ) { + self::remove_notice( 'maxmind_license_key' ); + return; + } + + include dirname( __FILE__ ) . '/views/html-notice-maxmind-license-key.php'; + } + + /** + * Notice about Redirect-Only download method. + * + * @since 4.0 + */ + public static function redirect_download_method_notice() { + if ( apply_filters( 'woocommerce_hide_redirect_method_nag', get_user_meta( get_current_user_id(), 'dismissed_redirect_download_method_notice', true ) ) ) { + self::remove_notice( 'redirect_download_method' ); + return; + } + + include dirname( __FILE__ ) . '/views/html-notice-redirect-only-download.php'; + } + + /** + * Notice about uploads directory begin unprotected. + * + * @since 4.2.0 + */ + public static function uploads_directory_is_unprotected_notice() { + if ( get_user_meta( get_current_user_id(), 'dismissed_uploads_directory_is_unprotected_notice', true ) || self::is_uploads_directory_protected() ) { + self::remove_notice( 'uploads_directory_is_unprotected' ); + return; + } + + include dirname( __FILE__ ) . '/views/html-notice-uploads-directory-is-unprotected.php'; + } + + /** + * Notice about base tables missing. + */ + public static function base_tables_missing_notice() { + $notice_dismissed = apply_filters( + 'woocommerce_hide_base_tables_missing_nag', + get_user_meta( get_current_user_id(), 'dismissed_base_tables_missing_notice', true ) + ); + if ( $notice_dismissed ) { + self::remove_notice( 'base_tables_missing' ); + } + + include dirname( __FILE__ ) . '/views/html-notice-base-table-missing.php'; + } + + /** + * Determine if the store is running SSL. + * + * @return bool Flag SSL enabled. + * @since 3.5.1 + */ + protected static function is_ssl() { + $shop_page = wc_get_page_permalink( 'shop' ); + + return ( is_ssl() && 'https' === substr( $shop_page, 0, 5 ) ); + } + + /** + * Wrapper for is_plugin_active. + * + * @param string $plugin Plugin to check. + * @return boolean + */ + protected static function is_plugin_active( $plugin ) { + if ( ! function_exists( 'is_plugin_active' ) ) { + include_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + return is_plugin_active( $plugin ); + } + + /** + * Simplify Commerce is no longer in core. + * + * @deprecated 3.6.0 No longer shown. + */ + public static function simplify_commerce_notice() { + wc_deprecated_function( 'WC_Admin_Notices::simplify_commerce_notice', '3.6.0' ); + } + + /** + * Show the Theme Check notice. + * + * @deprecated 3.3.0 No longer shown. + */ + public static function theme_check_notice() { + wc_deprecated_function( 'WC_Admin_Notices::theme_check_notice', '3.3.0' ); + } + + /** + * Check if uploads directory is protected. + * + * @since 4.2.0 + * @return bool + */ + protected static function is_uploads_directory_protected() { + $cache_key = '_woocommerce_upload_directory_status'; + $status = get_transient( $cache_key ); + + // Check for cache. + if ( false !== $status ) { + return 'protected' === $status; + } + + // Get only data from the uploads directory. + $uploads = wp_get_upload_dir(); + + // Check for the "uploads/woocommerce_uploads" directory. + $response = wp_safe_remote_get( + esc_url_raw( $uploads['baseurl'] . '/woocommerce_uploads/' ), + array( + 'redirection' => 0, + ) + ); + $response_code = intval( wp_remote_retrieve_response_code( $response ) ); + $response_content = wp_remote_retrieve_body( $response ); + + // Check if returns 200 with empty content in case can open an index.html file, + // and check for non-200 codes in case the directory is protected. + $is_protected = ( 200 === $response_code && empty( $response_content ) ) || ( 200 !== $response_code ); + set_transient( $cache_key, $is_protected ? 'protected' : 'unprotected', 1 * DAY_IN_SECONDS ); + + return $is_protected; + } +} + +WC_Admin_Notices::init(); diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-permalink-settings.php b/plugins/woocommerce/includes/admin/class-wc-admin-permalink-settings.php new file mode 100644 index 00000000000..04f4ddbd14f --- /dev/null +++ b/plugins/woocommerce/includes/admin/class-wc-admin-permalink-settings.php @@ -0,0 +1,215 @@ +settings_init(); + $this->settings_save(); + } + + /** + * Init our settings. + */ + public function settings_init() { + add_settings_section( 'woocommerce-permalink', __( 'Product permalinks', 'woocommerce' ), array( $this, 'settings' ), 'permalink' ); + + add_settings_field( + 'woocommerce_product_category_slug', + __( 'Product category base', 'woocommerce' ), + array( $this, 'product_category_slug_input' ), + 'permalink', + 'optional' + ); + add_settings_field( + 'woocommerce_product_tag_slug', + __( 'Product tag base', 'woocommerce' ), + array( $this, 'product_tag_slug_input' ), + 'permalink', + 'optional' + ); + add_settings_field( + 'woocommerce_product_attribute_slug', + __( 'Product attribute base', 'woocommerce' ), + array( $this, 'product_attribute_slug_input' ), + 'permalink', + 'optional' + ); + + $this->permalinks = wc_get_permalink_structure(); + } + + /** + * Show a slug input box. + */ + public function product_category_slug_input() { + ?> + + + + + /attribute-name/attribute/ + shop would make your product links like %sshop/sample-product/. This setting affects product URLs only, not things such as product categories.', 'woocommerce' ), esc_url( home_url( '/' ) ) ) ) ); + + $shop_page_id = wc_get_page_id( 'shop' ); + $base_slug = urldecode( ( $shop_page_id > 0 && get_post( $shop_page_id ) ) ? get_page_uri( $shop_page_id ) : _x( 'shop', 'default-slug', 'woocommerce' ) ); + $product_base = _x( 'product', 'default-slug', 'woocommerce' ); + + $structures = array( + 0 => '', + 1 => '/' . trailingslashit( $base_slug ), + 2 => '/' . trailingslashit( $base_slug ) . trailingslashit( '%product_cat%' ), + ); + ?> + + + + + + + + + + + + + + + + + + + + + + + + + 0 && get_post( $shop_page_id ) ) ? get_page_uri( $shop_page_id ) : _x( 'shop', 'default-slug', 'woocommerce' ); + + if ( $shop_page_id && stristr( trim( $permalinks['product_base'], '/' ), $shop_permalink ) ) { + $permalinks['use_verbose_page_rules'] = true; + } + + update_option( 'woocommerce_permalinks', $permalinks ); + wc_restore_locale(); + } + } +} + +return new WC_Admin_Permalink_Settings(); diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-pointers.php b/plugins/woocommerce/includes/admin/class-wc-admin-pointers.php new file mode 100644 index 00000000000..c846276edde --- /dev/null +++ b/plugins/woocommerce/includes/admin/class-wc-admin-pointers.php @@ -0,0 +1,287 @@ +id ) { + case 'product': + $this->create_product_tutorial(); + break; + } + } + + /** + * Pointers for creating a product. + */ + public function create_product_tutorial() { + if ( ! isset( $_GET['tutorial'] ) || ! current_user_can( 'manage_options' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return; + } + + // These pointers will chain - they will not be shown at once. + $pointers = array( + 'pointers' => array( + 'title' => array( + 'target' => '#title', + 'next' => 'content', + 'next_trigger' => array( + 'target' => '#title', + 'event' => 'input', + ), + 'options' => array( + 'content' => '

    ' . esc_html__( 'Product name', 'woocommerce' ) . '

    ' . + '

    ' . esc_html__( 'Give your new product a name here. This is a required field and will be what your customers will see in your store.', 'woocommerce' ) . '

    ', + 'position' => array( + 'edge' => 'top', + 'align' => 'left', + ), + ), + ), + 'content' => array( + 'target' => '#wp-content-editor-container', + 'next' => 'product-type', + 'next_trigger' => array(), + 'options' => array( + 'content' => '

    ' . esc_html__( 'Product description', 'woocommerce' ) . '

    ' . + '

    ' . esc_html__( 'This is your products main body of content. Here you should describe your product in detail.', 'woocommerce' ) . '

    ', + 'position' => array( + 'edge' => 'bottom', + 'align' => 'middle', + ), + ), + ), + 'product-type' => array( + 'target' => '#product-type', + 'next' => 'virtual', + 'next_trigger' => array( + 'target' => '#product-type', + 'event' => 'change blur click', + ), + 'options' => array( + 'content' => '

    ' . esc_html__( 'Choose product type', 'woocommerce' ) . '

    ' . + '

    ' . esc_html__( 'Choose a type for this product. Simple is suitable for most physical goods and services (we recommend setting up a simple product for now).', 'woocommerce' ) . '

    ' . + '

    ' . esc_html__( 'Variable is for more complex products such as t-shirts with multiple sizes.', 'woocommerce' ) . '

    ' . + '

    ' . esc_html__( 'Grouped products are for grouping several simple products into one.', 'woocommerce' ) . '

    ' . + '

    ' . esc_html__( 'Finally, external products are for linking off-site.', 'woocommerce' ) . '

    ', + 'position' => array( + 'edge' => 'bottom', + 'align' => 'middle', + ), + ), + ), + 'virtual' => array( + 'target' => '#_virtual', + 'next' => 'downloadable', + 'next_trigger' => array( + 'target' => '#_virtual', + 'event' => 'change', + ), + 'options' => array( + 'content' => '

    ' . esc_html__( 'Virtual products', 'woocommerce' ) . '

    ' . + '

    ' . esc_html__( 'Check the "Virtual" box if this is a non-physical item, for example a service, which does not need shipping.', 'woocommerce' ) . '

    ', + 'position' => array( + 'edge' => 'bottom', + 'align' => 'middle', + ), + ), + ), + 'downloadable' => array( + 'target' => '#_downloadable', + 'next' => 'regular_price', + 'next_trigger' => array( + 'target' => '#_downloadable', + 'event' => 'change', + ), + 'options' => array( + 'content' => '

    ' . esc_html__( 'Downloadable products', 'woocommerce' ) . '

    ' . + '

    ' . esc_html__( 'If purchasing this product gives a customer access to a downloadable file, e.g. software, check this box.', 'woocommerce' ) . '

    ', + 'position' => array( + 'edge' => 'bottom', + 'align' => 'middle', + ), + ), + ), + 'regular_price' => array( + 'target' => '#_regular_price', + 'next' => 'postexcerpt', + 'next_trigger' => array( + 'target' => '#_regular_price', + 'event' => 'input', + ), + 'options' => array( + 'content' => '

    ' . esc_html__( 'Prices', 'woocommerce' ) . '

    ' . + '

    ' . esc_html__( 'Next you need to give your product a price.', 'woocommerce' ) . '

    ', + 'position' => array( + 'edge' => 'bottom', + 'align' => 'middle', + ), + ), + ), + 'postexcerpt' => array( + 'target' => '#postexcerpt', + 'next' => 'postimagediv', + 'next_trigger' => array( + 'target' => '#postexcerpt', + 'event' => 'input', + ), + 'options' => array( + 'content' => '

    ' . esc_html__( 'Product short description', 'woocommerce' ) . '

    ' . + '

    ' . esc_html__( 'Add a quick summary for your product here. This will appear on the product page under the product name.', 'woocommerce' ) . '

    ', + 'position' => array( + 'edge' => 'bottom', + 'align' => 'middle', + ), + ), + ), + 'postimagediv' => array( + 'target' => '#postimagediv', + 'next' => 'product_tag', + 'options' => array( + 'content' => '

    ' . esc_html__( 'Product images', 'woocommerce' ) . '

    ' . + '

    ' . esc_html__( "Upload or assign an image to your product here. This image will be shown in your store's catalog.", 'woocommerce' ) . '

    ', + 'position' => array( + 'edge' => 'right', + 'align' => 'middle', + ), + ), + ), + 'product_tag' => array( + 'target' => '#tagsdiv-product_tag', + 'next' => 'product_catdiv', + 'options' => array( + 'content' => '

    ' . esc_html__( 'Product tags', 'woocommerce' ) . '

    ' . + '

    ' . esc_html__( 'You can optionally "tag" your products here. Tags are a method of labeling your products to make them easier for customers to find.', 'woocommerce' ) . '

    ', + 'position' => array( + 'edge' => 'right', + 'align' => 'middle', + ), + ), + ), + 'product_catdiv' => array( + 'target' => '#product_catdiv', + 'next' => 'submitdiv', + 'options' => array( + 'content' => '

    ' . esc_html__( 'Product categories', 'woocommerce' ) . '

    ' . + '

    ' . esc_html__( 'Optionally assign categories to your products to make them easier to browse through and find in your store.', 'woocommerce' ) . '

    ', + 'position' => array( + 'edge' => 'right', + 'align' => 'middle', + ), + ), + ), + 'submitdiv' => array( + 'target' => '#submitdiv', + 'next' => '', + 'options' => array( + 'content' => '

    ' . esc_html__( 'Publish your product!', 'woocommerce' ) . '

    ' . + '

    ' . esc_html__( 'When you are finished editing your product, hit the "Publish" button to publish your product to your store.', 'woocommerce' ) . '

    ', + 'position' => array( + 'edge' => 'right', + 'align' => 'middle', + ), + ), + ), + ), + ); + + $this->enqueue_pointers( $pointers ); + } + + /** + * Enqueue pointers and add script to page. + * + * @param array $pointers Pointers data. + */ + public function enqueue_pointers( $pointers ) { + $pointers = rawurlencode( wp_json_encode( $pointers ) ); + wp_enqueue_style( 'wp-pointer' ); + wp_enqueue_script( 'wp-pointer' ); + wc_enqueue_js( + "jQuery( function( $ ) { + var wc_pointers = JSON.parse( decodeURIComponent( '{$pointers}' ) ); + + setTimeout( init_wc_pointers, 800 ); + + function init_wc_pointers() { + $.each( wc_pointers.pointers, function( i ) { + show_wc_pointer( i ); + return false; + }); + } + + function show_wc_pointer( id ) { + var pointer = wc_pointers.pointers[ id ]; + var options = $.extend( pointer.options, { + pointerClass: 'wp-pointer wc-pointer', + close: function() { + if ( pointer.next ) { + show_wc_pointer( pointer.next ); + } + }, + buttons: function( event, t ) { + var close = '" . esc_js( __( 'Dismiss', 'woocommerce' ) ) . "', + next = '" . esc_js( __( 'Next', 'woocommerce' ) ) . "', + button = $( '' + close + '' ), + button2 = $( '' + next + '' ), + wrapper = $( '
    ' ); + + button.on( 'click.pointer', function(e) { + e.preventDefault(); + t.element.pointer('destroy'); + }); + + button2.on( 'click.pointer', function(e) { + e.preventDefault(); + t.element.pointer('close'); + }); + + wrapper.append( button ); + wrapper.append( button2 ); + + return wrapper; + }, + } ); + var this_pointer = $( pointer.target ).pointer( options ); + this_pointer.pointer( 'open' ); + + if ( pointer.next_trigger ) { + $( pointer.next_trigger.target ).on( pointer.next_trigger.event, function() { + setTimeout( function() { this_pointer.pointer( 'close' ); }, 400 ); + }); + } + } + });" + ); + } +} + +new WC_Admin_Pointers(); diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-post-types.php b/plugins/woocommerce/includes/admin/class-wc-admin-post-types.php new file mode 100644 index 00000000000..2a5ca1281f0 --- /dev/null +++ b/plugins/woocommerce/includes/admin/class-wc-admin-post-types.php @@ -0,0 +1,998 @@ +request_data(); + + $screen_id = false; + + if ( function_exists( 'get_current_screen' ) ) { + $screen = get_current_screen(); + $screen_id = isset( $screen, $screen->id ) ? $screen->id : ''; + } + + if ( ! empty( $request_data['screen'] ) ) { + $screen_id = wc_clean( wp_unslash( $request_data['screen'] ) ); + } + + switch ( $screen_id ) { + case 'edit-shop_order': + include_once __DIR__ . '/list-tables/class-wc-admin-list-table-orders.php'; + $wc_list_table = new WC_Admin_List_Table_Orders(); + break; + case 'edit-shop_coupon': + include_once __DIR__ . '/list-tables/class-wc-admin-list-table-coupons.php'; + $wc_list_table = new WC_Admin_List_Table_Coupons(); + break; + case 'edit-product': + include_once __DIR__ . '/list-tables/class-wc-admin-list-table-products.php'; + $wc_list_table = new WC_Admin_List_Table_Products(); + break; + } + + // Ensure the table handler is only loaded once. Prevents multiple loads if a plugin calls check_ajax_referer many times. + remove_action( 'current_screen', array( $this, 'setup_screen' ) ); + remove_action( 'check_ajax_referer', array( $this, 'setup_screen' ) ); + } + + /** + * Change messages when a post type is updated. + * + * @param array $messages Array of messages. + * @return array + */ + public function post_updated_messages( $messages ) { + global $post; + + $messages['product'] = array( + 0 => '', // Unused. Messages start at index 1. + /* translators: %s: Product view URL. */ + 1 => sprintf( __( 'Product updated. View Product', 'woocommerce' ), esc_url( get_permalink( $post->ID ) ) ), + 2 => __( 'Custom field updated.', 'woocommerce' ), + 3 => __( 'Custom field deleted.', 'woocommerce' ), + 4 => __( 'Product updated.', 'woocommerce' ), + 5 => __( 'Revision restored.', 'woocommerce' ), + /* translators: %s: product url */ + 6 => sprintf( __( 'Product published. View Product', 'woocommerce' ), esc_url( get_permalink( $post->ID ) ) ), + 7 => __( 'Product saved.', 'woocommerce' ), + /* translators: %s: product url */ + 8 => sprintf( __( 'Product submitted. Preview product', 'woocommerce' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ) ) ), + 9 => sprintf( + /* translators: 1: date 2: product url */ + __( 'Product scheduled for: %1$s. Preview product', 'woocommerce' ), + '' . date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $post->post_date ) ) . '', + esc_url( get_permalink( $post->ID ) ) + ), + /* translators: %s: product url */ + 10 => sprintf( __( 'Product draft updated. Preview product', 'woocommerce' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ) ) ), + ); + + $messages['shop_order'] = array( + 0 => '', // Unused. Messages start at index 1. + 1 => __( 'Order updated.', 'woocommerce' ), + 2 => __( 'Custom field updated.', 'woocommerce' ), + 3 => __( 'Custom field deleted.', 'woocommerce' ), + 4 => __( 'Order updated.', 'woocommerce' ), + 5 => __( 'Revision restored.', 'woocommerce' ), + 6 => __( 'Order updated.', 'woocommerce' ), + 7 => __( 'Order saved.', 'woocommerce' ), + 8 => __( 'Order submitted.', 'woocommerce' ), + 9 => sprintf( + /* translators: %s: date */ + __( 'Order scheduled for: %s.', 'woocommerce' ), + '' . date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $post->post_date ) ) . '' + ), + 10 => __( 'Order draft updated.', 'woocommerce' ), + 11 => __( 'Order updated and sent.', 'woocommerce' ), + ); + + $messages['shop_coupon'] = array( + 0 => '', // Unused. Messages start at index 1. + 1 => __( 'Coupon updated.', 'woocommerce' ), + 2 => __( 'Custom field updated.', 'woocommerce' ), + 3 => __( 'Custom field deleted.', 'woocommerce' ), + 4 => __( 'Coupon updated.', 'woocommerce' ), + 5 => __( 'Revision restored.', 'woocommerce' ), + 6 => __( 'Coupon updated.', 'woocommerce' ), + 7 => __( 'Coupon saved.', 'woocommerce' ), + 8 => __( 'Coupon submitted.', 'woocommerce' ), + 9 => sprintf( + /* translators: %s: date */ + __( 'Coupon scheduled for: %s.', 'woocommerce' ), + '' . date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $post->post_date ) ) . '' + ), + 10 => __( 'Coupon draft updated.', 'woocommerce' ), + ); + + return $messages; + } + + /** + * Specify custom bulk actions messages for different post types. + * + * @param array $bulk_messages Array of messages. + * @param array $bulk_counts Array of how many objects were updated. + * @return array + */ + public function bulk_post_updated_messages( $bulk_messages, $bulk_counts ) { + $bulk_messages['product'] = array( + /* translators: %s: product count */ + 'updated' => _n( '%s product updated.', '%s products updated.', $bulk_counts['updated'], 'woocommerce' ), + /* translators: %s: product count */ + 'locked' => _n( '%s product not updated, somebody is editing it.', '%s products not updated, somebody is editing them.', $bulk_counts['locked'], 'woocommerce' ), + /* translators: %s: product count */ + 'deleted' => _n( '%s product permanently deleted.', '%s products permanently deleted.', $bulk_counts['deleted'], 'woocommerce' ), + /* translators: %s: product count */ + 'trashed' => _n( '%s product moved to the Trash.', '%s products moved to the Trash.', $bulk_counts['trashed'], 'woocommerce' ), + /* translators: %s: product count */ + 'untrashed' => _n( '%s product restored from the Trash.', '%s products restored from the Trash.', $bulk_counts['untrashed'], 'woocommerce' ), + ); + + $bulk_messages['shop_order'] = array( + /* translators: %s: order count */ + 'updated' => _n( '%s order updated.', '%s orders updated.', $bulk_counts['updated'], 'woocommerce' ), + /* translators: %s: order count */ + 'locked' => _n( '%s order not updated, somebody is editing it.', '%s orders not updated, somebody is editing them.', $bulk_counts['locked'], 'woocommerce' ), + /* translators: %s: order count */ + 'deleted' => _n( '%s order permanently deleted.', '%s orders permanently deleted.', $bulk_counts['deleted'], 'woocommerce' ), + /* translators: %s: order count */ + 'trashed' => _n( '%s order moved to the Trash.', '%s orders moved to the Trash.', $bulk_counts['trashed'], 'woocommerce' ), + /* translators: %s: order count */ + 'untrashed' => _n( '%s order restored from the Trash.', '%s orders restored from the Trash.', $bulk_counts['untrashed'], 'woocommerce' ), + ); + + $bulk_messages['shop_coupon'] = array( + /* translators: %s: coupon count */ + 'updated' => _n( '%s coupon updated.', '%s coupons updated.', $bulk_counts['updated'], 'woocommerce' ), + /* translators: %s: coupon count */ + 'locked' => _n( '%s coupon not updated, somebody is editing it.', '%s coupons not updated, somebody is editing them.', $bulk_counts['locked'], 'woocommerce' ), + /* translators: %s: coupon count */ + 'deleted' => _n( '%s coupon permanently deleted.', '%s coupons permanently deleted.', $bulk_counts['deleted'], 'woocommerce' ), + /* translators: %s: coupon count */ + 'trashed' => _n( '%s coupon moved to the Trash.', '%s coupons moved to the Trash.', $bulk_counts['trashed'], 'woocommerce' ), + /* translators: %s: coupon count */ + 'untrashed' => _n( '%s coupon restored from the Trash.', '%s coupons restored from the Trash.', $bulk_counts['untrashed'], 'woocommerce' ), + ); + + return $bulk_messages; + } + + /** + * Custom bulk edit - form. + * + * @param string $column_name Column being shown. + * @param string $post_type Post type being shown. + */ + public function bulk_edit( $column_name, $post_type ) { + if ( 'price' !== $column_name || 'product' !== $post_type ) { + return; + } + + $shipping_class = get_terms( + 'product_shipping_class', + array( + 'hide_empty' => false, + ) + ); + + include WC()->plugin_path() . '/includes/admin/views/html-bulk-edit-product.php'; + } + + /** + * Custom quick edit - form. + * + * @param string $column_name Column being shown. + * @param string $post_type Post type being shown. + */ + public function quick_edit( $column_name, $post_type ) { + if ( 'price' !== $column_name || 'product' !== $post_type ) { + return; + } + + $shipping_class = get_terms( + 'product_shipping_class', + array( + 'hide_empty' => false, + ) + ); + + include WC()->plugin_path() . '/includes/admin/views/html-quick-edit-product.php'; + } + + /** + * Offers a way to hook into save post without causing an infinite loop + * when quick/bulk saving product info. + * + * @since 3.0.0 + * @param int $post_id Post ID being saved. + * @param object $post Post object being saved. + */ + public function bulk_and_quick_edit_hook( $post_id, $post ) { + remove_action( 'save_post', array( $this, 'bulk_and_quick_edit_hook' ) ); + do_action( 'woocommerce_product_bulk_and_quick_edit', $post_id, $post ); + add_action( 'save_post', array( $this, 'bulk_and_quick_edit_hook' ), 10, 2 ); + } + + /** + * Quick and bulk edit saving. + * + * @param int $post_id Post ID being saved. + * @param object $post Post object being saved. + * @return int + */ + public function bulk_and_quick_edit_save_post( $post_id, $post ) { + $request_data = $this->request_data(); + + // If this is an autosave, our form has not been submitted, so we don't want to do anything. + if ( Constants::is_true( 'DOING_AUTOSAVE' ) ) { + return $post_id; + } + + // Don't save revisions and autosaves. + if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) || 'product' !== $post->post_type || ! current_user_can( 'edit_post', $post_id ) ) { + return $post_id; + } + + // Check nonce. + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + if ( ! isset( $request_data['woocommerce_quick_edit_nonce'] ) || ! wp_verify_nonce( $request_data['woocommerce_quick_edit_nonce'], 'woocommerce_quick_edit_nonce' ) ) { + return $post_id; + } + + // Get the product and save. + $product = wc_get_product( $post ); + + if ( ! empty( $request_data['woocommerce_quick_edit'] ) ) { // WPCS: input var ok. + $this->quick_edit_save( $post_id, $product ); + } else { + $this->bulk_edit_save( $post_id, $product ); + } + + return $post_id; + } + + /** + * Quick edit. + * + * @param int $post_id Post ID being saved. + * @param WC_Product $product Product object. + */ + private function quick_edit_save( $post_id, $product ) { + $request_data = $this->request_data(); + + $data_store = $product->get_data_store(); + $old_regular_price = $product->get_regular_price(); + $old_sale_price = $product->get_sale_price(); + $input_to_props = array( + '_weight' => 'weight', + '_length' => 'length', + '_width' => 'width', + '_height' => 'height', + '_visibility' => 'catalog_visibility', + '_tax_class' => 'tax_class', + '_tax_status' => 'tax_status', + ); + + foreach ( $input_to_props as $input_var => $prop ) { + if ( isset( $request_data[ $input_var ] ) ) { + $product->{"set_{$prop}"}( wc_clean( wp_unslash( $request_data[ $input_var ] ) ) ); + } + } + + if ( isset( $request_data['_sku'] ) ) { + $sku = $product->get_sku(); + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash + $new_sku = (string) wc_clean( $request_data['_sku'] ); + + if ( $new_sku !== $sku ) { + if ( ! empty( $new_sku ) ) { + $unique_sku = wc_product_has_unique_sku( $post_id, $new_sku ); + if ( $unique_sku ) { + $product->set_sku( wc_clean( wp_unslash( $new_sku ) ) ); + } + } else { + $product->set_sku( '' ); + } + } + } + + if ( ! empty( $request_data['_shipping_class'] ) ) { + if ( '_no_shipping_class' === $request_data['_shipping_class'] ) { + $product->set_shipping_class_id( 0 ); + } else { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash + $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $request_data['_shipping_class'] ) ); + $product->set_shipping_class_id( $shipping_class_id ); + } + } + + $product->set_featured( isset( $request_data['_featured'] ) ); + + if ( $product->is_type( 'simple' ) || $product->is_type( 'external' ) ) { + + if ( isset( $request_data['_regular_price'] ) ) { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash + $new_regular_price = ( '' === $request_data['_regular_price'] ) ? '' : wc_format_decimal( $request_data['_regular_price'] ); + $product->set_regular_price( $new_regular_price ); + } else { + $new_regular_price = null; + } + if ( isset( $request_data['_sale_price'] ) ) { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash + $new_sale_price = ( '' === $request_data['_sale_price'] ) ? '' : wc_format_decimal( $request_data['_sale_price'] ); + $product->set_sale_price( $new_sale_price ); + } else { + $new_sale_price = null; + } + + // Handle price - remove dates and set to lowest. + $price_changed = false; + + if ( ! is_null( $new_regular_price ) && $new_regular_price !== $old_regular_price ) { + $price_changed = true; + } elseif ( ! is_null( $new_sale_price ) && $new_sale_price !== $old_sale_price ) { + $price_changed = true; + } + + if ( $price_changed ) { + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + } + } + + // Handle Stock Data. + // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $manage_stock = ! empty( $request_data['_manage_stock'] ) && 'grouped' !== $product->get_type() ? 'yes' : 'no'; + $backorders = ! empty( $request_data['_backorders'] ) ? wc_clean( $request_data['_backorders'] ) : 'no'; + if ( ! empty( $request_data['_stock_status'] ) ) { + $stock_status = wc_clean( $request_data['_stock_status'] ); + } else { + $stock_status = $product->is_type( 'variable' ) ? null : 'instock'; + } + // phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + $product->set_manage_stock( $manage_stock ); + + if ( 'external' !== $product->get_type() ) { + $product->set_backorders( $backorders ); + } + + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + $stock_amount = 'yes' === $manage_stock && isset( $request_data['_stock'] ) && is_numeric( wp_unslash( $request_data['_stock'] ) ) ? wc_stock_amount( wp_unslash( $request_data['_stock'] ) ) : ''; + $product->set_stock_quantity( $stock_amount ); + } + + $product = $this->maybe_update_stock_status( $product, $stock_status ); + + $product->save(); + + do_action( 'woocommerce_product_quick_edit_save', $product ); + } + + /** + * Bulk edit. + * + * @param int $post_id Post ID being saved. + * @param WC_Product $product Product object. + */ + public function bulk_edit_save( $post_id, $product ) { + // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash + + $request_data = $this->request_data(); + + $data_store = $product->get_data_store(); + + if ( ! empty( $request_data['change_weight'] ) && isset( $request_data['_weight'] ) ) { + $product->set_weight( wc_clean( wp_unslash( $request_data['_weight'] ) ) ); + } + + if ( ! empty( $request_data['change_dimensions'] ) ) { + if ( isset( $request_data['_length'] ) ) { + $product->set_length( wc_clean( wp_unslash( $request_data['_length'] ) ) ); + } + if ( isset( $request_data['_width'] ) ) { + $product->set_width( wc_clean( wp_unslash( $request_data['_width'] ) ) ); + } + if ( isset( $request_data['_height'] ) ) { + $product->set_height( wc_clean( wp_unslash( $request_data['_height'] ) ) ); + } + } + + if ( ! empty( $request_data['_tax_status'] ) ) { + $product->set_tax_status( wc_clean( $request_data['_tax_status'] ) ); + } + + if ( ! empty( $request_data['_tax_class'] ) ) { + $tax_class = wc_clean( wp_unslash( $request_data['_tax_class'] ) ); + if ( 'standard' === $tax_class ) { + $tax_class = ''; + } + $product->set_tax_class( $tax_class ); + } + + if ( ! empty( $request_data['_shipping_class'] ) ) { + if ( '_no_shipping_class' === $request_data['_shipping_class'] ) { + $product->set_shipping_class_id( 0 ); + } else { + $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $request_data['_shipping_class'] ) ); + $product->set_shipping_class_id( $shipping_class_id ); + } + } + + if ( ! empty( $request_data['_visibility'] ) ) { + $product->set_catalog_visibility( wc_clean( $request_data['_visibility'] ) ); + } + + if ( ! empty( $request_data['_featured'] ) ) { + // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $product->set_featured( wp_unslash( $request_data['_featured'] ) ); + // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + } + + if ( ! empty( $request_data['_sold_individually'] ) ) { + if ( 'yes' === $request_data['_sold_individually'] ) { + $product->set_sold_individually( 'yes' ); + } else { + $product->set_sold_individually( '' ); + } + } + + // Handle price - remove dates and set to lowest. + $change_price_product_types = apply_filters( 'woocommerce_bulk_edit_save_price_product_types', array( 'simple', 'external' ) ); + $can_product_type_change_price = false; + foreach ( $change_price_product_types as $product_type ) { + if ( $product->is_type( $product_type ) ) { + $can_product_type_change_price = true; + break; + } + } + + if ( $can_product_type_change_price ) { + $regular_price_changed = $this->set_new_price( $product, 'regular' ); + $sale_price_changed = $this->set_new_price( $product, 'sale' ); + + if ( $regular_price_changed || $sale_price_changed ) { + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + + if ( $product->get_regular_price() < $product->get_sale_price() ) { + $product->set_sale_price( '' ); + } + } + } + + // Handle Stock Data. + $was_managing_stock = $product->get_manage_stock() ? 'yes' : 'no'; + $backorders = $product->get_backorders(); + $backorders = ! empty( $request_data['_backorders'] ) ? wc_clean( $request_data['_backorders'] ) : $backorders; + + if ( ! empty( $request_data['_manage_stock'] ) ) { + $manage_stock = 'yes' === wc_clean( $request_data['_manage_stock'] ) && 'grouped' !== $product->get_type() ? 'yes' : 'no'; + } else { + $manage_stock = $was_managing_stock; + } + + $stock_amount = 'yes' === $manage_stock && ! empty( $request_data['change_stock'] ) && isset( $request_data['_stock'] ) ? wc_stock_amount( $request_data['_stock'] ) : $product->get_stock_quantity(); + + $product->set_manage_stock( $manage_stock ); + + if ( 'external' !== $product->get_type() ) { + $product->set_backorders( $backorders ); + } + + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + $change_stock = absint( $request_data['change_stock'] ); + switch ( $change_stock ) { + case 2: + wc_update_product_stock( $product, $stock_amount, 'increase', true ); + break; + case 3: + wc_update_product_stock( $product, $stock_amount, 'decrease', true ); + break; + default: + wc_update_product_stock( $product, $stock_amount, 'set', true ); + break; + } + } else { + // Reset values if WooCommerce Setting - Manage Stock status is disabled. + $product->set_stock_quantity( '' ); + $product->set_manage_stock( 'no' ); + } + + $stock_status = empty( $request_data['_stock_status'] ) ? null : wc_clean( $request_data['_stock_status'] ); + $product = $this->maybe_update_stock_status( $product, $stock_status ); + + $product->save(); + + do_action( 'woocommerce_product_bulk_edit_save', $product ); + + // phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash + } + + /** + * Disable the auto-save functionality for Orders. + */ + public function disable_autosave() { + global $post; + + if ( $post && in_array( get_post_type( $post->ID ), wc_get_order_types( 'order-meta-boxes' ), true ) ) { + wp_dequeue_script( 'autosave' ); + } + } + + /** + * Output extra data on post forms. + * + * @param WP_Post $post Current post object. + */ + public function edit_form_top( $post ) { + echo ''; + } + + /** + * Change title boxes in admin. + * + * @param string $text Text to shown. + * @param WP_Post $post Current post object. + * @return string + */ + public function enter_title_here( $text, $post ) { + switch ( $post->post_type ) { + case 'product': + $text = esc_html__( 'Product name', 'woocommerce' ); + break; + case 'shop_coupon': + $text = esc_html__( 'Coupon code', 'woocommerce' ); + break; + } + return $text; + } + + /** + * Print coupon description textarea field. + * + * @param WP_Post $post Current post object. + */ + public function edit_form_after_title( $post ) { + // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped + if ( 'shop_coupon' === $post->post_type ) { + ?> + + post_type && 'post' === $screen->base ) { + $hidden = array_merge( $hidden, array( 'postcustom' ) ); + } + + return $hidden; + } + + /** + * Output product visibility options. + */ + public function product_data_visibility() { + global $post, $thepostid, $product_object; + + if ( 'product' !== $post->post_type ) { + return; + } + + $thepostid = $post->ID; + $product_object = $thepostid ? wc_get_product( $thepostid ) : new WC_Product(); + $current_visibility = $product_object->get_catalog_visibility(); + $current_featured = wc_bool_to_string( $product_object->get_featured() ); + $visibility_options = wc_get_product_visibility_options(); + ?> +
    + + + + + + + +
    + + + + + ' . esc_html__( 'This setting determines which shop pages products will be listed on.', 'woocommerce' ) . '

    '; + + foreach ( $visibility_options as $name => $label ) { + echo '
    '; + } + + echo '

    '; + ?> +

    + + +

    +
    +
    + unique_filename( $full_filename, $ext ); + // phpcs:enable WordPress.Security.NonceVerification.Missing + } + + /** + * Change filename to append random text. + * + * @param string $full_filename Original filename with extension. + * @param string $ext Extension. + * + * @return string Modified filename. + */ + public function unique_filename( $full_filename, $ext ) { + $ideal_random_char_length = 6; // Not going with a larger length because then downloaded filename will not be pretty. + $max_filename_length = 255; // Max file name length for most file systems. + $length_to_prepend = min( $ideal_random_char_length, $max_filename_length - strlen( $full_filename ) - 1 ); + + if ( 1 > $length_to_prepend ) { + return $full_filename; + } + + $suffix = strtolower( wp_generate_password( $length_to_prepend, false, false ) ); + $filename = $full_filename; + + if ( strlen( $ext ) > 0 ) { + $filename = substr( $filename, 0, strlen( $filename ) - strlen( $ext ) ); + } + + $full_filename = str_replace( + $filename, + "$filename-$suffix", + $full_filename + ); + + return $full_filename; + } + + /** + * Run a filter when uploading a downloadable product. + */ + public function woocommerce_media_upload_downloadable_product() { + do_action( 'media_upload_file' ); + } + + /** + * Grant downloadable file access to any newly added files on any existing. + * orders for this product that have previously been granted downloadable file access. + * + * @param int $product_id product identifier. + * @param int $variation_id optional product variation identifier. + * @param array $downloadable_files newly set files. + * @deprecated 3.3.0 and moved to post-data class. + */ + public function process_product_file_download_paths( $product_id, $variation_id, $downloadable_files ) { + wc_deprecated_function( 'WC_Admin_Post_Types::process_product_file_download_paths', '3.3', '' ); + WC_Post_Data::process_product_file_download_paths( $product_id, $variation_id, $downloadable_files ); + } + + /** + * When editing the shop page, we should hide templates. + * + * @param array $page_templates Templates array. + * @param string $theme Classname. + * @param WP_Post $post The current post object. + * @return array + */ + public function hide_cpt_archive_templates( $page_templates, $theme, $post ) { + $shop_page_id = wc_get_page_id( 'shop' ); + + if ( $post && absint( $post->ID ) === $shop_page_id ) { + $page_templates = array(); + } + + return $page_templates; + } + + /** + * Show a notice above the CPT archive. + * + * @param WP_Post $post The current post object. + */ + public function show_cpt_archive_notice( $post ) { + $shop_page_id = wc_get_page_id( 'shop' ); + + if ( $post && absint( $post->ID ) === $shop_page_id ) { + echo '
    '; + /* translators: %s: URL to read more about the shop page. */ + echo '

    ' . sprintf( wp_kses_post( __( 'This is the WooCommerce shop page. The shop page is a special archive that lists your products. You can read more about this here.', 'woocommerce' ) ), 'https://docs.woocommerce.com/document/woocommerce-pages/#section-4' ) . '

    '; + echo '
    '; + } + } + + /** + * Add a post display state for special WC pages in the page list table. + * + * @param array $post_states An array of post display states. + * @param WP_Post $post The current post object. + */ + public function add_display_post_states( $post_states, $post ) { + if ( wc_get_page_id( 'shop' ) === $post->ID ) { + $post_states['wc_page_for_shop'] = __( 'Shop Page', 'woocommerce' ); + } + + if ( wc_get_page_id( 'cart' ) === $post->ID ) { + $post_states['wc_page_for_cart'] = __( 'Cart Page', 'woocommerce' ); + } + + if ( wc_get_page_id( 'checkout' ) === $post->ID ) { + $post_states['wc_page_for_checkout'] = __( 'Checkout Page', 'woocommerce' ); + } + + if ( wc_get_page_id( 'myaccount' ) === $post->ID ) { + $post_states['wc_page_for_myaccount'] = __( 'My Account Page', 'woocommerce' ); + } + + if ( wc_get_page_id( 'terms' ) === $post->ID ) { + $post_states['wc_page_for_terms'] = __( 'Terms and Conditions Page', 'woocommerce' ); + } + + return $post_states; + } + + /** + * Apply product type constraints to stock status. + * + * @param WC_Product $product The product whose stock status will be adjusted. + * @param string|null $stock_status The stock status to use for adjustment, or null if no new stock status has been supplied in the request. + * @return WC_Product The supplied product, or the synced product if it was a variable product. + */ + private function maybe_update_stock_status( $product, $stock_status ) { + if ( $product->is_type( 'external' ) ) { + // External products are always in stock. + $product->set_stock_status( 'instock' ); + } elseif ( isset( $stock_status ) ) { + if ( $product->is_type( 'variable' ) && ! $product->get_manage_stock() ) { + // Stock status is determined by children. + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! $product->get_manage_stock() ) { + $child->set_stock_status( $stock_status ); + $child->save(); + } + } + $product = WC_Product_Variable::sync( $product, false ); + } else { + $product->set_stock_status( $stock_status ); + } + } + + return $product; + } + + /** + * Set the new regular or sale price if requested. + * + * @param WC_Product $product The product to set the new price for. + * @param string $price_type 'regular' or 'sale'. + * @return bool true if a new price has been set, false otherwise. + */ + private function set_new_price( $product, $price_type ) { + // phpcs:disable WordPress.Security.NonceVerification.Recommended + + $request_data = $this->request_data(); + + if ( empty( $request_data[ "change_{$price_type}_price" ] ) || ! isset( $request_data[ "_{$price_type}_price" ] ) ) { + return false; + } + + $old_price = (float) $product->{"get_{$price_type}_price"}(); + $price_changed = false; + + $change_price = absint( $request_data[ "change_{$price_type}_price" ] ); + $raw_price = wc_clean( wp_unslash( $request_data[ "_{$price_type}_price" ] ) ); + $is_percentage = (bool) strstr( $raw_price, '%' ); + $price = wc_format_decimal( $raw_price ); + + switch ( $change_price ) { + case 1: + $new_price = $price; + break; + case 2: + if ( $is_percentage ) { + $percent = $price / 100; + $new_price = $old_price + ( $old_price * $percent ); + } else { + $new_price = $old_price + $price; + } + break; + case 3: + if ( $is_percentage ) { + $percent = $price / 100; + $new_price = max( 0, $old_price - ( $old_price * $percent ) ); + } else { + $new_price = max( 0, $old_price - $price ); + } + break; + case 4: + if ( 'sale' !== $price_type ) { + break; + } + $regular_price = $product->get_regular_price(); + if ( $is_percentage ) { + $percent = $price / 100; + $new_price = max( 0, $regular_price - ( NumberUtil::round( $regular_price * $percent, wc_get_price_decimals() ) ) ); + } else { + $new_price = max( 0, $regular_price - $price ); + } + break; + + default: + break; + } + + if ( isset( $new_price ) && $new_price !== $old_price ) { + $price_changed = true; + $new_price = NumberUtil::round( $new_price, wc_get_price_decimals() ); + $product->{"set_{$price_type}_price"}( $new_price ); + } + + return $price_changed; + + // phpcs:disable WordPress.Security.NonceVerification.Recommended + } + + /** + * Get the current request data ($_REQUEST superglobal). + * This method is added to ease unit testing. + * + * @return array The $_REQUEST superglobal. + */ + protected function request_data() { + return $_REQUEST; + } +} + +new WC_Admin_Post_Types(); diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-profile.php b/plugins/woocommerce/includes/admin/class-wc-admin-profile.php new file mode 100644 index 00000000000..be3f1fc661a --- /dev/null +++ b/plugins/woocommerce/includes/admin/class-wc-admin-profile.php @@ -0,0 +1,252 @@ + array( + 'title' => __( 'Customer billing address', 'woocommerce' ), + 'fields' => array( + 'billing_first_name' => array( + 'label' => __( 'First name', 'woocommerce' ), + 'description' => '', + ), + 'billing_last_name' => array( + 'label' => __( 'Last name', 'woocommerce' ), + 'description' => '', + ), + 'billing_company' => array( + 'label' => __( 'Company', 'woocommerce' ), + 'description' => '', + ), + 'billing_address_1' => array( + 'label' => __( 'Address line 1', 'woocommerce' ), + 'description' => '', + ), + 'billing_address_2' => array( + 'label' => __( 'Address line 2', 'woocommerce' ), + 'description' => '', + ), + 'billing_city' => array( + 'label' => __( 'City', 'woocommerce' ), + 'description' => '', + ), + 'billing_postcode' => array( + 'label' => __( 'Postcode / ZIP', 'woocommerce' ), + 'description' => '', + ), + 'billing_country' => array( + 'label' => __( 'Country / Region', 'woocommerce' ), + 'description' => '', + 'class' => 'js_field-country', + 'type' => 'select', + 'options' => array( '' => __( 'Select a country / region…', 'woocommerce' ) ) + WC()->countries->get_allowed_countries(), + ), + 'billing_state' => array( + 'label' => __( 'State / County', 'woocommerce' ), + 'description' => __( 'State / County or state code', 'woocommerce' ), + 'class' => 'js_field-state', + ), + 'billing_phone' => array( + 'label' => __( 'Phone', 'woocommerce' ), + 'description' => '', + ), + 'billing_email' => array( + 'label' => __( 'Email address', 'woocommerce' ), + 'description' => '', + ), + ), + ), + 'shipping' => array( + 'title' => __( 'Customer shipping address', 'woocommerce' ), + 'fields' => array( + 'copy_billing' => array( + 'label' => __( 'Copy from billing address', 'woocommerce' ), + 'description' => '', + 'class' => 'js_copy-billing', + 'type' => 'button', + 'text' => __( 'Copy', 'woocommerce' ), + ), + 'shipping_first_name' => array( + 'label' => __( 'First name', 'woocommerce' ), + 'description' => '', + ), + 'shipping_last_name' => array( + 'label' => __( 'Last name', 'woocommerce' ), + 'description' => '', + ), + 'shipping_company' => array( + 'label' => __( 'Company', 'woocommerce' ), + 'description' => '', + ), + 'shipping_address_1' => array( + 'label' => __( 'Address line 1', 'woocommerce' ), + 'description' => '', + ), + 'shipping_address_2' => array( + 'label' => __( 'Address line 2', 'woocommerce' ), + 'description' => '', + ), + 'shipping_city' => array( + 'label' => __( 'City', 'woocommerce' ), + 'description' => '', + ), + 'shipping_postcode' => array( + 'label' => __( 'Postcode / ZIP', 'woocommerce' ), + 'description' => '', + ), + 'shipping_country' => array( + 'label' => __( 'Country / Region', 'woocommerce' ), + 'description' => '', + 'class' => 'js_field-country', + 'type' => 'select', + 'options' => array( '' => __( 'Select a country / region…', 'woocommerce' ) ) + WC()->countries->get_allowed_countries(), + ), + 'shipping_state' => array( + 'label' => __( 'State / County', 'woocommerce' ), + 'description' => __( 'State / County or state code', 'woocommerce' ), + 'class' => 'js_field-state', + ), + 'shipping_phone' => array( + 'label' => __( 'Phone', 'woocommerce' ), + 'description' => '', + ), + ), + ), + ) + ); + return $show_fields; + } + + /** + * Show Address Fields on edit user pages. + * + * @param WP_User $user + */ + public function add_customer_meta_fields( $user ) { + if ( ! apply_filters( 'woocommerce_current_user_can_edit_customer_meta_fields', current_user_can( 'manage_woocommerce' ), $user->ID ) ) { + return; + } + + $show_fields = $this->get_customer_meta_fields(); + + foreach ( $show_fields as $fieldset_key => $fieldset ) : + ?> +

    + + $field ) : ?> + + + + + +
    + + + + + + ID, $key, true ), 1, true ); ?> /> + + + + + +

    +
    + get_customer_meta_fields(); + + foreach ( $save_fields as $fieldset ) { + + foreach ( $fieldset['fields'] as $key => $field ) { + + if ( isset( $field['type'] ) && 'checkbox' === $field['type'] ) { + update_user_meta( $user_id, $key, isset( $_POST[ $key ] ) ); + } elseif ( isset( $_POST[ $key ] ) ) { + update_user_meta( $user_id, $key, wc_clean( $_POST[ $key ] ) ); + } + } + } + } + + /** + * Get user meta for a given key, with fallbacks to core user info for pre-existing fields. + * + * @since 3.1.0 + * @param int $user_id User ID of the user being edited + * @param string $key Key for user meta field + * @return string + */ + protected function get_user_meta( $user_id, $key ) { + $value = get_user_meta( $user_id, $key, true ); + $existing_fields = array( 'billing_first_name', 'billing_last_name' ); + if ( ! $value && in_array( $key, $existing_fields ) ) { + $value = get_user_meta( $user_id, str_replace( 'billing_', '', $key ), true ); + } elseif ( ! $value && ( 'billing_email' === $key ) ) { + $user = get_userdata( $user_id ); + $value = $user->user_email; + } + + return $value; + } + } + +endif; + +return new WC_Admin_Profile(); diff --git a/includes/admin/class-wc-admin-reports.php b/plugins/woocommerce/includes/admin/class-wc-admin-reports.php similarity index 100% rename from includes/admin/class-wc-admin-reports.php rename to plugins/woocommerce/includes/admin/class-wc-admin-reports.php diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-settings.php b/plugins/woocommerce/includes/admin/class-wc-admin-settings.php new file mode 100644 index 00000000000..a6a2499c7c1 --- /dev/null +++ b/plugins/woocommerce/includes/admin/class-wc-admin-settings.php @@ -0,0 +1,950 @@ +query->init_query_vars(); + WC()->query->add_endpoints(); + + do_action( 'woocommerce_settings_saved' ); + } + + /** + * Add a message. + * + * @param string $text Message. + */ + public static function add_message( $text ) { + self::$messages[] = $text; + } + + /** + * Add an error. + * + * @param string $text Message. + */ + public static function add_error( $text ) { + self::$errors[] = $text; + } + + /** + * Output messages + errors. + */ + public static function show_messages() { + if ( count( self::$errors ) > 0 ) { + foreach ( self::$errors as $error ) { + echo '

    ' . esc_html( $error ) . '

    '; + } + } elseif ( count( self::$messages ) > 0 ) { + foreach ( self::$messages as $message ) { + echo '

    ' . esc_html( $message ) . '

    '; + } + } + } + + /** + * Settings page. + * + * Handles the display of the main woocommerce settings page in admin. + */ + public static function output() { + global $current_section, $current_tab; + + $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; + + do_action( 'woocommerce_settings_start' ); + + wp_enqueue_script( 'woocommerce_settings', WC()->plugin_url() . '/assets/js/admin/settings' . $suffix . '.js', array( 'jquery', 'wp-util', 'jquery-ui-datepicker', 'jquery-ui-sortable', 'iris', 'selectWoo' ), WC()->version, true ); + + wp_localize_script( + 'woocommerce_settings', + 'woocommerce_settings_params', + array( + 'i18n_nav_warning' => __( 'The changes you made will be lost if you navigate away from this page.', 'woocommerce' ), + 'i18n_moved_up' => __( 'Item moved up', 'woocommerce' ), + 'i18n_moved_down' => __( 'Item moved down', 'woocommerce' ), + 'i18n_no_specific_countries_selected' => __( 'Selecting no country / region to sell to prevents from completing the checkout. Continue anyway?', 'woocommerce' ), + ) + ); + + // Get tabs for the settings page. + $tabs = apply_filters( 'woocommerce_settings_tabs_array', array() ); + + include dirname( __FILE__ ) . '/views/html-admin-settings.php'; + } + + /** + * Get a setting from the settings API. + * + * @param string $option_name Option name. + * @param mixed $default Default value. + * @return mixed + */ + public static function get_option( $option_name, $default = '' ) { + if ( ! $option_name ) { + return $default; + } + + // Array value. + if ( strstr( $option_name, '[' ) ) { + + parse_str( $option_name, $option_array ); + + // Option name is first key. + $option_name = current( array_keys( $option_array ) ); + + // Get value. + $option_values = get_option( $option_name, '' ); + + $key = key( $option_array[ $option_name ] ); + + if ( isset( $option_values[ $key ] ) ) { + $option_value = $option_values[ $key ]; + } else { + $option_value = null; + } + } else { + // Single value. + $option_value = get_option( $option_name, null ); + } + + if ( is_array( $option_value ) ) { + $option_value = wp_unslash( $option_value ); + } elseif ( ! is_null( $option_value ) ) { + $option_value = stripslashes( $option_value ); + } + + return ( null === $option_value ) ? $default : $option_value; + } + + /** + * Output admin fields. + * + * Loops through the woocommerce options array and outputs each field. + * + * @param array[] $options Opens array to output. + */ + public static function output_fields( $options ) { + foreach ( $options as $value ) { + if ( ! isset( $value['type'] ) ) { + continue; + } + if ( ! isset( $value['id'] ) ) { + $value['id'] = ''; + } + if ( ! isset( $value['title'] ) ) { + $value['title'] = isset( $value['name'] ) ? $value['name'] : ''; + } + if ( ! isset( $value['class'] ) ) { + $value['class'] = ''; + } + if ( ! isset( $value['css'] ) ) { + $value['css'] = ''; + } + if ( ! isset( $value['default'] ) ) { + $value['default'] = ''; + } + if ( ! isset( $value['desc'] ) ) { + $value['desc'] = ''; + } + if ( ! isset( $value['desc_tip'] ) ) { + $value['desc_tip'] = false; + } + if ( ! isset( $value['placeholder'] ) ) { + $value['placeholder'] = ''; + } + if ( ! isset( $value['suffix'] ) ) { + $value['suffix'] = ''; + } + if ( ! isset( $value['value'] ) ) { + $value['value'] = self::get_option( $value['id'], $value['default'] ); + } + + // Custom attribute handling. + $custom_attributes = array(); + + if ( ! empty( $value['custom_attributes'] ) && is_array( $value['custom_attributes'] ) ) { + foreach ( $value['custom_attributes'] as $attribute => $attribute_value ) { + $custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $attribute_value ) . '"'; + } + } + + // Description handling. + $field_description = self::get_field_description( $value ); + $description = $field_description['description']; + $tooltip_html = $field_description['tooltip_html']; + + // Switch based on type. + switch ( $value['type'] ) { + + // Section Titles. + case 'title': + if ( ! empty( $value['title'] ) ) { + echo '

    ' . esc_html( $value['title'] ) . '

    '; + } + if ( ! empty( $value['desc'] ) ) { + echo '
    '; + echo wp_kses_post( wpautop( wptexturize( $value['desc'] ) ) ); + echo '
    '; + } + echo '' . "\n\n"; + if ( ! empty( $value['id'] ) ) { + do_action( 'woocommerce_settings_' . sanitize_title( $value['id'] ) ); + } + break; + + case 'info': + echo ''; + break; + + // Section Ends. + case 'sectionend': + if ( ! empty( $value['id'] ) ) { + do_action( 'woocommerce_settings_' . sanitize_title( $value['id'] ) . '_end' ); + } + echo '
    '; + echo wp_kses_post( wpautop( wptexturize( $value['text'] ) ) ); + echo '
    '; + if ( ! empty( $value['id'] ) ) { + do_action( 'woocommerce_settings_' . sanitize_title( $value['id'] ) . '_after' ); + } + break; + + // Standard text inputs and subtypes like 'number'. + case 'text': + case 'password': + case 'datetime': + case 'datetime-local': + case 'date': + case 'month': + case 'time': + case 'week': + case 'number': + case 'email': + case 'url': + case 'tel': + $option_value = $value['value']; + + ?> + + + + + + /> + + + + + + + + ‎ +   + + />‎ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
      + $val ) { + ?> +
    • + +
    • + +
    +
    + + + + + + +
    + +
    + + + + + +
    + + + +
    + ' . esc_html__( 'The settings of this image size have been disabled because its values are being overwritten by a filter.', 'woocommerce' ) . '

    '; + } + + ?> + + + + + + + id="-width" type="text" size="3" value="" /> × id="-height" type="text" size="3" value="" />px + + + + + + $value['id'], + 'id' => $value['id'], + 'sort_column' => 'menu_order', + 'sort_order' => 'ASC', + 'show_option_none' => ' ', + 'class' => $value['class'], + 'echo' => false, + 'selected' => absint( $value['value'] ), + 'post_status' => 'publish,private,draft', + ); + + if ( isset( $value['args'] ) ) { + $args = wp_parse_args( $value['args'], $args ); + } + + ?> + + + + + + + + + post_title, + $option_value + ); + } + ?> + + + + + + + + + + + + + + + + + countries->countries; + } + + asort( $countries ); + ?> + + + + + +
    + + + __( 'Day(s)', 'woocommerce' ), + 'weeks' => __( 'Week(s)', 'woocommerce' ), + 'months' => __( 'Month(s)', 'woocommerce' ), + 'years' => __( 'Year(s)', 'woocommerce' ), + ); + $option_value = wc_parse_relative_date_option( $value['value'] ); + ?> + + + + + + + />  + + + + ' . wp_kses_post( $description ) . '

    '; + } elseif ( $description && in_array( $value['type'], array( 'checkbox' ), true ) ) { + $description = wp_kses_post( $description ); + } elseif ( $description ) { + $description = '

    ' . wp_kses_post( $description ) . '

    '; + } + + if ( $tooltip_html && in_array( $value['type'], array( 'checkbox' ), true ) ) { + $tooltip_html = '

    ' . $tooltip_html . '

    '; + } elseif ( $tooltip_html ) { + $tooltip_html = wc_help_tip( $tooltip_html ); + } + + return array( + 'description' => $description, + 'tooltip_html' => $tooltip_html, + ); + } + + /** + * Save admin fields. + * + * Loops through the woocommerce options array and outputs each field. + * + * @param array $options Options array to output. + * @param array $data Optional. Data to use for saving. Defaults to $_POST. + * @return bool + */ + public static function save_fields( $options, $data = null ) { + if ( is_null( $data ) ) { + $data = $_POST; // WPCS: input var okay, CSRF ok. + } + if ( empty( $data ) ) { + return false; + } + + // Options to update will be stored here and saved later. + $update_options = array(); + $autoload_options = array(); + + // Loop options and get values to save. + foreach ( $options as $option ) { + if ( ! isset( $option['id'] ) || ! isset( $option['type'] ) || ( isset( $option['is_option'] ) && false === $option['is_option'] ) ) { + continue; + } + + // Get posted value. + if ( strstr( $option['id'], '[' ) ) { + parse_str( $option['id'], $option_name_array ); + $option_name = current( array_keys( $option_name_array ) ); + $setting_name = key( $option_name_array[ $option_name ] ); + $raw_value = isset( $data[ $option_name ][ $setting_name ] ) ? wp_unslash( $data[ $option_name ][ $setting_name ] ) : null; + } else { + $option_name = $option['id']; + $setting_name = ''; + $raw_value = isset( $data[ $option['id'] ] ) ? wp_unslash( $data[ $option['id'] ] ) : null; + } + + // Format the value based on option type. + switch ( $option['type'] ) { + case 'checkbox': + $value = '1' === $raw_value || 'yes' === $raw_value ? 'yes' : 'no'; + break; + case 'textarea': + $value = wp_kses_post( trim( $raw_value ) ); + break; + case 'multiselect': + case 'multi_select_countries': + $value = array_filter( array_map( 'wc_clean', (array) $raw_value ) ); + break; + case 'image_width': + $value = array(); + if ( isset( $raw_value['width'] ) ) { + $value['width'] = wc_clean( $raw_value['width'] ); + $value['height'] = wc_clean( $raw_value['height'] ); + $value['crop'] = isset( $raw_value['crop'] ) ? 1 : 0; + } else { + $value['width'] = $option['default']['width']; + $value['height'] = $option['default']['height']; + $value['crop'] = $option['default']['crop']; + } + break; + case 'select': + $allowed_values = empty( $option['options'] ) ? array() : array_map( 'strval', array_keys( $option['options'] ) ); + if ( empty( $option['default'] ) && empty( $allowed_values ) ) { + $value = null; + break; + } + $default = ( empty( $option['default'] ) ? $allowed_values[0] : $option['default'] ); + $value = in_array( $raw_value, $allowed_values, true ) ? $raw_value : $default; + break; + case 'relative_date_selector': + $value = wc_parse_relative_date_option( $raw_value ); + break; + default: + $value = wc_clean( $raw_value ); + break; + } + + /** + * Fire an action when a certain 'type' of field is being saved. + * + * @deprecated 2.4.0 - doesn't allow manipulation of values! + */ + if ( has_action( 'woocommerce_update_option_' . sanitize_title( $option['type'] ) ) ) { + wc_deprecated_function( 'The woocommerce_update_option_X action', '2.4.0', 'woocommerce_admin_settings_sanitize_option filter' ); + do_action( 'woocommerce_update_option_' . sanitize_title( $option['type'] ), $option ); + continue; + } + + /** + * Sanitize the value of an option. + * + * @since 2.4.0 + */ + $value = apply_filters( 'woocommerce_admin_settings_sanitize_option', $value, $option, $raw_value ); + + /** + * Sanitize the value of an option by option name. + * + * @since 2.4.0 + */ + $value = apply_filters( "woocommerce_admin_settings_sanitize_option_$option_name", $value, $option, $raw_value ); + + if ( is_null( $value ) ) { + continue; + } + + // Check if option is an array and handle that differently to single values. + if ( $option_name && $setting_name ) { + if ( ! isset( $update_options[ $option_name ] ) ) { + $update_options[ $option_name ] = get_option( $option_name, array() ); + } + if ( ! is_array( $update_options[ $option_name ] ) ) { + $update_options[ $option_name ] = array(); + } + $update_options[ $option_name ][ $setting_name ] = $value; + } else { + $update_options[ $option_name ] = $value; + } + + $autoload_options[ $option_name ] = isset( $option['autoload'] ) ? (bool) $option['autoload'] : true; + + /** + * Fire an action before saved. + * + * @deprecated 2.4.0 - doesn't allow manipulation of values! + */ + do_action( 'woocommerce_update_option', $option ); + } + + // Save all options in our array. + foreach ( $update_options as $name => $value ) { + update_option( $name, $value, $autoload_options[ $name ] ? 'yes' : 'no' ); + } + + return true; + } + + /** + * Checks which method we're using to serve downloads. + * + * If using force or x-sendfile, this ensures the .htaccess is in place. + */ + public static function check_download_folder_protection() { + $upload_dir = wp_get_upload_dir(); + $downloads_path = $upload_dir['basedir'] . '/woocommerce_uploads'; + $download_method = get_option( 'woocommerce_file_download_method' ); + $file_path = $downloads_path . '/.htaccess'; + $file_content = 'redirect' === $download_method ? 'Options -Indexes' : 'deny from all'; + $create = false; + + if ( wp_mkdir_p( $downloads_path ) && ! file_exists( $file_path ) ) { + $create = true; + } else { + $current_content = @file_get_contents( $file_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + + if ( $current_content !== $file_content ) { + unlink( $file_path ); + $create = true; + } + } + + if ( $create ) { + $file_handle = @fopen( $file_path, 'wb' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen + if ( $file_handle ) { + fwrite( $file_handle, $file_content ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite + fclose( $file_handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose + } + } + } + } + +endif; diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-setup-wizard.php b/plugins/woocommerce/includes/admin/class-wc-admin-setup-wizard.php new file mode 100644 index 00000000000..1b03e352a9c --- /dev/null +++ b/plugins/woocommerce/includes/admin/class-wc-admin-setup-wizard.php @@ -0,0 +1,2306 @@ +countries->get_base_country(); + // https://developers.taxjar.com/api/reference/#countries . + $tax_supported_countries = array_merge( + array( 'US', 'CA', 'AU', 'GB' ), + WC()->countries->get_european_union_countries() + ); + + return in_array( $country_code, $tax_supported_countries, true ); + } + + /** + * Should we show the MailChimp install option? + * True only if the user can install plugins. + * + * @deprecated 4.6.0 + * @return boolean + */ + protected function should_show_mailchimp() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + return current_user_can( 'install_plugins' ); + } + + /** + * Should we show the Facebook install option? + * True only if the user can install plugins, + * and up until the end date of the recommendation. + * + * @deprecated 4.6.0 + * @return boolean + */ + protected function should_show_facebook() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + return current_user_can( 'install_plugins' ); + } + + /** + * Is the WooCommerce Admin actively included in the WooCommerce core? + * Based on presence of a basic WC Admin function. + * + * @deprecated 4.6.0 + * @return boolean + */ + protected function is_wc_admin_active() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + return function_exists( 'wc_admin_url' ); + } + + /** + * Should we show the WooCommerce Admin install option? + * True only if the user can install plugins, + * and is running the correct version of WordPress. + * + * @see WC_Admin_Setup_Wizard::$wc_admin_plugin_minimum_wordpress_version + * + * @deprecated 4.6.0 + * @return boolean + */ + protected function should_show_wc_admin() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + $wordpress_minimum_met = version_compare( get_bloginfo( 'version' ), $this->wc_admin_plugin_minimum_wordpress_version, '>=' ); + return current_user_can( 'install_plugins' ) && $wordpress_minimum_met && ! $this->is_wc_admin_active(); + } + + /** + * Should we show the new WooCommerce Admin onboarding experience? + * + * @deprecated 4.6.0 + * @return boolean + */ + protected function should_show_wc_admin_onboarding() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + // As of WooCommerce 4.1, all new sites should use the latest OBW from wc-admin package. + // This filter will allow for forcing the old wizard while we migrate e2e tests. + return ! apply_filters( 'woocommerce_setup_wizard_force_legacy', false ); + } + + /** + * Should we display the 'Recommended' step? + * True if at least one of the recommendations will be displayed. + * + * @deprecated 4.6.0 + * @return boolean + */ + protected function should_show_recommended_step() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + return $this->should_show_theme() + || $this->should_show_automated_tax() + || $this->should_show_mailchimp() + || $this->should_show_facebook() + || $this->should_show_wc_admin(); + } + + /** + * Register/enqueue scripts and styles for the Setup Wizard. + * + * Hooked onto 'admin_enqueue_scripts'. + * + * @deprecated 4.6.0 + */ + public function enqueue_scripts() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + } + + /** + * Show the setup wizard. + * + * @deprecated 4.6.0 + */ + public function setup_wizard() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + if ( empty( $_GET['page'] ) || 'wc-setup' !== $_GET['page'] ) { // WPCS: CSRF ok, input var ok. + return; + } + $default_steps = array( + 'new_onboarding' => array( + 'name' => '', + 'view' => array( $this, 'wc_setup_new_onboarding' ), + 'handler' => array( $this, 'wc_setup_new_onboarding_save' ), + ), + 'store_setup' => array( + 'name' => __( 'Store setup', 'woocommerce' ), + 'view' => array( $this, 'wc_setup_store_setup' ), + 'handler' => array( $this, 'wc_setup_store_setup_save' ), + ), + 'payment' => array( + 'name' => __( 'Payment', 'woocommerce' ), + 'view' => array( $this, 'wc_setup_payment' ), + 'handler' => array( $this, 'wc_setup_payment_save' ), + ), + 'shipping' => array( + 'name' => __( 'Shipping', 'woocommerce' ), + 'view' => array( $this, 'wc_setup_shipping' ), + 'handler' => array( $this, 'wc_setup_shipping_save' ), + ), + 'recommended' => array( + 'name' => __( 'Recommended', 'woocommerce' ), + 'view' => array( $this, 'wc_setup_recommended' ), + 'handler' => array( $this, 'wc_setup_recommended_save' ), + ), + 'activate' => array( + 'name' => __( 'Activate', 'woocommerce' ), + 'view' => array( $this, 'wc_setup_activate' ), + 'handler' => array( $this, 'wc_setup_activate_save' ), + ), + 'next_steps' => array( + 'name' => __( 'Ready!', 'woocommerce' ), + 'view' => array( $this, 'wc_setup_ready' ), + 'handler' => '', + ), + ); + + // Hide the new/improved onboarding experience screen if the user is not part of the a/b test. + if ( ! $this->should_show_wc_admin_onboarding() ) { + unset( $default_steps['new_onboarding'] ); + } + + // Hide recommended step if nothing is going to be shown there. + if ( ! $this->should_show_recommended_step() ) { + unset( $default_steps['recommended'] ); + } + + // Hide shipping step if the store is selling digital products only. + if ( 'virtual' === get_option( 'woocommerce_product_type' ) ) { + unset( $default_steps['shipping'] ); + } + + // Hide activate section when the user does not have capabilities to install plugins, think multiside admins not being a super admin. + if ( ! current_user_can( 'install_plugins' ) ) { + unset( $default_steps['activate'] ); + } + + $this->steps = apply_filters( 'woocommerce_setup_wizard_steps', $default_steps ); + $this->step = isset( $_GET['step'] ) ? sanitize_key( $_GET['step'] ) : current( array_keys( $this->steps ) ); // WPCS: CSRF ok, input var ok. + + // @codingStandardsIgnoreStart + if ( ! empty( $_POST['save_step'] ) && isset( $this->steps[ $this->step ]['handler'] ) ) { + call_user_func( $this->steps[ $this->step ]['handler'], $this ); + } + // @codingStandardsIgnoreEnd + + ob_start(); + $this->setup_wizard_header(); + $this->setup_wizard_steps(); + $this->setup_wizard_content(); + $this->setup_wizard_footer(); + exit; + } + + /** + * Get the URL for the next step's screen. + * + * @param string $step slug (default: current step). + * @return string URL for next step if a next step exists. + * Admin URL if it's the last step. + * Empty string on failure. + * + * @deprecated 4.6.0 + * @since 3.0.0 + */ + public function get_next_step_link( $step = '' ) { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + if ( ! $step ) { + $step = $this->step; + } + + $keys = array_keys( $this->steps ); + if ( end( $keys ) === $step ) { + return admin_url(); + } + + $step_index = array_search( $step, $keys, true ); + if ( false === $step_index ) { + return ''; + } + + return add_query_arg( 'step', $keys[ $step_index + 1 ], remove_query_arg( 'activate_error' ) ); + } + + /** + * Setup Wizard Header. + * + * @deprecated 4.6.0 + */ + public function setup_wizard_header() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + // same as default WP from wp-admin/admin-header.php. + $wp_version_class = 'branch-' . str_replace( array( '.', ',' ), '-', floatval( get_bloginfo( 'version' ) ) ); + + set_current_screen(); + ?> + + > + + + + <?php esc_html_e( 'WooCommerce › Setup Wizard', 'woocommerce' ); ?> + + + + + + +

    <?php esc_attr_e( 'WooCommerce', 'woocommerce' ); ?>

    + step; + ?> + + + + + + + + + steps; + $selected_features = array_filter( $this->wc_setup_activate_get_feature_list() ); + + // Hide the activate step if Jetpack is already active, unless WooCommerce Services + // features are selected, or unless the Activate step was already taken. + if ( class_exists( 'Jetpack' ) && Jetpack::is_active() && empty( $selected_features ) && 'yes' !== get_transient( 'wc_setup_activated' ) ) { + unset( $output_steps['activate'] ); + } + + unset( $output_steps['new_onboarding'] ); + + ?> +
      + $step ) { + $is_completed = array_search( $this->step, array_keys( $this->steps ), true ) > array_search( $step_key, array_keys( $this->steps ), true ); + + if ( $step_key === $this->step ) { + ?> +
    1. + +
    2. + +
    3. + +
    4. + +
    + '; + if ( ! empty( $this->steps[ $this->step ]['view'] ) ) { + call_user_func( $this->steps[ $this->step ]['view'], $this ); + } + echo '
    '; + } + + /** + * Display's a prompt for users to try out the new improved WooCommerce onboarding experience in WooCommerce Admin. + * + * @deprecated 4.6.0 + */ + public function wc_setup_new_onboarding() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + ?> +
    +

    +

    <?php esc_attr_e( 'WooCommerce', 'woocommerce' ); ?>

    +

    + +
    + + +

    + +

    +
    + is_wc_admin_active() ) : ?> +

    + +
    + countries->get_base_address(); + $address_2 = WC()->countries->get_base_address_2(); + $city = WC()->countries->get_base_city(); + $state = WC()->countries->get_base_state(); + $country = WC()->countries->get_base_country(); + $postcode = WC()->countries->get_base_postcode(); + $currency = get_option( 'woocommerce_currency', 'USD' ); + $product_type = get_option( 'woocommerce_product_type', 'both' ); + $sell_in_person = get_option( 'woocommerce_sell_in_person', 'none_selected' ); + + if ( empty( $country ) ) { + $user_location = WC_Geolocation::geolocate_ip(); + $country = $user_location['country']; + $state = $user_location['state']; + } + + $locale_info = include WC()->plugin_path() . '/i18n/locale-info.php'; + $currency_by_country = wp_list_pluck( $locale_info, 'currency_code' ); + ?> +
    + + +

    + +
    + + + + + + + + + + +
    +
    + + +
    + +
    + + +
    +
    +
    + +
    + + + +
    + +
    + + +
    + +
    + + /> + +
    + + /> + + tracking_modal(); ?> + +

    + +

    +
    + + + close_http_connection(); + foreach ( $this->deferred_actions as $action ) { + $action['func']( ...$action['args'] ); + + // Clear the background installation flag if this is a plugin. + if ( + isset( $action['func'][1] ) && + 'background_installer' === $action['func'][1] && + isset( $action['args'][0] ) + ) { + delete_option( 'woocommerce_setup_background_installing_' . $action['args'][0] ); + } + } + } + + /** + * Helper method to queue the background install of a plugin. + * + * @param string $plugin_id Plugin id used for background install. + * @param array $plugin_info Plugin info array containing name and repo-slug, and optionally file if different from [repo-slug].php. + * + * @deprecated 4.6.0 + */ + protected function install_plugin( $plugin_id, $plugin_info ) { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + // Make sure we don't trigger multiple simultaneous installs. + if ( get_option( 'woocommerce_setup_background_installing_' . $plugin_id ) ) { + return; + } + + $plugin_file = isset( $plugin_info['file'] ) ? $plugin_info['file'] : $plugin_info['repo-slug'] . '.php'; + if ( is_plugin_active( $plugin_info['repo-slug'] . '/' . $plugin_file ) ) { + return; + } + + if ( empty( $this->deferred_actions ) ) { + add_action( 'shutdown', array( $this, 'run_deferred_actions' ) ); + } + + array_push( + $this->deferred_actions, + array( + 'func' => array( 'WC_Install', 'background_installer' ), + 'args' => array( $plugin_id, $plugin_info ), + ) + ); + + // Set the background installation flag for this plugin. + update_option( 'woocommerce_setup_background_installing_' . $plugin_id, true ); + } + + + /** + * Helper method to queue the background install of a theme. + * + * @param string $theme_id Theme id used for background install. + * + * @deprecated 4.6.0 + */ + protected function install_theme( $theme_id ) { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + if ( empty( $this->deferred_actions ) ) { + add_action( 'shutdown', array( $this, 'run_deferred_actions' ) ); + } + array_push( + $this->deferred_actions, + array( + 'func' => array( 'WC_Install', 'theme_background_installer' ), + 'args' => array( $theme_id ), + ) + ); + } + + /** + * Helper method to install Jetpack. + * + * @deprecated 4.6.0 + */ + protected function install_jetpack() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + $this->install_plugin( + 'jetpack', + array( + 'name' => __( 'Jetpack', 'woocommerce' ), + 'repo-slug' => 'jetpack', + ) + ); + } + + /** + * Helper method to install WooCommerce Services and its Jetpack dependency. + * + * @deprecated 4.6.0 + */ + protected function install_woocommerce_services() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + $this->install_jetpack(); + $this->install_plugin( + 'woocommerce-services', + array( + 'name' => __( 'WooCommerce Services', 'woocommerce' ), + 'repo-slug' => 'woocommerce-services', + ) + ); + } + + /** + * Retrieve info for missing WooCommerce Services and/or Jetpack plugin. + * + * @deprecated 4.6.0 + * @return array + */ + protected function get_wcs_requisite_plugins() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + $plugins = array(); + if ( ! is_plugin_active( 'woocommerce-services/woocommerce-services.php' ) && ! get_option( 'woocommerce_setup_background_installing_woocommerce-services' ) ) { + $plugins[] = array( + 'name' => __( 'WooCommerce Services', 'woocommerce' ), + 'slug' => 'woocommerce-services', + ); + } + if ( ! is_plugin_active( 'jetpack/jetpack.php' ) && ! get_option( 'woocommerce_setup_background_installing_jetpack' ) ) { + $plugins[] = array( + 'name' => __( 'Jetpack', 'woocommerce' ), + 'slug' => 'jetpack', + ); + } + return $plugins; + } + + /** + * Plugin install info message markup with heading. + * + * @deprecated 4.6.0 + */ + public function plugin_install_info() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + ?> + + + + + array( + 'name' => __( 'Flat Rate', 'woocommerce' ), + 'description' => __( 'Set a fixed price to cover shipping costs.', 'woocommerce' ), + 'settings' => array( + 'cost' => array( + 'type' => 'text', + 'default_value' => __( 'Cost', 'woocommerce' ), + 'description' => __( 'What would you like to charge for flat rate shipping?', 'woocommerce' ), + 'required' => true, + ), + ), + ), + 'free_shipping' => array( + 'name' => __( 'Free Shipping', 'woocommerce' ), + 'description' => __( "Don't charge for shipping.", 'woocommerce' ), + ), + ); + + return $shipping_methods; + } + + /** + * Render the available shipping methods for a given country code. + * + * @param string $country_code Country code. + * @param string $currency_code Currency code. + * @param string $input_prefix Input prefix. + * + * @deprecated 4.6.0 + */ + protected function shipping_method_selection_form( $country_code, $currency_code, $input_prefix ) { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + $selected = 'flat_rate'; + $shipping_methods = $this->get_wizard_shipping_methods( $country_code, $currency_code ); + ?> +
    +
    + +
    +
    + $method ) : ?> +

    + +

    + +
    +
    + +
    + $method ) : ?> + +
    + $setting ) : ?> + + + /> +

    + +

    + +
    + +
    + + + + + + + + + countries->get_base_country(); + $country_name = WC()->countries->countries[ $country_code ]; + $prefixed_country_name = WC()->countries->estimated_for_prefix( $country_code ) . $country_name; + $currency_code = get_woocommerce_currency(); + $existing_zones = WC_Shipping_Zones::get_zones(); + $intro_text = ''; + + if ( empty( $existing_zones ) ) { + $intro_text = sprintf( + /* translators: %s: country name including the 'the' prefix if needed */ + __( "We've created two Shipping Zones - for %s and for the rest of the world. Below you can set Flat Rate shipping costs for these Zones or offer Free Shipping.", 'woocommerce' ), + $prefixed_country_name + ); + } + + $is_wcs_labels_supported = $this->is_wcs_shipping_labels_supported_country( $country_code ); + $is_shipstation_supported = $this->is_shipstation_supported_country( $country_code ); + + ?> +

    + +

    + +
    + + + + + +
      +
    • +
      +

      +
      +
      +

      +
      +
    • +
    • +
      +

      +
      +
      + shipping_method_selection_form( $country_code, $currency_code, 'shipping_zones[domestic]' ); ?> +
      +
      + + + +
      +
    • +
    • +
      +

      +
      +
      + shipping_method_selection_form( $country_code, $currency_code, 'shipping_zones[intl]' ); ?> +
      +
      + + + +
      +
    • +
    • +

      + live rates from a specific carrier (e.g. UPS) you can find a variety of extensions available for WooCommerce here.', 'woocommerce' ), + array( + 'span' => array( + 'class' => array(), + 'data-tip' => array(), + ), + 'a' => array( + 'href' => array(), + 'target' => array(), + ), + ) + ), + esc_attr__( 'A live rate is the exact cost to ship an order, quoted directly from the shipping carrier.', 'woocommerce' ), + 'https://woocommerce.com/product-category/woocommerce-extensions/shipping-methods/shipping-carriers/' + ); + ?> +

      +
    • +
    + + +
    +

    + get_product_weight_selection(), + $this->get_product_dimension_selection() + ), + array( + 'span' => array( + 'class' => array(), + ), + 'select' => array( + 'id' => array(), + 'name' => array(), + 'class' => array(), + ), + 'option' => array( + 'value' => array(), + 'selected' => array(), + ), + ) + ); + ?> +

    +
    + +

    + plugin_install_info(); ?> + + +

    +
    + user_email; + + return $user_email; + } + + /** + * Array of all possible "in cart" gateways that can be offered. + * + * @deprecated 4.6.0 + * @return array + */ + protected function get_wizard_available_in_cart_payment_gateways() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + $user_email = $this->get_current_user_email(); + + $stripe_description = '

    ' . sprintf( + /* translators: %s: URL */ + __( 'Accept debit and credit cards in 135+ currencies, methods such as Alipay, and one-touch checkout with Apple Pay. Learn more.', 'woocommerce' ), + 'https://woocommerce.com/products/stripe/' + ) . '

    '; + $paypal_checkout_description = '

    ' . sprintf( + /* translators: %s: URL */ + __( 'Safe and secure payments using credit cards or your customer\'s PayPal account. Learn more.', 'woocommerce' ), + 'https://woocommerce.com/products/woocommerce-gateway-paypal-checkout/' + ) . '

    '; + $klarna_checkout_description = '

    ' . sprintf( + /* translators: %s: URL */ + __( 'Full checkout experience with pay now, pay later and slice it. No credit card numbers, no passwords, no worries. Learn more about Klarna.', 'woocommerce' ), + 'https://woocommerce.com/products/klarna-checkout/' + ) . '

    '; + $klarna_payments_description = '

    ' . sprintf( + /* translators: %s: URL */ + __( 'Choose the payment that you want, pay now, pay later or slice it. No credit card numbers, no passwords, no worries. Learn more about Klarna.', 'woocommerce' ), + 'https://woocommerce.com/products/klarna-payments/ ' + ) . '

    '; + $square_description = '

    ' . sprintf( + /* translators: %s: URL */ + __( 'Securely accept credit and debit cards with one low rate, no surprise fees (custom rates available). Sell online and in store and track sales and inventory in one place. Learn more about Square.', 'woocommerce' ), + 'https://woocommerce.com/products/square/' + ) . '

    '; + + return array( + 'stripe' => array( + 'name' => __( 'WooCommerce Stripe Gateway', 'woocommerce' ), + 'image' => WC()->plugin_url() . '/assets/images/stripe.png', + 'description' => $stripe_description, + 'class' => 'checked stripe-logo', + 'repo-slug' => 'woocommerce-gateway-stripe', + 'settings' => array( + 'create_account' => array( + 'label' => __( 'Set up Stripe for me using this email:', 'woocommerce' ), + 'type' => 'checkbox', + 'value' => 'yes', + 'default' => 'yes', + 'placeholder' => '', + 'required' => false, + 'plugins' => $this->get_wcs_requisite_plugins(), + ), + 'email' => array( + 'label' => __( 'Stripe email address:', 'woocommerce' ), + 'type' => 'email', + 'value' => $user_email, + 'placeholder' => __( 'Stripe email address', 'woocommerce' ), + 'required' => true, + ), + ), + ), + 'ppec_paypal' => array( + 'name' => __( 'WooCommerce PayPal Checkout Gateway', 'woocommerce' ), + 'image' => WC()->plugin_url() . '/assets/images/paypal.png', + 'description' => $paypal_checkout_description, + 'enabled' => false, + 'class' => 'checked paypal-logo', + 'repo-slug' => 'woocommerce-gateway-paypal-express-checkout', + 'settings' => array( + 'reroute_requests' => array( + 'label' => __( 'Set up PayPal for me using this email:', 'woocommerce' ), + 'type' => 'checkbox', + 'value' => 'yes', + 'default' => 'yes', + 'placeholder' => '', + 'required' => false, + 'plugins' => $this->get_wcs_requisite_plugins(), + ), + 'email' => array( + 'label' => __( 'Direct payments to email address:', 'woocommerce' ), + 'type' => 'email', + 'value' => $user_email, + 'placeholder' => __( 'Email address to receive payments', 'woocommerce' ), + 'required' => true, + ), + ), + ), + 'paypal' => array( + 'name' => __( 'PayPal Standard', 'woocommerce' ), + 'description' => __( 'Accept payments via PayPal using account balance or credit card.', 'woocommerce' ), + 'image' => '', + 'settings' => array( + 'email' => array( + 'label' => __( 'PayPal email address:', 'woocommerce' ), + 'type' => 'email', + 'value' => $user_email, + 'placeholder' => __( 'PayPal email address', 'woocommerce' ), + 'required' => true, + ), + ), + ), + 'klarna_checkout' => array( + 'name' => __( 'Klarna Checkout for WooCommerce', 'woocommerce' ), + 'description' => $klarna_checkout_description, + 'image' => WC()->plugin_url() . '/assets/images/klarna-black.png', + 'enabled' => true, + 'class' => 'klarna-logo', + 'repo-slug' => 'klarna-checkout-for-woocommerce', + ), + 'klarna_payments' => array( + 'name' => __( 'Klarna Payments for WooCommerce', 'woocommerce' ), + 'description' => $klarna_payments_description, + 'image' => WC()->plugin_url() . '/assets/images/klarna-black.png', + 'enabled' => true, + 'class' => 'klarna-logo', + 'repo-slug' => 'klarna-payments-for-woocommerce', + ), + 'square' => array( + 'name' => __( 'WooCommerce Square', 'woocommerce' ), + 'description' => $square_description, + 'image' => WC()->plugin_url() . '/assets/images/square-black.png', + 'class' => 'square-logo', + 'enabled' => false, + 'repo-slug' => 'woocommerce-square', + ), + 'eway' => array( + 'name' => __( 'WooCommerce eWAY Gateway', 'woocommerce' ), + 'description' => __( 'The eWAY extension for WooCommerce allows you to take credit card payments directly on your store without redirecting your customers to a third party site to make payment.', 'woocommerce' ), + 'image' => WC()->plugin_url() . '/assets/images/eway-logo.jpg', + 'enabled' => false, + 'class' => 'eway-logo', + 'repo-slug' => 'woocommerce-gateway-eway', + ), + 'payfast' => array( + 'name' => __( 'WooCommerce PayFast Gateway', 'woocommerce' ), + 'description' => __( 'The PayFast extension for WooCommerce enables you to accept payments by Credit Card and EFT via one of South Africa’s most popular payment gateways. No setup fees or monthly subscription costs.', 'woocommerce' ), + 'image' => WC()->plugin_url() . '/assets/images/payfast.png', + 'class' => 'payfast-logo', + 'enabled' => false, + 'repo-slug' => 'woocommerce-payfast-gateway', + 'file' => 'gateway-payfast.php', + ), + ); + } + + /** + * Simple array of "in cart" gateways to show in wizard. + * + * @deprecated 4.6.0 + * @return array + */ + public function get_wizard_in_cart_payment_gateways() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + $gateways = $this->get_wizard_available_in_cart_payment_gateways(); + $country = WC()->countries->get_base_country(); + $currency = get_woocommerce_currency(); + + $can_stripe = $this->is_stripe_supported_country( $country ); + $can_eway = $this->is_eway_payments_supported_country( $country ); + $can_payfast = ( 'ZA' === $country ); // South Africa. + $can_paypal = $this->is_paypal_supported_currency( $currency ); + + if ( ! current_user_can( 'install_plugins' ) ) { + return $can_paypal ? array( 'paypal' => $gateways['paypal'] ) : array(); + } + + $klarna_or_square = false; + + if ( $this->is_klarna_checkout_supported_country( $country ) ) { + $klarna_or_square = 'klarna_checkout'; + } elseif ( $this->is_klarna_payments_supported_country( $country ) ) { + $klarna_or_square = 'klarna_payments'; + } elseif ( $this->is_square_supported_country( $country ) && get_option( 'woocommerce_sell_in_person' ) ) { + $klarna_or_square = 'square'; + } + + $offered_gateways = array(); + + if ( $can_stripe ) { + $gateways['stripe']['enabled'] = true; + $gateways['stripe']['featured'] = true; + $offered_gateways += array( 'stripe' => $gateways['stripe'] ); + } elseif ( $can_paypal ) { + $gateways['ppec_paypal']['enabled'] = true; + } + + if ( $klarna_or_square ) { + if ( in_array( $klarna_or_square, array( 'klarna_checkout', 'klarna_payments' ), true ) ) { + $gateways[ $klarna_or_square ]['enabled'] = true; + $gateways[ $klarna_or_square ]['featured'] = false; + $offered_gateways += array( + $klarna_or_square => $gateways[ $klarna_or_square ], + ); + } else { + $offered_gateways += array( + $klarna_or_square => $gateways[ $klarna_or_square ], + ); + } + } + + if ( $can_paypal ) { + $offered_gateways += array( 'ppec_paypal' => $gateways['ppec_paypal'] ); + } + + if ( $can_eway ) { + $offered_gateways += array( 'eway' => $gateways['eway'] ); + } + + if ( $can_payfast ) { + $offered_gateways += array( 'payfast' => $gateways['payfast'] ); + } + + return $offered_gateways; + } + + /** + * Simple array of "manual" gateways to show in wizard. + * + * @deprecated 4.6.0 + * @return array + */ + public function get_wizard_manual_payment_gateways() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + $gateways = array( + 'cheque' => array( + 'name' => _x( 'Check payments', 'Check payment method', 'woocommerce' ), + 'description' => __( 'A simple offline gateway that lets you accept a check as method of payment.', 'woocommerce' ), + 'image' => '', + 'class' => '', + ), + 'bacs' => array( + 'name' => __( 'Bank transfer (BACS) payments', 'woocommerce' ), + 'description' => __( 'A simple offline gateway that lets you accept BACS payment.', 'woocommerce' ), + 'image' => '', + 'class' => '', + ), + 'cod' => array( + 'name' => __( 'Cash on delivery', 'woocommerce' ), + 'description' => __( 'A simple offline gateway that lets you accept cash on delivery.', 'woocommerce' ), + 'image' => '', + 'class' => '', + ), + ); + + return $gateways; + } + + /** + * Display service item in list. + * + * @param int $item_id Item ID. + * @param array $item_info Item info array. + * + * @deprecated 4.6.0 + */ + public function display_service_item( $item_id, $item_info ) { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + $item_class = 'wc-wizard-service-item'; + if ( isset( $item_info['class'] ) ) { + $item_class .= ' ' . $item_info['class']; + } + + $previously_saved_settings = get_option( 'woocommerce_' . $item_id . '_settings' ); + + // Show the user-saved state if it was previously saved. + // Otherwise, rely on the item info. + if ( is_array( $previously_saved_settings ) ) { + $should_enable_toggle = ( isset( $previously_saved_settings['enabled'] ) && 'yes' === $previously_saved_settings['enabled'] ) ? true : ( isset( $item_info['enabled'] ) && $item_info['enabled'] ); + } else { + $should_enable_toggle = isset( $item_info['enabled'] ) && $item_info['enabled']; + } + + $plugins = null; + if ( isset( $item_info['repo-slug'] ) ) { + $plugin = array( + 'slug' => $item_info['repo-slug'], + 'name' => $item_info['name'], + ); + $plugins = array( $plugin ); + } + + ?> +
  • +
    + + <?php echo esc_attr( $item_info['name'] ); ?> + +

    + +
    +
    + + + data-plugins="" + /> + +
    +
    + + +
    + $setting ) : ?> + + +
    + + + + data-plugins="" + /> + + + +
    + +
    + +
    +
  • + is_featured_service( $service ); + } + + /** + * Payment Step. + * + * @deprecated 4.6.0 + */ + public function wc_setup_payment() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + $featured_gateways = array_filter( $this->get_wizard_in_cart_payment_gateways(), array( $this, 'is_featured_service' ) ); + $in_cart_gateways = array_filter( $this->get_wizard_in_cart_payment_gateways(), array( $this, 'is_not_featured_service' ) ); + $manual_gateways = $this->get_wizard_manual_payment_gateways(); + ?> +

    +
    +

    + Additional payment methods can be installed later.', 'woocommerce' ), + array( + 'a' => array( + 'href' => array(), + 'target' => array(), + ), + ) + ), + esc_url( admin_url( 'admin.php?page=wc-addons§ion=payment-gateways' ) ) + ); + ?> +

    + + + + +
      + $gateway ) { + $this->display_service_item( $gateway_id, $gateway ); + } + ?> +
    + +
      +
    • +
      + +
      +
      + +
      +
      + + +
      +
    • + $gateway ) { + $this->display_service_item( $gateway_id, $gateway ); + } + ?> +
    +

    + plugin_install_info(); ?> + + +

    +
    + + + +

    +

    + +

    +
    + +

    + plugin_install_info(); ?> + + +

    +
    + get_next_step_link() ) ) ); + exit; + } + } + + /** + * + * @deprecated 4.6.0 + */ + protected function wc_setup_activate_get_feature_list() { + $features = array(); + + $stripe_settings = get_option( 'woocommerce_stripe_settings', false ); + $stripe_enabled = is_array( $stripe_settings ) + && isset( $stripe_settings['create_account'] ) && 'yes' === $stripe_settings['create_account'] + && isset( $stripe_settings['enabled'] ) && 'yes' === $stripe_settings['enabled']; + $ppec_settings = get_option( 'woocommerce_ppec_paypal_settings', false ); + $ppec_enabled = is_array( $ppec_settings ) + && isset( $ppec_settings['reroute_requests'] ) && 'yes' === $ppec_settings['reroute_requests'] + && isset( $ppec_settings['enabled'] ) && 'yes' === $ppec_settings['enabled']; + + $features['payment'] = $stripe_enabled || $ppec_enabled; + $features['taxes'] = (bool) get_option( 'woocommerce_setup_automated_taxes', false ); + $features['labels'] = (bool) get_option( 'woocommerce_setup_shipping_labels', false ); + + return $features; + } + + /** + * + * @deprecated 4.6.0 + */ + protected function wc_setup_activate_get_feature_list_str() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + $features = $this->wc_setup_activate_get_feature_list(); + if ( $features['payment'] && $features['taxes'] && $features['labels'] ) { + return __( 'payment setup, automated taxes and discounted shipping labels', 'woocommerce' ); + } else if ( $features['payment'] && $features['taxes'] ) { + return __( 'payment setup and automated taxes', 'woocommerce' ); + } else if ( $features['payment'] && $features['labels'] ) { + return __( 'payment setup and discounted shipping labels', 'woocommerce' ); + } else if ( $features['payment'] ) { + return __( 'payment setup', 'woocommerce' ); + } else if ( $features['taxes'] && $features['labels'] ) { + return __( 'automated taxes and discounted shipping labels', 'woocommerce' ); + } else if ( $features['taxes'] ) { + return __( 'automated taxes', 'woocommerce' ); + } else if ( $features['labels'] ) { + return __( 'discounted shipping labels', 'woocommerce' ); + } + return false; + } + + /** + * Activate step. + * + * @deprecated 4.6.0 + */ + public function wc_setup_activate() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + $this->wc_setup_activate_actions(); + + $jetpack_connected = class_exists( 'Jetpack' ) && Jetpack::is_active(); + + $has_jetpack_error = false; + if ( isset( $_GET['activate_error'] ) ) { + $has_jetpack_error = true; + + $title = __( "Sorry, we couldn't connect your store to Jetpack", 'woocommerce' ); + + $error_message = $this->get_activate_error_message( sanitize_text_field( wp_unslash( $_GET['activate_error'] ) ) ); + $description = $error_message; + } else { + $feature_list = $this->wc_setup_activate_get_feature_list_str(); + + $description = false; + + if ( $feature_list ) { + if ( ! $jetpack_connected ) { + /* translators: %s: list of features, potentially comma separated */ + $description_base = __( 'Your store is almost ready! To activate services like %s, just connect with Jetpack.', 'woocommerce' ); + } else { + $description_base = __( 'Thanks for using Jetpack! Your store is almost ready: to activate services like %s, just connect your store.', 'woocommerce' ); + } + $description = sprintf( $description_base, $feature_list ); + } + + if ( ! $jetpack_connected ) { + $title = $feature_list ? + __( 'Connect your store to Jetpack', 'woocommerce' ) : + __( 'Connect your store to Jetpack to enable extra features', 'woocommerce' ); + $button_text = __( 'Continue with Jetpack', 'woocommerce' ); + } elseif ( $feature_list ) { + $title = __( 'Connect your store to activate WooCommerce Services', 'woocommerce' ); + $button_text = __( 'Continue with WooCommerce Services', 'woocommerce' ); + } else { + wp_redirect( esc_url_raw( $this->get_next_step_link() ) ); + exit; + } + } + ?> +

    +

    + + +
    + + +
    + + + + + +

    + + + +

    + +

    + Terms of Service and to share details with WordPress.com', 'woocommerce' ) ), + 'https://wordpress.com/tos', + 'https://jetpack.com/support/what-data-does-jetpack-sync' + ); + ?> +

    +
    +

    + +

    + + +
    + +

    + +

    +
      +
    • +

      + +

      +

      + +

      +
    • +
    • +

      + +

      +

      + +

      +
    • +
    • +

      + +

      +

      + +

      +
    • +
    • +

      + +

      +

      + +

      +
    • +
    + + + __( "Sorry! We tried, but we couldn't connect Jetpack just now 😭. Please go to the Plugins tab to connect Jetpack, so that you can finish setting up your store.", 'woocommerce' ), + 'jetpack_cant_be_installed' => __( "Sorry! We tried, but we couldn't install Jetpack for you 😭. Please go to the Plugins tab to install it, and finish setting up your store.", 'woocommerce' ), + 'register_http_request_failed' => __( "Sorry! We couldn't contact Jetpack just now 😭. Please make sure that your site is visible over the internet, and that it accepts incoming and outgoing requests via curl. You can also try to connect to Jetpack again, and if you run into any more issues, please contact support.", 'woocommerce' ), + 'siteurl_private_ip_dev' => __( "Your site might be on a private network. Jetpack can only connect to public sites. Please make sure your site is visible over the internet, and then try connecting again 🙏." , 'woocommerce' ), + ); + } + + /** + * + * @deprecated 4.6.0 + */ + protected function get_activate_error_message( $code = '' ) { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + $errors = $this->get_all_activate_errors(); + return array_key_exists( $code, $errors ) ? $errors[ $code ] : $errors['default']; + } + + /** + * Activate step save. + * + * Install, activate, and launch connection flow for Jetpack. + * + * @deprecated 4.6.0 + */ + public function wc_setup_activate_save() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + } + + /** + * Final step. + * + * @deprecated 4.6.0 + */ + public function wc_setup_ready() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); + // We've made it! Don't prompt the user to run the wizard again. + WC_Admin_Notices::remove_notice( 'install', true ); + + $user_email = $this->get_current_user_email(); + $docs_url = 'https://docs.woocommerce.com/documentation/plugins/woocommerce/getting-started/?utm_source=setupwizard&utm_medium=product&utm_content=docs&utm_campaign=woocommerceplugin'; + $help_text = sprintf( + /* translators: %1$s: link to docs */ + __( 'Visit WooCommerce.com to learn more about getting started.', 'woocommerce' ), + $docs_url + ); + ?> +

    + +
    +

    +
    + +
    +
    + + +

    + execute_tool( $action ); + + $tool = $tools[ $action ]; + $tool_requires_refresh = ArrayUtil::get_value_or_default( $tool, 'requires_refresh', false ); + $tool = array( + 'id' => $action, + 'name' => $tool['name'], + 'action' => $tool['button'], + 'description' => $tool['desc'], + 'disabled' => ArrayUtil::get_value_or_default( $tool, 'disabled', false ), + ); + $tool = array_merge( $tool, $response ); + + /** + * Fires after a WooCommerce system status tool has been executed. + * + * @param array $tool Details about the tool that has been executed. + */ + do_action( 'woocommerce_system_status_tool_executed', $tool ); + } else { + $response = array( + 'success' => false, + 'message' => __( 'Tool does not exist.', 'woocommerce' ), + ); + } + + if ( $response['success'] ) { + echo '

    ' . esc_html( $response['message'] ) . '

    '; + } else { + echo '

    ' . esc_html( $response['message'] ) . '

    '; + } + } + + // Display message if settings settings have been saved. + if ( isset( $_REQUEST['settings-updated'] ) ) { // WPCS: input var ok. + echo '

    ' . esc_html__( 'Your changes have been saved.', 'woocommerce' ) . '

    '; + } + + if ( $tool_requires_refresh ) { + $tools = self::get_tools(); + } + + include_once __DIR__ . '/views/html-admin-page-status-tools.php'; + } + + /** + * Get tools. + * + * @return array of tools + */ + public static function get_tools() { + $tools_controller = new WC_REST_System_Status_Tools_Controller(); + return $tools_controller->get_tools(); + } + + /** + * Show the logs page. + */ + public static function status_logs() { + $log_handler = Constants::get_constant( 'WC_LOG_HANDLER' ); + + if ( 'WC_Log_Handler_DB' === $log_handler ) { + self::status_logs_db(); + } else { + self::status_logs_file(); + } + } + + /** + * Show the log page contents for file log handler. + */ + public static function status_logs_file() { + $logs = self::scan_log_files(); + + if ( ! empty( $_REQUEST['log_file'] ) && isset( $logs[ sanitize_title( wp_unslash( $_REQUEST['log_file'] ) ) ] ) ) { // WPCS: input var ok, CSRF ok. + $viewed_log = $logs[ sanitize_title( wp_unslash( $_REQUEST['log_file'] ) ) ]; // WPCS: input var ok, CSRF ok. + } elseif ( ! empty( $logs ) ) { + $viewed_log = current( $logs ); + } + + $handle = ! empty( $viewed_log ) ? self::get_log_file_handle( $viewed_log ) : ''; + + if ( ! empty( $_REQUEST['handle'] ) ) { // WPCS: input var ok, CSRF ok. + self::remove_log(); + } + + include_once __DIR__ . '/views/html-admin-page-status-logs.php'; + } + + /** + * Show the log page contents for db log handler. + */ + public static function status_logs_db() { + if ( ! empty( $_REQUEST['flush-logs'] ) ) { // WPCS: input var ok, CSRF ok. + self::flush_db_logs(); + } + + if ( isset( $_REQUEST['action'] ) && isset( $_REQUEST['log'] ) ) { // WPCS: input var ok, CSRF ok. + self::log_table_bulk_actions(); + } + + $log_table_list = new WC_Admin_Log_Table_List(); + $log_table_list->prepare_items(); + + include_once __DIR__ . '/views/html-admin-page-status-logs-db.php'; + } + + /** + * Retrieve metadata from a file. Based on WP Core's get_file_data function. + * + * @since 2.1.1 + * @param string $file Path to the file. + * @return string + */ + public static function get_file_version( $file ) { + + // Avoid notices if file does not exist. + if ( ! file_exists( $file ) ) { + return ''; + } + + // We don't need to write to the file, so just open for reading. + $fp = fopen( $file, 'r' ); // @codingStandardsIgnoreLine. + + // Pull only the first 8kiB of the file in. + $file_data = fread( $fp, 8192 ); // @codingStandardsIgnoreLine. + + // PHP will close file handle, but we are good citizens. + fclose( $fp ); // @codingStandardsIgnoreLine. + + // Make sure we catch CR-only line endings. + $file_data = str_replace( "\r", "\n", $file_data ); + $version = ''; + + if ( preg_match( '/^[ \t\/*#@]*' . preg_quote( '@version', '/' ) . '(.*)$/mi', $file_data, $match ) && $match[1] ) { + $version = _cleanup_header_comment( $match[1] ); + } + + return $version; + } + + /** + * Return the log file handle. + * + * @param string $filename Filename to get the handle for. + * @return string + */ + public static function get_log_file_handle( $filename ) { + return substr( $filename, 0, strlen( $filename ) > 48 ? strlen( $filename ) - 48 : strlen( $filename ) - 4 ); + } + + /** + * Scan the template files. + * + * @param string $template_path Path to the template directory. + * @return array + */ + public static function scan_template_files( $template_path ) { + $files = @scandir( $template_path ); // @codingStandardsIgnoreLine. + $result = array(); + + if ( ! empty( $files ) ) { + + foreach ( $files as $key => $value ) { + + if ( ! in_array( $value, array( '.', '..' ), true ) ) { + + if ( is_dir( $template_path . DIRECTORY_SEPARATOR . $value ) ) { + $sub_files = self::scan_template_files( $template_path . DIRECTORY_SEPARATOR . $value ); + foreach ( $sub_files as $sub_file ) { + $result[] = $value . DIRECTORY_SEPARATOR . $sub_file; + } + } else { + $result[] = $value; + } + } + } + } + return $result; + } + + /** + * Scan the log files. + * + * @return array + */ + public static function scan_log_files() { + return WC_Log_Handler_File::get_log_files(); + } + + /** + * Get latest version of a theme by slug. + * + * @param object $theme WP_Theme object. + * @return string Version number if found. + */ + public static function get_latest_theme_version( $theme ) { + include_once ABSPATH . 'wp-admin/includes/theme.php'; + + $api = themes_api( + 'theme_information', + array( + 'slug' => $theme->get_stylesheet(), + 'fields' => array( + 'sections' => false, + 'tags' => false, + ), + ) + ); + + $update_theme_version = 0; + + // Check .org for updates. + if ( is_object( $api ) && ! is_wp_error( $api ) ) { + $update_theme_version = $api->version; + } elseif ( strstr( $theme->{'Author URI'}, 'woothemes' ) ) { // Check WooThemes Theme Version. + $theme_dir = substr( strtolower( str_replace( ' ', '', $theme->Name ) ), 0, 45 ); // @codingStandardsIgnoreLine. + $theme_version_data = get_transient( $theme_dir . '_version_data' ); + + if ( false === $theme_version_data ) { + $theme_changelog = wp_safe_remote_get( 'http://dzv365zjfbd8v.cloudfront.net/changelogs/' . $theme_dir . '/changelog.txt' ); + $cl_lines = explode( "\n", wp_remote_retrieve_body( $theme_changelog ) ); + if ( ! empty( $cl_lines ) ) { + foreach ( $cl_lines as $line_num => $cl_line ) { + if ( preg_match( '/^[0-9]/', $cl_line ) ) { + $theme_date = str_replace( '.', '-', trim( substr( $cl_line, 0, strpos( $cl_line, '-' ) ) ) ); + $theme_version = preg_replace( '~[^0-9,.]~', '', stristr( $cl_line, 'version' ) ); + $theme_update = trim( str_replace( '*', '', $cl_lines[ $line_num + 1 ] ) ); + $theme_version_data = array( + 'date' => $theme_date, + 'version' => $theme_version, + 'update' => $theme_update, + 'changelog' => $theme_changelog, + ); + set_transient( $theme_dir . '_version_data', $theme_version_data, DAY_IN_SECONDS ); + break; + } + } + } + } + + if ( ! empty( $theme_version_data['version'] ) ) { + $update_theme_version = $theme_version_data['version']; + } + } + + return $update_theme_version; + } + + /** + * Remove/delete the chosen file. + */ + public static function remove_log() { + if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'remove_log' ) ) { // WPCS: input var ok, sanitization ok. + wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); + } + + if ( ! empty( $_REQUEST['handle'] ) ) { // WPCS: input var ok. + $log_handler = new WC_Log_Handler_File(); + $log_handler->remove( wp_unslash( $_REQUEST['handle'] ) ); // WPCS: input var ok, sanitization ok. + } + + wp_safe_redirect( esc_url_raw( admin_url( 'admin.php?page=wc-status&tab=logs' ) ) ); + exit(); + } + + /** + * Clear DB log table. + * + * @since 3.0.0 + */ + private static function flush_db_logs() { + if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'woocommerce-status-logs' ) ) { // WPCS: input var ok, sanitization ok. + wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); + } + + WC_Log_Handler_DB::flush(); + + wp_safe_redirect( esc_url_raw( admin_url( 'admin.php?page=wc-status&tab=logs' ) ) ); + exit(); + } + + /** + * Bulk DB log table actions. + * + * @since 3.0.0 + */ + private static function log_table_bulk_actions() { + if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'woocommerce-status-logs' ) ) { // WPCS: input var ok, sanitization ok. + wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); + } + + $log_ids = array_map( 'absint', (array) isset( $_REQUEST['log'] ) ? wp_unslash( $_REQUEST['log'] ) : array() ); // WPCS: input var ok, sanitization ok. + + if ( ( isset( $_REQUEST['action'] ) && 'delete' === $_REQUEST['action'] ) || ( isset( $_REQUEST['action2'] ) && 'delete' === $_REQUEST['action2'] ) ) { // WPCS: input var ok, sanitization ok. + WC_Log_Handler_DB::delete( $log_ids ); + wp_safe_redirect( esc_url_raw( admin_url( 'admin.php?page=wc-status&tab=logs' ) ) ); + exit(); + } + } + + /** + * Prints table info if a base table is not present. + */ + private static function output_tables_info() { + $missing_tables = WC_Install::verify_base_tables( false ); + if ( 0 === count( $missing_tables ) ) { + return; + } + ?> + +
    + + + + + + ' . $plugin_name . ''; + } + + $has_newer_version = false; + $version_string = $plugin['version']; + $network_string = ''; + if ( strstr( $plugin['url'], 'woothemes.com' ) || strstr( $plugin['url'], 'woocommerce.com' ) ) { + if ( ! empty( $plugin['version_latest'] ) && version_compare( $plugin['version_latest'], $plugin['version'], '>' ) ) { + /* translators: 1: current version. 2: latest version */ + $version_string = sprintf( __( '%1$s (update to version %2$s is available)', 'woocommerce' ), $plugin['version'], $plugin['version_latest'] ); + } + + if ( false !== $plugin['network_activated'] ) { + $network_string = ' – ' . esc_html__( 'Network enabled', 'woocommerce' ) . ''; + } + } + $untested_string = ''; + if ( array_key_exists( $plugin['plugin'], $untested_plugins ) ) { + $untested_string = ' – '; + + /* translators: %s: version */ + $untested_string .= esc_html( sprintf( __( 'Installed version not tested with active version of WooCommerce %s', 'woocommerce' ), $wc_version ) ); + + $untested_string .= ''; + } + ?> + + +   + + + + + default_cat_id = get_option( 'default_product_cat', 0 ); + + // Category/term ordering. + add_action( 'create_term', array( $this, 'create_term' ), 5, 3 ); + add_action( + 'delete_product_cat', + function() { + wc_get_container()->get( AssignDefaultCategory::class )->schedule_action(); + } + ); + + // Add form. + add_action( 'product_cat_add_form_fields', array( $this, 'add_category_fields' ) ); + add_action( 'product_cat_edit_form_fields', array( $this, 'edit_category_fields' ), 10 ); + add_action( 'created_term', array( $this, 'save_category_fields' ), 10, 3 ); + add_action( 'edit_term', array( $this, 'save_category_fields' ), 10, 3 ); + + // Add columns. + add_filter( 'manage_edit-product_cat_columns', array( $this, 'product_cat_columns' ) ); + add_filter( 'manage_product_cat_custom_column', array( $this, 'product_cat_column' ), 10, 3 ); + + // Add row actions. + add_filter( 'product_cat_row_actions', array( $this, 'product_cat_row_actions' ), 10, 2 ); + add_filter( 'admin_init', array( $this, 'handle_product_cat_row_actions' ) ); + + // Taxonomy page descriptions. + add_action( 'product_cat_pre_add_form', array( $this, 'product_cat_description' ) ); + add_action( 'after-product_cat-table', array( $this, 'product_cat_notes' ) ); + + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + if ( ! empty( $attribute_taxonomies ) ) { + foreach ( $attribute_taxonomies as $attribute ) { + add_action( 'pa_' . $attribute->attribute_name . '_pre_add_form', array( $this, 'product_attribute_description' ) ); + } + } + + // Maintain hierarchy of terms. + add_filter( 'wp_terms_checklist_args', array( $this, 'disable_checked_ontop' ) ); + + // Admin footer scripts for this product categories admin screen. + add_action( 'admin_footer', array( $this, 'scripts_at_product_cat_screen_footer' ) ); + } + + /** + * Order term when created (put in position 0). + * + * @param mixed $term_id Term ID. + * @param mixed $tt_id Term taxonomy ID. + * @param string $taxonomy Taxonomy slug. + */ + public function create_term( $term_id, $tt_id = '', $taxonomy = '' ) { + if ( 'product_cat' !== $taxonomy && ! taxonomy_is_product_attribute( $taxonomy ) ) { + return; + } + + $meta_name = taxonomy_is_product_attribute( $taxonomy ) ? 'order_' . esc_attr( $taxonomy ) : 'order'; + + update_term_meta( $term_id, $meta_name, 0 ); + } + + /** + * When a term is deleted, delete its meta. + * + * @deprecated 3.6.0 No longer needed. + * @param mixed $term_id Term ID. + */ + public function delete_term( $term_id ) { + wc_deprecated_function( 'delete_term', '3.6' ); + } + + /** + * Category thumbnail fields. + */ + public function add_category_fields() { + ?> +
    + + +
    +
    + +
    +
    + + + +
    + +
    +
    + term_id, 'display_type', true ); + $thumbnail_id = absint( get_term_meta( $term->term_id, 'thumbnail_id', true ) ); + + if ( $thumbnail_id ) { + $image = wp_get_attachment_thumb_url( $thumbnail_id ); + } else { + $image = wc_placeholder_img_src(); + } + ?> + + + + + + + + + +
    +
    + + + +
    + +
    + + + array() ) + ); + } + + /** + * Add some notes to describe the behavior of the default category. + */ + public function product_cat_notes() { + $category_id = get_option( 'default_product_cat', 0 ); + $category = get_term( $category_id, 'product_cat' ); + $category_name = ( ! $category || is_wp_error( $category ) ) ? _x( 'Uncategorized', 'Default category slug', 'woocommerce' ) : $category->name; + ?> +
    +

    +
    + ' . esc_html( $category_name ) . '' + ); + ?> +

    +
    +
    Note: Deleting a term will remove it from all products and variations to which it has been assigned. Recreating a term will not automatically assign it back to products.', 'woocommerce' ) ), + array( 'p' => array() ) + ); + } + + /** + * Thumbnail column added to category admin. + * + * @param mixed $columns Columns array. + * @return array + */ + public function product_cat_columns( $columns ) { + $new_columns = array(); + + if ( isset( $columns['cb'] ) ) { + $new_columns['cb'] = $columns['cb']; + unset( $columns['cb'] ); + } + + $new_columns['thumb'] = __( 'Image', 'woocommerce' ); + + $columns = array_merge( $new_columns, $columns ); + $columns['handle'] = ''; + + return $columns; + } + + /** + * Adjust row actions. + * + * @param array $actions Array of actions. + * @param object $term Term object. + * @return array + */ + public function product_cat_row_actions( $actions, $term ) { + $default_category_id = absint( get_option( 'default_product_cat', 0 ) ); + + if ( $default_category_id !== $term->term_id && current_user_can( 'edit_term', $term->term_id ) ) { + $actions['make_default'] = sprintf( + '%s', + wp_nonce_url( 'edit-tags.php?action=make_default&taxonomy=product_cat&post_type=product&tag_ID=' . absint( $term->term_id ), 'make_default_' . absint( $term->term_id ) ), + /* translators: %s: taxonomy term name */ + esc_attr( sprintf( __( 'Make “%s” the default category', 'woocommerce' ), $term->name ) ), + __( 'Make default', 'woocommerce' ) + ); + } + + return $actions; + } + + /** + * Handle custom row actions. + */ + public function handle_product_cat_row_actions() { + if ( isset( $_GET['action'], $_GET['tag_ID'], $_GET['_wpnonce'] ) && 'make_default' === $_GET['action'] ) { // WPCS: CSRF ok, input var ok. + $make_default_id = absint( $_GET['tag_ID'] ); // WPCS: Input var ok. + + if ( wp_verify_nonce( $_GET['_wpnonce'], 'make_default_' . $make_default_id ) && current_user_can( 'edit_term', $make_default_id ) ) { // WPCS: Sanitization ok, input var ok, CSRF ok. + update_option( 'default_product_cat', $make_default_id ); + } + } + } + + /** + * Thumbnail column value added to category admin. + * + * @param string $columns Column HTML output. + * @param string $column Column name. + * @param int $id Product ID. + * + * @return string + */ + public function product_cat_column( $columns, $column, $id ) { + if ( 'thumb' === $column ) { + // Prepend tooltip for default category. + $default_category_id = absint( get_option( 'default_product_cat', 0 ) ); + + if ( $default_category_id === $id ) { + $columns .= wc_help_tip( __( 'This is the default category and it cannot be deleted. It will be automatically assigned to products with no category.', 'woocommerce' ) ); + } + + $thumbnail_id = get_term_meta( $id, 'thumbnail_id', true ); + + if ( $thumbnail_id ) { + $image = wp_get_attachment_thumb_url( $thumbnail_id ); + } else { + $image = wc_placeholder_img_src(); + } + + // Prevent esc_url from breaking spaces in urls for image embeds. Ref: https://core.trac.wordpress.org/ticket/23605 . + $image = str_replace( ' ', '%20', $image ); + $columns .= '' . esc_attr__( 'Thumbnail', 'woocommerce' ) . ''; + } + if ( 'handle' === $column ) { + $columns .= ''; + } + return $columns; + } + + /** + * Maintain term hierarchy when editing a product. + * + * @param array $args Term checklist args. + * @return array + */ + public function disable_checked_ontop( $args ) { + if ( ! empty( $args['taxonomy'] ) && 'product_cat' === $args['taxonomy'] ) { + $args['checked_ontop'] = false; + } + return $args; + } + + /** + * Admin footer scripts for the product categories admin screen + * + * @return void + */ + public function scripts_at_product_cat_screen_footer() { + if ( ! isset( $_GET['taxonomy'] ) || 'product_cat' !== $_GET['taxonomy'] ) { // WPCS: CSRF ok, input var ok. + return; + } + // Ensure the tooltip is displayed when the image column is disabled on product categories. + wc_enqueue_js( + "(function( $ ) { + 'use strict'; + var product_cat = $( 'tr#tag-" . absint( $this->default_cat_id ) . "' ); + product_cat.find( 'th' ).empty(); + product_cat.find( 'td.thumb span' ).detach( 'span' ).appendTo( product_cat.find( 'th' ) ); + })( jQuery );" + ); + } +} + +$wc_admin_taxonomies = WC_Admin_Taxonomies::get_instance(); diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-webhooks-table-list.php b/plugins/woocommerce/includes/admin/class-wc-admin-webhooks-table-list.php new file mode 100644 index 00000000000..c9fcdaf90c2 --- /dev/null +++ b/plugins/woocommerce/includes/admin/class-wc-admin-webhooks-table-list.php @@ -0,0 +1,316 @@ + 'webhook', + 'plural' => 'webhooks', + 'ajax' => false, + ) + ); + } + + /** + * No items found text. + */ + public function no_items() { + esc_html_e( 'No webhooks found.', 'woocommerce' ); + } + + /** + * Get list columns. + * + * @return array + */ + public function get_columns() { + return array( + 'cb' => '', + 'title' => __( 'Name', 'woocommerce' ), + 'status' => __( 'Status', 'woocommerce' ), + 'topic' => __( 'Topic', 'woocommerce' ), + 'delivery_url' => __( 'Delivery URL', 'woocommerce' ), + ); + } + + /** + * Column cb. + * + * @param WC_Webhook $webhook Webhook instance. + * @return string + */ + public function column_cb( $webhook ) { + return sprintf( '', $this->_args['singular'], $webhook->get_id() ); + } + + /** + * Return title column. + * + * @param WC_Webhook $webhook Webhook instance. + * @return string + */ + public function column_title( $webhook ) { + $edit_link = admin_url( 'admin.php?page=wc-settings&tab=advanced&section=webhooks&edit-webhook=' . $webhook->get_id() ); + $output = ''; + + // Title. + $output .= '' . esc_html( $webhook->get_name() ) . ''; + + // Get actions. + $actions = array( + /* translators: %s: webhook ID. */ + 'id' => sprintf( __( 'ID: %d', 'woocommerce' ), $webhook->get_id() ), + 'edit' => '' . esc_html__( 'Edit', 'woocommerce' ) . '', + /* translators: %s: webhook name */ + 'delete' => 'get_name() ) ) . '" href="' . esc_url( + wp_nonce_url( + add_query_arg( + array( + 'delete' => $webhook->get_id(), + ), + admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks' ) + ), + 'delete-webhook' + ) + ) . '">' . esc_html__( 'Delete permanently', 'woocommerce' ) . '', + ); + + $actions = apply_filters( 'webhook_row_actions', $actions, $webhook ); + $row_actions = array(); + + foreach ( $actions as $action => $link ) { + $row_actions[] = '' . $link . ''; + } + + $output .= '
    ' . implode( ' | ', $row_actions ) . '
    '; + + return $output; + } + + /** + * Return status column. + * + * @param WC_Webhook $webhook Webhook instance. + * @return string + */ + public function column_status( $webhook ) { + return $webhook->get_i18n_status(); + } + + /** + * Return topic column. + * + * @param WC_Webhook $webhook Webhook instance. + * @return string + */ + public function column_topic( $webhook ) { + return $webhook->get_topic(); + } + + /** + * Return delivery URL column. + * + * @param WC_Webhook $webhook Webhook instance. + * @return string + */ + public function column_delivery_url( $webhook ) { + return $webhook->get_delivery_url(); + } + + /** + * Get the status label for webhooks. + * + * @param string $status_name Status name. + * @param int $amount Amount of webhooks. + * @return array + */ + private function get_status_label( $status_name, $amount ) { + $statuses = wc_get_webhook_statuses(); + + if ( isset( $statuses[ $status_name ] ) ) { + return array( + 'singular' => sprintf( '%s (%s)', esc_html( $statuses[ $status_name ] ), $amount ), + 'plural' => sprintf( '%s (%s)', esc_html( $statuses[ $status_name ] ), $amount ), + 'context' => '', + 'domain' => 'woocommerce', + ); + } + + return array( + 'singular' => sprintf( '%s (%s)', esc_html( $status_name ), $amount ), + 'plural' => sprintf( '%s (%s)', esc_html( $status_name ), $amount ), + 'context' => '', + 'domain' => 'woocommerce', + ); + } + + /** + * Table list views. + * + * @return array + */ + protected function get_views() { + $status_links = array(); + $data_store = WC_Data_Store::load( 'webhook' ); + $num_webhooks = $data_store->get_count_webhooks_by_status(); + $total_webhooks = array_sum( (array) $num_webhooks ); + $statuses = array_keys( wc_get_webhook_statuses() ); + $class = empty( $_REQUEST['status'] ) ? ' class="current"' : ''; // WPCS: input var okay. CSRF ok. + + /* translators: %s: count */ + $status_links['all'] = "" . sprintf( _nx( 'All (%s)', 'All (%s)', $total_webhooks, 'posts', 'woocommerce' ), number_format_i18n( $total_webhooks ) ) . ''; + + foreach ( $statuses as $status_name ) { + $class = ''; + + if ( empty( $num_webhooks[ $status_name ] ) ) { + continue; + } + + if ( isset( $_REQUEST['status'] ) && sanitize_key( wp_unslash( $_REQUEST['status'] ) ) === $status_name ) { // WPCS: input var okay, CSRF ok. + $class = ' class="current"'; + } + + $label = $this->get_status_label( $status_name, $num_webhooks[ $status_name ] ); + + $status_links[ $status_name ] = "" . sprintf( translate_nooped_plural( $label, $num_webhooks[ $status_name ] ), number_format_i18n( $num_webhooks[ $status_name ] ) ) . ''; + } + + return $status_links; + } + + /** + * Get bulk actions. + * + * @return array + */ + protected function get_bulk_actions() { + return array( + 'delete' => __( 'Delete permanently', 'woocommerce' ), + ); + } + + /** + * Process bulk actions. + */ + public function process_bulk_action() { + $action = $this->current_action(); + $webhooks = isset( $_REQUEST['webhook'] ) ? array_map( 'absint', (array) $_REQUEST['webhook'] ) : array(); // WPCS: input var okay, CSRF ok. + + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_die( esc_html__( 'You do not have permission to edit Webhooks', 'woocommerce' ) ); + } + + if ( 'delete' === $action ) { + WC_Admin_Webhooks::bulk_delete( $webhooks ); + } + } + + /** + * Generate the table navigation above or below the table. + * Included to remove extra nonce input. + * + * @param string $which The location of the extra table nav markup: 'top' or 'bottom'. + */ + protected function display_tablenav( $which ) { + echo '
    '; + + if ( $this->has_items() ) { + echo '
    '; + $this->bulk_actions( $which ); + echo '
    '; + } + + $this->extra_tablenav( $which ); + $this->pagination( $which ); + echo '
    '; + echo '
    '; + } + + /** + * Search box. + * + * @param string $text Button text. + * @param string $input_id Input ID. + */ + public function search_box( $text, $input_id ) { + if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) { // WPCS: input var okay, CSRF ok. + return; + } + + $input_id = $input_id . '-search-input'; + $search_query = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : ''; // WPCS: input var okay, CSRF ok. + + echo ''; + } + + /** + * Prepare table list items. + */ + public function prepare_items() { + $per_page = $this->get_items_per_page( 'woocommerce_webhooks_per_page' ); + $current_page = $this->get_pagenum(); + + // Query args. + $args = array( + 'limit' => $per_page, + 'offset' => $per_page * ( $current_page - 1 ), + ); + + // Handle the status query. + if ( ! empty( $_REQUEST['status'] ) ) { // WPCS: input var okay, CSRF ok. + $args['status'] = sanitize_key( wp_unslash( $_REQUEST['status'] ) ); // WPCS: input var okay, CSRF ok. + } + + if ( ! empty( $_REQUEST['s'] ) ) { // WPCS: input var okay, CSRF ok. + $args['search'] = sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ); // WPCS: input var okay, CSRF ok. + } + + $args['paginate'] = true; + + // Get the webhooks. + $data_store = WC_Data_Store::load( 'webhook' ); + $webhooks = $data_store->search_webhooks( $args ); + $this->items = array_map( 'wc_get_webhook', $webhooks->webhooks ); + + // Set the pagination. + $this->set_pagination_args( + array( + 'total_items' => $webhooks->total, + 'per_page' => $per_page, + 'total_pages' => $webhooks->max_num_pages, + ) + ); + } +} diff --git a/includes/admin/class-wc-admin-webhooks.php b/plugins/woocommerce/includes/admin/class-wc-admin-webhooks.php similarity index 100% rename from includes/admin/class-wc-admin-webhooks.php rename to plugins/woocommerce/includes/admin/class-wc-admin-webhooks.php diff --git a/plugins/woocommerce/includes/admin/class-wc-admin.php b/plugins/woocommerce/includes/admin/class-wc-admin.php new file mode 100644 index 00000000000..a8e38be1671 --- /dev/null +++ b/plugins/woocommerce/includes/admin/class-wc-admin.php @@ -0,0 +1,317 @@ +id ) { + case 'dashboard': + case 'dashboard-network': + include __DIR__ . '/class-wc-admin-dashboard-setup.php'; + include __DIR__ . '/class-wc-admin-dashboard.php'; + break; + case 'options-permalink': + include __DIR__ . '/class-wc-admin-permalink-settings.php'; + break; + case 'plugins': + include __DIR__ . '/plugin-updates/class-wc-plugins-screen-updates.php'; + break; + case 'update-core': + include __DIR__ . '/plugin-updates/class-wc-updates-screen-updates.php'; + break; + case 'users': + case 'user': + case 'profile': + case 'user-edit': + include __DIR__ . '/class-wc-admin-profile.php'; + break; + } + } + + /** + * Handle redirects to setup/welcome page after install and updates. + * + * The user must have access rights, and we must ignore the network/bulk plugin updaters. + */ + public function admin_redirects() { + // Don't run this fn from Action Scheduler requests, as it would clear _wc_activation_redirect transient. + // That means OBW would never be shown. + if ( wc_is_running_from_async_action_scheduler() ) { + return; + } + + // phpcs:disable WordPress.Security.NonceVerification.Recommended + // Nonced plugin install redirects. + if ( ! empty( $_GET['wc-install-plugin-redirect'] ) ) { + $plugin_slug = wc_clean( wp_unslash( $_GET['wc-install-plugin-redirect'] ) ); + + if ( current_user_can( 'install_plugins' ) && in_array( $plugin_slug, array( 'woocommerce-gateway-stripe' ), true ) ) { + $nonce = wp_create_nonce( 'install-plugin_' . $plugin_slug ); + $url = self_admin_url( 'update.php?action=install-plugin&plugin=' . $plugin_slug . '&_wpnonce=' . $nonce ); + } else { + $url = admin_url( 'plugin-install.php?tab=search&type=term&s=' . $plugin_slug ); + } + + wp_safe_redirect( $url ); + exit; + } + + // phpcs:enable WordPress.Security.NonceVerification.Recommended + } + + /** + * Prevent any user who cannot 'edit_posts' (subscribers, customers etc) from accessing admin. + */ + public function prevent_admin_access() { + $prevent_access = false; + + if ( apply_filters( 'woocommerce_disable_admin_bar', true ) && ! wp_doing_ajax() && isset( $_SERVER['SCRIPT_FILENAME'] ) && basename( sanitize_text_field( wp_unslash( $_SERVER['SCRIPT_FILENAME'] ) ) ) !== 'admin-post.php' ) { + $has_cap = false; + $access_caps = array( 'edit_posts', 'manage_woocommerce', 'view_admin_dashboard' ); + + foreach ( $access_caps as $access_cap ) { + if ( current_user_can( $access_cap ) ) { + $has_cap = true; + break; + } + } + + if ( ! $has_cap ) { + $prevent_access = true; + } + } + + if ( apply_filters( 'woocommerce_prevent_admin_access', $prevent_access ) ) { + wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) ); + exit; + } + } + + /** + * Preview email template. + */ + public function preview_emails() { + + if ( isset( $_GET['preview_woocommerce_mail'] ) ) { + if ( ! ( isset( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'preview-mail' ) ) ) { + die( 'Security check' ); + } + + // load the mailer class. + $mailer = WC()->mailer(); + + // get the preview email subject. + $email_heading = __( 'HTML email template', 'woocommerce' ); + + // get the preview email content. + ob_start(); + include __DIR__ . '/views/html-email-template-preview.php'; + $message = ob_get_clean(); + + // create a new email. + $email = new WC_Email(); + + // wrap the content with the email template and then add styles. + $message = apply_filters( 'woocommerce_mail_content', $email->style_inline( $mailer->wrap_message( $email_heading, $message ) ) ); + + // print the preview email. + // phpcs:ignore WordPress.Security.EscapeOutput + echo $message; + // phpcs:enable + exit; + } + } + + /** + * Change the admin footer text on WooCommerce admin pages. + * + * @since 2.3 + * @param string $footer_text text to be rendered in the footer. + * @return string + */ + public function admin_footer_text( $footer_text ) { + if ( ! current_user_can( 'manage_woocommerce' ) || ! function_exists( 'wc_get_screen_ids' ) ) { + return $footer_text; + } + $current_screen = get_current_screen(); + $wc_pages = wc_get_screen_ids(); + + // Set only WC pages. + $wc_pages = array_diff( $wc_pages, array( 'profile', 'user-edit' ) ); + + // Check to make sure we're on a WooCommerce admin page. + if ( isset( $current_screen->id ) && apply_filters( 'woocommerce_display_admin_footer_text', in_array( $current_screen->id, $wc_pages, true ) ) ) { + // Change the footer text. + if ( ! get_option( 'woocommerce_admin_footer_text_rated' ) ) { + $footer_text = sprintf( + /* translators: 1: WooCommerce 2:: five stars */ + __( 'If you like %1$s please leave us a %2$s rating. A huge thanks in advance!', 'woocommerce' ), + sprintf( '%s', esc_html__( 'WooCommerce', 'woocommerce' ) ), + '★★★★★' + ); + wc_enqueue_js( + "jQuery( 'a.wc-rating-link' ).on( 'click', function() { + jQuery.post( '" . WC()->ajax_url() . "', { action: 'woocommerce_rated' } ); + jQuery( this ).parent().text( jQuery( this ).data( 'rated' ) ); + });" + ); + } else { + $footer_text = __( 'Thank you for selling with WooCommerce.', 'woocommerce' ); + } + } + + return $footer_text; + } + + /** + * Check on a Jetpack install queued by the Setup Wizard. + * + * See: WC_Admin_Setup_Wizard::install_jetpack() + */ + public function setup_wizard_check_jetpack() { + $jetpack_active = class_exists( 'Jetpack' ); + + wp_send_json_success( + array( + 'is_active' => $jetpack_active ? 'yes' : 'no', + ) + ); + } + + /** + * Disable WXR export of scheduled action posts. + * + * @since 3.6.2 + * + * @param array $args Scheduled action post type registration args. + * + * @return array + */ + public function disable_webhook_post_export( $args ) { + $args['can_export'] = false; + return $args; + } + + /** + * Include admin classes. + * + * @since 4.2.0 + * @param string $classes Body classes string. + * @return string + */ + public function include_admin_body_class( $classes ) { + if ( in_array( array( 'wc-wp-version-gte-53', 'wc-wp-version-gte-55' ), explode( ' ', $classes ), true ) ) { + return $classes; + } + + $raw_version = get_bloginfo( 'version' ); + $version_parts = explode( '-', $raw_version ); + $version = count( $version_parts ) > 1 ? $version_parts[0] : $raw_version; + + // Add WP 5.3+ compatibility class. + if ( $raw_version && version_compare( $version, '5.3', '>=' ) ) { + $classes .= ' wc-wp-version-gte-53'; + } + + // Add WP 5.5+ compatibility class. + if ( $raw_version && version_compare( $version, '5.5', '>=' ) ) { + $classes .= ' wc-wp-version-gte-55'; + } + + return $classes; + } +} + +return new WC_Admin(); diff --git a/includes/admin/helper/class-wc-helper-api.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-api.php similarity index 100% rename from includes/admin/helper/class-wc-helper-api.php rename to plugins/woocommerce/includes/admin/helper/class-wc-helper-api.php diff --git a/includes/admin/helper/class-wc-helper-compat.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-compat.php similarity index 100% rename from includes/admin/helper/class-wc-helper-compat.php rename to plugins/woocommerce/includes/admin/helper/class-wc-helper-compat.php diff --git a/includes/admin/helper/class-wc-helper-options.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-options.php similarity index 100% rename from includes/admin/helper/class-wc-helper-options.php rename to plugins/woocommerce/includes/admin/helper/class-wc-helper-options.php diff --git a/includes/admin/helper/class-wc-helper-plugin-info.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-plugin-info.php similarity index 100% rename from includes/admin/helper/class-wc-helper-plugin-info.php rename to plugins/woocommerce/includes/admin/helper/class-wc-helper-plugin-info.php diff --git a/includes/admin/helper/class-wc-helper-updater.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php similarity index 100% rename from includes/admin/helper/class-wc-helper-updater.php rename to plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper.php new file mode 100644 index 00000000000..ae9afbf0a6f --- /dev/null +++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper.php @@ -0,0 +1,1641 @@ + 'wc-addons', + 'section' => 'helper', + 'wc-helper-connect' => 1, + 'wc-helper-nonce' => wp_create_nonce( 'connect' ), + ), + admin_url( 'admin.php' ) + ); + + include self::get_view_filename( 'html-oauth-start.php' ); + return; + } + $disconnect_url = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'wc-helper-disconnect' => 1, + 'wc-helper-nonce' => wp_create_nonce( 'disconnect' ), + ), + admin_url( 'admin.php' ) + ); + + $current_filter = self::get_current_filter(); + $refresh_url = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'filter' => $current_filter, + 'wc-helper-refresh' => 1, + 'wc-helper-nonce' => wp_create_nonce( 'refresh' ), + ), + admin_url( 'admin.php' ) + ); + + // Installed plugins and themes, with or without an active subscription. + $woo_plugins = self::get_local_woo_plugins(); + $woo_themes = self::get_local_woo_themes(); + + $site_id = absint( $auth['site_id'] ); + $subscriptions = self::get_subscriptions(); + $updates = WC_Helper_Updater::get_update_data(); + $subscriptions_product_ids = wp_list_pluck( $subscriptions, 'product_id' ); + + foreach ( $subscriptions as &$subscription ) { + $subscription['active'] = in_array( $site_id, $subscription['connections'] ); + + $subscription['activate_url'] = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'filter' => $current_filter, + 'wc-helper-activate' => 1, + 'wc-helper-product-key' => $subscription['product_key'], + 'wc-helper-product-id' => $subscription['product_id'], + 'wc-helper-nonce' => wp_create_nonce( 'activate:' . $subscription['product_key'] ), + ), + admin_url( 'admin.php' ) + ); + + $subscription['deactivate_url'] = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'filter' => $current_filter, + 'wc-helper-deactivate' => 1, + 'wc-helper-product-key' => $subscription['product_key'], + 'wc-helper-product-id' => $subscription['product_id'], + 'wc-helper-nonce' => wp_create_nonce( 'deactivate:' . $subscription['product_key'] ), + ), + admin_url( 'admin.php' ) + ); + + $subscription['local'] = array( + 'installed' => false, + 'active' => false, + 'version' => null, + ); + + $subscription['update_url'] = admin_url( 'update-core.php' ); + + $local = wp_list_filter( array_merge( $woo_plugins, $woo_themes ), array( '_product_id' => $subscription['product_id'] ) ); + + if ( ! empty( $local ) ) { + $local = array_shift( $local ); + $subscription['local']['installed'] = true; + $subscription['local']['version'] = $local['Version']; + + if ( 'plugin' == $local['_type'] ) { + if ( is_plugin_active( $local['_filename'] ) ) { + $subscription['local']['active'] = true; + } elseif ( is_multisite() && is_plugin_active_for_network( $local['_filename'] ) ) { + $subscription['local']['active'] = true; + } + + // A magic update_url. + $subscription['update_url'] = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' ) . $local['_filename'], 'upgrade-plugin_' . $local['_filename'] ); + + } elseif ( 'theme' == $local['_type'] ) { + if ( in_array( $local['_stylesheet'], array( get_stylesheet(), get_template() ) ) ) { + $subscription['local']['active'] = true; + } + + // Another magic update_url. + $subscription['update_url'] = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-theme&theme=' . $local['_stylesheet'] ), 'upgrade-theme_' . $local['_stylesheet'] ); + } + } + + $subscription['has_update'] = false; + if ( $subscription['local']['installed'] && ! empty( $updates[ $subscription['product_id'] ] ) ) { + $subscription['has_update'] = version_compare( $updates[ $subscription['product_id'] ]['version'], $subscription['local']['version'], '>' ); + } + + $subscription['download_primary'] = true; + $subscription['download_url'] = 'https://woocommerce.com/my-account/downloads/'; + if ( ! $subscription['local']['installed'] && ! empty( $updates[ $subscription['product_id'] ] ) ) { + $subscription['download_url'] = $updates[ $subscription['product_id'] ]['package']; + } + + $subscription['actions'] = array(); + + if ( $subscription['has_update'] && ! $subscription['expired'] ) { + $action = array( + /* translators: %s: version number */ + 'message' => sprintf( __( 'Version %s is available.', 'woocommerce' ), esc_html( $updates[ $subscription['product_id'] ]['version'] ) ), + 'button_label' => __( 'Update', 'woocommerce' ), + 'button_url' => $subscription['update_url'], + 'status' => 'update-available', + 'icon' => 'dashicons-update', + ); + + // Subscription is not active on this site. + if ( ! $subscription['active'] ) { + $action['message'] .= ' ' . __( 'To enable this update you need to activate this subscription.', 'woocommerce' ); + $action['button_label'] = null; + $action['button_url'] = null; + } + + $subscription['actions'][] = $action; + } + + if ( $subscription['has_update'] && $subscription['expired'] ) { + $action = array( + /* translators: %s: version number */ + 'message' => sprintf( __( 'Version %s is available.', 'woocommerce' ), esc_html( $updates[ $subscription['product_id'] ]['version'] ) ), + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $action['message'] .= ' ' . __( 'To enable this update you need to purchase a new subscription.', 'woocommerce' ); + $action['button_label'] = __( 'Purchase', 'woocommerce' ); + $action['button_url'] = $subscription['product_url']; + + $subscription['actions'][] = $action; + } elseif ( $subscription['expired'] && ! empty( $subscription['master_user_email'] ) ) { + $action = array( + 'message' => sprintf( __( 'This subscription has expired. Contact the owner to renew the subscription to receive updates and support.', 'woocommerce' ) ), + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $subscription['actions'][] = $action; + } elseif ( $subscription['expired'] ) { + $action = array( + 'message' => sprintf( __( 'This subscription has expired. Please renew to receive updates and support.', 'woocommerce' ) ), + 'button_label' => __( 'Renew', 'woocommerce' ), + 'button_url' => 'https://woocommerce.com/my-account/my-subscriptions/', + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $subscription['actions'][] = $action; + } + + if ( $subscription['expiring'] && ! $subscription['autorenew'] ) { + $action = array( + 'message' => __( 'Subscription is expiring soon.', 'woocommerce' ), + 'button_label' => __( 'Enable auto-renew', 'woocommerce' ), + 'button_url' => 'https://woocommerce.com/my-account/my-subscriptions/', + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $subscription['download_primary'] = false; + $subscription['actions'][] = $action; + } elseif ( $subscription['expiring'] ) { + $action = array( + 'message' => sprintf( __( 'This subscription is expiring soon. Please renew to continue receiving updates and support.', 'woocommerce' ) ), + 'button_label' => __( 'Renew', 'woocommerce' ), + 'button_url' => 'https://woocommerce.com/my-account/my-subscriptions/', + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $subscription['download_primary'] = false; + $subscription['actions'][] = $action; + } + + // Mark the first action primary. + foreach ( $subscription['actions'] as $key => $action ) { + if ( ! empty( $action['button_label'] ) ) { + $subscription['actions'][ $key ]['primary'] = true; + break; + } + } + } + + // Break the by-ref. + unset( $subscription ); + + // Installed products without a subscription. + $no_subscriptions = array(); + foreach ( array_merge( $woo_plugins, $woo_themes ) as $filename => $data ) { + if ( in_array( $data['_product_id'], $subscriptions_product_ids ) ) { + continue; + } + + $data['_product_url'] = '#'; + $data['_has_update'] = false; + + if ( ! empty( $updates[ $data['_product_id'] ] ) ) { + $data['_has_update'] = version_compare( $updates[ $data['_product_id'] ]['version'], $data['Version'], '>' ); + + if ( ! empty( $updates[ $data['_product_id'] ]['url'] ) ) { + $data['_product_url'] = $updates[ $data['_product_id'] ]['url']; + } elseif ( ! empty( $data['PluginURI'] ) ) { + $data['_product_url'] = $data['PluginURI']; + } + } + + $data['_actions'] = array(); + + if ( $data['_has_update'] ) { + $action = array( + /* translators: %s: version number */ + 'message' => sprintf( __( 'Version %s is available. To enable this update you need to purchase a new subscription.', 'woocommerce' ), esc_html( $updates[ $data['_product_id'] ]['version'] ) ), + 'button_label' => __( 'Purchase', 'woocommerce' ), + 'button_url' => $data['_product_url'], + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $data['_actions'][] = $action; + } else { + $action = array( + /* translators: 1: subscriptions docs 2: subscriptions docs */ + 'message' => sprintf( __( 'To receive updates and support for this extension, you need to purchase a new subscription or consolidate your extensions to one connected account by sharing or transferring this extension to this connected account.', 'woocommerce' ), 'https://docs.woocommerce.com/document/managing-woocommerce-com-subscriptions/#section-10', 'https://docs.woocommerce.com/document/managing-woocommerce-com-subscriptions/#section-5' ), + 'button_label' => __( 'Purchase', 'woocommerce' ), + 'button_url' => $data['_product_url'], + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $data['_actions'][] = $action; + } + + $no_subscriptions[ $filename ] = $data; + } + + // Update the user id if it came from a migrated connection. + if ( empty( $auth['user_id'] ) ) { + $auth['user_id'] = get_current_user_id(); + WC_Helper_Options::update( 'auth', $auth ); + } + + // Sort alphabetically. + uasort( $subscriptions, array( __CLASS__, '_sort_by_product_name' ) ); + uasort( $no_subscriptions, array( __CLASS__, '_sort_by_name' ) ); + + // Filters. + self::get_filters_counts( $subscriptions ); // Warm it up. + self::_filter( $subscriptions, self::get_current_filter() ); + + // We have an active connection. + include self::get_view_filename( 'html-main.php' ); + return; + } + + /** + * Get available subscriptions filters. + * + * @return array An array of filter keys and labels. + */ + public static function get_filters() { + $filters = array( + 'all' => __( 'All', 'woocommerce' ), + 'active' => __( 'Active', 'woocommerce' ), + 'inactive' => __( 'Inactive', 'woocommerce' ), + 'installed' => __( 'Installed', 'woocommerce' ), + 'update-available' => __( 'Update Available', 'woocommerce' ), + 'expiring' => __( 'Expiring Soon', 'woocommerce' ), + 'expired' => __( 'Expired', 'woocommerce' ), + 'download' => __( 'Download', 'woocommerce' ), + ); + + return $filters; + } + + /** + * Get counts data for the filters array. + * + * @param array $subscriptions The array of all available subscriptions. + * + * @return array Filter counts (filter => count). + */ + public static function get_filters_counts( $subscriptions = null ) { + static $filters; + + if ( isset( $filters ) ) { + return $filters; + } + + $filters = array_fill_keys( array_keys( self::get_filters() ), 0 ); + if ( empty( $subscriptions ) ) { + return array(); + } + + foreach ( $filters as $key => $count ) { + $_subs = $subscriptions; + self::_filter( $_subs, $key ); + $filters[ $key ] = count( $_subs ); + } + + return $filters; + } + + /** + * Get current filter. + * + * @return string The current filter. + */ + public static function get_current_filter() { + $current_filter = 'all'; + $valid_filters = array_keys( self::get_filters() ); + + if ( ! empty( $_GET['filter'] ) && in_array( wp_unslash( $_GET['filter'] ), $valid_filters ) ) { + $current_filter = wc_clean( wp_unslash( $_GET['filter'] ) ); + } + + return $current_filter; + } + + /** + * Filter an array of subscriptions by $filter. + * + * @param array $subscriptions The subscriptions array, passed by ref. + * @param string $filter The filter. + */ + private static function _filter( &$subscriptions, $filter ) { + switch ( $filter ) { + case 'active': + $subscriptions = wp_list_filter( $subscriptions, array( 'active' => true ) ); + break; + + case 'inactive': + $subscriptions = wp_list_filter( $subscriptions, array( 'active' => false ) ); + break; + + case 'installed': + foreach ( $subscriptions as $key => $subscription ) { + if ( empty( $subscription['local']['installed'] ) ) { + unset( $subscriptions[ $key ] ); + } + } + break; + + case 'update-available': + $subscriptions = wp_list_filter( $subscriptions, array( 'has_update' => true ) ); + break; + + case 'expiring': + $subscriptions = wp_list_filter( $subscriptions, array( 'expiring' => true ) ); + break; + + case 'expired': + $subscriptions = wp_list_filter( $subscriptions, array( 'expired' => true ) ); + break; + + case 'download': + foreach ( $subscriptions as $key => $subscription ) { + if ( $subscription['local']['installed'] || $subscription['expired'] ) { + unset( $subscriptions[ $key ] ); + } + } + break; + } + } + + /** + * Enqueue admin scripts and styles. + */ + public static function admin_enqueue_scripts() { + $screen = get_current_screen(); + $screen_id = $screen ? $screen->id : ''; + $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) ); + + if ( $wc_screen_id . '_page_wc-addons' === $screen_id && isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) { + wp_enqueue_style( 'woocommerce-helper', WC()->plugin_url() . '/assets/css/helper.css', array(), Constants::get_constant( 'WC_VERSION' ) ); + wp_style_add_data( 'woocommerce-helper', 'rtl', 'replace' ); + } + } + + /** + * Various success/error notices. + * + * Runs during admin page render, so no headers/redirects here. + * + * @return array Array pairs of message/type strings with notices. + */ + private static function _get_return_notices() { + $return_status = isset( $_GET['wc-helper-status'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-status'] ) ) : null; + $notices = array(); + + switch ( $return_status ) { + case 'activate-success': + $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; + $subscription = self::_get_subscriptions_from_product_id( $product_id ); + $notices[] = array( + 'type' => 'updated', + 'message' => sprintf( + /* translators: %s: product name */ + __( '%s activated successfully. You will now receive updates for this product.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '' + ), + ); + break; + + case 'activate-error': + $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; + $subscription = self::_get_subscriptions_from_product_id( $product_id ); + $notices[] = array( + 'type' => 'error', + 'message' => sprintf( + /* translators: %s: product name */ + __( 'An error has occurred when activating %s. Please try again later.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '' + ), + ); + break; + + case 'deactivate-success': + $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; + $subscription = self::_get_subscriptions_from_product_id( $product_id ); + $local = self::_get_local_from_product_id( $product_id ); + + $message = sprintf( + /* translators: %s: product name */ + __( 'Subscription for %s deactivated successfully. You will no longer receive updates for this product.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '' + ); + + if ( $local && is_plugin_active( $local['_filename'] ) && current_user_can( 'activate_plugins' ) ) { + $deactivate_plugin_url = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'filter' => self::get_current_filter(), + 'wc-helper-deactivate-plugin' => 1, + 'wc-helper-product-id' => $subscription['product_id'], + 'wc-helper-nonce' => wp_create_nonce( 'deactivate-plugin:' . $subscription['product_id'] ), + ), + admin_url( 'admin.php' ) + ); + + $message = sprintf( + /* translators: %1$s: product name, %2$s: deactivate url */ + __( 'Subscription for %1$s deactivated successfully. You will no longer receive updates for this product. Click here if you wish to deactivate the plugin as well.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '', + esc_url( $deactivate_plugin_url ) + ); + } + + $notices[] = array( + 'message' => $message, + 'type' => 'updated', + ); + break; + + case 'deactivate-error': + $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; + $subscription = self::_get_subscriptions_from_product_id( $product_id ); + $notices[] = array( + 'type' => 'error', + 'message' => sprintf( + /* translators: %s: product name */ + __( 'An error has occurred when deactivating the subscription for %s. Please try again later.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '' + ), + ); + break; + + case 'deactivate-plugin-success': + $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; + $subscription = self::_get_subscriptions_from_product_id( $product_id ); + $notices[] = array( + 'type' => 'updated', + 'message' => sprintf( + /* translators: %s: product name */ + __( 'The extension %s has been deactivated successfully.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '' + ), + ); + break; + + case 'deactivate-plugin-error': + $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; + $subscription = self::_get_subscriptions_from_product_id( $product_id ); + $notices[] = array( + 'type' => 'error', + 'message' => sprintf( + /* translators: %1$s: product name, %2$s: plugins screen url */ + __( 'An error has occurred when deactivating the extension %1$s. Please proceed to the Plugins screen to deactivate it manually.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '', + admin_url( 'plugins.php' ) + ), + ); + break; + + case 'helper-connected': + $notices[] = array( + 'message' => __( 'You have successfully connected your store to WooCommerce.com', 'woocommerce' ), + 'type' => 'updated', + ); + break; + + case 'helper-disconnected': + $notices[] = array( + 'message' => __( 'You have successfully disconnected your store from WooCommerce.com', 'woocommerce' ), + 'type' => 'updated', + ); + break; + + case 'helper-refreshed': + $notices[] = array( + 'message' => __( 'Authentication and subscription caches refreshed successfully.', 'woocommerce' ), + 'type' => 'updated', + ); + break; + } + + return $notices; + } + + /** + * Various early-phase actions with possible redirects. + * + * @param object $screen WP screen object. + */ + public static function current_screen( $screen ) { + $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) ); + + if ( $wc_screen_id . '_page_wc-addons' !== $screen->id ) { + return; + } + + if ( empty( $_GET['section'] ) || 'helper' !== $_GET['section'] ) { + return; + } + + if ( ! empty( $_GET['wc-helper-connect'] ) ) { + return self::_helper_auth_connect(); + } + + if ( ! empty( $_GET['wc-helper-return'] ) ) { + return self::_helper_auth_return(); + } + + if ( ! empty( $_GET['wc-helper-disconnect'] ) ) { + return self::_helper_auth_disconnect(); + } + + if ( ! empty( $_GET['wc-helper-refresh'] ) ) { + return self::_helper_auth_refresh(); + } + + if ( ! empty( $_GET['wc-helper-activate'] ) ) { + return self::_helper_subscription_activate(); + } + + if ( ! empty( $_GET['wc-helper-deactivate'] ) ) { + return self::_helper_subscription_deactivate(); + } + + if ( ! empty( $_GET['wc-helper-deactivate-plugin'] ) ) { + return self::_helper_plugin_deactivate(); + } + } + + /** + * Initiate a new OAuth connection. + */ + private static function _helper_auth_connect() { + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'connect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + self::log( 'Could not verify nonce in _helper_auth_connect' ); + wp_die( 'Could not verify nonce' ); + } + + $redirect_uri = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'wc-helper-return' => 1, + 'wc-helper-nonce' => wp_create_nonce( 'connect' ), + ), + admin_url( 'admin.php' ) + ); + + $request = WC_Helper_API::post( + 'oauth/request_token', + array( + 'body' => array( + 'home_url' => home_url(), + 'redirect_uri' => $redirect_uri, + ), + ) + ); + + $code = wp_remote_retrieve_response_code( $request ); + + if ( 200 !== $code ) { + self::log( sprintf( 'Call to oauth/request_token returned a non-200 response code (%d)', $code ) ); + wp_die( 'Something went wrong' ); + } + + $secret = json_decode( wp_remote_retrieve_body( $request ) ); + if ( empty( $secret ) ) { + self::log( sprintf( 'Call to oauth/request_token returned an invalid body: %s', wp_remote_retrieve_body( $request ) ) ); + wp_die( 'Something went wrong' ); + } + + /** + * Fires when the Helper connection process is initiated. + */ + do_action( 'woocommerce_helper_connect_start' ); + + $connect_url = add_query_arg( + array( + 'home_url' => rawurlencode( home_url() ), + 'redirect_uri' => rawurlencode( $redirect_uri ), + 'secret' => rawurlencode( $secret ), + ), + WC_Helper_API::url( 'oauth/authorize' ) + ); + + wp_redirect( esc_url_raw( $connect_url ) ); + die(); + } + + /** + * Return from WooCommerce.com OAuth flow. + */ + private static function _helper_auth_return() { + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'connect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + self::log( 'Could not verify nonce in _helper_auth_return' ); + wp_die( 'Something went wrong' ); + } + + // Bail if the user clicked deny. + if ( ! empty( $_GET['deny'] ) ) { + /** + * Fires when the Helper connection process is denied/cancelled. + */ + do_action( 'woocommerce_helper_denied' ); + wp_safe_redirect( admin_url( 'admin.php?page=wc-addons§ion=helper' ) ); + die(); + } + + // We do need a request token... + if ( empty( $_GET['request_token'] ) ) { + self::log( 'Request token not found in _helper_auth_return' ); + wp_die( 'Something went wrong' ); + } + + // Obtain an access token. + $request = WC_Helper_API::post( + 'oauth/access_token', + array( + 'body' => array( + 'request_token' => wp_unslash( $_GET['request_token'] ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + 'home_url' => home_url(), + ), + ) + ); + + $code = wp_remote_retrieve_response_code( $request ); + + if ( 200 !== $code ) { + self::log( sprintf( 'Call to oauth/access_token returned a non-200 response code (%d)', $code ) ); + wp_die( 'Something went wrong' ); + } + + $access_token = json_decode( wp_remote_retrieve_body( $request ), true ); + if ( ! $access_token ) { + self::log( sprintf( 'Call to oauth/access_token returned an invalid body: %s', wp_remote_retrieve_body( $request ) ) ); + wp_die( 'Something went wrong' ); + } + + WC_Helper_Options::update( + 'auth', + array( + 'access_token' => $access_token['access_token'], + 'access_token_secret' => $access_token['access_token_secret'], + 'site_id' => $access_token['site_id'], + 'user_id' => get_current_user_id(), + 'updated' => time(), + ) + ); + + // Obtain the connected user info. + if ( ! self::_flush_authentication_cache() ) { + self::log( 'Could not obtain connected user info in _helper_auth_return' ); + WC_Helper_Options::update( 'auth', array() ); + wp_die( 'Something went wrong.' ); + } + + self::_flush_subscriptions_cache(); + self::_flush_updates_cache(); + + /** + * Fires when the Helper connection process has completed successfully. + */ + do_action( 'woocommerce_helper_connected' ); + + // Enable tracking when connected. + if ( class_exists( 'WC_Tracker' ) ) { + update_option( 'woocommerce_allow_tracking', 'yes' ); + WC_Tracker::send_tracking_data( true ); + } + + // If connecting through in-app purchase, redirects back to WooCommerce.com + // for product installation. + if ( ! empty( $_GET['wccom-install-url'] ) ) { + wp_redirect( wp_unslash( $_GET['wccom-install-url'] ) ); + exit; + } + + wp_safe_redirect( + add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'wc-helper-status' => 'helper-connected', + ), + admin_url( 'admin.php' ) + ) + ); + die(); + } + + /** + * Disconnect from WooCommerce.com, clear OAuth tokens. + */ + private static function _helper_auth_disconnect() { + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'disconnect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + self::log( 'Could not verify nonce in _helper_auth_disconnect' ); + wp_die( 'Could not verify nonce' ); + } + + /** + * Fires when the Helper has been disconnected. + */ + do_action( 'woocommerce_helper_disconnected' ); + + $redirect_uri = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'wc-helper-status' => 'helper-disconnected', + ), + admin_url( 'admin.php' ) + ); + + WC_Helper_API::post( + 'oauth/invalidate_token', + array( + 'authenticated' => true, + ) + ); + + WC_Helper_Options::update( 'auth', array() ); + WC_Helper_Options::update( 'auth_user_data', array() ); + + self::_flush_subscriptions_cache(); + self::_flush_updates_cache(); + + wp_safe_redirect( $redirect_uri ); + die(); + } + + /** + * User hit the Refresh button, clear all caches. + */ + private static function _helper_auth_refresh() { + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'refresh' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + self::log( 'Could not verify nonce in _helper_auth_refresh' ); + wp_die( 'Could not verify nonce' ); + } + + /** + * Fires when Helper subscriptions are refreshed. + */ + do_action( 'woocommerce_helper_subscriptions_refresh' ); + + $redirect_uri = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'filter' => self::get_current_filter(), + 'wc-helper-status' => 'helper-refreshed', + ), + admin_url( 'admin.php' ) + ); + + self::_flush_authentication_cache(); + self::_flush_subscriptions_cache(); + self::_flush_updates_cache(); + + wp_safe_redirect( $redirect_uri ); + die(); + } + + /** + * Active a product subscription. + */ + private static function _helper_subscription_activate() { + $product_key = isset( $_GET['wc-helper-product-key'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-product-key'] ) ) : ''; + $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; + + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'activate:' . $product_key ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + self::log( 'Could not verify nonce in _helper_subscription_activate' ); + wp_die( 'Could not verify nonce' ); + } + + // Activate subscription. + $activation_response = WC_Helper_API::post( + 'activate', + array( + 'authenticated' => true, + 'body' => wp_json_encode( + array( + 'product_key' => $product_key, + ) + ), + ) + ); + + $activated = wp_remote_retrieve_response_code( $activation_response ) === 200; + $body = json_decode( wp_remote_retrieve_body( $activation_response ), true ); + + if ( ! $activated && ! empty( $body['code'] ) && 'already_connected' === $body['code'] ) { + $activated = true; + } + + if ( $activated ) { + /** + * Fires when the Helper activates a product successfully. + * + * @param int $product_id Product ID being activated. + * @param string $product_key Subscription product key. + * @param array $activation_response The response object from wp_safe_remote_request(). + */ + do_action( 'woocommerce_helper_subscription_activate_success', $product_id, $product_key, $activation_response ); + } else { + /** + * Fires when the Helper fails to activate a product. + * + * @param int $product_id Product ID being activated. + * @param string $product_key Subscription product key. + * @param array $activation_response The response object from wp_safe_remote_request(). + */ + do_action( 'woocommerce_helper_subscription_activate_error', $product_id, $product_key, $activation_response ); + } + + // Attempt to activate this plugin. + $local = self::_get_local_from_product_id( $product_id ); + if ( $local && 'plugin' == $local['_type'] && current_user_can( 'activate_plugins' ) && ! is_plugin_active( $local['_filename'] ) ) { + activate_plugin( $local['_filename'] ); + } + + self::_flush_subscriptions_cache(); + self::_flush_updates_cache(); + + $redirect_uri = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'filter' => self::get_current_filter(), + 'wc-helper-status' => $activated ? 'activate-success' : 'activate-error', + 'wc-helper-product-id' => $product_id, + ), + admin_url( 'admin.php' ) + ); + + wp_safe_redirect( $redirect_uri ); + die(); + } + + /** + * Deactivate a product subscription. + */ + private static function _helper_subscription_deactivate() { + $product_key = isset( $_GET['wc-helper-product-key'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-product-key'] ) ) : ''; + $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; + + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'deactivate:' . $product_key ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + self::log( 'Could not verify nonce in _helper_subscription_deactivate' ); + wp_die( 'Could not verify nonce' ); + } + + $deactivation_response = WC_Helper_API::post( + 'deactivate', + array( + 'authenticated' => true, + 'body' => wp_json_encode( + array( + 'product_key' => $product_key, + ) + ), + ) + ); + + $code = wp_remote_retrieve_response_code( $deactivation_response ); + $deactivated = 200 === $code; + + if ( $deactivated ) { + /** + * Fires when the Helper activates a product successfully. + * + * @param int $product_id Product ID being deactivated. + * @param string $product_key Subscription product key. + * @param array $deactivation_response The response object from wp_safe_remote_request(). + */ + do_action( 'woocommerce_helper_subscription_deactivate_success', $product_id, $product_key, $deactivation_response ); + } else { + self::log( sprintf( 'Deactivate API call returned a non-200 response code (%d)', $code ) ); + + /** + * Fires when the Helper fails to activate a product. + * + * @param int $product_id Product ID being deactivated. + * @param string $product_key Subscription product key. + * @param array $deactivation_response The response object from wp_safe_remote_request(). + */ + do_action( 'woocommerce_helper_subscription_deactivate_error', $product_id, $product_key, $deactivation_response ); + } + + self::_flush_subscriptions_cache(); + + $redirect_uri = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'filter' => self::get_current_filter(), + 'wc-helper-status' => $deactivated ? 'deactivate-success' : 'deactivate-error', + 'wc-helper-product-id' => $product_id, + ), + admin_url( 'admin.php' ) + ); + + wp_safe_redirect( $redirect_uri ); + die(); + } + + /** + * Deactivate a plugin. + */ + private static function _helper_plugin_deactivate() { + $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; + $deactivated = false; + + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'deactivate-plugin:' . $product_id ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + self::log( 'Could not verify nonce in _helper_plugin_deactivate' ); + wp_die( 'Could not verify nonce' ); + } + + if ( ! current_user_can( 'activate_plugins' ) ) { + wp_die( 'You are not allowed to manage plugins on this site.' ); + } + + $local = wp_list_filter( + array_merge( + self::get_local_woo_plugins(), + self::get_local_woo_themes() + ), + array( '_product_id' => $product_id ) + ); + + // Attempt to deactivate this plugin or theme. + if ( ! empty( $local ) ) { + $local = array_shift( $local ); + if ( is_plugin_active( $local['_filename'] ) ) { + deactivate_plugins( $local['_filename'] ); + } + + $deactivated = ! is_plugin_active( $local['_filename'] ); + } + + $redirect_uri = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'filter' => self::get_current_filter(), + 'wc-helper-status' => $deactivated ? 'deactivate-plugin-success' : 'deactivate-plugin-error', + 'wc-helper-product-id' => $product_id, + ), + admin_url( 'admin.php' ) + ); + + wp_safe_redirect( $redirect_uri ); + die(); + } + + /** + * Get a local plugin/theme entry from product_id. + * + * @param int $product_id The product id. + * + * @return array|bool The array containing the local plugin/theme data or false. + */ + private static function _get_local_from_product_id( $product_id ) { + $local = wp_list_filter( + array_merge( + self::get_local_woo_plugins(), + self::get_local_woo_themes() + ), + array( '_product_id' => $product_id ) + ); + + if ( ! empty( $local ) ) { + return array_shift( $local ); + } + + return false; + } + + /** + * Checks whether current site has product subscription of a given ID. + * + * @since 3.7.0 + * + * @param int $product_id The product id. + * + * @return bool Returns true if product subscription exists, false otherwise. + */ + public static function has_product_subscription( $product_id ) { + $subscription = self::_get_subscriptions_from_product_id( $product_id, true ); + return ! empty( $subscription ); + } + + /** + * Get a subscription entry from product_id. If multiple subscriptions are + * found with the same product id and $single is set to true, will return the + * first one in the list, so you can use this method to get things like extension + * name, version, etc. + * + * @param int $product_id The product id. + * @param bool $single Whether to return a single subscription or all matching a product id. + * + * @return array|bool The array containing sub data or false. + */ + private static function _get_subscriptions_from_product_id( $product_id, $single = true ) { + $subscriptions = wp_list_filter( self::get_subscriptions(), array( 'product_id' => $product_id ) ); + if ( ! empty( $subscriptions ) ) { + return $single ? array_shift( $subscriptions ) : $subscriptions; + } + + return false; + } + + /** + * Obtain a list of data about locally installed Woo extensions. + */ + public static function get_local_woo_plugins() { + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $plugins = get_plugins(); + + /** + * Check if plugins have WC headers, if not then clear cache and fetch again. + * WC Headers will not be present if `wc_enable_wc_plugin_headers` hook was added after a `get_plugins` call -- for example when WC is activated/updated. + * Also, get_plugins call is expensive, so we should clear this cache very conservatively. + */ + if ( ! empty( $plugins ) && ! array_key_exists( 'Woo', current( $plugins ) ) ) { + wp_clean_plugins_cache( false ); + $plugins = get_plugins(); + } + + $woo_plugins = array(); + + // Backwards compatibility for woothemes_queue_update(). + $_compat = array(); + if ( ! empty( $GLOBALS['woothemes_queued_updates'] ) ) { + foreach ( $GLOBALS['woothemes_queued_updates'] as $_compat_plugin ) { + $_compat[ $_compat_plugin->file ] = array( + 'product_id' => $_compat_plugin->product_id, + 'file_id' => $_compat_plugin->file_id, + ); + } + } + + foreach ( $plugins as $filename => $data ) { + if ( empty( $data['Woo'] ) && ! empty( $_compat[ $filename ] ) ) { + $data['Woo'] = sprintf( '%d:%s', $_compat[ $filename ]['product_id'], $_compat[ $filename ]['file_id'] ); + } + + if ( empty( $data['Woo'] ) ) { + continue; + } + + list( $product_id, $file_id ) = explode( ':', $data['Woo'] ); + if ( empty( $product_id ) || empty( $file_id ) ) { + continue; + } + + $data['_filename'] = $filename; + $data['_product_id'] = absint( $product_id ); + $data['_file_id'] = $file_id; + $data['_type'] = 'plugin'; + $data['slug'] = dirname( $filename ); + $woo_plugins[ $filename ] = $data; + } + + return $woo_plugins; + } + + /** + * Get locally installed Woo themes. + */ + public static function get_local_woo_themes() { + $themes = wp_get_themes(); + $woo_themes = array(); + + foreach ( $themes as $theme ) { + $header = $theme->get( 'Woo' ); + + // Backwards compatibility for theme_info.txt. + if ( ! $header ) { + $txt = $theme->get_stylesheet_directory() . '/theme_info.txt'; + if ( is_readable( $txt ) ) { + $txt = file_get_contents( $txt ); + $txt = preg_split( '#\s#', $txt ); + if ( count( $txt ) >= 2 ) { + $header = sprintf( '%d:%s', $txt[0], $txt[1] ); + } + } + } + + if ( empty( $header ) ) { + continue; + } + + list( $product_id, $file_id ) = explode( ':', $header ); + if ( empty( $product_id ) || empty( $file_id ) ) { + continue; + } + + $data = array( + 'Name' => $theme->get( 'Name' ), + 'Version' => $theme->get( 'Version' ), + 'Woo' => $header, + + '_filename' => $theme->get_stylesheet() . '/style.css', + '_stylesheet' => $theme->get_stylesheet(), + '_product_id' => absint( $product_id ), + '_file_id' => $file_id, + '_type' => 'theme', + ); + + $woo_themes[ $data['_filename'] ] = $data; + } + + return $woo_themes; + } + + /** + * Get the connected user's subscriptions. + * + * @return array + */ + public static function get_subscriptions() { + $cache_key = '_woocommerce_helper_subscriptions'; + $data = get_transient( $cache_key ); + if ( false !== $data ) { + return $data; + } + + // Obtain the connected user info. + $request = WC_Helper_API::get( + 'subscriptions', + array( + 'authenticated' => true, + ) + ); + + if ( wp_remote_retrieve_response_code( $request ) !== 200 ) { + set_transient( $cache_key, array(), 15 * MINUTE_IN_SECONDS ); + return array(); + } + + $data = json_decode( wp_remote_retrieve_body( $request ), true ); + if ( empty( $data ) || ! is_array( $data ) ) { + $data = array(); + } + + set_transient( $cache_key, $data, 1 * HOUR_IN_SECONDS ); + return $data; + } + + /** + * Runs when any plugin is activated. + * + * Depending on the activated plugin attempts to look through available + * subscriptions and auto-activate one if possible, so the user does not + * need to visit the Helper UI at all after installing a new extension. + * + * @param string $filename The filename of the activated plugin. + */ + public static function activated_plugin( $filename ) { + $plugins = self::get_local_woo_plugins(); + + // Not a local woo plugin. + if ( empty( $plugins[ $filename ] ) ) { + return; + } + + // Make sure we have a connection. + $auth = WC_Helper_Options::get( 'auth' ); + if ( empty( $auth ) ) { + return; + } + + $plugin = $plugins[ $filename ]; + $product_id = $plugin['_product_id']; + $subscriptions = self::_get_subscriptions_from_product_id( $product_id, false ); + + // No valid subscriptions for this product. + if ( empty( $subscriptions ) ) { + return; + } + + $subscription = null; + foreach ( $subscriptions as $_sub ) { + + // Don't attempt to activate expired subscriptions. + if ( $_sub['expired'] ) { + continue; + } + + // No more sites available in this subscription. + if ( $_sub['sites_max'] && $_sub['sites_active'] >= $_sub['sites_max'] ) { + continue; + } + + // Looks good. + $subscription = $_sub; + break; + } + + // No valid subscription found. + if ( ! $subscription ) { + return; + } + + $product_key = $subscription['product_key']; + $activation_response = WC_Helper_API::post( + 'activate', + array( + 'authenticated' => true, + 'body' => wp_json_encode( + array( + 'product_key' => $product_key, + ) + ), + ) + ); + + $activated = wp_remote_retrieve_response_code( $activation_response ) === 200; + $body = json_decode( wp_remote_retrieve_body( $activation_response ), true ); + + if ( ! $activated && ! empty( $body['code'] ) && 'already_connected' === $body['code'] ) { + $activated = true; + } + + if ( $activated ) { + self::log( 'Auto-activated a subscription for ' . $filename ); + /** + * Fires when the Helper activates a product successfully. + * + * @param int $product_id Product ID being activated. + * @param string $product_key Subscription product key. + * @param array $activation_response The response object from wp_safe_remote_request(). + */ + do_action( 'woocommerce_helper_subscription_activate_success', $product_id, $product_key, $activation_response ); + } else { + self::log( 'Could not activate a subscription upon plugin activation: ' . $filename ); + + /** + * Fires when the Helper fails to activate a product. + * + * @param int $product_id Product ID being activated. + * @param string $product_key Subscription product key. + * @param array $activation_response The response object from wp_safe_remote_request(). + */ + do_action( 'woocommerce_helper_subscription_activate_error', $product_id, $product_key, $activation_response ); + } + + self::_flush_subscriptions_cache(); + self::_flush_updates_cache(); + } + + /** + * Runs when any plugin is deactivated. + * + * When a user deactivates a plugin, attempt to deactivate any subscriptions + * associated with the extension. + * + * @param string $filename The filename of the deactivated plugin. + */ + public static function deactivated_plugin( $filename ) { + $plugins = self::get_local_woo_plugins(); + + // Not a local woo plugin. + if ( empty( $plugins[ $filename ] ) ) { + return; + } + + // Make sure we have a connection. + $auth = WC_Helper_Options::get( 'auth' ); + if ( empty( $auth ) ) { + return; + } + + $plugin = $plugins[ $filename ]; + $product_id = $plugin['_product_id']; + $subscriptions = self::_get_subscriptions_from_product_id( $product_id, false ); + $site_id = absint( $auth['site_id'] ); + + // No valid subscriptions for this product. + if ( empty( $subscriptions ) ) { + return; + } + + $deactivated = 0; + + foreach ( $subscriptions as $subscription ) { + // Don't touch subscriptions that aren't activated on this site. + if ( ! in_array( $site_id, $subscription['connections'], true ) ) { + continue; + } + + $product_key = $subscription['product_key']; + $deactivation_response = WC_Helper_API::post( + 'deactivate', + array( + 'authenticated' => true, + 'body' => wp_json_encode( + array( + 'product_key' => $product_key, + ) + ), + ) + ); + + if ( wp_remote_retrieve_response_code( $deactivation_response ) === 200 ) { + $deactivated++; + + /** + * Fires when the Helper activates a product successfully. + * + * @param int $product_id Product ID being deactivated. + * @param string $product_key Subscription product key. + * @param array $deactivation_response The response object from wp_safe_remote_request(). + */ + do_action( 'woocommerce_helper_subscription_deactivate_success', $product_id, $product_key, $deactivation_response ); + } else { + /** + * Fires when the Helper fails to activate a product. + * + * @param int $product_id Product ID being deactivated. + * @param string $product_key Subscription product key. + * @param array $deactivation_response The response object from wp_safe_remote_request(). + */ + do_action( 'woocommerce_helper_subscription_deactivate_error', $product_id, $product_key, $deactivation_response ); + } + } + + if ( $deactivated ) { + self::log( sprintf( 'Auto-deactivated %d subscription(s) for %s', $deactivated, $filename ) ); + self::_flush_subscriptions_cache(); + self::_flush_updates_cache(); + } + } + + /** + * Various Helper-related admin notices. + */ + public static function admin_notices() { + if ( apply_filters( 'woocommerce_helper_suppress_admin_notices', false ) ) { + return; + } + + $screen = get_current_screen(); + $screen_id = $screen ? $screen->id : ''; + + if ( 'update-core' !== $screen_id ) { + return; + } + + // Don't nag if Woo doesn't have an update available. + if ( ! self::_woo_core_update_available() ) { + return; + } + + // Add a note about available extension updates if Woo core has an update available. + $notice = self::_get_extensions_update_notice(); + if ( ! empty( $notice ) ) { + echo '

    ' . $notice . '

    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } + + /** + * Get an update notice if one or more Woo extensions has an update available. + * + * @return string|null The update notice or null if everything is up to date. + */ + private static function _get_extensions_update_notice() { + $plugins = self::get_local_woo_plugins(); + $updates = WC_Helper_Updater::get_update_data(); + $available = 0; + + foreach ( $plugins as $data ) { + if ( empty( $updates[ $data['_product_id'] ] ) ) { + continue; + } + + $product_id = $data['_product_id']; + if ( version_compare( $updates[ $product_id ]['version'], $data['Version'], '>' ) ) { + $available++; + } + } + + if ( ! $available ) { + return; + } + + return sprintf( + /* translators: %1$s: helper url, %2$d: number of extensions */ + _n( 'Note: You currently have %2$d paid extension which should be updated first before updating WooCommerce.', 'Note: You currently have %2$d paid extensions which should be updated first before updating WooCommerce.', $available, 'woocommerce' ), + admin_url( 'admin.php?page=wc-addons§ion=helper' ), + $available + ); + } + + /** + * Whether WooCommerce has an update available. + * + * @return bool True if a Woo core update is available. + */ + private static function _woo_core_update_available() { + $updates = get_site_transient( 'update_plugins' ); + if ( empty( $updates->response ) ) { + return false; + } + + if ( empty( $updates->response['woocommerce/woocommerce.php'] ) ) { + return false; + } + + $data = $updates->response['woocommerce/woocommerce.php']; + if ( version_compare( Constants::get_constant( 'WC_VERSION' ), $data->new_version, '>=' ) ) { + return false; + } + + return true; + } + + /** + * Flush subscriptions cache. + */ + public static function _flush_subscriptions_cache() { + delete_transient( '_woocommerce_helper_subscriptions' ); + } + + /** + * Flush auth cache. + */ + public static function _flush_authentication_cache() { + $request = WC_Helper_API::get( + 'oauth/me', + array( + 'authenticated' => true, + ) + ); + + if ( wp_remote_retrieve_response_code( $request ) !== 200 ) { + return false; + } + + $user_data = json_decode( wp_remote_retrieve_body( $request ), true ); + if ( ! $user_data ) { + return false; + } + + WC_Helper_Options::update( + 'auth_user_data', + array( + 'name' => $user_data['name'], + 'email' => $user_data['email'], + ) + ); + + return true; + } + + /** + * Flush updates cache. + */ + private static function _flush_updates_cache() { + WC_Helper_Updater::flush_updates_cache(); + } + + /** + * Sort subscriptions by the product_name. + * + * @param array $a Subscription array. + * @param array $b Subscription array. + * + * @return int + */ + public static function _sort_by_product_name( $a, $b ) { + return strcmp( $a['product_name'], $b['product_name'] ); + } + + /** + * Sort subscriptions by the Name. + * + * @param array $a Product array. + * @param array $b Product array. + * + * @return int + */ + public static function _sort_by_name( $a, $b ) { + return strcmp( $a['Name'], $b['Name'] ); + } + + /** + * Log a helper event. + * + * @param string $message Log message. + * @param string $level Optional, defaults to info, valid levels: emergency|alert|critical|error|warning|notice|info|debug. + */ + public static function log( $message, $level = 'info' ) { + if ( ! Constants::is_true( 'WP_DEBUG' ) ) { + return; + } + + if ( ! isset( self::$log ) ) { + self::$log = wc_get_logger(); + } + + self::$log->log( $level, $message, array( 'source' => 'helper' ) ); + } +} + +WC_Helper::load(); diff --git a/includes/admin/helper/views/html-helper-compat.php b/plugins/woocommerce/includes/admin/helper/views/html-helper-compat.php similarity index 100% rename from includes/admin/helper/views/html-helper-compat.php rename to plugins/woocommerce/includes/admin/helper/views/html-helper-compat.php diff --git a/plugins/woocommerce/includes/admin/helper/views/html-main.php b/plugins/woocommerce/includes/admin/helper/views/html-main.php new file mode 100644 index 00000000000..7aa6a8fba41 --- /dev/null +++ b/plugins/woocommerce/includes/admin/helper/views/html-main.php @@ -0,0 +1,257 @@ + + + +
    + +

    + + + +
    +

    + +

    + Plugins screen.', + 'woocommerce' + ), + array( + 'a' => array( + 'href' => array(), + ), + ) + ), + esc_url( + admin_url( 'plugins.php' ) + ) + ); + ?> +

    +
    + +
      + + + + $label ) : + // Don't show empty filters. + if ( empty( $counts[ $key ] ) ) { + continue; + } + + $url = admin_url( 'admin.php?page=wc-addons§ion=helper&filter=' . $key ); + $class_html = $current_filter === $key ? 'class="current"' : ''; + ?> +
    • + href=""> + + () + +
    • + +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + 0 ) { + /* translators: %1$d: sites active, %2$d max sites active */ + printf( esc_html__( 'Subscription: Using %1$d of %2$d sites available', 'woocommerce' ), absint( $subscription['sites_active'] ), absint( $subscription['sites_max'] ) ); + } else { + esc_html_e( 'Subscription: Unlimited', 'woocommerce' ); + } + + // Check shared. + if ( ! empty( $subscription['is_shared'] ) && ! empty( $subscription['owner_email'] ) ) { + /* translators: Email address of person who shared the subscription. */ + printf( '
    ' . esc_html__( 'Shared by %s', 'woocommerce' ), esc_html( $subscription['owner_email'] ) ); + } elseif ( isset( $subscription['master_user_email'] ) ) { + /* translators: Email address of person who shared the subscription. */ + printf( '
    ' . esc_html__( 'Shared by %s', 'woocommerce' ), esc_html( $subscription['master_user_email'] ) ); + } + ?> +
    +
    +
    + + + + + + + + + + + + + + + + + + + + +
    +

    + +

    +
    + + + +
    + + +

    +

    Below is a list of WooCommerce.com products available on your site - but are either out-dated or do not have a valid subscription.

    + + + + $data ) : ?> + + + + + + + + + + + + + + + + +
    +
    + +
    +
    +
    +
    + + + + +
    +

    + array( + 'href' => array(), + 'title' => array(), + ), + 'br' => array(), + 'em' => array(), + 'strong' => array(), + ) + ); + ?> +

    +
    + +
    + +
    diff --git a/plugins/woocommerce/includes/admin/helper/views/html-oauth-start.php b/plugins/woocommerce/includes/admin/helper/views/html-oauth-start.php new file mode 100644 index 00000000000..e1a14a93d01 --- /dev/null +++ b/plugins/woocommerce/includes/admin/helper/views/html-oauth-start.php @@ -0,0 +1,30 @@ + WooCommerce -> Extensions -> WooCommerce.com Subscriptions main page. + * + * @package WooCommerce\Views + */ + +defined( 'ABSPATH' ) || exit(); + +?> + +
    + +

    + + +
    +
    + <?php esc_attr_e( 'WooCommerce', 'woocommerce' ); ?> + + +

    + + +

    +

    +

    +
    +
    +
    diff --git a/includes/admin/helper/views/html-section-account.php b/plugins/woocommerce/includes/admin/helper/views/html-section-account.php similarity index 100% rename from includes/admin/helper/views/html-section-account.php rename to plugins/woocommerce/includes/admin/helper/views/html-section-account.php diff --git a/plugins/woocommerce/includes/admin/helper/views/html-section-nav.php b/plugins/woocommerce/includes/admin/helper/views/html-section-nav.php new file mode 100644 index 00000000000..5fa3d9b9d59 --- /dev/null +++ b/plugins/woocommerce/includes/admin/helper/views/html-section-nav.php @@ -0,0 +1,21 @@ + + + diff --git a/includes/admin/helper/views/html-section-notices.php b/plugins/woocommerce/includes/admin/helper/views/html-section-notices.php similarity index 100% rename from includes/admin/helper/views/html-section-notices.php rename to plugins/woocommerce/includes/admin/helper/views/html-section-notices.php diff --git a/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php b/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php new file mode 100644 index 00000000000..17b13f81653 --- /dev/null +++ b/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php @@ -0,0 +1,749 @@ + 'text/csv', + 'txt' => 'text/plain', + ) + ); + } + + /** + * Constructor. + */ + public function __construct() { + $default_steps = array( + 'upload' => array( + 'name' => __( 'Upload CSV file', 'woocommerce' ), + 'view' => array( $this, 'upload_form' ), + 'handler' => array( $this, 'upload_form_handler' ), + ), + 'mapping' => array( + 'name' => __( 'Column mapping', 'woocommerce' ), + 'view' => array( $this, 'mapping_form' ), + 'handler' => '', + ), + 'import' => array( + 'name' => __( 'Import', 'woocommerce' ), + 'view' => array( $this, 'import' ), + 'handler' => '', + ), + 'done' => array( + 'name' => __( 'Done!', 'woocommerce' ), + 'view' => array( $this, 'done' ), + 'handler' => '', + ), + ); + + $this->steps = apply_filters( 'woocommerce_product_csv_importer_steps', $default_steps ); + + // phpcs:disable WordPress.Security.NonceVerification.Recommended + $this->step = isset( $_REQUEST['step'] ) ? sanitize_key( $_REQUEST['step'] ) : current( array_keys( $this->steps ) ); + $this->file = isset( $_REQUEST['file'] ) ? wc_clean( wp_unslash( $_REQUEST['file'] ) ) : ''; + $this->update_existing = isset( $_REQUEST['update_existing'] ) ? (bool) $_REQUEST['update_existing'] : false; + $this->delimiter = ! empty( $_REQUEST['delimiter'] ) ? wc_clean( wp_unslash( $_REQUEST['delimiter'] ) ) : ','; + $this->map_preferences = isset( $_REQUEST['map_preferences'] ) ? (bool) $_REQUEST['map_preferences'] : false; + // phpcs:enable + + // Import mappings for CSV data. + include_once dirname( __FILE__ ) . '/mappings/mappings.php'; + + if ( $this->map_preferences ) { + add_filter( 'woocommerce_csv_product_import_mapped_columns', array( $this, 'auto_map_user_preferences' ), 9999 ); + } + } + + /** + * Get the URL for the next step's screen. + * + * @param string $step slug (default: current step). + * @return string URL for next step if a next step exists. + * Admin URL if it's the last step. + * Empty string on failure. + */ + public function get_next_step_link( $step = '' ) { + if ( ! $step ) { + $step = $this->step; + } + + $keys = array_keys( $this->steps ); + + if ( end( $keys ) === $step ) { + return admin_url(); + } + + $step_index = array_search( $step, $keys, true ); + + if ( false === $step_index ) { + return ''; + } + + $params = array( + 'step' => $keys[ $step_index + 1 ], + 'file' => str_replace( DIRECTORY_SEPARATOR, '/', $this->file ), + 'delimiter' => $this->delimiter, + 'update_existing' => $this->update_existing, + 'map_preferences' => $this->map_preferences, + '_wpnonce' => wp_create_nonce( 'woocommerce-csv-importer' ), // wp_nonce_url() escapes & to & breaking redirects. + ); + + return add_query_arg( $params ); + } + + /** + * Output header view. + */ + protected function output_header() { + include dirname( __FILE__ ) . '/views/html-csv-import-header.php'; + } + + /** + * Output steps view. + */ + protected function output_steps() { + include dirname( __FILE__ ) . '/views/html-csv-import-steps.php'; + } + + /** + * Output footer view. + */ + protected function output_footer() { + include dirname( __FILE__ ) . '/views/html-csv-import-footer.php'; + } + + /** + * Add error message. + * + * @param string $message Error message. + * @param array $actions List of actions with 'url' and 'label'. + */ + protected function add_error( $message, $actions = array() ) { + $this->errors[] = array( + 'message' => $message, + 'actions' => $actions, + ); + } + + /** + * Add error message. + */ + protected function output_errors() { + if ( ! $this->errors ) { + return; + } + + foreach ( $this->errors as $error ) { + echo '
    '; + echo '

    ' . esc_html( $error['message'] ) . '

    '; + + if ( ! empty( $error['actions'] ) ) { + echo '

    '; + foreach ( $error['actions'] as $action ) { + echo '' . esc_html( $action['label'] ) . ' '; + } + echo '

    '; + } + echo '
    '; + } + } + + /** + * Dispatch current step and show correct view. + */ + public function dispatch() { + // phpcs:ignore WordPress.Security.NonceVerification.Missing + if ( ! empty( $_POST['save_step'] ) && ! empty( $this->steps[ $this->step ]['handler'] ) ) { + call_user_func( $this->steps[ $this->step ]['handler'], $this ); + } + $this->output_header(); + $this->output_steps(); + $this->output_errors(); + call_user_func( $this->steps[ $this->step ]['view'], $this ); + $this->output_footer(); + } + + /** + * Output information about the uploading process. + */ + protected function upload_form() { + $bytes = apply_filters( 'import_upload_size_limit', wp_max_upload_size() ); + $size = size_format( $bytes ); + $upload_dir = wp_upload_dir(); + + include dirname( __FILE__ ) . '/views/html-product-csv-import-form.php'; + } + + /** + * Handle the upload form and store options. + */ + public function upload_form_handler() { + check_admin_referer( 'woocommerce-csv-importer' ); + + $file = $this->handle_upload(); + + if ( is_wp_error( $file ) ) { + $this->add_error( $file->get_error_message() ); + return; + } else { + $this->file = $file; + } + + wp_redirect( esc_url_raw( $this->get_next_step_link() ) ); + exit; + } + + /** + * Handles the CSV upload and initial parsing of the file to prepare for + * displaying author import options. + * + * @return string|WP_Error + */ + public function handle_upload() { + // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce already verified in WC_Product_CSV_Importer_Controller::upload_form_handler() + $file_url = isset( $_POST['file_url'] ) ? wc_clean( wp_unslash( $_POST['file_url'] ) ) : ''; + + if ( empty( $file_url ) ) { + if ( ! isset( $_FILES['import'] ) ) { + return new WP_Error( 'woocommerce_product_csv_importer_upload_file_empty', __( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your php.ini or by post_max_size being defined as smaller than upload_max_filesize in php.ini.', 'woocommerce' ) ); + } + + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated + if ( ! self::is_file_valid_csv( wc_clean( wp_unslash( $_FILES['import']['name'] ) ), false ) ) { + return new WP_Error( 'woocommerce_product_csv_importer_upload_file_invalid', __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); + } + + $overrides = array( + 'test_form' => false, + 'mimes' => self::get_valid_csv_filetypes(), + ); + $import = $_FILES['import']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash + $upload = wp_handle_upload( $import, $overrides ); + + if ( isset( $upload['error'] ) ) { + return new WP_Error( 'woocommerce_product_csv_importer_upload_error', $upload['error'] ); + } + + // Construct the object array. + $object = array( + 'post_title' => basename( $upload['file'] ), + 'post_content' => $upload['url'], + 'post_mime_type' => $upload['type'], + 'guid' => $upload['url'], + 'context' => 'import', + 'post_status' => 'private', + ); + + // Save the data. + $id = wp_insert_attachment( $object, $upload['file'] ); + + /* + * Schedule a cleanup for one day from now in case of failed + * import or missing wp_import_cleanup() call. + */ + wp_schedule_single_event( time() + DAY_IN_SECONDS, 'importer_scheduled_cleanup', array( $id ) ); + + return $upload['file']; + } elseif ( + ( 0 === stripos( realpath( ABSPATH . $file_url ), ABSPATH ) ) && + file_exists( ABSPATH . $file_url ) + ) { + if ( ! self::is_file_valid_csv( ABSPATH . $file_url ) ) { + return new WP_Error( 'woocommerce_product_csv_importer_upload_file_invalid', __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); + } + + return ABSPATH . $file_url; + } + // phpcs:enable + + return new WP_Error( 'woocommerce_product_csv_importer_upload_invalid_file', __( 'Please upload or provide the link to a valid CSV file.', 'woocommerce' ) ); + } + + /** + * Mapping step. + */ + protected function mapping_form() { + check_admin_referer( 'woocommerce-csv-importer' ); + $args = array( + 'lines' => 1, + 'delimiter' => $this->delimiter, + ); + + $importer = self::get_importer( $this->file, $args ); + $headers = $importer->get_raw_keys(); + $mapped_items = $this->auto_map_columns( $headers ); + $sample = current( $importer->get_raw_data() ); + + if ( empty( $sample ) ) { + $this->add_error( + __( 'The file is empty or using a different encoding than UTF-8, please try again with a new file.', 'woocommerce' ), + array( + array( + 'url' => admin_url( 'edit.php?post_type=product&page=product_importer' ), + 'label' => __( 'Upload a new file', 'woocommerce' ), + ), + ) + ); + + // Force output the errors in the same page. + $this->output_errors(); + return; + } + + include_once dirname( __FILE__ ) . '/views/html-csv-import-mapping.php'; + } + + /** + * Import the file if it exists and is valid. + */ + public function import() { + // Displaying this page triggers Ajax action to run the import with a valid nonce, + // therefore this page needs to be nonce protected as well. + check_admin_referer( 'woocommerce-csv-importer' ); + + if ( ! self::is_file_valid_csv( $this->file ) ) { + $this->add_error( __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); + $this->output_errors(); + return; + } + + if ( ! is_file( $this->file ) ) { + $this->add_error( __( 'The file does not exist, please try again.', 'woocommerce' ) ); + $this->output_errors(); + return; + } + + if ( ! empty( $_POST['map_from'] ) && ! empty( $_POST['map_to'] ) ) { + $mapping_from = wc_clean( wp_unslash( $_POST['map_from'] ) ); + $mapping_to = wc_clean( wp_unslash( $_POST['map_to'] ) ); + + // Save mapping preferences for future imports. + update_user_option( get_current_user_id(), 'woocommerce_product_import_mapping', $mapping_to ); + } else { + wp_redirect( esc_url_raw( $this->get_next_step_link( 'upload' ) ) ); + exit; + } + + wp_localize_script( + 'wc-product-import', + 'wc_product_import_params', + array( + 'import_nonce' => wp_create_nonce( 'wc-product-import' ), + 'mapping' => array( + 'from' => $mapping_from, + 'to' => $mapping_to, + ), + 'file' => $this->file, + 'update_existing' => $this->update_existing, + 'delimiter' => $this->delimiter, + ) + ); + wp_enqueue_script( 'wc-product-import' ); + + include_once dirname( __FILE__ ) . '/views/html-csv-import-progress.php'; + } + + /** + * Done step. + */ + protected function done() { + check_admin_referer( 'woocommerce-csv-importer' ); + $imported = isset( $_GET['products-imported'] ) ? absint( $_GET['products-imported'] ) : 0; + $updated = isset( $_GET['products-updated'] ) ? absint( $_GET['products-updated'] ) : 0; + $failed = isset( $_GET['products-failed'] ) ? absint( $_GET['products-failed'] ) : 0; + $skipped = isset( $_GET['products-skipped'] ) ? absint( $_GET['products-skipped'] ) : 0; + $file_name = isset( $_GET['file-name'] ) ? sanitize_text_field( wp_unslash( $_GET['file-name'] ) ) : ''; + $errors = array_filter( (array) get_user_option( 'product_import_error_log' ) ); + + include_once dirname( __FILE__ ) . '/views/html-csv-import-done.php'; + } + + /** + * Columns to normalize. + * + * @param array $columns List of columns names and keys. + * @return array + */ + protected function normalize_columns_names( $columns ) { + $normalized = array(); + + foreach ( $columns as $key => $value ) { + $normalized[ strtolower( $key ) ] = $value; + } + + return $normalized; + } + + /** + * Auto map column names. + * + * @param array $raw_headers Raw header columns. + * @param bool $num_indexes If should use numbers or raw header columns as indexes. + * @return array + */ + protected function auto_map_columns( $raw_headers, $num_indexes = true ) { + $weight_unit = get_option( 'woocommerce_weight_unit' ); + $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + + /* + * @hooked wc_importer_generic_mappings - 10 + * @hooked wc_importer_wordpress_mappings - 10 + * @hooked wc_importer_default_english_mappings - 100 + */ + $default_columns = $this->normalize_columns_names( + apply_filters( + 'woocommerce_csv_product_import_mapping_default_columns', + array( + __( 'ID', 'woocommerce' ) => 'id', + __( 'Type', 'woocommerce' ) => 'type', + __( 'SKU', 'woocommerce' ) => 'sku', + __( 'Name', 'woocommerce' ) => 'name', + __( 'Published', 'woocommerce' ) => 'published', + __( 'Is featured?', 'woocommerce' ) => 'featured', + __( 'Visibility in catalog', 'woocommerce' ) => 'catalog_visibility', + __( 'Short description', 'woocommerce' ) => 'short_description', + __( 'Description', 'woocommerce' ) => 'description', + __( 'Date sale price starts', 'woocommerce' ) => 'date_on_sale_from', + __( 'Date sale price ends', 'woocommerce' ) => 'date_on_sale_to', + __( 'Tax status', 'woocommerce' ) => 'tax_status', + __( 'Tax class', 'woocommerce' ) => 'tax_class', + __( 'In stock?', 'woocommerce' ) => 'stock_status', + __( 'Stock', 'woocommerce' ) => 'stock_quantity', + __( 'Backorders allowed?', 'woocommerce' ) => 'backorders', + __( 'Low stock amount', 'woocommerce' ) => 'low_stock_amount', + __( 'Sold individually?', 'woocommerce' ) => 'sold_individually', + /* translators: %s: Weight unit */ + sprintf( __( 'Weight (%s)', 'woocommerce' ), $weight_unit ) => 'weight', + /* translators: %s: Length unit */ + sprintf( __( 'Length (%s)', 'woocommerce' ), $dimension_unit ) => 'length', + /* translators: %s: Width unit */ + sprintf( __( 'Width (%s)', 'woocommerce' ), $dimension_unit ) => 'width', + /* translators: %s: Height unit */ + sprintf( __( 'Height (%s)', 'woocommerce' ), $dimension_unit ) => 'height', + __( 'Allow customer reviews?', 'woocommerce' ) => 'reviews_allowed', + __( 'Purchase note', 'woocommerce' ) => 'purchase_note', + __( 'Sale price', 'woocommerce' ) => 'sale_price', + __( 'Regular price', 'woocommerce' ) => 'regular_price', + __( 'Categories', 'woocommerce' ) => 'category_ids', + __( 'Tags', 'woocommerce' ) => 'tag_ids', + __( 'Shipping class', 'woocommerce' ) => 'shipping_class_id', + __( 'Images', 'woocommerce' ) => 'images', + __( 'Download limit', 'woocommerce' ) => 'download_limit', + __( 'Download expiry days', 'woocommerce' ) => 'download_expiry', + __( 'Parent', 'woocommerce' ) => 'parent_id', + __( 'Upsells', 'woocommerce' ) => 'upsell_ids', + __( 'Cross-sells', 'woocommerce' ) => 'cross_sell_ids', + __( 'Grouped products', 'woocommerce' ) => 'grouped_products', + __( 'External URL', 'woocommerce' ) => 'product_url', + __( 'Button text', 'woocommerce' ) => 'button_text', + __( 'Position', 'woocommerce' ) => 'menu_order', + ), + $raw_headers + ) + ); + + $special_columns = $this->get_special_columns( + $this->normalize_columns_names( + apply_filters( + 'woocommerce_csv_product_import_mapping_special_columns', + array( + /* translators: %d: Attribute number */ + __( 'Attribute %d name', 'woocommerce' ) => 'attributes:name', + /* translators: %d: Attribute number */ + __( 'Attribute %d value(s)', 'woocommerce' ) => 'attributes:value', + /* translators: %d: Attribute number */ + __( 'Attribute %d visible', 'woocommerce' ) => 'attributes:visible', + /* translators: %d: Attribute number */ + __( 'Attribute %d global', 'woocommerce' ) => 'attributes:taxonomy', + /* translators: %d: Attribute number */ + __( 'Attribute %d default', 'woocommerce' ) => 'attributes:default', + /* translators: %d: Download number */ + __( 'Download %d ID', 'woocommerce' ) => 'downloads:id', + /* translators: %d: Download number */ + __( 'Download %d name', 'woocommerce' ) => 'downloads:name', + /* translators: %d: Download number */ + __( 'Download %d URL', 'woocommerce' ) => 'downloads:url', + /* translators: %d: Meta number */ + __( 'Meta: %s', 'woocommerce' ) => 'meta:', + ), + $raw_headers + ) + ) + ); + + $headers = array(); + foreach ( $raw_headers as $key => $field ) { + $normalized_field = strtolower( $field ); + $index = $num_indexes ? $key : $field; + $headers[ $index ] = $normalized_field; + + if ( isset( $default_columns[ $normalized_field ] ) ) { + $headers[ $index ] = $default_columns[ $normalized_field ]; + } else { + foreach ( $special_columns as $regex => $special_key ) { + // Don't use the normalized field in the regex since meta might be case-sensitive. + if ( preg_match( $regex, $field, $matches ) ) { + $headers[ $index ] = $special_key . $matches[1]; + break; + } + } + } + } + + return apply_filters( 'woocommerce_csv_product_import_mapped_columns', $headers, $raw_headers ); + } + + /** + * Map columns using the user's latest import mappings. + * + * @param array $headers Header columns. + * @return array + */ + public function auto_map_user_preferences( $headers ) { + $mapping_preferences = get_user_option( 'woocommerce_product_import_mapping' ); + + if ( ! empty( $mapping_preferences ) && is_array( $mapping_preferences ) ) { + return $mapping_preferences; + } + + return $headers; + } + + /** + * Sanitize special column name regex. + * + * @param string $value Raw special column name. + * @return string + */ + protected function sanitize_special_column_name_regex( $value ) { + return '/' . str_replace( array( '%d', '%s' ), '(.*)', trim( quotemeta( $value ) ) ) . '/i'; + } + + /** + * Get special columns. + * + * @param array $columns Raw special columns. + * @return array + */ + protected function get_special_columns( $columns ) { + $formatted = array(); + + foreach ( $columns as $key => $value ) { + $regex = $this->sanitize_special_column_name_regex( $key ); + + $formatted[ $regex ] = $value; + } + + return $formatted; + } + + /** + * Get mapping options. + * + * @param string $item Item name. + * @return array + */ + protected function get_mapping_options( $item = '' ) { + // Get index for special column names. + $index = $item; + + if ( preg_match( '/\d+/', $item, $matches ) ) { + $index = $matches[0]; + } + + // Properly format for meta field. + $meta = str_replace( 'meta:', '', $item ); + + // Available options. + $weight_unit = get_option( 'woocommerce_weight_unit' ); + $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $options = array( + 'id' => __( 'ID', 'woocommerce' ), + 'type' => __( 'Type', 'woocommerce' ), + 'sku' => __( 'SKU', 'woocommerce' ), + 'name' => __( 'Name', 'woocommerce' ), + 'published' => __( 'Published', 'woocommerce' ), + 'featured' => __( 'Is featured?', 'woocommerce' ), + 'catalog_visibility' => __( 'Visibility in catalog', 'woocommerce' ), + 'short_description' => __( 'Short description', 'woocommerce' ), + 'description' => __( 'Description', 'woocommerce' ), + 'price' => array( + 'name' => __( 'Price', 'woocommerce' ), + 'options' => array( + 'regular_price' => __( 'Regular price', 'woocommerce' ), + 'sale_price' => __( 'Sale price', 'woocommerce' ), + 'date_on_sale_from' => __( 'Date sale price starts', 'woocommerce' ), + 'date_on_sale_to' => __( 'Date sale price ends', 'woocommerce' ), + ), + ), + 'tax_status' => __( 'Tax status', 'woocommerce' ), + 'tax_class' => __( 'Tax class', 'woocommerce' ), + 'stock_status' => __( 'In stock?', 'woocommerce' ), + 'stock_quantity' => _x( 'Stock', 'Quantity in stock', 'woocommerce' ), + 'backorders' => __( 'Backorders allowed?', 'woocommerce' ), + 'low_stock_amount' => __( 'Low stock amount', 'woocommerce' ), + 'sold_individually' => __( 'Sold individually?', 'woocommerce' ), + /* translators: %s: weight unit */ + 'weight' => sprintf( __( 'Weight (%s)', 'woocommerce' ), $weight_unit ), + 'dimensions' => array( + 'name' => __( 'Dimensions', 'woocommerce' ), + 'options' => array( + /* translators: %s: dimension unit */ + 'length' => sprintf( __( 'Length (%s)', 'woocommerce' ), $dimension_unit ), + /* translators: %s: dimension unit */ + 'width' => sprintf( __( 'Width (%s)', 'woocommerce' ), $dimension_unit ), + /* translators: %s: dimension unit */ + 'height' => sprintf( __( 'Height (%s)', 'woocommerce' ), $dimension_unit ), + ), + ), + 'category_ids' => __( 'Categories', 'woocommerce' ), + 'tag_ids' => __( 'Tags (comma separated)', 'woocommerce' ), + 'tag_ids_spaces' => __( 'Tags (space separated)', 'woocommerce' ), + 'shipping_class_id' => __( 'Shipping class', 'woocommerce' ), + 'images' => __( 'Images', 'woocommerce' ), + 'parent_id' => __( 'Parent', 'woocommerce' ), + 'upsell_ids' => __( 'Upsells', 'woocommerce' ), + 'cross_sell_ids' => __( 'Cross-sells', 'woocommerce' ), + 'grouped_products' => __( 'Grouped products', 'woocommerce' ), + 'external' => array( + 'name' => __( 'External product', 'woocommerce' ), + 'options' => array( + 'product_url' => __( 'External URL', 'woocommerce' ), + 'button_text' => __( 'Button text', 'woocommerce' ), + ), + ), + 'downloads' => array( + 'name' => __( 'Downloads', 'woocommerce' ), + 'options' => array( + 'downloads:id' . $index => __( 'Download ID', 'woocommerce' ), + 'downloads:name' . $index => __( 'Download name', 'woocommerce' ), + 'downloads:url' . $index => __( 'Download URL', 'woocommerce' ), + 'download_limit' => __( 'Download limit', 'woocommerce' ), + 'download_expiry' => __( 'Download expiry days', 'woocommerce' ), + ), + ), + 'attributes' => array( + 'name' => __( 'Attributes', 'woocommerce' ), + 'options' => array( + 'attributes:name' . $index => __( 'Attribute name', 'woocommerce' ), + 'attributes:value' . $index => __( 'Attribute value(s)', 'woocommerce' ), + 'attributes:taxonomy' . $index => __( 'Is a global attribute?', 'woocommerce' ), + 'attributes:visible' . $index => __( 'Attribute visibility', 'woocommerce' ), + 'attributes:default' . $index => __( 'Default attribute', 'woocommerce' ), + ), + ), + 'reviews_allowed' => __( 'Allow customer reviews?', 'woocommerce' ), + 'purchase_note' => __( 'Purchase note', 'woocommerce' ), + 'meta:' . $meta => __( 'Import as meta data', 'woocommerce' ), + 'menu_order' => __( 'Position', 'woocommerce' ), + ); + + return apply_filters( 'woocommerce_csv_product_import_mapping_options', $options, $item ); + } +} diff --git a/plugins/woocommerce/includes/admin/importers/class-wc-tax-rate-importer.php b/plugins/woocommerce/includes/admin/importers/class-wc-tax-rate-importer.php new file mode 100644 index 00000000000..54fd048c8ee --- /dev/null +++ b/plugins/woocommerce/includes/admin/importers/class-wc-tax-rate-importer.php @@ -0,0 +1,368 @@ +import_page = 'woocommerce_tax_rate_csv'; + $this->delimiter = empty( $_POST['delimiter'] ) ? ',' : (string) wc_clean( wp_unslash( $_POST['delimiter'] ) ); // WPCS: CSRF ok. + } + + /** + * Registered callback function for the WordPress Importer. + * + * Manages the three separate stages of the CSV import process. + */ + public function dispatch() { + + $this->header(); + + $step = empty( $_GET['step'] ) ? 0 : (int) $_GET['step']; + + switch ( $step ) { + + case 0: + $this->greet(); + break; + + case 1: + check_admin_referer( 'import-upload' ); + + if ( $this->handle_upload() ) { + + if ( $this->id ) { + $file = get_attached_file( $this->id ); + } else { + $file = ABSPATH . $this->file_url; + } + + add_filter( 'http_request_timeout', array( $this, 'bump_request_timeout' ) ); + + $this->import( $file ); + } else { + $this->import_error( $this->import_error_message ); + } + break; + } + + $this->footer(); + } + + /** + * Import is starting. + */ + private function import_start() { + if ( function_exists( 'gc_enable' ) ) { + gc_enable(); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.gc_enableFound + } + wc_set_time_limit( 0 ); + @ob_flush(); + @flush(); + @ini_set( 'auto_detect_line_endings', '1' ); + } + + /** + * UTF-8 encode the data if `$enc` value isn't UTF-8. + * + * @param mixed $data Data. + * @param string $enc Encoding. + * @return string + */ + public function format_data_from_csv( $data, $enc ) { + return ( 'UTF-8' === $enc ) ? $data : utf8_encode( $data ); + } + + /** + * Import the file if it exists and is valid. + * + * @param mixed $file File. + */ + public function import( $file ) { + if ( ! is_file( $file ) ) { + $this->import_error( __( 'The file does not exist, please try again.', 'woocommerce' ) ); + } + + $this->import_start(); + + $loop = 0; + $handle = fopen( $file, 'r' ); + + if ( false !== $handle ) { + + $header = fgetcsv( $handle, 0, $this->delimiter ); + + if ( 10 === count( $header ) ) { + + $row = fgetcsv( $handle, 0, $this->delimiter ); + + while ( false !== $row ) { + + list( $country, $state, $postcode, $city, $rate, $name, $priority, $compound, $shipping, $class ) = $row; + + $tax_rate = array( + 'tax_rate_country' => $country, + 'tax_rate_state' => $state, + 'tax_rate' => $rate, + 'tax_rate_name' => $name, + 'tax_rate_priority' => $priority, + 'tax_rate_compound' => $compound ? 1 : 0, + 'tax_rate_shipping' => $shipping ? 1 : 0, + 'tax_rate_order' => $loop ++, + 'tax_rate_class' => $class, + ); + + $tax_rate_id = WC_Tax::_insert_tax_rate( $tax_rate ); + WC_Tax::_update_tax_rate_postcodes( $tax_rate_id, wc_clean( $postcode ) ); + WC_Tax::_update_tax_rate_cities( $tax_rate_id, wc_clean( $city ) ); + + $row = fgetcsv( $handle, 0, $this->delimiter ); + } + } else { + $this->import_error( __( 'The CSV is invalid.', 'woocommerce' ) ); + } + + fclose( $handle ); + } + + // Show Result. + echo '

    '; + printf( + /* translators: %s: tax rates count */ + esc_html__( 'Import complete - imported %s tax rates.', 'woocommerce' ), + '' . absint( $loop ) . '' + ); + echo '

    '; + + $this->import_end(); + } + + /** + * Performs post-import cleanup of files and the cache. + */ + public function import_end() { + echo '

    ' . esc_html__( 'All done!', 'woocommerce' ) . ' ' . esc_html__( 'View tax rates', 'woocommerce' ) . '

    '; + + do_action( 'import_end' ); + } + + /** + * Set the import error message. + * + * @param string $message Error message. + */ + protected function set_import_error_message( $message ) { + $this->import_error_message = $message; + } + + /** + * Handles the CSV upload and initial parsing of the file to prepare for. + * displaying author import options. + * + * @return bool False if error uploading or invalid file, true otherwise + */ + public function handle_upload() { + $file_url = isset( $_POST['file_url'] ) ? wc_clean( wp_unslash( $_POST['file_url'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce already verified in WC_Tax_Rate_Importer::dispatch() + + if ( empty( $file_url ) ) { + $file = wp_import_handle_upload(); + + if ( isset( $file['error'] ) ) { + $this->set_import_error_message( $file['error'] ); + + return false; + } + + if ( ! wc_is_file_valid_csv( $file['file'], false ) ) { + // Remove file if not valid. + wp_delete_attachment( $file['id'], true ); + + $this->set_import_error_message( __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); + + return false; + } + + $this->id = absint( $file['id'] ); + } elseif ( + ( 0 === stripos( realpath( ABSPATH . $file_url ), ABSPATH ) ) && + file_exists( ABSPATH . $file_url ) + ) { + if ( ! wc_is_file_valid_csv( ABSPATH . $file_url ) ) { + $this->set_import_error_message( __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); + + return false; + } + + $this->file_url = esc_attr( $file_url ); + } else { + return false; + } + + return true; + } + + /** + * Output header html. + */ + public function header() { + echo '
    '; + echo '

    ' . esc_html__( 'Import tax rates', 'woocommerce' ) . '

    '; + } + + /** + * Output footer html. + */ + public function footer() { + echo '
    '; + } + + /** + * Output information about the uploading process. + */ + public function greet() { + + echo '
    '; + echo '

    ' . esc_html__( 'Hi there! Upload a CSV file containing tax rates to import the contents into your shop. Choose a .csv file to upload, then click "Upload file and import".', 'woocommerce' ) . '

    '; + + /* translators: 1: Link to tax rates sample file 2: Closing link. */ + echo '

    ' . sprintf( esc_html__( 'Your CSV needs to include columns in a specific order. %1$sClick here to download a sample%2$s.', 'woocommerce' ), '', '' ) . '

    '; + + $action = 'admin.php?import=woocommerce_tax_rate_csv&step=1'; + + $bytes = apply_filters( 'import_upload_size_limit', wp_max_upload_size() ); + $size = size_format( $bytes ); + $upload_dir = wp_upload_dir(); + if ( ! empty( $upload_dir['error'] ) ) : + ?> +
    +

    +

    +
    + +
    + + + + + + + + + + + + + + + +
    + + + + + + + + +
    + + + +

    +

    + +

    +
    + '; + } + + /** + * Show import error and quit. + * + * @param string $message Error message. + */ + private function import_error( $message = '' ) { + echo '

    ' . esc_html__( 'Sorry, there has been an error.', 'woocommerce' ) . '
    '; + if ( $message ) { + echo esc_html( $message ); + } + echo '

    '; + $this->footer(); + die(); + } + + /** + * Added to http_request_timeout filter to force timeout at 60 seconds during import. + * + * @param int $val Value. + * @return int 60 + */ + public function bump_request_timeout( $val ) { + return 60; + } +} diff --git a/plugins/woocommerce/includes/admin/importers/mappings/default.php b/plugins/woocommerce/includes/admin/importers/mappings/default.php new file mode 100644 index 00000000000..0d9599da04c --- /dev/null +++ b/plugins/woocommerce/includes/admin/importers/mappings/default.php @@ -0,0 +1,113 @@ + 'id', + 'Type' => 'type', + 'SKU' => 'sku', + 'Name' => 'name', + 'Published' => 'published', + 'Is featured?' => 'featured', + 'Visibility in catalog' => 'catalog_visibility', + 'Short description' => 'short_description', + 'Description' => 'description', + 'Date sale price starts' => 'date_on_sale_from', + 'Date sale price ends' => 'date_on_sale_to', + 'Tax status' => 'tax_status', + 'Tax class' => 'tax_class', + 'In stock?' => 'stock_status', + 'Stock' => 'stock_quantity', + 'Backorders allowed?' => 'backorders', + 'Low stock amount' => 'low_stock_amount', + 'Sold individually?' => 'sold_individually', + sprintf( 'Weight (%s)', $weight_unit ) => 'weight', + sprintf( 'Length (%s)', $dimension_unit ) => 'length', + sprintf( 'Width (%s)', $dimension_unit ) => 'width', + sprintf( 'Height (%s)', $dimension_unit ) => 'height', + 'Allow customer reviews?' => 'reviews_allowed', + 'Purchase note' => 'purchase_note', + 'Sale price' => 'sale_price', + 'Regular price' => 'regular_price', + 'Categories' => 'category_ids', + 'Tags' => 'tag_ids', + 'Shipping class' => 'shipping_class_id', + 'Images' => 'images', + 'Download limit' => 'download_limit', + 'Download expiry days' => 'download_expiry', + 'Parent' => 'parent_id', + 'Upsells' => 'upsell_ids', + 'Cross-sells' => 'cross_sell_ids', + 'Grouped products' => 'grouped_products', + 'External URL' => 'product_url', + 'Button text' => 'button_text', + 'Position' => 'menu_order', + ); + + return array_merge( $mappings, $new_mappings ); +} +add_filter( 'woocommerce_csv_product_import_mapping_default_columns', 'wc_importer_default_english_mappings', 100 ); + +/** + * Add English special mapping placeholders when not using English as current language. + * + * @since 3.1.0 + * @param array $mappings Importer columns mappings. + * @return array + */ +function wc_importer_default_special_english_mappings( $mappings ) { + if ( 'en_US' === wc_importer_current_locale() ) { + return $mappings; + } + + $new_mappings = array( + 'Attribute %d name' => 'attributes:name', + 'Attribute %d value(s)' => 'attributes:value', + 'Attribute %d visible' => 'attributes:visible', + 'Attribute %d global' => 'attributes:taxonomy', + 'Attribute %d default' => 'attributes:default', + 'Download %d ID' => 'downloads:id', + 'Download %d name' => 'downloads:name', + 'Download %d URL' => 'downloads:url', + 'Meta: %s' => 'meta:', + ); + + return array_merge( $mappings, $new_mappings ); +} +add_filter( 'woocommerce_csv_product_import_mapping_special_columns', 'wc_importer_default_special_english_mappings', 100 ); diff --git a/includes/admin/importers/mappings/generic.php b/plugins/woocommerce/includes/admin/importers/mappings/generic.php similarity index 100% rename from includes/admin/importers/mappings/generic.php rename to plugins/woocommerce/includes/admin/importers/mappings/generic.php diff --git a/includes/admin/importers/mappings/mappings.php b/plugins/woocommerce/includes/admin/importers/mappings/mappings.php similarity index 100% rename from includes/admin/importers/mappings/mappings.php rename to plugins/woocommerce/includes/admin/importers/mappings/mappings.php diff --git a/includes/admin/importers/mappings/shopify.php b/plugins/woocommerce/includes/admin/importers/mappings/shopify.php similarity index 100% rename from includes/admin/importers/mappings/shopify.php rename to plugins/woocommerce/includes/admin/importers/mappings/shopify.php diff --git a/includes/admin/importers/mappings/wordpress.php b/plugins/woocommerce/includes/admin/importers/mappings/wordpress.php similarity index 100% rename from includes/admin/importers/mappings/wordpress.php rename to plugins/woocommerce/includes/admin/importers/mappings/wordpress.php diff --git a/plugins/woocommerce/includes/admin/importers/views/html-csv-import-done.php b/plugins/woocommerce/includes/admin/importers/views/html-csv-import-done.php new file mode 100644 index 00000000000..3abb51c3a76 --- /dev/null +++ b/plugins/woocommerce/includes/admin/importers/views/html-csv-import-done.php @@ -0,0 +1,104 @@ + +
    +
    + ' . number_format_i18n( $imported ) . '' + ); + } + + if ( 0 < $updated ) { + $results[] = sprintf( + /* translators: %d: products count */ + _n( '%s product updated', '%s products updated', $updated, 'woocommerce' ), + '' . number_format_i18n( $updated ) . '' + ); + } + + if ( 0 < $skipped ) { + $results[] = sprintf( + /* translators: %d: products count */ + _n( '%s product was skipped', '%s products were skipped', $skipped, 'woocommerce' ), + '' . number_format_i18n( $skipped ) . '' + ); + } + + if ( 0 < $failed ) { + $results [] = sprintf( + /* translators: %d: products count */ + _n( 'Failed to import %s product', 'Failed to import %s products', $failed, 'woocommerce' ), + '' . number_format_i18n( $failed ) . '' + ); + } + + if ( 0 < $failed || 0 < $skipped ) { + $results[] = '' . __( 'View import log', 'woocommerce' ) . ''; + } + + if ( ! empty( $file_name ) ) { + $results[] = sprintf( + /* translators: %s: File name */ + __( 'File uploaded: %s', 'woocommerce' ), + '' . $file_name . '' + ); + } + + /* translators: %d: import results */ + echo wp_kses_post( __( 'Import complete!', 'woocommerce' ) . ' ' . implode( '. ', $results ) ); + ?> +
    + + +
    + +
    +
    diff --git a/includes/admin/importers/views/html-csv-import-footer.php b/plugins/woocommerce/includes/admin/importers/views/html-csv-import-footer.php similarity index 100% rename from includes/admin/importers/views/html-csv-import-footer.php rename to plugins/woocommerce/includes/admin/importers/views/html-csv-import-footer.php diff --git a/includes/admin/importers/views/html-csv-import-header.php b/plugins/woocommerce/includes/admin/importers/views/html-csv-import-header.php similarity index 100% rename from includes/admin/importers/views/html-csv-import-header.php rename to plugins/woocommerce/includes/admin/importers/views/html-csv-import-header.php diff --git a/includes/admin/importers/views/html-csv-import-mapping.php b/plugins/woocommerce/includes/admin/importers/views/html-csv-import-mapping.php similarity index 100% rename from includes/admin/importers/views/html-csv-import-mapping.php rename to plugins/woocommerce/includes/admin/importers/views/html-csv-import-mapping.php diff --git a/includes/admin/importers/views/html-csv-import-progress.php b/plugins/woocommerce/includes/admin/importers/views/html-csv-import-progress.php similarity index 100% rename from includes/admin/importers/views/html-csv-import-progress.php rename to plugins/woocommerce/includes/admin/importers/views/html-csv-import-progress.php diff --git a/includes/admin/importers/views/html-csv-import-steps.php b/plugins/woocommerce/includes/admin/importers/views/html-csv-import-steps.php similarity index 100% rename from includes/admin/importers/views/html-csv-import-steps.php rename to plugins/woocommerce/includes/admin/importers/views/html-csv-import-steps.php diff --git a/includes/admin/importers/views/html-product-csv-import-form.php b/plugins/woocommerce/includes/admin/importers/views/html-product-csv-import-form.php similarity index 100% rename from includes/admin/importers/views/html-product-csv-import-form.php rename to plugins/woocommerce/includes/admin/importers/views/html-product-csv-import-form.php diff --git a/includes/admin/list-tables/abstract-class-wc-admin-list-table.php b/plugins/woocommerce/includes/admin/list-tables/abstract-class-wc-admin-list-table.php similarity index 100% rename from includes/admin/list-tables/abstract-class-wc-admin-list-table.php rename to plugins/woocommerce/includes/admin/list-tables/abstract-class-wc-admin-list-table.php diff --git a/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-coupons.php b/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-coupons.php new file mode 100644 index 00000000000..c5398e542b8 --- /dev/null +++ b/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-coupons.php @@ -0,0 +1,232 @@ +'; + echo '

    ' . esc_html__( 'Coupons are a great way to offer discounts and rewards to your customers. They will appear here once created.', 'woocommerce' ) . '

    '; + echo '' . esc_html__( 'Create your first coupon', 'woocommerce' ) . ''; + echo '' . esc_html__( 'Learn more about coupons', 'woocommerce' ) . ''; + echo '
    '; + } + + /** + * Define primary column. + * + * @return string + */ + protected function get_primary_column() { + return 'coupon_code'; + } + + /** + * Get row actions to show in the list table. + * + * @param array $actions Array of actions. + * @param WP_Post $post Current post object. + * @return array + */ + protected function get_row_actions( $actions, $post ) { + unset( $actions['inline hide-if-no-js'] ); + return $actions; + } + + /** + * Define which columns to show on this screen. + * + * @param array $columns Existing columns. + * @return array + */ + public function define_columns( $columns ) { + $show_columns = array(); + $show_columns['cb'] = $columns['cb']; + $show_columns['coupon_code'] = __( 'Code', 'woocommerce' ); + $show_columns['type'] = __( 'Coupon type', 'woocommerce' ); + $show_columns['amount'] = __( 'Coupon amount', 'woocommerce' ); + $show_columns['description'] = __( 'Description', 'woocommerce' ); + $show_columns['products'] = __( 'Product IDs', 'woocommerce' ); + $show_columns['usage'] = __( 'Usage / Limit', 'woocommerce' ); + $show_columns['expiry_date'] = __( 'Expiry date', 'woocommerce' ); + + return $show_columns; + } + + /** + * Pre-fetch any data for the row each column has access to it. the_coupon global is there for bw compat. + * + * @param int $post_id Post ID being shown. + */ + protected function prepare_row_data( $post_id ) { + global $the_coupon; + + if ( empty( $this->object ) || $this->object->get_id() !== $post_id ) { + $this->object = new WC_Coupon( $post_id ); + $the_coupon = $this->object; + } + } + + /** + * Render column: coupon_code. + */ + protected function render_coupon_code_column() { + global $post; + + $edit_link = get_edit_post_link( $this->object->get_id() ); + $title = $this->object->get_code(); + + echo '' . esc_html( $title ) . ''; + _post_states( $post ); + echo ''; + } + + /** + * Render column: type. + */ + protected function render_type_column() { + echo esc_html( wc_get_coupon_type( $this->object->get_discount_type() ) ); + } + + /** + * Render column: amount. + */ + protected function render_amount_column() { + echo esc_html( wc_format_localized_price( $this->object->get_amount() ) ); + } + /** + * Render column: products. + */ + protected function render_products_column() { + $product_ids = $this->object->get_product_ids(); + + if ( count( $product_ids ) > 0 ) { + echo esc_html( implode( ', ', $product_ids ) ); + } else { + echo '–'; + } + } + + /** + * Render column: usage_limit. + */ + protected function render_usage_limit_column() { + $usage_limit = $this->object->get_usage_limit(); + + if ( $usage_limit ) { + echo esc_html( $usage_limit ); + } else { + echo '–'; + } + } + + /** + * Render column: usage. + */ + protected function render_usage_column() { + $usage_count = $this->object->get_usage_count(); + $usage_limit = $this->object->get_usage_limit(); + + printf( + /* translators: 1: count 2: limit */ + __( '%1$s / %2$s', 'woocommerce' ), + esc_html( $usage_count ), + $usage_limit ? esc_html( $usage_limit ) : '∞' + ); + } + + /** + * Render column: expiry_date. + */ + protected function render_expiry_date_column() { + $expiry_date = $this->object->get_date_expires(); + + if ( $expiry_date ) { + echo esc_html( $expiry_date->date_i18n( 'F j, Y' ) ); + } else { + echo '–'; + } + } + + /** + * Render column: description. + */ + protected function render_description_column() { + echo wp_kses_post( $this->object->get_description() ? $this->object->get_description() : '–' ); + } + + /** + * Render any custom filters and search inputs for the list table. + */ + protected function render_filters() { + ?> + + '; + + echo '

    ' . esc_html__( 'When you receive a new order, it will appear here.', 'woocommerce' ) . '

    '; + + echo ''; + + do_action( 'wc_marketplace_suggestions_orders_empty_state' ); + + echo ''; + } + + /** + * Define primary column. + * + * @return string + */ + protected function get_primary_column() { + return 'order_number'; + } + + /** + * Get row actions to show in the list table. + * + * @param array $actions Array of actions. + * @param WP_Post $post Current post object. + * @return array + */ + protected function get_row_actions( $actions, $post ) { + return array(); + } + + /** + * Define hidden columns. + * + * @return array + */ + protected function define_hidden_columns() { + return array( + 'shipping_address', + 'billing_address', + 'wc_actions', + ); + } + + /** + * Define which columns are sortable. + * + * @param array $columns Existing columns. + * @return array + */ + public function define_sortable_columns( $columns ) { + $custom = array( + 'order_number' => 'ID', + 'order_total' => 'order_total', + 'order_date' => 'date', + ); + unset( $columns['comments'] ); + + return wp_parse_args( $custom, $columns ); + } + + /** + * Define which columns to show on this screen. + * + * @param array $columns Existing columns. + * @return array + */ + public function define_columns( $columns ) { + $show_columns = array(); + $show_columns['cb'] = $columns['cb']; + $show_columns['order_number'] = __( 'Order', 'woocommerce' ); + $show_columns['order_date'] = __( 'Date', 'woocommerce' ); + $show_columns['order_status'] = __( 'Status', 'woocommerce' ); + $show_columns['billing_address'] = __( 'Billing', 'woocommerce' ); + $show_columns['shipping_address'] = __( 'Ship to', 'woocommerce' ); + $show_columns['order_total'] = __( 'Total', 'woocommerce' ); + $show_columns['wc_actions'] = __( 'Actions', 'woocommerce' ); + + wp_enqueue_script( 'wc-orders' ); + + return $show_columns; + } + + /** + * Define bulk actions. + * + * @param array $actions Existing actions. + * @return array + */ + public function define_bulk_actions( $actions ) { + if ( isset( $actions['edit'] ) ) { + unset( $actions['edit'] ); + } + + $actions['mark_processing'] = __( 'Change status to processing', 'woocommerce' ); + $actions['mark_on-hold'] = __( 'Change status to on-hold', 'woocommerce' ); + $actions['mark_completed'] = __( 'Change status to completed', 'woocommerce' ); + $actions['mark_cancelled'] = __( 'Change status to cancelled', 'woocommerce' ); + + if ( wc_string_to_bool( get_option( 'woocommerce_allow_bulk_remove_personal_data', 'no' ) ) ) { + $actions['remove_personal_data'] = __( 'Remove personal data', 'woocommerce' ); + } + + return $actions; + } + + /** + * Pre-fetch any data for the row each column has access to it. the_order global is there for bw compat. + * + * @param int $post_id Post ID being shown. + */ + protected function prepare_row_data( $post_id ) { + global $the_order; + + if ( empty( $this->object ) || $this->object->get_id() !== $post_id ) { + $this->object = wc_get_order( $post_id ); + $the_order = $this->object; + } + } + + /** + * Render column: order_number. + */ + protected function render_order_number_column() { + $buyer = ''; + + if ( $this->object->get_billing_first_name() || $this->object->get_billing_last_name() ) { + /* translators: 1: first name 2: last name */ + $buyer = trim( sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $this->object->get_billing_first_name(), $this->object->get_billing_last_name() ) ); + } elseif ( $this->object->get_billing_company() ) { + $buyer = trim( $this->object->get_billing_company() ); + } elseif ( $this->object->get_customer_id() ) { + $user = get_user_by( 'id', $this->object->get_customer_id() ); + $buyer = ucwords( $user->display_name ); + } + + /** + * Filter buyer name in list table orders. + * + * @since 3.7.0 + * @param string $buyer Buyer name. + * @param WC_Order $order Order data. + */ + $buyer = apply_filters( 'woocommerce_admin_order_buyer_name', $buyer, $this->object ); + + if ( $this->object->get_status() === 'trash' ) { + echo '#' . esc_attr( $this->object->get_order_number() ) . ' ' . esc_html( $buyer ) . ''; + } else { + echo '' . esc_html( __( 'Preview', 'woocommerce' ) ) . ''; + echo '#' . esc_attr( $this->object->get_order_number() ) . ' ' . esc_html( $buyer ) . ''; + } + } + + /** + * Render column: order_status. + */ + protected function render_order_status_column() { + $tooltip = ''; + $comment_count = get_comment_count( $this->object->get_id() ); + $approved_comments_count = absint( $comment_count['approved'] ); + + if ( $approved_comments_count ) { + $latest_notes = wc_get_order_notes( + array( + 'order_id' => $this->object->get_id(), + 'limit' => 1, + 'orderby' => 'date_created_gmt', + ) + ); + + $latest_note = current( $latest_notes ); + + if ( isset( $latest_note->content ) && 1 === $approved_comments_count ) { + $tooltip = wc_sanitize_tooltip( $latest_note->content ); + } elseif ( isset( $latest_note->content ) ) { + /* translators: %d: notes count */ + $tooltip = wc_sanitize_tooltip( $latest_note->content . '
    ' . sprintf( _n( 'Plus %d other note', 'Plus %d other notes', ( $approved_comments_count - 1 ), 'woocommerce' ), $approved_comments_count - 1 ) . '' ); + } else { + /* translators: %d: notes count */ + $tooltip = wc_sanitize_tooltip( sprintf( _n( '%d note', '%d notes', $approved_comments_count, 'woocommerce' ), $approved_comments_count ) ); + } + } + + if ( $tooltip ) { + printf( '%s', esc_attr( sanitize_html_class( 'status-' . $this->object->get_status() ) ), wp_kses_post( $tooltip ), esc_html( wc_get_order_status_name( $this->object->get_status() ) ) ); + } else { + printf( '%s', esc_attr( sanitize_html_class( 'status-' . $this->object->get_status() ) ), esc_html( wc_get_order_status_name( $this->object->get_status() ) ) ); + } + } + + /** + * Render column: order_date. + */ + protected function render_order_date_column() { + $order_timestamp = $this->object->get_date_created() ? $this->object->get_date_created()->getTimestamp() : ''; + + if ( ! $order_timestamp ) { + echo '–'; + return; + } + + // Check if the order was created within the last 24 hours, and not in the future. + if ( $order_timestamp > strtotime( '-1 day', time() ) && $order_timestamp <= time() ) { + $show_date = sprintf( + /* translators: %s: human-readable time difference */ + _x( '%s ago', '%s = human-readable time difference', 'woocommerce' ), + human_time_diff( $this->object->get_date_created()->getTimestamp(), time() ) + ); + } else { + $show_date = $this->object->get_date_created()->date_i18n( apply_filters( 'woocommerce_admin_order_date_format', __( 'M j, Y', 'woocommerce' ) ) ); + } + printf( + '', + esc_attr( $this->object->get_date_created()->date( 'c' ) ), + esc_html( $this->object->get_date_created()->date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ) ), + esc_html( $show_date ) + ); + } + + /** + * Render column: order_total. + */ + protected function render_order_total_column() { + if ( $this->object->get_payment_method_title() ) { + /* translators: %s: method */ + echo '' . wp_kses_post( $this->object->get_formatted_order_total() ) . ''; + } else { + echo wp_kses_post( $this->object->get_formatted_order_total() ); + } + } + + /** + * Render column: wc_actions. + */ + protected function render_wc_actions_column() { + echo '

    '; + + do_action( 'woocommerce_admin_order_actions_start', $this->object ); + + $actions = array(); + + if ( $this->object->has_status( array( 'pending', 'on-hold' ) ) ) { + $actions['processing'] = array( + 'url' => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=processing&order_id=' . $this->object->get_id() ), 'woocommerce-mark-order-status' ), + 'name' => __( 'Processing', 'woocommerce' ), + 'action' => 'processing', + ); + } + + if ( $this->object->has_status( array( 'pending', 'on-hold', 'processing' ) ) ) { + $actions['complete'] = array( + 'url' => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=completed&order_id=' . $this->object->get_id() ), 'woocommerce-mark-order-status' ), + 'name' => __( 'Complete', 'woocommerce' ), + 'action' => 'complete', + ); + } + + $actions = apply_filters( 'woocommerce_admin_order_actions', $actions, $this->object ); + + echo wc_render_action_buttons( $actions ); // WPCS: XSS ok. + + do_action( 'woocommerce_admin_order_actions_end', $this->object ); + + echo '

    '; + } + + /** + * Render column: billing_address. + */ + protected function render_billing_address_column() { + $address = $this->object->get_formatted_billing_address(); + + if ( $address ) { + echo esc_html( preg_replace( '##i', ', ', $address ) ); + + if ( $this->object->get_payment_method() ) { + /* translators: %s: payment method */ + echo '' . sprintf( __( 'via %s', 'woocommerce' ), esc_html( $this->object->get_payment_method_title() ) ) . ''; // WPCS: XSS ok. + } + } else { + echo '–'; + } + } + + /** + * Render column: shipping_address. + */ + protected function render_shipping_address_column() { + $address = $this->object->get_formatted_shipping_address(); + + if ( $address ) { + echo '' . esc_html( preg_replace( '##i', ', ', $address ) ) . ''; + if ( $this->object->get_shipping_method() ) { + /* translators: %s: shipping method */ + echo '' . sprintf( __( 'via %s', 'woocommerce' ), esc_html( $this->object->get_shipping_method() ) ) . ''; // WPCS: XSS ok. + } + } else { + echo '–'; + } + } + + /** + * Template for order preview. + * + * @since 3.3.0 + */ + public function order_preview_template() { + ?> + + get_items(), $order ); + $columns = apply_filters( + 'woocommerce_admin_order_preview_line_item_columns', + array( + 'product' => __( 'Product', 'woocommerce' ), + 'quantity' => __( 'Quantity', 'woocommerce' ), + 'tax' => __( 'Tax', 'woocommerce' ), + 'total' => __( 'Total', 'woocommerce' ), + ), + $order + ); + + if ( ! wc_tax_enabled() ) { + unset( $columns['tax'] ); + } + + $html = ' +
    + + + '; + + foreach ( $columns as $column => $label ) { + $html .= ''; + } + + $html .= ' + + + '; + + foreach ( $line_items as $item_id => $item ) { + + $product_object = is_callable( array( $item, 'get_product' ) ) ? $item->get_product() : null; + $row_class = apply_filters( 'woocommerce_admin_html_order_preview_item_class', '', $item, $order ); + + $html .= ''; + + foreach ( $columns as $column => $label ) { + $html .= ''; + } + + $html .= ''; + } + + $html .= ' + +
    ' . esc_html( $label ) . '
    '; + switch ( $column ) { + case 'product': + $html .= wp_kses_post( $item->get_name() ); + + if ( $product_object ) { + $html .= '
    ' . esc_html( $product_object->get_sku() ) . '
    '; + } + + $meta_data = $item->get_all_formatted_meta_data( '' ); + + if ( $meta_data ) { + $html .= ''; + + foreach ( $meta_data as $meta_id => $meta ) { + if ( in_array( $meta->key, $hidden_order_itemmeta, true ) ) { + continue; + } + $html .= ''; + } + $html .= '
    ' . wp_kses_post( $meta->display_key ) . ':' . wp_kses_post( force_balance_tags( $meta->display_value ) ) . '
    '; + } + break; + case 'quantity': + $html .= esc_html( $item->get_quantity() ); + break; + case 'tax': + $html .= wc_price( $item->get_total_tax(), array( 'currency' => $order->get_currency() ) ); + break; + case 'total': + $html .= wc_price( $item->get_total(), array( 'currency' => $order->get_currency() ) ); + break; + default: + $html .= apply_filters( 'woocommerce_admin_order_preview_line_item_column_' . sanitize_key( $column ), '', $item, $item_id, $order ); + break; + } + $html .= '
    +
    '; + + return $html; + } + + /** + * Get actions to display in the preview as HTML. + * + * @param WC_Order $order Order object. + * @return string + */ + public static function get_order_preview_actions_html( $order ) { + $actions = array(); + $status_actions = array(); + + if ( $order->has_status( array( 'pending' ) ) ) { + $status_actions['on-hold'] = array( + 'url' => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=on-hold&order_id=' . $order->get_id() ), 'woocommerce-mark-order-status' ), + 'name' => __( 'On-hold', 'woocommerce' ), + 'title' => __( 'Change order status to on-hold', 'woocommerce' ), + 'action' => 'on-hold', + ); + } + + if ( $order->has_status( array( 'pending', 'on-hold' ) ) ) { + $status_actions['processing'] = array( + 'url' => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=processing&order_id=' . $order->get_id() ), 'woocommerce-mark-order-status' ), + 'name' => __( 'Processing', 'woocommerce' ), + 'title' => __( 'Change order status to processing', 'woocommerce' ), + 'action' => 'processing', + ); + } + + if ( $order->has_status( array( 'pending', 'on-hold', 'processing' ) ) ) { + $status_actions['complete'] = array( + 'url' => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=completed&order_id=' . $order->get_id() ), 'woocommerce-mark-order-status' ), + 'name' => __( 'Completed', 'woocommerce' ), + 'title' => __( 'Change order status to completed', 'woocommerce' ), + 'action' => 'complete', + ); + } + + if ( $status_actions ) { + $actions['status'] = array( + 'group' => __( 'Change status: ', 'woocommerce' ), + 'actions' => $status_actions, + ); + } + + return wc_render_action_buttons( apply_filters( 'woocommerce_admin_order_preview_actions', $actions, $order ) ); + } + + /** + * Get order details to send to the ajax endpoint for previews. + * + * @param WC_Order $order Order object. + * @return array + */ + public static function order_preview_get_order_details( $order ) { + if ( ! $order ) { + return array(); + } + + $payment_via = $order->get_payment_method_title(); + $payment_method = $order->get_payment_method(); + $payment_gateways = WC()->payment_gateways() ? WC()->payment_gateways->payment_gateways() : array(); + $transaction_id = $order->get_transaction_id(); + + if ( $transaction_id ) { + + $url = isset( $payment_gateways[ $payment_method ] ) ? $payment_gateways[ $payment_method ]->get_transaction_url( $order ) : false; + + if ( $url ) { + $payment_via .= ' (' . esc_html( $transaction_id ) . ')'; + } else { + $payment_via .= ' (' . esc_html( $transaction_id ) . ')'; + } + } + + $billing_address = $order->get_formatted_billing_address(); + $shipping_address = $order->get_formatted_shipping_address(); + + return apply_filters( + 'woocommerce_admin_order_preview_get_order_details', + array( + 'data' => $order->get_data(), + 'order_number' => $order->get_order_number(), + 'item_html' => self::get_order_preview_item_html( $order ), + 'actions_html' => self::get_order_preview_actions_html( $order ), + 'ship_to_billing' => wc_ship_to_billing_address_only(), + 'needs_shipping' => $order->needs_shipping_address(), + 'formatted_billing_address' => $billing_address ? $billing_address : __( 'N/A', 'woocommerce' ), + 'formatted_shipping_address' => $shipping_address ? $shipping_address : __( 'N/A', 'woocommerce' ), + 'shipping_address_map_url' => $order->get_shipping_address_map_url(), + 'payment_via' => $payment_via, + 'shipping_via' => $order->get_shipping_method(), + 'status' => $order->get_status(), + 'status_name' => wc_get_order_status_name( $order->get_status() ), + ), + $order + ); + } + + /** + * Handle bulk actions. + * + * @param string $redirect_to URL to redirect to. + * @param string $action Action name. + * @param array $ids List of ids. + * @return string + */ + public function handle_bulk_actions( $redirect_to, $action, $ids ) { + $ids = apply_filters( 'woocommerce_bulk_action_ids', array_reverse( array_map( 'absint', $ids ) ), $action, 'order' ); + $changed = 0; + + if ( 'remove_personal_data' === $action ) { + $report_action = 'removed_personal_data'; + + foreach ( $ids as $id ) { + $order = wc_get_order( $id ); + + if ( $order ) { + do_action( 'woocommerce_remove_order_personal_data', $order ); + $changed++; + } + } + } elseif ( false !== strpos( $action, 'mark_' ) ) { + $order_statuses = wc_get_order_statuses(); + $new_status = substr( $action, 5 ); // Get the status name from action. + $report_action = 'marked_' . $new_status; + + // Sanity check: bail out if this is actually not a status, or is not a registered status. + if ( isset( $order_statuses[ 'wc-' . $new_status ] ) ) { + // Initialize payment gateways in case order has hooked status transition actions. + WC()->payment_gateways(); + + foreach ( $ids as $id ) { + $order = wc_get_order( $id ); + $order->update_status( $new_status, __( 'Order status changed by bulk edit:', 'woocommerce' ), true ); + do_action( 'woocommerce_order_edit_status', $id, $new_status ); + $changed++; + } + } + } + + if ( $changed ) { + $redirect_to = add_query_arg( + array( + 'post_type' => $this->list_table_type, + 'bulk_action' => $report_action, + 'changed' => $changed, + 'ids' => join( ',', $ids ), + ), + $redirect_to + ); + } + + return esc_url_raw( $redirect_to ); + } + + /** + * Show confirmation message that order status changed for number of orders. + */ + public function bulk_admin_notices() { + global $post_type, $pagenow; + + // Bail out if not on shop order list page. + if ( 'edit.php' !== $pagenow || 'shop_order' !== $post_type || ! isset( $_REQUEST['bulk_action'] ) ) { // WPCS: input var ok, CSRF ok. + return; + } + + $order_statuses = wc_get_order_statuses(); + $number = isset( $_REQUEST['changed'] ) ? absint( $_REQUEST['changed'] ) : 0; // WPCS: input var ok, CSRF ok. + $bulk_action = wc_clean( wp_unslash( $_REQUEST['bulk_action'] ) ); // WPCS: input var ok, CSRF ok. + + // Check if any status changes happened. + foreach ( $order_statuses as $slug => $name ) { + if ( 'marked_' . str_replace( 'wc-', '', $slug ) === $bulk_action ) { // WPCS: input var ok, CSRF ok. + /* translators: %d: orders count */ + $message = sprintf( _n( '%d order status changed.', '%d order statuses changed.', $number, 'woocommerce' ), number_format_i18n( $number ) ); + echo '

    ' . esc_html( $message ) . '

    '; + break; + } + } + + if ( 'removed_personal_data' === $bulk_action ) { // WPCS: input var ok, CSRF ok. + /* translators: %d: orders count */ + $message = sprintf( _n( 'Removed personal data from %d order.', 'Removed personal data from %d orders.', $number, 'woocommerce' ), number_format_i18n( $number ) ); + echo '

    ' . esc_html( $message ) . '

    '; + } + } + + /** + * See if we should render search filters or not. + */ + public function restrict_manage_posts() { + global $typenow; + + if ( in_array( $typenow, wc_get_order_types( 'order-meta-boxes' ), true ) ) { + $this->render_filters(); + } + } + + /** + * Render any custom filters and search inputs for the list table. + */ + protected function render_filters() { + $user_string = ''; + $user_id = ''; + + if ( ! empty( $_GET['_customer_user'] ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended + $user_id = absint( $_GET['_customer_user'] ); // WPCS: input var ok, sanitization ok. + $user = get_user_by( 'id', $user_id ); + + $user_string = sprintf( + /* translators: 1: user display name 2: user ID 3: user email */ + esc_html__( '%1$s (#%2$s – %3$s)', 'woocommerce' ), + $user->display_name, + absint( $user->ID ), + $user->user_email + ); + } + ?> + + query_filters( $query_vars ); + } + + return $query_vars; + } + + /** + * Handle any custom filters. + * + * @param array $query_vars Query vars. + * @return array + */ + protected function query_filters( $query_vars ) { + global $wp_post_statuses; + + // Filter the orders by the posted customer. + if ( ! empty( $_GET['_customer_user'] ) ) { // WPCS: input var ok. + // @codingStandardsIgnoreStart. + $query_vars['meta_query'] = array( + array( + 'key' => '_customer_user', + 'value' => (int) $_GET['_customer_user'], // WPCS: input var ok, sanitization ok. + 'compare' => '=', + ), + ); + // @codingStandardsIgnoreEnd + } + + // Sorting. + if ( isset( $query_vars['orderby'] ) ) { + if ( 'order_total' === $query_vars['orderby'] ) { + // @codingStandardsIgnoreStart + $query_vars = array_merge( $query_vars, array( + 'meta_key' => '_order_total', + 'orderby' => 'meta_value_num', + ) ); + // @codingStandardsIgnoreEnd + } + } + + // Status. + if ( empty( $query_vars['post_status'] ) ) { + $post_statuses = wc_get_order_statuses(); + + foreach ( $post_statuses as $status => $value ) { + if ( isset( $wp_post_statuses[ $status ] ) && false === $wp_post_statuses[ $status ]->show_in_admin_all_list ) { + unset( $post_statuses[ $status ] ); + } + } + + $query_vars['post_status'] = array_keys( $post_statuses ); + } + return $query_vars; + } + + /** + * Change the label when searching orders. + * + * @param mixed $query Current search query. + * @return string + */ + public function search_label( $query ) { + global $pagenow, $typenow; + + if ( 'edit.php' !== $pagenow || 'shop_order' !== $typenow || ! get_query_var( 'shop_order_search' ) || ! isset( $_GET['s'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return $query; + } + + return wc_clean( wp_unslash( $_GET['s'] ) ); // WPCS: input var ok, sanitization ok. + } + + /** + * Query vars for custom searches. + * + * @param mixed $public_query_vars Array of query vars. + * @return array + */ + public function add_custom_query_var( $public_query_vars ) { + $public_query_vars[] = 'shop_order_search'; + return $public_query_vars; + } + + /** + * Search custom fields as well as content. + * + * @param WP_Query $wp Query object. + */ + public function search_custom_fields( $wp ) { + global $pagenow; + + if ( 'edit.php' !== $pagenow || empty( $wp->query_vars['s'] ) || 'shop_order' !== $wp->query_vars['post_type'] || ! isset( $_GET['s'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return; + } + + $post_ids = wc_order_search( wc_clean( wp_unslash( $_GET['s'] ) ) ); // WPCS: input var ok, sanitization ok. + + if ( ! empty( $post_ids ) ) { + // Remove "s" - we don't want to search order name. + unset( $wp->query_vars['s'] ); + + // so we know we're doing this. + $wp->query_vars['shop_order_search'] = true; + + // Search by found posts. + $wp->query_vars['post__in'] = array_merge( $post_ids, array( 0 ) ); + } + } +} diff --git a/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-products.php b/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-products.php new file mode 100644 index 00000000000..73c58f160b6 --- /dev/null +++ b/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-products.php @@ -0,0 +1,661 @@ +'; + + echo '

    ' . esc_html__( 'Ready to start selling something awesome?', 'woocommerce' ) . '

    '; + + echo ''; + + do_action( 'wc_marketplace_suggestions_products_empty_state' ); + + echo ''; + } + + /** + * Define primary column. + * + * @return string + */ + protected function get_primary_column() { + return 'name'; + } + + /** + * Get row actions to show in the list table. + * + * @param array $actions Array of actions. + * @param WP_Post $post Current post object. + * @return array + */ + protected function get_row_actions( $actions, $post ) { + /* translators: %d: product ID. */ + return array_merge( array( 'id' => sprintf( __( 'ID: %d', 'woocommerce' ), $post->ID ) ), $actions ); + } + + /** + * Define which columns are sortable. + * + * @param array $columns Existing columns. + * @return array + */ + public function define_sortable_columns( $columns ) { + $custom = array( + 'price' => 'price', + 'sku' => 'sku', + 'name' => 'title', + ); + return wp_parse_args( $custom, $columns ); + } + + /** + * Define which columns to show on this screen. + * + * @param array $columns Existing columns. + * @return array + */ + public function define_columns( $columns ) { + if ( empty( $columns ) && ! is_array( $columns ) ) { + $columns = array(); + } + + unset( $columns['title'], $columns['comments'], $columns['date'] ); + + $show_columns = array(); + $show_columns['cb'] = ''; + $show_columns['thumb'] = '' . __( 'Image', 'woocommerce' ) . ''; + $show_columns['name'] = __( 'Name', 'woocommerce' ); + + if ( wc_product_sku_enabled() ) { + $show_columns['sku'] = __( 'SKU', 'woocommerce' ); + } + + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + $show_columns['is_in_stock'] = __( 'Stock', 'woocommerce' ); + } + + $show_columns['price'] = __( 'Price', 'woocommerce' ); + $show_columns['product_cat'] = __( 'Categories', 'woocommerce' ); + $show_columns['product_tag'] = __( 'Tags', 'woocommerce' ); + $show_columns['featured'] = '' . __( 'Featured', 'woocommerce' ) . ''; + $show_columns['date'] = __( 'Date', 'woocommerce' ); + + return array_merge( $show_columns, $columns ); + } + + /** + * Pre-fetch any data for the row each column has access to it. the_product global is there for bw compat. + * + * @param int $post_id Post ID being shown. + */ + protected function prepare_row_data( $post_id ) { + global $the_product; + + if ( empty( $this->object ) || $this->object->get_id() !== $post_id ) { + $the_product = wc_get_product( $post_id ); + $this->object = $the_product; + } + } + + /** + * Render column: thumb. + */ + protected function render_thumb_column() { + echo '' . $this->object->get_image( 'thumbnail' ) . ''; // WPCS: XSS ok. + } + + /** + * Render column: name. + */ + protected function render_name_column() { + global $post; + + $edit_link = get_edit_post_link( $this->object->get_id() ); + $title = _draft_or_post_title(); + + echo '' . esc_html( $title ) . ''; + + _post_states( $post ); + + echo ''; + + if ( $this->object->get_parent_id() > 0 ) { + echo '  ← ' . get_the_title( $this->object->get_parent_id() ) . ''; // @codingStandardsIgnoreLine. + } + + get_inline_data( $post ); + + /* Custom inline data for woocommerce. */ + echo ' + + '; + } + + /** + * Render column: sku. + */ + protected function render_sku_column() { + echo $this->object->get_sku() ? esc_html( $this->object->get_sku() ) : ''; + } + + /** + * Render column: price. + */ + protected function render_price_column() { + echo $this->object->get_price_html() ? wp_kses_post( $this->object->get_price_html() ) : ''; + } + + /** + * Render column: product_cat. + */ + protected function render_product_cat_column() { + $terms = get_the_terms( $this->object->get_id(), 'product_cat' ); + if ( ! $terms ) { + echo ''; + } else { + $termlist = array(); + foreach ( $terms as $term ) { + $termlist[] = '' . esc_html( $term->name ) . ''; + } + + echo apply_filters( 'woocommerce_admin_product_term_list', implode( ', ', $termlist ), 'product_cat', $this->object->get_id(), $termlist, $terms ); // WPCS: XSS ok. + } + } + + /** + * Render column: product_tag. + */ + protected function render_product_tag_column() { + $terms = get_the_terms( $this->object->get_id(), 'product_tag' ); + if ( ! $terms ) { + echo ''; + } else { + $termlist = array(); + foreach ( $terms as $term ) { + $termlist[] = '' . esc_html( $term->name ) . ''; + } + + echo apply_filters( 'woocommerce_admin_product_term_list', implode( ', ', $termlist ), 'product_tag', $this->object->get_id(), $termlist, $terms ); // WPCS: XSS ok. + } + } + + /** + * Render column: featured. + */ + protected function render_featured_column() { + $url = wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_feature_product&product_id=' . $this->object->get_id() ), 'woocommerce-feature-product' ); + echo ''; + if ( $this->object->is_featured() ) { + echo '' . esc_html__( 'Yes', 'woocommerce' ) . ''; + } else { + echo '' . esc_html__( 'No', 'woocommerce' ) . ''; + } + echo ''; + } + + /** + * Render column: is_in_stock. + */ + protected function render_is_in_stock_column() { + if ( $this->object->is_on_backorder() ) { + $stock_html = '' . __( 'On backorder', 'woocommerce' ) . ''; + } elseif ( $this->object->is_in_stock() ) { + $stock_html = '' . __( 'In stock', 'woocommerce' ) . ''; + } else { + $stock_html = '' . __( 'Out of stock', 'woocommerce' ) . ''; + } + + if ( $this->object->managing_stock() ) { + $stock_html .= ' (' . wc_stock_amount( $this->object->get_stock_quantity() ) . ')'; + } + + echo wp_kses_post( apply_filters( 'woocommerce_admin_stock_html', $stock_html, $this->object ) ); + } + + /** + * Query vars for custom searches. + * + * @param mixed $public_query_vars Array of query vars. + * @return array + */ + public function add_custom_query_var( $public_query_vars ) { + $public_query_vars[] = 'sku'; + return $public_query_vars; + } + + /** + * Render any custom filters and search inputs for the list table. + */ + protected function render_filters() { + $filters = apply_filters( + 'woocommerce_products_admin_list_table_filters', + array( + 'product_category' => array( $this, 'render_products_category_filter' ), + 'product_type' => array( $this, 'render_products_type_filter' ), + 'stock_status' => array( $this, 'render_products_stock_status_filter' ), + ) + ); + + ob_start(); + foreach ( $filters as $filter_callback ) { + call_user_func( $filter_callback ); + } + $output = ob_get_clean(); + + echo apply_filters( 'woocommerce_product_filters', $output ); // WPCS: XSS ok. + } + + /** + * Render the product category filter for the list table. + * + * @since 3.5.0 + */ + protected function render_products_category_filter() { + $categories_count = (int) wp_count_terms( 'product_cat' ); + + if ( $categories_count <= apply_filters( 'woocommerce_product_category_filter_threshold', 100 ) ) { + wc_product_dropdown_categories( + array( + 'option_select_text' => __( 'Filter by category', 'woocommerce' ), + 'hide_empty' => 0, + ) + ); + } else { + $current_category_slug = isset( $_GET['product_cat'] ) ? wc_clean( wp_unslash( $_GET['product_cat'] ) ) : false; // WPCS: input var ok, CSRF ok. + $current_category = $current_category_slug ? get_term_by( 'slug', $current_category_slug, 'product_cat' ) : false; + ?> + + '; + + foreach ( wc_get_product_types() as $value => $label ) { + $output .= ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } + } + } + + /** + * Get country address formats. + * + * These define how addresses are formatted for display in various countries. + * + * @return array + */ + public function get_address_formats() { + if ( empty( $this->address_formats ) ) { + $this->address_formats = apply_filters( + 'woocommerce_localisation_address_formats', + array( + 'default' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode}\n{country}", + 'AT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'AU' => "{name}\n{company}\n{address_1}\n{address_2}\n{city} {state} {postcode}\n{country}", + 'BE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'CA' => "{company}\n{name}\n{address_1}\n{address_2}\n{city} {state_code} {postcode}\n{country}", + 'CH' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'CL' => "{company}\n{name}\n{address_1}\n{address_2}\n{state}\n{postcode} {city}\n{country}", + 'CN' => "{country} {postcode}\n{state}, {city}, {address_2}, {address_1}\n{company}\n{name}", + 'CZ' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'DE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'DK' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'EE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'ES' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city}\n{state}\n{country}", + 'FI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'FR' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city_upper}\n{country}", + 'HK' => "{company}\n{first_name} {last_name_upper}\n{address_1}\n{address_2}\n{city_upper}\n{state_upper}\n{country}", + 'HU' => "{last_name} {first_name}\n{company}\n{city}\n{address_1}\n{address_2}\n{postcode}\n{country}", + 'IN' => "{company}\n{name}\n{address_1}\n{address_2}\n{city} {postcode}\n{state}, {country}", + 'IS' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'IT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode}\n{city}\n{state_upper}\n{country}", + 'JM' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode_upper}\n{country}", + 'JP' => "{postcode}\n{state} {city} {address_1}\n{address_2}\n{company}\n{last_name} {first_name}\n{country}", + 'LI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'NL' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'NO' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'NZ' => "{name}\n{company}\n{address_1}\n{address_2}\n{city} {postcode}\n{country}", + 'PL' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'PR' => "{company}\n{name}\n{address_1} {address_2}\n{city} \n{country} {postcode}", + 'PT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'RS' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'SE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'SI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'SK' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'TR' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city} {state}\n{country}", + 'TW' => "{company}\n{last_name} {first_name}\n{address_1}\n{address_2}\n{state}, {city} {postcode}\n{country}", + 'UG' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}, {country}", + 'US' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}, {state_code} {postcode}\n{country}", + 'VN' => "{name}\n{company}\n{address_1}\n{city}\n{country}", + ) + ); + } + return $this->address_formats; + } + + /** + * Get country address format. + * + * @param array $args Arguments. + * @param string $separator How to separate address lines. @since 3.5.0. + * @return string + */ + public function get_formatted_address( $args = array(), $separator = '
    ' ) { + $default_args = array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'state' => '', + 'postcode' => '', + 'country' => '', + ); + + $args = array_map( 'trim', wp_parse_args( $args, $default_args ) ); + $state = $args['state']; + $country = $args['country']; + + // Get all formats. + $formats = $this->get_address_formats(); + + // Get format for the address' country. + $format = ( $country && isset( $formats[ $country ] ) ) ? $formats[ $country ] : $formats['default']; + + // Handle full country name. + $full_country = ( isset( $this->countries[ $country ] ) ) ? $this->countries[ $country ] : $country; + + // Country is not needed if the same as base. + if ( $country === $this->get_base_country() && ! apply_filters( 'woocommerce_formatted_address_force_country_display', false ) ) { + $format = str_replace( '{country}', '', $format ); + } + + // Handle full state name. + $full_state = ( $country && $state && isset( $this->states[ $country ][ $state ] ) ) ? $this->states[ $country ][ $state ] : $state; + + // Substitute address parts into the string. + $replace = array_map( + 'esc_html', + apply_filters( + 'woocommerce_formatted_address_replacements', + array( + '{first_name}' => $args['first_name'], + '{last_name}' => $args['last_name'], + '{name}' => sprintf( + /* translators: 1: first name 2: last name */ + _x( '%1$s %2$s', 'full name', 'woocommerce' ), + $args['first_name'], + $args['last_name'] + ), + '{company}' => $args['company'], + '{address_1}' => $args['address_1'], + '{address_2}' => $args['address_2'], + '{city}' => $args['city'], + '{state}' => $full_state, + '{postcode}' => $args['postcode'], + '{country}' => $full_country, + '{first_name_upper}' => wc_strtoupper( $args['first_name'] ), + '{last_name_upper}' => wc_strtoupper( $args['last_name'] ), + '{name_upper}' => wc_strtoupper( + sprintf( + /* translators: 1: first name 2: last name */ + _x( '%1$s %2$s', 'full name', 'woocommerce' ), + $args['first_name'], + $args['last_name'] + ) + ), + '{company_upper}' => wc_strtoupper( $args['company'] ), + '{address_1_upper}' => wc_strtoupper( $args['address_1'] ), + '{address_2_upper}' => wc_strtoupper( $args['address_2'] ), + '{city_upper}' => wc_strtoupper( $args['city'] ), + '{state_upper}' => wc_strtoupper( $full_state ), + '{state_code}' => wc_strtoupper( $state ), + '{postcode_upper}' => wc_strtoupper( $args['postcode'] ), + '{country_upper}' => wc_strtoupper( $full_country ), + ), + $args + ) + ); + + $formatted_address = str_replace( array_keys( $replace ), $replace, $format ); + + // Clean up white space. + $formatted_address = preg_replace( '/ +/', ' ', trim( $formatted_address ) ); + $formatted_address = preg_replace( '/\n\n+/', "\n", $formatted_address ); + + // Break newlines apart and remove empty lines/trim commas and white space. + $formatted_address = array_filter( array_map( array( $this, 'trim_formatted_address_line' ), explode( "\n", $formatted_address ) ) ); + + // Add html breaks. + $formatted_address = implode( $separator, $formatted_address ); + + // We're done! + return $formatted_address; + } + + /** + * Trim white space and commas off a line. + * + * @param string $line Line. + * @return string + */ + private function trim_formatted_address_line( $line ) { + return trim( $line, ', ' ); + } + + /** + * Returns the fields we show by default. This can be filtered later on. + * + * @return array + */ + public function get_default_address_fields() { + $address_2_label = __( 'Apartment, suite, unit, etc.', 'woocommerce' ); + + // If necessary, append '(optional)' to the placeholder: we don't need to worry about the + // label, though, as woocommerce_form_field() takes care of that. + if ( 'optional' === get_option( 'woocommerce_checkout_address_2_field', 'optional' ) ) { + $address_2_placeholder = __( 'Apartment, suite, unit, etc. (optional)', 'woocommerce' ); + } else { + $address_2_placeholder = $address_2_label; + } + + $fields = array( + 'first_name' => array( + 'label' => __( 'First name', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-first' ), + 'autocomplete' => 'given-name', + 'priority' => 10, + ), + 'last_name' => array( + 'label' => __( 'Last name', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-last' ), + 'autocomplete' => 'family-name', + 'priority' => 20, + ), + 'company' => array( + 'label' => __( 'Company name', 'woocommerce' ), + 'class' => array( 'form-row-wide' ), + 'autocomplete' => 'organization', + 'priority' => 30, + 'required' => 'required' === get_option( 'woocommerce_checkout_company_field', 'optional' ), + ), + 'country' => array( + 'type' => 'country', + 'label' => __( 'Country / Region', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-wide', 'address-field', 'update_totals_on_change' ), + 'autocomplete' => 'country', + 'priority' => 40, + ), + 'address_1' => array( + 'label' => __( 'Street address', 'woocommerce' ), + /* translators: use local order of street name and house number. */ + 'placeholder' => esc_attr__( 'House number and street name', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-wide', 'address-field' ), + 'autocomplete' => 'address-line1', + 'priority' => 50, + ), + 'address_2' => array( + 'label' => $address_2_label, + 'label_class' => array( 'screen-reader-text' ), + 'placeholder' => esc_attr( $address_2_placeholder ), + 'class' => array( 'form-row-wide', 'address-field' ), + 'autocomplete' => 'address-line2', + 'priority' => 60, + 'required' => 'required' === get_option( 'woocommerce_checkout_address_2_field', 'optional' ), + ), + 'city' => array( + 'label' => __( 'Town / City', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-wide', 'address-field' ), + 'autocomplete' => 'address-level2', + 'priority' => 70, + ), + 'state' => array( + 'type' => 'state', + 'label' => __( 'State / County', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-wide', 'address-field' ), + 'validate' => array( 'state' ), + 'autocomplete' => 'address-level1', + 'priority' => 80, + ), + 'postcode' => array( + 'label' => __( 'Postcode / ZIP', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-wide', 'address-field' ), + 'validate' => array( 'postcode' ), + 'autocomplete' => 'postal-code', + 'priority' => 90, + ), + ); + + if ( 'hidden' === get_option( 'woocommerce_checkout_company_field', 'optional' ) ) { + unset( $fields['company'] ); + } + + if ( 'hidden' === get_option( 'woocommerce_checkout_address_2_field', 'optional' ) ) { + unset( $fields['address_2'] ); + } + + $default_address_fields = apply_filters( 'woocommerce_default_address_fields', $fields ); + // Sort each of the fields based on priority. + uasort( $default_address_fields, 'wc_checkout_fields_uasort_comparison' ); + + return $default_address_fields; + } + + /** + * Get JS selectors for fields which are shown/hidden depending on the locale. + * + * @return array + */ + public function get_country_locale_field_selectors() { + $locale_fields = array( + 'address_1' => '#billing_address_1_field, #shipping_address_1_field', + 'address_2' => '#billing_address_2_field, #shipping_address_2_field', + 'state' => '#billing_state_field, #shipping_state_field, #calc_shipping_state_field', + 'postcode' => '#billing_postcode_field, #shipping_postcode_field, #calc_shipping_postcode_field', + 'city' => '#billing_city_field, #shipping_city_field, #calc_shipping_city_field', + ); + return apply_filters( 'woocommerce_country_locale_field_selectors', $locale_fields ); + } + + /** + * Get country locale settings. + * + * These locales override the default country selections after a country is chosen. + * + * @return array + */ + public function get_country_locale() { + if ( empty( $this->locale ) ) { + $this->locale = apply_filters( + 'woocommerce_get_country_locale', + array( + 'AE' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + 'state' => array( + 'required' => false, + ), + ), + 'AF' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'AL' => array( + 'state' => array( + 'label' => __( 'County', 'woocommerce' ), + ), + ), + 'AO' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'AT' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'AU' => array( + 'city' => array( + 'label' => __( 'Suburb', 'woocommerce' ), + ), + 'postcode' => array( + 'label' => __( 'Postcode', 'woocommerce' ), + ), + 'state' => array( + 'label' => __( 'State', 'woocommerce' ), + ), + ), + 'AX' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'BA' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'label' => __( 'Canton', 'woocommerce' ), + 'required' => false, + 'hidden' => true, + ), + ), + 'BD' => array( + 'postcode' => array( + 'required' => false, + ), + 'state' => array( + 'label' => __( 'District', 'woocommerce' ), + ), + ), + 'BE' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'BH' => array( + 'postcode' => array( + 'required' => false, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'BI' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'BO' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + 'state' => array( + 'label' => __( 'Department', 'woocommerce' ), + ), + ), + 'BS' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'CA' => array( + 'postcode' => array( + 'label' => __( 'Postal code', 'woocommerce' ), + ), + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'CH' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'label' => __( 'Canton', 'woocommerce' ), + 'required' => false, + ), + ), + 'CL' => array( + 'city' => array( + 'required' => true, + ), + 'postcode' => array( + 'required' => false, + ), + 'state' => array( + 'label' => __( 'Region', 'woocommerce' ), + ), + ), + 'CN' => array( + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'CO' => array( + 'postcode' => array( + 'required' => false, + ), + 'state' => array( + 'label' => __( 'Department', 'woocommerce' ), + ), + ), + 'CR' => array( + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'CW' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + 'state' => array( + 'required' => false, + ), + ), + 'CZ' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'DE' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'DK' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'DO' => array( + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'EC' => array( + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'EE' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'FI' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'FR' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'GH' => array( + 'postcode' => array( + 'required' => false, + ), + 'state' => array( + 'label' => __( 'Region', 'woocommerce' ), + ), + ), + 'GP' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'GF' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'GR' => array( + 'state' => array( + 'required' => false, + ), + ), + 'GT' => array( + 'postcode' => array( + 'required' => false, + ), + 'state' => array( + 'label' => __( 'Department', 'woocommerce' ), + ), + ), + 'HK' => array( + 'postcode' => array( + 'required' => false, + ), + 'city' => array( + 'label' => __( 'Town / District', 'woocommerce' ), + ), + 'state' => array( + 'label' => __( 'Region', 'woocommerce' ), + ), + ), + 'HN' => array( + 'state' => array( + 'label' => __( 'Department', 'woocommerce' ), + ), + ), + 'HU' => array( + 'last_name' => array( + 'class' => array( 'form-row-first' ), + 'priority' => 10, + ), + 'first_name' => array( + 'class' => array( 'form-row-last' ), + 'priority' => 20, + ), + 'postcode' => array( + 'class' => array( 'form-row-first', 'address-field' ), + 'priority' => 65, + ), + 'city' => array( + 'class' => array( 'form-row-last', 'address-field' ), + ), + 'address_1' => array( + 'priority' => 71, + ), + 'address_2' => array( + 'priority' => 72, + ), + 'state' => array( + 'label' => __( 'County', 'woocommerce' ), + ), + ), + 'ID' => array( + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'IE' => array( + 'postcode' => array( + 'required' => false, + 'label' => __( 'Eircode', 'woocommerce' ), + ), + 'state' => array( + 'label' => __( 'County', 'woocommerce' ), + ), + ), + 'IS' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'IL' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'IM' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'IN' => array( + 'postcode' => array( + 'label' => __( 'PIN', 'woocommerce' ), + ), + 'state' => array( + 'label' => __( 'State', 'woocommerce' ), + ), + ), + 'IT' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => true, + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'JM' => array( + 'city' => array( + 'label' => __( 'Town / City / Post Office', 'woocommerce' ), + ), + 'postcode' => array( + 'required' => false, + 'label' => __( 'Postal Code', 'woocommerce' ), + ), + 'state' => array( + 'required' => true, + 'label' => __( 'Parish', 'woocommerce' ), + ), + ), + 'JP' => array( + 'last_name' => array( + 'class' => array( 'form-row-first' ), + 'priority' => 10, + ), + 'first_name' => array( + 'class' => array( 'form-row-last' ), + 'priority' => 20, + ), + 'postcode' => array( + 'class' => array( 'form-row-first', 'address-field' ), + 'priority' => 65, + ), + 'state' => array( + 'label' => __( 'Prefecture', 'woocommerce' ), + 'class' => array( 'form-row-last', 'address-field' ), + 'priority' => 66, + ), + 'city' => array( + 'priority' => 67, + ), + 'address_1' => array( + 'priority' => 68, + ), + 'address_2' => array( + 'priority' => 69, + ), + ), + 'KR' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'KW' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'LV' => array( + 'state' => array( + 'label' => __( 'Municipality', 'woocommerce' ), + 'required' => false, + ), + ), + 'LB' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'MQ' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'MT' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'MZ' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'NI' => array( + 'state' => array( + 'label' => __( 'Department', 'woocommerce' ), + ), + ), + 'NL' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'NG' => array( + 'postcode' => array( + 'label' => __( 'Postcode', 'woocommerce' ), + 'required' => false, + 'hidden' => true, + ), + 'state' => array( + 'label' => __( 'State', 'woocommerce' ), + ), + ), + 'NZ' => array( + 'postcode' => array( + 'label' => __( 'Postcode', 'woocommerce' ), + ), + 'state' => array( + 'required' => false, + 'label' => __( 'Region', 'woocommerce' ), + ), + ), + 'NO' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'NP' => array( + 'state' => array( + 'label' => __( 'State / Zone', 'woocommerce' ), + ), + 'postcode' => array( + 'required' => false, + ), + ), + 'PA' => array( + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'PL' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'PR' => array( + 'city' => array( + 'label' => __( 'Municipality', 'woocommerce' ), + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'PT' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'PY' => array( + 'state' => array( + 'label' => __( 'Department', 'woocommerce' ), + ), + ), + 'RE' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'RO' => array( + 'state' => array( + 'label' => __( 'County', 'woocommerce' ), + 'required' => true, + ), + ), + 'RS' => array( + 'city' => array( + 'required' => true, + ), + 'postcode' => array( + 'required' => true, + ), + 'state' => array( + 'label' => __( 'District', 'woocommerce' ), + 'required' => false, + ), + ), + 'SG' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + 'city' => array( + 'required' => false, + ), + ), + 'SK' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'SI' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'SR' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'SV' => array( + 'state' => array( + 'label' => __( 'Department', 'woocommerce' ), + ), + ), + 'ES' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'LI' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'label' => __( 'Municipality', 'woocommerce' ), + 'required' => false, + ), + ), + 'LK' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'LU' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'MD' => array( + 'state' => array( + 'label' => __( 'Municipality / District', 'woocommerce' ), + ), + ), + 'SE' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'TR' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'UG' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + 'city' => array( + 'label' => __( 'Town / Village', 'woocommerce' ), + 'required' => true, + ), + 'state' => array( + 'label' => __( 'District', 'woocommerce' ), + 'required' => true, + ), + ), + 'US' => array( + 'postcode' => array( + 'label' => __( 'ZIP Code', 'woocommerce' ), + ), + 'state' => array( + 'label' => __( 'State', 'woocommerce' ), + ), + ), + 'UY' => array( + 'state' => array( + 'label' => __( 'Department', 'woocommerce' ), + ), + ), + 'GB' => array( + 'postcode' => array( + 'label' => __( 'Postcode', 'woocommerce' ), + ), + 'state' => array( + 'label' => __( 'County', 'woocommerce' ), + 'required' => false, + ), + ), + 'ST' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + 'state' => array( + 'label' => __( 'District', 'woocommerce' ), + ), + ), + 'VN' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + 'postcode' => array( + 'priority' => 65, + 'required' => false, + 'hidden' => false, + ), + 'address_2' => array( + 'required' => false, + 'hidden' => false, + ), + ), + 'WS' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'YT' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'ZA' => array( + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'ZW' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + ), + ) + ); + + $this->locale = array_intersect_key( $this->locale, array_merge( $this->get_allowed_countries(), $this->get_shipping_countries() ) ); + + // Default Locale Can be filtered to override fields in get_address_fields(). Countries with no specific locale will use default. + $this->locale['default'] = apply_filters( 'woocommerce_get_country_locale_default', $this->get_default_address_fields() ); + + // Filter default AND shop base locales to allow overides via a single function. These will be used when changing countries on the checkout. + if ( ! isset( $this->locale[ $this->get_base_country() ] ) ) { + $this->locale[ $this->get_base_country() ] = $this->locale['default']; + } + + $this->locale['default'] = apply_filters( 'woocommerce_get_country_locale_base', $this->locale['default'] ); + $this->locale[ $this->get_base_country() ] = apply_filters( 'woocommerce_get_country_locale_base', $this->locale[ $this->get_base_country() ] ); + } + + return $this->locale; + } + + /** + * Apply locale and get address fields. + * + * @param mixed $country Country. + * @param string $type Address type, defaults to 'billing_'. + * @return array + */ + public function get_address_fields( $country = '', $type = 'billing_' ) { + if ( ! $country ) { + $country = $this->get_base_country(); + } + + $fields = $this->get_default_address_fields(); + $locale = $this->get_country_locale(); + + if ( isset( $locale[ $country ] ) ) { + $fields = wc_array_overlay( $fields, $locale[ $country ] ); + } + + // Prepend field keys. + $address_fields = array(); + + foreach ( $fields as $key => $value ) { + if ( 'state' === $key ) { + $value['country_field'] = $type . 'country'; + $value['country'] = $country; + } + $address_fields[ $type . $key ] = $value; + } + + // Add email and phone fields. + if ( 'billing_' === $type ) { + if ( 'hidden' !== get_option( 'woocommerce_checkout_phone_field', 'required' ) ) { + $address_fields['billing_phone'] = array( + 'label' => __( 'Phone', 'woocommerce' ), + 'required' => 'required' === get_option( 'woocommerce_checkout_phone_field', 'required' ), + 'type' => 'tel', + 'class' => array( 'form-row-wide' ), + 'validate' => array( 'phone' ), + 'autocomplete' => 'tel', + 'priority' => 100, + ); + } + $address_fields['billing_email'] = array( + 'label' => __( 'Email address', 'woocommerce' ), + 'required' => true, + 'type' => 'email', + 'class' => array( 'form-row-wide' ), + 'validate' => array( 'email' ), + 'autocomplete' => 'no' === get_option( 'woocommerce_registration_generate_username' ) ? 'email' : 'email username', + 'priority' => 110, + ); + } + + /** + * Important note on this filter: Changes to address fields can and will be overridden by + * the woocommerce_default_address_fields. The locales/default locales apply on top based + * on country selection. If you want to change things like the required status of an + * address field, filter woocommerce_default_address_fields instead. + */ + $address_fields = apply_filters( 'woocommerce_' . $type . 'fields', $address_fields, $country ); + // Sort each of the fields based on priority. + uasort( $address_fields, 'wc_checkout_fields_uasort_comparison' ); + + return $address_fields; + } +} diff --git a/plugins/woocommerce/includes/class-wc-coupon.php b/plugins/woocommerce/includes/class-wc-coupon.php new file mode 100644 index 00000000000..9d4649baa4e --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-coupon.php @@ -0,0 +1,1099 @@ + '', + 'amount' => 0, + 'status' => null, + 'date_created' => null, + 'date_modified' => null, + 'date_expires' => null, + 'discount_type' => 'fixed_cart', + 'description' => '', + 'usage_count' => 0, + 'individual_use' => false, + 'product_ids' => array(), + 'excluded_product_ids' => array(), + 'usage_limit' => 0, + 'usage_limit_per_user' => 0, + 'limit_usage_to_x_items' => null, + 'free_shipping' => false, + 'product_categories' => array(), + 'excluded_product_categories' => array(), + 'exclude_sale_items' => false, + 'minimum_amount' => '', + 'maximum_amount' => '', + 'email_restrictions' => array(), + 'used_by' => array(), + 'virtual' => false, + ); + + // Coupon message codes. + const E_WC_COUPON_INVALID_FILTERED = 100; + const E_WC_COUPON_INVALID_REMOVED = 101; + const E_WC_COUPON_NOT_YOURS_REMOVED = 102; + const E_WC_COUPON_ALREADY_APPLIED = 103; + const E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY = 104; + const E_WC_COUPON_NOT_EXIST = 105; + const E_WC_COUPON_USAGE_LIMIT_REACHED = 106; + const E_WC_COUPON_EXPIRED = 107; + const E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET = 108; + const E_WC_COUPON_NOT_APPLICABLE = 109; + const E_WC_COUPON_NOT_VALID_SALE_ITEMS = 110; + const E_WC_COUPON_PLEASE_ENTER = 111; + const E_WC_COUPON_MAX_SPEND_LIMIT_MET = 112; + const E_WC_COUPON_EXCLUDED_PRODUCTS = 113; + const E_WC_COUPON_EXCLUDED_CATEGORIES = 114; + const E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK = 115; + const E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST = 116; + const WC_COUPON_SUCCESS = 200; + const WC_COUPON_REMOVED = 201; + + /** + * Cache group. + * + * @var string + */ + protected $cache_group = 'coupons'; + + /** + * Coupon constructor. Loads coupon data. + * + * @param mixed $data Coupon data, object, ID or code. + */ + public function __construct( $data = '' ) { + parent::__construct( $data ); + + // If we already have a coupon object, read it again. + if ( $data instanceof WC_Coupon ) { + $this->set_id( absint( $data->get_id() ) ); + $this->read_object_from_database(); + return; + } + + // This filter allows custom coupon objects to be created on the fly. + $coupon = apply_filters( 'woocommerce_get_shop_coupon_data', false, $data, $this ); + + if ( $coupon ) { + $this->read_manual_coupon( $data, $coupon ); + return; + } + + // Try to load coupon using ID or code. + if ( is_int( $data ) && 'shop_coupon' === get_post_type( $data ) ) { + $this->set_id( $data ); + } elseif ( ! empty( $data ) ) { + $id = wc_get_coupon_id_by_code( $data ); + // Need to support numeric strings for backwards compatibility. + if ( ! $id && 'shop_coupon' === get_post_type( $data ) ) { + $this->set_id( $data ); + } else { + $this->set_id( $id ); + $this->set_code( $data ); + } + } else { + $this->set_object_read( true ); + } + + $this->read_object_from_database(); + } + + /** + * If the object has an ID, read using the data store. + * + * @since 3.4.1 + */ + protected function read_object_from_database() { + $this->data_store = WC_Data_Store::load( 'coupon' ); + + if ( $this->get_id() > 0 ) { + $this->data_store->read( $this ); + } + } + /** + * Checks the coupon type. + * + * @param string|array $type Array or string of types. + * @return bool + */ + public function is_type( $type ) { + return ( $this->get_discount_type() === $type || ( is_array( $type ) && in_array( $this->get_discount_type(), $type, true ) ) ); + } + + /** + * Prefix for action and filter hooks on data. + * + * @since 3.0.0 + * @return string + */ + protected function get_hook_prefix() { + return 'woocommerce_coupon_get_'; + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + | + | Methods for getting data from the coupon object. + | + */ + + /** + * Get coupon code. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_code( $context = 'view' ) { + return $this->get_prop( 'code', $context ); + } + + /** + * Get coupon description. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_description( $context = 'view' ) { + return $this->get_prop( 'description', $context ); + } + + /** + * Get coupon status. + * + * @since 6.2.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_status( $context = 'view' ) { + return $this->get_prop( 'status', $context ); + } + + /** + * Get discount type. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_discount_type( $context = 'view' ) { + return $this->get_prop( 'discount_type', $context ); + } + + /** + * Get coupon amount. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return float + */ + public function get_amount( $context = 'view' ) { + return wc_format_decimal( $this->get_prop( 'amount', $context ) ); + } + + /** + * Get coupon expiration date. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_expires( $context = 'view' ) { + return $this->get_prop( 'date_expires', $context ); + } + + /** + * Get date_created + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_created( $context = 'view' ) { + return $this->get_prop( 'date_created', $context ); + } + + /** + * Get date_modified + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_modified( $context = 'view' ) { + return $this->get_prop( 'date_modified', $context ); + } + + /** + * Get coupon usage count. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return integer + */ + public function get_usage_count( $context = 'view' ) { + return $this->get_prop( 'usage_count', $context ); + } + + /** + * Get the "individual use" checkbox status. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return bool + */ + public function get_individual_use( $context = 'view' ) { + return $this->get_prop( 'individual_use', $context ); + } + + /** + * Get product IDs this coupon can apply to. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return array + */ + public function get_product_ids( $context = 'view' ) { + return $this->get_prop( 'product_ids', $context ); + } + + /** + * Get product IDs that this coupon should not apply to. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return array + */ + public function get_excluded_product_ids( $context = 'view' ) { + return $this->get_prop( 'excluded_product_ids', $context ); + } + + /** + * Get coupon usage limit. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return integer + */ + public function get_usage_limit( $context = 'view' ) { + return $this->get_prop( 'usage_limit', $context ); + } + + /** + * Get coupon usage limit per customer (for a single customer) + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return integer + */ + public function get_usage_limit_per_user( $context = 'view' ) { + return $this->get_prop( 'usage_limit_per_user', $context ); + } + + /** + * Usage limited to certain amount of items + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return integer|null + */ + public function get_limit_usage_to_x_items( $context = 'view' ) { + return $this->get_prop( 'limit_usage_to_x_items', $context ); + } + + /** + * If this coupon grants free shipping or not. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return bool + */ + public function get_free_shipping( $context = 'view' ) { + return $this->get_prop( 'free_shipping', $context ); + } + + /** + * Get product categories this coupon can apply to. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return array + */ + public function get_product_categories( $context = 'view' ) { + return $this->get_prop( 'product_categories', $context ); + } + + /** + * Get product categories this coupon cannot not apply to. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return array + */ + public function get_excluded_product_categories( $context = 'view' ) { + return $this->get_prop( 'excluded_product_categories', $context ); + } + + /** + * If this coupon should exclude items on sale. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return bool + */ + public function get_exclude_sale_items( $context = 'view' ) { + return $this->get_prop( 'exclude_sale_items', $context ); + } + + /** + * Get minimum spend amount. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return float + */ + public function get_minimum_amount( $context = 'view' ) { + return $this->get_prop( 'minimum_amount', $context ); + } + /** + * Get maximum spend amount. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return float + */ + public function get_maximum_amount( $context = 'view' ) { + return $this->get_prop( 'maximum_amount', $context ); + } + + /** + * Get emails to check customer usage restrictions. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return array + */ + public function get_email_restrictions( $context = 'view' ) { + return $this->get_prop( 'email_restrictions', $context ); + } + + /** + * Get records of all users who have used the current coupon. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return array + */ + public function get_used_by( $context = 'view' ) { + return $this->get_prop( 'used_by', $context ); + } + + /** + * If the filter is added through the woocommerce_get_shop_coupon_data filter, it's virtual and not in the DB. + * + * @since 3.2.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return boolean + */ + public function get_virtual( $context = 'view' ) { + return (bool) $this->get_prop( 'virtual', $context ); + } + + /** + * Get discount amount for a cart item. + * + * @param float $discounting_amount Amount the coupon is being applied to. + * @param array|null $cart_item Cart item being discounted if applicable. + * @param boolean $single True if discounting a single qty item, false if its the line. + * @return float Amount this coupon has discounted. + */ + public function get_discount_amount( $discounting_amount, $cart_item = null, $single = false ) { + $discount = 0; + $cart_item_qty = is_null( $cart_item ) ? 1 : $cart_item['quantity']; + + if ( $this->is_type( array( 'percent' ) ) ) { + $discount = (float) $this->get_amount() * ( $discounting_amount / 100 ); + } elseif ( $this->is_type( 'fixed_cart' ) && ! is_null( $cart_item ) && WC()->cart->subtotal_ex_tax ) { + /** + * This is the most complex discount - we need to divide the discount between rows based on their price in. + * proportion to the subtotal. This is so rows with different tax rates get a fair discount, and so rows. + * with no price (free) don't get discounted. + * + * Get item discount by dividing item cost by subtotal to get a %. + * + * Uses price inc tax if prices include tax to work around https://github.com/woocommerce/woocommerce/issues/7669 and https://github.com/woocommerce/woocommerce/issues/8074. + */ + if ( wc_prices_include_tax() ) { + $discount_percent = ( wc_get_price_including_tax( $cart_item['data'] ) * $cart_item_qty ) / WC()->cart->subtotal; + } else { + $discount_percent = ( wc_get_price_excluding_tax( $cart_item['data'] ) * $cart_item_qty ) / WC()->cart->subtotal_ex_tax; + } + $discount = ( (float) $this->get_amount() * $discount_percent ) / $cart_item_qty; + + } elseif ( $this->is_type( 'fixed_product' ) ) { + $discount = min( $this->get_amount(), $discounting_amount ); + $discount = $single ? $discount : $discount * $cart_item_qty; + } + + return apply_filters( + 'woocommerce_coupon_get_discount_amount', + NumberUtil::round( min( $discount, $discounting_amount ), wc_get_rounding_precision() ), + $discounting_amount, + $cart_item, + $single, + $this + ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + | + | Functions for setting coupon data. These should not update anything in the + | database itself and should only change what is stored in the class + | object. + | + */ + + /** + * Set coupon code. + * + * @since 3.0.0 + * @param string $code Coupon code. + */ + public function set_code( $code ) { + $this->set_prop( 'code', wc_format_coupon_code( $code ) ); + } + + /** + * Set coupon description. + * + * @since 3.0.0 + * @param string $description Description. + */ + public function set_description( $description ) { + $this->set_prop( 'description', $description ); + } + + /** + * Set coupon status. + * + * @since 3.0.0 + * @param string $status Status. + */ + public function set_status( $status ) { + $this->set_prop( 'status', $status ); + } + + /** + * Set discount type. + * + * @since 3.0.0 + * @param string $discount_type Discount type. + */ + public function set_discount_type( $discount_type ) { + if ( 'percent_product' === $discount_type ) { + $discount_type = 'percent'; // Backwards compatibility. + } + if ( ! in_array( $discount_type, array_keys( wc_get_coupon_types() ), true ) ) { + $this->error( 'coupon_invalid_discount_type', __( 'Invalid discount type', 'woocommerce' ) ); + } + $this->set_prop( 'discount_type', $discount_type ); + } + + /** + * Set amount. + * + * @since 3.0.0 + * @param float $amount Amount. + */ + public function set_amount( $amount ) { + $amount = wc_format_decimal( $amount ); + + if ( ! is_numeric( $amount ) ) { + $amount = 0; + } + + if ( $amount < 0 ) { + $this->error( 'coupon_invalid_amount', __( 'Invalid discount amount', 'woocommerce' ) ); + } + + if ( 'percent' === $this->get_discount_type() && $amount > 100 ) { + $this->error( 'coupon_invalid_amount', __( 'Invalid discount amount', 'woocommerce' ) ); + } + + $this->set_prop( 'amount', $amount ); + } + + /** + * Set expiration date. + * + * @since 3.0.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. + */ + public function set_date_expires( $date ) { + $this->set_date_prop( 'date_expires', $date ); + } + + /** + * Set date_created + * + * @since 3.0.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. + */ + public function set_date_created( $date ) { + $this->set_date_prop( 'date_created', $date ); + } + + /** + * Set date_modified + * + * @since 3.0.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. + */ + public function set_date_modified( $date ) { + $this->set_date_prop( 'date_modified', $date ); + } + + /** + * Set how many times this coupon has been used. + * + * @since 3.0.0 + * @param int $usage_count Usage count. + */ + public function set_usage_count( $usage_count ) { + $this->set_prop( 'usage_count', absint( $usage_count ) ); + } + + /** + * Set if this coupon can only be used once. + * + * @since 3.0.0 + * @param bool $is_individual_use If is for individual use. + */ + public function set_individual_use( $is_individual_use ) { + $this->set_prop( 'individual_use', (bool) $is_individual_use ); + } + + /** + * Set the product IDs this coupon can be used with. + * + * @since 3.0.0 + * @param array $product_ids Products IDs. + */ + public function set_product_ids( $product_ids ) { + $this->set_prop( 'product_ids', array_filter( wp_parse_id_list( (array) $product_ids ) ) ); + } + + /** + * Set the product IDs this coupon cannot be used with. + * + * @since 3.0.0 + * @param array $excluded_product_ids Exclude product IDs. + */ + public function set_excluded_product_ids( $excluded_product_ids ) { + $this->set_prop( 'excluded_product_ids', array_filter( wp_parse_id_list( (array) $excluded_product_ids ) ) ); + } + + /** + * Set the amount of times this coupon can be used. + * + * @since 3.0.0 + * @param int $usage_limit Usage limit. + */ + public function set_usage_limit( $usage_limit ) { + $this->set_prop( 'usage_limit', absint( $usage_limit ) ); + } + + /** + * Set the amount of times this coupon can be used per user. + * + * @since 3.0.0 + * @param int $usage_limit Usage limit. + */ + public function set_usage_limit_per_user( $usage_limit ) { + $this->set_prop( 'usage_limit_per_user', absint( $usage_limit ) ); + } + + /** + * Set usage limit to x number of items. + * + * @since 3.0.0 + * @param int|null $limit_usage_to_x_items Limit usage to X items. + */ + public function set_limit_usage_to_x_items( $limit_usage_to_x_items ) { + $this->set_prop( 'limit_usage_to_x_items', is_null( $limit_usage_to_x_items ) ? null : absint( $limit_usage_to_x_items ) ); + } + + /** + * Set if this coupon enables free shipping or not. + * + * @since 3.0.0 + * @param bool $free_shipping If grant free shipping. + */ + public function set_free_shipping( $free_shipping ) { + $this->set_prop( 'free_shipping', (bool) $free_shipping ); + } + + /** + * Set the product category IDs this coupon can be used with. + * + * @since 3.0.0 + * @param array $product_categories List of product categories. + */ + public function set_product_categories( $product_categories ) { + $this->set_prop( 'product_categories', array_filter( wp_parse_id_list( (array) $product_categories ) ) ); + } + + /** + * Set the product category IDs this coupon cannot be used with. + * + * @since 3.0.0 + * @param array $excluded_product_categories List of excluded product categories. + */ + public function set_excluded_product_categories( $excluded_product_categories ) { + $this->set_prop( 'excluded_product_categories', array_filter( wp_parse_id_list( (array) $excluded_product_categories ) ) ); + } + + /** + * Set if this coupon should excluded sale items or not. + * + * @since 3.0.0 + * @param bool $exclude_sale_items If should exclude sale items. + */ + public function set_exclude_sale_items( $exclude_sale_items ) { + $this->set_prop( 'exclude_sale_items', (bool) $exclude_sale_items ); + } + + /** + * Set the minimum spend amount. + * + * @since 3.0.0 + * @param float $amount Minium amount. + */ + public function set_minimum_amount( $amount ) { + $this->set_prop( 'minimum_amount', wc_format_decimal( $amount ) ); + } + + /** + * Set the maximum spend amount. + * + * @since 3.0.0 + * @param float $amount Maximum amount. + */ + public function set_maximum_amount( $amount ) { + $this->set_prop( 'maximum_amount', wc_format_decimal( $amount ) ); + } + + /** + * Set email restrictions. + * + * @since 3.0.0 + * @param array $emails List of emails. + */ + public function set_email_restrictions( $emails = array() ) { + $emails = array_filter( array_map( 'sanitize_email', array_map( 'strtolower', (array) $emails ) ) ); + foreach ( $emails as $email ) { + if ( ! is_email( $email ) ) { + $this->error( 'coupon_invalid_email_address', __( 'Invalid email address restriction', 'woocommerce' ) ); + } + } + $this->set_prop( 'email_restrictions', $emails ); + } + + /** + * Set which users have used this coupon. + * + * @since 3.0.0 + * @param array $used_by List of user IDs. + */ + public function set_used_by( $used_by ) { + $this->set_prop( 'used_by', array_filter( $used_by ) ); + } + + /** + * Set coupon virtual state. + * + * @param boolean $virtual Whether it is virtual or not. + * @since 3.2.0 + */ + public function set_virtual( $virtual ) { + $this->set_prop( 'virtual', (bool) $virtual ); + } + + /* + |-------------------------------------------------------------------------- + | Other Actions + |-------------------------------------------------------------------------- + */ + + /** + * Developers can programmatically return coupons. This function will read those values into our WC_Coupon class. + * + * @since 3.0.0 + * @param string $code Coupon code. + * @param array $coupon Array of coupon properties. + */ + public function read_manual_coupon( $code, $coupon ) { + foreach ( $coupon as $key => $value ) { + switch ( $key ) { + case 'excluded_product_ids': + case 'exclude_product_ids': + if ( ! is_array( $coupon[ $key ] ) ) { + wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' ); + $coupon['excluded_product_ids'] = wc_string_to_array( $value ); + } + break; + case 'exclude_product_categories': + case 'excluded_product_categories': + if ( ! is_array( $coupon[ $key ] ) ) { + wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' ); + $coupon['excluded_product_categories'] = wc_string_to_array( $value ); + } + break; + case 'product_ids': + if ( ! is_array( $coupon[ $key ] ) ) { + wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' ); + $coupon[ $key ] = wc_string_to_array( $value ); + } + break; + case 'individual_use': + case 'free_shipping': + case 'exclude_sale_items': + if ( ! is_bool( $coupon[ $key ] ) ) { + wc_doing_it_wrong( $key, $key . ' should be true or false instead of yes or no.', '3.0' ); + $coupon[ $key ] = wc_string_to_bool( $value ); + } + break; + case 'expiry_date': + $coupon['date_expires'] = $value; + break; + } + } + $this->set_props( $coupon ); + $this->set_code( $code ); + $this->set_id( 0 ); + $this->set_virtual( true ); + } + + /** + * Increase usage count for current coupon. + * + * @param string $used_by Either user ID or billing email. + * @param WC_Order $order If provided, will clear the coupons held by this order. + */ + public function increase_usage_count( $used_by = '', $order = null ) { + if ( $this->get_id() && $this->data_store ) { + $new_count = $this->data_store->increase_usage_count( $this, $used_by, $order ); + + // Bypass set_prop and remove pending changes since the data store saves the count already. + $this->data['usage_count'] = $new_count; + if ( isset( $this->changes['usage_count'] ) ) { + unset( $this->changes['usage_count'] ); + } + } + } + + /** + * Decrease usage count for current coupon. + * + * @param string $used_by Either user ID or billing email. + */ + public function decrease_usage_count( $used_by = '' ) { + if ( $this->get_id() && $this->get_usage_count() > 0 && $this->data_store ) { + $new_count = $this->data_store->decrease_usage_count( $this, $used_by ); + + // Bypass set_prop and remove pending changes since the data store saves the count already. + $this->data['usage_count'] = $new_count; + if ( isset( $this->changes['usage_count'] ) ) { + unset( $this->changes['usage_count'] ); + } + } + } + + /* + |-------------------------------------------------------------------------- + | Validation & Error Handling + |-------------------------------------------------------------------------- + */ + + /** + * Returns the error_message string. + + * @return string + */ + public function get_error_message() { + return $this->error_message; + } + + /** + * Check if a coupon is valid for the cart. + * + * @deprecated 3.2.0 In favor of WC_Discounts->is_coupon_valid. + * @return bool + */ + public function is_valid() { + $discounts = new WC_Discounts( WC()->cart ); + $valid = $discounts->is_coupon_valid( $this ); + + if ( is_wp_error( $valid ) ) { + $this->error_message = $valid->get_error_message(); + return false; + } + + return $valid; + } + + /** + * Check if a coupon is valid. + * + * @return bool + */ + public function is_valid_for_cart() { + return apply_filters( 'woocommerce_coupon_is_valid_for_cart', $this->is_type( wc_get_cart_coupon_types() ), $this ); + } + + /** + * Check if a coupon is valid for a product. + * + * @param WC_Product $product Product instance. + * @param array $values Values. + * @return bool + */ + public function is_valid_for_product( $product, $values = array() ) { + if ( ! $this->is_type( wc_get_product_coupon_types() ) ) { + return apply_filters( 'woocommerce_coupon_is_valid_for_product', false, $product, $this, $values ); + } + + $valid = false; + $product_cats = wc_get_product_cat_ids( $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id() ); + $product_ids = array( $product->get_id(), $product->get_parent_id() ); + + // Specific products get the discount. + if ( count( $this->get_product_ids() ) && count( array_intersect( $product_ids, $this->get_product_ids() ) ) ) { + $valid = true; + } + + // Category discounts. + if ( count( $this->get_product_categories() ) && count( array_intersect( $product_cats, $this->get_product_categories() ) ) ) { + $valid = true; + } + + // No product ids - all items discounted. + if ( ! count( $this->get_product_ids() ) && ! count( $this->get_product_categories() ) ) { + $valid = true; + } + + // Specific product IDs excluded from the discount. + if ( count( $this->get_excluded_product_ids() ) && count( array_intersect( $product_ids, $this->get_excluded_product_ids() ) ) ) { + $valid = false; + } + + // Specific categories excluded from the discount. + if ( count( $this->get_excluded_product_categories() ) && count( array_intersect( $product_cats, $this->get_excluded_product_categories() ) ) ) { + $valid = false; + } + + // Sale Items excluded from discount. + if ( $this->get_exclude_sale_items() && $product->is_on_sale() ) { + $valid = false; + } + + return apply_filters( 'woocommerce_coupon_is_valid_for_product', $valid, $product, $this, $values ); + } + + /** + * Converts one of the WC_Coupon message/error codes to a message string and. + * displays the message/error. + * + * @param int $msg_code Message/error code. + */ + public function add_coupon_message( $msg_code ) { + $msg = $msg_code < 200 ? $this->get_coupon_error( $msg_code ) : $this->get_coupon_message( $msg_code ); + + if ( ! $msg ) { + return; + } + + if ( $msg_code < 200 ) { + wc_add_notice( $msg, 'error' ); + } else { + wc_add_notice( $msg ); + } + } + + /** + * Map one of the WC_Coupon message codes to a message string. + * + * @param integer $msg_code Message code. + * @return string Message/error string. + */ + public function get_coupon_message( $msg_code ) { + switch ( $msg_code ) { + case self::WC_COUPON_SUCCESS: + $msg = __( 'Coupon code applied successfully.', 'woocommerce' ); + break; + case self::WC_COUPON_REMOVED: + $msg = __( 'Coupon code removed successfully.', 'woocommerce' ); + break; + default: + $msg = ''; + break; + } + return apply_filters( 'woocommerce_coupon_message', $msg, $msg_code, $this ); + } + + /** + * Map one of the WC_Coupon error codes to a message string. + * + * @param int $err_code Message/error code. + * @return string Message/error string + */ + public function get_coupon_error( $err_code ) { + switch ( $err_code ) { + case self::E_WC_COUPON_INVALID_FILTERED: + $err = __( 'Coupon is not valid.', 'woocommerce' ); + break; + case self::E_WC_COUPON_NOT_EXIST: + /* translators: %s: coupon code */ + $err = sprintf( __( 'Coupon "%s" does not exist!', 'woocommerce' ), esc_html( $this->get_code() ) ); + break; + case self::E_WC_COUPON_INVALID_REMOVED: + /* translators: %s: coupon code */ + $err = sprintf( __( 'Sorry, it seems the coupon "%s" is invalid - it has now been removed from your order.', 'woocommerce' ), esc_html( $this->get_code() ) ); + break; + case self::E_WC_COUPON_NOT_YOURS_REMOVED: + /* translators: %s: coupon code */ + $err = sprintf( __( 'Sorry, it seems the coupon "%s" is not yours - it has now been removed from your order.', 'woocommerce' ), esc_html( $this->get_code() ) ); + break; + case self::E_WC_COUPON_ALREADY_APPLIED: + $err = __( 'Coupon code already applied!', 'woocommerce' ); + break; + case self::E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY: + /* translators: %s: coupon code */ + $err = sprintf( __( 'Sorry, coupon "%s" has already been applied and cannot be used in conjunction with other coupons.', 'woocommerce' ), esc_html( $this->get_code() ) ); + break; + case self::E_WC_COUPON_USAGE_LIMIT_REACHED: + $err = __( 'Coupon usage limit has been reached.', 'woocommerce' ); + break; + case self::E_WC_COUPON_EXPIRED: + $err = __( 'This coupon has expired.', 'woocommerce' ); + break; + case self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET: + /* translators: %s: coupon minimum amount */ + $err = sprintf( __( 'The minimum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->get_minimum_amount() ) ); + break; + case self::E_WC_COUPON_MAX_SPEND_LIMIT_MET: + /* translators: %s: coupon maximum amount */ + $err = sprintf( __( 'The maximum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->get_maximum_amount() ) ); + break; + case self::E_WC_COUPON_NOT_APPLICABLE: + $err = __( 'Sorry, this coupon is not applicable to your cart contents.', 'woocommerce' ); + break; + case self::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK: + if ( is_user_logged_in() && wc_get_page_id( 'myaccount' ) > 0 ) { + /* translators: %s: myaccount page link. */ + $err = sprintf( __( 'Coupon usage limit has been reached. If you were using this coupon just now but order was not complete, you can retry or cancel the order by going to the my account page.', 'woocommerce' ), wc_get_endpoint_url( 'orders', '', wc_get_page_permalink( 'myaccount' ) ) ); + } else { + $err = $this->get_coupon_error( self::E_WC_COUPON_USAGE_LIMIT_REACHED ); + } + break; + case self::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST: + $err = __( 'Coupon usage limit has been reached. Please try again after some time, or contact us for help.', 'woocommerce' ); + break; + case self::E_WC_COUPON_EXCLUDED_PRODUCTS: + // Store excluded products that are in cart in $products. + $products = array(); + if ( ! WC()->cart->is_empty() ) { + foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + if ( in_array( intval( $cart_item['product_id'] ), $this->get_excluded_product_ids(), true ) || in_array( intval( $cart_item['variation_id'] ), $this->get_excluded_product_ids(), true ) || in_array( intval( $cart_item['data']->get_parent_id() ), $this->get_excluded_product_ids(), true ) ) { + $products[] = $cart_item['data']->get_name(); + } + } + } + + /* translators: %s: products list */ + $err = sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) ); + break; + case self::E_WC_COUPON_EXCLUDED_CATEGORIES: + // Store excluded categories that are in cart in $categories. + $categories = array(); + if ( ! WC()->cart->is_empty() ) { + foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + $product_cats = wc_get_product_cat_ids( $cart_item['product_id'] ); + $intersect = array_intersect( $product_cats, $this->get_excluded_product_categories() ); + + if ( count( $intersect ) > 0 ) { + foreach ( $intersect as $cat_id ) { + $cat = get_term( $cat_id, 'product_cat' ); + $categories[] = $cat->name; + } + } + } + } + + /* translators: %s: categories list */ + $err = sprintf( __( 'Sorry, this coupon is not applicable to the categories: %s.', 'woocommerce' ), implode( ', ', array_unique( $categories ) ) ); + break; + case self::E_WC_COUPON_NOT_VALID_SALE_ITEMS: + $err = __( 'Sorry, this coupon is not valid for sale items.', 'woocommerce' ); + break; + default: + $err = ''; + break; + } + return apply_filters( 'woocommerce_coupon_error', $err, $err_code, $this ); + } + + /** + * Map one of the WC_Coupon error codes to an error string. + * No coupon instance will be available where a coupon does not exist, + * so this static method exists. + * + * @param int $err_code Error code. + * @return string Error string. + */ + public static function get_generic_coupon_error( $err_code ) { + switch ( $err_code ) { + case self::E_WC_COUPON_NOT_EXIST: + $err = __( 'Coupon does not exist!', 'woocommerce' ); + break; + case self::E_WC_COUPON_PLEASE_ENTER: + $err = __( 'Please enter a coupon code.', 'woocommerce' ); + break; + default: + $err = ''; + break; + } + // When using this static method, there is no $this to pass to filter. + return apply_filters( 'woocommerce_coupon_error', $err, $err_code, null ); + } +} diff --git a/includes/class-wc-customer-download-log.php b/plugins/woocommerce/includes/class-wc-customer-download-log.php similarity index 100% rename from includes/class-wc-customer-download-log.php rename to plugins/woocommerce/includes/class-wc-customer-download-log.php diff --git a/plugins/woocommerce/includes/class-wc-customer-download.php b/plugins/woocommerce/includes/class-wc-customer-download.php new file mode 100644 index 00000000000..43d5a2101af --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-customer-download.php @@ -0,0 +1,412 @@ + '', + 'product_id' => 0, + 'user_id' => 0, + 'user_email' => '', + 'order_id' => 0, + 'order_key' => '', + 'downloads_remaining' => '', + 'access_granted' => null, + 'access_expires' => null, + 'download_count' => 0, + ); + + /** + * Constructor. + * + * @param int|object|array $download Download ID, instance or data. + */ + public function __construct( $download = 0 ) { + parent::__construct( $download ); + + if ( is_numeric( $download ) && $download > 0 ) { + $this->set_id( $download ); + } elseif ( $download instanceof self ) { + $this->set_id( $download->get_id() ); + } elseif ( is_object( $download ) && ! empty( $download->permission_id ) ) { + $this->set_id( $download->permission_id ); + $this->set_props( (array) $download ); + $this->set_object_read( true ); + } else { + $this->set_object_read( true ); + } + + $this->data_store = WC_Data_Store::load( 'customer-download' ); + + if ( $this->get_id() > 0 ) { + $this->data_store->read( $this ); + } + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get download id. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_download_id( $context = 'view' ) { + return $this->get_prop( 'download_id', $context ); + } + + /** + * Get product id. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return integer + */ + public function get_product_id( $context = 'view' ) { + return $this->get_prop( 'product_id', $context ); + } + + /** + * Get user id. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return integer + */ + public function get_user_id( $context = 'view' ) { + return $this->get_prop( 'user_id', $context ); + } + + /** + * Get user_email. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_user_email( $context = 'view' ) { + return $this->get_prop( 'user_email', $context ); + } + + /** + * Get order_id. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return integer + */ + public function get_order_id( $context = 'view' ) { + return $this->get_prop( 'order_id', $context ); + } + + /** + * Get order_key. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_order_key( $context = 'view' ) { + return $this->get_prop( 'order_key', $context ); + } + + /** + * Get downloads_remaining. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return integer|string + */ + public function get_downloads_remaining( $context = 'view' ) { + return $this->get_prop( 'downloads_remaining', $context ); + } + + /** + * Get access_granted. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return WC_DateTime|null Object if the date is set or null if there is no date. + */ + public function get_access_granted( $context = 'view' ) { + return $this->get_prop( 'access_granted', $context ); + } + + /** + * Get access_expires. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return WC_DateTime|null Object if the date is set or null if there is no date. + */ + public function get_access_expires( $context = 'view' ) { + return $this->get_prop( 'access_expires', $context ); + } + + /** + * Get download_count. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return integer + */ + public function get_download_count( $context = 'view' ) { + // Check for count of download logs. + $data_store = WC_Data_Store::load( 'customer-download-log' ); + $download_log_ids = $data_store->get_download_logs_for_permission( $this->get_id() ); + + $download_log_count = 0; + if ( ! empty( $download_log_ids ) ) { + $download_log_count = count( $download_log_ids ); + } + + // Check download count in prop. + $download_count_prop = $this->get_prop( 'download_count', $context ); + + // Return the larger of the two in case they differ. + // If logs are removed for some reason, we should still respect the + // count stored in the prop. + return max( $download_log_count, $download_count_prop ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set download id. + * + * @param string $value Download ID. + */ + public function set_download_id( $value ) { + $this->set_prop( 'download_id', $value ); + } + /** + * Set product id. + * + * @param int $value Product ID. + */ + public function set_product_id( $value ) { + $this->set_prop( 'product_id', absint( $value ) ); + } + + /** + * Set user id. + * + * @param int $value User ID. + */ + public function set_user_id( $value ) { + $this->set_prop( 'user_id', absint( $value ) ); + } + + /** + * Set user_email. + * + * @param int $value User email. + */ + public function set_user_email( $value ) { + $this->set_prop( 'user_email', sanitize_email( $value ) ); + } + + /** + * Set order_id. + * + * @param int $value Order ID. + */ + public function set_order_id( $value ) { + $this->set_prop( 'order_id', absint( $value ) ); + } + + /** + * Set order_key. + * + * @param string $value Order key. + */ + public function set_order_key( $value ) { + $this->set_prop( 'order_key', $value ); + } + + /** + * Set downloads_remaining. + * + * @param integer|string $value Amount of downloads remaining. + */ + public function set_downloads_remaining( $value ) { + $this->set_prop( 'downloads_remaining', '' === $value ? '' : absint( $value ) ); + } + + /** + * Set access_granted. + * + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. + */ + public function set_access_granted( $date = null ) { + $this->set_date_prop( 'access_granted', $date ); + } + + /** + * Set access_expires. + * + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. + */ + public function set_access_expires( $date = null ) { + $this->set_date_prop( 'access_expires', $date ); + } + + /** + * Set download_count. + * + * @param int $value Download count. + */ + public function set_download_count( $value ) { + $this->set_prop( 'download_count', absint( $value ) ); + } + + /** + * Track a download on this permission. + * + * @since 3.3.0 + * @throws Exception When permission ID is invalid. + * @param int $user_id Id of the user performing the download. + * @param string $user_ip_address IP Address of the user performing the download. + */ + public function track_download( $user_id = null, $user_ip_address = null ) { + global $wpdb; + + // Must have a permission_id to track download log. + if ( ! ( $this->get_id() > 0 ) ) { + throw new Exception( __( 'Invalid permission ID.', 'woocommerce' ) ); + } + + // Increment download count, and decrement downloads remaining. + // Use SQL to avoid possible issues with downloads in quick succession. + // If downloads_remaining is blank, leave it blank (unlimited). + // Also, ensure downloads_remaining doesn't drop below zero. + $query = $wpdb->prepare( + " +UPDATE {$wpdb->prefix}woocommerce_downloadable_product_permissions +SET download_count = download_count + 1, +downloads_remaining = IF( downloads_remaining = '', '', GREATEST( 0, downloads_remaining - 1 ) ) +WHERE permission_id = %d", + $this->get_id() + ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( $query ); + + // Re-read this download from the data store to pull updated counts. + $this->data_store->read( $this ); + + // Track download in download log. + $download_log = new WC_Customer_Download_Log(); + $download_log->set_timestamp( time() ); + $download_log->set_permission_id( $this->get_id() ); + + if ( ! is_null( $user_id ) ) { + $download_log->set_user_id( $user_id ); + } + + if ( ! is_null( $user_ip_address ) ) { + $download_log->set_user_ip_address( $user_ip_address ); + } + + $download_log->save(); + } + + /* + |-------------------------------------------------------------------------- + | ArrayAccess/Backwards compatibility. + |-------------------------------------------------------------------------- + */ + + /** + * OffsetGet. + * + * @param mixed $offset Offset. + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet( $offset ) { + if ( is_callable( array( $this, "get_$offset" ) ) ) { + return $this->{"get_$offset"}(); + } + } + + /** + * OffsetSet. + * + * @param mixed $offset Offset. + * @param mixed $value Value. + */ + #[\ReturnTypeWillChange] + public function offsetSet( $offset, $value ) { + if ( is_callable( array( $this, "set_$offset" ) ) ) { + $this->{"set_$offset"}( $value ); + } + } + + /** + * OffsetUnset + * + * @param mixed $offset Offset. + */ + #[\ReturnTypeWillChange] + public function offsetUnset( $offset ) { + if ( is_callable( array( $this, "set_$offset" ) ) ) { + $this->{"set_$offset"}( '' ); + } + } + + /** + * OffsetExists. + * + * @param mixed $offset Offset. + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists( $offset ) { + return in_array( $offset, array_keys( $this->data ), true ); + } + + /** + * Magic __isset method for backwards compatibility. Legacy properties which could be accessed directly in the past. + * + * @param string $key Key name. + * @return bool + */ + public function __isset( $key ) { + return in_array( $key, array_keys( $this->data ), true ); + } + + /** + * Magic __get method for backwards compatibility. Maps legacy vars to new getters. + * + * @param string $key Key name. + * @return mixed + */ + public function __get( $key ) { + if ( is_callable( array( $this, "get_$key" ) ) ) { + return $this->{"get_$key"}( '' ); + } + } +} diff --git a/plugins/woocommerce/includes/class-wc-customer.php b/plugins/woocommerce/includes/class-wc-customer.php new file mode 100644 index 00000000000..43587c5d1e3 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-customer.php @@ -0,0 +1,1149 @@ + null, + 'date_modified' => null, + 'email' => '', + 'first_name' => '', + 'last_name' => '', + 'display_name' => '', + 'role' => 'customer', + 'username' => '', + 'billing' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'postcode' => '', + 'country' => '', + 'state' => '', + 'email' => '', + 'phone' => '', + ), + 'shipping' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'postcode' => '', + 'country' => '', + 'state' => '', + 'phone' => '', + ), + 'is_paying_customer' => false, + ); + + /** + * Stores a password if this needs to be changed. Write-only and hidden from _data. + * + * @var string + */ + protected $password = ''; + + /** + * Stores if user is VAT exempt for this session. + * + * @var string + */ + protected $is_vat_exempt = false; + + /** + * Stores if user has calculated shipping in this session. + * + * @var string + */ + protected $calculated_shipping = false; + + /** + * This is the name of this object type. + * + * @since 5.6.0 + * @var string + */ + protected $object_type = 'customer'; + + /** + * Load customer data based on how WC_Customer is called. + * + * If $customer is 'new', you can build a new WC_Customer object. If it's empty, some + * data will be pulled from the session for the current user/customer. + * + * @param WC_Customer|int $data Customer ID or data. + * @param bool $is_session True if this is the customer session. + * @throws Exception If customer cannot be read/found and $data is set. + */ + public function __construct( $data = 0, $is_session = false ) { + parent::__construct( $data ); + + if ( $data instanceof WC_Customer ) { + $this->set_id( absint( $data->get_id() ) ); + } elseif ( is_numeric( $data ) ) { + $this->set_id( $data ); + } + + $this->data_store = WC_Data_Store::load( 'customer' ); + + // If we have an ID, load the user from the DB. + if ( $this->get_id() ) { + try { + $this->data_store->read( $this ); + } catch ( Exception $e ) { + $this->set_id( 0 ); + $this->set_object_read( true ); + } + } else { + $this->set_object_read( true ); + } + + // If this is a session, set or change the data store to sessions. Changes do not persist in the database. + if ( $is_session && isset( WC()->session ) ) { + $this->data_store = WC_Data_Store::load( 'customer-session' ); + $this->data_store->read( $this ); + } + } + + /** + * Delete a customer and reassign posts.. + * + * @param int $reassign Reassign posts and links to new User ID. + * @since 3.0.0 + * @return bool + */ + public function delete_and_reassign( $reassign = null ) { + if ( $this->data_store ) { + $this->data_store->delete( + $this, + array( + 'force_delete' => true, + 'reassign' => $reassign, + ) + ); + $this->set_id( 0 ); + return true; + } + return false; + } + + /** + * Is customer outside base country (for tax purposes)? + * + * @return bool + */ + public function is_customer_outside_base() { + list( $country, $state ) = $this->get_taxable_address(); + if ( $country ) { + $default = wc_get_base_location(); + if ( $default['country'] !== $country ) { + return true; + } + if ( $default['state'] && $default['state'] !== $state ) { + return true; + } + } + return false; + } + + /** + * Return this customer's avatar. + * + * @since 3.0.0 + * @return string + */ + public function get_avatar_url() { + return get_avatar_url( $this->get_email() ); + } + + /** + * Get taxable address. + * + * @return array + */ + public function get_taxable_address() { + $tax_based_on = get_option( 'woocommerce_tax_based_on' ); + + // Check shipping method at this point to see if we need special handling. + if ( true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ) && count( array_intersect( wc_get_chosen_shipping_method_ids(), apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) ) ) ) > 0 ) { + $tax_based_on = 'base'; + } + + if ( 'base' === $tax_based_on ) { + $country = WC()->countries->get_base_country(); + $state = WC()->countries->get_base_state(); + $postcode = WC()->countries->get_base_postcode(); + $city = WC()->countries->get_base_city(); + } elseif ( 'billing' === $tax_based_on ) { + $country = $this->get_billing_country(); + $state = $this->get_billing_state(); + $postcode = $this->get_billing_postcode(); + $city = $this->get_billing_city(); + } else { + $country = $this->get_shipping_country(); + $state = $this->get_shipping_state(); + $postcode = $this->get_shipping_postcode(); + $city = $this->get_shipping_city(); + } + + return apply_filters( 'woocommerce_customer_taxable_address', array( $country, $state, $postcode, $city ) ); + } + + /** + * Gets a customer's downloadable products. + * + * @return array Array of downloadable products + */ + public function get_downloadable_products() { + $downloads = array(); + if ( $this->get_id() ) { + $downloads = wc_get_customer_available_downloads( $this->get_id() ); + } + return apply_filters( 'woocommerce_customer_get_downloadable_products', $downloads ); + } + + /** + * Is customer VAT exempt? + * + * @return bool + */ + public function is_vat_exempt() { + return $this->get_is_vat_exempt(); + } + + /** + * Has calculated shipping? + * + * @return bool + */ + public function has_calculated_shipping() { + return $this->get_calculated_shipping(); + } + + /** + * Indicates if the customer has a non-empty shipping address. + * + * Note that this does not indicate if the customer's shipping address + * is complete, only that one or more fields are populated. + * + * @since 5.3.0 + * + * @return bool + */ + public function has_shipping_address() { + foreach ( $this->get_shipping() as $address_field ) { + // Trim guards against a case where a subset of saved shipping address fields contain whitespace. + if ( strlen( trim( $address_field ) ) > 0 ) { + return true; + } + } + + return false; + } + + /** + * Get if customer is VAT exempt? + * + * @since 3.0.0 + * @return bool + */ + public function get_is_vat_exempt() { + return $this->is_vat_exempt; + } + + /** + * Get password (only used when updating the user object). + * + * @return string + */ + public function get_password() { + return $this->password; + } + + /** + * Has customer calculated shipping? + * + * @return bool + */ + public function get_calculated_shipping() { + return $this->calculated_shipping; + } + + /** + * Set if customer has tax exemption. + * + * @param bool $is_vat_exempt If is vat exempt. + */ + public function set_is_vat_exempt( $is_vat_exempt ) { + $this->is_vat_exempt = wc_string_to_bool( $is_vat_exempt ); + } + + /** + * Calculated shipping? + * + * @param bool $calculated If shipping is calculated. + */ + public function set_calculated_shipping( $calculated = true ) { + $this->calculated_shipping = wc_string_to_bool( $calculated ); + } + + /** + * Set customer's password. + * + * @since 3.0.0 + * @param string $password Password. + */ + public function set_password( $password ) { + $this->password = $password; + } + + /** + * Gets the customers last order. + * + * @return WC_Order|false + */ + public function get_last_order() { + return $this->data_store->get_last_order( $this ); + } + + /** + * Return the number of orders this customer has. + * + * @return integer + */ + public function get_order_count() { + return $this->data_store->get_order_count( $this ); + } + + /** + * Return how much money this customer has spent. + * + * @return float + */ + public function get_total_spent() { + return $this->data_store->get_total_spent( $this ); + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Return the customer's username. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_username( $context = 'view' ) { + return $this->get_prop( 'username', $context ); + } + + /** + * Return the customer's email. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_email( $context = 'view' ) { + return $this->get_prop( 'email', $context ); + } + + /** + * Return customer's first name. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_first_name( $context = 'view' ) { + return $this->get_prop( 'first_name', $context ); + } + + /** + * Return customer's last name. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_last_name( $context = 'view' ) { + return $this->get_prop( 'last_name', $context ); + } + + /** + * Return customer's display name. + * + * @since 3.1.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_display_name( $context = 'view' ) { + return $this->get_prop( 'display_name', $context ); + } + + /** + * Return customer's user role. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_role( $context = 'view' ) { + return $this->get_prop( 'role', $context ); + } + + /** + * Return the date this customer was created. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return WC_DateTime|null object if the date is set or null if there is no date. + */ + public function get_date_created( $context = 'view' ) { + return $this->get_prop( 'date_created', $context ); + } + + /** + * Return the date this customer was last updated. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return WC_DateTime|null object if the date is set or null if there is no date. + */ + public function get_date_modified( $context = 'view' ) { + return $this->get_prop( 'date_modified', $context ); + } + + /** + * Gets a prop for a getter method. + * + * @since 3.0.0 + * @param string $prop Name of prop to get. + * @param string $address billing or shipping. + * @param string $context What the value is for. Valid values are 'view' and 'edit'. What the value is for. Valid values are view and edit. + * @return mixed + */ + protected function get_address_prop( $prop, $address = 'billing', $context = 'view' ) { + $value = null; + + if ( array_key_exists( $prop, $this->data[ $address ] ) ) { + $value = isset( $this->changes[ $address ][ $prop ] ) ? $this->changes[ $address ][ $prop ] : $this->data[ $address ][ $prop ]; + + if ( 'view' === $context ) { + $value = apply_filters( $this->get_hook_prefix() . $address . '_' . $prop, $value, $this ); + } + } + return $value; + } + + /** + * Get billing. + * + * @since 3.2.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return array + */ + public function get_billing( $context = 'view' ) { + $value = null; + $prop = 'billing'; + + if ( array_key_exists( $prop, $this->data ) ) { + $changes = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : array(); + $value = array_merge( $this->data[ $prop ], $changes ); + + if ( 'view' === $context ) { + $value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this ); + } + } + + return $value; + } + + /** + * Get billing_first_name. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_billing_first_name( $context = 'view' ) { + return $this->get_address_prop( 'first_name', 'billing', $context ); + } + + /** + * Get billing_last_name. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_billing_last_name( $context = 'view' ) { + return $this->get_address_prop( 'last_name', 'billing', $context ); + } + + /** + * Get billing_company. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_billing_company( $context = 'view' ) { + return $this->get_address_prop( 'company', 'billing', $context ); + } + + /** + * Get billing_address_1. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_billing_address( $context = 'view' ) { + return $this->get_billing_address_1( $context ); + } + + /** + * Get billing_address_1. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_billing_address_1( $context = 'view' ) { + return $this->get_address_prop( 'address_1', 'billing', $context ); + } + + /** + * Get billing_address_2. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string $value + */ + public function get_billing_address_2( $context = 'view' ) { + return $this->get_address_prop( 'address_2', 'billing', $context ); + } + + /** + * Get billing_city. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string $value + */ + public function get_billing_city( $context = 'view' ) { + return $this->get_address_prop( 'city', 'billing', $context ); + } + + /** + * Get billing_state. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_billing_state( $context = 'view' ) { + return $this->get_address_prop( 'state', 'billing', $context ); + } + + /** + * Get billing_postcode. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_billing_postcode( $context = 'view' ) { + return $this->get_address_prop( 'postcode', 'billing', $context ); + } + + /** + * Get billing_country. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_billing_country( $context = 'view' ) { + return $this->get_address_prop( 'country', 'billing', $context ); + } + + /** + * Get billing_email. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_billing_email( $context = 'view' ) { + return $this->get_address_prop( 'email', 'billing', $context ); + } + + /** + * Get billing_phone. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_billing_phone( $context = 'view' ) { + return $this->get_address_prop( 'phone', 'billing', $context ); + } + + /** + * Get shipping. + * + * @since 3.2.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return array + */ + public function get_shipping( $context = 'view' ) { + $value = null; + $prop = 'shipping'; + + if ( array_key_exists( $prop, $this->data ) ) { + $changes = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : array(); + $value = array_merge( $this->data[ $prop ], $changes ); + + if ( 'view' === $context ) { + $value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this ); + } + } + + return $value; + } + + /** + * Get shipping_first_name. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_first_name( $context = 'view' ) { + return $this->get_address_prop( 'first_name', 'shipping', $context ); + } + + /** + * Get shipping_last_name. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_last_name( $context = 'view' ) { + return $this->get_address_prop( 'last_name', 'shipping', $context ); + } + + /** + * Get shipping_company. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_company( $context = 'view' ) { + return $this->get_address_prop( 'company', 'shipping', $context ); + } + + /** + * Get shipping_address_1. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_address( $context = 'view' ) { + return $this->get_shipping_address_1( $context ); + } + + /** + * Get shipping_address_1. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_address_1( $context = 'view' ) { + return $this->get_address_prop( 'address_1', 'shipping', $context ); + } + + /** + * Get shipping_address_2. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_address_2( $context = 'view' ) { + return $this->get_address_prop( 'address_2', 'shipping', $context ); + } + + /** + * Get shipping_city. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_city( $context = 'view' ) { + return $this->get_address_prop( 'city', 'shipping', $context ); + } + + /** + * Get shipping_state. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_state( $context = 'view' ) { + return $this->get_address_prop( 'state', 'shipping', $context ); + } + + /** + * Get shipping_postcode. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_postcode( $context = 'view' ) { + return $this->get_address_prop( 'postcode', 'shipping', $context ); + } + + /** + * Get shipping_country. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_country( $context = 'view' ) { + return $this->get_address_prop( 'country', 'shipping', $context ); + } + + /** + * Get shipping phone. + * + * @since 5.6.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_phone( $context = 'view' ) { + return $this->get_address_prop( 'phone', 'shipping', $context ); + } + + /** + * Is the user a paying customer? + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return bool + */ + public function get_is_paying_customer( $context = 'view' ) { + return $this->get_prop( 'is_paying_customer', $context ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set customer's username. + * + * @since 3.0.0 + * @param string $username Username. + */ + public function set_username( $username ) { + $this->set_prop( 'username', $username ); + } + + /** + * Set customer's email. + * + * @since 3.0.0 + * @param string $value Email. + */ + public function set_email( $value ) { + if ( $value && ! is_email( $value ) ) { + $this->error( 'customer_invalid_email', __( 'Invalid email address', 'woocommerce' ) ); + } + $this->set_prop( 'email', sanitize_email( $value ) ); + } + + /** + * Set customer's first name. + * + * @since 3.0.0 + * @param string $first_name First name. + */ + public function set_first_name( $first_name ) { + $this->set_prop( 'first_name', $first_name ); + } + + /** + * Set customer's last name. + * + * @since 3.0.0 + * @param string $last_name Last name. + */ + public function set_last_name( $last_name ) { + $this->set_prop( 'last_name', $last_name ); + } + + /** + * Set customer's display name. + * + * @since 3.1.0 + * @param string $display_name Display name. + */ + public function set_display_name( $display_name ) { + /* translators: 1: first name 2: last name */ + $this->set_prop( 'display_name', is_email( $display_name ) ? sprintf( _x( '%1$s %2$s', 'display name', 'woocommerce' ), $this->get_first_name(), $this->get_last_name() ) : $display_name ); + } + + /** + * Set customer's user role(s). + * + * @since 3.0.0 + * @param mixed $role User role. + */ + public function set_role( $role ) { + global $wp_roles; + + if ( $role && ! empty( $wp_roles->roles ) && ! in_array( $role, array_keys( $wp_roles->roles ), true ) ) { + $this->error( 'customer_invalid_role', __( 'Invalid role', 'woocommerce' ) ); + } + $this->set_prop( 'role', $role ); + } + + /** + * Set the date this customer was last updated. + * + * @since 3.0.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. + */ + public function set_date_created( $date = null ) { + $this->set_date_prop( 'date_created', $date ); + } + + /** + * Set the date this customer was last updated. + * + * @since 3.0.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. + */ + public function set_date_modified( $date = null ) { + $this->set_date_prop( 'date_modified', $date ); + } + + /** + * Set customer address to match shop base address. + * + * @since 3.0.0 + */ + public function set_billing_address_to_base() { + $base = wc_get_customer_default_location(); + $this->set_billing_location( $base['country'], $base['state'], '', '' ); + } + + /** + * Set customer shipping address to base address. + * + * @since 3.0.0 + */ + public function set_shipping_address_to_base() { + $base = wc_get_customer_default_location(); + $this->set_shipping_location( $base['country'], $base['state'], '', '' ); + } + + /** + * Sets all address info at once. + * + * @param string $country Country. + * @param string $state State. + * @param string $postcode Postcode. + * @param string $city City. + */ + public function set_billing_location( $country, $state = '', $postcode = '', $city = '' ) { + $address_data = $this->get_prop( 'billing', 'edit' ); + + $address_data['address_1'] = ''; + $address_data['address_2'] = ''; + $address_data['city'] = $city; + $address_data['state'] = $state; + $address_data['postcode'] = $postcode; + $address_data['country'] = $country; + + $this->set_prop( 'billing', $address_data ); + } + + /** + * Sets all shipping info at once. + * + * @param string $country Country. + * @param string $state State. + * @param string $postcode Postcode. + * @param string $city City. + */ + public function set_shipping_location( $country, $state = '', $postcode = '', $city = '' ) { + $address_data = $this->get_prop( 'shipping', 'edit' ); + + $address_data['address_1'] = ''; + $address_data['address_2'] = ''; + $address_data['city'] = $city; + $address_data['state'] = $state; + $address_data['postcode'] = $postcode; + $address_data['country'] = $country; + + $this->set_prop( 'shipping', $address_data ); + } + + /** + * Sets a prop for a setter method. + * + * @since 3.0.0 + * @param string $prop Name of prop to set. + * @param string $address Name of address to set. billing or shipping. + * @param mixed $value Value of the prop. + */ + protected function set_address_prop( $prop, $address, $value ) { + if ( array_key_exists( $prop, $this->data[ $address ] ) ) { + if ( true === $this->object_read ) { + if ( $value !== $this->data[ $address ][ $prop ] || ( isset( $this->changes[ $address ] ) && array_key_exists( $prop, $this->changes[ $address ] ) ) ) { + $this->changes[ $address ][ $prop ] = $value; + } + } else { + $this->data[ $address ][ $prop ] = $value; + } + } + } + + /** + * Set billing_first_name. + * + * @param string $value Billing first name. + */ + public function set_billing_first_name( $value ) { + $this->set_address_prop( 'first_name', 'billing', $value ); + } + + /** + * Set billing_last_name. + * + * @param string $value Billing last name. + */ + public function set_billing_last_name( $value ) { + $this->set_address_prop( 'last_name', 'billing', $value ); + } + + /** + * Set billing_company. + * + * @param string $value Billing company. + */ + public function set_billing_company( $value ) { + $this->set_address_prop( 'company', 'billing', $value ); + } + + /** + * Set billing_address_1. + * + * @param string $value Billing address line 1. + */ + public function set_billing_address( $value ) { + $this->set_billing_address_1( $value ); + } + + /** + * Set billing_address_1. + * + * @param string $value Billing address line 1. + */ + public function set_billing_address_1( $value ) { + $this->set_address_prop( 'address_1', 'billing', $value ); + } + + /** + * Set billing_address_2. + * + * @param string $value Billing address line 2. + */ + public function set_billing_address_2( $value ) { + $this->set_address_prop( 'address_2', 'billing', $value ); + } + + /** + * Set billing_city. + * + * @param string $value Billing city. + */ + public function set_billing_city( $value ) { + $this->set_address_prop( 'city', 'billing', $value ); + } + + /** + * Set billing_state. + * + * @param string $value Billing state. + */ + public function set_billing_state( $value ) { + $this->set_address_prop( 'state', 'billing', $value ); + } + + /** + * Set billing_postcode. + * + * @param string $value Billing postcode. + */ + public function set_billing_postcode( $value ) { + $this->set_address_prop( 'postcode', 'billing', $value ); + } + + /** + * Set billing_country. + * + * @param string $value Billing country. + */ + public function set_billing_country( $value ) { + $this->set_address_prop( 'country', 'billing', $value ); + } + + /** + * Set billing_email. + * + * @param string $value Billing email. + */ + public function set_billing_email( $value ) { + if ( $value && ! is_email( $value ) ) { + $this->error( 'customer_invalid_billing_email', __( 'Invalid billing email address', 'woocommerce' ) ); + } + $this->set_address_prop( 'email', 'billing', sanitize_email( $value ) ); + } + + /** + * Set billing_phone. + * + * @param string $value Billing phone. + */ + public function set_billing_phone( $value ) { + $this->set_address_prop( 'phone', 'billing', $value ); + } + + /** + * Set shipping_first_name. + * + * @param string $value Shipping first name. + */ + public function set_shipping_first_name( $value ) { + $this->set_address_prop( 'first_name', 'shipping', $value ); + } + + /** + * Set shipping_last_name. + * + * @param string $value Shipping last name. + */ + public function set_shipping_last_name( $value ) { + $this->set_address_prop( 'last_name', 'shipping', $value ); + } + + /** + * Set shipping_company. + * + * @param string $value Shipping company. + */ + public function set_shipping_company( $value ) { + $this->set_address_prop( 'company', 'shipping', $value ); + } + + /** + * Set shipping_address_1. + * + * @param string $value Shipping address line 1. + */ + public function set_shipping_address( $value ) { + $this->set_shipping_address_1( $value ); + } + + /** + * Set shipping_address_1. + * + * @param string $value Shipping address line 1. + */ + public function set_shipping_address_1( $value ) { + $this->set_address_prop( 'address_1', 'shipping', $value ); + } + + /** + * Set shipping_address_2. + * + * @param string $value Shipping address line 2. + */ + public function set_shipping_address_2( $value ) { + $this->set_address_prop( 'address_2', 'shipping', $value ); + } + + /** + * Set shipping_city. + * + * @param string $value Shipping city. + */ + public function set_shipping_city( $value ) { + $this->set_address_prop( 'city', 'shipping', $value ); + } + + /** + * Set shipping_state. + * + * @param string $value Shipping state. + */ + public function set_shipping_state( $value ) { + $this->set_address_prop( 'state', 'shipping', $value ); + } + + /** + * Set shipping_postcode. + * + * @param string $value Shipping postcode. + */ + public function set_shipping_postcode( $value ) { + $this->set_address_prop( 'postcode', 'shipping', $value ); + } + + /** + * Set shipping_country. + * + * @param string $value Shipping country. + */ + public function set_shipping_country( $value ) { + $this->set_address_prop( 'country', 'shipping', $value ); + } + + /** + * Set shipping phone. + * + * @since 5.6.0 + * @param string $value Shipping phone. + */ + public function set_shipping_phone( $value ) { + $this->set_address_prop( 'phone', 'shipping', $value ); + } + + /** + * Set if the user a paying customer. + * + * @since 3.0.0 + * @param bool $is_paying_customer If is a paying customer. + */ + public function set_is_paying_customer( $is_paying_customer ) { + $this->set_prop( 'is_paying_customer', (bool) $is_paying_customer ); + } +} diff --git a/includes/class-wc-data-exception.php b/plugins/woocommerce/includes/class-wc-data-exception.php similarity index 100% rename from includes/class-wc-data-exception.php rename to plugins/woocommerce/includes/class-wc-data-exception.php diff --git a/includes/class-wc-data-store.php b/plugins/woocommerce/includes/class-wc-data-store.php similarity index 100% rename from includes/class-wc-data-store.php rename to plugins/woocommerce/includes/class-wc-data-store.php diff --git a/plugins/woocommerce/includes/class-wc-datetime.php b/plugins/woocommerce/includes/class-wc-datetime.php new file mode 100644 index 00000000000..839312b6e0a --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-datetime.php @@ -0,0 +1,106 @@ +format( DATE_ATOM ); + } + + /** + * Set UTC offset - this is a fixed offset instead of a timezone. + * + * @param int $offset Offset. + */ + public function set_utc_offset( $offset ) { + $this->utc_offset = intval( $offset ); + } + + /** + * Get UTC offset if set, or default to the DateTime object's offset. + */ + #[\ReturnTypeWillChange] + public function getOffset() { + return $this->utc_offset ?: parent::getOffset(); + } + + /** + * Set timezone. + * + * @param DateTimeZone $timezone DateTimeZone instance. + * @return DateTime + */ + #[\ReturnTypeWillChange] + public function setTimezone( $timezone ) { + $this->utc_offset = 0; + return parent::setTimezone( $timezone ); + } + + /** + * Missing in PHP 5.2 so just here so it can be supported consistently. + * + * @since 3.0.0 + * @return int + */ + #[\ReturnTypeWillChange] + public function getTimestamp() { + return method_exists( 'DateTime', 'getTimestamp' ) ? parent::getTimestamp() : $this->format( 'U' ); + } + + /** + * Get the timestamp with the WordPress timezone offset added or subtracted. + * + * @since 3.0.0 + * @return int + */ + public function getOffsetTimestamp() { + return $this->getTimestamp() + $this->getOffset(); + } + + /** + * Format a date based on the offset timestamp. + * + * @since 3.0.0 + * @param string $format Date format. + * @return string + */ + public function date( $format ) { + return gmdate( $format, $this->getOffsetTimestamp() ); + } + + /** + * Return a localised date based on offset timestamp. Wrapper for date_i18n function. + * + * @since 3.0.0 + * @param string $format Date format. + * @return string + */ + public function date_i18n( $format = 'Y-m-d' ) { + return date_i18n( $format, $this->getOffsetTimestamp() ); + } +} diff --git a/includes/class-wc-deprecated-action-hooks.php b/plugins/woocommerce/includes/class-wc-deprecated-action-hooks.php similarity index 100% rename from includes/class-wc-deprecated-action-hooks.php rename to plugins/woocommerce/includes/class-wc-deprecated-action-hooks.php diff --git a/includes/class-wc-deprecated-filter-hooks.php b/plugins/woocommerce/includes/class-wc-deprecated-filter-hooks.php similarity index 100% rename from includes/class-wc-deprecated-filter-hooks.php rename to plugins/woocommerce/includes/class-wc-deprecated-filter-hooks.php diff --git a/plugins/woocommerce/includes/class-wc-discounts.php b/plugins/woocommerce/includes/class-wc-discounts.php new file mode 100644 index 00000000000..63ff52691cb --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-discounts.php @@ -0,0 +1,1021 @@ + Item Key => Value + */ + protected $discounts = array(); + + /** + * WC_Discounts Constructor. + * + * @param WC_Cart|WC_Order $object Cart or order object. + */ + public function __construct( $object = null ) { + if ( is_a( $object, 'WC_Cart' ) ) { + $this->set_items_from_cart( $object ); + } elseif ( is_a( $object, 'WC_Order' ) ) { + $this->set_items_from_order( $object ); + } + } + + /** + * Set items directly. Used by WC_Cart_Totals. + * + * @since 3.2.3 + * @param array $items Items to set. + */ + public function set_items( $items ) { + $this->items = $items; + $this->discounts = array(); + uasort( $this->items, array( $this, 'sort_by_price' ) ); + } + + /** + * Normalise cart items which will be discounted. + * + * @since 3.2.0 + * @param WC_Cart $cart Cart object. + */ + public function set_items_from_cart( $cart ) { + $this->items = array(); + $this->discounts = array(); + + if ( ! is_a( $cart, 'WC_Cart' ) ) { + return; + } + + $this->object = $cart; + + foreach ( $cart->get_cart() as $key => $cart_item ) { + $item = new stdClass(); + $item->key = $key; + $item->object = $cart_item; + $item->product = $cart_item['data']; + $item->quantity = $cart_item['quantity']; + $item->price = wc_add_number_precision_deep( (float) $item->product->get_price() * (float) $item->quantity ); + $this->items[ $key ] = $item; + } + + uasort( $this->items, array( $this, 'sort_by_price' ) ); + } + + /** + * Normalise order items which will be discounted. + * + * @since 3.2.0 + * @param WC_Order $order Order object. + */ + public function set_items_from_order( $order ) { + $this->items = array(); + $this->discounts = array(); + + if ( ! is_a( $order, 'WC_Order' ) ) { + return; + } + + $this->object = $order; + + foreach ( $order->get_items() as $order_item ) { + $item = new stdClass(); + $item->key = $order_item->get_id(); + $item->object = $order_item; + $item->product = $order_item->get_product(); + $item->quantity = $order_item->get_quantity(); + $item->price = wc_add_number_precision_deep( $order_item->get_subtotal() ); + + if ( $order->get_prices_include_tax() ) { + $item->price += wc_add_number_precision_deep( $order_item->get_subtotal_tax() ); + } + + $this->items[ $order_item->get_id() ] = $item; + } + + uasort( $this->items, array( $this, 'sort_by_price' ) ); + } + + /** + * Get the object concerned. + * + * @since 3.3.2 + * @return object + */ + public function get_object() { + return $this->object; + } + + /** + * Get items. + * + * @since 3.2.0 + * @return object[] + */ + public function get_items() { + return $this->items; + } + + /** + * Get items to validate. + * + * @since 3.3.2 + * @return object[] + */ + public function get_items_to_validate() { + return apply_filters( 'woocommerce_coupon_get_items_to_validate', $this->get_items(), $this ); + } + + /** + * Get discount by key with or without precision. + * + * @since 3.2.0 + * @param string $key name of discount row to return. + * @param bool $in_cents Should the totals be returned in cents, or without precision. + * @return float + */ + public function get_discount( $key, $in_cents = false ) { + $item_discount_totals = $this->get_discounts_by_item( $in_cents ); + return isset( $item_discount_totals[ $key ] ) ? $item_discount_totals[ $key ] : 0; + } + + /** + * Get all discount totals. + * + * @since 3.2.0 + * @param bool $in_cents Should the totals be returned in cents, or without precision. + * @return array + */ + public function get_discounts( $in_cents = false ) { + $discounts = $this->discounts; + return $in_cents ? $discounts : wc_remove_number_precision_deep( $discounts ); + } + + /** + * Get all discount totals per item. + * + * @since 3.2.0 + * @param bool $in_cents Should the totals be returned in cents, or without precision. + * @return array + */ + public function get_discounts_by_item( $in_cents = false ) { + $discounts = $this->discounts; + $item_discount_totals = (array) array_shift( $discounts ); + + foreach ( $discounts as $item_discounts ) { + foreach ( $item_discounts as $item_key => $item_discount ) { + $item_discount_totals[ $item_key ] += $item_discount; + } + } + + return $in_cents ? $item_discount_totals : wc_remove_number_precision_deep( $item_discount_totals ); + } + + /** + * Get all discount totals per coupon. + * + * @since 3.2.0 + * @param bool $in_cents Should the totals be returned in cents, or without precision. + * @return array + */ + public function get_discounts_by_coupon( $in_cents = false ) { + $coupon_discount_totals = array_map( 'array_sum', $this->discounts ); + + return $in_cents ? $coupon_discount_totals : wc_remove_number_precision_deep( $coupon_discount_totals ); + } + + /** + * Get discounted price of an item without precision. + * + * @since 3.2.0 + * @param object $item Get data for this item. + * @return float + */ + public function get_discounted_price( $item ) { + return wc_remove_number_precision_deep( $this->get_discounted_price_in_cents( $item ) ); + } + + /** + * Get discounted price of an item to precision (in cents). + * + * @since 3.2.0 + * @param object $item Get data for this item. + * @return int + */ + public function get_discounted_price_in_cents( $item ) { + return absint( NumberUtil::round( $item->price - $this->get_discount( $item->key, true ) ) ); + } + + /** + * Apply a discount to all items using a coupon. + * + * @since 3.2.0 + * @param WC_Coupon $coupon Coupon object being applied to the items. + * @param bool $validate Set to false to skip coupon validation. + * @throws Exception Error message when coupon isn't valid. + * @return bool|WP_Error True if applied or WP_Error instance in failure. + */ + public function apply_coupon( $coupon, $validate = true ) { + if ( ! is_a( $coupon, 'WC_Coupon' ) ) { + return new WP_Error( 'invalid_coupon', __( 'Invalid coupon', 'woocommerce' ) ); + } + + $is_coupon_valid = $validate ? $this->is_coupon_valid( $coupon ) : true; + + if ( is_wp_error( $is_coupon_valid ) ) { + return $is_coupon_valid; + } + + if ( ! isset( $this->discounts[ $coupon->get_code() ] ) ) { + $this->discounts[ $coupon->get_code() ] = array_fill_keys( array_keys( $this->items ), 0 ); + } + + $items_to_apply = $this->get_items_to_apply_coupon( $coupon ); + + // Core discounts are handled here as of 3.2. + switch ( $coupon->get_discount_type() ) { + case 'percent': + $this->apply_coupon_percent( $coupon, $items_to_apply ); + break; + case 'fixed_product': + $this->apply_coupon_fixed_product( $coupon, $items_to_apply ); + break; + case 'fixed_cart': + $this->apply_coupon_fixed_cart( $coupon, $items_to_apply ); + break; + default: + $this->apply_coupon_custom( $coupon, $items_to_apply ); + break; + } + + return true; + } + + /** + * Sort by price. + * + * @since 3.2.0 + * @param array $a First element. + * @param array $b Second element. + * @return int + */ + protected function sort_by_price( $a, $b ) { + $price_1 = $a->price * $a->quantity; + $price_2 = $b->price * $b->quantity; + if ( $price_1 === $price_2 ) { + return 0; + } + return ( $price_1 < $price_2 ) ? 1 : -1; + } + + /** + * Filter out all products which have been fully discounted to 0. + * Used as array_filter callback. + * + * @since 3.2.0 + * @param object $item Get data for this item. + * @return bool + */ + protected function filter_products_with_price( $item ) { + return $this->get_discounted_price_in_cents( $item ) > 0; + } + + /** + * Get items which the coupon should be applied to. + * + * @since 3.2.0 + * @param object $coupon Coupon object. + * @return array + */ + protected function get_items_to_apply_coupon( $coupon ) { + $items_to_apply = array(); + + foreach ( $this->get_items_to_validate() as $item ) { + $item_to_apply = clone $item; // Clone the item so changes to this item do not affect the originals. + + if ( 0 === $this->get_discounted_price_in_cents( $item_to_apply ) || 0 >= $item_to_apply->quantity ) { + continue; + } + + if ( ! $coupon->is_valid_for_product( $item_to_apply->product, $item_to_apply->object ) && ! $coupon->is_valid_for_cart() ) { + continue; + } + + $items_to_apply[] = $item_to_apply; + } + return $items_to_apply; + } + + /** + * Apply percent discount to items and return an array of discounts granted. + * + * @since 3.2.0 + * @param WC_Coupon $coupon Coupon object. Passed through filters. + * @param array $items_to_apply Array of items to apply the coupon to. + * @return int Total discounted. + */ + protected function apply_coupon_percent( $coupon, $items_to_apply ) { + $total_discount = 0; + $cart_total = 0; + $limit_usage_qty = 0; + $applied_count = 0; + $adjust_final_discount = true; + + if ( null !== $coupon->get_limit_usage_to_x_items() ) { + $limit_usage_qty = $coupon->get_limit_usage_to_x_items(); + } + + $coupon_amount = $coupon->get_amount(); + + foreach ( $items_to_apply as $item ) { + // Find out how much price is available to discount for the item. + $discounted_price = $this->get_discounted_price_in_cents( $item ); + + // Get the price we actually want to discount, based on settings. + $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : NumberUtil::round( $item->price ); + + // See how many and what price to apply to. + $apply_quantity = $limit_usage_qty && ( $limit_usage_qty - $applied_count ) < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity; + $apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) ); + $price_to_discount = ( $price_to_discount / $item->quantity ) * $apply_quantity; + + // Run coupon calculations. + $discount = floor( $price_to_discount * ( $coupon_amount / 100 ) ); + + if ( is_a( $this->object, 'WC_Cart' ) && has_filter( 'woocommerce_coupon_get_discount_amount' ) ) { + // Send through the legacy filter, but not as cents. + $filtered_discount = wc_add_number_precision( apply_filters( 'woocommerce_coupon_get_discount_amount', wc_remove_number_precision( $discount ), wc_remove_number_precision( $price_to_discount ), $item->object, false, $coupon ) ); + + if ( $filtered_discount !== $discount ) { + $discount = $filtered_discount; + $adjust_final_discount = false; + } + } + + $discount = wc_round_discount( min( $discounted_price, $discount ), 0 ); + $cart_total = $cart_total + $price_to_discount; + $total_discount = $total_discount + $discount; + $applied_count = $applied_count + $apply_quantity; + + // Store code and discount amount per item. + $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; + } + + // Work out how much discount would have been given to the cart as a whole and compare to what was discounted on all line items. + $cart_total_discount = wc_round_discount( $cart_total * ( $coupon_amount / 100 ), 0 ); + + if ( $total_discount < $cart_total_discount && $adjust_final_discount ) { + $total_discount += $this->apply_coupon_remainder( $coupon, $items_to_apply, $cart_total_discount - $total_discount ); + } + + return $total_discount; + } + + /** + * Apply fixed product discount to items. + * + * @since 3.2.0 + * @param WC_Coupon $coupon Coupon object. Passed through filters. + * @param array $items_to_apply Array of items to apply the coupon to. + * @param int $amount Fixed discount amount to apply in cents. Leave blank to pull from coupon. + * @return int Total discounted. + */ + protected function apply_coupon_fixed_product( $coupon, $items_to_apply, $amount = null ) { + $total_discount = 0; + $amount = $amount ? $amount : wc_add_number_precision( $coupon->get_amount() ); + $limit_usage_qty = 0; + $applied_count = 0; + + if ( null !== $coupon->get_limit_usage_to_x_items() ) { + $limit_usage_qty = $coupon->get_limit_usage_to_x_items(); + } + + foreach ( $items_to_apply as $item ) { + // Find out how much price is available to discount for the item. + $discounted_price = $this->get_discounted_price_in_cents( $item ); + + // Get the price we actually want to discount, based on settings. + $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : $item->price; + + // Run coupon calculations. + if ( $limit_usage_qty ) { + $apply_quantity = $limit_usage_qty - $applied_count < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity; + $apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) ); + $discount = min( $amount, $item->price / $item->quantity ) * $apply_quantity; + } else { + $apply_quantity = apply_filters( 'woocommerce_coupon_get_apply_quantity', $item->quantity, $item, $coupon, $this ); + $discount = $amount * $apply_quantity; + } + + if ( is_a( $this->object, 'WC_Cart' ) && has_filter( 'woocommerce_coupon_get_discount_amount' ) ) { + // Send through the legacy filter, but not as cents. + $discount = wc_add_number_precision( apply_filters( 'woocommerce_coupon_get_discount_amount', wc_remove_number_precision( $discount ), wc_remove_number_precision( $price_to_discount ), $item->object, false, $coupon ) ); + } + + $discount = min( $discounted_price, $discount ); + $total_discount = $total_discount + $discount; + $applied_count = $applied_count + $apply_quantity; + + // Store code and discount amount per item. + $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; + } + return $total_discount; + } + + /** + * Apply fixed cart discount to items. + * + * @since 3.2.0 + * @param WC_Coupon $coupon Coupon object. Passed through filters. + * @param array $items_to_apply Array of items to apply the coupon to. + * @param int $amount Fixed discount amount to apply in cents. Leave blank to pull from coupon. + * @return int Total discounted. + */ + protected function apply_coupon_fixed_cart( $coupon, $items_to_apply, $amount = null ) { + $total_discount = 0; + $amount = $amount ? $amount : wc_add_number_precision( $coupon->get_amount() ); + $items_to_apply = array_filter( $items_to_apply, array( $this, 'filter_products_with_price' ) ); + $item_count = array_sum( wp_list_pluck( $items_to_apply, 'quantity' ) ); + + if ( ! $item_count ) { + return $total_discount; + } + + if ( ! $amount ) { + // If there is no amount we still send it through so filters are fired. + $total_discount = $this->apply_coupon_fixed_product( $coupon, $items_to_apply, 0 ); + } else { + $per_item_discount = absint( $amount / $item_count ); // round it down to the nearest cent. + + if ( $per_item_discount > 0 ) { + $total_discount = $this->apply_coupon_fixed_product( $coupon, $items_to_apply, $per_item_discount ); + + /** + * If there is still discount remaining, repeat the process. + */ + if ( $total_discount > 0 && $total_discount < $amount ) { + $total_discount += $this->apply_coupon_fixed_cart( $coupon, $items_to_apply, $amount - $total_discount ); + } + } elseif ( $amount > 0 ) { + $total_discount += $this->apply_coupon_remainder( $coupon, $items_to_apply, $amount ); + } + } + return $total_discount; + } + + /** + * Apply custom coupon discount to items. + * + * @since 3.3 + * @param WC_Coupon $coupon Coupon object. Passed through filters. + * @param array $items_to_apply Array of items to apply the coupon to. + * @return int Total discounted. + */ + protected function apply_coupon_custom( $coupon, $items_to_apply ) { + $limit_usage_qty = 0; + $applied_count = 0; + + if ( null !== $coupon->get_limit_usage_to_x_items() ) { + $limit_usage_qty = $coupon->get_limit_usage_to_x_items(); + } + + // Apply the coupon to each item. + foreach ( $items_to_apply as $item ) { + // Find out how much price is available to discount for the item. + $discounted_price = $this->get_discounted_price_in_cents( $item ); + + // Get the price we actually want to discount, based on settings. + $price_to_discount = wc_remove_number_precision( ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : $item->price ); + + // See how many and what price to apply to. + $apply_quantity = $limit_usage_qty && ( $limit_usage_qty - $applied_count ) < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity; + $apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) ); + + // Run coupon calculations. + $discount = wc_add_number_precision( $coupon->get_discount_amount( $price_to_discount / $item->quantity, $item->object, true ) ) * $apply_quantity; + $discount = wc_round_discount( min( $discounted_price, $discount ), 0 ); + $applied_count = $applied_count + $apply_quantity; + + // Store code and discount amount per item. + $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; + } + + // Allow post-processing for custom coupon types (e.g. calculating discrepancy, etc). + $this->discounts[ $coupon->get_code() ] = apply_filters( 'woocommerce_coupon_custom_discounts_array', $this->discounts[ $coupon->get_code() ], $coupon ); + + return array_sum( $this->discounts[ $coupon->get_code() ] ); + } + + /** + * Deal with remaining fractional discounts by splitting it over items + * until the amount is expired, discounting 1 cent at a time. + * + * @since 3.2.0 + * @param WC_Coupon $coupon Coupon object if applicable. Passed through filters. + * @param array $items_to_apply Array of items to apply the coupon to. + * @param int $amount Fixed discount amount to apply. + * @return int Total discounted. + */ + protected function apply_coupon_remainder( $coupon, $items_to_apply, $amount ) { + $total_discount = 0; + + foreach ( $items_to_apply as $item ) { + for ( $i = 0; $i < $item->quantity; $i ++ ) { + // Find out how much price is available to discount for the item. + $price_to_discount = $this->get_discounted_price_in_cents( $item ); + + // Run coupon calculations. + $discount = min( $price_to_discount, 1 ); + + // Store totals. + $total_discount += $discount; + + // Store code and discount amount per item. + $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; + + if ( $total_discount >= $amount ) { + break 2; + } + } + if ( $total_discount >= $amount ) { + break; + } + } + return $total_discount; + } + + /** + * Ensure coupon exists or throw exception. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_exists( $coupon ) { + if ( ! $coupon->get_id() && ! $coupon->get_virtual() ) { + /* translators: %s: coupon code */ + throw new Exception( sprintf( __( 'Coupon "%s" does not exist!', 'woocommerce' ), esc_html( $coupon->get_code() ) ), 105 ); + } + + return true; + } + + /** + * Ensure coupon usage limit is valid or throw exception. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_usage_limit( $coupon ) { + if ( ! $coupon->get_usage_limit() ) { + return true; + } + $usage_count = $coupon->get_usage_count(); + $data_store = $coupon->get_data_store(); + $tentative_usage_count = is_callable( array( $data_store, 'get_tentative_usage_count' ) ) ? $data_store->get_tentative_usage_count( $coupon->get_id() ) : 0; + if ( $usage_count + $tentative_usage_count < $coupon->get_usage_limit() ) { + // All good. + return true; + } + // Coupon usage limit is reached. Let's show as informative error message as we can. + if ( 0 === $tentative_usage_count ) { + // No held coupon, usage limit is indeed reached. + $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED; + } elseif ( is_user_logged_in() ) { + $recent_pending_orders = wc_get_orders( + array( + 'limit' => 1, + 'post_status' => array( 'wc-failed', 'wc-pending' ), + 'customer' => get_current_user_id(), + 'return' => 'ids', + ) + ); + if ( count( $recent_pending_orders ) > 0 ) { + // User logged in and have a pending order, maybe they are trying to use the coupon. + $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK; + } else { + $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED; + } + } else { + // Maybe this user was trying to use the coupon but got stuck. We can't know for sure (performantly). Show a slightly better error message. + $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST; + } + throw new Exception( $coupon->get_coupon_error( $error_code ), $error_code ); + } + + /** + * Ensure coupon user usage limit is valid or throw exception. + * + * Per user usage limit - check here if user is logged in (against user IDs). + * Checked again for emails later on in WC_Cart::check_customer_coupons(). + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @param int $user_id User ID. + * @return bool + */ + protected function validate_coupon_user_usage_limit( $coupon, $user_id = 0 ) { + if ( empty( $user_id ) ) { + if ( $this->object instanceof WC_Order ) { + $user_id = $this->object->get_customer_id(); + } else { + $user_id = get_current_user_id(); + } + } + + if ( $coupon && $user_id && apply_filters( 'woocommerce_coupon_validate_user_usage_limit', $coupon->get_usage_limit_per_user() > 0, $user_id, $coupon, $this ) && $coupon->get_id() && $coupon->get_data_store() ) { + $data_store = $coupon->get_data_store(); + $usage_count = $data_store->get_usage_by_user_id( $coupon, $user_id ); + if ( $usage_count >= $coupon->get_usage_limit_per_user() ) { + if ( $data_store->get_tentative_usages_for_user( $coupon->get_id(), array( $user_id ) ) > 0 ) { + $error_message = $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK ); + $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK; + } else { + $error_message = $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED ); + $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED; + } + throw new Exception( $error_message, $error_code ); + } + } + + return true; + } + + /** + * Ensure coupon date is valid or throw exception. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_expiry_date( $coupon ) { + if ( $coupon->get_date_expires() && apply_filters( 'woocommerce_coupon_validate_expiry_date', time() > $coupon->get_date_expires()->getTimestamp(), $coupon, $this ) ) { + throw new Exception( __( 'This coupon has expired.', 'woocommerce' ), 107 ); + } + + return true; + } + + /** + * Ensure coupon amount is valid or throw exception. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_minimum_amount( $coupon ) { + $subtotal = wc_remove_number_precision( $this->get_object_subtotal() ); + + if ( $coupon->get_minimum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_minimum_amount', $coupon->get_minimum_amount() > $subtotal, $coupon, $subtotal ) ) { + /* translators: %s: coupon minimum amount */ + throw new Exception( sprintf( __( 'The minimum spend for this coupon is %s.', 'woocommerce' ), wc_price( $coupon->get_minimum_amount() ) ), 108 ); + } + + return true; + } + + /** + * Ensure coupon amount is valid or throw exception. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_maximum_amount( $coupon ) { + $subtotal = wc_remove_number_precision( $this->get_object_subtotal() ); + + if ( $coupon->get_maximum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_maximum_amount', $coupon->get_maximum_amount() < $subtotal, $coupon ) ) { + /* translators: %s: coupon maximum amount */ + throw new Exception( sprintf( __( 'The maximum spend for this coupon is %s.', 'woocommerce' ), wc_price( $coupon->get_maximum_amount() ) ), 112 ); + } + + return true; + } + + /** + * Ensure coupon is valid for products in the list is valid or throw exception. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_product_ids( $coupon ) { + if ( count( $coupon->get_product_ids() ) > 0 ) { + $valid = false; + + foreach ( $this->get_items_to_validate() as $item ) { + if ( $item->product && in_array( $item->product->get_id(), $coupon->get_product_ids(), true ) || in_array( $item->product->get_parent_id(), $coupon->get_product_ids(), true ) ) { + $valid = true; + break; + } + } + + if ( ! $valid ) { + throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 ); + } + } + + return true; + } + + /** + * Ensure coupon is valid for product categories in the list is valid or throw exception. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_product_categories( $coupon ) { + if ( count( $coupon->get_product_categories() ) > 0 ) { + $valid = false; + + foreach ( $this->get_items_to_validate() as $item ) { + if ( $coupon->get_exclude_sale_items() && $item->product && $item->product->is_on_sale() ) { + continue; + } + + $product_cats = wc_get_product_cat_ids( $item->product->get_id() ); + + if ( $item->product->get_parent_id() ) { + $product_cats = array_merge( $product_cats, wc_get_product_cat_ids( $item->product->get_parent_id() ) ); + } + + // If we find an item with a cat in our allowed cat list, the coupon is valid. + if ( count( array_intersect( $product_cats, $coupon->get_product_categories() ) ) > 0 ) { + $valid = true; + break; + } + } + + if ( ! $valid ) { + throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 ); + } + } + + return true; + } + + /** + * Ensure coupon is valid for sale items in the list is valid or throw exception. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_sale_items( $coupon ) { + if ( $coupon->get_exclude_sale_items() ) { + $valid = true; + + foreach ( $this->get_items_to_validate() as $item ) { + if ( $item->product && $item->product->is_on_sale() ) { + $valid = false; + break; + } + } + + if ( ! $valid ) { + throw new Exception( __( 'Sorry, this coupon is not valid for sale items.', 'woocommerce' ), 110 ); + } + } + + return true; + } + + /** + * All exclusion rules must pass at the same time for a product coupon to be valid. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_excluded_items( $coupon ) { + $items = $this->get_items_to_validate(); + if ( ! empty( $items ) && $coupon->is_type( wc_get_product_coupon_types() ) ) { + $valid = false; + + foreach ( $items as $item ) { + if ( $item->product && $coupon->is_valid_for_product( $item->product, $item->object ) ) { + $valid = true; + break; + } + } + + if ( ! $valid ) { + throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 ); + } + } + + return true; + } + + /** + * Cart discounts cannot be added if non-eligible product is found. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_eligible_items( $coupon ) { + if ( ! $coupon->is_type( wc_get_product_coupon_types() ) ) { + $this->validate_coupon_sale_items( $coupon ); + $this->validate_coupon_excluded_product_ids( $coupon ); + $this->validate_coupon_excluded_product_categories( $coupon ); + } + + return true; + } + + /** + * Exclude products. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_excluded_product_ids( $coupon ) { + // Exclude Products. + if ( count( $coupon->get_excluded_product_ids() ) > 0 ) { + $products = array(); + + foreach ( $this->get_items_to_validate() as $item ) { + if ( $item->product && in_array( $item->product->get_id(), $coupon->get_excluded_product_ids(), true ) || in_array( $item->product->get_parent_id(), $coupon->get_excluded_product_ids(), true ) ) { + $products[] = $item->product->get_name(); + } + } + + if ( ! empty( $products ) ) { + /* translators: %s: products list */ + throw new Exception( sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) ), 113 ); + } + } + + return true; + } + + /** + * Exclude categories from product list. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_excluded_product_categories( $coupon ) { + if ( count( $coupon->get_excluded_product_categories() ) > 0 ) { + $categories = array(); + + foreach ( $this->get_items_to_validate() as $item ) { + if ( ! $item->product ) { + continue; + } + + $product_cats = wc_get_product_cat_ids( $item->product->get_id() ); + + if ( $item->product->get_parent_id() ) { + $product_cats = array_merge( $product_cats, wc_get_product_cat_ids( $item->product->get_parent_id() ) ); + } + + $cat_id_list = array_intersect( $product_cats, $coupon->get_excluded_product_categories() ); + if ( count( $cat_id_list ) > 0 ) { + foreach ( $cat_id_list as $cat_id ) { + $cat = get_term( $cat_id, 'product_cat' ); + $categories[] = $cat->name; + } + } + } + + if ( ! empty( $categories ) ) { + /* translators: %s: categories list */ + throw new Exception( sprintf( __( 'Sorry, this coupon is not applicable to the categories: %s.', 'woocommerce' ), implode( ', ', array_unique( $categories ) ) ), 114 ); + } + } + + return true; + } + + /** + * Get the object subtotal + * + * @return int + */ + protected function get_object_subtotal() { + if ( is_a( $this->object, 'WC_Cart' ) ) { + return wc_add_number_precision( $this->object->get_displayed_subtotal() ); + } elseif ( is_a( $this->object, 'WC_Order' ) ) { + $subtotal = wc_add_number_precision( $this->object->get_subtotal() ); + + if ( $this->object->get_prices_include_tax() ) { + // Add tax to tax-exclusive subtotal. + $subtotal = $subtotal + wc_add_number_precision( NumberUtil::round( $this->object->get_total_tax(), wc_get_price_decimals() ) ); + } + + return $subtotal; + } else { + return array_sum( wp_list_pluck( $this->items, 'price' ) ); + } + } + + /** + * Check if a coupon is valid. + * + * Error Codes: + * - 100: Invalid filtered. + * - 101: Invalid removed. + * - 102: Not yours removed. + * - 103: Already applied. + * - 104: Individual use only. + * - 105: Not exists. + * - 106: Usage limit reached. + * - 107: Expired. + * - 108: Minimum spend limit not met. + * - 109: Not applicable. + * - 110: Not valid for sale items. + * - 111: Missing coupon code. + * - 112: Maximum spend limit met. + * - 113: Excluded products. + * - 114: Excluded categories. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool|WP_Error + */ + public function is_coupon_valid( $coupon ) { + try { + $this->validate_coupon_exists( $coupon ); + $this->validate_coupon_usage_limit( $coupon ); + $this->validate_coupon_user_usage_limit( $coupon ); + $this->validate_coupon_expiry_date( $coupon ); + $this->validate_coupon_minimum_amount( $coupon ); + $this->validate_coupon_maximum_amount( $coupon ); + $this->validate_coupon_product_ids( $coupon ); + $this->validate_coupon_product_categories( $coupon ); + $this->validate_coupon_excluded_items( $coupon ); + $this->validate_coupon_eligible_items( $coupon ); + + if ( ! apply_filters( 'woocommerce_coupon_is_valid', true, $coupon, $this ) ) { + throw new Exception( __( 'Coupon is not valid.', 'woocommerce' ), 100 ); + } + } catch ( Exception $e ) { + /** + * Filter the coupon error message. + * + * @param string $error_message Error message. + * @param int $error_code Error code. + * @param WC_Coupon $coupon Coupon data. + */ + $message = apply_filters( 'woocommerce_coupon_error', is_numeric( $e->getMessage() ) ? $coupon->get_coupon_error( $e->getMessage() ) : $e->getMessage(), $e->getCode(), $coupon ); + + return new WP_Error( + 'invalid_coupon', + $message, + array( + 'status' => 400, + ) + ); + } + return true; + } +} diff --git a/plugins/woocommerce/includes/class-wc-download-handler.php b/plugins/woocommerce/includes/class-wc-download-handler.php new file mode 100644 index 00000000000..93e0cebad01 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-download-handler.php @@ -0,0 +1,669 @@ +get_billing_email() : null; + + // Prepare email address hash. + $email_hash = function_exists( 'hash' ) ? hash( 'sha256', $email_address ) : sha1( $email_address ); + + if ( is_null( $email_address ) || ! hash_equals( wp_unslash( $_GET['uid'] ), $email_hash ) ) { // WPCS: input var ok, CSRF ok, sanitization ok. + self::download_error( __( 'Invalid download link.', 'woocommerce' ) ); + } + } + + $download_ids = $data_store->get_downloads( + array( + 'user_email' => sanitize_email( str_replace( ' ', '+', $email_address ) ), + 'order_key' => wc_clean( wp_unslash( $_GET['order'] ) ), // WPCS: input var ok, CSRF ok. + 'product_id' => $product_id, + 'download_id' => wc_clean( preg_replace( '/\s+/', ' ', wp_unslash( $_GET['key'] ) ) ), // WPCS: input var ok, CSRF ok, sanitization ok. + 'orderby' => 'downloads_remaining', + 'order' => 'DESC', + 'limit' => 1, + 'return' => 'ids', + ) + ); + + if ( empty( $download_ids ) ) { + self::download_error( __( 'Invalid download link.', 'woocommerce' ) ); + } + + $download = new WC_Customer_Download( current( $download_ids ) ); + + /** + * Filter download filepath. + * + * @since 4.0.0 + * @param string $file_path File path. + * @param string $email_address Email address. + * @param WC_Order|bool $order Order object or false. + * @param WC_Product $product Product object. + * @param WC_Customer_Download $download Download data. + */ + $file_path = apply_filters( + 'woocommerce_download_product_filepath', + $product->get_file_download_path( $download->get_download_id() ), + $email_address, + $order, + $product, + $download + ); + + $parsed_file_path = self::parse_file_path( $file_path ); + $download_range = self::get_download_range( @filesize( $parsed_file_path['file_path'] ) ); // @codingStandardsIgnoreLine. + + self::check_order_is_valid( $download ); + if ( ! $download_range['is_range_request'] ) { + // If the remaining download count goes to 0, allow range requests to be able to finish streaming from iOS devices. + self::check_downloads_remaining( $download ); + } + self::check_download_expiry( $download ); + self::check_download_login_required( $download ); + + do_action( + 'woocommerce_download_product', + $download->get_user_email(), + $download->get_order_key(), + $download->get_product_id(), + $download->get_user_id(), + $download->get_download_id(), + $download->get_order_id() + ); + $download->save(); + + // Track the download in logs and change remaining/counts. + $current_user_id = get_current_user_id(); + $ip_address = WC_Geolocation::get_ip_address(); + if ( ! $download_range['is_range_request'] ) { + $download->track_download( $current_user_id > 0 ? $current_user_id : null, ! empty( $ip_address ) ? $ip_address : null ); + } + + self::download( $file_path, $download->get_product_id() ); + } + + /** + * Check if an order is valid for downloading from. + * + * @param WC_Customer_Download $download Download instance. + */ + private static function check_order_is_valid( $download ) { + if ( $download->get_order_id() ) { + $order = wc_get_order( $download->get_order_id() ); + + if ( $order && ! $order->is_download_permitted() ) { + self::download_error( __( 'Invalid order.', 'woocommerce' ), '', 403 ); + } + } + } + + /** + * Check if there are downloads remaining. + * + * @param WC_Customer_Download $download Download instance. + */ + private static function check_downloads_remaining( $download ) { + if ( '' !== $download->get_downloads_remaining() && 0 >= $download->get_downloads_remaining() ) { + self::download_error( __( 'Sorry, you have reached your download limit for this file', 'woocommerce' ), '', 403 ); + } + } + + /** + * Check if the download has expired. + * + * @param WC_Customer_Download $download Download instance. + */ + private static function check_download_expiry( $download ) { + if ( ! is_null( $download->get_access_expires() ) && $download->get_access_expires()->getTimestamp() < strtotime( 'midnight', time() ) ) { + self::download_error( __( 'Sorry, this download has expired', 'woocommerce' ), '', 403 ); + } + } + + /** + * Check if a download requires the user to login first. + * + * @param WC_Customer_Download $download Download instance. + */ + private static function check_download_login_required( $download ) { + if ( $download->get_user_id() && 'yes' === get_option( 'woocommerce_downloads_require_login' ) ) { + if ( ! is_user_logged_in() ) { + if ( wc_get_page_id( 'myaccount' ) ) { + wp_safe_redirect( add_query_arg( 'wc_error', rawurlencode( __( 'You must be logged in to download files.', 'woocommerce' ) ), wc_get_page_permalink( 'myaccount' ) ) ); + exit; + } else { + self::download_error( __( 'You must be logged in to download files.', 'woocommerce' ) . ' ' . __( 'Login', 'woocommerce' ) . '', __( 'Log in to Download Files', 'woocommerce' ), 403 ); + } + } elseif ( ! current_user_can( 'download_file', $download ) ) { + self::download_error( __( 'This is not your download link.', 'woocommerce' ), '', 403 ); + } + } + } + + /** + * Count download. + * + * @deprecated 4.4.0 + * @param array $download_data Download data. + */ + public static function count_download( $download_data ) { + wc_deprecated_function( 'WC_Download_Handler::count_download', '4.4.0', '' ); + } + + /** + * Download a file - hook into init function. + * + * @param string $file_path URL to file. + * @param integer $product_id Product ID of the product being downloaded. + */ + public static function download( $file_path, $product_id ) { + if ( ! $file_path ) { + self::download_error( __( 'No file defined', 'woocommerce' ) ); + } + + $filename = basename( $file_path ); + + if ( strstr( $filename, '?' ) ) { + $filename = current( explode( '?', $filename ) ); + } + + $filename = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id ); + + /** + * Filter download method. + * + * @since 4.5.0 + * @param string $method Download method. + * @param int $product_id Product ID. + * @param string $file_path URL to file. + */ + $file_download_method = apply_filters( 'woocommerce_file_download_method', get_option( 'woocommerce_file_download_method', 'force' ), $product_id, $file_path ); + + // Add action to prevent issues in IE. + add_action( 'nocache_headers', array( __CLASS__, 'ie_nocache_headers_fix' ) ); + + // Trigger download via one of the methods. + do_action( 'woocommerce_download_file_' . $file_download_method, $file_path, $filename ); + } + + /** + * Redirect to a file to start the download. + * + * @param string $file_path File path. + * @param string $filename File name. + */ + public static function download_file_redirect( $file_path, $filename = '' ) { + header( 'Location: ' . $file_path ); + exit; + } + + /** + * Parse file path and see if its remote or local. + * + * @param string $file_path File path. + * @return array + */ + public static function parse_file_path( $file_path ) { + $wp_uploads = wp_upload_dir(); + $wp_uploads_dir = $wp_uploads['basedir']; + $wp_uploads_url = $wp_uploads['baseurl']; + + /** + * Replace uploads dir, site url etc with absolute counterparts if we can. + * Note the str_replace on site_url is on purpose, so if https is forced + * via filters we can still do the string replacement on a HTTP file. + */ + $replacements = array( + $wp_uploads_url => $wp_uploads_dir, + network_site_url( '/', 'https' ) => ABSPATH, + str_replace( 'https:', 'http:', network_site_url( '/', 'http' ) ) => ABSPATH, + site_url( '/', 'https' ) => ABSPATH, + str_replace( 'https:', 'http:', site_url( '/', 'http' ) ) => ABSPATH, + ); + + $count = 0; + $file_path = str_replace( array_keys( $replacements ), array_values( $replacements ), $file_path, $count ); + $parsed_file_path = wp_parse_url( $file_path ); + $remote_file = null === $count || 0 === $count; // Remote file only if there were no replacements. + + // Paths that begin with '//' are always remote URLs. + if ( '//' === substr( $file_path, 0, 2 ) ) { + $file_path = ( is_ssl() ? 'https:' : 'http:' ) . $file_path; + + /** + * Filter the remote filepath for download. + * + * @since 6.5.0 + * @param string $file_path File path. + */ + return array( + 'remote_file' => true, + 'file_path' => apply_filters( 'woocommerce_download_parse_remote_file_path', $file_path ), + ); + } + + // See if path needs an abspath prepended to work. + if ( file_exists( ABSPATH . $file_path ) ) { + $remote_file = false; + $file_path = ABSPATH . $file_path; + + } elseif ( '/wp-content' === substr( $file_path, 0, 11 ) ) { + $remote_file = false; + $file_path = realpath( WP_CONTENT_DIR . substr( $file_path, 11 ) ); + + // Check if we have an absolute path. + } elseif ( ( ! isset( $parsed_file_path['scheme'] ) || ! in_array( $parsed_file_path['scheme'], array( 'http', 'https', 'ftp' ), true ) ) && isset( $parsed_file_path['path'] ) ) { + $remote_file = false; + $file_path = $parsed_file_path['path']; + } + + /** + * Filter the filepath for download. + * + * @since 6.5.0 + * @param string $file_path File path. + * @param bool $remote_file Remote File Indicator. + */ + return array( + 'remote_file' => $remote_file, + 'file_path' => apply_filters( 'woocommerce_download_parse_file_path', $file_path, $remote_file ), + ); + } + + /** + * Download a file using X-Sendfile, X-Lighttpd-Sendfile, or X-Accel-Redirect if available. + * + * @param string $file_path File path. + * @param string $filename File name. + */ + public static function download_file_xsendfile( $file_path, $filename ) { + $parsed_file_path = self::parse_file_path( $file_path ); + + /** + * Fallback on force download method for remote files. This is because: + * 1. xsendfile needs proxy configuration to work for remote files, which cannot be assumed to be available on most hosts. + * 2. Force download method is more secure than redirect method if `allow_url_fopen` is enabled in `php.ini`. + */ + if ( $parsed_file_path['remote_file'] && ! apply_filters( 'woocommerce_use_xsendfile_for_remote', false ) ) { + do_action( 'woocommerce_download_file_force', $file_path, $filename ); + return; + } + + if ( function_exists( 'apache_get_modules' ) && in_array( 'mod_xsendfile', apache_get_modules(), true ) ) { + self::download_headers( $parsed_file_path['file_path'], $filename ); + $filepath = apply_filters( 'woocommerce_download_file_xsendfile_file_path', $parsed_file_path['file_path'], $file_path, $filename, $parsed_file_path ); + header( 'X-Sendfile: ' . $filepath ); + exit; + } elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'lighttpd' ) ) { + self::download_headers( $parsed_file_path['file_path'], $filename ); + $filepath = apply_filters( 'woocommerce_download_file_xsendfile_lighttpd_file_path', $parsed_file_path['file_path'], $file_path, $filename, $parsed_file_path ); + header( 'X-Lighttpd-Sendfile: ' . $filepath ); + exit; + } elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'nginx' ) || stristr( getenv( 'SERVER_SOFTWARE' ), 'cherokee' ) ) { + self::download_headers( $parsed_file_path['file_path'], $filename ); + $xsendfile_path = trim( preg_replace( '`^' . str_replace( '\\', '/', getcwd() ) . '`', '', $parsed_file_path['file_path'] ), '/' ); + $xsendfile_path = apply_filters( 'woocommerce_download_file_xsendfile_x_accel_redirect_file_path', $xsendfile_path, $file_path, $filename, $parsed_file_path ); + header( "X-Accel-Redirect: /$xsendfile_path" ); + exit; + } + + // Fallback. + wc_get_logger()->warning( + sprintf( + /* translators: %1$s contains the filepath of the digital asset. */ + __( '%1$s could not be served using the X-Accel-Redirect/X-Sendfile method. A Force Download will be used instead.', 'woocommerce' ), + $file_path + ) + ); + self::download_file_force( $file_path, $filename ); + } + + /** + * Parse the HTTP_RANGE request from iOS devices. + * Does not support multi-range requests. + * + * @param int $file_size Size of file in bytes. + * @return array { + * Information about range download request: beginning and length of + * file chunk, whether the range is valid/supported and whether the request is a range request. + * + * @type int $start Byte offset of the beginning of the range. Default 0. + * @type int $length Length of the requested file chunk in bytes. Optional. + * @type bool $is_range_valid Whether the requested range is a valid and supported range. + * @type bool $is_range_request Whether the request is a range request. + * } + */ + protected static function get_download_range( $file_size ) { + $start = 0; + $download_range = array( + 'start' => $start, + 'is_range_valid' => false, + 'is_range_request' => false, + ); + + if ( ! $file_size ) { + return $download_range; + } + + $end = $file_size - 1; + $download_range['length'] = $file_size; + + if ( isset( $_SERVER['HTTP_RANGE'] ) ) { // @codingStandardsIgnoreLine. + $http_range = sanitize_text_field( wp_unslash( $_SERVER['HTTP_RANGE'] ) ); // WPCS: input var ok. + $download_range['is_range_request'] = true; + + $c_start = $start; + $c_end = $end; + // Extract the range string. + list( , $range ) = explode( '=', $http_range, 2 ); + // Make sure the client hasn't sent us a multibyte range. + if ( strpos( $range, ',' ) !== false ) { + return $download_range; + } + + /* + * If the range starts with an '-' we start from the beginning. + * If not, we forward the file pointer + * and make sure to get the end byte if specified. + */ + if ( '-' === $range[0] ) { + // The n-number of the last bytes is requested. + $c_start = $file_size - substr( $range, 1 ); + } else { + $range = explode( '-', $range ); + $c_start = ( isset( $range[0] ) && is_numeric( $range[0] ) ) ? (int) $range[0] : 0; + $c_end = ( isset( $range[1] ) && is_numeric( $range[1] ) ) ? (int) $range[1] : $file_size; + } + + /* + * Check the range and make sure it's treated according to the specs: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html. + * End bytes can not be larger than $end. + */ + $c_end = ( $c_end > $end ) ? $end : $c_end; + // Validate the requested range and return an error if it's not correct. + if ( $c_start > $c_end || $c_start > $file_size - 1 || $c_end >= $file_size ) { + return $download_range; + } + $start = $c_start; + $end = $c_end; + $length = $end - $start + 1; + + $download_range['start'] = $start; + $download_range['length'] = $length; + $download_range['is_range_valid'] = true; + } + return $download_range; + } + + /** + * Force download - this is the default method. + * + * @param string $file_path File path. + * @param string $filename File name. + */ + public static function download_file_force( $file_path, $filename ) { + $parsed_file_path = self::parse_file_path( $file_path ); + $download_range = self::get_download_range( @filesize( $parsed_file_path['file_path'] ) ); // @codingStandardsIgnoreLine. + + self::download_headers( $parsed_file_path['file_path'], $filename, $download_range ); + + $start = isset( $download_range['start'] ) ? $download_range['start'] : 0; + $length = isset( $download_range['length'] ) ? $download_range['length'] : 0; + if ( ! self::readfile_chunked( $parsed_file_path['file_path'], $start, $length ) ) { + if ( $parsed_file_path['remote_file'] && 'yes' === get_option( 'woocommerce_downloads_redirect_fallback_allowed' ) ) { + wc_get_logger()->warning( + sprintf( + /* translators: %1$s contains the filepath of the digital asset. */ + __( '%1$s could not be served using the Force Download method. A redirect will be used instead.', 'woocommerce' ), + $file_path + ) + ); + self::download_file_redirect( $file_path ); + } else { + self::download_error( __( 'File not found', 'woocommerce' ) ); + } + } + + exit; + } + + /** + * Get content type of a download. + * + * @param string $file_path File path. + * @return string + */ + private static function get_download_content_type( $file_path ) { + $file_extension = strtolower( substr( strrchr( $file_path, '.' ), 1 ) ); + $ctype = 'application/force-download'; + + foreach ( get_allowed_mime_types() as $mime => $type ) { + $mimes = explode( '|', $mime ); + if ( in_array( $file_extension, $mimes, true ) ) { + $ctype = $type; + break; + } + } + + return $ctype; + } + + /** + * Set headers for the download. + * + * @param string $file_path File path. + * @param string $filename File name. + * @param array $download_range Array containing info about range download request (see {@see get_download_range} for structure). + */ + private static function download_headers( $file_path, $filename, $download_range = array() ) { + self::check_server_config(); + self::clean_buffers(); + wc_nocache_headers(); + + header( 'X-Robots-Tag: noindex, nofollow', true ); + header( 'Content-Type: ' . self::get_download_content_type( $file_path ) ); + header( 'Content-Description: File Transfer' ); + header( 'Content-Disposition: attachment; filename="' . $filename . '";' ); + header( 'Content-Transfer-Encoding: binary' ); + + $file_size = @filesize( $file_path ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged + if ( ! $file_size ) { + return; + } + + if ( isset( $download_range['is_range_request'] ) && true === $download_range['is_range_request'] ) { + if ( false === $download_range['is_range_valid'] ) { + header( 'HTTP/1.1 416 Requested Range Not Satisfiable' ); + header( 'Content-Range: bytes 0-' . ( $file_size - 1 ) . '/' . $file_size ); + exit; + } + + $start = $download_range['start']; + $end = $download_range['start'] + $download_range['length'] - 1; + $length = $download_range['length']; + + header( 'HTTP/1.1 206 Partial Content' ); + header( "Accept-Ranges: 0-$file_size" ); + header( "Content-Range: bytes $start-$end/$file_size" ); + header( "Content-Length: $length" ); + } else { + header( 'Content-Length: ' . $file_size ); + } + } + + /** + * Check and set certain server config variables to ensure downloads work as intended. + */ + private static function check_server_config() { + wc_set_time_limit( 0 ); + if ( function_exists( 'apache_setenv' ) ) { + @apache_setenv( 'no-gzip', 1 ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_apache_setenv + } + @ini_set( 'zlib.output_compression', 'Off' ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_ini_set + @session_write_close(); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.VIP.SessionFunctionsUsage.session_session_write_close + } + + /** + * Clean all output buffers. + * + * Can prevent errors, for example: transfer closed with 3 bytes remaining to read. + */ + private static function clean_buffers() { + if ( ob_get_level() ) { + $levels = ob_get_level(); + for ( $i = 0; $i < $levels; $i++ ) { + @ob_end_clean(); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged + } + } else { + @ob_end_clean(); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged + } + } + + /** + * Read file chunked. + * + * Reads file in chunks so big downloads are possible without changing PHP.INI - http://codeigniter.com/wiki/Download_helper_for_large_files/. + * + * @param string $file File. + * @param int $start Byte offset/position of the beginning from which to read from the file. + * @param int $length Length of the chunk to be read from the file in bytes, 0 means full file. + * @return bool Success or fail + */ + public static function readfile_chunked( $file, $start = 0, $length = 0 ) { + if ( ! defined( 'WC_CHUNK_SIZE' ) ) { + define( 'WC_CHUNK_SIZE', 1024 * 1024 ); + } + $handle = @fopen( $file, 'r' ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen + + if ( false === $handle ) { + return false; + } + + if ( ! $length ) { + $length = @filesize( $file ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged + } + + $read_length = (int) WC_CHUNK_SIZE; + + if ( $length ) { + $end = $start + $length - 1; + + @fseek( $handle, $start ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged + $p = @ftell( $handle ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged + + while ( ! @feof( $handle ) && $p <= $end ) { // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged + // Don't run past the end of file. + if ( $p + $read_length > $end ) { + $read_length = $end - $p + 1; + } + + echo @fread( $handle, $read_length ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.WP.AlternativeFunctions.file_system_read_fread + $p = @ftell( $handle ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged + + if ( ob_get_length() ) { + ob_flush(); + flush(); + } + } + } else { + while ( ! @feof( $handle ) ) { // @codingStandardsIgnoreLine. + echo @fread( $handle, $read_length ); // @codingStandardsIgnoreLine. + if ( ob_get_length() ) { + ob_flush(); + flush(); + } + } + } + + return @fclose( $handle ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fclose + } + + /** + * Filter headers for IE to fix issues over SSL. + * + * IE bug prevents download via SSL when Cache Control and Pragma no-cache headers set. + * + * @param array $headers HTTP headers. + * @return array + */ + public static function ie_nocache_headers_fix( $headers ) { + if ( is_ssl() && ! empty( $GLOBALS['is_IE'] ) ) { + $headers['Cache-Control'] = 'private'; + unset( $headers['Pragma'] ); + } + return $headers; + } + + /** + * Die with an error message if the download fails. + * + * @param string $message Error message. + * @param string $title Error title. + * @param integer $status Error status. + */ + private static function download_error( $message, $title = '', $status = 404 ) { + /* + * Since we will now render a message instead of serving a download, we should unwind some of the previously set + * headers. + */ + header( 'Content-Type: ' . get_option( 'html_type' ) . '; charset=' . get_option( 'blog_charset' ) ); + header_remove( 'Content-Description;' ); + header_remove( 'Content-Disposition' ); + header_remove( 'Content-Transfer-Encoding' ); + + if ( ! strstr( $message, '' . esc_html__( 'Go to shop', 'woocommerce' ) . ''; + } + wp_die( $message, $title, array( 'response' => $status ) ); // WPCS: XSS ok. + } +} + +WC_Download_Handler::init(); diff --git a/includes/class-wc-emails.php b/plugins/woocommerce/includes/class-wc-emails.php similarity index 100% rename from includes/class-wc-emails.php rename to plugins/woocommerce/includes/class-wc-emails.php diff --git a/includes/class-wc-embed.php b/plugins/woocommerce/includes/class-wc-embed.php similarity index 100% rename from includes/class-wc-embed.php rename to plugins/woocommerce/includes/class-wc-embed.php diff --git a/plugins/woocommerce/includes/class-wc-form-handler.php b/plugins/woocommerce/includes/class-wc-form-handler.php new file mode 100644 index 00000000000..3ec4872d871 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-form-handler.php @@ -0,0 +1,1128 @@ +ID : 0; + } else { + $user_id = absint( $_GET['id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + } + + // If the reset token is not for the current user, ignore the reset request (don't redirect). + $logged_in_user_id = get_current_user_id(); + if ( $logged_in_user_id && $logged_in_user_id !== $user_id ) { + wc_add_notice( __( 'This password reset key is for a different user account. Please log out and try again.', 'woocommerce' ), 'error' ); + return; + } + + $action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : ''; + $value = sprintf( '%d:%s', $user_id, wp_unslash( $_GET['key'] ) ); // phpcs:ignore + WC_Shortcode_My_Account::set_reset_password_cookie( $value ); + wp_safe_redirect( + add_query_arg( + array( + 'show-reset-form' => 'true', + 'action' => $action, + ), + wc_lostpassword_url() + ) + ); + exit; + } + } + + /** + * Save and and update a billing or shipping address if the + * form was submitted through the user account page. + */ + public static function save_address() { + global $wp; + + $nonce_value = wc_get_var( $_REQUEST['woocommerce-edit-address-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. + + if ( ! wp_verify_nonce( $nonce_value, 'woocommerce-edit_address' ) ) { + return; + } + + if ( empty( $_POST['action'] ) || 'edit_address' !== $_POST['action'] ) { + return; + } + + wc_nocache_headers(); + + $user_id = get_current_user_id(); + + if ( $user_id <= 0 ) { + return; + } + + $customer = new WC_Customer( $user_id ); + + if ( ! $customer ) { + return; + } + + $load_address = isset( $wp->query_vars['edit-address'] ) ? wc_edit_address_i18n( sanitize_title( $wp->query_vars['edit-address'] ), true ) : 'billing'; + + if ( ! isset( $_POST[ $load_address . '_country' ] ) ) { + return; + } + + $address = WC()->countries->get_address_fields( wc_clean( wp_unslash( $_POST[ $load_address . '_country' ] ) ), $load_address . '_' ); + + foreach ( $address as $key => $field ) { + if ( ! isset( $field['type'] ) ) { + $field['type'] = 'text'; + } + + // Get Value. + if ( 'checkbox' === $field['type'] ) { + $value = (int) isset( $_POST[ $key ] ); + } else { + $value = isset( $_POST[ $key ] ) ? wc_clean( wp_unslash( $_POST[ $key ] ) ) : ''; + } + + // Hook to allow modification of value. + $value = apply_filters( 'woocommerce_process_myaccount_field_' . $key, $value ); + + // Validation: Required fields. + if ( ! empty( $field['required'] ) && empty( $value ) ) { + /* translators: %s: Field name. */ + wc_add_notice( sprintf( __( '%s is a required field.', 'woocommerce' ), $field['label'] ), 'error', array( 'id' => $key ) ); + } + + if ( ! empty( $value ) ) { + // Validation and formatting rules. + if ( ! empty( $field['validate'] ) && is_array( $field['validate'] ) ) { + foreach ( $field['validate'] as $rule ) { + switch ( $rule ) { + case 'postcode': + $country = wc_clean( wp_unslash( $_POST[ $load_address . '_country' ] ) ); + $value = wc_format_postcode( $value, $country ); + + if ( '' !== $value && ! WC_Validation::is_postcode( $value, $country ) ) { + switch ( $country ) { + case 'IE': + $postcode_validation_notice = __( 'Please enter a valid Eircode.', 'woocommerce' ); + break; + default: + $postcode_validation_notice = __( 'Please enter a valid postcode / ZIP.', 'woocommerce' ); + } + wc_add_notice( $postcode_validation_notice, 'error' ); + } + break; + case 'phone': + if ( '' !== $value && ! WC_Validation::is_phone( $value ) ) { + /* translators: %s: Phone number. */ + wc_add_notice( sprintf( __( '%s is not a valid phone number.', 'woocommerce' ), '' . $field['label'] . '' ), 'error' ); + } + break; + case 'email': + $value = strtolower( $value ); + + if ( ! is_email( $value ) ) { + /* translators: %s: Email address. */ + wc_add_notice( sprintf( __( '%s is not a valid email address.', 'woocommerce' ), '' . $field['label'] . '' ), 'error' ); + } + break; + } + } + } + } + + try { + // Set prop in customer object. + if ( is_callable( array( $customer, "set_$key" ) ) ) { + $customer->{"set_$key"}( $value ); + } else { + $customer->update_meta_data( $key, $value ); + } + } catch ( WC_Data_Exception $e ) { + // Set notices. Ignore invalid billing email, since is already validated. + if ( 'customer_invalid_billing_email' !== $e->getErrorCode() ) { + wc_add_notice( $e->getMessage(), 'error' ); + } + } + } + + /** + * Hook: woocommerce_after_save_address_validation. + * + * Allow developers to add custom validation logic and throw an error to prevent save. + * + * @param int $user_id User ID being saved. + * @param string $load_address Type of address e.g. billing or shipping. + * @param array $address The address fields. + * @param WC_Customer $customer The customer object being saved. @since 3.6.0 + */ + do_action( 'woocommerce_after_save_address_validation', $user_id, $load_address, $address, $customer ); + + if ( 0 < wc_notice_count( 'error' ) ) { + return; + } + + $customer->save(); + + wc_add_notice( __( 'Address changed successfully.', 'woocommerce' ) ); + + do_action( 'woocommerce_customer_save_address', $user_id, $load_address ); + + wp_safe_redirect( wc_get_endpoint_url( 'edit-address', '', wc_get_page_permalink( 'myaccount' ) ) ); + exit; + } + + /** + * Save the password/account details and redirect back to the my account page. + */ + public static function save_account_details() { + $nonce_value = wc_get_var( $_REQUEST['save-account-details-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. + + if ( ! wp_verify_nonce( $nonce_value, 'save_account_details' ) ) { + return; + } + + if ( empty( $_POST['action'] ) || 'save_account_details' !== $_POST['action'] ) { + return; + } + + wc_nocache_headers(); + + $user_id = get_current_user_id(); + + if ( $user_id <= 0 ) { + return; + } + + $account_first_name = ! empty( $_POST['account_first_name'] ) ? wc_clean( wp_unslash( $_POST['account_first_name'] ) ) : ''; + $account_last_name = ! empty( $_POST['account_last_name'] ) ? wc_clean( wp_unslash( $_POST['account_last_name'] ) ) : ''; + $account_display_name = ! empty( $_POST['account_display_name'] ) ? wc_clean( wp_unslash( $_POST['account_display_name'] ) ) : ''; + $account_email = ! empty( $_POST['account_email'] ) ? wc_clean( wp_unslash( $_POST['account_email'] ) ) : ''; + $pass_cur = ! empty( $_POST['password_current'] ) ? $_POST['password_current'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + $pass1 = ! empty( $_POST['password_1'] ) ? $_POST['password_1'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + $pass2 = ! empty( $_POST['password_2'] ) ? $_POST['password_2'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + $save_pass = true; + + // Current user data. + $current_user = get_user_by( 'id', $user_id ); + $current_first_name = $current_user->first_name; + $current_last_name = $current_user->last_name; + $current_email = $current_user->user_email; + + // New user data. + $user = new stdClass(); + $user->ID = $user_id; + $user->first_name = $account_first_name; + $user->last_name = $account_last_name; + $user->display_name = $account_display_name; + + // Prevent display name to be changed to email. + if ( is_email( $account_display_name ) ) { + wc_add_notice( __( 'Display name cannot be changed to email address due to privacy concern.', 'woocommerce' ), 'error' ); + } + + // Handle required fields. + $required_fields = apply_filters( + 'woocommerce_save_account_details_required_fields', + array( + 'account_first_name' => __( 'First name', 'woocommerce' ), + 'account_last_name' => __( 'Last name', 'woocommerce' ), + 'account_display_name' => __( 'Display name', 'woocommerce' ), + 'account_email' => __( 'Email address', 'woocommerce' ), + ) + ); + + foreach ( $required_fields as $field_key => $field_name ) { + if ( empty( $_POST[ $field_key ] ) ) { + /* translators: %s: Field name. */ + wc_add_notice( sprintf( __( '%s is a required field.', 'woocommerce' ), '' . esc_html( $field_name ) . '' ), 'error', array( 'id' => $field_key ) ); + } + } + + if ( $account_email ) { + $account_email = sanitize_email( $account_email ); + if ( ! is_email( $account_email ) ) { + wc_add_notice( __( 'Please provide a valid email address.', 'woocommerce' ), 'error' ); + } elseif ( email_exists( $account_email ) && $account_email !== $current_user->user_email ) { + wc_add_notice( __( 'This email address is already registered.', 'woocommerce' ), 'error' ); + } + $user->user_email = $account_email; + } + + if ( ! empty( $pass_cur ) && empty( $pass1 ) && empty( $pass2 ) ) { + wc_add_notice( __( 'Please fill out all password fields.', 'woocommerce' ), 'error' ); + $save_pass = false; + } elseif ( ! empty( $pass1 ) && empty( $pass_cur ) ) { + wc_add_notice( __( 'Please enter your current password.', 'woocommerce' ), 'error' ); + $save_pass = false; + } elseif ( ! empty( $pass1 ) && empty( $pass2 ) ) { + wc_add_notice( __( 'Please re-enter your password.', 'woocommerce' ), 'error' ); + $save_pass = false; + } elseif ( ( ! empty( $pass1 ) || ! empty( $pass2 ) ) && $pass1 !== $pass2 ) { + wc_add_notice( __( 'New passwords do not match.', 'woocommerce' ), 'error' ); + $save_pass = false; + } elseif ( ! empty( $pass1 ) && ! wp_check_password( $pass_cur, $current_user->user_pass, $current_user->ID ) ) { + wc_add_notice( __( 'Your current password is incorrect.', 'woocommerce' ), 'error' ); + $save_pass = false; + } + + if ( $pass1 && $save_pass ) { + $user->user_pass = $pass1; + } + + // Allow plugins to return their own errors. + $errors = new WP_Error(); + do_action_ref_array( 'woocommerce_save_account_details_errors', array( &$errors, &$user ) ); + + if ( $errors->get_error_messages() ) { + foreach ( $errors->get_error_messages() as $error ) { + wc_add_notice( $error, 'error' ); + } + } + + if ( wc_notice_count( 'error' ) === 0 ) { + wp_update_user( $user ); + + // Update customer object to keep data in sync. + $customer = new WC_Customer( $user->ID ); + + if ( $customer ) { + // Keep billing data in sync if data changed. + if ( is_email( $user->user_email ) && $current_email !== $user->user_email ) { + $customer->set_billing_email( $user->user_email ); + } + + if ( $current_first_name !== $user->first_name ) { + $customer->set_billing_first_name( $user->first_name ); + } + + if ( $current_last_name !== $user->last_name ) { + $customer->set_billing_last_name( $user->last_name ); + } + + $customer->save(); + } + + wc_add_notice( __( 'Account details changed successfully.', 'woocommerce' ) ); + + do_action( 'woocommerce_save_account_details', $user->ID ); + + wp_safe_redirect( wc_get_endpoint_url( 'edit-account', '', wc_get_page_permalink( 'myaccount' ) ) ); + exit; + } + } + + /** + * Process the checkout form. + */ + public static function checkout_action() { + if ( isset( $_POST['woocommerce_checkout_place_order'] ) || isset( $_POST['woocommerce_checkout_update_totals'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing + wc_nocache_headers(); + + if ( WC()->cart->is_empty() ) { + wp_safe_redirect( wc_get_cart_url() ); + exit; + } + + wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true ); + + WC()->checkout()->process_checkout(); + } + } + + /** + * Process the pay form. + * + * @throws Exception On payment error. + */ + public static function pay_action() { + global $wp; + + if ( isset( $_POST['woocommerce_pay'], $_GET['key'] ) ) { + wc_nocache_headers(); + + $nonce_value = wc_get_var( $_REQUEST['woocommerce-pay-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. + + if ( ! wp_verify_nonce( $nonce_value, 'woocommerce-pay' ) ) { + return; + } + + ob_start(); + + // Pay for existing order. + $order_key = wp_unslash( $_GET['key'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $order_id = absint( $wp->query_vars['order-pay'] ); + $order = wc_get_order( $order_id ); + + if ( $order_id === $order->get_id() && hash_equals( $order->get_order_key(), $order_key ) && $order->needs_payment() ) { + + do_action( 'woocommerce_before_pay_action', $order ); + + WC()->customer->set_props( + array( + 'billing_country' => $order->get_billing_country() ? $order->get_billing_country() : null, + 'billing_state' => $order->get_billing_state() ? $order->get_billing_state() : null, + 'billing_postcode' => $order->get_billing_postcode() ? $order->get_billing_postcode() : null, + 'billing_city' => $order->get_billing_city() ? $order->get_billing_city() : null, + ) + ); + WC()->customer->save(); + + if ( ! empty( $_POST['terms-field'] ) && empty( $_POST['terms'] ) ) { + wc_add_notice( __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ), 'error' ); + return; + } + + // Update payment method. + if ( $order->needs_payment() ) { + try { + $payment_method_id = isset( $_POST['payment_method'] ) ? wc_clean( wp_unslash( $_POST['payment_method'] ) ) : false; + + if ( ! $payment_method_id ) { + throw new Exception( __( 'Invalid payment method.', 'woocommerce' ) ); + } + + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + $payment_method = isset( $available_gateways[ $payment_method_id ] ) ? $available_gateways[ $payment_method_id ] : false; + + if ( ! $payment_method ) { + throw new Exception( __( 'Invalid payment method.', 'woocommerce' ) ); + } + + $order->set_payment_method( $payment_method ); + $order->save(); + + $payment_method->validate_fields(); + + if ( 0 === wc_notice_count( 'error' ) ) { + + $result = $payment_method->process_payment( $order_id ); + + // Redirect to success/confirmation/payment page. + if ( isset( $result['result'] ) && 'success' === $result['result'] ) { + $result['order_id'] = $order_id; + + $result = apply_filters( 'woocommerce_payment_successful_result', $result, $order_id ); + + wp_redirect( $result['redirect'] ); //phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect + exit; + } + } + } catch ( Exception $e ) { + wc_add_notice( $e->getMessage(), 'error' ); + } + } else { + // No payment was required for order. + $order->payment_complete(); + wp_safe_redirect( $order->get_checkout_order_received_url() ); + exit; + } + + do_action( 'woocommerce_after_pay_action', $order ); + + } + } + } + + /** + * Process the add payment method form. + */ + public static function add_payment_method_action() { + if ( isset( $_POST['woocommerce_add_payment_method'], $_POST['payment_method'] ) ) { + wc_nocache_headers(); + + $nonce_value = wc_get_var( $_REQUEST['woocommerce-add-payment-method-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. + + if ( ! wp_verify_nonce( $nonce_value, 'woocommerce-add-payment-method' ) ) { + return; + } + + if ( ! apply_filters( 'woocommerce_add_payment_method_form_is_valid', true ) ) { + return; + } + + // Test rate limit. + $current_user_id = get_current_user_id(); + $rate_limit_id = 'add_payment_method_' . $current_user_id; + $delay = (int) apply_filters( 'woocommerce_payment_gateway_add_payment_method_delay', 20 ); + + if ( WC_Rate_Limiter::retried_too_soon( $rate_limit_id ) ) { + wc_add_notice( + sprintf( + /* translators: %d number of seconds */ + _n( + 'You cannot add a new payment method so soon after the previous one. Please wait for %d second.', + 'You cannot add a new payment method so soon after the previous one. Please wait for %d seconds.', + $delay, + 'woocommerce' + ), + $delay + ), + 'error' + ); + return; + } + + WC_Rate_Limiter::set_rate_limit( $rate_limit_id, $delay ); + + ob_start(); + + $payment_method_id = wc_clean( wp_unslash( $_POST['payment_method'] ) ); + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + + if ( isset( $available_gateways[ $payment_method_id ] ) ) { + $gateway = $available_gateways[ $payment_method_id ]; + + if ( ! $gateway->supports( 'add_payment_method' ) && ! $gateway->supports( 'tokenization' ) ) { + wc_add_notice( __( 'Invalid payment gateway.', 'woocommerce' ), 'error' ); + return; + } + + $gateway->validate_fields(); + + if ( wc_notice_count( 'error' ) > 0 ) { + return; + } + + $result = $gateway->add_payment_method(); + + if ( 'success' === $result['result'] ) { + wc_add_notice( __( 'Payment method successfully added.', 'woocommerce' ) ); + } + + if ( 'failure' === $result['result'] ) { + wc_add_notice( __( 'Unable to add payment method to your account.', 'woocommerce' ), 'error' ); + } + + if ( ! empty( $result['redirect'] ) ) { + wp_redirect( $result['redirect'] ); //phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect + exit(); + } + } + } + } + + /** + * Process the delete payment method form. + */ + public static function delete_payment_method_action() { + global $wp; + + if ( isset( $wp->query_vars['delete-payment-method'] ) ) { + wc_nocache_headers(); + + $token_id = absint( $wp->query_vars['delete-payment-method'] ); + $token = WC_Payment_Tokens::get( $token_id ); + + if ( is_null( $token ) || get_current_user_id() !== $token->get_user_id() || ! isset( $_REQUEST['_wpnonce'] ) || false === wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'delete-payment-method-' . $token_id ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + wc_add_notice( __( 'Invalid payment method.', 'woocommerce' ), 'error' ); + } else { + WC_Payment_Tokens::delete( $token_id ); + wc_add_notice( __( 'Payment method deleted.', 'woocommerce' ) ); + } + + wp_safe_redirect( wc_get_account_endpoint_url( 'payment-methods' ) ); + exit(); + } + + } + + /** + * Process the delete payment method form. + */ + public static function set_default_payment_method_action() { + global $wp; + + if ( isset( $wp->query_vars['set-default-payment-method'] ) ) { + wc_nocache_headers(); + + $token_id = absint( $wp->query_vars['set-default-payment-method'] ); + $token = WC_Payment_Tokens::get( $token_id ); + + if ( is_null( $token ) || get_current_user_id() !== $token->get_user_id() || ! isset( $_REQUEST['_wpnonce'] ) || false === wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'set-default-payment-method-' . $token_id ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + wc_add_notice( __( 'Invalid payment method.', 'woocommerce' ), 'error' ); + } else { + WC_Payment_Tokens::set_users_default( $token->get_user_id(), intval( $token_id ) ); + wc_add_notice( __( 'This payment method was successfully set as your default.', 'woocommerce' ) ); + } + + wp_safe_redirect( wc_get_account_endpoint_url( 'payment-methods' ) ); + exit(); + } + + } + + /** + * Remove from cart/update. + */ + public static function update_cart_action() { + if ( ! ( isset( $_REQUEST['apply_coupon'] ) || isset( $_REQUEST['remove_coupon'] ) || isset( $_REQUEST['remove_item'] ) || isset( $_REQUEST['undo_item'] ) || isset( $_REQUEST['update_cart'] ) || isset( $_REQUEST['proceed'] ) ) ) { + return; + } + + wc_nocache_headers(); + + $nonce_value = wc_get_var( $_REQUEST['woocommerce-cart-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. + + if ( ! empty( $_POST['apply_coupon'] ) && ! empty( $_POST['coupon_code'] ) ) { + WC()->cart->add_discount( wc_format_coupon_code( wp_unslash( $_POST['coupon_code'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + } elseif ( isset( $_GET['remove_coupon'] ) ) { + WC()->cart->remove_coupon( wc_format_coupon_code( urldecode( wp_unslash( $_GET['remove_coupon'] ) ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + } elseif ( ! empty( $_GET['remove_item'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-cart' ) ) { + $cart_item_key = sanitize_text_field( wp_unslash( $_GET['remove_item'] ) ); + $cart_item = WC()->cart->get_cart_item( $cart_item_key ); + + if ( $cart_item ) { + WC()->cart->remove_cart_item( $cart_item_key ); + + $product = wc_get_product( $cart_item['product_id'] ); + + /* translators: %s: Item name. */ + $item_removed_title = apply_filters( 'woocommerce_cart_item_removed_title', $product ? sprintf( _x( '“%s”', 'Item name in quotes', 'woocommerce' ), $product->get_name() ) : __( 'Item', 'woocommerce' ), $cart_item ); + + // Don't show undo link if removed item is out of stock. + if ( $product && $product->is_in_stock() && $product->has_enough_stock( $cart_item['quantity'] ) ) { + /* Translators: %s Product title. */ + $removed_notice = sprintf( __( '%s removed.', 'woocommerce' ), $item_removed_title ); + $removed_notice .= ' ' . __( 'Undo?', 'woocommerce' ) . ''; + } else { + /* Translators: %s Product title. */ + $removed_notice = sprintf( __( '%s removed.', 'woocommerce' ), $item_removed_title ); + } + + wc_add_notice( $removed_notice, apply_filters( 'woocommerce_cart_item_removed_notice_type', 'success' ) ); + } + + $referer = wp_get_referer() ? remove_query_arg( array( 'remove_item', 'add-to-cart', 'added-to-cart', 'order_again', '_wpnonce' ), add_query_arg( 'removed_item', '1', wp_get_referer() ) ) : wc_get_cart_url(); + wp_safe_redirect( $referer ); + exit; + + } elseif ( ! empty( $_GET['undo_item'] ) && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-cart' ) ) { + + // Undo Cart Item. + $cart_item_key = sanitize_text_field( wp_unslash( $_GET['undo_item'] ) ); + + WC()->cart->restore_cart_item( $cart_item_key ); + + $referer = wp_get_referer() ? remove_query_arg( array( 'undo_item', '_wpnonce' ), wp_get_referer() ) : wc_get_cart_url(); + wp_safe_redirect( $referer ); + exit; + + } + + // Update Cart - checks apply_coupon too because they are in the same form. + if ( ( ! empty( $_POST['apply_coupon'] ) || ! empty( $_POST['update_cart'] ) || ! empty( $_POST['proceed'] ) ) && wp_verify_nonce( $nonce_value, 'woocommerce-cart' ) ) { + + $cart_updated = false; + $cart_totals = isset( $_POST['cart'] ) ? wp_unslash( $_POST['cart'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + if ( ! WC()->cart->is_empty() && is_array( $cart_totals ) ) { + foreach ( WC()->cart->get_cart() as $cart_item_key => $values ) { + + $_product = $values['data']; + + // Skip product if no updated quantity was posted. + if ( ! isset( $cart_totals[ $cart_item_key ] ) || ! isset( $cart_totals[ $cart_item_key ]['qty'] ) ) { + continue; + } + + // Sanitize. + $quantity = apply_filters( 'woocommerce_stock_amount_cart_item', wc_stock_amount( preg_replace( '/[^0-9\.]/', '', $cart_totals[ $cart_item_key ]['qty'] ) ), $cart_item_key ); + + if ( '' === $quantity || $quantity === $values['quantity'] ) { + continue; + } + + // Update cart validation. + $passed_validation = apply_filters( 'woocommerce_update_cart_validation', true, $cart_item_key, $values, $quantity ); + + // is_sold_individually. + if ( $_product->is_sold_individually() && $quantity > 1 ) { + /* Translators: %s Product title. */ + wc_add_notice( sprintf( __( 'You can only have 1 %s in your cart.', 'woocommerce' ), $_product->get_name() ), 'error' ); + $passed_validation = false; + } + + if ( $passed_validation ) { + WC()->cart->set_quantity( $cart_item_key, $quantity, false ); + $cart_updated = true; + } + } + } + + // Trigger action - let 3rd parties update the cart if they need to and update the $cart_updated variable. + $cart_updated = apply_filters( 'woocommerce_update_cart_action_cart_updated', $cart_updated ); + + if ( $cart_updated ) { + WC()->cart->calculate_totals(); + } + + if ( ! empty( $_POST['proceed'] ) ) { + wp_safe_redirect( wc_get_checkout_url() ); + exit; + } elseif ( $cart_updated ) { + wc_add_notice( __( 'Cart updated.', 'woocommerce' ), apply_filters( 'woocommerce_cart_updated_notice_type', 'success' ) ); + $referer = remove_query_arg( array( 'remove_coupon', 'add-to-cart' ), ( wp_get_referer() ? wp_get_referer() : wc_get_cart_url() ) ); + wp_safe_redirect( $referer ); + exit; + } + } + } + + /** + * Place a previous order again. + * + * @deprecated 3.5.0 Logic moved to cart session handling. + */ + public static function order_again() { + wc_deprecated_function( 'WC_Form_Handler::order_again', '3.5', 'This method should not be called manually.' ); + } + + /** + * Cancel a pending order. + */ + public static function cancel_order() { + if ( + isset( $_GET['cancel_order'] ) && + isset( $_GET['order'] ) && + isset( $_GET['order_id'] ) && + ( isset( $_GET['_wpnonce'] ) && wp_verify_nonce( wp_unslash( $_GET['_wpnonce'] ), 'woocommerce-cancel_order' ) ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + ) { + wc_nocache_headers(); + + $order_key = wp_unslash( $_GET['order'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $order_id = absint( $_GET['order_id'] ); + $order = wc_get_order( $order_id ); + $user_can_cancel = current_user_can( 'cancel_order', $order_id ); + $order_can_cancel = $order->has_status( apply_filters( 'woocommerce_valid_order_statuses_for_cancel', array( 'pending', 'failed' ), $order ) ); + $redirect = isset( $_GET['redirect'] ) ? wp_unslash( $_GET['redirect'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + if ( $user_can_cancel && $order_can_cancel && $order->get_id() === $order_id && hash_equals( $order->get_order_key(), $order_key ) ) { + + // Cancel the order + restore stock. + WC()->session->set( 'order_awaiting_payment', false ); + $order->update_status( 'cancelled', __( 'Order cancelled by customer.', 'woocommerce' ) ); + + wc_add_notice( apply_filters( 'woocommerce_order_cancelled_notice', __( 'Your order was cancelled.', 'woocommerce' ) ), apply_filters( 'woocommerce_order_cancelled_notice_type', 'notice' ) ); + + do_action( 'woocommerce_cancelled_order', $order->get_id() ); + + } elseif ( $user_can_cancel && ! $order_can_cancel ) { + wc_add_notice( __( 'Your order can no longer be cancelled. Please contact us if you need assistance.', 'woocommerce' ), 'error' ); + } else { + wc_add_notice( __( 'Invalid order.', 'woocommerce' ), 'error' ); + } + + if ( $redirect ) { + wp_safe_redirect( $redirect ); + exit; + } + } + } + + /** + * Add to cart action. + * + * Checks for a valid request, does validation (via hooks) and then redirects if valid. + * + * @param bool $url (default: false) URL to redirect to. + */ + public static function add_to_cart_action( $url = false ) { + if ( ! isset( $_REQUEST['add-to-cart'] ) || ! is_numeric( wp_unslash( $_REQUEST['add-to-cart'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + return; + } + + wc_nocache_headers(); + + $product_id = apply_filters( 'woocommerce_add_to_cart_product_id', absint( wp_unslash( $_REQUEST['add-to-cart'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $was_added_to_cart = false; + $adding_to_cart = wc_get_product( $product_id ); + + if ( ! $adding_to_cart ) { + return; + } + + $add_to_cart_handler = apply_filters( 'woocommerce_add_to_cart_handler', $adding_to_cart->get_type(), $adding_to_cart ); + + if ( 'variable' === $add_to_cart_handler || 'variation' === $add_to_cart_handler ) { + $was_added_to_cart = self::add_to_cart_handler_variable( $product_id ); + } elseif ( 'grouped' === $add_to_cart_handler ) { + $was_added_to_cart = self::add_to_cart_handler_grouped( $product_id ); + } elseif ( has_action( 'woocommerce_add_to_cart_handler_' . $add_to_cart_handler ) ) { + do_action( 'woocommerce_add_to_cart_handler_' . $add_to_cart_handler, $url ); // Custom handler. + } else { + $was_added_to_cart = self::add_to_cart_handler_simple( $product_id ); + } + + // If we added the product to the cart we can now optionally do a redirect. + if ( $was_added_to_cart && 0 === wc_notice_count( 'error' ) ) { + $url = apply_filters( 'woocommerce_add_to_cart_redirect', $url, $adding_to_cart ); + + if ( $url ) { + wp_safe_redirect( $url ); + exit; + } elseif ( 'yes' === get_option( 'woocommerce_cart_redirect_after_add' ) ) { + wp_safe_redirect( wc_get_cart_url() ); + exit; + } + } + } + + /** + * Handle adding simple products to the cart. + * + * @since 2.4.6 Split from add_to_cart_action. + * @param int $product_id Product ID to add to the cart. + * @return bool success or not + */ + private static function add_to_cart_handler_simple( $product_id ) { + $quantity = empty( $_REQUEST['quantity'] ) ? 1 : wc_stock_amount( wp_unslash( $_REQUEST['quantity'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity ); + + if ( $passed_validation && false !== WC()->cart->add_to_cart( $product_id, $quantity ) ) { + wc_add_to_cart_message( array( $product_id => $quantity ), true ); + return true; + } + return false; + } + + /** + * Handle adding grouped products to the cart. + * + * @since 2.4.6 Split from add_to_cart_action. + * @param int $product_id Product ID to add to the cart. + * @return bool success or not + */ + private static function add_to_cart_handler_grouped( $product_id ) { + $was_added_to_cart = false; + $added_to_cart = array(); + $items = isset( $_REQUEST['quantity'] ) && is_array( $_REQUEST['quantity'] ) ? wp_unslash( $_REQUEST['quantity'] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + if ( ! empty( $items ) ) { + $quantity_set = false; + + foreach ( $items as $item => $quantity ) { + $quantity = wc_stock_amount( $quantity ); + if ( $quantity <= 0 ) { + continue; + } + $quantity_set = true; + + // Add to cart validation. + $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $item, $quantity ); + + // Suppress total recalculation until finished. + remove_action( 'woocommerce_add_to_cart', array( WC()->cart, 'calculate_totals' ), 20, 0 ); + + if ( $passed_validation && false !== WC()->cart->add_to_cart( $item, $quantity ) ) { + $was_added_to_cart = true; + $added_to_cart[ $item ] = $quantity; + } + + add_action( 'woocommerce_add_to_cart', array( WC()->cart, 'calculate_totals' ), 20, 0 ); + } + + if ( ! $was_added_to_cart && ! $quantity_set ) { + wc_add_notice( __( 'Please choose the quantity of items you wish to add to your cart…', 'woocommerce' ), 'error' ); + } elseif ( $was_added_to_cart ) { + wc_add_to_cart_message( $added_to_cart ); + WC()->cart->calculate_totals(); + return true; + } + } elseif ( $product_id ) { + /* Link on product archives */ + wc_add_notice( __( 'Please choose a product to add to your cart…', 'woocommerce' ), 'error' ); + } + return false; + } + + /** + * Handle adding variable products to the cart. + * + * @since 2.4.6 Split from add_to_cart_action. + * @throws Exception If add to cart fails. + * @param int $product_id Product ID to add to the cart. + * @return bool success or not + */ + private static function add_to_cart_handler_variable( $product_id ) { + $variation_id = empty( $_REQUEST['variation_id'] ) ? '' : absint( wp_unslash( $_REQUEST['variation_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $quantity = empty( $_REQUEST['quantity'] ) ? 1 : wc_stock_amount( wp_unslash( $_REQUEST['quantity'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $variations = array(); + + $product = wc_get_product( $product_id ); + + foreach ( $_REQUEST as $key => $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( 'attribute_' !== substr( $key, 0, 10 ) ) { + continue; + } + + $variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value ); + } + + $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity, $variation_id, $variations ); + + if ( ! $passed_validation ) { + return false; + } + + // Prevent parent variable product from being added to cart. + if ( empty( $variation_id ) && $product && $product->is_type( 'variable' ) ) { + /* translators: 1: product link, 2: product name */ + wc_add_notice( sprintf( __( 'Please choose product options by visiting %2$s.', 'woocommerce' ), esc_url( get_permalink( $product_id ) ), esc_html( $product->get_name() ) ), 'error' ); + + return false; + } + + if ( false !== WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $variations ) ) { + wc_add_to_cart_message( array( $product_id => $quantity ), true ); + return true; + } + + return false; + } + + /** + * Process the login form. + * + * @throws Exception On login error. + */ + public static function process_login() { + // The global form-login.php template used `_wpnonce` in template versions < 3.3.0. + $nonce_value = wc_get_var( $_REQUEST['woocommerce-login-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. + + if ( isset( $_POST['login'], $_POST['username'], $_POST['password'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-login' ) ) { + + try { + $creds = array( + 'user_login' => trim( wp_unslash( $_POST['username'] ) ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + 'user_password' => $_POST['password'], // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + 'remember' => isset( $_POST['rememberme'] ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + ); + + $validation_error = new WP_Error(); + $validation_error = apply_filters( 'woocommerce_process_login_errors', $validation_error, $creds['user_login'], $creds['user_password'] ); + + if ( $validation_error->get_error_code() ) { + throw new Exception( '' . __( 'Error:', 'woocommerce' ) . ' ' . $validation_error->get_error_message() ); + } + + if ( empty( $creds['user_login'] ) ) { + throw new Exception( '' . __( 'Error:', 'woocommerce' ) . ' ' . __( 'Username is required.', 'woocommerce' ) ); + } + + // On multisite, ensure user exists on current site, if not add them before allowing login. + if ( is_multisite() ) { + $user_data = get_user_by( is_email( $creds['user_login'] ) ? 'email' : 'login', $creds['user_login'] ); + + if ( $user_data && ! is_user_member_of_blog( $user_data->ID, get_current_blog_id() ) ) { + add_user_to_blog( get_current_blog_id(), $user_data->ID, 'customer' ); + } + } + + // Perform the login. + $user = wp_signon( apply_filters( 'woocommerce_login_credentials', $creds ), is_ssl() ); + + if ( is_wp_error( $user ) ) { + throw new Exception( $user->get_error_message() ); + } else { + + if ( ! empty( $_POST['redirect'] ) ) { + $redirect = wp_unslash( $_POST['redirect'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + } elseif ( wc_get_raw_referer() ) { + $redirect = wc_get_raw_referer(); + } else { + $redirect = wc_get_page_permalink( 'myaccount' ); + } + + wp_redirect( wp_validate_redirect( apply_filters( 'woocommerce_login_redirect', remove_query_arg( 'wc_error', $redirect ), $user ), wc_get_page_permalink( 'myaccount' ) ) ); // phpcs:ignore + exit; + } + } catch ( Exception $e ) { + wc_add_notice( apply_filters( 'login_errors', $e->getMessage() ), 'error' ); + do_action( 'woocommerce_login_failed' ); + } + } + } + + /** + * Handle lost password form. + */ + public static function process_lost_password() { + if ( isset( $_POST['wc_reset_password'], $_POST['user_login'] ) ) { + $nonce_value = wc_get_var( $_REQUEST['woocommerce-lost-password-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. + + if ( ! wp_verify_nonce( $nonce_value, 'lost_password' ) ) { + return; + } + + $success = WC_Shortcode_My_Account::retrieve_password(); + + // If successful, redirect to my account with query arg set. + if ( $success ) { + wp_safe_redirect( add_query_arg( 'reset-link-sent', 'true', wc_get_account_endpoint_url( 'lost-password' ) ) ); + exit; + } + } + } + + /** + * Handle reset password form. + */ + public static function process_reset_password() { + $nonce_value = wc_get_var( $_REQUEST['woocommerce-reset-password-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. + + if ( ! wp_verify_nonce( $nonce_value, 'reset_password' ) ) { + return; + } + + $posted_fields = array( 'wc_reset_password', 'password_1', 'password_2', 'reset_key', 'reset_login' ); + + foreach ( $posted_fields as $field ) { + if ( ! isset( $_POST[ $field ] ) ) { + return; + } + + if ( in_array( $field, array( 'password_1', 'password_2' ), true ) ) { + // Don't unslash password fields + // @see https://github.com/woocommerce/woocommerce/issues/23922. + $posted_fields[ $field ] = $_POST[ $field ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + } else { + $posted_fields[ $field ] = wp_unslash( $_POST[ $field ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + } + } + + $user = WC_Shortcode_My_Account::check_password_reset_key( $posted_fields['reset_key'], $posted_fields['reset_login'] ); + + if ( $user instanceof WP_User ) { + if ( empty( $posted_fields['password_1'] ) ) { + wc_add_notice( __( 'Please enter your password.', 'woocommerce' ), 'error' ); + } + + if ( $posted_fields['password_1'] !== $posted_fields['password_2'] ) { + wc_add_notice( __( 'Passwords do not match.', 'woocommerce' ), 'error' ); + } + + $errors = new WP_Error(); + + do_action( 'validate_password_reset', $errors, $user ); + + wc_add_wp_error_notices( $errors ); + + if ( 0 === wc_notice_count( 'error' ) ) { + WC_Shortcode_My_Account::reset_password( $user, $posted_fields['password_1'] ); + + do_action( 'woocommerce_customer_reset_password', $user ); + + wp_safe_redirect( add_query_arg( 'password-reset', 'true', wc_get_page_permalink( 'myaccount' ) ) ); + exit; + } + } + } + + /** + * Process the registration form. + * + * @throws Exception On registration error. + */ + public static function process_registration() { + $nonce_value = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $nonce_value = isset( $_POST['woocommerce-register-nonce'] ) ? wp_unslash( $_POST['woocommerce-register-nonce'] ) : $nonce_value; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + if ( isset( $_POST['register'], $_POST['email'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-register' ) ) { + $username = 'no' === get_option( 'woocommerce_registration_generate_username' ) && isset( $_POST['username'] ) ? wp_unslash( $_POST['username'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $password = 'no' === get_option( 'woocommerce_registration_generate_password' ) && isset( $_POST['password'] ) ? $_POST['password'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + $email = wp_unslash( $_POST['email'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + try { + $validation_error = new WP_Error(); + $validation_error = apply_filters( 'woocommerce_process_registration_errors', $validation_error, $username, $password, $email ); + $validation_errors = $validation_error->get_error_messages(); + + if ( 1 === count( $validation_errors ) ) { + throw new Exception( $validation_error->get_error_message() ); + } elseif ( $validation_errors ) { + foreach ( $validation_errors as $message ) { + wc_add_notice( '' . __( 'Error:', 'woocommerce' ) . ' ' . $message, 'error' ); + } + throw new Exception(); + } + + $new_customer = wc_create_new_customer( sanitize_email( $email ), wc_clean( $username ), $password ); + + if ( is_wp_error( $new_customer ) ) { + throw new Exception( $new_customer->get_error_message() ); + } + + if ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) ) { + wc_add_notice( __( 'Your account was created successfully and a password has been sent to your email address.', 'woocommerce' ) ); + } else { + wc_add_notice( __( 'Your account was created successfully. Your login details have been sent to your email address.', 'woocommerce' ) ); + } + + // Only redirect after a forced login - otherwise output a success notice. + if ( apply_filters( 'woocommerce_registration_auth_new_customer', true, $new_customer ) ) { + wc_set_customer_auth_cookie( $new_customer ); + + if ( ! empty( $_POST['redirect'] ) ) { + $redirect = wp_sanitize_redirect( wp_unslash( $_POST['redirect'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + } elseif ( wc_get_raw_referer() ) { + $redirect = wc_get_raw_referer(); + } else { + $redirect = wc_get_page_permalink( 'myaccount' ); + } + + wp_redirect( wp_validate_redirect( apply_filters( 'woocommerce_registration_redirect', $redirect ), wc_get_page_permalink( 'myaccount' ) ) ); //phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect + exit; + } + } catch ( Exception $e ) { + if ( $e->getMessage() ) { + wc_add_notice( '' . __( 'Error:', 'woocommerce' ) . ' ' . $e->getMessage(), 'error' ); + } + } + } + } +} + +WC_Form_Handler::init(); diff --git a/plugins/woocommerce/includes/class-wc-frontend-scripts.php b/plugins/woocommerce/includes/class-wc-frontend-scripts.php new file mode 100644 index 00000000000..0b23f6fcca1 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-frontend-scripts.php @@ -0,0 +1,621 @@ + array( + 'src' => self::get_asset_url( 'assets/css/woocommerce-layout.css' ), + 'deps' => '', + 'version' => $version, + 'media' => 'all', + 'has_rtl' => true, + ), + 'woocommerce-smallscreen' => array( + 'src' => self::get_asset_url( 'assets/css/woocommerce-smallscreen.css' ), + 'deps' => 'woocommerce-layout', + 'version' => $version, + 'media' => 'only screen and (max-width: ' . apply_filters( 'woocommerce_style_smallscreen_breakpoint', '768px' ) . ')', + 'has_rtl' => true, + ), + 'woocommerce-general' => array( + 'src' => self::get_asset_url( 'assets/css/woocommerce.css' ), + 'deps' => '', + 'version' => $version, + 'media' => 'all', + 'has_rtl' => true, + ), + ) + ); + } + + /** + * Return asset URL. + * + * @param string $path Assets path. + * @return string + */ + private static function get_asset_url( $path ) { + return apply_filters( 'woocommerce_get_asset_url', plugins_url( $path, WC_PLUGIN_FILE ), $path ); + } + + /** + * Register a script for use. + * + * @uses wp_register_script() + * @param string $handle Name of the script. Should be unique. + * @param string $path Full URL of the script, or path of the script relative to the WordPress root directory. + * @param string[] $deps An array of registered script handles this script depends on. + * @param string $version String specifying script version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added. + * @param boolean $in_footer Whether to enqueue the script before instead of in the . Default 'false'. + */ + private static function register_script( $handle, $path, $deps = array( 'jquery' ), $version = WC_VERSION, $in_footer = true ) { + self::$scripts[] = $handle; + wp_register_script( $handle, $path, $deps, $version, $in_footer ); + } + + /** + * Register and enqueue a script for use. + * + * @uses wp_enqueue_script() + * @param string $handle Name of the script. Should be unique. + * @param string $path Full URL of the script, or path of the script relative to the WordPress root directory. + * @param string[] $deps An array of registered script handles this script depends on. + * @param string $version String specifying script version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added. + * @param boolean $in_footer Whether to enqueue the script before instead of in the . Default 'false'. + */ + private static function enqueue_script( $handle, $path = '', $deps = array( 'jquery' ), $version = WC_VERSION, $in_footer = true ) { + if ( ! in_array( $handle, self::$scripts, true ) && $path ) { + self::register_script( $handle, $path, $deps, $version, $in_footer ); + } + wp_enqueue_script( $handle ); + } + + /** + * Register a style for use. + * + * @uses wp_register_style() + * @param string $handle Name of the stylesheet. Should be unique. + * @param string $path Full URL of the stylesheet, or path of the stylesheet relative to the WordPress root directory. + * @param string[] $deps An array of registered stylesheet handles this stylesheet depends on. + * @param string $version String specifying stylesheet version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added. + * @param string $media The media for which this stylesheet has been defined. Accepts media types like 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'. + * @param boolean $has_rtl If has RTL version to load too. + */ + private static function register_style( $handle, $path, $deps = array(), $version = WC_VERSION, $media = 'all', $has_rtl = false ) { + self::$styles[] = $handle; + wp_register_style( $handle, $path, $deps, $version, $media ); + + if ( $has_rtl ) { + wp_style_add_data( $handle, 'rtl', 'replace' ); + } + } + + /** + * Register and enqueue a styles for use. + * + * @uses wp_enqueue_style() + * @param string $handle Name of the stylesheet. Should be unique. + * @param string $path Full URL of the stylesheet, or path of the stylesheet relative to the WordPress root directory. + * @param string[] $deps An array of registered stylesheet handles this stylesheet depends on. + * @param string $version String specifying stylesheet version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added. + * @param string $media The media for which this stylesheet has been defined. Accepts media types like 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'. + * @param boolean $has_rtl If has RTL version to load too. + */ + private static function enqueue_style( $handle, $path = '', $deps = array(), $version = WC_VERSION, $media = 'all', $has_rtl = false ) { + if ( ! in_array( $handle, self::$styles, true ) && $path ) { + self::register_style( $handle, $path, $deps, $version, $media, $has_rtl ); + } + wp_enqueue_style( $handle ); + } + + /** + * Register all WC scripts. + */ + private static function register_scripts() { + $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; + $version = Constants::get_constant( 'WC_VERSION' ); + + $register_scripts = array( + 'flexslider' => array( + 'src' => self::get_asset_url( 'assets/js/flexslider/jquery.flexslider' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '2.7.2-wc.' . $version, + ), + 'js-cookie' => array( + 'src' => self::get_asset_url( 'assets/js/js-cookie/js.cookie' . $suffix . '.js' ), + 'deps' => array(), + 'version' => '2.1.4-wc.' . $version, + ), + 'jquery-blockui' => array( + 'src' => self::get_asset_url( 'assets/js/jquery-blockui/jquery.blockUI' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '2.7.0-wc.' . $version, + ), + 'jquery-cookie' => array( // deprecated. + 'src' => self::get_asset_url( 'assets/js/jquery-cookie/jquery.cookie' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '1.4.1-wc.' . $version, + ), + 'jquery-payment' => array( + 'src' => self::get_asset_url( 'assets/js/jquery-payment/jquery.payment' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '3.0.0-wc.' . $version, + ), + 'photoswipe' => array( + 'src' => self::get_asset_url( 'assets/js/photoswipe/photoswipe' . $suffix . '.js' ), + 'deps' => array(), + 'version' => '4.1.1-wc.' . $version, + ), + 'photoswipe-ui-default' => array( + 'src' => self::get_asset_url( 'assets/js/photoswipe/photoswipe-ui-default' . $suffix . '.js' ), + 'deps' => array( 'photoswipe' ), + 'version' => '4.1.1-wc.' . $version, + ), + 'prettyPhoto' => array( // deprecated. + 'src' => self::get_asset_url( 'assets/js/prettyPhoto/jquery.prettyPhoto' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '3.1.6-wc.' . $version, + ), + 'prettyPhoto-init' => array( // deprecated. + 'src' => self::get_asset_url( 'assets/js/prettyPhoto/jquery.prettyPhoto.init' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'prettyPhoto' ), + 'version' => $version, + ), + 'select2' => array( + 'src' => self::get_asset_url( 'assets/js/select2/select2.full' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '4.0.3-wc.' . $version, + ), + 'selectWoo' => array( + 'src' => self::get_asset_url( 'assets/js/selectWoo/selectWoo.full' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '1.0.9-wc.' . $version, + ), + 'wc-address-i18n' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/address-i18n' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'wc-country-select' ), + 'version' => $version, + ), + 'wc-add-payment-method' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/add-payment-method' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'woocommerce' ), + 'version' => $version, + ), + 'wc-cart' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/cart' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'woocommerce', 'wc-country-select', 'wc-address-i18n' ), + 'version' => $version, + ), + 'wc-cart-fragments' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/cart-fragments' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'js-cookie' ), + 'version' => $version, + ), + 'wc-checkout' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/checkout' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'woocommerce', 'wc-country-select', 'wc-address-i18n' ), + 'version' => $version, + ), + 'wc-country-select' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/country-select' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => $version, + ), + 'wc-credit-card-form' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/credit-card-form' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'jquery-payment' ), + 'version' => $version, + ), + 'wc-add-to-cart' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/add-to-cart' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'jquery-blockui' ), + 'version' => $version, + ), + 'wc-add-to-cart-variation' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/add-to-cart-variation' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'wp-util', 'jquery-blockui' ), + 'version' => $version, + ), + 'wc-geolocation' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/geolocation' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => $version, + ), + 'wc-lost-password' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/lost-password' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'woocommerce' ), + 'version' => $version, + ), + 'wc-password-strength-meter' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/password-strength-meter' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'password-strength-meter' ), + 'version' => $version, + ), + 'wc-single-product' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/single-product' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => $version, + ), + 'woocommerce' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/woocommerce' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'jquery-blockui', 'js-cookie' ), + 'version' => $version, + ), + 'zoom' => array( + 'src' => self::get_asset_url( 'assets/js/zoom/jquery.zoom' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '1.7.21-wc.' . $version, + ), + ); + foreach ( $register_scripts as $name => $props ) { + self::register_script( $name, $props['src'], $props['deps'], $props['version'] ); + } + } + + /** + * Register all WC styles. + */ + private static function register_styles() { + $version = Constants::get_constant( 'WC_VERSION' ); + + $register_styles = array( + 'photoswipe' => array( + 'src' => self::get_asset_url( 'assets/css/photoswipe/photoswipe.min.css' ), + 'deps' => array(), + 'version' => $version, + 'has_rtl' => false, + ), + 'photoswipe-default-skin' => array( + 'src' => self::get_asset_url( 'assets/css/photoswipe/default-skin/default-skin.min.css' ), + 'deps' => array( 'photoswipe' ), + 'version' => $version, + 'has_rtl' => false, + ), + 'select2' => array( + 'src' => self::get_asset_url( 'assets/css/select2.css' ), + 'deps' => array(), + 'version' => $version, + 'has_rtl' => false, + ), + 'woocommerce_prettyPhoto_css' => array( // deprecated. + 'src' => self::get_asset_url( 'assets/css/prettyPhoto.css' ), + 'deps' => array(), + 'version' => $version, + 'has_rtl' => true, + ), + ); + foreach ( $register_styles as $name => $props ) { + self::register_style( $name, $props['src'], $props['deps'], $props['version'], 'all', $props['has_rtl'] ); + } + } + + /** + * Register/queue frontend scripts. + */ + public static function load_scripts() { + global $post; + + if ( ! did_action( 'before_woocommerce_init' ) ) { + return; + } + + self::register_scripts(); + self::register_styles(); + + if ( 'yes' === get_option( 'woocommerce_enable_ajax_add_to_cart' ) ) { + self::enqueue_script( 'wc-add-to-cart' ); + } + if ( is_cart() ) { + self::enqueue_script( 'wc-cart' ); + } + if ( is_cart() || is_checkout() || is_account_page() ) { + self::enqueue_script( 'selectWoo' ); + self::enqueue_style( 'select2' ); + + // Password strength meter. Load in checkout, account login and edit account page. + if ( ( 'no' === get_option( 'woocommerce_registration_generate_password' ) && ! is_user_logged_in() ) || is_edit_account_page() || is_lost_password_page() ) { + self::enqueue_script( 'wc-password-strength-meter' ); + } + } + if ( is_checkout() ) { + self::enqueue_script( 'wc-checkout' ); + } + if ( is_add_payment_method_page() ) { + self::enqueue_script( 'wc-add-payment-method' ); + } + if ( is_lost_password_page() ) { + self::enqueue_script( 'wc-lost-password' ); + } + + // Load gallery scripts on product pages only if supported. + if ( is_product() || ( ! empty( $post->post_content ) && strstr( $post->post_content, '[product_page' ) ) ) { + if ( current_theme_supports( 'wc-product-gallery-zoom' ) ) { + self::enqueue_script( 'zoom' ); + } + if ( current_theme_supports( 'wc-product-gallery-slider' ) ) { + self::enqueue_script( 'flexslider' ); + } + if ( current_theme_supports( 'wc-product-gallery-lightbox' ) ) { + self::enqueue_script( 'photoswipe-ui-default' ); + self::enqueue_style( 'photoswipe-default-skin' ); + add_action( 'wp_footer', 'woocommerce_photoswipe' ); + } + self::enqueue_script( 'wc-single-product' ); + } + + // Only enqueue the geolocation script if the Default Current Address is set to "Geolocate + // (with Page Caching Support) and outside of the cart, checkout, account and customizer preview. + if ( + 'geolocation_ajax' === get_option( 'woocommerce_default_customer_address' ) + && ! ( is_cart() || is_account_page() || is_checkout() || is_customize_preview() ) + ) { + $ua = strtolower( wc_get_user_agent() ); // Exclude common bots from geolocation by user agent. + + if ( ! strstr( $ua, 'bot' ) && ! strstr( $ua, 'spider' ) && ! strstr( $ua, 'crawl' ) ) { + self::enqueue_script( 'wc-geolocation' ); + } + } + + // Global frontend scripts. + self::enqueue_script( 'woocommerce' ); + self::enqueue_script( 'wc-cart-fragments' ); + + // CSS Styles. + $enqueue_styles = self::get_styles(); + if ( $enqueue_styles ) { + foreach ( $enqueue_styles as $handle => $args ) { + if ( ! isset( $args['has_rtl'] ) ) { + $args['has_rtl'] = false; + } + + self::enqueue_style( $handle, $args['src'], $args['deps'], $args['version'], $args['media'], $args['has_rtl'] ); + } + } + + // Placeholder style. + wp_register_style( 'woocommerce-inline', false ); // phpcs:ignore + wp_enqueue_style( 'woocommerce-inline' ); + + if ( true === wc_string_to_bool( get_option( 'woocommerce_checkout_highlight_required_fields', 'yes' ) ) ) { + wp_add_inline_style( 'woocommerce-inline', '.woocommerce form .form-row .required { visibility: visible; }' ); + } else { + wp_add_inline_style( 'woocommerce-inline', '.woocommerce form .form-row .required { visibility: hidden; }' ); + } + } + + /** + * Localize a WC script once. + * + * @since 2.3.0 this needs less wp_script_is() calls due to https://core.trac.wordpress.org/ticket/28404 being added in WP 4.0. + * @param string $handle Script handle the data will be attached to. + */ + private static function localize_script( $handle ) { + if ( ! in_array( $handle, self::$wp_localize_scripts, true ) && wp_script_is( $handle ) ) { + $data = self::get_script_data( $handle ); + + if ( ! $data ) { + return; + } + + $name = str_replace( '-', '_', $handle ) . '_params'; + self::$wp_localize_scripts[] = $handle; + wp_localize_script( $handle, $name, apply_filters( $name, $data ) ); + } + } + + /** + * Return data for script handles. + * + * @param string $handle Script handle the data will be attached to. + * @return array|bool + */ + private static function get_script_data( $handle ) { + global $wp; + + switch ( $handle ) { + case 'woocommerce': + $params = array( + 'ajax_url' => WC()->ajax_url(), + 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), + ); + break; + case 'wc-geolocation': + $params = array( + 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), + 'home_url' => remove_query_arg( 'lang', home_url() ), // FIX for WPML compatibility. + ); + break; + case 'wc-single-product': + $params = array( + 'i18n_required_rating_text' => esc_attr__( 'Please select a rating', 'woocommerce' ), + 'review_rating_required' => wc_review_ratings_required() ? 'yes' : 'no', + 'flexslider' => apply_filters( + 'woocommerce_single_product_carousel_options', + array( + 'rtl' => is_rtl(), + 'animation' => 'slide', + 'smoothHeight' => true, + 'directionNav' => false, + 'controlNav' => 'thumbnails', + 'slideshow' => false, + 'animationSpeed' => 500, + 'animationLoop' => false, // Breaks photoswipe pagination if true. + 'allowOneSlide' => false, + ) + ), + 'zoom_enabled' => apply_filters( 'woocommerce_single_product_zoom_enabled', get_theme_support( 'wc-product-gallery-zoom' ) ), + 'zoom_options' => apply_filters( 'woocommerce_single_product_zoom_options', array() ), + 'photoswipe_enabled' => apply_filters( 'woocommerce_single_product_photoswipe_enabled', get_theme_support( 'wc-product-gallery-lightbox' ) ), + 'photoswipe_options' => apply_filters( + 'woocommerce_single_product_photoswipe_options', + array( + 'shareEl' => false, + 'closeOnScroll' => false, + 'history' => false, + 'hideAnimationDuration' => 0, + 'showAnimationDuration' => 0, + ) + ), + 'flexslider_enabled' => apply_filters( 'woocommerce_single_product_flexslider_enabled', get_theme_support( 'wc-product-gallery-slider' ) ), + ); + break; + case 'wc-checkout': + $params = array( + 'ajax_url' => WC()->ajax_url(), + 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), + 'update_order_review_nonce' => wp_create_nonce( 'update-order-review' ), + 'apply_coupon_nonce' => wp_create_nonce( 'apply-coupon' ), + 'remove_coupon_nonce' => wp_create_nonce( 'remove-coupon' ), + 'option_guest_checkout' => get_option( 'woocommerce_enable_guest_checkout' ), + 'checkout_url' => WC_AJAX::get_endpoint( 'checkout' ), + 'is_checkout' => is_checkout() && empty( $wp->query_vars['order-pay'] ) && ! isset( $wp->query_vars['order-received'] ) ? 1 : 0, + 'debug_mode' => Constants::is_true( 'WP_DEBUG' ), + 'i18n_checkout_error' => esc_attr__( 'Error processing checkout. Please try again.', 'woocommerce' ), + ); + break; + case 'wc-address-i18n': + $params = array( + 'locale' => wp_json_encode( WC()->countries->get_country_locale() ), + 'locale_fields' => wp_json_encode( WC()->countries->get_country_locale_field_selectors() ), + 'i18n_required_text' => esc_attr__( 'required', 'woocommerce' ), + 'i18n_optional_text' => esc_html__( 'optional', 'woocommerce' ), + ); + break; + case 'wc-cart': + $params = array( + 'ajax_url' => WC()->ajax_url(), + 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), + 'update_shipping_method_nonce' => wp_create_nonce( 'update-shipping-method' ), + 'apply_coupon_nonce' => wp_create_nonce( 'apply-coupon' ), + 'remove_coupon_nonce' => wp_create_nonce( 'remove-coupon' ), + ); + break; + case 'wc-cart-fragments': + $params = array( + 'ajax_url' => WC()->ajax_url(), + 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), + 'cart_hash_key' => apply_filters( 'woocommerce_cart_hash_key', 'wc_cart_hash_' . md5( get_current_blog_id() . '_' . get_site_url( get_current_blog_id(), '/' ) . get_template() ) ), + 'fragment_name' => apply_filters( 'woocommerce_cart_fragment_name', 'wc_fragments_' . md5( get_current_blog_id() . '_' . get_site_url( get_current_blog_id(), '/' ) . get_template() ) ), + 'request_timeout' => 5000, + ); + break; + case 'wc-add-to-cart': + $params = array( + 'ajax_url' => WC()->ajax_url(), + 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), + 'i18n_view_cart' => esc_attr__( 'View cart', 'woocommerce' ), + 'cart_url' => apply_filters( 'woocommerce_add_to_cart_redirect', wc_get_cart_url(), null ), + 'is_cart' => is_cart(), + 'cart_redirect_after_add' => get_option( 'woocommerce_cart_redirect_after_add' ), + ); + break; + case 'wc-add-to-cart-variation': + // We also need the wp.template for this script :). + wc_get_template( 'single-product/add-to-cart/variation.php' ); + + $params = array( + 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), + 'i18n_no_matching_variations_text' => esc_attr__( 'Sorry, no products matched your selection. Please choose a different combination.', 'woocommerce' ), + 'i18n_make_a_selection_text' => esc_attr__( 'Please select some product options before adding this product to your cart.', 'woocommerce' ), + 'i18n_unavailable_text' => esc_attr__( 'Sorry, this product is unavailable. Please choose a different combination.', 'woocommerce' ), + ); + break; + case 'wc-country-select': + $params = array( + 'countries' => wp_json_encode( array_merge( WC()->countries->get_allowed_country_states(), WC()->countries->get_shipping_country_states() ) ), + 'i18n_select_state_text' => esc_attr__( 'Select an option…', 'woocommerce' ), + 'i18n_no_matches' => _x( 'No matches found', 'enhanced select', 'woocommerce' ), + 'i18n_ajax_error' => _x( 'Loading failed', 'enhanced select', 'woocommerce' ), + 'i18n_input_too_short_1' => _x( 'Please enter 1 or more characters', 'enhanced select', 'woocommerce' ), + 'i18n_input_too_short_n' => _x( 'Please enter %qty% or more characters', 'enhanced select', 'woocommerce' ), + 'i18n_input_too_long_1' => _x( 'Please delete 1 character', 'enhanced select', 'woocommerce' ), + 'i18n_input_too_long_n' => _x( 'Please delete %qty% characters', 'enhanced select', 'woocommerce' ), + 'i18n_selection_too_long_1' => _x( 'You can only select 1 item', 'enhanced select', 'woocommerce' ), + 'i18n_selection_too_long_n' => _x( 'You can only select %qty% items', 'enhanced select', 'woocommerce' ), + 'i18n_load_more' => _x( 'Loading more results…', 'enhanced select', 'woocommerce' ), + 'i18n_searching' => _x( 'Searching…', 'enhanced select', 'woocommerce' ), + ); + break; + case 'wc-password-strength-meter': + $params = array( + 'min_password_strength' => apply_filters( 'woocommerce_min_password_strength', 3 ), + 'stop_checkout' => apply_filters( 'woocommerce_enforce_password_strength_meter_on_checkout', false ), + 'i18n_password_error' => esc_attr__( 'Please enter a stronger password.', 'woocommerce' ), + 'i18n_password_hint' => esc_attr( wp_get_password_hint() ), + ); + break; + default: + $params = false; + } + + $params = apply_filters_deprecated( $handle . '_params', array( $params ), '3.0.0', 'woocommerce_get_script_data' ); + + return apply_filters( 'woocommerce_get_script_data', $params, $handle ); + } + + /** + * Localize scripts only when enqueued. + */ + public static function localize_printed_scripts() { + foreach ( self::$scripts as $handle ) { + self::localize_script( $handle ); + } + } +} + +WC_Frontend_Scripts::init(); diff --git a/plugins/woocommerce/includes/class-wc-geo-ip.php b/plugins/woocommerce/includes/class-wc-geo-ip.php new file mode 100644 index 00000000000..d856be2848a --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-geo-ip.php @@ -0,0 +1,1814 @@ +log( $level, $message, array( 'source' => 'geoip' ) ); + } + + /** + * Open geoip file. + * + * @param string $filename + * @param int $flags + */ + public function geoip_open( $filename, $flags ) { + $this->flags = $flags; + if ( $this->flags & self::GEOIP_SHARED_MEMORY ) { + $this->shmid = @shmop_open( self::GEOIP_SHM_KEY, 'a', 0, 0 ); + } else { + if ( $this->filehandle = fopen( $filename, 'rb' ) ) { + if ( $this->flags & self::GEOIP_MEMORY_CACHE ) { + $s_array = fstat( $this->filehandle ); + $this->memory_buffer = fread( $this->filehandle, $s_array['size'] ); + } + } else { + $this->log( 'GeoIP API: Can not open ' . $filename, 'error' ); + } + } + + $this->_setup_segments(); + } + + /** + * Setup segments. + * + * @return WC_Geo_IP instance + */ + private function _setup_segments() { + $this->databaseType = self::GEOIP_COUNTRY_EDITION; + $this->record_length = self::STANDARD_RECORD_LENGTH; + + if ( $this->flags & self::GEOIP_SHARED_MEMORY ) { + $offset = @shmop_size( $this->shmid ) - 3; + + for ( $i = 0; $i < self::STRUCTURE_INFO_MAX_SIZE; $i++ ) { + $delim = @shmop_read( $this->shmid, $offset, 3 ); + $offset += 3; + + if ( ( chr( 255 ) . chr( 255 ) . chr( 255 ) ) == $delim ) { + $this->databaseType = ord( @shmop_read( $this->shmid, $offset, 1 ) ); + + if ( $this->databaseType >= 106 ) { + $this->databaseType -= 105; + } + + $offset++; + + if ( self::GEOIP_REGION_EDITION_REV0 == $this->databaseType ) { + $this->databaseSegments = self::GEOIP_STATE_BEGIN_REV0; + } elseif ( self::GEOIP_REGION_EDITION_REV1 == $this->databaseType ) { + $this->databaseSegments = self::GEOIP_STATE_BEGIN_REV1; + } elseif ( ( self::GEOIP_CITY_EDITION_REV0 == $this->databaseType ) + || ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) + || ( self::GEOIP_ORG_EDITION == $this->databaseType ) + || ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_USERTYPE_EDITION == $this->databaseType ) + || ( self::GEOIP_USERTYPE_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_LOCATIONA_EDITION == $this->databaseType ) + || ( self::GEOIP_ACCURACYRADIUS_EDITION == $this->databaseType ) + || ( self::GEOIP_CITY_EDITION_REV0_V6 == $this->databaseType ) + || ( self::GEOIP_CITY_EDITION_REV1_V6 == $this->databaseType ) + || ( self::GEOIP_NETSPEED_EDITION_REV1 == $this->databaseType ) + || ( self::GEOIP_NETSPEED_EDITION_REV1_V6 == $this->databaseType ) + || ( self::GEOIP_ASNUM_EDITION == $this->databaseType ) + || ( self::GEOIP_ASNUM_EDITION_V6 == $this->databaseType ) + ) { + $this->databaseSegments = 0; + $buf = @shmop_read( $this->shmid, $offset, self::SEGMENT_RECORD_LENGTH ); + + for ( $j = 0; $j < self::SEGMENT_RECORD_LENGTH; $j++ ) { + $this->databaseSegments += ( ord( $buf[ $j ] ) << ( $j * 8 ) ); + } + + if ( ( self::GEOIP_ORG_EDITION == $this->databaseType ) + || ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType ) + ) { + $this->record_length = self::ORG_RECORD_LENGTH; + } + } + + break; + } else { + $offset -= 4; + } + } + if ( ( self::GEOIP_COUNTRY_EDITION == $this->databaseType ) + || ( self::GEOIP_COUNTRY_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_PROXY_EDITION == $this->databaseType ) + || ( self::GEOIP_NETSPEED_EDITION == $this->databaseType ) + ) { + $this->databaseSegments = self::GEOIP_COUNTRY_BEGIN; + } + } else { + $filepos = ftell( $this->filehandle ); + fseek( $this->filehandle, -3, SEEK_END ); + + for ( $i = 0; $i < self::STRUCTURE_INFO_MAX_SIZE; $i++ ) { + + $delim = fread( $this->filehandle, 3 ); + if ( ( chr( 255 ) . chr( 255 ) . chr( 255 ) ) == $delim ) { + + $this->databaseType = ord( fread( $this->filehandle, 1 ) ); + if ( $this->databaseType >= 106 ) { + $this->databaseType -= 105; + } + + if ( self::GEOIP_REGION_EDITION_REV0 == $this->databaseType ) { + $this->databaseSegments = self::GEOIP_STATE_BEGIN_REV0; + } elseif ( self::GEOIP_REGION_EDITION_REV1 == $this->databaseType ) { + $this->databaseSegments = self::GEOIP_STATE_BEGIN_REV1; + } elseif ( ( self::GEOIP_CITY_EDITION_REV0 == $this->databaseType ) + || ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) + || ( self::GEOIP_CITY_EDITION_REV0_V6 == $this->databaseType ) + || ( self::GEOIP_CITY_EDITION_REV1_V6 == $this->databaseType ) + || ( self::GEOIP_ORG_EDITION == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION == $this->databaseType ) + || ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_LOCATIONA_EDITION == $this->databaseType ) + || ( self::GEOIP_ACCURACYRADIUS_EDITION == $this->databaseType ) + || ( self::GEOIP_NETSPEED_EDITION_REV1 == $this->databaseType ) + || ( self::GEOIP_NETSPEED_EDITION_REV1_V6 == $this->databaseType ) + || ( self::GEOIP_USERTYPE_EDITION == $this->databaseType ) + || ( self::GEOIP_USERTYPE_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_ASNUM_EDITION == $this->databaseType ) + || ( self::GEOIP_ASNUM_EDITION_V6 == $this->databaseType ) + ) { + $this->databaseSegments = 0; + $buf = fread( $this->filehandle, self::SEGMENT_RECORD_LENGTH ); + + for ( $j = 0; $j < self::SEGMENT_RECORD_LENGTH; $j++ ) { + $this->databaseSegments += ( ord( $buf[ $j ] ) << ( $j * 8 ) ); + } + + if ( ( self::GEOIP_ORG_EDITION == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION == $this->databaseType ) + || ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType ) + ) { + $this->record_length = self::ORG_RECORD_LENGTH; + } + } + + break; + } else { + fseek( $this->filehandle, -4, SEEK_CUR ); + } + } + + if ( ( self::GEOIP_COUNTRY_EDITION == $this->databaseType ) + || ( self::GEOIP_COUNTRY_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_PROXY_EDITION == $this->databaseType ) + || ( self::GEOIP_NETSPEED_EDITION == $this->databaseType ) + ) { + $this->databaseSegments = self::GEOIP_COUNTRY_BEGIN; + } + + fseek( $this->filehandle, $filepos, SEEK_SET ); + } + + return $this; + } + + /** + * Close geoip file. + * + * @return bool + */ + public function geoip_close() { + if ( $this->flags & self::GEOIP_SHARED_MEMORY ) { + return true; + } + + return fclose( $this->filehandle ); + } + + /** + * Common get record. + * + * @param string $seek_country + * @return WC_Geo_IP_Record instance + */ + private function _common_get_record( $seek_country ) { + // workaround php's broken substr, strpos, etc handling with + // mbstring.func_overload and mbstring.internal_encoding + $mbExists = extension_loaded( 'mbstring' ); + if ( $mbExists ) { + $enc = mb_internal_encoding(); + mb_internal_encoding( 'ISO-8859-1' ); + } + + $record_pointer = $seek_country + ( 2 * $this->record_length - 1 ) * $this->databaseSegments; + + if ( $this->flags & self::GEOIP_MEMORY_CACHE ) { + $record_buf = substr( $this->memory_buffer, $record_pointer, FULL_RECORD_LENGTH ); + } elseif ( $this->flags & self::GEOIP_SHARED_MEMORY ) { + $record_buf = @shmop_read( $this->shmid, $record_pointer, FULL_RECORD_LENGTH ); + } else { + fseek( $this->filehandle, $record_pointer, SEEK_SET ); + $record_buf = fread( $this->filehandle, FULL_RECORD_LENGTH ); + } + + $record = new WC_Geo_IP_Record(); + $record_buf_pos = 0; + $char = ord( substr( $record_buf, $record_buf_pos, 1 ) ); + $record->country_code = $this->GEOIP_COUNTRY_CODES[ $char ]; + $record->country_code3 = $this->GEOIP_COUNTRY_CODES3[ $char ]; + $record->country_name = $this->GEOIP_COUNTRY_NAMES[ $char ]; + $record->continent_code = $this->GEOIP_CONTINENT_CODES[ $char ]; + $str_length = 0; + + $record_buf_pos++; + + // Get region + $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); + while ( 0 != $char ) { + $str_length++; + $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); + } + + if ( $str_length > 0 ) { + $record->region = substr( $record_buf, $record_buf_pos, $str_length ); + } + + $record_buf_pos += $str_length + 1; + $str_length = 0; + + // Get city + $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); + while ( 0 != $char ) { + $str_length++; + $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); + } + + if ( $str_length > 0 ) { + $record->city = substr( $record_buf, $record_buf_pos, $str_length ); + } + + $record_buf_pos += $str_length + 1; + $str_length = 0; + + // Get postal code + $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); + while ( 0 != $char ) { + $str_length++; + $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); + } + + if ( $str_length > 0 ) { + $record->postal_code = substr( $record_buf, $record_buf_pos, $str_length ); + } + + $record_buf_pos += $str_length + 1; + + // Get latitude and longitude + $latitude = 0; + $longitude = 0; + for ( $j = 0; $j < 3; ++$j ) { + $char = ord( substr( $record_buf, $record_buf_pos++, 1 ) ); + $latitude += ( $char << ( $j * 8 ) ); + } + + $record->latitude = ( $latitude / 10000 ) - 180; + + for ( $j = 0; $j < 3; ++$j ) { + $char = ord( substr( $record_buf, $record_buf_pos++, 1 ) ); + $longitude += ( $char << ( $j * 8 ) ); + } + + $record->longitude = ( $longitude / 10000 ) - 180; + + if ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) { + $metroarea_combo = 0; + if ( 'US' === $record->country_code ) { + for ( $j = 0; $j < 3; ++$j ) { + $char = ord( substr( $record_buf, $record_buf_pos++, 1 ) ); + $metroarea_combo += ( $char << ( $j * 8 ) ); + } + + $record->metro_code = $record->dma_code = floor( $metroarea_combo / 1000 ); + $record->area_code = $metroarea_combo % 1000; + } + } + + if ( $mbExists ) { + mb_internal_encoding( $enc ); + } + + return $record; + } + + /** + * Get record. + * + * @param int $ipnum + * @return WC_Geo_IP_Record instance + */ + private function _get_record( $ipnum ) { + $seek_country = $this->_geoip_seek_country( $ipnum ); + if ( $seek_country == $this->databaseSegments ) { + return null; + } + + return $this->_common_get_record( $seek_country ); + } + + /** + * Seek country IPv6. + * + * @param int $ipnum + * @return string + */ + public function _geoip_seek_country_v6( $ipnum ) { + // arrays from unpack start with offset 1 + // yet another php mystery. array_merge work around + // this broken behaviour + $v6vec = array_merge( unpack( 'C16', $ipnum ) ); + + $offset = 0; + for ( $depth = 127; $depth >= 0; --$depth ) { + if ( $this->flags & self::GEOIP_MEMORY_CACHE ) { + $buf = $this->_safe_substr( + $this->memory_buffer, + 2 * $this->record_length * $offset, + 2 * $this->record_length + ); + } elseif ( $this->flags & self::GEOIP_SHARED_MEMORY ) { + $buf = @shmop_read( + $this->shmid, + 2 * $this->record_length * $offset, + 2 * $this->record_length + ); + } else { + if ( 0 != fseek( $this->filehandle, 2 * $this->record_length * $offset, SEEK_SET ) ) { + break; + } + + $buf = fread( $this->filehandle, 2 * $this->record_length ); + } + $x = array( 0, 0 ); + for ( $i = 0; $i < 2; ++$i ) { + for ( $j = 0; $j < $this->record_length; ++$j ) { + $x[ $i ] += ord( $buf[ $this->record_length * $i + $j ] ) << ( $j * 8 ); + } + } + + $bnum = 127 - $depth; + $idx = $bnum >> 3; + $b_mask = 1 << ( $bnum & 7 ^ 7 ); + if ( ( $v6vec[ $idx ] & $b_mask ) > 0 ) { + if ( $x[1] >= $this->databaseSegments ) { + return $x[1]; + } + $offset = $x[1]; + } else { + if ( $x[0] >= $this->databaseSegments ) { + return $x[0]; + } + $offset = $x[0]; + } + } + + $this->log( 'GeoIP API: Error traversing database - perhaps it is corrupt?', 'error' ); + + return false; + } + + /** + * Seek country. + * + * @param int $ipnum + * @return string + */ + private function _geoip_seek_country( $ipnum ) { + $offset = 0; + for ( $depth = 31; $depth >= 0; --$depth ) { + if ( $this->flags & self::GEOIP_MEMORY_CACHE ) { + $buf = $this->_safe_substr( + $this->memory_buffer, + 2 * $this->record_length * $offset, + 2 * $this->record_length + ); + } elseif ( $this->flags & self::GEOIP_SHARED_MEMORY ) { + $buf = @shmop_read( + $this->shmid, + 2 * $this->record_length * $offset, + 2 * $this->record_length + ); + } else { + if ( 0 != fseek( $this->filehandle, 2 * $this->record_length * $offset, SEEK_SET ) ) { + break; + } + + $buf = fread( $this->filehandle, 2 * $this->record_length ); + } + + $x = array( 0, 0 ); + for ( $i = 0; $i < 2; ++$i ) { + for ( $j = 0; $j < $this->record_length; ++$j ) { + $x[ $i ] += ord( $buf[ $this->record_length * $i + $j ] ) << ( $j * 8 ); + } + } + if ( $ipnum & ( 1 << $depth ) ) { + if ( $x[1] >= $this->databaseSegments ) { + return $x[1]; + } + + $offset = $x[1]; + } else { + if ( $x[0] >= $this->databaseSegments ) { + return $x[0]; + } + + $offset = $x[0]; + } + } + + $this->log( 'GeoIP API: Error traversing database - perhaps it is corrupt?', 'error' ); + + return false; + } + + /** + * Record by addr. + * + * @param string $addr + * + * @return WC_Geo_IP_Record + */ + public function geoip_record_by_addr( $addr ) { + if ( null == $addr ) { + return 0; + } + + $ipnum = ip2long( $addr ); + return $this->_get_record( $ipnum ); + } + + /** + * Country ID by addr IPv6. + * + * @param string $addr + * @return int|bool + */ + public function geoip_country_id_by_addr_v6( $addr ) { + if ( ! defined( 'AF_INET6' ) ) { + $this->log( 'GEOIP (geoip_country_id_by_addr_v6): PHP was compiled with --disable-ipv6 option' ); + return false; + } + $ipnum = inet_pton( $addr ); + return $this->_geoip_seek_country_v6( $ipnum ) - self::GEOIP_COUNTRY_BEGIN; + } + + /** + * Country ID by addr. + * + * @param string $addr + * @return int + */ + public function geoip_country_id_by_addr( $addr ) { + $ipnum = ip2long( $addr ); + return $this->_geoip_seek_country( $ipnum ) - self::GEOIP_COUNTRY_BEGIN; + } + + /** + * Country code by addr IPv6. + * + * @param string $addr + * @return string + */ + public function geoip_country_code_by_addr_v6( $addr ) { + $country_id = $this->geoip_country_id_by_addr_v6( $addr ); + if ( false !== $country_id && isset( $this->GEOIP_COUNTRY_CODES[ $country_id ] ) ) { + return $this->GEOIP_COUNTRY_CODES[ $country_id ]; + } + + return false; + } + + /** + * Country code by addr. + * + * @param string $addr + * @return string + */ + public function geoip_country_code_by_addr( $addr ) { + if ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) { + $record = $this->geoip_record_by_addr( $addr ); + if ( false !== $record ) { + return $record->country_code; + } + } else { + $country_id = $this->geoip_country_id_by_addr( $addr ); + if ( false !== $country_id && isset( $this->GEOIP_COUNTRY_CODES[ $country_id ] ) ) { + return $this->GEOIP_COUNTRY_CODES[ $country_id ]; + } + } + + return false; + } + + /** + * Encode string. + * + * @param string $string + * @param int $start + * @param int $length + * @return string + */ + private function _safe_substr( $string, $start, $length ) { + // workaround php's broken substr, strpos, etc handling with + // mbstring.func_overload and mbstring.internal_encoding + $mb_exists = extension_loaded( 'mbstring' ); + + if ( $mb_exists ) { + $enc = mb_internal_encoding(); + mb_internal_encoding( 'ISO-8859-1' ); + } + + $buf = substr( $string, $start, $length ); + + if ( $mb_exists ) { + mb_internal_encoding( $enc ); + } + + return $buf; + } +} + +/** + * Geo IP Record class. + */ +class WC_Geo_IP_Record { + + /** + * Country code. + * + * @var string + */ + public $country_code; + + /** + * 3 letters country code. + * + * @var string + */ + public $country_code3; + + /** + * Country name. + * + * @var string + */ + public $country_name; + + /** + * Region. + * + * @var string + */ + public $region; + + /** + * City. + * + * @var string + */ + public $city; + + /** + * Postal code. + * + * @var string + */ + public $postal_code; + + /** + * Latitude + * + * @var int + */ + public $latitude; + + /** + * Longitude. + * + * @var int + */ + public $longitude; + + /** + * Area code. + * + * @var int + */ + public $area_code; + + /** + * DMA Code. + * + * Metro and DMA code are the same. + * Use metro code instead. + * + * @var float + */ + public $dma_code; + + /** + * Metro code. + * + * @var float + */ + public $metro_code; + + /** + * Continent code. + * + * @var string + */ + public $continent_code; +} diff --git a/includes/class-wc-geolite-integration.php b/plugins/woocommerce/includes/class-wc-geolite-integration.php similarity index 100% rename from includes/class-wc-geolite-integration.php rename to plugins/woocommerce/includes/class-wc-geolite-integration.php diff --git a/plugins/woocommerce/includes/class-wc-geolocation.php b/plugins/woocommerce/includes/class-wc-geolocation.php new file mode 100644 index 00000000000..2e02d855890 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-geolocation.php @@ -0,0 +1,355 @@ + 'http://api.ipify.org/', + 'ipecho' => 'http://ipecho.net/plain', + 'ident' => 'http://ident.me', + 'tnedi' => 'http://tnedi.me', + ); + + /** + * API endpoints for geolocating an IP address + * + * @var array + */ + private static $geoip_apis = array( + 'ipinfo.io' => 'https://ipinfo.io/%s/json', + 'ip-api.com' => 'http://ip-api.com/json/%s', + ); + + /** + * Check if geolocation is enabled. + * + * @since 3.4.0 + * @param string $current_settings Current geolocation settings. + * @return bool + */ + private static function is_geolocation_enabled( $current_settings ) { + return in_array( $current_settings, array( 'geolocation', 'geolocation_ajax' ), true ); + } + + /** + * Get current user IP Address. + * + * @return string + */ + public static function get_ip_address() { + if ( isset( $_SERVER['HTTP_X_REAL_IP'] ) ) { + return sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_REAL_IP'] ) ); + } elseif ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { + // Proxy servers can send through this header like this: X-Forwarded-For: client1, proxy1, proxy2 + // Make sure we always only send through the first IP in the list which should always be the client IP. + return (string) rest_is_ip_address( trim( current( preg_split( '/,/', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) ) ) ) ); + } elseif ( isset( $_SERVER['REMOTE_ADDR'] ) ) { + return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); + } + return ''; + } + + /** + * Get user IP Address using an external service. + * This can be used as a fallback for users on localhost where + * get_ip_address() will be a local IP and non-geolocatable. + * + * @return string + */ + public static function get_external_ip_address() { + $external_ip_address = '0.0.0.0'; + + if ( '' !== self::get_ip_address() ) { + $transient_name = 'external_ip_address_' . self::get_ip_address(); + $external_ip_address = get_transient( $transient_name ); + } + + if ( false === $external_ip_address ) { + $external_ip_address = '0.0.0.0'; + $ip_lookup_services = apply_filters( 'woocommerce_geolocation_ip_lookup_apis', self::$ip_lookup_apis ); + $ip_lookup_services_keys = array_keys( $ip_lookup_services ); + shuffle( $ip_lookup_services_keys ); + + foreach ( $ip_lookup_services_keys as $service_name ) { + $service_endpoint = $ip_lookup_services[ $service_name ]; + $response = wp_safe_remote_get( $service_endpoint, array( 'timeout' => 2 ) ); + + if ( ! is_wp_error( $response ) && rest_is_ip_address( $response['body'] ) ) { + $external_ip_address = apply_filters( 'woocommerce_geolocation_ip_lookup_api_response', wc_clean( $response['body'] ), $service_name ); + break; + } + } + + set_transient( $transient_name, $external_ip_address, DAY_IN_SECONDS ); + } + + return $external_ip_address; + } + + /** + * Geolocate an IP address. + * + * @param string $ip_address IP Address. + * @param bool $fallback If true, fallbacks to alternative IP detection (can be slower). + * @param bool $api_fallback If true, uses geolocation APIs if the database file doesn't exist (can be slower). + * @return array + */ + public static function geolocate_ip( $ip_address = '', $fallback = false, $api_fallback = true ) { + // Filter to allow custom geolocation of the IP address. + $country_code = apply_filters( 'woocommerce_geolocate_ip', false, $ip_address, $fallback, $api_fallback ); + + if ( false !== $country_code ) { + return array( + 'country' => $country_code, + 'state' => '', + 'city' => '', + 'postcode' => '', + ); + } + + if ( empty( $ip_address ) ) { + $ip_address = self::get_ip_address(); + $country_code = self::get_country_code_from_headers(); + } + + /** + * Get geolocation filter. + * + * @since 3.9.0 + * @param array $geolocation Geolocation data, including country, state, city, and postcode. + * @param string $ip_address IP Address. + */ + $geolocation = apply_filters( + 'woocommerce_get_geolocation', + array( + 'country' => $country_code, + 'state' => '', + 'city' => '', + 'postcode' => '', + ), + $ip_address + ); + + // If we still haven't found a country code, let's consider doing an API lookup. + if ( '' === $geolocation['country'] && $api_fallback ) { + $geolocation['country'] = self::geolocate_via_api( $ip_address ); + } + + // It's possible that we're in a local environment, in which case the geolocation needs to be done from the + // external address. + if ( '' === $geolocation['country'] && $fallback ) { + $external_ip_address = self::get_external_ip_address(); + + // Only bother with this if the external IP differs. + if ( '0.0.0.0' !== $external_ip_address && $external_ip_address !== $ip_address ) { + return self::geolocate_ip( $external_ip_address, false, $api_fallback ); + } + } + + return array( + 'country' => $geolocation['country'], + 'state' => $geolocation['state'], + 'city' => $geolocation['city'], + 'postcode' => $geolocation['postcode'], + ); + } + + /** + * Path to our local db. + * + * @deprecated 3.9.0 + * @param string $deprecated Deprecated since 3.4.0. + * @return string + */ + public static function get_local_database_path( $deprecated = '2' ) { + wc_deprecated_function( 'WC_Geolocation::get_local_database_path', '3.9.0' ); + $integration = wc()->integrations->get_integration( 'maxmind_geolocation' ); + return $integration->get_database_service()->get_database_path(); + } + + /** + * Update geoip database. + * + * @deprecated 3.9.0 + * Extract files with PharData. Tool built into PHP since 5.3. + */ + public static function update_database() { + wc_deprecated_function( 'WC_Geolocation::update_database', '3.9.0' ); + $integration = wc()->integrations->get_integration( 'maxmind_geolocation' ); + $integration->update_database(); + } + + /** + * Fetches the country code from the request headers, if one is available. + * + * @since 3.9.0 + * @return string The country code pulled from the headers, or empty string if one was not found. + */ + private static function get_country_code_from_headers() { + $country_code = ''; + + $headers = array( + 'MM_COUNTRY_CODE', + 'GEOIP_COUNTRY_CODE', + 'HTTP_CF_IPCOUNTRY', + 'HTTP_X_COUNTRY_CODE', + ); + + foreach ( $headers as $header ) { + if ( empty( $_SERVER[ $header ] ) ) { + continue; + } + + $country_code = strtoupper( sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) ) ); + break; + } + + return $country_code; + } + + /** + * Use APIs to Geolocate the user. + * + * Geolocation APIs can be added through the use of the woocommerce_geolocation_geoip_apis filter. + * Provide a name=>value pair for service-slug=>endpoint. + * + * If APIs are defined, one will be chosen at random to fulfil the request. After completing, the result + * will be cached in a transient. + * + * @param string $ip_address IP address. + * @return string + */ + private static function geolocate_via_api( $ip_address ) { + $country_code = get_transient( 'geoip_' . $ip_address ); + + if ( false === $country_code ) { + $geoip_services = apply_filters( 'woocommerce_geolocation_geoip_apis', self::$geoip_apis ); + + if ( empty( $geoip_services ) ) { + return ''; + } + + $geoip_services_keys = array_keys( $geoip_services ); + + shuffle( $geoip_services_keys ); + + foreach ( $geoip_services_keys as $service_name ) { + $service_endpoint = $geoip_services[ $service_name ]; + $response = wp_safe_remote_get( sprintf( $service_endpoint, $ip_address ), array( 'timeout' => 2 ) ); + + if ( ! is_wp_error( $response ) && $response['body'] ) { + switch ( $service_name ) { + case 'ipinfo.io': + $data = json_decode( $response['body'] ); + $country_code = isset( $data->country ) ? $data->country : ''; + break; + case 'ip-api.com': + $data = json_decode( $response['body'] ); + $country_code = isset( $data->countryCode ) ? $data->countryCode : ''; // @codingStandardsIgnoreLine + break; + default: + $country_code = apply_filters( 'woocommerce_geolocation_geoip_response_' . $service_name, '', $response['body'] ); + break; + } + + $country_code = sanitize_text_field( strtoupper( $country_code ) ); + + if ( $country_code ) { + break; + } + } + } + + set_transient( 'geoip_' . $ip_address, $country_code, DAY_IN_SECONDS ); + } + + return $country_code; + } + + /** + * Hook in geolocation functionality. + * + * @deprecated 3.9.0 + * @return null + */ + public static function init() { + wc_deprecated_function( 'WC_Geolocation::init', '3.9.0' ); + return null; + } + + /** + * Prevent geolocation via MaxMind when using legacy versions of php. + * + * @deprecated 3.9.0 + * @since 3.4.0 + * @param string $default_customer_address current value. + * @return string + */ + public static function disable_geolocation_on_legacy_php( $default_customer_address ) { + wc_deprecated_function( 'WC_Geolocation::disable_geolocation_on_legacy_php', '3.9.0' ); + + if ( self::is_geolocation_enabled( $default_customer_address ) ) { + $default_customer_address = 'base'; + } + + return $default_customer_address; + } + + /** + * Maybe trigger a DB update for the first time. + * + * @deprecated 3.9.0 + * @param string $new_value New value. + * @param string $old_value Old value. + * @return string + */ + public static function maybe_update_database( $new_value, $old_value ) { + wc_deprecated_function( 'WC_Geolocation::maybe_update_database', '3.9.0' ); + if ( $new_value !== $old_value && self::is_geolocation_enabled( $new_value ) ) { + self::update_database(); + } + + return $new_value; + } +} diff --git a/plugins/woocommerce/includes/class-wc-https.php b/plugins/woocommerce/includes/class-wc-https.php new file mode 100644 index 00000000000..045d594dc2b --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-https.php @@ -0,0 +1,138 @@ + array( + 'wc_update_200_file_paths', + 'wc_update_200_permalinks', + 'wc_update_200_subcat_display', + 'wc_update_200_taxrates', + 'wc_update_200_line_items', + 'wc_update_200_images', + 'wc_update_200_db_version', + ), + '2.0.9' => array( + 'wc_update_209_brazillian_state', + 'wc_update_209_db_version', + ), + '2.1.0' => array( + 'wc_update_210_remove_pages', + 'wc_update_210_file_paths', + 'wc_update_210_db_version', + ), + '2.2.0' => array( + 'wc_update_220_shipping', + 'wc_update_220_order_status', + 'wc_update_220_variations', + 'wc_update_220_attributes', + 'wc_update_220_db_version', + ), + '2.3.0' => array( + 'wc_update_230_options', + 'wc_update_230_db_version', + ), + '2.4.0' => array( + 'wc_update_240_options', + 'wc_update_240_shipping_methods', + 'wc_update_240_api_keys', + 'wc_update_240_refunds', + 'wc_update_240_db_version', + ), + '2.4.1' => array( + 'wc_update_241_variations', + 'wc_update_241_db_version', + ), + '2.5.0' => array( + 'wc_update_250_currency', + 'wc_update_250_db_version', + ), + '2.6.0' => array( + 'wc_update_260_options', + 'wc_update_260_termmeta', + 'wc_update_260_zones', + 'wc_update_260_zone_methods', + 'wc_update_260_refunds', + 'wc_update_260_db_version', + ), + '3.0.0' => array( + 'wc_update_300_grouped_products', + 'wc_update_300_settings', + 'wc_update_300_product_visibility', + 'wc_update_300_db_version', + ), + '3.1.0' => array( + 'wc_update_310_downloadable_products', + 'wc_update_310_old_comments', + 'wc_update_310_db_version', + ), + '3.1.2' => array( + 'wc_update_312_shop_manager_capabilities', + 'wc_update_312_db_version', + ), + '3.2.0' => array( + 'wc_update_320_mexican_states', + 'wc_update_320_db_version', + ), + '3.3.0' => array( + 'wc_update_330_image_options', + 'wc_update_330_webhooks', + 'wc_update_330_product_stock_status', + 'wc_update_330_set_default_product_cat', + 'wc_update_330_clear_transients', + 'wc_update_330_set_paypal_sandbox_credentials', + 'wc_update_330_db_version', + ), + '3.4.0' => array( + 'wc_update_340_states', + 'wc_update_340_state', + 'wc_update_340_last_active', + 'wc_update_340_db_version', + ), + '3.4.3' => array( + 'wc_update_343_cleanup_foreign_keys', + 'wc_update_343_db_version', + ), + '3.4.4' => array( + 'wc_update_344_recreate_roles', + 'wc_update_344_db_version', + ), + '3.5.0' => array( + 'wc_update_350_reviews_comment_type', + 'wc_update_350_db_version', + ), + '3.5.2' => array( + 'wc_update_352_drop_download_log_fk', + ), + '3.5.4' => array( + 'wc_update_354_modify_shop_manager_caps', + 'wc_update_354_db_version', + ), + '3.6.0' => array( + 'wc_update_360_product_lookup_tables', + 'wc_update_360_term_meta', + 'wc_update_360_downloadable_product_permissions_index', + 'wc_update_360_db_version', + ), + '3.7.0' => array( + 'wc_update_370_tax_rate_classes', + 'wc_update_370_mro_std_currency', + 'wc_update_370_db_version', + ), + '3.9.0' => array( + 'wc_update_390_move_maxmind_database', + 'wc_update_390_change_geolocation_database_update_cron', + 'wc_update_390_db_version', + ), + '4.0.0' => array( + 'wc_update_product_lookup_tables', + 'wc_update_400_increase_size_of_column', + 'wc_update_400_reset_action_scheduler_migration_status', + 'wc_admin_update_0201_order_status_index', + 'wc_admin_update_0230_rename_gross_total', + 'wc_admin_update_0251_remove_unsnooze_action', + 'wc_update_400_db_version', + ), + '4.4.0' => array( + 'wc_update_440_insert_attribute_terms_for_variable_products', + 'wc_admin_update_110_remove_facebook_note', + 'wc_admin_update_130_remove_dismiss_action_from_tracking_opt_in_note', + 'wc_update_440_db_version', + ), + '4.5.0' => array( + 'wc_update_450_sanitize_coupons_code', + 'wc_update_450_db_version', + ), + '5.0.0' => array( + 'wc_update_500_fix_product_review_count', + 'wc_admin_update_160_remove_facebook_note', + 'wc_admin_update_170_homescreen_layout', + 'wc_update_500_db_version', + ), + '5.6.0' => array( + 'wc_update_560_create_refund_returns_page', + 'wc_update_560_db_version', + ), + '6.0.0' => array( + 'wc_update_600_migrate_rate_limit_options', + 'wc_admin_update_270_delete_report_downloads', + 'wc_admin_update_271_update_task_list_options', + 'wc_admin_update_280_order_status', + 'wc_admin_update_290_update_apperance_task_option', + 'wc_admin_update_290_delete_default_homepage_layout_option', + 'wc_update_600_db_version', + ), + '6.3.0' => array( + 'wc_update_630_create_product_attributes_lookup_table', + 'wc_admin_update_300_update_is_read_from_last_read', + 'wc_update_630_db_version', + ), + '6.4.0' => array( + 'wc_update_640_add_primary_key_to_product_attributes_lookup_table', + 'wc_update_640_approved_download_directories', + 'wc_admin_update_340_remove_is_primary_from_note_action', + 'wc_update_640_db_version', + ), + ); + + /** + * Hook in tabs. + */ + public static function init() { + add_action( 'init', array( __CLASS__, 'check_version' ), 5 ); + add_action( 'init', array( __CLASS__, 'manual_database_update' ), 20 ); + add_action( 'admin_init', array( __CLASS__, 'wc_admin_db_update_notice' ) ); + add_action( 'admin_init', array( __CLASS__, 'add_admin_note_after_page_created' ) ); + add_action( 'woocommerce_run_update_callback', array( __CLASS__, 'run_update_callback' ) ); + add_action( 'woocommerce_update_db_to_current_version', array( __CLASS__, 'update_db_version' ) ); + add_action( 'admin_init', array( __CLASS__, 'install_actions' ) ); + add_action( 'woocommerce_page_created', array( __CLASS__, 'page_created' ), 10, 2 ); + add_filter( 'plugin_action_links_' . WC_PLUGIN_BASENAME, array( __CLASS__, 'plugin_action_links' ) ); + add_filter( 'plugin_row_meta', array( __CLASS__, 'plugin_row_meta' ), 10, 2 ); + add_filter( 'wpmu_drop_tables', array( __CLASS__, 'wpmu_drop_tables' ) ); + add_filter( 'cron_schedules', array( __CLASS__, 'cron_schedules' ) ); + } + + /** + * Check WooCommerce version and run the updater is required. + * + * This check is done on all requests and runs if the versions do not match. + */ + public static function check_version() { + $wc_version = get_option( 'woocommerce_version' ); + $wc_code_version = WC()->version; + $requires_update = version_compare( $wc_version, $wc_code_version, '<' ); + if ( ! Constants::is_defined( 'IFRAME_REQUEST' ) && $requires_update ) { + self::install(); + do_action( 'woocommerce_updated' ); + do_action_deprecated( 'woocommerce_admin_updated', array(), $wc_code_version, 'woocommerce_updated' ); + // If there is no woocommerce_version option, consider it as a new install. + if ( ! $wc_version ) { + do_action( 'woocommerce_newly_installed' ); + do_action_deprecated( 'woocommerce_admin_newly_installed', array(), $wc_code_version, 'woocommerce_newly_installed' ); + } + } + } + + /** + * Performan manual database update when triggered by WooCommerce System Tools. + * + * @since 3.6.5 + */ + public static function manual_database_update() { + $blog_id = get_current_blog_id(); + + add_action( 'wp_' . $blog_id . '_wc_updater_cron', array( __CLASS__, 'run_manual_database_update' ) ); + } + + /** + * Add WC Admin based db update notice. + * + * @since 4.0.0 + */ + public static function wc_admin_db_update_notice() { + if ( + WC()->is_wc_admin_active() && + false !== get_option( 'woocommerce_admin_install_timestamp' ) + ) { + new WC_Notes_Run_Db_Update(); + } + } + + /** + * Run manual database update. + */ + public static function run_manual_database_update() { + self::update(); + } + + /** + * Run an update callback when triggered by ActionScheduler. + * + * @param string $update_callback Callback name. + * + * @since 3.6.0 + */ + public static function run_update_callback( $update_callback ) { + include_once dirname( __FILE__ ) . '/wc-update-functions.php'; + + if ( is_callable( $update_callback ) ) { + self::run_update_callback_start( $update_callback ); + $result = (bool) call_user_func( $update_callback ); + self::run_update_callback_end( $update_callback, $result ); + } + } + + /** + * Triggered when a callback will run. + * + * @since 3.6.0 + * @param string $callback Callback name. + */ + protected static function run_update_callback_start( $callback ) { + wc_maybe_define_constant( 'WC_UPDATING', true ); + } + + /** + * Triggered when a callback has ran. + * + * @since 3.6.0 + * @param string $callback Callback name. + * @param bool $result Return value from callback. Non-false need to run again. + */ + protected static function run_update_callback_end( $callback, $result ) { + if ( $result ) { + WC()->queue()->add( + 'woocommerce_run_update_callback', + array( + 'update_callback' => $callback, + ), + 'woocommerce-db-updates' + ); + } + } + + /** + * Install actions when a update button is clicked within the admin area. + * + * This function is hooked into admin_init to affect admin only. + */ + public static function install_actions() { + if ( ! empty( $_GET['do_update_woocommerce'] ) ) { // WPCS: input var ok. + check_admin_referer( 'wc_db_update', 'wc_db_update_nonce' ); + self::update(); + WC_Admin_Notices::add_notice( 'update', true ); + } + } + + /** + * Install WC. + */ + public static function install() { + if ( ! is_blog_installed() ) { + return; + } + + // Check if we are not already running this routine. + if ( self::is_installing() ) { + return; + } + + // If we made it till here nothing is running yet, lets set the transient now. + set_transient( 'wc_installing', 'yes', MINUTE_IN_SECONDS * 10 ); + wc_maybe_define_constant( 'WC_INSTALLING', true ); + + WC()->wpdb_table_fix(); + self::remove_admin_notices(); + self::create_tables(); + self::verify_base_tables(); + self::create_options(); + self::migrate_options(); + self::create_roles(); + self::setup_environment(); + self::create_terms(); + self::create_cron_jobs(); + self::delete_obsolete_notes(); + self::create_files(); + self::maybe_create_pages(); + self::maybe_set_activation_transients(); + self::set_paypal_standard_load_eligibility(); + self::update_wc_version(); + self::maybe_update_db_version(); + + delete_transient( 'wc_installing' ); + + // Use add_option() here to avoid overwriting this value with each + // plugin version update. We base plugin age off of this value. + add_option( 'woocommerce_admin_install_timestamp', time() ); + + do_action( 'woocommerce_flush_rewrite_rules' ); + do_action( 'woocommerce_installed' ); + do_action( 'woocommerce_admin_installed' ); + } + + /** + * Returns true if we're installing. + * + * @return bool + */ + private static function is_installing() { + return 'yes' === get_transient( 'wc_installing' ); + } + + /** + * Check if all the base tables are present. + * + * @param bool $modify_notice Whether to modify notice based on if all tables are present. + * @param bool $execute Whether to execute get_schema queries as well. + * + * @return array List of queries. + */ + public static function verify_base_tables( $modify_notice = true, $execute = false ) { + if ( $execute ) { + self::create_tables(); + } + + $missing_tables = wc_get_container() + ->get( DatabaseUtil::class ) + ->get_missing_tables( self::get_schema() ); + + if ( 0 < count( $missing_tables ) ) { + if ( $modify_notice ) { + WC_Admin_Notices::add_notice( 'base_tables_missing' ); + } + update_option( 'woocommerce_schema_missing_tables', $missing_tables ); + } else { + if ( $modify_notice ) { + WC_Admin_Notices::remove_notice( 'base_tables_missing' ); + } + update_option( 'woocommerce_schema_version', WC()->db_version ); + delete_option( 'woocommerce_schema_missing_tables' ); + } + return $missing_tables; + } + + /** + * Reset any notices added to admin. + * + * @since 3.2.0 + */ + private static function remove_admin_notices() { + include_once dirname( __FILE__ ) . '/admin/class-wc-admin-notices.php'; + WC_Admin_Notices::remove_all_notices(); + } + + /** + * Setup WC environment - post types, taxonomies, endpoints. + * + * @since 3.2.0 + */ + private static function setup_environment() { + WC_Post_types::register_post_types(); + WC_Post_types::register_taxonomies(); + WC()->query->init_query_vars(); + WC()->query->add_endpoints(); + WC_API::add_endpoint(); + WC_Auth::add_endpoint(); + } + + /** + * Is this a brand new WC install? + * + * A brand new install has no version yet. Also treat empty installs as 'new'. + * + * @since 3.2.0 + * @return boolean + */ + public static function is_new_install() { + $product_count = array_sum( (array) wp_count_posts( 'product' ) ); + + return is_null( get_option( 'woocommerce_version', null ) ) || ( 0 === $product_count && -1 === wc_get_page_id( 'shop' ) ); + } + + /** + * Is a DB update needed? + * + * @since 3.2.0 + * @return boolean + */ + public static function needs_db_update() { + $current_db_version = get_option( 'woocommerce_db_version', null ); + $updates = self::get_db_update_callbacks(); + $update_versions = array_keys( $updates ); + usort( $update_versions, 'version_compare' ); + + return ! is_null( $current_db_version ) && version_compare( $current_db_version, end( $update_versions ), '<' ); + } + + /** + * See if we need to set redirect transients for activation or not. + * + * @since 4.6.0 + */ + private static function maybe_set_activation_transients() { + if ( self::is_new_install() ) { + set_transient( '_wc_activation_redirect', 1, 30 ); + } + } + + /** + * See if we need to show or run database updates during install. + * + * @since 3.2.0 + */ + private static function maybe_update_db_version() { + if ( self::needs_db_update() ) { + if ( apply_filters( 'woocommerce_enable_auto_update_db', false ) ) { + self::update(); + } else { + WC_Admin_Notices::add_notice( 'update', true ); + } + } else { + self::update_db_version(); + } + } + + /** + * Update WC version to current. + */ + private static function update_wc_version() { + update_option( 'woocommerce_version', WC()->version ); + } + + /** + * Get list of DB update callbacks. + * + * @since 3.0.0 + * @return array + */ + public static function get_db_update_callbacks() { + return self::$db_updates; + } + + /** + * Push all needed DB updates to the queue for processing. + */ + private static function update() { + $current_db_version = get_option( 'woocommerce_db_version' ); + $loop = 0; + + foreach ( self::get_db_update_callbacks() as $version => $update_callbacks ) { + if ( version_compare( $current_db_version, $version, '<' ) ) { + foreach ( $update_callbacks as $update_callback ) { + WC()->queue()->schedule_single( + time() + $loop, + 'woocommerce_run_update_callback', + array( + 'update_callback' => $update_callback, + ), + 'woocommerce-db-updates' + ); + $loop++; + } + } + } + + // After the callbacks finish, update the db version to the current WC version. + $current_wc_version = WC()->version; + if ( version_compare( $current_db_version, $current_wc_version, '<' ) && + ! WC()->queue()->get_next( 'woocommerce_update_db_to_current_version' ) ) { + WC()->queue()->schedule_single( + time() + $loop, + 'woocommerce_update_db_to_current_version', + array( + 'version' => $current_wc_version, + ), + 'woocommerce-db-updates' + ); + } + } + + /** + * Update DB version to current. + * + * @param string|null $version New WooCommerce DB version or null. + */ + public static function update_db_version( $version = null ) { + update_option( 'woocommerce_db_version', is_null( $version ) ? WC()->version : $version ); + } + + /** + * Add more cron schedules. + * + * @param array $schedules List of WP scheduled cron jobs. + * + * @return array + */ + public static function cron_schedules( $schedules ) { + $schedules['monthly'] = array( + 'interval' => 2635200, + 'display' => __( 'Monthly', 'woocommerce' ), + ); + $schedules['fifteendays'] = array( + 'interval' => 1296000, + 'display' => __( 'Every 15 Days', 'woocommerce' ), + ); + return $schedules; + } + + /** + * Create cron jobs (clear them first). + */ + private static function create_cron_jobs() { + wp_clear_scheduled_hook( 'woocommerce_scheduled_sales' ); + wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' ); + wp_clear_scheduled_hook( 'woocommerce_cleanup_sessions' ); + wp_clear_scheduled_hook( 'woocommerce_cleanup_personal_data' ); + wp_clear_scheduled_hook( 'woocommerce_cleanup_logs' ); + wp_clear_scheduled_hook( 'woocommerce_geoip_updater' ); + wp_clear_scheduled_hook( 'woocommerce_tracker_send_event' ); + wp_clear_scheduled_hook( 'woocommerce_cleanup_rate_limits' ); + + $ve = get_option( 'gmt_offset' ) > 0 ? '-' : '+'; + + wp_schedule_event( strtotime( '00:00 tomorrow ' . $ve . absint( get_option( 'gmt_offset' ) ) . ' HOURS' ), 'daily', 'woocommerce_scheduled_sales' ); + + $held_duration = get_option( 'woocommerce_hold_stock_minutes', '60' ); + + if ( '' !== $held_duration ) { + $cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $held_duration ) ); + wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' ); + } + + // Delay the first run of `woocommerce_cleanup_personal_data` by 10 seconds + // so it doesn't occur in the same request. WooCommerce Admin also schedules + // a daily cron that gets lost due to a race condition. WC_Privacy's background + // processing instance updates the cron schedule from within a cron job. + wp_schedule_event( time() + 10, 'daily', 'woocommerce_cleanup_personal_data' ); + wp_schedule_event( time() + ( 3 * HOUR_IN_SECONDS ), 'daily', 'woocommerce_cleanup_logs' ); + wp_schedule_event( time() + ( 6 * HOUR_IN_SECONDS ), 'twicedaily', 'woocommerce_cleanup_sessions' ); + wp_schedule_event( time() + MINUTE_IN_SECONDS, 'fifteendays', 'woocommerce_geoip_updater' ); + wp_schedule_event( time() + 10, apply_filters( 'woocommerce_tracker_event_recurrence', 'daily' ), 'woocommerce_tracker_send_event' ); + wp_schedule_event( time() + ( 3 * HOUR_IN_SECONDS ), 'daily', 'woocommerce_cleanup_rate_limits' ); + + if ( ! wp_next_scheduled( 'wc_admin_daily' ) ) { + wp_schedule_event( time(), 'daily', 'wc_admin_daily' ); + } + // Note: this is potentially redundant when the core package exists. + wp_schedule_single_event( time() + 10, 'generate_category_lookup_table' ); + } + + /** + * Create pages on installation. + */ + public static function maybe_create_pages() { + if ( empty( get_option( 'woocommerce_db_version' ) ) ) { + self::create_pages(); + } + } + + /** + * Create pages that the plugin relies on, storing page IDs in variables. + */ + public static function create_pages() { + include_once dirname( __FILE__ ) . '/admin/wc-admin-functions.php'; + + $pages = apply_filters( + 'woocommerce_create_pages', + array( + 'shop' => array( + 'name' => _x( 'shop', 'Page slug', 'woocommerce' ), + 'title' => _x( 'Shop', 'Page title', 'woocommerce' ), + 'content' => '', + ), + 'cart' => array( + 'name' => _x( 'cart', 'Page slug', 'woocommerce' ), + 'title' => _x( 'Cart', 'Page title', 'woocommerce' ), + 'content' => '[' . apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) . ']', + ), + 'checkout' => array( + 'name' => _x( 'checkout', 'Page slug', 'woocommerce' ), + 'title' => _x( 'Checkout', 'Page title', 'woocommerce' ), + 'content' => '[' . apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) . ']', + ), + 'myaccount' => array( + 'name' => _x( 'my-account', 'Page slug', 'woocommerce' ), + 'title' => _x( 'My account', 'Page title', 'woocommerce' ), + 'content' => '[' . apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) . ']', + ), + 'refund_returns' => array( + 'name' => _x( 'refund_returns', 'Page slug', 'woocommerce' ), + 'title' => _x( 'Refund and Returns Policy', 'Page title', 'woocommerce' ), + 'content' => self::get_refunds_return_policy_page_content(), + 'post_status' => 'draft', + ), + ) + ); + + foreach ( $pages as $key => $page ) { + wc_create_page( + esc_sql( $page['name'] ), + 'woocommerce_' . $key . '_page_id', + $page['title'], + $page['content'], + ! empty( $page['parent'] ) ? wc_get_page_id( $page['parent'] ) : '', + ! empty( $page['post_status'] ) ? $page['post_status'] : 'publish' + ); + } + } + + /** + * Default options. + * + * Sets up the default options used on the settings page. + */ + private static function create_options() { + // Include settings so that we can run through defaults. + include_once dirname( __FILE__ ) . '/admin/class-wc-admin-settings.php'; + + $settings = WC_Admin_Settings::get_settings_pages(); + + foreach ( $settings as $section ) { + if ( ! method_exists( $section, 'get_settings' ) ) { + continue; + } + $subsections = array_unique( array_merge( array( '' ), array_keys( $section->get_sections() ) ) ); + + /** + * We are using 'WC_Settings_Page::get_settings' on purpose even thought it's deprecated. + * See the method documentation for an explanation. + */ + + foreach ( $subsections as $subsection ) { + foreach ( $section->get_settings( $subsection ) as $value ) { + if ( isset( $value['default'] ) && isset( $value['id'] ) ) { + $autoload = isset( $value['autoload'] ) ? (bool) $value['autoload'] : true; + add_option( $value['id'], $value['default'], '', ( $autoload ? 'yes' : 'no' ) ); + } + } + } + } + + // Define other defaults if not in setting screens. + add_option( 'woocommerce_single_image_width', '600', '', 'yes' ); + add_option( 'woocommerce_thumbnail_image_width', '300', '', 'yes' ); + add_option( 'woocommerce_checkout_highlight_required_fields', 'yes', '', 'yes' ); + add_option( 'woocommerce_demo_store', 'no', '', 'no' ); + + if ( self::is_new_install() ) { + // Define initial tax classes. + WC_Tax::create_tax_class( __( 'Reduced rate', 'woocommerce' ) ); + WC_Tax::create_tax_class( __( 'Zero rate', 'woocommerce' ) ); + + // For new installs, setup and enable Approved Product Download Directories. + wc_get_container()->get( Download_Directories_Sync::class )->init_feature( false, true ); + } + } + + /** + * Delete obsolete notes. + */ + public static function delete_obsolete_notes() { + global $wpdb; + $obsolete_notes_names = array( + 'wc-admin-welcome-note', + 'wc-admin-store-notice-setting-moved', + 'wc-admin-store-notice-giving-feedback', + 'wc-admin-learn-more-about-product-settings', + 'wc-admin-onboarding-profiler-reminder', + 'wc-admin-historical-data', + 'wc-admin-review-shipping-settings', + 'wc-admin-home-screen-feedback', + 'wc-admin-effortless-payments-by-mollie', + 'wc-admin-google-ads-and-marketing', + 'wc-admin-marketing-intro', + 'wc-admin-draw-attention', + 'wc-admin-need-some-inspiration', + 'wc-admin-choose-niche', + 'wc-admin-start-dropshipping-business', + 'wc-admin-filter-by-product-variations-in-reports', + 'wc-admin-learn-more-about-variable-products', + 'wc-admin-getting-started-ecommerce-webinar', + 'wc-admin-navigation-feedback', + 'wc-admin-navigation-feedback-follow-up', + ); + + $additional_obsolete_notes_names = apply_filters( + 'woocommerce_admin_obsolete_notes_names', + array() + ); + + if ( is_array( $additional_obsolete_notes_names ) ) { + $obsolete_notes_names = array_merge( + $obsolete_notes_names, + $additional_obsolete_notes_names + ); + } + + foreach ( $obsolete_notes_names as $obsolete_notes_name ) { + $wpdb->delete( $wpdb->prefix . 'wc_admin_notes', array( 'name' => $obsolete_notes_name ) ); + $wpdb->delete( $wpdb->prefix . 'wc_admin_note_actions', array( 'name' => $obsolete_notes_name ) ); + } + } + + /** + * Migrate option values to their new keys/names. + */ + public static function migrate_options() { + + $migrated_options = array( + 'woocommerce_onboarding_profile' => 'wc_onboarding_profile', + 'woocommerce_admin_install_timestamp' => 'wc_admin_install_timestamp', + 'woocommerce_onboarding_opt_in' => 'wc_onboarding_opt_in', + 'woocommerce_admin_import_stats' => 'wc_admin_import_stats', + 'woocommerce_admin_version' => 'wc_admin_version', + 'woocommerce_admin_last_orders_milestone' => 'wc_admin_last_orders_milestone', + 'woocommerce_admin-wc-helper-last-refresh' => 'wc-admin-wc-helper-last-refresh', + 'woocommerce_admin_report_export_status' => 'wc_admin_report_export_status', + 'woocommerce_task_list_complete' => 'woocommerce_task_list_complete', + 'woocommerce_task_list_hidden' => 'woocommerce_task_list_hidden', + 'woocommerce_extended_task_list_complete' => 'woocommerce_extended_task_list_complete', + 'woocommerce_extended_task_list_hidden' => 'woocommerce_extended_task_list_hidden', + ); + + wc_maybe_define_constant( 'WC_ADMIN_MIGRATING_OPTIONS', true ); + + foreach ( $migrated_options as $new_option => $old_option ) { + $old_option_value = get_option( $old_option, false ); + + // Continue if no option value was previously set. + if ( false === $old_option_value ) { + continue; + } + + if ( '1' === $old_option_value ) { + $old_option_value = 'yes'; + } elseif ( '0' === $old_option_value ) { + $old_option_value = 'no'; + } + + update_option( $new_option, $old_option_value ); + if ( $new_option !== $old_option ) { + delete_option( $old_option ); + } + } + } + /** + * Add the default terms for WC taxonomies - product types and order statuses. Modify this at your own risk. + */ + public static function create_terms() { + $taxonomies = array( + 'product_type' => array( + 'simple', + 'grouped', + 'variable', + 'external', + ), + 'product_visibility' => array( + 'exclude-from-search', + 'exclude-from-catalog', + 'featured', + 'outofstock', + 'rated-1', + 'rated-2', + 'rated-3', + 'rated-4', + 'rated-5', + ), + ); + + foreach ( $taxonomies as $taxonomy => $terms ) { + foreach ( $terms as $term ) { + if ( ! get_term_by( 'name', $term, $taxonomy ) ) { // @codingStandardsIgnoreLine. + wp_insert_term( $term, $taxonomy ); + } + } + } + + $woocommerce_default_category = (int) get_option( 'default_product_cat', 0 ); + + if ( ! $woocommerce_default_category || ! term_exists( $woocommerce_default_category, 'product_cat' ) ) { + $default_product_cat_id = 0; + $default_product_cat_slug = sanitize_title( _x( 'Uncategorized', 'Default category slug', 'woocommerce' ) ); + $default_product_cat = get_term_by( 'slug', $default_product_cat_slug, 'product_cat' ); // @codingStandardsIgnoreLine. + + if ( $default_product_cat ) { + $default_product_cat_id = absint( $default_product_cat->term_taxonomy_id ); + } else { + $result = wp_insert_term( _x( 'Uncategorized', 'Default category slug', 'woocommerce' ), 'product_cat', array( 'slug' => $default_product_cat_slug ) ); + + if ( ! is_wp_error( $result ) && ! empty( $result['term_taxonomy_id'] ) ) { + $default_product_cat_id = absint( $result['term_taxonomy_id'] ); + } + } + + if ( $default_product_cat_id ) { + update_option( 'default_product_cat', $default_product_cat_id ); + } + } + } + + /** + * Set up the database tables which the plugin needs to function. + * WARNING: If you are modifying this method, make sure that its safe to call regardless of the state of database. + * + * This is called from `install` method and is executed in-sync when WC is installed or updated. This can also be called optionally from `verify_base_tables`. + * + * TODO: Add all crucial tables that we have created from workers in the past. + * + * Tables: + * woocommerce_attribute_taxonomies - Table for storing attribute taxonomies - these are user defined + * woocommerce_downloadable_product_permissions - Table for storing user and guest download permissions. + * KEY(order_id, product_id, download_id) used for organizing downloads on the My Account page + * woocommerce_order_items - Order line items are stored in a table to make them easily queryable for reports + * woocommerce_order_itemmeta - Order line item meta is stored in a table for storing extra data. + * woocommerce_tax_rates - Tax Rates are stored inside 2 tables making tax queries simple and efficient. + * woocommerce_tax_rate_locations - Each rate can be applied to more than one postcode/city hence the second table. + */ + public static function create_tables() { + global $wpdb; + + $wpdb->hide_errors(); + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + + /** + * Before updating with DBDELTA, remove any primary keys which could be + * modified due to schema updates. + */ + if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_downloadable_product_permissions';" ) ) { + if ( ! $wpdb->get_var( "SHOW COLUMNS FROM `{$wpdb->prefix}woocommerce_downloadable_product_permissions` LIKE 'permission_id';" ) ) { + $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions DROP PRIMARY KEY, ADD `permission_id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT;" ); + } + } + + /** + * Change wp_woocommerce_sessions schema to use a bigint auto increment field instead of char(32) field as + * the primary key as it is not a good practice to use a char(32) field as the primary key of a table and as + * there were reports of issues with this table (see https://github.com/woocommerce/woocommerce/issues/20912). + * + * This query needs to run before dbDelta() as this WP function is not able to handle primary key changes + * (see https://github.com/woocommerce/woocommerce/issues/21534 and https://core.trac.wordpress.org/ticket/40357). + */ + if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_sessions'" ) ) { + if ( ! $wpdb->get_var( "SHOW KEYS FROM {$wpdb->prefix}woocommerce_sessions WHERE Key_name = 'PRIMARY' AND Column_name = 'session_id'" ) ) { + $wpdb->query( + "ALTER TABLE `{$wpdb->prefix}woocommerce_sessions` DROP PRIMARY KEY, DROP KEY `session_id`, ADD PRIMARY KEY(`session_id`), ADD UNIQUE KEY(`session_key`)" + ); + } + } + + dbDelta( self::get_schema() ); + + $index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->comments} WHERE column_name = 'comment_type' and key_name = 'woo_idx_comment_type'" ); + + if ( is_null( $index_exists ) ) { + // Add an index to the field comment_type to improve the response time of the query + // used by WC_Comments::wp_count_comments() to get the number of comments by type. + $wpdb->query( "ALTER TABLE {$wpdb->comments} ADD INDEX woo_idx_comment_type (comment_type)" ); + } + + // Get tables data types and check it matches before adding constraint. + $download_log_columns = $wpdb->get_results( "SHOW COLUMNS FROM {$wpdb->prefix}wc_download_log WHERE Field = 'permission_id'", ARRAY_A ); + $download_log_column_type = ''; + if ( isset( $download_log_columns[0]['Type'] ) ) { + $download_log_column_type = $download_log_columns[0]['Type']; + } + + $download_permissions_columns = $wpdb->get_results( "SHOW COLUMNS FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE Field = 'permission_id'", ARRAY_A ); + $download_permissions_column_type = ''; + if ( isset( $download_permissions_columns[0]['Type'] ) ) { + $download_permissions_column_type = $download_permissions_columns[0]['Type']; + } + + // Add constraint to download logs if the columns matches. + if ( ! empty( $download_permissions_column_type ) && ! empty( $download_log_column_type ) && $download_permissions_column_type === $download_log_column_type ) { + $fk_result = $wpdb->get_row( "SHOW CREATE TABLE {$wpdb->prefix}wc_download_log" ); + if ( false === strpos( $fk_result->{'Create Table'}, "fk_{$wpdb->prefix}wc_download_log_permission_id" ) ) { + $wpdb->query( + "ALTER TABLE `{$wpdb->prefix}wc_download_log` + ADD CONSTRAINT `fk_{$wpdb->prefix}wc_download_log_permission_id` + FOREIGN KEY (`permission_id`) + REFERENCES `{$wpdb->prefix}woocommerce_downloadable_product_permissions` (`permission_id`) ON DELETE CASCADE;" + ); + } + } + + // Clear table caches. + delete_transient( 'wc_attribute_taxonomies' ); + } + + /** + * Get Table schema. + * + * See https://github.com/woocommerce/woocommerce/wiki/Database-Description/ + * + * A note on indexes; Indexes have a maximum size of 767 bytes. Historically, we haven't need to be concerned about that. + * As of WordPress 4.2, however, we moved to utf8mb4, which uses 4 bytes per character. This means that an index which + * used to have room for floor(767/3) = 255 characters, now only has room for floor(767/4) = 191 characters. + * + * Changing indexes may cause duplicate index notices in logs due to https://core.trac.wordpress.org/ticket/34870 but dropping + * indexes first causes too much load on some servers/larger DB. + * + * When adding or removing a table, make sure to update the list of tables in WC_Install::get_tables(). + * + * @return string + */ + private static function get_schema() { + global $wpdb; + + $collate = ''; + + if ( $wpdb->has_cap( 'collation' ) ) { + $collate = $wpdb->get_charset_collate(); + } + + /* + * Indexes have a maximum size of 767 bytes. Historically, we haven't need to be concerned about that. + * As of WP 4.2, however, they moved to utf8mb4, which uses 4 bytes per character. This means that an index which + * used to have room for floor(767/3) = 255 characters, now only has room for floor(767/4) = 191 characters. + */ + $max_index_length = 191; + + $product_attributes_lookup_table_creation_sql = wc_get_container()->get( DataRegenerator::class )->get_table_creation_sql(); + + $tables = " +CREATE TABLE {$wpdb->prefix}woocommerce_sessions ( + session_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + session_key char(32) NOT NULL, + session_value longtext NOT NULL, + session_expiry BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (session_id), + UNIQUE KEY session_key (session_key) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_api_keys ( + key_id BIGINT UNSIGNED NOT NULL auto_increment, + user_id BIGINT UNSIGNED NOT NULL, + description varchar(200) NULL, + permissions varchar(10) NOT NULL, + consumer_key char(64) NOT NULL, + consumer_secret char(43) NOT NULL, + nonces longtext NULL, + truncated_key char(7) NOT NULL, + last_access datetime NULL default null, + PRIMARY KEY (key_id), + KEY consumer_key (consumer_key), + KEY consumer_secret (consumer_secret) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_attribute_taxonomies ( + attribute_id BIGINT UNSIGNED NOT NULL auto_increment, + attribute_name varchar(200) NOT NULL, + attribute_label varchar(200) NULL, + attribute_type varchar(20) NOT NULL, + attribute_orderby varchar(20) NOT NULL, + attribute_public int(1) NOT NULL DEFAULT 1, + PRIMARY KEY (attribute_id), + KEY attribute_name (attribute_name(20)) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions ( + permission_id BIGINT UNSIGNED NOT NULL auto_increment, + download_id varchar(36) NOT NULL, + product_id BIGINT UNSIGNED NOT NULL, + order_id BIGINT UNSIGNED NOT NULL DEFAULT 0, + order_key varchar(200) NOT NULL, + user_email varchar(200) NOT NULL, + user_id BIGINT UNSIGNED NULL, + downloads_remaining varchar(9) NULL, + access_granted datetime NOT NULL default '0000-00-00 00:00:00', + access_expires datetime NULL default null, + download_count BIGINT UNSIGNED NOT NULL DEFAULT 0, + PRIMARY KEY (permission_id), + KEY download_order_key_product (product_id,order_id,order_key(16),download_id), + KEY download_order_product (download_id,order_id,product_id), + KEY order_id (order_id), + KEY user_order_remaining_expires (user_id,order_id,downloads_remaining,access_expires) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_order_items ( + order_item_id BIGINT UNSIGNED NOT NULL auto_increment, + order_item_name TEXT NOT NULL, + order_item_type varchar(200) NOT NULL DEFAULT '', + order_id BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (order_item_id), + KEY order_id (order_id) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_order_itemmeta ( + meta_id BIGINT UNSIGNED NOT NULL auto_increment, + order_item_id BIGINT UNSIGNED NOT NULL, + meta_key varchar(255) default NULL, + meta_value longtext NULL, + PRIMARY KEY (meta_id), + KEY order_item_id (order_item_id), + KEY meta_key (meta_key(32)) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_tax_rates ( + tax_rate_id BIGINT UNSIGNED NOT NULL auto_increment, + tax_rate_country varchar(2) NOT NULL DEFAULT '', + tax_rate_state varchar(200) NOT NULL DEFAULT '', + tax_rate varchar(8) NOT NULL DEFAULT '', + tax_rate_name varchar(200) NOT NULL DEFAULT '', + tax_rate_priority BIGINT UNSIGNED NOT NULL, + tax_rate_compound int(1) NOT NULL DEFAULT 0, + tax_rate_shipping int(1) NOT NULL DEFAULT 1, + tax_rate_order BIGINT UNSIGNED NOT NULL, + tax_rate_class varchar(200) NOT NULL DEFAULT '', + PRIMARY KEY (tax_rate_id), + KEY tax_rate_country (tax_rate_country), + KEY tax_rate_state (tax_rate_state(2)), + KEY tax_rate_class (tax_rate_class(10)), + KEY tax_rate_priority (tax_rate_priority) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_tax_rate_locations ( + location_id BIGINT UNSIGNED NOT NULL auto_increment, + location_code varchar(200) NOT NULL, + tax_rate_id BIGINT UNSIGNED NOT NULL, + location_type varchar(40) NOT NULL, + PRIMARY KEY (location_id), + KEY tax_rate_id (tax_rate_id), + KEY location_type_code (location_type(10),location_code(20)) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zones ( + zone_id BIGINT UNSIGNED NOT NULL auto_increment, + zone_name varchar(200) NOT NULL, + zone_order BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (zone_id) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zone_locations ( + location_id BIGINT UNSIGNED NOT NULL auto_increment, + zone_id BIGINT UNSIGNED NOT NULL, + location_code varchar(200) NOT NULL, + location_type varchar(40) NOT NULL, + PRIMARY KEY (location_id), + KEY location_id (location_id), + KEY location_type_code (location_type(10),location_code(20)) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zone_methods ( + zone_id BIGINT UNSIGNED NOT NULL, + instance_id BIGINT UNSIGNED NOT NULL auto_increment, + method_id varchar(200) NOT NULL, + method_order BIGINT UNSIGNED NOT NULL, + is_enabled tinyint(1) NOT NULL DEFAULT '1', + PRIMARY KEY (instance_id) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_payment_tokens ( + token_id BIGINT UNSIGNED NOT NULL auto_increment, + gateway_id varchar(200) NOT NULL, + token text NOT NULL, + user_id BIGINT UNSIGNED NOT NULL DEFAULT '0', + type varchar(200) NOT NULL, + is_default tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (token_id), + KEY user_id (user_id) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_payment_tokenmeta ( + meta_id BIGINT UNSIGNED NOT NULL auto_increment, + payment_token_id BIGINT UNSIGNED NOT NULL, + meta_key varchar(255) NULL, + meta_value longtext NULL, + PRIMARY KEY (meta_id), + KEY payment_token_id (payment_token_id), + KEY meta_key (meta_key(32)) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_log ( + log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + timestamp datetime NOT NULL, + level smallint(4) NOT NULL, + source varchar(200) NOT NULL, + message longtext NOT NULL, + context longtext NULL, + PRIMARY KEY (log_id), + KEY level (level) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_webhooks ( + webhook_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + status varchar(200) NOT NULL, + name text NOT NULL, + user_id BIGINT UNSIGNED NOT NULL, + delivery_url text NOT NULL, + secret text NOT NULL, + topic varchar(200) NOT NULL, + date_created datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + date_created_gmt datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + date_modified datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + date_modified_gmt datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + api_version smallint(4) NOT NULL, + failure_count smallint(10) NOT NULL DEFAULT '0', + pending_delivery tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (webhook_id), + KEY user_id (user_id) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_download_log ( + download_log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + timestamp datetime NOT NULL, + permission_id BIGINT UNSIGNED NOT NULL, + user_id BIGINT UNSIGNED NULL, + user_ip_address VARCHAR(100) NULL DEFAULT '', + PRIMARY KEY (download_log_id), + KEY permission_id (permission_id), + KEY timestamp (timestamp) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_product_meta_lookup ( + `product_id` bigint(20) NOT NULL, + `sku` varchar(100) NULL default '', + `virtual` tinyint(1) NULL default 0, + `downloadable` tinyint(1) NULL default 0, + `min_price` decimal(19,4) NULL default NULL, + `max_price` decimal(19,4) NULL default NULL, + `onsale` tinyint(1) NULL default 0, + `stock_quantity` double NULL default NULL, + `stock_status` varchar(100) NULL default 'instock', + `rating_count` bigint(20) NULL default 0, + `average_rating` decimal(3,2) NULL default 0.00, + `total_sales` bigint(20) NULL default 0, + `tax_status` varchar(100) NULL default 'taxable', + `tax_class` varchar(100) NULL default '', + PRIMARY KEY (`product_id`), + KEY `virtual` (`virtual`), + KEY `downloadable` (`downloadable`), + KEY `stock_status` (`stock_status`), + KEY `stock_quantity` (`stock_quantity`), + KEY `onsale` (`onsale`), + KEY min_max_price (`min_price`, `max_price`) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_tax_rate_classes ( + tax_rate_class_id BIGINT UNSIGNED NOT NULL auto_increment, + name varchar(200) NOT NULL DEFAULT '', + slug varchar(200) NOT NULL DEFAULT '', + PRIMARY KEY (tax_rate_class_id), + UNIQUE KEY slug (slug($max_index_length)) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_reserved_stock ( + `order_id` bigint(20) NOT NULL, + `product_id` bigint(20) NOT NULL, + `stock_quantity` double NOT NULL DEFAULT 0, + `timestamp` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + `expires` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY (`order_id`, `product_id`) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_rate_limits ( + rate_limit_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + rate_limit_key varchar(200) NOT NULL, + rate_limit_expiry BIGINT UNSIGNED NOT NULL, + rate_limit_remaining smallint(10) NOT NULL DEFAULT '0', + PRIMARY KEY (rate_limit_id), + UNIQUE KEY rate_limit_key (rate_limit_key($max_index_length)) +) $collate; +$product_attributes_lookup_table_creation_sql +CREATE TABLE {$wpdb->prefix}wc_product_download_directories ( + url_id BIGINT UNSIGNED NOT NULL auto_increment, + url varchar(256) NOT NULL, + enabled TINYINT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (url_id), + KEY `url` (`url`) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_order_stats ( + order_id bigint(20) unsigned NOT NULL, + parent_id bigint(20) unsigned DEFAULT 0 NOT NULL, + date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + date_created_gmt datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + num_items_sold int(11) DEFAULT 0 NOT NULL, + total_sales double DEFAULT 0 NOT NULL, + tax_total double DEFAULT 0 NOT NULL, + shipping_total double DEFAULT 0 NOT NULL, + net_total double DEFAULT 0 NOT NULL, + returning_customer boolean DEFAULT NULL, + status varchar(200) NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (order_id), + KEY date_created (date_created), + KEY customer_id (customer_id), + KEY status (status({$max_index_length})) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_order_product_lookup ( + order_item_id BIGINT UNSIGNED NOT NULL, + order_id BIGINT UNSIGNED NOT NULL, + product_id BIGINT UNSIGNED NOT NULL, + variation_id BIGINT UNSIGNED NOT NULL, + customer_id BIGINT UNSIGNED NULL, + date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + product_qty INT NOT NULL, + product_net_revenue double DEFAULT 0 NOT NULL, + product_gross_revenue double DEFAULT 0 NOT NULL, + coupon_amount double DEFAULT 0 NOT NULL, + tax_amount double DEFAULT 0 NOT NULL, + shipping_amount double DEFAULT 0 NOT NULL, + shipping_tax_amount double DEFAULT 0 NOT NULL, + PRIMARY KEY (order_item_id), + KEY order_id (order_id), + KEY product_id (product_id), + KEY customer_id (customer_id), + KEY date_created (date_created) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_order_tax_lookup ( + order_id BIGINT UNSIGNED NOT NULL, + tax_rate_id BIGINT UNSIGNED NOT NULL, + date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + shipping_tax double DEFAULT 0 NOT NULL, + order_tax double DEFAULT 0 NOT NULL, + total_tax double DEFAULT 0 NOT NULL, + PRIMARY KEY (order_id, tax_rate_id), + KEY tax_rate_id (tax_rate_id), + KEY date_created (date_created) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_order_coupon_lookup ( + order_id BIGINT UNSIGNED NOT NULL, + coupon_id BIGINT NOT NULL, + date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + discount_amount double DEFAULT 0 NOT NULL, + PRIMARY KEY (order_id, coupon_id), + KEY coupon_id (coupon_id), + KEY date_created (date_created) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_admin_notes ( + note_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + name varchar(255) NOT NULL, + type varchar(20) NOT NULL, + locale varchar(20) NOT NULL, + title longtext NOT NULL, + content longtext NOT NULL, + content_data longtext NULL default null, + status varchar(200) NOT NULL, + source varchar(200) NOT NULL, + date_created datetime NOT NULL default '0000-00-00 00:00:00', + date_reminder datetime NULL default null, + is_snoozable boolean DEFAULT 0 NOT NULL, + layout varchar(20) DEFAULT '' NOT NULL, + image varchar(200) NULL DEFAULT NULL, + is_deleted boolean DEFAULT 0 NOT NULL, + is_read boolean DEFAULT 0 NOT NULL, + icon varchar(200) NOT NULL default 'info', + PRIMARY KEY (note_id) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_admin_note_actions ( + action_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + note_id BIGINT UNSIGNED NOT NULL, + name varchar(255) NOT NULL, + label varchar(255) NOT NULL, + query longtext NOT NULL, + status varchar(255) NOT NULL, + actioned_text varchar(255) NOT NULL, + nonce_action varchar(255) NULL DEFAULT NULL, + nonce_name varchar(255) NULL DEFAULT NULL, + PRIMARY KEY (action_id), + KEY note_id (note_id) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_customer_lookup ( + customer_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + user_id BIGINT UNSIGNED DEFAULT NULL, + username varchar(60) DEFAULT '' NOT NULL, + first_name varchar(255) NOT NULL, + last_name varchar(255) NOT NULL, + email varchar(100) NULL default NULL, + date_last_active timestamp NULL default null, + date_registered timestamp NULL default null, + country char(2) DEFAULT '' NOT NULL, + postcode varchar(20) DEFAULT '' NOT NULL, + city varchar(100) DEFAULT '' NOT NULL, + state varchar(100) DEFAULT '' NOT NULL, + PRIMARY KEY (customer_id), + UNIQUE KEY user_id (user_id), + KEY email (email) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_category_lookup ( + category_tree_id BIGINT UNSIGNED NOT NULL, + category_id BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (category_tree_id,category_id) +) $collate; + "; + + return $tables; + } + + /** + * Return a list of WooCommerce tables. Used to make sure all WC tables are dropped when uninstalling the plugin + * in a single site or multi site environment. + * + * @return array WC tables. + */ + public static function get_tables() { + global $wpdb; + + $tables = array( + "{$wpdb->prefix}wc_download_log", + "{$wpdb->prefix}wc_product_download_directories", + "{$wpdb->prefix}wc_product_meta_lookup", + "{$wpdb->prefix}wc_tax_rate_classes", + "{$wpdb->prefix}wc_webhooks", + "{$wpdb->prefix}woocommerce_api_keys", + "{$wpdb->prefix}woocommerce_attribute_taxonomies", + "{$wpdb->prefix}woocommerce_downloadable_product_permissions", + "{$wpdb->prefix}woocommerce_log", + "{$wpdb->prefix}woocommerce_order_itemmeta", + "{$wpdb->prefix}woocommerce_order_items", + "{$wpdb->prefix}woocommerce_payment_tokenmeta", + "{$wpdb->prefix}woocommerce_payment_tokens", + "{$wpdb->prefix}woocommerce_sessions", + "{$wpdb->prefix}woocommerce_shipping_zone_locations", + "{$wpdb->prefix}woocommerce_shipping_zone_methods", + "{$wpdb->prefix}woocommerce_shipping_zones", + "{$wpdb->prefix}woocommerce_tax_rate_locations", + "{$wpdb->prefix}woocommerce_tax_rates", + "{$wpdb->prefix}wc_reserved_stock", + "{$wpdb->prefix}wc_rate_limits", + wc_get_container()->get( DataRegenerator::class )->get_lookup_table_name(), + + // WCA Tables. + "{$wpdb->prefix}wc_order_stats", + "{$wpdb->prefix}wc_order_product_lookup", + "{$wpdb->prefix}wc_order_tax_lookup", + "{$wpdb->prefix}wc_order_coupon_lookup", + "{$wpdb->prefix}wc_admin_notes", + "{$wpdb->prefix}wc_admin_note_actions", + "{$wpdb->prefix}wc_customer_lookup", + "{$wpdb->prefix}wc_category_lookup", + ); + + /** + * Filter the list of known WooCommerce tables. + * + * If WooCommerce plugins need to add new tables, they can inject them here. + * + * @param array $tables An array of WooCommerce-specific database table names. + */ + $tables = apply_filters( 'woocommerce_install_get_tables', $tables ); + + return $tables; + } + + /** + * Drop WooCommerce tables. + * + * @return void + */ + public static function drop_tables() { + global $wpdb; + + $tables = self::get_tables(); + + foreach ( $tables as $table ) { + $wpdb->query( "DROP TABLE IF EXISTS {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } + } + + /** + * Uninstall tables when MU blog is deleted. + * + * @param array $tables List of tables that will be deleted by WP. + * + * @return string[] + */ + public static function wpmu_drop_tables( $tables ) { + return array_merge( $tables, self::get_tables() ); + } + + /** + * Create roles and capabilities. + */ + public static function create_roles() { + global $wp_roles; + + if ( ! class_exists( 'WP_Roles' ) ) { + return; + } + + if ( ! isset( $wp_roles ) ) { + $wp_roles = new WP_Roles(); // @codingStandardsIgnoreLine + } + + // Dummy gettext calls to get strings in the catalog. + /* translators: user role */ + _x( 'Customer', 'User role', 'woocommerce' ); + /* translators: user role */ + _x( 'Shop manager', 'User role', 'woocommerce' ); + + // Customer role. + add_role( + 'customer', + 'Customer', + array( + 'read' => true, + ) + ); + + // Shop manager role. + add_role( + 'shop_manager', + 'Shop manager', + array( + 'level_9' => true, + 'level_8' => true, + 'level_7' => true, + 'level_6' => true, + 'level_5' => true, + 'level_4' => true, + 'level_3' => true, + 'level_2' => true, + 'level_1' => true, + 'level_0' => true, + 'read' => true, + 'read_private_pages' => true, + 'read_private_posts' => true, + 'edit_posts' => true, + 'edit_pages' => true, + 'edit_published_posts' => true, + 'edit_published_pages' => true, + 'edit_private_pages' => true, + 'edit_private_posts' => true, + 'edit_others_posts' => true, + 'edit_others_pages' => true, + 'publish_posts' => true, + 'publish_pages' => true, + 'delete_posts' => true, + 'delete_pages' => true, + 'delete_private_pages' => true, + 'delete_private_posts' => true, + 'delete_published_pages' => true, + 'delete_published_posts' => true, + 'delete_others_posts' => true, + 'delete_others_pages' => true, + 'manage_categories' => true, + 'manage_links' => true, + 'moderate_comments' => true, + 'upload_files' => true, + 'export' => true, + 'import' => true, + 'list_users' => true, + 'edit_theme_options' => true, + ) + ); + + $capabilities = self::get_core_capabilities(); + + foreach ( $capabilities as $cap_group ) { + foreach ( $cap_group as $cap ) { + $wp_roles->add_cap( 'shop_manager', $cap ); + $wp_roles->add_cap( 'administrator', $cap ); + } + } + } + + /** + * Get capabilities for WooCommerce - these are assigned to admin/shop manager during installation or reset. + * + * @return array + */ + public static function get_core_capabilities() { + $capabilities = array(); + + $capabilities['core'] = array( + 'manage_woocommerce', + 'view_woocommerce_reports', + ); + + $capability_types = array( 'product', 'shop_order', 'shop_coupon' ); + + foreach ( $capability_types as $capability_type ) { + + $capabilities[ $capability_type ] = array( + // Post type. + "edit_{$capability_type}", + "read_{$capability_type}", + "delete_{$capability_type}", + "edit_{$capability_type}s", + "edit_others_{$capability_type}s", + "publish_{$capability_type}s", + "read_private_{$capability_type}s", + "delete_{$capability_type}s", + "delete_private_{$capability_type}s", + "delete_published_{$capability_type}s", + "delete_others_{$capability_type}s", + "edit_private_{$capability_type}s", + "edit_published_{$capability_type}s", + + // Terms. + "manage_{$capability_type}_terms", + "edit_{$capability_type}_terms", + "delete_{$capability_type}_terms", + "assign_{$capability_type}_terms", + ); + } + + return $capabilities; + } + + /** + * Remove WooCommerce roles. + */ + public static function remove_roles() { + global $wp_roles; + + if ( ! class_exists( 'WP_Roles' ) ) { + return; + } + + if ( ! isset( $wp_roles ) ) { + $wp_roles = new WP_Roles(); // @codingStandardsIgnoreLine + } + + $capabilities = self::get_core_capabilities(); + + foreach ( $capabilities as $cap_group ) { + foreach ( $cap_group as $cap ) { + $wp_roles->remove_cap( 'shop_manager', $cap ); + $wp_roles->remove_cap( 'administrator', $cap ); + } + } + + remove_role( 'customer' ); + remove_role( 'shop_manager' ); + } + + /** + * Create files/directories. + */ + private static function create_files() { + // Bypass if filesystem is read-only and/or non-standard upload system is used. + if ( apply_filters( 'woocommerce_install_skip_create_files', false ) ) { + return; + } + + // Install files and folders for uploading files and prevent hotlinking. + $upload_dir = wp_get_upload_dir(); + $download_method = get_option( 'woocommerce_file_download_method', 'force' ); + + $files = array( + array( + 'base' => $upload_dir['basedir'] . '/woocommerce_uploads', + 'file' => 'index.html', + 'content' => '', + ), + array( + 'base' => WC_LOG_DIR, + 'file' => '.htaccess', + 'content' => 'deny from all', + ), + array( + 'base' => WC_LOG_DIR, + 'file' => 'index.html', + 'content' => '', + ), + array( + 'base' => $upload_dir['basedir'] . '/woocommerce_uploads', + 'file' => '.htaccess', + 'content' => 'redirect' === $download_method ? 'Options -Indexes' : 'deny from all', + ), + ); + + foreach ( $files as $file ) { + if ( wp_mkdir_p( $file['base'] ) && ! file_exists( trailingslashit( $file['base'] ) . $file['file'] ) ) { + $file_handle = @fopen( trailingslashit( $file['base'] ) . $file['file'], 'wb' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen + if ( $file_handle ) { + fwrite( $file_handle, $file['content'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite + fclose( $file_handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose + } + } + } + + // Create attachment for placeholders. + self::create_placeholder_image(); + } + + /** + * Create a placeholder image in the media library. + * + * @since 3.5.0 + */ + private static function create_placeholder_image() { + $placeholder_image = get_option( 'woocommerce_placeholder_image', 0 ); + + // Validate current setting if set. If set, return. + if ( ! empty( $placeholder_image ) ) { + if ( ! is_numeric( $placeholder_image ) ) { + return; + } elseif ( $placeholder_image && wp_attachment_is_image( $placeholder_image ) ) { + return; + } + } + + $upload_dir = wp_upload_dir(); + $source = WC()->plugin_path() . '/assets/images/placeholder-attachment.png'; + $filename = $upload_dir['basedir'] . '/woocommerce-placeholder.png'; + + if ( ! file_exists( $filename ) ) { + copy( $source, $filename ); // @codingStandardsIgnoreLine. + } + + if ( ! file_exists( $filename ) ) { + update_option( 'woocommerce_placeholder_image', 0 ); + return; + } + + $filetype = wp_check_filetype( basename( $filename ), null ); + $attachment = array( + 'guid' => $upload_dir['url'] . '/' . basename( $filename ), + 'post_mime_type' => $filetype['type'], + 'post_title' => preg_replace( '/\.[^.]+$/', '', basename( $filename ) ), + 'post_content' => '', + 'post_status' => 'inherit', + ); + + $attach_id = wp_insert_attachment( $attachment, $filename ); + if ( is_wp_error( $attach_id ) ) { + update_option( 'woocommerce_placeholder_image', 0 ); + return; + } + + update_option( 'woocommerce_placeholder_image', $attach_id ); + + // Make sure that this file is included, as wp_generate_attachment_metadata() depends on it. + require_once ABSPATH . 'wp-admin/includes/image.php'; + + // Generate the metadata for the attachment, and update the database record. + $attach_data = wp_generate_attachment_metadata( $attach_id, $filename ); + wp_update_attachment_metadata( $attach_id, $attach_data ); + } + + /** + * Show action links on the plugin screen. + * + * @param mixed $links Plugin Action links. + * + * @return array + */ + public static function plugin_action_links( $links ) { + $action_links = array( + 'settings' => '' . esc_html__( 'Settings', 'woocommerce' ) . '', + ); + + return array_merge( $action_links, $links ); + } + + /** + * Show row meta on the plugin screen. + * + * @param mixed $links Plugin Row Meta. + * @param mixed $file Plugin Base file. + * + * @return array + */ + public static function plugin_row_meta( $links, $file ) { + if ( WC_PLUGIN_BASENAME !== $file ) { + return $links; + } + + $row_meta = array( + 'docs' => '' . esc_html__( 'Docs', 'woocommerce' ) . '', + 'apidocs' => '' . esc_html__( 'API docs', 'woocommerce' ) . '', + 'support' => '' . esc_html__( 'Community support', 'woocommerce' ) . '', + ); + + if ( WCConnectionHelper::is_connected() ) { + $row_meta['premium_support'] = '' . esc_html__( 'Premium support', 'woocommerce' ) . ''; + } + + return array_merge( $links, $row_meta ); + } + + /** + * Get slug from path and associate it with the path. + * + * @param array $plugins Associative array of plugin files to paths. + * @param string $key Plugin relative path. Example: woocommerce/woocommerce.php. + */ + private static function associate_plugin_file( $plugins, $key ) { + $path = explode( '/', $key ); + $filename = end( $path ); + $plugins[ $filename ] = $key; + return $plugins; + } + + /** + * Install a plugin from .org in the background via a cron job (used by + * installer - opt in). + * + * @param string $plugin_to_install_id Plugin ID. + * @param array $plugin_to_install Plugin information. + * + * @throws Exception If unable to proceed with plugin installation. + * @since 2.6.0 + */ + public static function background_installer( $plugin_to_install_id, $plugin_to_install ) { + // Explicitly clear the event. + $args = func_get_args(); + + if ( ! empty( $plugin_to_install['repo-slug'] ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; + require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + + WP_Filesystem(); + + $skin = new Automatic_Upgrader_Skin(); + $upgrader = new WP_Upgrader( $skin ); + $installed_plugins = array_reduce( array_keys( get_plugins() ), array( __CLASS__, 'associate_plugin_file' ) ); + if ( empty( $installed_plugins ) ) { + $installed_plugins = array(); + } + $plugin_slug = $plugin_to_install['repo-slug']; + $plugin_file = isset( $plugin_to_install['file'] ) ? $plugin_to_install['file'] : $plugin_slug . '.php'; + $installed = false; + $activate = false; + + // See if the plugin is installed already. + if ( isset( $installed_plugins[ $plugin_file ] ) ) { + $installed = true; + $activate = ! is_plugin_active( $installed_plugins[ $plugin_file ] ); + } + + // Install this thing! + if ( ! $installed ) { + // Suppress feedback. + ob_start(); + + try { + $plugin_information = plugins_api( + 'plugin_information', + array( + 'slug' => $plugin_slug, + 'fields' => array( + 'short_description' => false, + 'sections' => false, + 'requires' => false, + 'rating' => false, + 'ratings' => false, + 'downloaded' => false, + 'last_updated' => false, + 'added' => false, + 'tags' => false, + 'homepage' => false, + 'donate_link' => false, + 'author_profile' => false, + 'author' => false, + ), + ) + ); + + if ( is_wp_error( $plugin_information ) ) { + throw new Exception( $plugin_information->get_error_message() ); + } + + $package = $plugin_information->download_link; + $download = $upgrader->download_package( $package ); + + if ( is_wp_error( $download ) ) { + throw new Exception( $download->get_error_message() ); + } + + $working_dir = $upgrader->unpack_package( $download, true ); + + if ( is_wp_error( $working_dir ) ) { + throw new Exception( $working_dir->get_error_message() ); + } + + $result = $upgrader->install_package( + array( + 'source' => $working_dir, + 'destination' => WP_PLUGIN_DIR, + 'clear_destination' => false, + 'abort_if_destination_exists' => false, + 'clear_working' => true, + 'hook_extra' => array( + 'type' => 'plugin', + 'action' => 'install', + ), + ) + ); + + if ( is_wp_error( $result ) ) { + throw new Exception( $result->get_error_message() ); + } + + $activate = true; + + } catch ( Exception $e ) { + WC_Admin_Notices::add_custom_notice( + $plugin_to_install_id . '_install_error', + sprintf( + // translators: 1: plugin name, 2: error message, 3: URL to install plugin manually. + __( '%1$s could not be installed (%2$s). Please install it manually by clicking here.', 'woocommerce' ), + $plugin_to_install['name'], + $e->getMessage(), + esc_url( admin_url( 'index.php?wc-install-plugin-redirect=' . $plugin_slug ) ) + ) + ); + } + + // Discard feedback. + ob_end_clean(); + } + + wp_clean_plugins_cache(); + + // Activate this thing. + if ( $activate ) { + try { + add_action( 'add_option_mailchimp_woocommerce_plugin_do_activation_redirect', array( __CLASS__, 'remove_mailchimps_redirect' ), 10, 2 ); + $result = activate_plugin( $installed ? $installed_plugins[ $plugin_file ] : $plugin_slug . '/' . $plugin_file ); + + if ( is_wp_error( $result ) ) { + throw new Exception( $result->get_error_message() ); + } + } catch ( Exception $e ) { + WC_Admin_Notices::add_custom_notice( + $plugin_to_install_id . '_install_error', + sprintf( + // translators: 1: plugin name, 2: URL to WP plugin page. + __( '%1$s was installed but could not be activated. Please activate it manually by clicking here.', 'woocommerce' ), + $plugin_to_install['name'], + admin_url( 'plugins.php' ) + ) + ); + } + } + } + } + + /** + * Removes redirect added during MailChimp plugin's activation. + * + * @param string $option Option name. + * @param string $value Option value. + */ + public static function remove_mailchimps_redirect( $option, $value ) { + // Remove this action to prevent infinite looping. + remove_action( 'add_option_mailchimp_woocommerce_plugin_do_activation_redirect', array( __CLASS__, 'remove_mailchimps_redirect' ) ); + + // Update redirect back to false. + update_option( 'mailchimp_woocommerce_plugin_do_activation_redirect', false ); + } + + /** + * Install a theme from .org in the background via a cron job (used by installer - opt in). + * + * @param string $theme_slug Theme slug. + * + * @throws Exception If unable to proceed with theme installation. + * @since 3.1.0 + */ + public static function theme_background_installer( $theme_slug ) { + // Explicitly clear the event. + $args = func_get_args(); + + if ( ! empty( $theme_slug ) ) { + // Suppress feedback. + ob_start(); + + try { + $theme = wp_get_theme( $theme_slug ); + + if ( ! $theme->exists() ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; + include_once ABSPATH . 'wp-admin/includes/theme.php'; + + WP_Filesystem(); + + $skin = new Automatic_Upgrader_Skin(); + $upgrader = new Theme_Upgrader( $skin ); + $api = themes_api( + 'theme_information', + array( + 'slug' => $theme_slug, + 'fields' => array( 'sections' => false ), + ) + ); + $result = $upgrader->install( $api->download_link ); + + if ( is_wp_error( $result ) ) { + throw new Exception( $result->get_error_message() ); + } elseif ( is_wp_error( $skin->result ) ) { + throw new Exception( $skin->result->get_error_message() ); + } elseif ( is_null( $result ) ) { + throw new Exception( 'Unable to connect to the filesystem. Please confirm your credentials.' ); + } + } + + switch_theme( $theme_slug ); + } catch ( Exception $e ) { + WC_Admin_Notices::add_custom_notice( + $theme_slug . '_install_error', + sprintf( + // translators: 1: theme slug, 2: error message, 3: URL to install theme manually. + __( '%1$s could not be installed (%2$s). Please install it manually by clicking here.', 'woocommerce' ), + $theme_slug, + $e->getMessage(), + esc_url( admin_url( 'update.php?action=install-theme&theme=' . $theme_slug . '&_wpnonce=' . wp_create_nonce( 'install-theme_' . $theme_slug ) ) ) + ) + ); + } + + // Discard feedback. + ob_end_clean(); + } + } + + /** + * Sets whether PayPal Standard will be loaded on install. + * + * @since 5.5.0 + */ + private static function set_paypal_standard_load_eligibility() { + // Initiating the payment gateways sets the flag. + if ( class_exists( 'WC_Gateway_Paypal' ) ) { + ( new WC_Gateway_Paypal() )->should_load(); + } + } + + /** + * Gets the content of the sample refunds and return policy page. + * + * @since 5.6.0 + * @return string The content for the page + */ + private static function get_refunds_return_policy_page_content() { + return << +

    This is a sample page.

    + + + +

    Overview

    + + + +

    Our refund and returns policy lasts 30 days. If 30 days have passed since your purchase, we can’t offer you a full refund or exchange.

    + + + +

    To be eligible for a return, your item must be unused and in the same condition that you received it. It must also be in the original packaging.

    + + + +

    Several types of goods are exempt from being returned. Perishable goods such as food, flowers, newspapers or magazines cannot be returned. We also do not accept products that are intimate or sanitary goods, hazardous materials, or flammable liquids or gases.

    + + + +

    Additional non-returnable items:

    + + + +
      +
    • Gift cards
    • +
    • Downloadable software products
    • +
    • Some health and personal care items
    • +
    + + + +

    To complete your return, we require a receipt or proof of purchase.

    + + + +

    Please do not send your purchase back to the manufacturer.

    + + + +

    There are certain situations where only partial refunds are granted:

    + + + +
      +
    • Book with obvious signs of use
    • +
    • CD, DVD, VHS tape, software, video game, cassette tape, or vinyl record that has been opened.
    • +
    • Any item not in its original condition, is damaged or missing parts for reasons not due to our error.
    • +
    • Any item that is returned more than 30 days after delivery
    • +
    + + + +

    Refunds

    + + + +

    Once your return is received and inspected, we will send you an email to notify you that we have received your returned item. We will also notify you of the approval or rejection of your refund.

    + + + +

    If you are approved, then your refund will be processed, and a credit will automatically be applied to your credit card or original method of payment, within a certain amount of days.

    + + + +Late or missing refunds + + + +

    If you haven’t received a refund yet, first check your bank account again.

    + + + +

    Then contact your credit card company, it may take some time before your refund is officially posted.

    + + + +

    Next contact your bank. There is often some processing time before a refund is posted.

    + + + +

    If you’ve done all of this and you still have not received your refund yet, please contact us at {email address}.

    + + + +Sale items + + + +

    Only regular priced items may be refunded. Sale items cannot be refunded.

    + + + +

    Exchanges

    + + + +

    We only replace items if they are defective or damaged. If you need to exchange it for the same item, send us an email at {email address} and send your item to: {physical address}.

    + + + +

    Gifts

    + + + +

    If the item was marked as a gift when purchased and shipped directly to you, you’ll receive a gift credit for the value of your return. Once the returned item is received, a gift certificate will be mailed to you.

    + + + +

    If the item wasn’t marked as a gift when purchased, or the gift giver had the order shipped to themselves to give to you later, we will send a refund to the gift giver and they will find out about your return.

    + + + +

    Shipping returns

    + + + +

    To return your product, you should mail your product to: {physical address}.

    + + + +

    You will be responsible for paying for your own shipping costs for returning your item. Shipping costs are non-refundable. If you receive a refund, the cost of return shipping will be deducted from your refund.

    + + + +

    Depending on where you live, the time it may take for your exchanged product to reach you may vary.

    + + + +

    If you are returning more expensive items, you may consider using a trackable shipping service or purchasing shipping insurance. We don’t guarantee that we will receive your returned item.

    + + + +

    Need help?

    + + + +

    Contact us at {email} for questions related to refunds and returns.

    + +EOT; + } + + /** + * Adds an admin inbox note after a page has been created to notify + * user. For example to take action to edit the page such as the + * Refund and returns page. + * + * @since 5.6.0 + * @return void + */ + public static function add_admin_note_after_page_created() { + if ( ! WC()->is_wc_admin_active() ) { + return; + } + + $page_id = get_option( 'woocommerce_refund_returns_page_created', null ); + + if ( null === $page_id ) { + return; + } + + WC_Notes_Refund_Returns::possibly_add_note( $page_id ); + } + + /** + * When pages are created, we might want to take some action. + * In this case we want to set an option when refund and returns + * page is created. + * + * @since 5.6.0 + * @param int $page_id ID of the page. + * @param array $page_data The data of the page created. + * @return void + */ + public static function page_created( $page_id, $page_data ) { + if ( 'refund_returns' === $page_data['post_name'] ) { + delete_option( 'woocommerce_refund_returns_page_created' ); + add_option( 'woocommerce_refund_returns_page_created', $page_id, '', false ); + } + } +} + +WC_Install::init(); diff --git a/includes/class-wc-integrations.php b/plugins/woocommerce/includes/class-wc-integrations.php similarity index 100% rename from includes/class-wc-integrations.php rename to plugins/woocommerce/includes/class-wc-integrations.php diff --git a/includes/class-wc-log-levels.php b/plugins/woocommerce/includes/class-wc-log-levels.php similarity index 100% rename from includes/class-wc-log-levels.php rename to plugins/woocommerce/includes/class-wc-log-levels.php diff --git a/plugins/woocommerce/includes/class-wc-logger.php b/plugins/woocommerce/includes/class-wc-logger.php new file mode 100644 index 00000000000..2febfea1cb6 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-logger.php @@ -0,0 +1,315 @@ +' . esc_html( is_object( $handler ) ? get_class( $handler ) : $handler ) . '', + 'WC_Log_Handler_Interface' + ), + '3.0' + ); + } + } + } + + // Support the constant as long as a valid log level has been set for it. + if ( null === $threshold ) { + $threshold = Constants::get_constant( 'WC_LOG_THRESHOLD' ); + if ( null !== $threshold && ! WC_Log_Levels::is_valid_level( $threshold ) ) { + $threshold = null; + } + } + + if ( null !== $threshold ) { + $threshold = WC_Log_Levels::get_level_severity( $threshold ); + } + + $this->handlers = $register_handlers; + $this->threshold = $threshold; + } + + /** + * Determine whether to handle or ignore log. + * + * @param string $level emergency|alert|critical|error|warning|notice|info|debug. + * @return bool True if the log should be handled. + */ + protected function should_handle( $level ) { + if ( null === $this->threshold ) { + return true; + } + return $this->threshold <= WC_Log_Levels::get_level_severity( $level ); + } + + /** + * Add a log entry. + * + * This is not the preferred method for adding log messages. Please use log() or any one of + * the level methods (debug(), info(), etc.). This method may be deprecated in the future. + * + * @param string $handle File handle. + * @param string $message Message to log. + * @param string $level Logging level. + * @return bool + */ + public function add( $handle, $message, $level = WC_Log_Levels::NOTICE ) { + $message = apply_filters( 'woocommerce_logger_add_message', $message, $handle ); + $this->log( + $level, + $message, + array( + 'source' => $handle, + '_legacy' => true, + ) + ); + wc_do_deprecated_action( 'woocommerce_log_add', array( $handle, $message ), '3.0', 'This action has been deprecated with no alternative.' ); + return true; + } + + /** + * Add a log entry. + * + * @param string $level One of the following: + * 'emergency': System is unusable. + * 'alert': Action must be taken immediately. + * 'critical': Critical conditions. + * 'error': Error conditions. + * 'warning': Warning conditions. + * 'notice': Normal but significant condition. + * 'info': Informational messages. + * 'debug': Debug-level messages. + * @param string $message Log message. + * @param array $context Optional. Additional information for log handlers. + */ + public function log( $level, $message, $context = array() ) { + if ( ! WC_Log_Levels::is_valid_level( $level ) ) { + /* translators: 1: WC_Logger::log 2: level */ + wc_doing_it_wrong( __METHOD__, sprintf( __( '%1$s was called with an invalid level "%2$s".', 'woocommerce' ), 'WC_Logger::log', $level ), '3.0' ); + } + + if ( $this->should_handle( $level ) ) { + $timestamp = time(); + + foreach ( $this->handlers as $handler ) { + /** + * Filter the logging message. Returning null will prevent logging from occurring since 5.3. + * + * @since 3.1 + * @param string $message Log message. + * @param string $level One of: emergency, alert, critical, error, warning, notice, info, or debug. + * @param array $context Additional information for log handlers. + * @param object $handler The handler object, such as WC_Log_Handler_File. Available since 5.3. + */ + $message = apply_filters( 'woocommerce_logger_log_message', $message, $level, $context, $handler ); + + if ( null !== $message ) { + $handler->handle( $timestamp, $level, $message, $context ); + } + } + } + } + + /** + * Adds an emergency level message. + * + * System is unusable. + * + * @see WC_Logger::log + * + * @param string $message Message to log. + * @param array $context Log context. + */ + public function emergency( $message, $context = array() ) { + $this->log( WC_Log_Levels::EMERGENCY, $message, $context ); + } + + /** + * Adds an alert level message. + * + * Action must be taken immediately. + * Example: Entire website down, database unavailable, etc. + * + * @see WC_Logger::log + * + * @param string $message Message to log. + * @param array $context Log context. + */ + public function alert( $message, $context = array() ) { + $this->log( WC_Log_Levels::ALERT, $message, $context ); + } + + /** + * Adds a critical level message. + * + * Critical conditions. + * Example: Application component unavailable, unexpected exception. + * + * @see WC_Logger::log + * + * @param string $message Message to log. + * @param array $context Log context. + */ + public function critical( $message, $context = array() ) { + $this->log( WC_Log_Levels::CRITICAL, $message, $context ); + } + + /** + * Adds an error level message. + * + * Runtime errors that do not require immediate action but should typically be logged + * and monitored. + * + * @see WC_Logger::log + * + * @param string $message Message to log. + * @param array $context Log context. + */ + public function error( $message, $context = array() ) { + $this->log( WC_Log_Levels::ERROR, $message, $context ); + } + + /** + * Adds a warning level message. + * + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things that are not + * necessarily wrong. + * + * @see WC_Logger::log + * + * @param string $message Message to log. + * @param array $context Log context. + */ + public function warning( $message, $context = array() ) { + $this->log( WC_Log_Levels::WARNING, $message, $context ); + } + + /** + * Adds a notice level message. + * + * Normal but significant events. + * + * @see WC_Logger::log + * + * @param string $message Message to log. + * @param array $context Log context. + */ + public function notice( $message, $context = array() ) { + $this->log( WC_Log_Levels::NOTICE, $message, $context ); + } + + /** + * Adds a info level message. + * + * Interesting events. + * Example: User logs in, SQL logs. + * + * @see WC_Logger::log + * + * @param string $message Message to log. + * @param array $context Log context. + */ + public function info( $message, $context = array() ) { + $this->log( WC_Log_Levels::INFO, $message, $context ); + } + + /** + * Adds a debug level message. + * + * Detailed debug information. + * + * @see WC_Logger::log + * + * @param string $message Message to log. + * @param array $context Log context. + */ + public function debug( $message, $context = array() ) { + $this->log( WC_Log_Levels::DEBUG, $message, $context ); + } + + /** + * Clear entries for a chosen file/source. + * + * @param string $source Source/handle to clear. + * @return bool + */ + public function clear( $source = '' ) { + if ( ! $source ) { + return false; + } + foreach ( $this->handlers as $handler ) { + if ( is_callable( array( $handler, 'clear' ) ) ) { + $handler->clear( $source ); + } + } + return true; + } + + /** + * Clear all logs older than a defined number of days. Defaults to 30 days. + * + * @since 3.4.0 + */ + public function clear_expired_logs() { + $days = absint( apply_filters( 'woocommerce_logger_days_to_retain_logs', 30 ) ); + $timestamp = strtotime( "-{$days} days" ); + + foreach ( $this->handlers as $handler ) { + if ( is_callable( array( $handler, 'delete_logs_before_timestamp' ) ) ) { + $handler->delete_logs_before_timestamp( $timestamp ); + } + } + } +} diff --git a/plugins/woocommerce/includes/class-wc-meta-data.php b/plugins/woocommerce/includes/class-wc-meta-data.php new file mode 100644 index 00000000000..dd0e0c752f1 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-meta-data.php @@ -0,0 +1,120 @@ +current_data = $meta; + $this->apply_changes(); + } + + /** + * When converted to JSON. + * + * @return object|array + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() { + return $this->get_data(); + } + + /** + * Merge changes with data and clear. + */ + public function apply_changes() { + $this->data = $this->current_data; + } + + /** + * Creates or updates a property in the metadata object. + * + * @param string $key Key to set. + * @param mixed $value Value to set. + */ + public function __set( $key, $value ) { + $this->current_data[ $key ] = $value; + } + + /** + * Checks if a given key exists in our data. This is called internally + * by `empty` and `isset`. + * + * @param string $key Key to check if set. + * + * @return bool + */ + public function __isset( $key ) { + return array_key_exists( $key, $this->current_data ); + } + + /** + * Returns the value of any property. + * + * @param string $key Key to get. + * @return mixed Property value or NULL if it does not exists + */ + public function __get( $key ) { + if ( array_key_exists( $key, $this->current_data ) ) { + return $this->current_data[ $key ]; + } + return null; + } + + /** + * Return data changes only. + * + * @return array + */ + public function get_changes() { + $changes = array(); + foreach ( $this->current_data as $id => $value ) { + if ( ! array_key_exists( $id, $this->data ) || $value !== $this->data[ $id ] ) { + $changes[ $id ] = $value; + } + } + return $changes; + } + + /** + * Return all data as an array. + * + * @return array + */ + public function get_data() { + return $this->data; + } +} diff --git a/includes/class-wc-order-factory.php b/plugins/woocommerce/includes/class-wc-order-factory.php similarity index 100% rename from includes/class-wc-order-factory.php rename to plugins/woocommerce/includes/class-wc-order-factory.php diff --git a/plugins/woocommerce/includes/class-wc-order-item-coupon.php b/plugins/woocommerce/includes/class-wc-order-item-coupon.php new file mode 100644 index 00000000000..df54e312a68 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-order-item-coupon.php @@ -0,0 +1,185 @@ + '', + 'discount' => 0, + 'discount_tax' => 0, + ); + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set order item name. + * + * @param string $value Coupon code. + */ + public function set_name( $value ) { + return $this->set_code( $value ); + } + + /** + * Set code. + * + * @param string $value Coupon code. + */ + public function set_code( $value ) { + $this->set_prop( 'code', wc_format_coupon_code( $value ) ); + } + + /** + * Set discount amount. + * + * @param string $value Discount. + */ + public function set_discount( $value ) { + $this->set_prop( 'discount', wc_format_decimal( $value ) ); + } + + /** + * Set discounted tax amount. + * + * @param string $value Discount tax. + */ + public function set_discount_tax( $value ) { + $this->set_prop( 'discount_tax', wc_format_decimal( $value ) ); + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get order item type. + * + * @return string + */ + public function get_type() { + return 'coupon'; + } + + /** + * Get order item name. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_name( $context = 'view' ) { + return $this->get_code( $context ); + } + + /** + * Get coupon code. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_code( $context = 'view' ) { + return $this->get_prop( 'code', $context ); + } + + /** + * Get discount amount. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_discount( $context = 'view' ) { + return $this->get_prop( 'discount', $context ); + } + + /** + * Get discounted tax amount. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * + * @return string + */ + public function get_discount_tax( $context = 'view' ) { + return $this->get_prop( 'discount_tax', $context ); + } + + /* + |-------------------------------------------------------------------------- + | Array Access Methods + |-------------------------------------------------------------------------- + | + | For backwards compatibility with legacy arrays. + | + */ + + /** + * OffsetGet for ArrayAccess/Backwards compatibility. + * + * @deprecated 4.4.0 + * @param string $offset Offset. + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet( $offset ) { + wc_deprecated_function( 'WC_Order_Item_Coupon::offsetGet', '4.4.0', '' ); + if ( 'discount_amount' === $offset ) { + $offset = 'discount'; + } elseif ( 'discount_amount_tax' === $offset ) { + $offset = 'discount_tax'; + } + return parent::offsetGet( $offset ); + } + + /** + * OffsetSet for ArrayAccess/Backwards compatibility. + * + * @deprecated 4.4.0 + * @param string $offset Offset. + * @param mixed $value Value. + */ + #[\ReturnTypeWillChange] + public function offsetSet( $offset, $value ) { + wc_deprecated_function( 'WC_Order_Item_Coupon::offsetSet', '4.4.0', '' ); + if ( 'discount_amount' === $offset ) { + $offset = 'discount'; + } elseif ( 'discount_amount_tax' === $offset ) { + $offset = 'discount_tax'; + } + parent::offsetSet( $offset, $value ); + } + + /** + * OffsetExists for ArrayAccess. + * + * @param string $offset Offset. + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists( $offset ) { + if ( in_array( $offset, array( 'discount_amount', 'discount_amount_tax' ), true ) ) { + return true; + } + return parent::offsetExists( $offset ); + } +} diff --git a/plugins/woocommerce/includes/class-wc-order-item-fee.php b/plugins/woocommerce/includes/class-wc-order-item-fee.php new file mode 100644 index 00000000000..bb7cf4da838 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-order-item-fee.php @@ -0,0 +1,341 @@ + '', + 'tax_status' => 'taxable', + 'amount' => '', + 'total' => '', + 'total_tax' => '', + 'taxes' => array( + 'total' => array(), + ), + ); + + /** + * Get item costs grouped by tax class. + * + * @since 3.2.0 + * @param WC_Order $order Order object. + * @return array + */ + protected function get_tax_class_costs( $order ) { + $order_item_tax_classes = $order->get_items_tax_classes(); + $costs = array_fill_keys( $order_item_tax_classes, 0 ); + $costs['non-taxable'] = 0; + + foreach ( $order->get_items( array( 'line_item', 'fee', 'shipping' ) ) as $item ) { + if ( 0 > $item->get_total() ) { + continue; + } + if ( 'taxable' !== $item->get_tax_status() ) { + $costs['non-taxable'] += $item->get_total(); + } elseif ( 'inherit' === $item->get_tax_class() ) { + $inherit_class = reset( $order_item_tax_classes ); + $costs[ $inherit_class ] += $item->get_total(); + } else { + $costs[ $item->get_tax_class() ] += $item->get_total(); + } + } + + return array_filter( $costs ); + } + /** + * Calculate item taxes. + * + * @since 3.2.0 + * @param array $calculate_tax_for Location data to get taxes for. Required. + * @return bool True if taxes were calculated. + */ + public function calculate_taxes( $calculate_tax_for = array() ) { + if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'] ) ) { + return false; + } + // Use regular calculation unless the fee is negative. + if ( 0 <= $this->get_total() ) { + return parent::calculate_taxes( $calculate_tax_for ); + } + + if ( wc_tax_enabled() && $this->get_order() ) { + // Apportion taxes to order items, shipping, and fees. + $order = $this->get_order(); + $tax_class_costs = $this->get_tax_class_costs( $order ); + $total_costs = array_sum( $tax_class_costs ); + $discount_taxes = array(); + if ( $total_costs ) { + foreach ( $tax_class_costs as $tax_class => $tax_class_cost ) { + if ( 'non-taxable' === $tax_class ) { + continue; + } + $proportion = $tax_class_cost / $total_costs; + $cart_discount_proportion = $this->get_total() * $proportion; + $calculate_tax_for['tax_class'] = $tax_class; + $tax_rates = WC_Tax::find_rates( $calculate_tax_for ); + $discount_taxes = wc_array_merge_recursive_numeric( $discount_taxes, WC_Tax::calc_tax( $cart_discount_proportion, $tax_rates ) ); + } + } + $this->set_taxes( array( 'total' => $discount_taxes ) ); + } else { + $this->set_taxes( false ); + } + + do_action( 'woocommerce_order_item_fee_after_calculate_taxes', $this, $calculate_tax_for ); + + return true; + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set fee amount. + * + * @param string $value Amount. + */ + public function set_amount( $value ) { + $this->set_prop( 'amount', wc_format_decimal( $value ) ); + } + + /** + * Set tax class. + * + * @param string $value Tax class. + */ + public function set_tax_class( $value ) { + if ( $value && ! in_array( $value, WC_Tax::get_tax_class_slugs(), true ) ) { + $this->error( 'order_item_fee_invalid_tax_class', __( 'Invalid tax class', 'woocommerce' ) ); + } + $this->set_prop( 'tax_class', $value ); + } + + /** + * Set tax_status. + * + * @param string $value Tax status. + */ + public function set_tax_status( $value ) { + if ( in_array( $value, array( 'taxable', 'none' ), true ) ) { + $this->set_prop( 'tax_status', $value ); + } else { + $this->set_prop( 'tax_status', 'taxable' ); + } + } + + /** + * Set total. + * + * @param string $amount Fee amount (do not enter negative amounts). + */ + public function set_total( $amount ) { + $this->set_prop( 'total', wc_format_decimal( $amount ) ); + } + + /** + * Set total tax. + * + * @param string $amount Amount. + */ + public function set_total_tax( $amount ) { + $this->set_prop( 'total_tax', wc_format_decimal( $amount ) ); + } + + /** + * Set taxes. + * + * This is an array of tax ID keys with total amount values. + * + * @param array $raw_tax_data Raw tax data. + */ + public function set_taxes( $raw_tax_data ) { + $raw_tax_data = maybe_unserialize( $raw_tax_data ); + $tax_data = array( + 'total' => array(), + ); + if ( ! empty( $raw_tax_data['total'] ) ) { + $tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data['total'] ); + } + $this->set_prop( 'taxes', $tax_data ); + + if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { + $this->set_total_tax( array_sum( $tax_data['total'] ) ); + } else { + $this->set_total_tax( array_sum( array_map( 'wc_round_tax_total', $tax_data['total'] ) ) ); + } + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get fee amount. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_amount( $context = 'view' ) { + return $this->get_prop( 'amount', $context ); + } + + /** + * Get order item name. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_name( $context = 'view' ) { + $name = $this->get_prop( 'name', $context ); + if ( 'view' === $context ) { + return $name ? $name : __( 'Fee', 'woocommerce' ); + } else { + return $name; + } + } + + /** + * Get order item type. + * + * @return string + */ + public function get_type() { + return 'fee'; + } + + /** + * Get tax class. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_tax_class( $context = 'view' ) { + return $this->get_prop( 'tax_class', $context ); + } + + /** + * Get tax status. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_tax_status( $context = 'view' ) { + return $this->get_prop( 'tax_status', $context ); + } + + /** + * Get total fee. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_total( $context = 'view' ) { + return $this->get_prop( 'total', $context ); + } + + /** + * Get total tax. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_total_tax( $context = 'view' ) { + return $this->get_prop( 'total_tax', $context ); + } + + /** + * Get fee taxes. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return array + */ + public function get_taxes( $context = 'view' ) { + return $this->get_prop( 'taxes', $context ); + } + + /* + |-------------------------------------------------------------------------- + | Array Access Methods + |-------------------------------------------------------------------------- + | + | For backwards compatibility with legacy arrays. + | + */ + + /** + * OffsetGet for ArrayAccess/Backwards compatibility. + * + * @param string $offset Offset. + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet( $offset ) { + if ( 'line_total' === $offset ) { + $offset = 'total'; + } elseif ( 'line_tax' === $offset ) { + $offset = 'total_tax'; + } elseif ( 'line_tax_data' === $offset ) { + $offset = 'taxes'; + } + return parent::offsetGet( $offset ); + } + + /** + * OffsetSet for ArrayAccess/Backwards compatibility. + * + * @deprecated 4.4.0 + * @param string $offset Offset. + * @param mixed $value Value. + */ + #[\ReturnTypeWillChange] + public function offsetSet( $offset, $value ) { + wc_deprecated_function( 'WC_Order_Item_Fee::offsetSet', '4.4.0', '' ); + if ( 'line_total' === $offset ) { + $offset = 'total'; + } elseif ( 'line_tax' === $offset ) { + $offset = 'total_tax'; + } elseif ( 'line_tax_data' === $offset ) { + $offset = 'taxes'; + } + parent::offsetSet( $offset, $value ); + } + + /** + * OffsetExists for ArrayAccess + * + * @param string $offset Offset. + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists( $offset ) { + if ( in_array( $offset, array( 'line_total', 'line_tax', 'line_tax_data' ), true ) ) { + return true; + } + return parent::offsetExists( $offset ); + } +} diff --git a/plugins/woocommerce/includes/class-wc-order-item-meta.php b/plugins/woocommerce/includes/class-wc-order-item-meta.php new file mode 100644 index 00000000000..badc1ba7d01 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-order-item-meta.php @@ -0,0 +1,215 @@ +legacy = true; + $this->meta = array_filter( (array) $item ); + return; + } + $this->item = $item; + $this->meta = array_filter( (array) $item['item_meta'] ); + $this->product = $product; + } + + /** + * Display meta in a formatted list. + * + * @param bool $flat Flat (default: false). + * @param bool $return Return (default: false). + * @param string $hideprefix Hide prefix (default: _). + * @param string $delimiter Delimiter used to separate items when $flat is true. + * @return string|void + */ + public function display( $flat = false, $return = false, $hideprefix = '_', $delimiter = ", \n" ) { + $output = ''; + $formatted_meta = $this->get_formatted( $hideprefix ); + + if ( ! empty( $formatted_meta ) ) { + $meta_list = array(); + + foreach ( $formatted_meta as $meta ) { + if ( $flat ) { + $meta_list[] = wp_kses_post( $meta['label'] . ': ' . $meta['value'] ); + } else { + $meta_list[] = ' +
    ' . wp_kses_post( $meta['label'] ) . ':
    +
    ' . wp_kses_post( wpautop( make_clickable( $meta['value'] ) ) ) . '
    + '; + } + } + + if ( ! empty( $meta_list ) ) { + if ( $flat ) { + $output .= implode( $delimiter, $meta_list ); + } else { + $output .= '
    ' . implode( '', $meta_list ) . '
    '; + } + } + } + + $output = apply_filters( 'woocommerce_order_items_meta_display', $output, $this, $flat ); + + if ( $return ) { + return $output; + } else { + echo $output; // WPCS: XSS ok. + } + } + + /** + * Return an array of formatted item meta in format e.g. + * + * Returns: array( + * 'pa_size' => array( + * 'label' => 'Size', + * 'value' => 'Medium', + * ) + * ) + * + * @since 2.4 + * @param string $hideprefix exclude meta when key is prefixed with this, defaults to '_'. + * @return array + */ + public function get_formatted( $hideprefix = '_' ) { + if ( $this->legacy ) { + return $this->get_formatted_legacy( $hideprefix ); + } + + $formatted_meta = array(); + + if ( ! empty( $this->item['item_meta_array'] ) ) { + foreach ( $this->item['item_meta_array'] as $meta_id => $meta ) { + if ( '' === $meta->value || is_serialized( $meta->value ) || ( ! empty( $hideprefix ) && substr( $meta->key, 0, 1 ) === $hideprefix ) ) { + continue; + } + + $attribute_key = urldecode( str_replace( 'attribute_', '', $meta->key ) ); + $meta_value = $meta->value; + + // If this is a term slug, get the term's nice name. + if ( taxonomy_exists( $attribute_key ) ) { + $term = get_term_by( 'slug', $meta_value, $attribute_key ); + + if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) { + $meta_value = $term->name; + } + } + + $formatted_meta[ $meta_id ] = array( + 'key' => $meta->key, + 'label' => wc_attribute_label( $attribute_key, $this->product ), + 'value' => apply_filters( 'woocommerce_order_item_display_meta_value', $meta_value, $meta, $this->item ), + ); + } + } + + return apply_filters( 'woocommerce_order_items_meta_get_formatted', $formatted_meta, $this ); + } + + /** + * Return an array of formatted item meta in format e.g. + * Handles @deprecated args. + * + * @param string $hideprefix Hide prefix. + * + * @return array + */ + public function get_formatted_legacy( $hideprefix = '_' ) { + if ( ! wp_doing_ajax() ) { + wc_deprecated_argument( 'WC_Order_Item_Meta::get_formatted', '2.4', 'Item Meta Data is being called with legacy arguments' ); + } + + $formatted_meta = array(); + + foreach ( $this->meta as $meta_key => $meta_values ) { + if ( empty( $meta_values ) || ( ! empty( $hideprefix ) && substr( $meta_key, 0, 1 ) === $hideprefix ) ) { + continue; + } + foreach ( (array) $meta_values as $meta_value ) { + // Skip serialised meta. + if ( is_serialized( $meta_value ) ) { + continue; + } + + $attribute_key = urldecode( str_replace( 'attribute_', '', $meta_key ) ); + + // If this is a term slug, get the term's nice name. + if ( taxonomy_exists( $attribute_key ) ) { + $term = get_term_by( 'slug', $meta_value, $attribute_key ); + if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) { + $meta_value = $term->name; + } + } + + // Unique key required. + $formatted_meta_key = $meta_key; + $loop = 0; + while ( isset( $formatted_meta[ $formatted_meta_key ] ) ) { + $loop ++; + $formatted_meta_key = $meta_key . '-' . $loop; + } + + $formatted_meta[ $formatted_meta_key ] = array( + 'key' => $meta_key, + 'label' => wc_attribute_label( $attribute_key, $this->product ), + 'value' => apply_filters( 'woocommerce_order_item_display_meta_value', $meta_value, $this->meta, $this->item ), + ); + } + } + + return $formatted_meta; + } +} diff --git a/plugins/woocommerce/includes/class-wc-order-item-product.php b/plugins/woocommerce/includes/class-wc-order-item-product.php new file mode 100644 index 00000000000..0f7f82654eb --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-order-item-product.php @@ -0,0 +1,488 @@ + 0, + 'variation_id' => 0, + 'quantity' => 1, + 'tax_class' => '', + 'subtotal' => 0, + 'subtotal_tax' => 0, + 'total' => 0, + 'total_tax' => 0, + 'taxes' => array( + 'subtotal' => array(), + 'total' => array(), + ), + ); + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set quantity. + * + * @param int $value Quantity. + */ + public function set_quantity( $value ) { + $this->set_prop( 'quantity', wc_stock_amount( $value ) ); + } + + /** + * Set tax class. + * + * @param string $value Tax class. + */ + public function set_tax_class( $value ) { + if ( $value && ! in_array( $value, WC_Tax::get_tax_class_slugs(), true ) ) { + $this->error( 'order_item_product_invalid_tax_class', __( 'Invalid tax class', 'woocommerce' ) ); + } + $this->set_prop( 'tax_class', $value ); + } + + /** + * Set Product ID + * + * @param int $value Product ID. + */ + public function set_product_id( $value ) { + if ( $value > 0 && 'product' !== get_post_type( absint( $value ) ) ) { + $this->error( 'order_item_product_invalid_product_id', __( 'Invalid product ID', 'woocommerce' ) ); + } + $this->set_prop( 'product_id', absint( $value ) ); + } + + /** + * Set variation ID. + * + * @param int $value Variation ID. + */ + public function set_variation_id( $value ) { + if ( $value > 0 && 'product_variation' !== get_post_type( $value ) ) { + $this->error( 'order_item_product_invalid_variation_id', __( 'Invalid variation ID', 'woocommerce' ) ); + } + $this->set_prop( 'variation_id', absint( $value ) ); + } + + /** + * Line subtotal (before discounts). + * + * @param string $value Subtotal. + */ + public function set_subtotal( $value ) { + $value = wc_format_decimal( $value ); + + if ( ! is_numeric( $value ) ) { + $value = 0; + } + + $this->set_prop( 'subtotal', $value ); + } + + /** + * Line total (after discounts). + * + * @param string $value Total. + */ + public function set_total( $value ) { + $value = wc_format_decimal( $value ); + + if ( ! is_numeric( $value ) ) { + $value = 0; + } + + $this->set_prop( 'total', $value ); + + // Subtotal cannot be less than total. + if ( '' === $this->get_subtotal() || $this->get_subtotal() < $this->get_total() ) { + $this->set_subtotal( $value ); + } + } + + /** + * Line subtotal tax (before discounts). + * + * @param string $value Subtotal tax. + */ + public function set_subtotal_tax( $value ) { + $this->set_prop( 'subtotal_tax', wc_format_decimal( $value ) ); + } + + /** + * Line total tax (after discounts). + * + * @param string $value Total tax. + */ + public function set_total_tax( $value ) { + $this->set_prop( 'total_tax', wc_format_decimal( $value ) ); + } + + /** + * Set line taxes and totals for passed in taxes. + * + * @param array $raw_tax_data Raw tax data. + */ + public function set_taxes( $raw_tax_data ) { + $raw_tax_data = maybe_unserialize( $raw_tax_data ); + $tax_data = array( + 'total' => array(), + 'subtotal' => array(), + ); + if ( ! empty( $raw_tax_data['total'] ) && ! empty( $raw_tax_data['subtotal'] ) ) { + $tax_data['subtotal'] = array_map( 'wc_format_decimal', $raw_tax_data['subtotal'] ); + $tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data['total'] ); + + // Subtotal cannot be less than total! + if ( array_sum( $tax_data['subtotal'] ) < array_sum( $tax_data['total'] ) ) { + $tax_data['subtotal'] = $tax_data['total']; + } + } + $this->set_prop( 'taxes', $tax_data ); + + if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { + $this->set_total_tax( array_sum( $tax_data['total'] ) ); + $this->set_subtotal_tax( array_sum( $tax_data['subtotal'] ) ); + } else { + $this->set_total_tax( array_sum( array_map( 'wc_round_tax_total', $tax_data['total'] ) ) ); + $this->set_subtotal_tax( array_sum( array_map( 'wc_round_tax_total', $tax_data['subtotal'] ) ) ); + } + } + + /** + * Set variation data (stored as meta data - write only). + * + * @param array $data Key/Value pairs. + */ + public function set_variation( $data = array() ) { + if ( is_array( $data ) ) { + foreach ( $data as $key => $value ) { + $this->add_meta_data( str_replace( 'attribute_', '', $key ), $value, true ); + } + } + } + + /** + * Set properties based on passed in product object. + * + * @param WC_Product $product Product instance. + */ + public function set_product( $product ) { + if ( ! is_a( $product, 'WC_Product' ) ) { + $this->error( 'order_item_product_invalid_product', __( 'Invalid product', 'woocommerce' ) ); + } + if ( $product->is_type( 'variation' ) ) { + $this->set_product_id( $product->get_parent_id() ); + $this->set_variation_id( $product->get_id() ); + $this->set_variation( is_callable( array( $product, 'get_variation_attributes' ) ) ? $product->get_variation_attributes() : array() ); + } else { + $this->set_product_id( $product->get_id() ); + } + $this->set_name( $product->get_name() ); + $this->set_tax_class( $product->get_tax_class() ); + } + + /** + * Set meta data for backordered products. + */ + public function set_backorder_meta() { + $product = $this->get_product(); + if ( $product && $product->backorders_require_notification() && $product->is_on_backorder( $this->get_quantity() ) ) { + $this->add_meta_data( apply_filters( 'woocommerce_backordered_item_meta_name', __( 'Backordered', 'woocommerce' ), $this ), $this->get_quantity() - max( 0, $product->get_stock_quantity() ), true ); + } + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get order item type. + * + * @return string + */ + public function get_type() { + return 'line_item'; + } + + /** + * Get product ID. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return int + */ + public function get_product_id( $context = 'view' ) { + return $this->get_prop( 'product_id', $context ); + } + + /** + * Get variation ID. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return int + */ + public function get_variation_id( $context = 'view' ) { + return $this->get_prop( 'variation_id', $context ); + } + + /** + * Get quantity. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return int + */ + public function get_quantity( $context = 'view' ) { + return $this->get_prop( 'quantity', $context ); + } + + /** + * Get tax class. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_tax_class( $context = 'view' ) { + return $this->get_prop( 'tax_class', $context ); + } + + /** + * Get subtotal. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_subtotal( $context = 'view' ) { + return $this->get_prop( 'subtotal', $context ); + } + + /** + * Get subtotal tax. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_subtotal_tax( $context = 'view' ) { + return $this->get_prop( 'subtotal_tax', $context ); + } + + /** + * Get total. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_total( $context = 'view' ) { + return $this->get_prop( 'total', $context ); + } + + /** + * Get total tax. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_total_tax( $context = 'view' ) { + return $this->get_prop( 'total_tax', $context ); + } + + /** + * Get taxes. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return array + */ + public function get_taxes( $context = 'view' ) { + return $this->get_prop( 'taxes', $context ); + } + + /** + * Get the associated product. + * + * @return WC_Product|bool + */ + public function get_product() { + if ( $this->get_variation_id() ) { + $product = wc_get_product( $this->get_variation_id() ); + } else { + $product = wc_get_product( $this->get_product_id() ); + } + + // Backwards compatible filter from WC_Order::get_product_from_item(). + if ( has_filter( 'woocommerce_get_product_from_item' ) ) { + $product = apply_filters( 'woocommerce_get_product_from_item', $product, $this, $this->get_order() ); + } + + return apply_filters( 'woocommerce_order_item_product', $product, $this ); + } + + /** + * Get the Download URL. + * + * @param int $download_id Download ID. + * @return string + */ + public function get_item_download_url( $download_id ) { + $order = $this->get_order(); + + return $order ? add_query_arg( + array( + 'download_file' => $this->get_variation_id() ? $this->get_variation_id() : $this->get_product_id(), + 'order' => $order->get_order_key(), + 'email' => rawurlencode( $order->get_billing_email() ), + 'key' => $download_id, + ), + trailingslashit( home_url() ) + ) : ''; + } + + /** + * Get any associated downloadable files. + * + * @return array + */ + public function get_item_downloads() { + $files = array(); + $product = $this->get_product(); + $order = $this->get_order(); + $product_id = $this->get_variation_id() ? $this->get_variation_id() : $this->get_product_id(); + + if ( $product && $order && $product->is_downloadable() && $order->is_download_permitted() ) { + $email_hash = function_exists( 'hash' ) ? hash( 'sha256', $order->get_billing_email() ) : sha1( $order->get_billing_email() ); + $data_store = WC_Data_Store::load( 'customer-download' ); + $customer_downloads = $data_store->get_downloads( + array( + 'user_email' => $order->get_billing_email(), + 'order_id' => $order->get_id(), + 'product_id' => $product_id, + ) + ); + foreach ( $customer_downloads as $customer_download ) { + $download_id = $customer_download->get_download_id(); + + if ( $product->has_file( $download_id ) ) { + $file = $product->get_file( $download_id ); + $files[ $download_id ] = $file->get_data(); + $files[ $download_id ]['downloads_remaining'] = $customer_download->get_downloads_remaining(); + $files[ $download_id ]['access_expires'] = $customer_download->get_access_expires(); + $files[ $download_id ]['download_url'] = add_query_arg( + array( + 'download_file' => $product_id, + 'order' => $order->get_order_key(), + 'uid' => $email_hash, + 'key' => $download_id, + ), + trailingslashit( home_url() ) + ); + } + } + } + + return apply_filters( 'woocommerce_get_item_downloads', $files, $this, $order ); + } + + /** + * Get tax status. + * + * @return string + */ + public function get_tax_status() { + $product = $this->get_product(); + return $product ? $product->get_tax_status() : 'taxable'; + } + + /* + |-------------------------------------------------------------------------- + | Array Access Methods + |-------------------------------------------------------------------------- + | + | For backwards compatibility with legacy arrays. + | + */ + + /** + * OffsetGet for ArrayAccess/Backwards compatibility. + * + * @param string $offset Offset. + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet( $offset ) { + if ( 'line_subtotal' === $offset ) { + $offset = 'subtotal'; + } elseif ( 'line_subtotal_tax' === $offset ) { + $offset = 'subtotal_tax'; + } elseif ( 'line_total' === $offset ) { + $offset = 'total'; + } elseif ( 'line_tax' === $offset ) { + $offset = 'total_tax'; + } elseif ( 'line_tax_data' === $offset ) { + $offset = 'taxes'; + } elseif ( 'qty' === $offset ) { + $offset = 'quantity'; + } + return parent::offsetGet( $offset ); + } + + /** + * OffsetSet for ArrayAccess/Backwards compatibility. + * + * @deprecated 4.4.0 + * @param string $offset Offset. + * @param mixed $value Value. + */ + #[\ReturnTypeWillChange] + public function offsetSet( $offset, $value ) { + wc_deprecated_function( 'WC_Order_Item_Product::offsetSet', '4.4.0', '' ); + if ( 'line_subtotal' === $offset ) { + $offset = 'subtotal'; + } elseif ( 'line_subtotal_tax' === $offset ) { + $offset = 'subtotal_tax'; + } elseif ( 'line_total' === $offset ) { + $offset = 'total'; + } elseif ( 'line_tax' === $offset ) { + $offset = 'total_tax'; + } elseif ( 'line_tax_data' === $offset ) { + $offset = 'taxes'; + } elseif ( 'qty' === $offset ) { + $offset = 'quantity'; + } + parent::offsetSet( $offset, $value ); + } + + /** + * OffsetExists for ArrayAccess. + * + * @param string $offset Offset. + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists( $offset ) { + if ( in_array( $offset, array( 'line_subtotal', 'line_subtotal_tax', 'line_total', 'line_tax', 'line_tax_data', 'item_meta_array', 'item_meta', 'qty' ), true ) ) { + return true; + } + return parent::offsetExists( $offset ); + } +} diff --git a/plugins/woocommerce/includes/class-wc-order-item-shipping.php b/plugins/woocommerce/includes/class-wc-order-item-shipping.php new file mode 100644 index 00000000000..f2da5c1eea1 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-order-item-shipping.php @@ -0,0 +1,319 @@ + '', + 'method_id' => '', + 'instance_id' => '', + 'total' => 0, + 'total_tax' => 0, + 'taxes' => array( + 'total' => array(), + ), + ); + + /** + * Calculate item taxes. + * + * @since 3.2.0 + * @param array $calculate_tax_for Location data to get taxes for. Required. + * @return bool True if taxes were calculated. + */ + public function calculate_taxes( $calculate_tax_for = array() ) { + if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'], $calculate_tax_for['tax_class'] ) ) { + return false; + } + if ( wc_tax_enabled() ) { + $tax_rates = WC_Tax::find_shipping_rates( $calculate_tax_for ); + $taxes = WC_Tax::calc_tax( $this->get_total(), $tax_rates, false ); + $this->set_taxes( array( 'total' => $taxes ) ); + } else { + $this->set_taxes( false ); + } + + do_action( 'woocommerce_order_item_shipping_after_calculate_taxes', $this, $calculate_tax_for ); + + return true; + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set order item name. + * + * @param string $value Value to set. + * @throws WC_Data_Exception May throw exception if data is invalid. + */ + public function set_name( $value ) { + $this->set_method_title( $value ); + } + + /** + * Set method title. + * + * @param string $value Value to set. + * @throws WC_Data_Exception May throw exception if data is invalid. + */ + public function set_method_title( $value ) { + $this->set_prop( 'name', wc_clean( $value ) ); + $this->set_prop( 'method_title', wc_clean( $value ) ); + } + + /** + * Set shipping method id. + * + * @param string $value Value to set. + * @throws WC_Data_Exception May throw exception if data is invalid. + */ + public function set_method_id( $value ) { + $this->set_prop( 'method_id', wc_clean( $value ) ); + } + + /** + * Set shipping instance id. + * + * @param string $value Value to set. + * @throws WC_Data_Exception May throw exception if data is invalid. + */ + public function set_instance_id( $value ) { + $this->set_prop( 'instance_id', wc_clean( $value ) ); + } + + /** + * Set total. + * + * @param string $value Value to set. + * @throws WC_Data_Exception May throw exception if data is invalid. + */ + public function set_total( $value ) { + $this->set_prop( 'total', wc_format_decimal( $value ) ); + } + + /** + * Set total tax. + * + * @param string $value Value to set. + * @throws WC_Data_Exception May throw exception if data is invalid. + */ + protected function set_total_tax( $value ) { + $this->set_prop( 'total_tax', wc_format_decimal( $value ) ); + } + + /** + * Set taxes. + * + * This is an array of tax ID keys with total amount values. + * + * @param array $raw_tax_data Value to set. + * @throws WC_Data_Exception May throw exception if data is invalid. + */ + public function set_taxes( $raw_tax_data ) { + $raw_tax_data = maybe_unserialize( $raw_tax_data ); + $tax_data = array( + 'total' => array(), + ); + if ( isset( $raw_tax_data['total'] ) ) { + $tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data['total'] ); + } elseif ( ! empty( $raw_tax_data ) && is_array( $raw_tax_data ) ) { + // Older versions just used an array. + $tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data ); + } + $this->set_prop( 'taxes', $tax_data ); + + if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { + $this->set_total_tax( array_sum( $tax_data['total'] ) ); + } else { + $this->set_total_tax( array_sum( array_map( 'wc_round_tax_total', $tax_data['total'] ) ) ); + } + } + + /** + * Set properties based on passed in shipping rate object. + * + * @param WC_Shipping_Rate $shipping_rate Shipping rate to set. + */ + public function set_shipping_rate( $shipping_rate ) { + $this->set_method_title( $shipping_rate->get_label() ); + $this->set_method_id( $shipping_rate->get_method_id() ); + $this->set_instance_id( $shipping_rate->get_instance_id() ); + $this->set_total( $shipping_rate->get_cost() ); + $this->set_taxes( $shipping_rate->get_taxes() ); + $this->set_meta_data( $shipping_rate->get_meta_data() ); + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get order item type. + * + * @return string + */ + public function get_type() { + return 'shipping'; + } + + /** + * Get order item name. + * + * @param string $context View or edit context. + * @return string + */ + public function get_name( $context = 'view' ) { + return $this->get_method_title( $context ); + } + + /** + * Get title. + * + * @param string $context View or edit context. + * @return string + */ + public function get_method_title( $context = 'view' ) { + $method_title = $this->get_prop( 'method_title', $context ); + if ( 'view' === $context ) { + return $method_title ? $method_title : __( 'Shipping', 'woocommerce' ); + } else { + return $method_title; + } + } + + /** + * Get method ID. + * + * @param string $context View or edit context. + * @return string + */ + public function get_method_id( $context = 'view' ) { + return $this->get_prop( 'method_id', $context ); + } + + /** + * Get instance ID. + * + * @param string $context View or edit context. + * @return string + */ + public function get_instance_id( $context = 'view' ) { + return $this->get_prop( 'instance_id', $context ); + } + + /** + * Get total cost. + * + * @param string $context View or edit context. + * @return string + */ + public function get_total( $context = 'view' ) { + return $this->get_prop( 'total', $context ); + } + + /** + * Get total tax. + * + * @param string $context View or edit context. + * @return string + */ + public function get_total_tax( $context = 'view' ) { + return $this->get_prop( 'total_tax', $context ); + } + + /** + * Get taxes. + * + * @param string $context View or edit context. + * @return array + */ + public function get_taxes( $context = 'view' ) { + return $this->get_prop( 'taxes', $context ); + } + + /** + * Get tax class. + * + * @param string $context View or edit context. + * @return string + */ + public function get_tax_class( $context = 'view' ) { + return get_option( 'woocommerce_shipping_tax_class' ); + } + + /* + |-------------------------------------------------------------------------- + | Array Access Methods + |-------------------------------------------------------------------------- + | + | For backwards compatibility with legacy arrays. + | + */ + + /** + * Offset get: for ArrayAccess/Backwards compatibility. + * + * @param string $offset Key. + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet( $offset ) { + if ( 'cost' === $offset ) { + $offset = 'total'; + } + return parent::offsetGet( $offset ); + } + + /** + * Offset set: for ArrayAccess/Backwards compatibility. + * + * @deprecated 4.4.0 + * @param string $offset Key. + * @param mixed $value Value to set. + */ + #[\ReturnTypeWillChange] + public function offsetSet( $offset, $value ) { + wc_deprecated_function( 'WC_Order_Item_Shipping::offsetSet', '4.4.0', '' ); + if ( 'cost' === $offset ) { + $offset = 'total'; + } + parent::offsetSet( $offset, $value ); + } + + /** + * Offset exists: for ArrayAccess. + * + * @param string $offset Key. + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists( $offset ) { + if ( in_array( $offset, array( 'cost' ), true ) ) { + return true; + } + return parent::offsetExists( $offset ); + } +} diff --git a/plugins/woocommerce/includes/class-wc-order-item-tax.php b/plugins/woocommerce/includes/class-wc-order-item-tax.php new file mode 100644 index 00000000000..4248839d93a --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-order-item-tax.php @@ -0,0 +1,291 @@ + '', + 'rate_id' => 0, + 'label' => '', + 'compound' => false, + 'tax_total' => 0, + 'shipping_tax_total' => 0, + 'rate_percent' => null, + ); + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set order item name. + * + * @param string $value Name. + */ + public function set_name( $value ) { + $this->set_rate_code( $value ); + } + + /** + * Set item name. + * + * @param string $value Rate code. + */ + public function set_rate_code( $value ) { + $this->set_prop( 'rate_code', wc_clean( $value ) ); + } + + /** + * Set item name. + * + * @param string $value Label. + */ + public function set_label( $value ) { + $this->set_prop( 'label', wc_clean( $value ) ); + } + + /** + * Set tax rate id. + * + * @param int $value Rate ID. + */ + public function set_rate_id( $value ) { + $this->set_prop( 'rate_id', absint( $value ) ); + } + + /** + * Set tax total. + * + * @param string $value Tax total. + */ + public function set_tax_total( $value ) { + $this->set_prop( 'tax_total', $value ? wc_format_decimal( $value ) : 0 ); + } + + /** + * Set shipping tax total. + * + * @param string $value Shipping tax total. + */ + public function set_shipping_tax_total( $value ) { + $this->set_prop( 'shipping_tax_total', $value ? wc_format_decimal( $value ) : 0 ); + } + + /** + * Set compound. + * + * @param bool $value If tax is compound. + */ + public function set_compound( $value ) { + $this->set_prop( 'compound', (bool) $value ); + } + + /** + * Set rate value. + * + * @param float $value tax rate value. + */ + public function set_rate_percent( $value ) { + $this->set_prop( 'rate_percent', (float) $value ); + } + + /** + * Set properties based on passed in tax rate by ID. + * + * @param int $tax_rate_id Tax rate ID. + */ + public function set_rate( $tax_rate_id ) { + $tax_rate = WC_Tax::_get_tax_rate( $tax_rate_id, OBJECT ); + + $this->set_rate_id( $tax_rate_id ); + $this->set_rate_code( WC_Tax::get_rate_code( $tax_rate ) ); + $this->set_label( WC_Tax::get_rate_label( $tax_rate ) ); + $this->set_compound( WC_Tax::is_compound( $tax_rate ) ); + $this->set_rate_percent( WC_Tax::get_rate_percent_value( $tax_rate ) ); + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get order item type. + * + * @return string + */ + public function get_type() { + return 'tax'; + } + + /** + * Get rate code/name. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_name( $context = 'view' ) { + return $this->get_rate_code( $context ); + } + + /** + * Get rate code/name. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_rate_code( $context = 'view' ) { + return $this->get_prop( 'rate_code', $context ); + } + + /** + * Get label. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_label( $context = 'view' ) { + $label = $this->get_prop( 'label', $context ); + if ( 'view' === $context ) { + return $label ? $label : __( 'Tax', 'woocommerce' ); + } else { + return $label; + } + } + + /** + * Get tax rate ID. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return int + */ + public function get_rate_id( $context = 'view' ) { + return $this->get_prop( 'rate_id', $context ); + } + + /** + * Get tax_total + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_tax_total( $context = 'view' ) { + return $this->get_prop( 'tax_total', $context ); + } + + /** + * Get shipping_tax_total + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_tax_total( $context = 'view' ) { + return $this->get_prop( 'shipping_tax_total', $context ); + } + + /** + * Get compound. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return bool + */ + public function get_compound( $context = 'view' ) { + return $this->get_prop( 'compound', $context ); + } + + /** + * Is this a compound tax rate? + * + * @return boolean + */ + public function is_compound() { + return $this->get_compound(); + } + + /** + * Get rate value + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return float + */ + public function get_rate_percent( $context = 'view' ) { + return $this->get_prop( 'rate_percent', $context ); + } + + /* + |-------------------------------------------------------------------------- + | Array Access Methods + |-------------------------------------------------------------------------- + | + | For backwards compatibility with legacy arrays. + | + */ + + /** + * O for ArrayAccess/Backwards compatibility. + * + * @param string $offset Offset. + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet( $offset ) { + if ( 'tax_amount' === $offset ) { + $offset = 'tax_total'; + } elseif ( 'shipping_tax_amount' === $offset ) { + $offset = 'shipping_tax_total'; + } + return parent::offsetGet( $offset ); + } + + /** + * OffsetSet for ArrayAccess/Backwards compatibility. + * + * @deprecated 4.4.0 + * @param string $offset Offset. + * @param mixed $value Value. + */ + #[\ReturnTypeWillChange] + public function offsetSet( $offset, $value ) { + wc_deprecated_function( 'WC_Order_Item_Tax::offsetSet', '4.4.0', '' ); + if ( 'tax_amount' === $offset ) { + $offset = 'tax_total'; + } elseif ( 'shipping_tax_amount' === $offset ) { + $offset = 'shipping_tax_total'; + } + parent::offsetSet( $offset, $value ); + } + + /** + * OffsetExists for ArrayAccess. + * + * @param string $offset Offset. + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists( $offset ) { + if ( in_array( $offset, array( 'tax_amount', 'shipping_tax_amount' ), true ) ) { + return true; + } + return parent::offsetExists( $offset ); + } +} diff --git a/plugins/woocommerce/includes/class-wc-order-item.php b/plugins/woocommerce/includes/class-wc-order-item.php new file mode 100644 index 00000000000..6900685e19e --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-order-item.php @@ -0,0 +1,424 @@ + 0, + 'name' => '', + ); + + /** + * Stores meta in cache for future reads. + * A group must be set to to enable caching. + * + * @var string + */ + protected $cache_group = 'order-items'; + + /** + * Meta type. This should match up with + * the types available at https://developer.wordpress.org/reference/functions/add_metadata/. + * WP defines 'post', 'user', 'comment', and 'term'. + * + * @var string + */ + protected $meta_type = 'order_item'; + + /** + * This is the name of this object type. + * + * @var string + */ + protected $object_type = 'order_item'; + + /** + * Constructor. + * + * @param int|object|array $item ID to load from the DB, or WC_Order_Item object. + */ + public function __construct( $item = 0 ) { + parent::__construct( $item ); + + if ( $item instanceof WC_Order_Item ) { + $this->set_id( $item->get_id() ); + } elseif ( is_numeric( $item ) && $item > 0 ) { + $this->set_id( $item ); + } else { + $this->set_object_read( true ); + } + + $type = 'line_item' === $this->get_type() ? 'product' : $this->get_type(); + $this->data_store = WC_Data_Store::load( 'order-item-' . $type ); + if ( $this->get_id() > 0 ) { + $this->data_store->read( $this ); + } + } + + /** + * Merge changes with data and clear. + * Overrides WC_Data::apply_changes. + * array_replace_recursive does not work well for order items because it merges taxes instead + * of replacing them. + * + * @since 3.2.0 + */ + public function apply_changes() { + if ( function_exists( 'array_replace' ) ) { + $this->data = array_replace( $this->data, $this->changes ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.array_replaceFound + } else { // PHP 5.2 compatibility. + foreach ( $this->changes as $key => $change ) { + $this->data[ $key ] = $change; + } + } + $this->changes = array(); + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get order ID this meta belongs to. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return int + */ + public function get_order_id( $context = 'view' ) { + return $this->get_prop( 'order_id', $context ); + } + + /** + * Get order item name. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_name( $context = 'view' ) { + return $this->get_prop( 'name', $context ); + } + + /** + * Get order item type. Overridden by child classes. + * + * @return string + */ + public function get_type() { + return ''; + } + + /** + * Get quantity. + * + * @return int + */ + public function get_quantity() { + return 1; + } + + /** + * Get tax status. + * + * @return string + */ + public function get_tax_status() { + return 'taxable'; + } + + /** + * Get tax class. + * + * @return string + */ + public function get_tax_class() { + return ''; + } + + /** + * Get parent order object. + * + * @return WC_Order + */ + public function get_order() { + return wc_get_order( $this->get_order_id() ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set order ID. + * + * @param int $value Order ID. + */ + public function set_order_id( $value ) { + $this->set_prop( 'order_id', absint( $value ) ); + } + + /** + * Set order item name. + * + * @param string $value Item name. + */ + public function set_name( $value ) { + $this->set_prop( 'name', wp_check_invalid_utf8( $value ) ); + } + + /* + |-------------------------------------------------------------------------- + | Other Methods + |-------------------------------------------------------------------------- + */ + + /** + * Type checking. + * + * @param string|array $type Type. + * @return boolean + */ + public function is_type( $type ) { + return is_array( $type ) ? in_array( $this->get_type(), $type, true ) : $type === $this->get_type(); + } + + /** + * Calculate item taxes. + * + * @since 3.2.0 + * @param array $calculate_tax_for Location data to get taxes for. Required. + * @return bool True if taxes were calculated. + */ + public function calculate_taxes( $calculate_tax_for = array() ) { + if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'] ) ) { + return false; + } + if ( '0' !== $this->get_tax_class() && 'taxable' === $this->get_tax_status() && wc_tax_enabled() ) { + $calculate_tax_for['tax_class'] = $this->get_tax_class(); + $tax_rates = WC_Tax::find_rates( $calculate_tax_for ); + $taxes = WC_Tax::calc_tax( $this->get_total(), $tax_rates, false ); + + if ( method_exists( $this, 'get_subtotal' ) ) { + $subtotal_taxes = WC_Tax::calc_tax( $this->get_subtotal(), $tax_rates, false ); + $this->set_taxes( + array( + 'total' => $taxes, + 'subtotal' => $subtotal_taxes, + ) + ); + } else { + $this->set_taxes( array( 'total' => $taxes ) ); + } + } else { + $this->set_taxes( false ); + } + + do_action( 'woocommerce_order_item_after_calculate_taxes', $this, $calculate_tax_for ); + + return true; + } + + /* + |-------------------------------------------------------------------------- + | Meta Data Handling + |-------------------------------------------------------------------------- + */ + + /** + * Wrapper for get_formatted_meta_data that includes all metadata by default. See https://github.com/woocommerce/woocommerce/pull/30948 + * + * @param string $hideprefix Meta data prefix, (default: _). + * @param bool $include_all Include all meta data, this stop skip items with values already in the product name. + * @return array + */ + public function get_all_formatted_meta_data( $hideprefix = '_', $include_all = true ) { + return $this->get_formatted_meta_data( $hideprefix, $include_all ); + } + + /** + * Expands things like term slugs before return. + * + * @param string $hideprefix Meta data prefix, (default: _). + * @param bool $include_all Include all meta data, this stop skip items with values already in the product name. + * @return array + */ + public function get_formatted_meta_data( $hideprefix = '_', $include_all = false ) { + $formatted_meta = array(); + $meta_data = $this->get_meta_data(); + $hideprefix_length = ! empty( $hideprefix ) ? strlen( $hideprefix ) : 0; + $product = is_callable( array( $this, 'get_product' ) ) ? $this->get_product() : false; + $order_item_name = $this->get_name(); + + foreach ( $meta_data as $meta ) { + if ( empty( $meta->id ) || '' === $meta->value || ! is_scalar( $meta->value ) || ( $hideprefix_length && substr( $meta->key, 0, $hideprefix_length ) === $hideprefix ) ) { + continue; + } + + $meta->key = rawurldecode( (string) $meta->key ); + $meta->value = rawurldecode( (string) $meta->value ); + $attribute_key = str_replace( 'attribute_', '', $meta->key ); + $display_key = wc_attribute_label( $attribute_key, $product ); + $display_value = wp_kses_post( $meta->value ); + + if ( taxonomy_exists( $attribute_key ) ) { + $term = get_term_by( 'slug', $meta->value, $attribute_key ); + if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) { + $display_value = $term->name; + } + } + + // Skip items with values already in the product details area of the product name. + if ( ! $include_all && $product && $product->is_type( 'variation' ) && wc_is_attribute_in_product_name( $display_value, $order_item_name ) ) { + continue; + } + + $formatted_meta[ $meta->id ] = (object) array( + 'key' => $meta->key, + 'value' => $meta->value, + 'display_key' => apply_filters( 'woocommerce_order_item_display_meta_key', $display_key, $meta, $this ), + 'display_value' => wpautop( make_clickable( apply_filters( 'woocommerce_order_item_display_meta_value', $display_value, $meta, $this ) ) ), + ); + } + + return apply_filters( 'woocommerce_order_item_get_formatted_meta_data', $formatted_meta, $this ); + } + + /* + |-------------------------------------------------------------------------- + | Array Access Methods + |-------------------------------------------------------------------------- + | + | For backwards compatibility with legacy arrays. + | + */ + + /** + * OffsetSet for ArrayAccess. + * + * @param string $offset Offset. + * @param mixed $value Value. + */ + #[\ReturnTypeWillChange] + public function offsetSet( $offset, $value ) { + if ( 'item_meta_array' === $offset ) { + foreach ( $value as $meta_id => $meta ) { + $this->update_meta_data( $meta->key, $meta->value, $meta_id ); + } + return; + } + + if ( array_key_exists( $offset, $this->data ) ) { + $setter = "set_$offset"; + if ( is_callable( array( $this, $setter ) ) ) { + $this->$setter( $value ); + } + return; + } + + $this->update_meta_data( $offset, $value ); + } + + /** + * OffsetUnset for ArrayAccess. + * + * @param string $offset Offset. + */ + #[\ReturnTypeWillChange] + public function offsetUnset( $offset ) { + $this->maybe_read_meta_data(); + + if ( 'item_meta_array' === $offset || 'item_meta' === $offset ) { + $this->meta_data = array(); + return; + } + + if ( array_key_exists( $offset, $this->data ) ) { + unset( $this->data[ $offset ] ); + } + + if ( array_key_exists( $offset, $this->changes ) ) { + unset( $this->changes[ $offset ] ); + } + + $this->delete_meta_data( $offset ); + } + + /** + * OffsetExists for ArrayAccess. + * + * @param string $offset Offset. + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists( $offset ) { + $this->maybe_read_meta_data(); + if ( 'item_meta_array' === $offset || 'item_meta' === $offset || array_key_exists( $offset, $this->data ) ) { + return true; + } + return array_key_exists( $offset, wp_list_pluck( $this->meta_data, 'value', 'key' ) ) || array_key_exists( '_' . $offset, wp_list_pluck( $this->meta_data, 'value', 'key' ) ); + } + + /** + * OffsetGet for ArrayAccess. + * + * @param string $offset Offset. + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet( $offset ) { + $this->maybe_read_meta_data(); + + if ( 'item_meta_array' === $offset ) { + $return = array(); + + foreach ( $this->meta_data as $meta ) { + $return[ $meta->id ] = $meta; + } + + return $return; + } + + $meta_values = wp_list_pluck( $this->meta_data, 'value', 'key' ); + + if ( 'item_meta' === $offset ) { + return $meta_values; + } elseif ( 'type' === $offset ) { + return $this->get_type(); + } elseif ( array_key_exists( $offset, $this->data ) ) { + $getter = "get_$offset"; + if ( is_callable( array( $this, $getter ) ) ) { + return $this->$getter(); + } + } elseif ( array_key_exists( '_' . $offset, $meta_values ) ) { + // Item meta was expanded in previous versions, with prefixes removed. This maintains support. + return $meta_values[ '_' . $offset ]; + } elseif ( array_key_exists( $offset, $meta_values ) ) { + return $meta_values[ $offset ]; + } + + return null; + } +} diff --git a/plugins/woocommerce/includes/class-wc-order-query.php b/plugins/woocommerce/includes/class-wc-order-query.php new file mode 100644 index 00000000000..bbad22a57d4 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-order-query.php @@ -0,0 +1,90 @@ + array_keys( wc_get_order_statuses() ), + 'type' => wc_get_order_types( 'view-orders' ), + 'currency' => '', + 'version' => '', + 'prices_include_tax' => '', + 'date_created' => '', + 'date_modified' => '', + 'date_completed' => '', + 'date_paid' => '', + 'discount_total' => '', + 'discount_tax' => '', + 'shipping_total' => '', + 'shipping_tax' => '', + 'cart_tax' => '', + 'total' => '', + 'total_tax' => '', + 'customer' => '', + 'customer_id' => '', + 'order_key' => '', + 'billing_first_name' => '', + 'billing_last_name' => '', + 'billing_company' => '', + 'billing_address_1' => '', + 'billing_address_2' => '', + 'billing_city' => '', + 'billing_state' => '', + 'billing_postcode' => '', + 'billing_country' => '', + 'billing_email' => '', + 'billing_phone' => '', + 'shipping_first_name' => '', + 'shipping_last_name' => '', + 'shipping_company' => '', + 'shipping_address_1' => '', + 'shipping_address_2' => '', + 'shipping_city' => '', + 'shipping_state' => '', + 'shipping_postcode' => '', + 'shipping_country' => '', + 'shipping_phone' => '', + 'payment_method' => '', + 'payment_method_title' => '', + 'transaction_id' => '', + 'customer_ip_address' => '', + 'customer_user_agent' => '', + 'created_via' => '', + 'customer_note' => '', + ) + ); + } + + /** + * Get orders matching the current query vars. + * + * @return array|object of WC_Order objects + * + * @throws Exception When WC_Data_Store validation fails. + */ + public function get_orders() { + $args = apply_filters( 'woocommerce_order_query_args', $this->get_query_vars() ); + $results = WC_Data_Store::load( 'order' )->query( $args ); + return apply_filters( 'woocommerce_order_query', $results, $args ); + } +} diff --git a/plugins/woocommerce/includes/class-wc-order-refund.php b/plugins/woocommerce/includes/class-wc-order-refund.php new file mode 100644 index 00000000000..b24ccc60619 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-order-refund.php @@ -0,0 +1,228 @@ + '', + 'reason' => '', + 'refunded_by' => 0, + 'refunded_payment' => false, + ); + + /** + * Get internal type (post type.) + * + * @return string + */ + public function get_type() { + return 'shop_order_refund'; + } + + /** + * Get status - always completed for refunds. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_status( $context = 'view' ) { + return 'completed'; + } + + /** + * Get a title for the new post type. + */ + public function get_post_title() { + // @codingStandardsIgnoreStart + return sprintf( __( 'Refund – %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Order date parsed by strftime', 'woocommerce' ) ) ); + // @codingStandardsIgnoreEnd + } + + /** + * Get refunded amount. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return int|float + */ + public function get_amount( $context = 'view' ) { + return $this->get_prop( 'amount', $context ); + } + + /** + * Get refund reason. + * + * @since 2.2 + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_reason( $context = 'view' ) { + return $this->get_prop( 'reason', $context ); + } + + /** + * Get ID of user who did the refund. + * + * @since 3.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return int + */ + public function get_refunded_by( $context = 'view' ) { + return $this->get_prop( 'refunded_by', $context ); + } + + /** + * Return if the payment was refunded via API. + * + * @since 3.3 + * @param string $context What the value is for. Valid values are view and edit. + * @return bool + */ + public function get_refunded_payment( $context = 'view' ) { + return $this->get_prop( 'refunded_payment', $context ); + } + + /** + * Get formatted refunded amount. + * + * @since 2.4 + * @return string + */ + public function get_formatted_refund_amount() { + return apply_filters( 'woocommerce_formatted_refund_amount', wc_price( $this->get_amount(), array( 'currency' => $this->get_currency() ) ), $this ); + } + + /** + * Set refunded amount. + * + * @param string $value Value to set. + * @throws WC_Data_Exception Exception if the amount is invalid. + */ + public function set_amount( $value ) { + $this->set_prop( 'amount', wc_format_decimal( $value ) ); + } + + /** + * Set refund reason. + * + * @param string $value Value to set. + * @throws WC_Data_Exception Exception if the amount is invalid. + */ + public function set_reason( $value ) { + $this->set_prop( 'reason', $value ); + } + + /** + * Set refunded by. + * + * @param int $value Value to set. + * @throws WC_Data_Exception Exception if the amount is invalid. + */ + public function set_refunded_by( $value ) { + $this->set_prop( 'refunded_by', absint( $value ) ); + } + + /** + * Set if the payment was refunded via API. + * + * @since 3.3 + * @param bool $value Value to set. + */ + public function set_refunded_payment( $value ) { + $this->set_prop( 'refunded_payment', (bool) $value ); + } + + /** + * Magic __get method for backwards compatibility. + * + * @param string $key Value to get. + * @return mixed + */ + public function __get( $key ) { + wc_doing_it_wrong( $key, 'Refund properties should not be accessed directly.', '3.0' ); + /** + * Maps legacy vars to new getters. + */ + if ( 'reason' === $key ) { + return $this->get_reason(); + } elseif ( 'refund_amount' === $key ) { + return $this->get_amount(); + } + return parent::__get( $key ); + } + + /** + * Gets an refund from the database. + * + * @deprecated 3.0 + * @param int $id (default: 0). + * @return bool + */ + public function get_refund( $id = 0 ) { + wc_deprecated_function( 'get_refund', '3.0', 'read' ); + + if ( ! $id ) { + return false; + } + + $result = get_post( $id ); + + if ( $result ) { + $this->populate( $result ); + return true; + } + + return false; + } + + /** + * Get refund amount. + * + * @deprecated 3.0 + * @return int|float + */ + public function get_refund_amount() { + wc_deprecated_function( 'get_refund_amount', '3.0', 'get_amount' ); + return $this->get_amount(); + } + + /** + * Get refund reason. + * + * @deprecated 3.0 + * @return string + */ + public function get_refund_reason() { + wc_deprecated_function( 'get_refund_reason', '3.0', 'get_reason' ); + return $this->get_reason(); + } +} diff --git a/plugins/woocommerce/includes/class-wc-order.php b/plugins/woocommerce/includes/class-wc-order.php new file mode 100644 index 00000000000..1962215143d --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-order.php @@ -0,0 +1,2114 @@ + 0, + 'status' => '', + 'currency' => '', + 'version' => '', + 'prices_include_tax' => false, + 'date_created' => null, + 'date_modified' => null, + 'discount_total' => 0, + 'discount_tax' => 0, + 'shipping_total' => 0, + 'shipping_tax' => 0, + 'cart_tax' => 0, + 'total' => 0, + 'total_tax' => 0, + + // Order props. + 'customer_id' => 0, + 'order_key' => '', + 'billing' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'state' => '', + 'postcode' => '', + 'country' => '', + 'email' => '', + 'phone' => '', + ), + 'shipping' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'state' => '', + 'postcode' => '', + 'country' => '', + 'phone' => '', + ), + 'payment_method' => '', + 'payment_method_title' => '', + 'transaction_id' => '', + 'customer_ip_address' => '', + 'customer_user_agent' => '', + 'created_via' => '', + 'customer_note' => '', + 'date_completed' => null, + 'date_paid' => null, + 'cart_hash' => '', + ); + + /** + * When a payment is complete this function is called. + * + * Most of the time this should mark an order as 'processing' so that admin can process/post the items. + * If the cart contains only downloadable items then the order is 'completed' since the admin needs to take no action. + * Stock levels are reduced at this point. + * Sales are also recorded for products. + * Finally, record the date of payment. + * + * @param string $transaction_id Optional transaction id to store in post meta. + * @return bool success + */ + public function payment_complete( $transaction_id = '' ) { + if ( ! $this->get_id() ) { // Order must exist. + return false; + } + + try { + do_action( 'woocommerce_pre_payment_complete', $this->get_id() ); + + if ( WC()->session ) { + WC()->session->set( 'order_awaiting_payment', false ); + } + + if ( $this->has_status( apply_filters( 'woocommerce_valid_order_statuses_for_payment_complete', array( 'on-hold', 'pending', 'failed', 'cancelled' ), $this ) ) ) { + if ( ! empty( $transaction_id ) ) { + $this->set_transaction_id( $transaction_id ); + } + if ( ! $this->get_date_paid( 'edit' ) ) { + $this->set_date_paid( time() ); + } + $this->set_status( apply_filters( 'woocommerce_payment_complete_order_status', $this->needs_processing() ? 'processing' : 'completed', $this->get_id(), $this ) ); + $this->save(); + + do_action( 'woocommerce_payment_complete', $this->get_id() ); + } else { + do_action( 'woocommerce_payment_complete_order_status_' . $this->get_status(), $this->get_id() ); + } + } catch ( Exception $e ) { + /** + * If there was an error completing the payment, log to a file and add an order note so the admin can take action. + */ + $logger = wc_get_logger(); + $logger->error( + sprintf( + 'Error completing payment for order #%d', + $this->get_id() + ), + array( + 'order' => $this, + 'error' => $e, + ) + ); + $this->add_order_note( __( 'Payment complete event failed.', 'woocommerce' ) . ' ' . $e->getMessage() ); + return false; + } + return true; + } + + /** + * Gets order total - formatted for display. + * + * @param string $tax_display Type of tax display. + * @param bool $display_refunded If should include refunded value. + * + * @return string + */ + public function get_formatted_order_total( $tax_display = '', $display_refunded = true ) { + $formatted_total = wc_price( $this->get_total(), array( 'currency' => $this->get_currency() ) ); + $order_total = $this->get_total(); + $total_refunded = $this->get_total_refunded(); + $tax_string = ''; + + // Tax for inclusive prices. + if ( wc_tax_enabled() && 'incl' === $tax_display ) { + $tax_string_array = array(); + $tax_totals = $this->get_tax_totals(); + + if ( 'itemized' === get_option( 'woocommerce_tax_total_display' ) ) { + foreach ( $tax_totals as $code => $tax ) { + $tax_amount = ( $total_refunded && $display_refunded ) ? wc_price( WC_Tax::round( $tax->amount - $this->get_total_tax_refunded_by_rate_id( $tax->rate_id ) ), array( 'currency' => $this->get_currency() ) ) : $tax->formatted_amount; + $tax_string_array[] = sprintf( '%s %s', $tax_amount, $tax->label ); + } + } elseif ( ! empty( $tax_totals ) ) { + $tax_amount = ( $total_refunded && $display_refunded ) ? $this->get_total_tax() - $this->get_total_tax_refunded() : $this->get_total_tax(); + $tax_string_array[] = sprintf( '%s %s', wc_price( $tax_amount, array( 'currency' => $this->get_currency() ) ), WC()->countries->tax_or_vat() ); + } + + if ( ! empty( $tax_string_array ) ) { + /* translators: %s: taxes */ + $tax_string = ' ' . sprintf( __( '(includes %s)', 'woocommerce' ), implode( ', ', $tax_string_array ) ) . ''; + } + } + + if ( $total_refunded && $display_refunded ) { + $formatted_total = ' ' . wc_price( $order_total - $total_refunded, array( 'currency' => $this->get_currency() ) ) . $tax_string . ''; + } else { + $formatted_total .= $tax_string; + } + + /** + * Filter WooCommerce formatted order total. + * + * @param string $formatted_total Total to display. + * @param WC_Order $order Order data. + * @param string $tax_display Type of tax display. + * @param bool $display_refunded If should include refunded value. + */ + return apply_filters( 'woocommerce_get_formatted_order_total', $formatted_total, $this, $tax_display, $display_refunded ); + } + + /* + |-------------------------------------------------------------------------- + | CRUD methods + |-------------------------------------------------------------------------- + | + | Methods which create, read, update and delete orders from the database. + | Written in abstract fashion so that the way orders are stored can be + | changed more easily in the future. + | + | A save method is included for convenience (chooses update or create based + | on if the order exists yet). + | + */ + + /** + * Save data to the database. + * + * @since 3.0.0 + * @return int order ID + */ + public function save() { + $this->maybe_set_user_billing_email(); + parent::save(); + $this->status_transition(); + + return $this->get_id(); + } + + /** + * Log an error about this order is exception is encountered. + * + * @param Exception $e Exception object. + * @param string $message Message regarding exception thrown. + * @since 3.7.0 + */ + protected function handle_exception( $e, $message = 'Error' ) { + wc_get_logger()->error( + $message, + array( + 'order' => $this, + 'error' => $e, + ) + ); + $this->add_order_note( $message . ' ' . $e->getMessage() ); + } + + /** + * Set order status. + * + * @since 3.0.0 + * @param string $new_status Status to change the order to. No internal wc- prefix is required. + * @param string $note Optional note to add. + * @param bool $manual_update Is this a manual order status change?. + * @return array + */ + public function set_status( $new_status, $note = '', $manual_update = false ) { + $result = parent::set_status( $new_status ); + + if ( true === $this->object_read && ! empty( $result['from'] ) && $result['from'] !== $result['to'] ) { + $this->status_transition = array( + 'from' => ! empty( $this->status_transition['from'] ) ? $this->status_transition['from'] : $result['from'], + 'to' => $result['to'], + 'note' => $note, + 'manual' => (bool) $manual_update, + ); + + if ( $manual_update ) { + do_action( 'woocommerce_order_edit_status', $this->get_id(), $result['to'] ); + } + + $this->maybe_set_date_paid(); + $this->maybe_set_date_completed(); + } + + return $result; + } + + /** + * Maybe set date paid. + * + * Sets the date paid variable when transitioning to the payment complete + * order status. This is either processing or completed. This is not filtered + * to avoid infinite loops e.g. if loading an order via the filter. + * + * Date paid is set once in this manner - only when it is not already set. + * This ensures the data exists even if a gateway does not use the + * `payment_complete` method. + * + * @since 3.0.0 + */ + public function maybe_set_date_paid() { + // This logic only runs if the date_paid prop has not been set yet. + if ( ! $this->get_date_paid( 'edit' ) ) { + $payment_completed_status = apply_filters( 'woocommerce_payment_complete_order_status', $this->needs_processing() ? 'processing' : 'completed', $this->get_id(), $this ); + + if ( $this->has_status( $payment_completed_status ) ) { + // If payment complete status is reached, set paid now. + $this->set_date_paid( time() ); + + } elseif ( 'processing' === $payment_completed_status && $this->has_status( 'completed' ) ) { + // If payment complete status was processing, but we've passed that and still have no date, set it now. + $this->set_date_paid( time() ); + } + } + } + + /** + * Maybe set date completed. + * + * Sets the date completed variable when transitioning to completed status. + * + * @since 3.0.0 + */ + protected function maybe_set_date_completed() { + if ( $this->has_status( 'completed' ) ) { + $this->set_date_completed( time() ); + } + } + + /** + * Updates status of order immediately. + * + * @uses WC_Order::set_status() + * @param string $new_status Status to change the order to. No internal wc- prefix is required. + * @param string $note Optional note to add. + * @param bool $manual Is this a manual order status change?. + * @return bool + */ + public function update_status( $new_status, $note = '', $manual = false ) { + if ( ! $this->get_id() ) { // Order must exist. + return false; + } + + try { + $this->set_status( $new_status, $note, $manual ); + $this->save(); + } catch ( Exception $e ) { + $logger = wc_get_logger(); + $logger->error( + sprintf( + 'Error updating status for order #%d', + $this->get_id() + ), + array( + 'order' => $this, + 'error' => $e, + ) + ); + $this->add_order_note( __( 'Update status event failed.', 'woocommerce' ) . ' ' . $e->getMessage() ); + return false; + } + return true; + } + + /** + * Handle the status transition. + */ + protected function status_transition() { + $status_transition = $this->status_transition; + + // Reset status transition variable. + $this->status_transition = false; + + if ( $status_transition ) { + try { + do_action( 'woocommerce_order_status_' . $status_transition['to'], $this->get_id(), $this ); + + if ( ! empty( $status_transition['from'] ) ) { + /* translators: 1: old order status 2: new order status */ + $transition_note = sprintf( __( 'Order status changed from %1$s to %2$s.', 'woocommerce' ), wc_get_order_status_name( $status_transition['from'] ), wc_get_order_status_name( $status_transition['to'] ) ); + + // Note the transition occurred. + $this->add_status_transition_note( $transition_note, $status_transition ); + + do_action( 'woocommerce_order_status_' . $status_transition['from'] . '_to_' . $status_transition['to'], $this->get_id(), $this ); + do_action( 'woocommerce_order_status_changed', $this->get_id(), $status_transition['from'], $status_transition['to'], $this ); + + // Work out if this was for a payment, and trigger a payment_status hook instead. + if ( + in_array( $status_transition['from'], apply_filters( 'woocommerce_valid_order_statuses_for_payment', array( 'pending', 'failed' ), $this ), true ) + && in_array( $status_transition['to'], wc_get_is_paid_statuses(), true ) + ) { + /** + * Fires when the order progresses from a pending payment status to a paid one. + * + * @since 3.9.0 + * @param int Order ID + * @param WC_Order Order object + */ + do_action( 'woocommerce_order_payment_status_changed', $this->get_id(), $this ); + } + } else { + /* translators: %s: new order status */ + $transition_note = sprintf( __( 'Order status set to %s.', 'woocommerce' ), wc_get_order_status_name( $status_transition['to'] ) ); + + // Note the transition occurred. + $this->add_status_transition_note( $transition_note, $status_transition ); + } + } catch ( Exception $e ) { + $logger = wc_get_logger(); + $logger->error( + sprintf( + 'Status transition of order #%d errored!', + $this->get_id() + ), + array( + 'order' => $this, + 'error' => $e, + ) + ); + $this->add_order_note( __( 'Error during status transition.', 'woocommerce' ) . ' ' . $e->getMessage() ); + } + } + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + | + | Methods for getting data from the order object. + | + */ + + /** + * Get basic order data in array format. + * + * @return array + */ + public function get_base_data() { + return array_merge( + array( 'id' => $this->get_id() ), + $this->data, + array( 'number' => $this->get_order_number() ) + ); + } + + /** + * Get all class data in array format. + * + * @since 3.0.0 + * @return array + */ + public function get_data() { + return array_merge( + $this->get_base_data(), + array( + 'meta_data' => $this->get_meta_data(), + 'line_items' => $this->get_items( 'line_item' ), + 'tax_lines' => $this->get_items( 'tax' ), + 'shipping_lines' => $this->get_items( 'shipping' ), + 'fee_lines' => $this->get_items( 'fee' ), + 'coupon_lines' => $this->get_items( 'coupon' ), + ) + ); + } + + /** + * Expands the shipping and billing information in the changes array. + */ + public function get_changes() { + $changed_props = parent::get_changes(); + $subs = array( 'shipping', 'billing' ); + foreach ( $subs as $sub ) { + if ( ! empty( $changed_props[ $sub ] ) ) { + foreach ( $changed_props[ $sub ] as $sub_prop => $value ) { + $changed_props[ $sub . '_' . $sub_prop ] = $value; + } + } + } + if ( isset( $changed_props['customer_note'] ) ) { + $changed_props['post_excerpt'] = $changed_props['customer_note']; + } + return $changed_props; + } + + /** + * Gets the order number for display (by default, order ID). + * + * @return string + */ + public function get_order_number() { + return (string) apply_filters( 'woocommerce_order_number', $this->get_id(), $this ); + } + + /** + * Get order key. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_order_key( $context = 'view' ) { + return $this->get_prop( 'order_key', $context ); + } + + /** + * Get customer_id. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return int + */ + public function get_customer_id( $context = 'view' ) { + return $this->get_prop( 'customer_id', $context ); + } + + /** + * Alias for get_customer_id(). + * + * @param string $context What the value is for. Valid values are view and edit. + * @return int + */ + public function get_user_id( $context = 'view' ) { + return $this->get_customer_id( $context ); + } + + /** + * Get the user associated with the order. False for guests. + * + * @return WP_User|false + */ + public function get_user() { + return $this->get_user_id() ? get_user_by( 'id', $this->get_user_id() ) : false; + } + + /** + * Gets a prop for a getter method. + * + * @since 3.0.0 + * @param string $prop Name of prop to get. + * @param string $address billing or shipping. + * @param string $context What the value is for. Valid values are view and edit. + * @return mixed + */ + protected function get_address_prop( $prop, $address = 'billing', $context = 'view' ) { + $value = null; + + if ( array_key_exists( $prop, $this->data[ $address ] ) ) { + $value = isset( $this->changes[ $address ][ $prop ] ) ? $this->changes[ $address ][ $prop ] : $this->data[ $address ][ $prop ]; + + if ( 'view' === $context ) { + $value = apply_filters( $this->get_hook_prefix() . $address . '_' . $prop, $value, $this ); + } + } + return $value; + } + + /** + * Get billing first name. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_billing_first_name( $context = 'view' ) { + return $this->get_address_prop( 'first_name', 'billing', $context ); + } + + /** + * Get billing last name. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_billing_last_name( $context = 'view' ) { + return $this->get_address_prop( 'last_name', 'billing', $context ); + } + + /** + * Get billing company. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_billing_company( $context = 'view' ) { + return $this->get_address_prop( 'company', 'billing', $context ); + } + + /** + * Get billing address line 1. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_billing_address_1( $context = 'view' ) { + return $this->get_address_prop( 'address_1', 'billing', $context ); + } + + /** + * Get billing address line 2. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_billing_address_2( $context = 'view' ) { + return $this->get_address_prop( 'address_2', 'billing', $context ); + } + + /** + * Get billing city. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_billing_city( $context = 'view' ) { + return $this->get_address_prop( 'city', 'billing', $context ); + } + + /** + * Get billing state. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_billing_state( $context = 'view' ) { + return $this->get_address_prop( 'state', 'billing', $context ); + } + + /** + * Get billing postcode. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_billing_postcode( $context = 'view' ) { + return $this->get_address_prop( 'postcode', 'billing', $context ); + } + + /** + * Get billing country. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_billing_country( $context = 'view' ) { + return $this->get_address_prop( 'country', 'billing', $context ); + } + + /** + * Get billing email. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_billing_email( $context = 'view' ) { + return $this->get_address_prop( 'email', 'billing', $context ); + } + + /** + * Get billing phone. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_billing_phone( $context = 'view' ) { + return $this->get_address_prop( 'phone', 'billing', $context ); + } + + /** + * Get shipping first name. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_shipping_first_name( $context = 'view' ) { + return $this->get_address_prop( 'first_name', 'shipping', $context ); + } + + /** + * Get shipping_last_name. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_shipping_last_name( $context = 'view' ) { + return $this->get_address_prop( 'last_name', 'shipping', $context ); + } + + /** + * Get shipping company. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_shipping_company( $context = 'view' ) { + return $this->get_address_prop( 'company', 'shipping', $context ); + } + + /** + * Get shipping address line 1. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_shipping_address_1( $context = 'view' ) { + return $this->get_address_prop( 'address_1', 'shipping', $context ); + } + + /** + * Get shipping address line 2. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_shipping_address_2( $context = 'view' ) { + return $this->get_address_prop( 'address_2', 'shipping', $context ); + } + + /** + * Get shipping city. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_shipping_city( $context = 'view' ) { + return $this->get_address_prop( 'city', 'shipping', $context ); + } + + /** + * Get shipping state. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_shipping_state( $context = 'view' ) { + return $this->get_address_prop( 'state', 'shipping', $context ); + } + + /** + * Get shipping postcode. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_shipping_postcode( $context = 'view' ) { + return $this->get_address_prop( 'postcode', 'shipping', $context ); + } + + /** + * Get shipping country. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_shipping_country( $context = 'view' ) { + return $this->get_address_prop( 'country', 'shipping', $context ); + } + + /** + * Get shipping phone. + * + * @since 5.6.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_shipping_phone( $context = 'view' ) { + return $this->get_address_prop( 'phone', 'shipping', $context ); + } + + /** + * Get the payment method. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_payment_method( $context = 'view' ) { + return $this->get_prop( 'payment_method', $context ); + } + + /** + * Get payment method title. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_payment_method_title( $context = 'view' ) { + return $this->get_prop( 'payment_method_title', $context ); + } + + /** + * Get transaction d. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_transaction_id( $context = 'view' ) { + return $this->get_prop( 'transaction_id', $context ); + } + + /** + * Get customer ip address. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_customer_ip_address( $context = 'view' ) { + return $this->get_prop( 'customer_ip_address', $context ); + } + + /** + * Get customer user agent. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_customer_user_agent( $context = 'view' ) { + return $this->get_prop( 'customer_user_agent', $context ); + } + + /** + * Get created via. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_created_via( $context = 'view' ) { + return $this->get_prop( 'created_via', $context ); + } + + /** + * Get customer note. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_customer_note( $context = 'view' ) { + return $this->get_prop( 'customer_note', $context ); + } + + /** + * Get date completed. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_completed( $context = 'view' ) { + return $this->get_prop( 'date_completed', $context ); + } + + /** + * Get date paid. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_paid( $context = 'view' ) { + $date_paid = $this->get_prop( 'date_paid', $context ); + + if ( 'view' === $context && ! $date_paid && version_compare( $this->get_version( 'edit' ), '3.0', '<' ) && $this->has_status( apply_filters( 'woocommerce_payment_complete_order_status', $this->needs_processing() ? 'processing' : 'completed', $this->get_id(), $this ) ) ) { + // In view context, return a date if missing. + $date_paid = $this->get_date_created( 'edit' ); + } + return $date_paid; + } + + /** + * Get cart hash. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_cart_hash( $context = 'view' ) { + return $this->get_prop( 'cart_hash', $context ); + } + + /** + * Returns the requested address in raw, non-formatted way. + * Note: Merges raw data with get_prop data so changes are returned too. + * + * @since 2.4.0 + * @param string $type Billing or shipping. Anything else besides 'billing' will return shipping address. + * @return array The stored address after filter. + */ + public function get_address( $type = 'billing' ) { + return apply_filters( 'woocommerce_get_order_address', array_merge( $this->data[ $type ], $this->get_prop( $type, 'view' ) ), $type, $this ); + } + + /** + * Get a formatted shipping address for the order. + * + * @return string + */ + public function get_shipping_address_map_url() { + $address = $this->get_address( 'shipping' ); + + // Remove name and company before generate the Google Maps URL. + unset( $address['first_name'], $address['last_name'], $address['company'], $address['phone'] ); + + $address = apply_filters( 'woocommerce_shipping_address_map_url_parts', $address, $this ); + + return apply_filters( 'woocommerce_shipping_address_map_url', 'https://maps.google.com/maps?&q=' . rawurlencode( implode( ', ', $address ) ) . '&z=16', $this ); + } + + /** + * Get a formatted billing full name. + * + * @return string + */ + public function get_formatted_billing_full_name() { + /* translators: 1: first name 2: last name */ + return sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $this->get_billing_first_name(), $this->get_billing_last_name() ); + } + + /** + * Get a formatted shipping full name. + * + * @return string + */ + public function get_formatted_shipping_full_name() { + /* translators: 1: first name 2: last name */ + return sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $this->get_shipping_first_name(), $this->get_shipping_last_name() ); + } + + /** + * Get a formatted billing address for the order. + * + * @param string $empty_content Content to show if no address is present. @since 3.3.0. + * @return string + */ + public function get_formatted_billing_address( $empty_content = '' ) { + $raw_address = apply_filters( 'woocommerce_order_formatted_billing_address', $this->get_address( 'billing' ), $this ); + $address = WC()->countries->get_formatted_address( $raw_address ); + + /** + * Filter orders formatted billing address. + * + * @since 3.8.0 + * @param string $address Formatted billing address string. + * @param array $raw_address Raw billing address. + * @param WC_Order $order Order data. @since 3.9.0 + */ + return apply_filters( 'woocommerce_order_get_formatted_billing_address', $address ? $address : $empty_content, $raw_address, $this ); + } + + /** + * Get a formatted shipping address for the order. + * + * @param string $empty_content Content to show if no address is present. @since 3.3.0. + * @return string + */ + public function get_formatted_shipping_address( $empty_content = '' ) { + $address = ''; + $raw_address = $this->get_address( 'shipping' ); + + if ( $this->has_shipping_address() ) { + $raw_address = apply_filters( 'woocommerce_order_formatted_shipping_address', $raw_address, $this ); + $address = WC()->countries->get_formatted_address( $raw_address ); + } + + /** + * Filter orders formatted shipping address. + * + * @since 3.8.0 + * @param string $address Formatted billing address string. + * @param array $raw_address Raw billing address. + * @param WC_Order $order Order data. @since 3.9.0 + */ + return apply_filters( 'woocommerce_order_get_formatted_shipping_address', $address ? $address : $empty_content, $raw_address, $this ); + } + + /** + * Returns true if the order has a billing address. + * + * @since 3.0.4 + * @return boolean + */ + public function has_billing_address() { + return $this->get_billing_address_1() || $this->get_billing_address_2(); + } + + /** + * Returns true if the order has a shipping address. + * + * @since 3.0.4 + * @return boolean + */ + public function has_shipping_address() { + return $this->get_shipping_address_1() || $this->get_shipping_address_2(); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + | + | Functions for setting order data. These should not update anything in the + | database itself and should only change what is stored in the class + | object. However, for backwards compatibility pre 3.0.0 some of these + | setters may handle both. + | + */ + + /** + * Sets a prop for a setter method. + * + * @since 3.0.0 + * @param string $prop Name of prop to set. + * @param string $address Name of address to set. billing or shipping. + * @param mixed $value Value of the prop. + */ + protected function set_address_prop( $prop, $address, $value ) { + if ( array_key_exists( $prop, $this->data[ $address ] ) ) { + if ( true === $this->object_read ) { + if ( $value !== $this->data[ $address ][ $prop ] || ( isset( $this->changes[ $address ] ) && array_key_exists( $prop, $this->changes[ $address ] ) ) ) { + $this->changes[ $address ][ $prop ] = $value; + } + } else { + $this->data[ $address ][ $prop ] = $value; + } + } + } + + /** + * Set order key. + * + * @param string $value Max length 22 chars. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_order_key( $value ) { + $this->set_prop( 'order_key', substr( $value, 0, 22 ) ); + } + + /** + * Set customer id. + * + * @param int $value Customer ID. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_customer_id( $value ) { + $this->set_prop( 'customer_id', absint( $value ) ); + } + + /** + * Set billing first name. + * + * @param string $value Billing first name. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_billing_first_name( $value ) { + $this->set_address_prop( 'first_name', 'billing', $value ); + } + + /** + * Set billing last name. + * + * @param string $value Billing last name. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_billing_last_name( $value ) { + $this->set_address_prop( 'last_name', 'billing', $value ); + } + + /** + * Set billing company. + * + * @param string $value Billing company. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_billing_company( $value ) { + $this->set_address_prop( 'company', 'billing', $value ); + } + + /** + * Set billing address line 1. + * + * @param string $value Billing address line 1. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_billing_address_1( $value ) { + $this->set_address_prop( 'address_1', 'billing', $value ); + } + + /** + * Set billing address line 2. + * + * @param string $value Billing address line 2. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_billing_address_2( $value ) { + $this->set_address_prop( 'address_2', 'billing', $value ); + } + + /** + * Set billing city. + * + * @param string $value Billing city. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_billing_city( $value ) { + $this->set_address_prop( 'city', 'billing', $value ); + } + + /** + * Set billing state. + * + * @param string $value Billing state. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_billing_state( $value ) { + $this->set_address_prop( 'state', 'billing', $value ); + } + + /** + * Set billing postcode. + * + * @param string $value Billing postcode. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_billing_postcode( $value ) { + $this->set_address_prop( 'postcode', 'billing', $value ); + } + + /** + * Set billing country. + * + * @param string $value Billing country. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_billing_country( $value ) { + $this->set_address_prop( 'country', 'billing', $value ); + } + + /** + * Maybe set empty billing email to that of the user who owns the order. + */ + protected function maybe_set_user_billing_email() { + $user = $this->get_user(); + if ( ! $this->get_billing_email() && $user ) { + try { + $this->set_billing_email( $user->user_email ); + } catch ( WC_Data_Exception $e ) { + unset( $e ); + } + } + } + + /** + * Set billing email. + * + * @param string $value Billing email. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_billing_email( $value ) { + if ( $value && ! is_email( $value ) ) { + $this->error( 'order_invalid_billing_email', __( 'Invalid billing email address', 'woocommerce' ) ); + } + $this->set_address_prop( 'email', 'billing', sanitize_email( $value ) ); + } + + /** + * Set billing phone. + * + * @param string $value Billing phone. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_billing_phone( $value ) { + $this->set_address_prop( 'phone', 'billing', $value ); + } + + /** + * Set shipping first name. + * + * @param string $value Shipping first name. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_shipping_first_name( $value ) { + $this->set_address_prop( 'first_name', 'shipping', $value ); + } + + /** + * Set shipping last name. + * + * @param string $value Shipping last name. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_shipping_last_name( $value ) { + $this->set_address_prop( 'last_name', 'shipping', $value ); + } + + /** + * Set shipping company. + * + * @param string $value Shipping company. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_shipping_company( $value ) { + $this->set_address_prop( 'company', 'shipping', $value ); + } + + /** + * Set shipping address line 1. + * + * @param string $value Shipping address line 1. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_shipping_address_1( $value ) { + $this->set_address_prop( 'address_1', 'shipping', $value ); + } + + /** + * Set shipping address line 2. + * + * @param string $value Shipping address line 2. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_shipping_address_2( $value ) { + $this->set_address_prop( 'address_2', 'shipping', $value ); + } + + /** + * Set shipping city. + * + * @param string $value Shipping city. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_shipping_city( $value ) { + $this->set_address_prop( 'city', 'shipping', $value ); + } + + /** + * Set shipping state. + * + * @param string $value Shipping state. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_shipping_state( $value ) { + $this->set_address_prop( 'state', 'shipping', $value ); + } + + /** + * Set shipping postcode. + * + * @param string $value Shipping postcode. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_shipping_postcode( $value ) { + $this->set_address_prop( 'postcode', 'shipping', $value ); + } + + /** + * Set shipping country. + * + * @param string $value Shipping country. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_shipping_country( $value ) { + $this->set_address_prop( 'country', 'shipping', $value ); + } + + /** + * Set shipping phone. + * + * @since 5.6.0 + * @param string $value Shipping phone. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_shipping_phone( $value ) { + $this->set_address_prop( 'phone', 'shipping', $value ); + } + + /** + * Set the payment method. + * + * @param string $payment_method Supports WC_Payment_Gateway for bw compatibility with < 3.0. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_payment_method( $payment_method = '' ) { + if ( is_object( $payment_method ) ) { + $this->set_payment_method( $payment_method->id ); + $this->set_payment_method_title( $payment_method->get_title() ); + } elseif ( '' === $payment_method ) { + $this->set_prop( 'payment_method', '' ); + $this->set_prop( 'payment_method_title', '' ); + } else { + $this->set_prop( 'payment_method', $payment_method ); + } + } + + /** + * Set payment method title. + * + * @param string $value Payment method title. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_payment_method_title( $value ) { + $this->set_prop( 'payment_method_title', $value ); + } + + /** + * Set transaction id. + * + * @param string $value Transaction id. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_transaction_id( $value ) { + $this->set_prop( 'transaction_id', $value ); + } + + /** + * Set customer ip address. + * + * @param string $value Customer ip address. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_customer_ip_address( $value ) { + $this->set_prop( 'customer_ip_address', $value ); + } + + /** + * Set customer user agent. + * + * @param string $value Customer user agent. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_customer_user_agent( $value ) { + $this->set_prop( 'customer_user_agent', $value ); + } + + /** + * Set created via. + * + * @param string $value Created via. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_created_via( $value ) { + $this->set_prop( 'created_via', $value ); + } + + /** + * Set customer note. + * + * @param string $value Customer note. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_customer_note( $value ) { + $this->set_prop( 'customer_note', $value ); + } + + /** + * Set date completed. + * + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_date_completed( $date = null ) { + $this->set_date_prop( 'date_completed', $date ); + } + + /** + * Set date paid. + * + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_date_paid( $date = null ) { + $this->set_date_prop( 'date_paid', $date ); + } + + /** + * Set cart hash. + * + * @param string $value Cart hash. + * @throws WC_Data_Exception Throws exception when invalid data is found. + */ + public function set_cart_hash( $value ) { + $this->set_prop( 'cart_hash', $value ); + } + + /* + |-------------------------------------------------------------------------- + | Conditionals + |-------------------------------------------------------------------------- + | + | Checks if a condition is true or false. + | + */ + + /** + * Check if an order key is valid. + * + * @param string $key Order key. + * @return bool + */ + public function key_is_valid( $key ) { + return hash_equals( $this->get_order_key(), $key ); + } + + /** + * See if order matches cart_hash. + * + * @param string $cart_hash Cart hash. + * @return bool + */ + public function has_cart_hash( $cart_hash = '' ) { + return hash_equals( $this->get_cart_hash(), $cart_hash ); // @codingStandardsIgnoreLine + } + + /** + * Checks if an order can be edited, specifically for use on the Edit Order screen. + * + * @return bool + */ + public function is_editable() { + return apply_filters( 'wc_order_is_editable', in_array( $this->get_status(), array( 'pending', 'on-hold', 'auto-draft' ), true ), $this ); + } + + /** + * Returns if an order has been paid for based on the order status. + * + * @since 2.5.0 + * @return bool + */ + public function is_paid() { + return apply_filters( 'woocommerce_order_is_paid', $this->has_status( wc_get_is_paid_statuses() ), $this ); + } + + /** + * Checks if product download is permitted. + * + * @return bool + */ + public function is_download_permitted() { + return apply_filters( 'woocommerce_order_is_download_permitted', $this->has_status( 'completed' ) || ( 'yes' === get_option( 'woocommerce_downloads_grant_access_after_payment' ) && $this->has_status( 'processing' ) ), $this ); + } + + /** + * Checks if an order needs display the shipping address, based on shipping method. + * + * @return bool + */ + public function needs_shipping_address() { + if ( 'no' === get_option( 'woocommerce_calc_shipping' ) ) { + return false; + } + + $hide = apply_filters( 'woocommerce_order_hide_shipping_address', array( 'local_pickup' ), $this ); + $needs_address = false; + + foreach ( $this->get_shipping_methods() as $shipping_method ) { + $shipping_method_id = $shipping_method->get_method_id(); + + if ( ! in_array( $shipping_method_id, $hide, true ) ) { + $needs_address = true; + break; + } + } + + return apply_filters( 'woocommerce_order_needs_shipping_address', $needs_address, $hide, $this ); + } + + /** + * Returns true if the order contains a downloadable product. + * + * @return bool + */ + public function has_downloadable_item() { + foreach ( $this->get_items() as $item ) { + if ( $item->is_type( 'line_item' ) ) { + $product = $item->get_product(); + + if ( $product && $product->has_file() ) { + return true; + } + } + } + return false; + } + + /** + * Get downloads from all line items for this order. + * + * @since 3.2.0 + * @return array + */ + public function get_downloadable_items() { + $downloads = array(); + + foreach ( $this->get_items() as $item ) { + if ( ! is_object( $item ) ) { + continue; + } + + // Check item refunds. + $refunded_qty = abs( $this->get_qty_refunded_for_item( $item->get_id() ) ); + if ( $refunded_qty && $item->get_quantity() === $refunded_qty ) { + continue; + } + + if ( $item->is_type( 'line_item' ) ) { + $item_downloads = $item->get_item_downloads(); + $product = $item->get_product(); + if ( $product && $item_downloads ) { + foreach ( $item_downloads as $file ) { + $downloads[] = array( + 'download_url' => $file['download_url'], + 'download_id' => $file['id'], + 'product_id' => $product->get_id(), + 'product_name' => $product->get_name(), + 'product_url' => $product->is_visible() ? $product->get_permalink() : '', // Since 3.3.0. + 'download_name' => $file['name'], + 'order_id' => $this->get_id(), + 'order_key' => $this->get_order_key(), + 'downloads_remaining' => $file['downloads_remaining'], + 'access_expires' => $file['access_expires'], + 'file' => array( + 'name' => $file['name'], + 'file' => $file['file'], + ), + ); + } + } + } + } + + return apply_filters( 'woocommerce_order_get_downloadable_items', $downloads, $this ); + } + + /** + * Checks if an order needs payment, based on status and order total. + * + * @return bool + */ + public function needs_payment() { + $valid_order_statuses = apply_filters( 'woocommerce_valid_order_statuses_for_payment', array( 'pending', 'failed' ), $this ); + return apply_filters( 'woocommerce_order_needs_payment', ( $this->has_status( $valid_order_statuses ) && $this->get_total() > 0 ), $this, $valid_order_statuses ); + } + + /** + * See if the order needs processing before it can be completed. + * + * Orders which only contain virtual, downloadable items do not need admin + * intervention. + * + * Uses a transient so these calls are not repeated multiple times, and because + * once the order is processed this code/transient does not need to persist. + * + * @since 3.0.0 + * @return bool + */ + public function needs_processing() { + $transient_name = 'wc_order_' . $this->get_id() . '_needs_processing'; + $needs_processing = get_transient( $transient_name ); + + if ( false === $needs_processing ) { + $needs_processing = 0; + + if ( count( $this->get_items() ) > 0 ) { + foreach ( $this->get_items() as $item ) { + if ( $item->is_type( 'line_item' ) ) { + $product = $item->get_product(); + + if ( ! $product ) { + continue; + } + + $virtual_downloadable_item = $product->is_downloadable() && $product->is_virtual(); + + if ( apply_filters( 'woocommerce_order_item_needs_processing', ! $virtual_downloadable_item, $product, $this->get_id() ) ) { + $needs_processing = 1; + break; + } + } + } + } + + set_transient( $transient_name, $needs_processing, DAY_IN_SECONDS ); + } + + return 1 === absint( $needs_processing ); + } + + /* + |-------------------------------------------------------------------------- + | URLs and Endpoints + |-------------------------------------------------------------------------- + */ + + /** + * Generates a URL so that a customer can pay for their (unpaid - pending) order. Pass 'true' for the checkout version which doesn't offer gateway choices. + * + * @param bool $on_checkout If on checkout. + * @return string + */ + public function get_checkout_payment_url( $on_checkout = false ) { + $pay_url = wc_get_endpoint_url( 'order-pay', $this->get_id(), wc_get_checkout_url() ); + + if ( $on_checkout ) { + $pay_url = add_query_arg( 'key', $this->get_order_key(), $pay_url ); + } else { + $pay_url = add_query_arg( + array( + 'pay_for_order' => 'true', + 'key' => $this->get_order_key(), + ), + $pay_url + ); + } + + return apply_filters( 'woocommerce_get_checkout_payment_url', $pay_url, $this ); + } + + /** + * Generates a URL for the thanks page (order received). + * + * @return string + */ + public function get_checkout_order_received_url() { + $order_received_url = wc_get_endpoint_url( 'order-received', $this->get_id(), wc_get_checkout_url() ); + $order_received_url = add_query_arg( 'key', $this->get_order_key(), $order_received_url ); + + return apply_filters( 'woocommerce_get_checkout_order_received_url', $order_received_url, $this ); + } + + /** + * Generates a URL so that a customer can cancel their (unpaid - pending) order. + * + * @param string $redirect Redirect URL. + * @return string + */ + public function get_cancel_order_url( $redirect = '' ) { + return apply_filters( + 'woocommerce_get_cancel_order_url', + wp_nonce_url( + add_query_arg( + array( + 'cancel_order' => 'true', + 'order' => $this->get_order_key(), + 'order_id' => $this->get_id(), + 'redirect' => $redirect, + ), + $this->get_cancel_endpoint() + ), + 'woocommerce-cancel_order' + ) + ); + } + + /** + * Generates a raw (unescaped) cancel-order URL for use by payment gateways. + * + * @param string $redirect Redirect URL. + * @return string The unescaped cancel-order URL. + */ + public function get_cancel_order_url_raw( $redirect = '' ) { + return apply_filters( + 'woocommerce_get_cancel_order_url_raw', + add_query_arg( + array( + 'cancel_order' => 'true', + 'order' => $this->get_order_key(), + 'order_id' => $this->get_id(), + 'redirect' => $redirect, + '_wpnonce' => wp_create_nonce( 'woocommerce-cancel_order' ), + ), + $this->get_cancel_endpoint() + ) + ); + } + + /** + * Helper method to return the cancel endpoint. + * + * @return string the cancel endpoint; either the cart page or the home page. + */ + public function get_cancel_endpoint() { + $cancel_endpoint = wc_get_cart_url(); + if ( ! $cancel_endpoint ) { + $cancel_endpoint = home_url(); + } + + if ( false === strpos( $cancel_endpoint, '?' ) ) { + $cancel_endpoint = trailingslashit( $cancel_endpoint ); + } + + return $cancel_endpoint; + } + + /** + * Generates a URL to view an order from the my account page. + * + * @return string + */ + public function get_view_order_url() { + return apply_filters( 'woocommerce_get_view_order_url', wc_get_endpoint_url( 'view-order', $this->get_id(), wc_get_page_permalink( 'myaccount' ) ), $this ); + } + + /** + * Get's the URL to edit the order in the backend. + * + * @since 3.3.0 + * @return string + */ + public function get_edit_order_url() { + return apply_filters( 'woocommerce_get_edit_order_url', get_admin_url( null, 'post.php?post=' . $this->get_id() . '&action=edit' ), $this ); + } + + /* + |-------------------------------------------------------------------------- + | Order notes. + |-------------------------------------------------------------------------- + */ + + /** + * Adds a note (comment) to the order. Order must exist. + * + * @param string $note Note to add. + * @param int $is_customer_note Is this a note for the customer?. + * @param bool $added_by_user Was the note added by a user?. + * @return int Comment ID. + */ + public function add_order_note( $note, $is_customer_note = 0, $added_by_user = false ) { + if ( ! $this->get_id() ) { + return 0; + } + + if ( is_user_logged_in() && current_user_can( 'edit_shop_orders', $this->get_id() ) && $added_by_user ) { + $user = get_user_by( 'id', get_current_user_id() ); + $comment_author = $user->display_name; + $comment_author_email = $user->user_email; + } else { + $comment_author = __( 'WooCommerce', 'woocommerce' ); + $comment_author_email = strtolower( __( 'WooCommerce', 'woocommerce' ) ) . '@'; + $comment_author_email .= isset( $_SERVER['HTTP_HOST'] ) ? str_replace( 'www.', '', sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) ) : 'noreply.com'; // WPCS: input var ok. + $comment_author_email = sanitize_email( $comment_author_email ); + } + $commentdata = apply_filters( + 'woocommerce_new_order_note_data', + array( + 'comment_post_ID' => $this->get_id(), + 'comment_author' => $comment_author, + 'comment_author_email' => $comment_author_email, + 'comment_author_url' => '', + 'comment_content' => $note, + 'comment_agent' => 'WooCommerce', + 'comment_type' => 'order_note', + 'comment_parent' => 0, + 'comment_approved' => 1, + ), + array( + 'order_id' => $this->get_id(), + 'is_customer_note' => $is_customer_note, + ) + ); + + $comment_id = wp_insert_comment( $commentdata ); + + if ( $is_customer_note ) { + add_comment_meta( $comment_id, 'is_customer_note', 1 ); + + do_action( + 'woocommerce_new_customer_note', + array( + 'order_id' => $this->get_id(), + 'customer_note' => $commentdata['comment_content'], + ) + ); + } + + /** + * Action hook fired after an order note is added. + * + * @param int $order_note_id Order note ID. + * @param WC_Order $order Order data. + * + * @since 4.4.0 + */ + do_action( 'woocommerce_order_note_added', $comment_id, $this ); + + return $comment_id; + } + + /** + * Add an order note for status transition + * + * @since 3.9.0 + * @uses WC_Order::add_order_note() + * @param string $note Note to be added giving status transition from and to details. + * @param bool $transition Details of the status transition. + * @return int Comment ID. + */ + private function add_status_transition_note( $note, $transition ) { + return $this->add_order_note( trim( $transition['note'] . ' ' . $note ), 0, $transition['manual'] ); + } + + /** + * List order notes (public) for the customer. + * + * @return array + */ + public function get_customer_order_notes() { + $notes = array(); + $args = array( + 'post_id' => $this->get_id(), + 'approve' => 'approve', + 'type' => '', + ); + + remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ) ); + + $comments = get_comments( $args ); + + foreach ( $comments as $comment ) { + if ( ! get_comment_meta( $comment->comment_ID, 'is_customer_note', true ) ) { + continue; + } + $comment->comment_content = make_clickable( $comment->comment_content ); + $notes[] = $comment; + } + + add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ) ); + + return $notes; + } + + /* + |-------------------------------------------------------------------------- + | Refunds + |-------------------------------------------------------------------------- + */ + + /** + * Get order refunds. + * + * @since 2.2 + * @return array of WC_Order_Refund objects + */ + public function get_refunds() { + $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $this->get_id(); + $cached_data = wp_cache_get( $cache_key, $this->cache_group ); + + if ( false !== $cached_data ) { + return $cached_data; + } + + $this->refunds = wc_get_orders( + array( + 'type' => 'shop_order_refund', + 'parent' => $this->get_id(), + 'limit' => -1, + ) + ); + + wp_cache_set( $cache_key, $this->refunds, $this->cache_group ); + + return $this->refunds; + } + + /** + * Get amount already refunded. + * + * @since 2.2 + * @return string + */ + public function get_total_refunded() { + $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'total_refunded' . $this->get_id(); + $cached_data = wp_cache_get( $cache_key, $this->cache_group ); + + if ( false !== $cached_data ) { + return $cached_data; + } + + $total_refunded = $this->data_store->get_total_refunded( $this ); + + wp_cache_set( $cache_key, $total_refunded, $this->cache_group ); + + return $total_refunded; + } + + /** + * Get the total tax refunded. + * + * @since 2.3 + * @return float + */ + public function get_total_tax_refunded() { + $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'total_tax_refunded' . $this->get_id(); + $cached_data = wp_cache_get( $cache_key, $this->cache_group ); + + if ( false !== $cached_data ) { + return $cached_data; + } + + $total_refunded = $this->data_store->get_total_tax_refunded( $this ); + + wp_cache_set( $cache_key, $total_refunded, $this->cache_group ); + + return $total_refunded; + } + + /** + * Get the total shipping refunded. + * + * @since 2.4 + * @return float + */ + public function get_total_shipping_refunded() { + $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'total_shipping_refunded' . $this->get_id(); + $cached_data = wp_cache_get( $cache_key, $this->cache_group ); + + if ( false !== $cached_data ) { + return $cached_data; + } + + $total_refunded = $this->data_store->get_total_shipping_refunded( $this ); + + wp_cache_set( $cache_key, $total_refunded, $this->cache_group ); + + return $total_refunded; + } + + /** + * Gets the count of order items of a certain type that have been refunded. + * + * @since 2.4.0 + * @param string $item_type Item type. + * @return string + */ + public function get_item_count_refunded( $item_type = '' ) { + if ( empty( $item_type ) ) { + $item_type = array( 'line_item' ); + } + if ( ! is_array( $item_type ) ) { + $item_type = array( $item_type ); + } + $count = 0; + + foreach ( $this->get_refunds() as $refund ) { + foreach ( $refund->get_items( $item_type ) as $refunded_item ) { + $count += abs( $refunded_item->get_quantity() ); + } + } + + return apply_filters( 'woocommerce_get_item_count_refunded', $count, $item_type, $this ); + } + + /** + * Get the total number of items refunded. + * + * @since 2.4.0 + * + * @param string $item_type Type of the item we're checking, if not a line_item. + * @return int + */ + public function get_total_qty_refunded( $item_type = 'line_item' ) { + $qty = 0; + foreach ( $this->get_refunds() as $refund ) { + foreach ( $refund->get_items( $item_type ) as $refunded_item ) { + $qty += $refunded_item->get_quantity(); + } + } + return $qty; + } + + /** + * Get the refunded amount for a line item. + * + * @param int $item_id ID of the item we're checking. + * @param string $item_type Type of the item we're checking, if not a line_item. + * @return int + */ + public function get_qty_refunded_for_item( $item_id, $item_type = 'line_item' ) { + $qty = 0; + foreach ( $this->get_refunds() as $refund ) { + foreach ( $refund->get_items( $item_type ) as $refunded_item ) { + if ( absint( $refunded_item->get_meta( '_refunded_item_id' ) ) === $item_id ) { + $qty += $refunded_item->get_quantity(); + } + } + } + return $qty; + } + + /** + * Get the refunded amount for a line item. + * + * @param int $item_id ID of the item we're checking. + * @param string $item_type Type of the item we're checking, if not a line_item. + * @return int + */ + public function get_total_refunded_for_item( $item_id, $item_type = 'line_item' ) { + $total = 0; + foreach ( $this->get_refunds() as $refund ) { + foreach ( $refund->get_items( $item_type ) as $refunded_item ) { + if ( absint( $refunded_item->get_meta( '_refunded_item_id' ) ) === $item_id ) { + $total += $refunded_item->get_total(); + } + } + } + return $total * -1; + } + + /** + * Get the refunded tax amount for a line item. + * + * @param int $item_id ID of the item we're checking. + * @param int $tax_id ID of the tax we're checking. + * @param string $item_type Type of the item we're checking, if not a line_item. + * @return double + */ + public function get_tax_refunded_for_item( $item_id, $tax_id, $item_type = 'line_item' ) { + $total = 0; + foreach ( $this->get_refunds() as $refund ) { + foreach ( $refund->get_items( $item_type ) as $refunded_item ) { + $refunded_item_id = (int) $refunded_item->get_meta( '_refunded_item_id' ); + if ( $refunded_item_id === $item_id ) { + $taxes = $refunded_item->get_taxes(); + $total += isset( $taxes['total'][ $tax_id ] ) ? (float) $taxes['total'][ $tax_id ] : 0; + break; + } + } + } + return wc_round_tax_total( $total ) * -1; + } + + /** + * Get total tax refunded by rate ID. + * + * @param int $rate_id Rate ID. + * @return float + */ + public function get_total_tax_refunded_by_rate_id( $rate_id ) { + $total = 0; + foreach ( $this->get_refunds() as $refund ) { + foreach ( $refund->get_items( 'tax' ) as $refunded_item ) { + if ( absint( $refunded_item->get_rate_id() ) === $rate_id ) { + $total += abs( $refunded_item->get_tax_total() ) + abs( $refunded_item->get_shipping_tax_total() ); + } + } + } + + return $total; + } + + /** + * How much money is left to refund? + * + * @return string + */ + public function get_remaining_refund_amount() { + return wc_format_decimal( $this->get_total() - $this->get_total_refunded(), wc_get_price_decimals() ); + } + + /** + * How many items are left to refund? + * + * @return int + */ + public function get_remaining_refund_items() { + return absint( $this->get_item_count() - $this->get_item_count_refunded() ); + } + + /** + * Add total row for the payment method. + * + * @param array $total_rows Total rows. + * @param string $tax_display Tax to display. + */ + protected function add_order_item_totals_payment_method_row( &$total_rows, $tax_display ) { + if ( $this->get_total() > 0 && $this->get_payment_method_title() && 'other' !== $this->get_payment_method_title() ) { + $total_rows['payment_method'] = array( + 'label' => __( 'Payment method:', 'woocommerce' ), + 'value' => $this->get_payment_method_title(), + ); + } + } + + /** + * Add total row for refunds. + * + * @param array $total_rows Total rows. + * @param string $tax_display Tax to display. + */ + protected function add_order_item_totals_refund_rows( &$total_rows, $tax_display ) { + $refunds = $this->get_refunds(); + if ( $refunds ) { + foreach ( $refunds as $id => $refund ) { + $total_rows[ 'refund_' . $id ] = array( + 'label' => $refund->get_reason() ? $refund->get_reason() : __( 'Refund', 'woocommerce' ) . ':', + 'value' => wc_price( '-' . $refund->get_amount(), array( 'currency' => $this->get_currency() ) ), + ); + } + } + } + + /** + * Get totals for display on pages and in emails. + * + * @param string $tax_display Tax to display. + * @return array + */ + public function get_order_item_totals( $tax_display = '' ) { + $tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' ); + $total_rows = array(); + + $this->add_order_item_totals_subtotal_row( $total_rows, $tax_display ); + $this->add_order_item_totals_discount_row( $total_rows, $tax_display ); + $this->add_order_item_totals_shipping_row( $total_rows, $tax_display ); + $this->add_order_item_totals_fee_rows( $total_rows, $tax_display ); + $this->add_order_item_totals_tax_rows( $total_rows, $tax_display ); + $this->add_order_item_totals_payment_method_row( $total_rows, $tax_display ); + $this->add_order_item_totals_refund_rows( $total_rows, $tax_display ); + $this->add_order_item_totals_total_row( $total_rows, $tax_display ); + + return apply_filters( 'woocommerce_get_order_item_totals', $total_rows, $this, $tax_display ); + } + + /** + * Check if order has been created via admin, checkout, or in another way. + * + * @since 4.0.0 + * @param string $modus Way of creating the order to test for. + * @return bool + */ + public function is_created_via( $modus ) { + return apply_filters( 'woocommerce_order_is_created_via', $modus === $this->get_created_via(), $this, $modus ); + } +} diff --git a/plugins/woocommerce/includes/class-wc-payment-gateways.php b/plugins/woocommerce/includes/class-wc-payment-gateways.php new file mode 100644 index 00000000000..04520322ee0 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-payment-gateways.php @@ -0,0 +1,236 @@ +init(); + } + + /** + * Load gateways and hook in functions. + */ + public function init() { + $load_gateways = array( + 'WC_Gateway_BACS', + 'WC_Gateway_Cheque', + 'WC_Gateway_COD', + ); + + if ( $this->should_load_paypal_standard() ) { + $load_gateways[] = 'WC_Gateway_Paypal'; + } + + // Filter. + $load_gateways = apply_filters( 'woocommerce_payment_gateways', $load_gateways ); + + // Get sort order option. + $ordering = (array) get_option( 'woocommerce_gateway_order' ); + $order_end = 999; + + // Load gateways in order. + foreach ( $load_gateways as $gateway ) { + if ( is_string( $gateway ) && class_exists( $gateway ) ) { + $gateway = new $gateway(); + } + + // Gateways need to be valid and extend WC_Payment_Gateway. + if ( ! is_a( $gateway, 'WC_Payment_Gateway' ) ) { + continue; + } + + if ( isset( $ordering[ $gateway->id ] ) && is_numeric( $ordering[ $gateway->id ] ) ) { + // Add in position. + $this->payment_gateways[ $ordering[ $gateway->id ] ] = $gateway; + } else { + // Add to end of the array. + $this->payment_gateways[ $order_end ] = $gateway; + $order_end++; + } + } + + ksort( $this->payment_gateways ); + } + + /** + * Get gateways. + * + * @return array + */ + public function payment_gateways() { + $_available_gateways = array(); + + if ( count( $this->payment_gateways ) > 0 ) { + foreach ( $this->payment_gateways as $gateway ) { + $_available_gateways[ $gateway->id ] = $gateway; + } + } + + return $_available_gateways; + } + + /** + * Get array of registered gateway ids + * + * @since 2.6.0 + * @return array of strings + */ + public function get_payment_gateway_ids() { + return wp_list_pluck( $this->payment_gateways, 'id' ); + } + + /** + * Get available gateways. + * + * @return array + */ + public function get_available_payment_gateways() { + $_available_gateways = array(); + + foreach ( $this->payment_gateways as $gateway ) { + if ( $gateway->is_available() ) { + if ( ! is_add_payment_method_page() ) { + $_available_gateways[ $gateway->id ] = $gateway; + } elseif ( $gateway->supports( 'add_payment_method' ) || $gateway->supports( 'tokenization' ) ) { + $_available_gateways[ $gateway->id ] = $gateway; + } + } + } + + return array_filter( (array) apply_filters( 'woocommerce_available_payment_gateways', $_available_gateways ), array( $this, 'filter_valid_gateway_class' ) ); + } + + /** + * Callback for array filter. Returns true if gateway is of correct type. + * + * @since 3.6.0 + * @param object $gateway Gateway to check. + * @return bool + */ + protected function filter_valid_gateway_class( $gateway ) { + return $gateway && is_a( $gateway, 'WC_Payment_Gateway' ); + } + + /** + * Set the current, active gateway. + * + * @param array $gateways Available payment gateways. + */ + public function set_current_gateway( $gateways ) { + // Be on the defensive. + if ( ! is_array( $gateways ) || empty( $gateways ) ) { + return; + } + + $current_gateway = false; + + if ( WC()->session ) { + $current = WC()->session->get( 'chosen_payment_method' ); + + if ( $current && isset( $gateways[ $current ] ) ) { + $current_gateway = $gateways[ $current ]; + } + } + + if ( ! $current_gateway ) { + $current_gateway = current( $gateways ); + } + + // Ensure we can make a call to set_current() without triggering an error. + if ( $current_gateway && is_callable( array( $current_gateway, 'set_current' ) ) ) { + $current_gateway->set_current(); + } + } + + /** + * Save options in admin. + */ + public function process_admin_options() { + $gateway_order = isset( $_POST['gateway_order'] ) ? wc_clean( wp_unslash( $_POST['gateway_order'] ) ) : ''; // WPCS: input var ok, CSRF ok. + $order = array(); + + if ( is_array( $gateway_order ) && count( $gateway_order ) > 0 ) { + $loop = 0; + foreach ( $gateway_order as $gateway_id ) { + $order[ esc_attr( $gateway_id ) ] = $loop; + $loop++; + } + } + + update_option( 'woocommerce_gateway_order', $order ); + } + + /** + * Determines if PayPal Standard should be loaded. + * + * @since 5.5.0 + * @return bool Whether PayPal Standard should be loaded or not. + */ + protected function should_load_paypal_standard() { + $paypal = new WC_Gateway_Paypal(); + return $paypal->should_load(); + } +} diff --git a/includes/class-wc-payment-tokens.php b/plugins/woocommerce/includes/class-wc-payment-tokens.php similarity index 100% rename from includes/class-wc-payment-tokens.php rename to plugins/woocommerce/includes/class-wc-payment-tokens.php diff --git a/plugins/woocommerce/includes/class-wc-post-data.php b/plugins/woocommerce/includes/class-wc-post-data.php new file mode 100644 index 00000000000..8473de148c2 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-post-data.php @@ -0,0 +1,588 @@ +ID, $post->post_type ) && 'product_variation' === $post->post_type ) { + $variation = wc_get_product( $post->ID ); + + if ( $variation && $variation->get_parent_id() ) { + return $variation->get_permalink(); + } + } + return $permalink; + } + + /** + * Sync products queued to sync. + */ + public static function do_deferred_product_sync() { + global $wc_deferred_product_sync; + + if ( ! empty( $wc_deferred_product_sync ) ) { + $wc_deferred_product_sync = wp_parse_id_list( $wc_deferred_product_sync ); + array_walk( $wc_deferred_product_sync, array( __CLASS__, 'deferred_product_sync' ) ); + } + } + + /** + * Sync a product. + * + * @param int $product_id Product ID. + */ + public static function deferred_product_sync( $product_id ) { + $product = wc_get_product( $product_id ); + + if ( is_callable( array( $product, 'sync' ) ) ) { + $product->sync( $product ); + } + } + + /** + * When a post status changes. + * + * @param string $new_status New status. + * @param string $old_status Old status. + * @param WP_Post $post Post data. + */ + public static function transition_post_status( $new_status, $old_status, $post ) { + if ( ( 'publish' === $new_status || 'publish' === $old_status ) && in_array( $post->post_type, array( 'product', 'product_variation' ), true ) ) { + self::delete_product_query_transients(); + } + } + + /** + * Delete product view transients when needed e.g. when post status changes, or visibility/stock status is modified. + */ + public static function delete_product_query_transients() { + WC_Cache_Helper::get_transient_version( 'product_query', true ); + } + + /** + * Handle type changes. + * + * @since 3.0.0 + * + * @param WC_Product $product Product data. + * @param string $from Origin type. + * @param string $to New type. + */ + public static function product_type_changed( $product, $from, $to ) { + /** + * Filter to prevent variations from being deleted while switching from a variable product type to a variable product type. + * + * @since 5.0.0 + * + * @param bool A boolean value of true will delete the variations. + * @param WC_Product $product Product data. + * @return string $from Origin type. + * @param string $to New type. + */ + if ( apply_filters( 'woocommerce_delete_variations_on_product_type_change', 'variable' === $from && 'variable' !== $to, $product, $from, $to ) ) { + // If the product is no longer variable, we should ensure all variations are removed. + $data_store = WC_Data_Store::load( 'product-variable' ); + $data_store->delete_variations( $product->get_id(), true ); + } + } + + /** + * When editing a term, check for product attributes. + * + * @param int $term_id Term ID. + * @param int $tt_id Term taxonomy ID. + * @param string $taxonomy Taxonomy slug. + */ + public static function edit_term( $term_id, $tt_id, $taxonomy ) { + if ( strpos( $taxonomy, 'pa_' ) === 0 ) { + self::$editing_term = get_term_by( 'id', $term_id, $taxonomy ); + } else { + self::$editing_term = null; + } + } + + /** + * When a term is edited, check for product attributes and update variations. + * + * @param int $term_id Term ID. + * @param int $tt_id Term taxonomy ID. + * @param string $taxonomy Taxonomy slug. + */ + public static function edited_term( $term_id, $tt_id, $taxonomy ) { + if ( ! is_null( self::$editing_term ) && strpos( $taxonomy, 'pa_' ) === 0 ) { + $edited_term = get_term_by( 'id', $term_id, $taxonomy ); + + if ( $edited_term->slug !== self::$editing_term->slug ) { + global $wpdb; + + $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = %s WHERE meta_key = %s AND meta_value = %s;", $edited_term->slug, 'attribute_' . sanitize_title( $taxonomy ), self::$editing_term->slug ) ); + + $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->postmeta} SET meta_value = REPLACE( meta_value, %s, %s ) WHERE meta_key = '_default_attributes'", + serialize( self::$editing_term->taxonomy ) . serialize( self::$editing_term->slug ), + serialize( $edited_term->taxonomy ) . serialize( $edited_term->slug ) + ) + ); + } + } else { + self::$editing_term = null; + } + } + + /** + * Ensure floats are correctly converted to strings based on PHP locale. + * + * @param null $check Whether to allow updating metadata for the given type. + * @param int $object_id Object ID. + * @param string $meta_key Meta key. + * @param mixed $meta_value Meta value. Must be serializable if non-scalar. + * @param mixed $prev_value If specified, only update existing metadata entries with the specified value. Otherwise, update all entries. + * @return null|bool + */ + public static function update_order_item_metadata( $check, $object_id, $meta_key, $meta_value, $prev_value ) { + if ( ! empty( $meta_value ) && is_float( $meta_value ) ) { + + // Convert float to string. + $meta_value = wc_float_to_string( $meta_value ); + + // Update meta value with new string. + update_metadata( 'order_item', $object_id, $meta_key, $meta_value, $prev_value ); + + return true; + } + return $check; + } + + /** + * Ensure floats are correctly converted to strings based on PHP locale. + * + * @param null $check Whether to allow updating metadata for the given type. + * @param int $object_id Object ID. + * @param string $meta_key Meta key. + * @param mixed $meta_value Meta value. Must be serializable if non-scalar. + * @param mixed $prev_value If specified, only update existing metadata entries with the specified value. Otherwise, update all entries. + * @return null|bool + */ + public static function update_post_metadata( $check, $object_id, $meta_key, $meta_value, $prev_value ) { + // Delete product cache if someone uses meta directly. + if ( in_array( get_post_type( $object_id ), array( 'product', 'product_variation' ), true ) ) { + wp_cache_delete( 'product-' . $object_id, 'products' ); + } + + if ( ! empty( $meta_value ) && is_float( $meta_value ) && ! registered_meta_key_exists( 'post', $meta_key ) && in_array( get_post_type( $object_id ), array_merge( wc_get_order_types(), array( 'shop_coupon', 'product', 'product_variation' ) ), true ) ) { + + // Convert float to string. + $meta_value = wc_float_to_string( $meta_value ); + + // Update meta value with new string. + update_metadata( 'post', $object_id, $meta_key, $meta_value, $prev_value ); + + return true; + } + return $check; + } + + /** + * Forces the order posts to have a title in a certain format (containing the date). + * Forces certain product data based on the product's type, e.g. grouped products cannot have a parent. + * + * @param array $data An array of slashed post data. + * @return array + */ + public static function wp_insert_post_data( $data ) { + if ( 'shop_order' === $data['post_type'] && isset( $data['post_date'] ) ) { + $order_title = 'Order'; + if ( $data['post_date'] ) { + $order_title .= ' – ' . date_i18n( 'F j, Y @ h:i A', strtotime( $data['post_date'] ) ); + } + $data['post_title'] = $order_title; + } elseif ( 'product' === $data['post_type'] && isset( $_POST['product-type'] ) ) { // WPCS: input var ok, CSRF ok. + $product_type = wc_clean( wp_unslash( $_POST['product-type'] ) ); // WPCS: input var ok, CSRF ok. + switch ( $product_type ) { + case 'grouped': + case 'variable': + $data['post_parent'] = 0; + break; + } + } elseif ( 'product' === $data['post_type'] && 'auto-draft' === $data['post_status'] ) { + $data['post_title'] = 'AUTO-DRAFT'; + } elseif ( 'shop_coupon' === $data['post_type'] ) { + // Coupons should never allow unfiltered HTML. + $data['post_title'] = wp_filter_kses( $data['post_title'] ); + } + + return $data; + } + + /** + * Change embed data for certain post types. + * + * @since 3.2.0 + * @param array $data The response data. + * @param WP_Post $post The post object. + * @return array + */ + public static function filter_oembed_response_data( $data, $post ) { + if ( in_array( $post->post_type, array( 'shop_order', 'shop_coupon' ), true ) ) { + return array(); + } + return $data; + } + + /** + * Removes variations etc belonging to a deleted post, and clears transients. + * + * @param mixed $id ID of post being deleted. + */ + public static function delete_post( $id ) { + $container = wc_get_container(); + if ( ! $container->get( LegacyProxy::class )->call_function( 'current_user_can', 'delete_posts' ) || ! $id ) { + return; + } + + $post_type = self::get_post_type( $id ); + switch ( $post_type ) { + case 'product': + $data_store = WC_Data_Store::load( 'product-variable' ); + $data_store->delete_variations( $id, true ); + $data_store->delete_from_lookup_table( $id, 'wc_product_meta_lookup' ); + $container->get( ProductAttributesLookupDataStore::class )->on_product_deleted( $id ); + + $parent_id = wp_get_post_parent_id( $id ); + if ( $parent_id ) { + wc_delete_product_transients( $parent_id ); + } + + break; + case 'product_variation': + $data_store = WC_Data_Store::load( 'product' ); + $data_store->delete_from_lookup_table( $id, 'wc_product_meta_lookup' ); + wc_delete_product_transients( wp_get_post_parent_id( $id ) ); + $container->get( ProductAttributesLookupDataStore::class )->on_product_deleted( $id ); + + break; + case 'shop_order': + global $wpdb; + + $refunds = $wpdb->get_results( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type = 'shop_order_refund' AND post_parent = %d", $id ) ); + + if ( ! is_null( $refunds ) ) { + foreach ( $refunds as $refund ) { + wp_delete_post( $refund->ID, true ); + } + } + break; + } + } + + /** + * Trash post. + * + * @param mixed $id Post ID. + */ + public static function trash_post( $id ) { + if ( ! $id ) { + return; + } + + $post_type = self::get_post_type( $id ); + + // If this is an order, trash any refunds too. + if ( in_array( $post_type, wc_get_order_types( 'order-count' ), true ) ) { + global $wpdb; + + $refunds = $wpdb->get_results( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type = 'shop_order_refund' AND post_parent = %d", $id ) ); + + foreach ( $refunds as $refund ) { + $wpdb->update( $wpdb->posts, array( 'post_status' => 'trash' ), array( 'ID' => $refund->ID ) ); + } + + wc_delete_shop_order_transients( $id ); + + // If this is a product, trash children variations. + } elseif ( 'product' === $post_type ) { + $data_store = WC_Data_Store::load( 'product-variable' ); + $data_store->delete_variations( $id, false ); + wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_deleted( $id ); + } elseif ( 'product_variation' === $post_type ) { + wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_deleted( $id ); + } + } + + /** + * Untrash post. + * + * @param mixed $id Post ID. + */ + public static function untrash_post( $id ) { + if ( ! $id ) { + return; + } + + $post_type = self::get_post_type( $id ); + + if ( in_array( $post_type, wc_get_order_types( 'order-count' ), true ) ) { + global $wpdb; + + $refunds = $wpdb->get_results( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type = 'shop_order_refund' AND post_parent = %d", $id ) ); + + foreach ( $refunds as $refund ) { + $wpdb->update( $wpdb->posts, array( 'post_status' => 'wc-completed' ), array( 'ID' => $refund->ID ) ); + } + + wc_delete_shop_order_transients( $id ); + + } elseif ( 'product' === $post_type ) { + $data_store = WC_Data_Store::load( 'product-variable' ); + $data_store->untrash_variations( $id ); + + wc_product_force_unique_sku( $id ); + + wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_changed( $id ); + } elseif ( 'product_variation' === $post_type ) { + wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_changed( $id ); + } + } + + /** + * Get the post type for a given post. + * + * @param int $id The post id. + * @return string The post type. + */ + private static function get_post_type( $id ) { + return wc_get_container()->get( LegacyProxy::class )->call_function( 'get_post_type', $id ); + } + + /** + * Before deleting an order, do some cleanup. + * + * @since 3.2.0 + * @param int $order_id Order ID. + */ + public static function before_delete_order( $order_id ) { + if ( in_array( get_post_type( $order_id ), wc_get_order_types(), true ) ) { + // Clean up user. + $order = wc_get_order( $order_id ); + + // Check for `get_customer_id`, since this may be e.g. a refund order (which doesn't implement it). + $customer_id = is_callable( array( $order, 'get_customer_id' ) ) ? $order->get_customer_id() : 0; + + if ( $customer_id > 0 && 'shop_order' === $order->get_type() ) { + $customer = new WC_Customer( $customer_id ); + $order_count = $customer->get_order_count(); + $order_count --; + + if ( 0 === $order_count ) { + $customer->set_is_paying_customer( false ); + $customer->save(); + } + + // Delete order count and last order meta. + delete_user_meta( $customer_id, '_order_count' ); + delete_user_meta( $customer_id, '_last_order' ); + } + + // Clean up items. + self::delete_order_items( $order_id ); + self::delete_order_downloadable_permissions( $order_id ); + } + } + + /** + * Remove item meta on permanent deletion. + * + * @param int $postid Post ID. + */ + public static function delete_order_items( $postid ) { + global $wpdb; + + if ( in_array( get_post_type( $postid ), wc_get_order_types(), true ) ) { + do_action( 'woocommerce_delete_order_items', $postid ); + + $wpdb->query( + " + DELETE {$wpdb->prefix}woocommerce_order_items, {$wpdb->prefix}woocommerce_order_itemmeta + FROM {$wpdb->prefix}woocommerce_order_items + JOIN {$wpdb->prefix}woocommerce_order_itemmeta ON {$wpdb->prefix}woocommerce_order_items.order_item_id = {$wpdb->prefix}woocommerce_order_itemmeta.order_item_id + WHERE {$wpdb->prefix}woocommerce_order_items.order_id = '{$postid}'; + " + ); // WPCS: unprepared SQL ok. + + do_action( 'woocommerce_deleted_order_items', $postid ); + } + } + + /** + * Remove downloadable permissions on permanent order deletion. + * + * @param int $postid Post ID. + */ + public static function delete_order_downloadable_permissions( $postid ) { + if ( in_array( get_post_type( $postid ), wc_get_order_types(), true ) ) { + do_action( 'woocommerce_delete_order_downloadable_permissions', $postid ); + + $data_store = WC_Data_Store::load( 'customer-download' ); + $data_store->delete_by_order_id( $postid ); + + do_action( 'woocommerce_deleted_order_downloadable_permissions', $postid ); + } + } + + /** + * Flush meta cache for CRUD objects on direct update. + * + * @param int $meta_id Meta ID. + * @param int $object_id Object ID. + * @param string $meta_key Meta key. + * @param string $meta_value Meta value. + */ + public static function flush_object_meta_cache( $meta_id, $object_id, $meta_key, $meta_value ) { + WC_Cache_Helper::invalidate_cache_group( 'object_' . $object_id ); + } + + /** + * Ensure default category gets set. + * + * @since 3.3.0 + * @param int $object_id Product ID. + * @param array $terms Terms array. + * @param array $tt_ids Term ids array. + * @param string $taxonomy Taxonomy name. + * @param bool $append Are we appending or setting terms. + */ + public static function force_default_term( $object_id, $terms, $tt_ids, $taxonomy, $append ) { + if ( ! $append && 'product_cat' === $taxonomy && empty( $tt_ids ) && 'product' === get_post_type( $object_id ) ) { + $default_term = absint( get_option( 'default_product_cat', 0 ) ); + $tt_ids = array_map( 'absint', $tt_ids ); + + if ( $default_term && ! in_array( $default_term, $tt_ids, true ) ) { + wp_set_post_terms( $object_id, array( $default_term ), 'product_cat', true ); + } + } + } + + /** + * Ensure statuses are correctly reassigned when restoring orders and products. + * + * @param string $new_status The new status of the post being restored. + * @param int $post_id The ID of the post being restored. + * @param string $previous_status The status of the post at the point where it was trashed. + * @return string + */ + public static function wp_untrash_post_status( $new_status, $post_id, $previous_status ) { + $post_types = array( 'shop_order', 'shop_coupon', 'product', 'product_variation' ); + + if ( in_array( get_post_type( $post_id ), $post_types, true ) ) { + $new_status = $previous_status; + } + + return $new_status; + } + + /** + * When setting stock level, ensure the stock status is kept in sync. + * + * @param int $meta_id Meta ID. + * @param int $object_id Object ID. + * @param string $meta_key Meta key. + * @param mixed $meta_value Meta value. + * @deprecated 3.3 + */ + public static function sync_product_stock_status( $meta_id, $object_id, $meta_key, $meta_value ) {} + + /** + * Update changed downloads. + * + * @deprecated 3.3.0 No action is necessary on changes to download paths since download_id is no longer based on file hash. + * @param int $product_id Product ID. + * @param int $variation_id Variation ID. Optional product variation identifier. + * @param array $downloads Newly set files. + */ + public static function process_product_file_download_paths( $product_id, $variation_id, $downloads ) { + wc_deprecated_function( __FUNCTION__, '3.3' ); + } + + /** + * Delete transients when terms are set. + * + * @deprecated 3.6 + * @param int $object_id Object ID. + * @param mixed $terms An array of object terms. + * @param array $tt_ids An array of term taxonomy IDs. + * @param string $taxonomy Taxonomy slug. + * @param mixed $append Whether to append new terms to the old terms. + * @param array $old_tt_ids Old array of term taxonomy IDs. + */ + public static function set_object_terms( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) { + if ( in_array( get_post_type( $object_id ), array( 'product', 'product_variation' ), true ) ) { + self::delete_product_query_transients(); + } + } +} + +WC_Post_Data::init(); diff --git a/plugins/woocommerce/includes/class-wc-post-types.php b/plugins/woocommerce/includes/class-wc-post-types.php new file mode 100644 index 00000000000..29481568841 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-post-types.php @@ -0,0 +1,685 @@ + false, + 'show_ui' => false, + 'show_in_nav_menus' => false, + 'query_var' => is_admin(), + 'rewrite' => false, + 'public' => false, + 'label' => _x( 'Product type', 'Taxonomy name', 'woocommerce' ), + ) + ) + ); + + register_taxonomy( + 'product_visibility', + apply_filters( 'woocommerce_taxonomy_objects_product_visibility', array( 'product', 'product_variation' ) ), + apply_filters( + 'woocommerce_taxonomy_args_product_visibility', + array( + 'hierarchical' => false, + 'show_ui' => false, + 'show_in_nav_menus' => false, + 'query_var' => is_admin(), + 'rewrite' => false, + 'public' => false, + 'label' => _x( 'Product visibility', 'Taxonomy name', 'woocommerce' ), + ) + ) + ); + + register_taxonomy( + 'product_cat', + apply_filters( 'woocommerce_taxonomy_objects_product_cat', array( 'product' ) ), + apply_filters( + 'woocommerce_taxonomy_args_product_cat', + array( + 'hierarchical' => true, + 'update_count_callback' => '_wc_term_recount', + 'label' => __( 'Categories', 'woocommerce' ), + 'labels' => array( + 'name' => __( 'Product categories', 'woocommerce' ), + 'singular_name' => __( 'Category', 'woocommerce' ), + 'menu_name' => _x( 'Categories', 'Admin menu name', 'woocommerce' ), + 'search_items' => __( 'Search categories', 'woocommerce' ), + 'all_items' => __( 'All categories', 'woocommerce' ), + 'parent_item' => __( 'Parent category', 'woocommerce' ), + 'parent_item_colon' => __( 'Parent category:', 'woocommerce' ), + 'edit_item' => __( 'Edit category', 'woocommerce' ), + 'update_item' => __( 'Update category', 'woocommerce' ), + 'add_new_item' => __( 'Add new category', 'woocommerce' ), + 'new_item_name' => __( 'New category name', 'woocommerce' ), + 'not_found' => __( 'No categories found', 'woocommerce' ), + 'item_link' => __( 'Product Category Link', 'woocommerce' ), + 'item_link_description' => __( 'A link to a product category.', 'woocommerce' ), + ), + 'show_in_rest' => true, + 'show_ui' => true, + 'query_var' => true, + 'capabilities' => array( + 'manage_terms' => 'manage_product_terms', + 'edit_terms' => 'edit_product_terms', + 'delete_terms' => 'delete_product_terms', + 'assign_terms' => 'assign_product_terms', + ), + 'rewrite' => array( + 'slug' => $permalinks['category_rewrite_slug'], + 'with_front' => false, + 'hierarchical' => true, + ), + ) + ) + ); + + register_taxonomy( + 'product_tag', + apply_filters( 'woocommerce_taxonomy_objects_product_tag', array( 'product' ) ), + apply_filters( + 'woocommerce_taxonomy_args_product_tag', + array( + 'hierarchical' => false, + 'update_count_callback' => '_wc_term_recount', + 'label' => __( 'Product tags', 'woocommerce' ), + 'labels' => array( + 'name' => __( 'Product tags', 'woocommerce' ), + 'singular_name' => __( 'Tag', 'woocommerce' ), + 'menu_name' => _x( 'Tags', 'Admin menu name', 'woocommerce' ), + 'search_items' => __( 'Search tags', 'woocommerce' ), + 'all_items' => __( 'All tags', 'woocommerce' ), + 'edit_item' => __( 'Edit tag', 'woocommerce' ), + 'update_item' => __( 'Update tag', 'woocommerce' ), + 'add_new_item' => __( 'Add new tag', 'woocommerce' ), + 'new_item_name' => __( 'New tag name', 'woocommerce' ), + 'popular_items' => __( 'Popular tags', 'woocommerce' ), + 'separate_items_with_commas' => __( 'Separate tags with commas', 'woocommerce' ), + 'add_or_remove_items' => __( 'Add or remove tags', 'woocommerce' ), + 'choose_from_most_used' => __( 'Choose from the most used tags', 'woocommerce' ), + 'not_found' => __( 'No tags found', 'woocommerce' ), + 'item_link' => __( 'Product Tag Link', 'woocommerce' ), + 'item_link_description' => __( 'A link to a product tag.', 'woocommerce' ), + ), + 'show_in_rest' => true, + 'show_ui' => true, + 'query_var' => true, + 'capabilities' => array( + 'manage_terms' => 'manage_product_terms', + 'edit_terms' => 'edit_product_terms', + 'delete_terms' => 'delete_product_terms', + 'assign_terms' => 'assign_product_terms', + ), + 'rewrite' => array( + 'slug' => $permalinks['tag_rewrite_slug'], + 'with_front' => false, + ), + ) + ) + ); + + register_taxonomy( + 'product_shipping_class', + apply_filters( 'woocommerce_taxonomy_objects_product_shipping_class', array( 'product', 'product_variation' ) ), + apply_filters( + 'woocommerce_taxonomy_args_product_shipping_class', + array( + 'hierarchical' => false, + 'update_count_callback' => '_update_post_term_count', + 'label' => __( 'Shipping classes', 'woocommerce' ), + 'labels' => array( + 'name' => __( 'Product shipping classes', 'woocommerce' ), + 'singular_name' => __( 'Shipping class', 'woocommerce' ), + 'menu_name' => _x( 'Shipping classes', 'Admin menu name', 'woocommerce' ), + 'search_items' => __( 'Search shipping classes', 'woocommerce' ), + 'all_items' => __( 'All shipping classes', 'woocommerce' ), + 'parent_item' => __( 'Parent shipping class', 'woocommerce' ), + 'parent_item_colon' => __( 'Parent shipping class:', 'woocommerce' ), + 'edit_item' => __( 'Edit shipping class', 'woocommerce' ), + 'update_item' => __( 'Update shipping class', 'woocommerce' ), + 'add_new_item' => __( 'Add new shipping class', 'woocommerce' ), + 'new_item_name' => __( 'New shipping class Name', 'woocommerce' ), + ), + 'show_ui' => false, + 'show_in_quick_edit' => false, + 'show_in_nav_menus' => false, + 'query_var' => is_admin(), + 'capabilities' => array( + 'manage_terms' => 'manage_product_terms', + 'edit_terms' => 'edit_product_terms', + 'delete_terms' => 'delete_product_terms', + 'assign_terms' => 'assign_product_terms', + ), + 'rewrite' => false, + ) + ) + ); + + global $wc_product_attributes; + + $wc_product_attributes = array(); + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + if ( $attribute_taxonomies ) { + foreach ( $attribute_taxonomies as $tax ) { + $name = wc_attribute_taxonomy_name( $tax->attribute_name ); + + if ( $name ) { + $tax->attribute_public = absint( isset( $tax->attribute_public ) ? $tax->attribute_public : 1 ); + $label = ! empty( $tax->attribute_label ) ? $tax->attribute_label : $tax->attribute_name; + $wc_product_attributes[ $name ] = $tax; + $taxonomy_data = array( + 'hierarchical' => false, + 'update_count_callback' => '_update_post_term_count', + 'labels' => array( + /* translators: %s: attribute name */ + 'name' => sprintf( _x( 'Product %s', 'Product Attribute', 'woocommerce' ), $label ), + 'singular_name' => $label, + /* translators: %s: attribute name */ + 'search_items' => sprintf( __( 'Search %s', 'woocommerce' ), $label ), + /* translators: %s: attribute name */ + 'all_items' => sprintf( __( 'All %s', 'woocommerce' ), $label ), + /* translators: %s: attribute name */ + 'parent_item' => sprintf( __( 'Parent %s', 'woocommerce' ), $label ), + /* translators: %s: attribute name */ + 'parent_item_colon' => sprintf( __( 'Parent %s:', 'woocommerce' ), $label ), + /* translators: %s: attribute name */ + 'edit_item' => sprintf( __( 'Edit %s', 'woocommerce' ), $label ), + /* translators: %s: attribute name */ + 'update_item' => sprintf( __( 'Update %s', 'woocommerce' ), $label ), + /* translators: %s: attribute name */ + 'add_new_item' => sprintf( __( 'Add new %s', 'woocommerce' ), $label ), + /* translators: %s: attribute name */ + 'new_item_name' => sprintf( __( 'New %s', 'woocommerce' ), $label ), + /* translators: %s: attribute name */ + 'not_found' => sprintf( __( 'No "%s" found', 'woocommerce' ), $label ), + /* translators: %s: attribute name */ + 'back_to_items' => sprintf( __( '← Back to "%s" attributes', 'woocommerce' ), $label ), + ), + 'show_ui' => true, + 'show_in_quick_edit' => false, + 'show_in_menu' => false, + 'meta_box_cb' => false, + 'query_var' => 1 === $tax->attribute_public, + 'rewrite' => false, + 'sort' => false, + 'public' => 1 === $tax->attribute_public, + 'show_in_nav_menus' => 1 === $tax->attribute_public && apply_filters( 'woocommerce_attribute_show_in_nav_menus', false, $name ), + 'capabilities' => array( + 'manage_terms' => 'manage_product_terms', + 'edit_terms' => 'edit_product_terms', + 'delete_terms' => 'delete_product_terms', + 'assign_terms' => 'assign_product_terms', + ), + ); + + if ( 1 === $tax->attribute_public && sanitize_title( $tax->attribute_name ) ) { + $taxonomy_data['rewrite'] = array( + 'slug' => trailingslashit( $permalinks['attribute_rewrite_slug'] ) . urldecode( sanitize_title( $tax->attribute_name ) ), + 'with_front' => false, + 'hierarchical' => true, + ); + } + + register_taxonomy( $name, apply_filters( "woocommerce_taxonomy_objects_{$name}", array( 'product' ) ), apply_filters( "woocommerce_taxonomy_args_{$name}", $taxonomy_data ) ); + } + } + } + + do_action( 'woocommerce_after_register_taxonomy' ); + } + + /** + * Register core post types. + */ + public static function register_post_types() { + if ( ! is_blog_installed() || post_type_exists( 'product' ) ) { + return; + } + + do_action( 'woocommerce_register_post_type' ); + + $permalinks = wc_get_permalink_structure(); + $supports = array( 'title', 'editor', 'excerpt', 'thumbnail', 'custom-fields', 'publicize', 'wpcom-markdown' ); + + if ( 'yes' === get_option( 'woocommerce_enable_reviews', 'yes' ) ) { + $supports[] = 'comments'; + } + + $shop_page_id = wc_get_page_id( 'shop' ); + + if ( wc_current_theme_supports_woocommerce_or_fse() ) { + $has_archive = $shop_page_id && get_post( $shop_page_id ) ? urldecode( get_page_uri( $shop_page_id ) ) : 'shop'; + } else { + $has_archive = false; + } + + // If theme support changes, we may need to flush permalinks since some are changed based on this flag. + $theme_support = wc_current_theme_supports_woocommerce_or_fse() ? 'yes' : 'no'; + if ( get_option( 'current_theme_supports_woocommerce' ) !== $theme_support && update_option( 'current_theme_supports_woocommerce', $theme_support ) ) { + update_option( 'woocommerce_queue_flush_rewrite_rules', 'yes' ); + } + + register_post_type( + 'product', + apply_filters( + 'woocommerce_register_post_type_product', + array( + 'labels' => array( + 'name' => __( 'Products', 'woocommerce' ), + 'singular_name' => __( 'Product', 'woocommerce' ), + 'all_items' => __( 'All Products', 'woocommerce' ), + 'menu_name' => _x( 'Products', 'Admin menu name', 'woocommerce' ), + 'add_new' => __( 'Add New', 'woocommerce' ), + 'add_new_item' => __( 'Add new product', 'woocommerce' ), + 'edit' => __( 'Edit', 'woocommerce' ), + 'edit_item' => __( 'Edit product', 'woocommerce' ), + 'new_item' => __( 'New product', 'woocommerce' ), + 'view_item' => __( 'View product', 'woocommerce' ), + 'view_items' => __( 'View products', 'woocommerce' ), + 'search_items' => __( 'Search products', 'woocommerce' ), + 'not_found' => __( 'No products found', 'woocommerce' ), + 'not_found_in_trash' => __( 'No products found in trash', 'woocommerce' ), + 'parent' => __( 'Parent product', 'woocommerce' ), + 'featured_image' => __( 'Product image', 'woocommerce' ), + 'set_featured_image' => __( 'Set product image', 'woocommerce' ), + 'remove_featured_image' => __( 'Remove product image', 'woocommerce' ), + 'use_featured_image' => __( 'Use as product image', 'woocommerce' ), + 'insert_into_item' => __( 'Insert into product', 'woocommerce' ), + 'uploaded_to_this_item' => __( 'Uploaded to this product', 'woocommerce' ), + 'filter_items_list' => __( 'Filter products', 'woocommerce' ), + 'items_list_navigation' => __( 'Products navigation', 'woocommerce' ), + 'items_list' => __( 'Products list', 'woocommerce' ), + 'item_link' => __( 'Product Link', 'woocommerce' ), + 'item_link_description' => __( 'A link to a product.', 'woocommerce' ), + ), + 'description' => __( 'This is where you can browse products in this store.', 'woocommerce' ), + 'public' => true, + 'show_ui' => true, + 'menu_icon' => 'dashicons-archive', + 'capability_type' => 'product', + 'map_meta_cap' => true, + 'publicly_queryable' => true, + 'exclude_from_search' => false, + 'hierarchical' => false, // Hierarchical causes memory issues - WP loads all records! + 'rewrite' => $permalinks['product_rewrite_slug'] ? array( + 'slug' => $permalinks['product_rewrite_slug'], + 'with_front' => false, + 'feeds' => true, + ) : false, + 'query_var' => true, + 'supports' => $supports, + 'has_archive' => $has_archive, + 'show_in_nav_menus' => true, + 'show_in_rest' => true, + ) + ) + ); + + register_post_type( + 'product_variation', + apply_filters( + 'woocommerce_register_post_type_product_variation', + array( + 'label' => __( 'Variations', 'woocommerce' ), + 'public' => false, + 'hierarchical' => false, + 'supports' => false, + 'capability_type' => 'product', + 'rewrite' => false, + ) + ) + ); + + wc_register_order_type( + 'shop_order', + apply_filters( + 'woocommerce_register_post_type_shop_order', + array( + 'labels' => array( + 'name' => __( 'Orders', 'woocommerce' ), + 'singular_name' => _x( 'Order', 'shop_order post type singular name', 'woocommerce' ), + 'add_new' => __( 'Add order', 'woocommerce' ), + 'add_new_item' => __( 'Add new order', 'woocommerce' ), + 'edit' => __( 'Edit', 'woocommerce' ), + 'edit_item' => __( 'Edit order', 'woocommerce' ), + 'new_item' => __( 'New order', 'woocommerce' ), + 'view_item' => __( 'View order', 'woocommerce' ), + 'search_items' => __( 'Search orders', 'woocommerce' ), + 'not_found' => __( 'No orders found', 'woocommerce' ), + 'not_found_in_trash' => __( 'No orders found in trash', 'woocommerce' ), + 'parent' => __( 'Parent orders', 'woocommerce' ), + 'menu_name' => _x( 'Orders', 'Admin menu name', 'woocommerce' ), + 'filter_items_list' => __( 'Filter orders', 'woocommerce' ), + 'items_list_navigation' => __( 'Orders navigation', 'woocommerce' ), + 'items_list' => __( 'Orders list', 'woocommerce' ), + ), + 'description' => __( 'This is where store orders are stored.', 'woocommerce' ), + 'public' => false, + 'show_ui' => true, + 'capability_type' => 'shop_order', + 'map_meta_cap' => true, + 'publicly_queryable' => false, + 'exclude_from_search' => true, + 'show_in_menu' => current_user_can( 'edit_others_shop_orders' ) ? 'woocommerce' : true, + 'hierarchical' => false, + 'show_in_nav_menus' => false, + 'rewrite' => false, + 'query_var' => false, + 'supports' => array( 'title', 'comments', 'custom-fields' ), + 'has_archive' => false, + ) + ) + ); + + wc_register_order_type( + 'shop_order_refund', + apply_filters( + 'woocommerce_register_post_type_shop_order_refund', + array( + 'label' => __( 'Refunds', 'woocommerce' ), + 'capability_type' => 'shop_order', + 'public' => false, + 'hierarchical' => false, + 'supports' => false, + 'exclude_from_orders_screen' => false, + 'add_order_meta_boxes' => false, + 'exclude_from_order_count' => true, + 'exclude_from_order_views' => false, + 'exclude_from_order_reports' => false, + 'exclude_from_order_sales_reports' => true, + 'class_name' => 'WC_Order_Refund', + 'rewrite' => false, + ) + ) + ); + + if ( 'yes' === get_option( 'woocommerce_enable_coupons' ) ) { + register_post_type( + 'shop_coupon', + apply_filters( + 'woocommerce_register_post_type_shop_coupon', + array( + 'labels' => array( + 'name' => __( 'Coupons', 'woocommerce' ), + 'singular_name' => __( 'Coupon', 'woocommerce' ), + 'menu_name' => _x( 'Coupons', 'Admin menu name', 'woocommerce' ), + 'add_new' => __( 'Add coupon', 'woocommerce' ), + 'add_new_item' => __( 'Add new coupon', 'woocommerce' ), + 'edit' => __( 'Edit', 'woocommerce' ), + 'edit_item' => __( 'Edit coupon', 'woocommerce' ), + 'new_item' => __( 'New coupon', 'woocommerce' ), + 'view_item' => __( 'View coupon', 'woocommerce' ), + 'search_items' => __( 'Search coupons', 'woocommerce' ), + 'not_found' => __( 'No coupons found', 'woocommerce' ), + 'not_found_in_trash' => __( 'No coupons found in trash', 'woocommerce' ), + 'parent' => __( 'Parent coupon', 'woocommerce' ), + 'filter_items_list' => __( 'Filter coupons', 'woocommerce' ), + 'items_list_navigation' => __( 'Coupons navigation', 'woocommerce' ), + 'items_list' => __( 'Coupons list', 'woocommerce' ), + ), + 'description' => __( 'This is where you can add new coupons that customers can use in your store.', 'woocommerce' ), + 'public' => false, + 'show_ui' => true, + 'capability_type' => 'shop_coupon', + 'map_meta_cap' => true, + 'publicly_queryable' => false, + 'exclude_from_search' => true, + 'show_in_menu' => current_user_can( 'edit_others_shop_orders' ) ? 'woocommerce' : true, + 'hierarchical' => false, + 'rewrite' => false, + 'query_var' => false, + 'supports' => array( 'title' ), + 'show_in_nav_menus' => false, + 'show_in_admin_bar' => true, + ) + ) + ); + } + + do_action( 'woocommerce_after_register_post_type' ); + } + + /** + * Customize taxonomies update messages. + * + * @param array $messages The list of available messages. + * @since 4.4.0 + * @return bool + */ + public static function updated_term_messages( $messages ) { + $messages['product_cat'] = array( + 0 => '', + 1 => __( 'Category added.', 'woocommerce' ), + 2 => __( 'Category deleted.', 'woocommerce' ), + 3 => __( 'Category updated.', 'woocommerce' ), + 4 => __( 'Category not added.', 'woocommerce' ), + 5 => __( 'Category not updated.', 'woocommerce' ), + 6 => __( 'Categories deleted.', 'woocommerce' ), + ); + + $messages['product_tag'] = array( + 0 => '', + 1 => __( 'Tag added.', 'woocommerce' ), + 2 => __( 'Tag deleted.', 'woocommerce' ), + 3 => __( 'Tag updated.', 'woocommerce' ), + 4 => __( 'Tag not added.', 'woocommerce' ), + 5 => __( 'Tag not updated.', 'woocommerce' ), + 6 => __( 'Tags deleted.', 'woocommerce' ), + ); + + $wc_product_attributes = array(); + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + if ( $attribute_taxonomies ) { + foreach ( $attribute_taxonomies as $tax ) { + $name = wc_attribute_taxonomy_name( $tax->attribute_name ); + + if ( $name ) { + $label = ! empty( $tax->attribute_label ) ? $tax->attribute_label : $tax->attribute_name; + + $messages[ $name ] = array( + 0 => '', + /* translators: %s: taxonomy label */ + 1 => sprintf( _x( '%s added', 'taxonomy term messages', 'woocommerce' ), $label ), + /* translators: %s: taxonomy label */ + 2 => sprintf( _x( '%s deleted', 'taxonomy term messages', 'woocommerce' ), $label ), + /* translators: %s: taxonomy label */ + 3 => sprintf( _x( '%s updated', 'taxonomy term messages', 'woocommerce' ), $label ), + /* translators: %s: taxonomy label */ + 4 => sprintf( _x( '%s not added', 'taxonomy term messages', 'woocommerce' ), $label ), + /* translators: %s: taxonomy label */ + 5 => sprintf( _x( '%s not updated', 'taxonomy term messages', 'woocommerce' ), $label ), + /* translators: %s: taxonomy label */ + 6 => sprintf( _x( '%s deleted', 'taxonomy term messages', 'woocommerce' ), $label ), + ); + } + } + } + + return $messages; + } + + /** + * Register our custom post statuses, used for order status. + */ + public static function register_post_status() { + + $order_statuses = apply_filters( + 'woocommerce_register_shop_order_post_statuses', + array( + 'wc-pending' => array( + 'label' => _x( 'Pending payment', 'Order status', 'woocommerce' ), + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + /* translators: %s: number of orders */ + 'label_count' => _n_noop( 'Pending payment (%s)', 'Pending payment (%s)', 'woocommerce' ), + ), + 'wc-processing' => array( + 'label' => _x( 'Processing', 'Order status', 'woocommerce' ), + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + /* translators: %s: number of orders */ + 'label_count' => _n_noop( 'Processing (%s)', 'Processing (%s)', 'woocommerce' ), + ), + 'wc-on-hold' => array( + 'label' => _x( 'On hold', 'Order status', 'woocommerce' ), + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + /* translators: %s: number of orders */ + 'label_count' => _n_noop( 'On hold (%s)', 'On hold (%s)', 'woocommerce' ), + ), + 'wc-completed' => array( + 'label' => _x( 'Completed', 'Order status', 'woocommerce' ), + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + /* translators: %s: number of orders */ + 'label_count' => _n_noop( 'Completed (%s)', 'Completed (%s)', 'woocommerce' ), + ), + 'wc-cancelled' => array( + 'label' => _x( 'Cancelled', 'Order status', 'woocommerce' ), + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + /* translators: %s: number of orders */ + 'label_count' => _n_noop( 'Cancelled (%s)', 'Cancelled (%s)', 'woocommerce' ), + ), + 'wc-refunded' => array( + 'label' => _x( 'Refunded', 'Order status', 'woocommerce' ), + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + /* translators: %s: number of orders */ + 'label_count' => _n_noop( 'Refunded (%s)', 'Refunded (%s)', 'woocommerce' ), + ), + 'wc-failed' => array( + 'label' => _x( 'Failed', 'Order status', 'woocommerce' ), + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + /* translators: %s: number of orders */ + 'label_count' => _n_noop( 'Failed (%s)', 'Failed (%s)', 'woocommerce' ), + ), + ) + ); + + foreach ( $order_statuses as $order_status => $values ) { + register_post_status( $order_status, $values ); + } + } + + /** + * Flush rules if the event is queued. + * + * @since 3.3.0 + */ + public static function maybe_flush_rewrite_rules() { + if ( 'yes' === get_option( 'woocommerce_queue_flush_rewrite_rules' ) ) { + update_option( 'woocommerce_queue_flush_rewrite_rules', 'no' ); + self::flush_rewrite_rules(); + } + } + + /** + * Flush rewrite rules. + */ + public static function flush_rewrite_rules() { + flush_rewrite_rules(); + } + + /** + * Disable Gutenberg for products. + * + * @param bool $can_edit Whether the post type can be edited or not. + * @param string $post_type The post type being checked. + * @return bool + */ + public static function gutenberg_can_edit_post_type( $can_edit, $post_type ) { + return 'product' === $post_type ? false : $can_edit; + } + + /** + * Add Product Support to Jetpack Omnisearch. + */ + public static function support_jetpack_omnisearch() { + if ( class_exists( 'Jetpack_Omnisearch_Posts' ) ) { + new Jetpack_Omnisearch_Posts( 'product' ); + } + } + + /** + * Added product for Jetpack related posts. + * + * @param array $post_types Post types. + * @return array + */ + public static function rest_api_allowed_post_types( $post_types ) { + $post_types[] = 'product'; + + return $post_types; + } +} + +WC_Post_types::init(); diff --git a/includes/class-wc-privacy-background-process.php b/plugins/woocommerce/includes/class-wc-privacy-background-process.php similarity index 100% rename from includes/class-wc-privacy-background-process.php rename to plugins/woocommerce/includes/class-wc-privacy-background-process.php diff --git a/plugins/woocommerce/includes/class-wc-privacy-erasers.php b/plugins/woocommerce/includes/class-wc-privacy-erasers.php new file mode 100644 index 00000000000..69cba905dce --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-privacy-erasers.php @@ -0,0 +1,414 @@ + false, + 'items_retained' => false, + 'messages' => array(), + 'done' => true, + ); + + $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. + + if ( ! $user instanceof WP_User ) { + return $response; + } + + $customer = new WC_Customer( $user->ID ); + + if ( ! $customer ) { + return $response; + } + + $props_to_erase = apply_filters( + 'woocommerce_privacy_erase_customer_personal_data_props', + array( + 'billing_first_name' => __( 'Billing First Name', 'woocommerce' ), + 'billing_last_name' => __( 'Billing Last Name', 'woocommerce' ), + 'billing_company' => __( 'Billing Company', 'woocommerce' ), + 'billing_address_1' => __( 'Billing Address 1', 'woocommerce' ), + 'billing_address_2' => __( 'Billing Address 2', 'woocommerce' ), + 'billing_city' => __( 'Billing City', 'woocommerce' ), + 'billing_postcode' => __( 'Billing Postal/Zip Code', 'woocommerce' ), + 'billing_state' => __( 'Billing State', 'woocommerce' ), + 'billing_country' => __( 'Billing Country / Region', 'woocommerce' ), + 'billing_phone' => __( 'Billing Phone Number', 'woocommerce' ), + 'billing_email' => __( 'Email Address', 'woocommerce' ), + 'shipping_first_name' => __( 'Shipping First Name', 'woocommerce' ), + 'shipping_last_name' => __( 'Shipping Last Name', 'woocommerce' ), + 'shipping_company' => __( 'Shipping Company', 'woocommerce' ), + 'shipping_address_1' => __( 'Shipping Address 1', 'woocommerce' ), + 'shipping_address_2' => __( 'Shipping Address 2', 'woocommerce' ), + 'shipping_city' => __( 'Shipping City', 'woocommerce' ), + 'shipping_postcode' => __( 'Shipping Postal/Zip Code', 'woocommerce' ), + 'shipping_state' => __( 'Shipping State', 'woocommerce' ), + 'shipping_country' => __( 'Shipping Country / Region', 'woocommerce' ), + 'shipping_phone' => __( 'Shipping Phone Number', 'woocommerce' ), + ), + $customer + ); + + foreach ( $props_to_erase as $prop => $label ) { + $erased = false; + + if ( is_callable( array( $customer, 'get_' . $prop ) ) && is_callable( array( $customer, 'set_' . $prop ) ) ) { + $value = $customer->{"get_$prop"}( 'edit' ); + + if ( $value ) { + $customer->{"set_$prop"}( '' ); + $erased = true; + } + } + + $erased = apply_filters( 'woocommerce_privacy_erase_customer_personal_data_prop', $erased, $prop, $customer ); + + if ( $erased ) { + /* Translators: %s Prop name. */ + $response['messages'][] = sprintf( __( 'Removed customer "%s"', 'woocommerce' ), $label ); + $response['items_removed'] = true; + } + } + + $customer->save(); + + /** + * Allow extensions to remove data for this customer and adjust the response. + * + * @since 3.4.0 + * @param array $response Array response data. Must include messages, num_items_removed, num_items_retained, done. + * @param WC_Order $order A customer object. + */ + return apply_filters( 'woocommerce_privacy_erase_personal_data_customer', $response, $customer ); + } + + /** + * Finds and erases data which could be used to identify a person from WooCommerce data associated with an email address. + * + * Orders are erased in blocks of 10 to avoid timeouts. + * + * @since 3.4.0 + * @param string $email_address The user email address. + * @param int $page Page. + * @return array An array of personal data in name value pairs + */ + public static function order_data_eraser( $email_address, $page ) { + $page = (int) $page; + $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. + $erasure_enabled = wc_string_to_bool( get_option( 'woocommerce_erasure_request_removes_order_data', 'no' ) ); + $response = array( + 'items_removed' => false, + 'items_retained' => false, + 'messages' => array(), + 'done' => true, + ); + + $order_query = array( + 'limit' => 10, + 'page' => $page, + 'customer' => array( $email_address ), + ); + + if ( $user instanceof WP_User ) { + $order_query['customer'][] = (int) $user->ID; + } + + $orders = wc_get_orders( $order_query ); + + if ( 0 < count( $orders ) ) { + foreach ( $orders as $order ) { + if ( apply_filters( 'woocommerce_privacy_erase_order_personal_data', $erasure_enabled, $order ) ) { + self::remove_order_personal_data( $order ); + + /* Translators: %s Order number. */ + $response['messages'][] = sprintf( __( 'Removed personal data from order %s.', 'woocommerce' ), $order->get_order_number() ); + $response['items_removed'] = true; + } else { + /* Translators: %s Order number. */ + $response['messages'][] = sprintf( __( 'Personal data within order %s has been retained.', 'woocommerce' ), $order->get_order_number() ); + $response['items_retained'] = true; + } + } + $response['done'] = 10 > count( $orders ); + } else { + $response['done'] = true; + } + + return $response; + } + + /** + * Finds and removes customer download logs by email address. + * + * @since 3.4.0 + * @param string $email_address The user email address. + * @param int $page Page. + * @return array An array of personal data in name value pairs + */ + public static function download_data_eraser( $email_address, $page ) { + $page = (int) $page; + $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. + $erasure_enabled = wc_string_to_bool( get_option( 'woocommerce_erasure_request_removes_download_data', 'no' ) ); + $response = array( + 'items_removed' => false, + 'items_retained' => false, + 'messages' => array(), + 'done' => true, + ); + + $downloads_query = array( + 'limit' => -1, + 'page' => $page, + 'return' => 'ids', + ); + + if ( $user instanceof WP_User ) { + $downloads_query['user_id'] = (int) $user->ID; + } else { + $downloads_query['user_email'] = $email_address; + } + + $customer_download_data_store = WC_Data_Store::load( 'customer-download' ); + + // Revoke download permissions. + if ( apply_filters( 'woocommerce_privacy_erase_download_personal_data', $erasure_enabled, $email_address ) ) { + if ( $user instanceof WP_User ) { + $result = $customer_download_data_store->delete_by_user_id( (int) $user->ID ); + } else { + $result = $customer_download_data_store->delete_by_user_email( $email_address ); + } + if ( $result ) { + $response['messages'][] = __( 'Removed access to downloadable files.', 'woocommerce' ); + $response['items_removed'] = true; + } + } else { + $response['messages'][] = __( 'Customer download permissions have been retained.', 'woocommerce' ); + $response['items_retained'] = true; + } + + return $response; + } + + /** + * Remove personal data specific to WooCommerce from an order object. + * + * Note; this will hinder order processing for obvious reasons! + * + * @param WC_Order $order Order object. + */ + public static function remove_order_personal_data( $order ) { + $anonymized_data = array(); + + /** + * Allow extensions to remove their own personal data for this order first, so order data is still available. + * + * @since 3.4.0 + * @param WC_Order $order A customer object. + */ + do_action( 'woocommerce_privacy_before_remove_order_personal_data', $order ); + + /** + * Expose props and data types we'll be anonymizing. + * + * @since 3.4.0 + * @param array $props Keys are the prop names, values are the data type we'll be passing to wp_privacy_anonymize_data(). + * @param WC_Order $order A customer object. + */ + $props_to_remove = apply_filters( + 'woocommerce_privacy_remove_order_personal_data_props', + array( + 'customer_ip_address' => 'ip', + 'customer_user_agent' => 'text', + 'billing_first_name' => 'text', + 'billing_last_name' => 'text', + 'billing_company' => 'text', + 'billing_address_1' => 'text', + 'billing_address_2' => 'text', + 'billing_city' => 'text', + 'billing_postcode' => 'text', + 'billing_state' => 'address_state', + 'billing_country' => 'address_country', + 'billing_phone' => 'phone', + 'billing_email' => 'email', + 'shipping_first_name' => 'text', + 'shipping_last_name' => 'text', + 'shipping_company' => 'text', + 'shipping_address_1' => 'text', + 'shipping_address_2' => 'text', + 'shipping_city' => 'text', + 'shipping_postcode' => 'text', + 'shipping_state' => 'address_state', + 'shipping_country' => 'address_country', + 'shipping_phone' => 'phone', + 'customer_id' => 'numeric_id', + 'transaction_id' => 'numeric_id', + ), + $order + ); + + if ( ! empty( $props_to_remove ) && is_array( $props_to_remove ) ) { + foreach ( $props_to_remove as $prop => $data_type ) { + // Get the current value in edit context. + $value = $order->{"get_$prop"}( 'edit' ); + + // If the value is empty, it does not need to be anonymized. + if ( empty( $value ) || empty( $data_type ) ) { + continue; + } + + $anon_value = function_exists( 'wp_privacy_anonymize_data' ) ? wp_privacy_anonymize_data( $data_type, $value ) : ''; + + /** + * Expose a way to control the anonymized value of a prop via 3rd party code. + * + * @since 3.4.0 + * @param string $anon_value Value of this prop after anonymization. + * @param string $prop Name of the prop being removed. + * @param string $value Current value of the data. + * @param string $data_type Type of data. + * @param WC_Order $order An order object. + */ + $anonymized_data[ $prop ] = apply_filters( 'woocommerce_privacy_remove_order_personal_data_prop_value', $anon_value, $prop, $value, $data_type, $order ); + } + } + + // Set all new props and persist the new data to the database. + $order->set_props( $anonymized_data ); + + // Remove meta data. + $meta_to_remove = apply_filters( + 'woocommerce_privacy_remove_order_personal_data_meta', + array( + 'Payer first name' => 'text', + 'Payer last name' => 'text', + 'Payer PayPal address' => 'email', + 'Transaction ID' => 'numeric_id', + ) + ); + + if ( ! empty( $meta_to_remove ) && is_array( $meta_to_remove ) ) { + foreach ( $meta_to_remove as $meta_key => $data_type ) { + $value = $order->get_meta( $meta_key ); + + // If the value is empty, it does not need to be anonymized. + if ( empty( $value ) || empty( $data_type ) ) { + continue; + } + + $anon_value = function_exists( 'wp_privacy_anonymize_data' ) ? wp_privacy_anonymize_data( $data_type, $value ) : ''; + + /** + * Expose a way to control the anonymized value of a value via 3rd party code. + * + * @since 3.4.0 + * @param string $anon_value Value of this data after anonymization. + * @param string $prop meta_key key being removed. + * @param string $value Current value of the data. + * @param string $data_type Type of data. + * @param WC_Order $order An order object. + */ + $anon_value = apply_filters( 'woocommerce_privacy_remove_order_personal_data_meta_value', $anon_value, $meta_key, $value, $data_type, $order ); + + if ( $anon_value ) { + $order->update_meta_data( $meta_key, $anon_value ); + } else { + $order->delete_meta_data( $meta_key ); + } + } + } + + $order->update_meta_data( '_anonymized', 'yes' ); + $order->save(); + + // Delete order notes which can contain PII. + $notes = wc_get_order_notes( + array( + 'order_id' => $order->get_id(), + ) + ); + + foreach ( $notes as $note ) { + wc_delete_order_note( $note->id ); + } + + // Add note that this event occurred. + $order->add_order_note( __( 'Personal data removed.', 'woocommerce' ) ); + + /** + * Allow extensions to remove their own personal data for this order. + * + * @since 3.4.0 + * @param WC_Order $order A customer object. + */ + do_action( 'woocommerce_privacy_remove_order_personal_data', $order ); + } + + /** + * Finds and erases customer tokens by email address. + * + * @since 3.4.0 + * @param string $email_address The user email address. + * @param int $page Page. + * @return array An array of personal data in name value pairs + */ + public static function customer_tokens_eraser( $email_address, $page ) { + $response = array( + 'items_removed' => false, + 'items_retained' => false, + 'messages' => array(), + 'done' => true, + ); + + $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. + + if ( ! $user instanceof WP_User ) { + return $response; + } + + $tokens = WC_Payment_Tokens::get_tokens( + array( + 'user_id' => $user->ID, + ) + ); + + if ( empty( $tokens ) ) { + return $response; + } + + foreach ( $tokens as $token ) { + WC_Payment_Tokens::delete( $token->get_id() ); + + /* Translators: %s Prop name. */ + $response['messages'][] = sprintf( __( 'Removed payment token "%d"', 'woocommerce' ), $token->get_id() ); + $response['items_removed'] = true; + } + + /** + * Allow extensions to remove data for tokens and adjust the response. + * + * @since 3.4.0 + * @param array $response Array response data. Must include messages, num_items_removed, num_items_retained, done. + * @param array $tokens Array of tokens. + */ + return apply_filters( 'woocommerce_privacy_erase_personal_data_tokens', $response, $tokens ); + } +} diff --git a/plugins/woocommerce/includes/class-wc-privacy-exporters.php b/plugins/woocommerce/includes/class-wc-privacy-exporters.php new file mode 100644 index 00000000000..1f32e9acd64 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-privacy-exporters.php @@ -0,0 +1,444 @@ + 'woocommerce_customer', + 'group_label' => __( 'Customer Data', 'woocommerce' ), + 'group_description' => __( 'User’s WooCommerce customer data.', 'woocommerce' ), + 'item_id' => 'user', + 'data' => $customer_personal_data, + ); + } + } + + return array( + 'data' => $data_to_export, + 'done' => true, + ); + } + + /** + * Finds and exports data which could be used to identify a person from WooCommerce data associated with an email address. + * + * Orders are exported in blocks of 10 to avoid timeouts. + * + * @since 3.4.0 + * @param string $email_address The user email address. + * @param int $page Page. + * @return array An array of personal data in name value pairs + */ + public static function order_data_exporter( $email_address, $page ) { + $done = true; + $page = (int) $page; + $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. + $data_to_export = array(); + $order_query = array( + 'limit' => 10, + 'page' => $page, + 'customer' => array( $email_address ), + ); + + if ( $user instanceof WP_User ) { + $order_query['customer'][] = (int) $user->ID; + } + + $orders = wc_get_orders( $order_query ); + + if ( 0 < count( $orders ) ) { + foreach ( $orders as $order ) { + $data_to_export[] = array( + 'group_id' => 'woocommerce_orders', + 'group_label' => __( 'Orders', 'woocommerce' ), + 'group_description' => __( 'User’s WooCommerce orders data.', 'woocommerce' ), + 'item_id' => 'order-' . $order->get_id(), + 'data' => self::get_order_personal_data( $order ), + ); + } + $done = 10 > count( $orders ); + } + + return array( + 'data' => $data_to_export, + 'done' => $done, + ); + } + + /** + * Finds and exports customer download logs by email address. + * + * @since 3.4.0 + * @param string $email_address The user email address. + * @param int $page Page. + * @throws Exception When WC_Data_Store validation fails. + * @return array An array of personal data in name value pairs + */ + public static function download_data_exporter( $email_address, $page ) { + $done = true; + $page = (int) $page; + $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. + $data_to_export = array(); + $downloads_query = array( + 'limit' => 10, + 'page' => $page, + ); + + if ( $user instanceof WP_User ) { + $downloads_query['user_id'] = (int) $user->ID; + } else { + $downloads_query['user_email'] = $email_address; + } + + $customer_download_data_store = WC_Data_Store::load( 'customer-download' ); + $customer_download_log_data_store = WC_Data_Store::load( 'customer-download-log' ); + $downloads = $customer_download_data_store->get_downloads( $downloads_query ); + + if ( 0 < count( $downloads ) ) { + foreach ( $downloads as $download ) { + $data_to_export[] = array( + 'group_id' => 'woocommerce_downloads', + /* translators: This is the headline for a list of downloads purchased from the store for a given user. */ + 'group_label' => __( 'Purchased Downloads', 'woocommerce' ), + 'group_description' => __( 'User’s WooCommerce purchased downloads data.', 'woocommerce' ), + 'item_id' => 'download-' . $download->get_id(), + 'data' => self::get_download_personal_data( $download ), + ); + + $download_logs = $customer_download_log_data_store->get_download_logs_for_permission( $download->get_id() ); + + foreach ( $download_logs as $download_log ) { + $data_to_export[] = array( + 'group_id' => 'woocommerce_download_logs', + /* translators: This is the headline for a list of access logs for downloads purchased from the store for a given user. */ + 'group_label' => __( 'Access to Purchased Downloads', 'woocommerce' ), + 'group_description' => __( 'User’s WooCommerce access to purchased downloads data.', 'woocommerce' ), + 'item_id' => 'download-log-' . $download_log->get_id(), + 'data' => array( + array( + 'name' => __( 'Download ID', 'woocommerce' ), + 'value' => $download_log->get_permission_id(), + ), + array( + 'name' => __( 'Timestamp', 'woocommerce' ), + 'value' => $download_log->get_timestamp(), + ), + array( + 'name' => __( 'IP Address', 'woocommerce' ), + 'value' => $download_log->get_user_ip_address(), + ), + ), + ); + } + } + $done = 10 > count( $downloads ); + } + + return array( + 'data' => $data_to_export, + 'done' => $done, + ); + } + + /** + * Get personal data (key/value pairs) for a user object. + * + * @since 3.4.0 + * @param WP_User $user user object. + * @throws Exception If customer cannot be read/found and $data is set to WC_Customer class. + * @return array + */ + protected static function get_customer_personal_data( $user ) { + $personal_data = array(); + $customer = new WC_Customer( $user->ID ); + + if ( ! $customer ) { + return array(); + } + + $props_to_export = apply_filters( + 'woocommerce_privacy_export_customer_personal_data_props', + array( + 'billing_first_name' => __( 'Billing First Name', 'woocommerce' ), + 'billing_last_name' => __( 'Billing Last Name', 'woocommerce' ), + 'billing_company' => __( 'Billing Company', 'woocommerce' ), + 'billing_address_1' => __( 'Billing Address 1', 'woocommerce' ), + 'billing_address_2' => __( 'Billing Address 2', 'woocommerce' ), + 'billing_city' => __( 'Billing City', 'woocommerce' ), + 'billing_postcode' => __( 'Billing Postal/Zip Code', 'woocommerce' ), + 'billing_state' => __( 'Billing State', 'woocommerce' ), + 'billing_country' => __( 'Billing Country / Region', 'woocommerce' ), + 'billing_phone' => __( 'Billing Phone Number', 'woocommerce' ), + 'billing_email' => __( 'Email Address', 'woocommerce' ), + 'shipping_first_name' => __( 'Shipping First Name', 'woocommerce' ), + 'shipping_last_name' => __( 'Shipping Last Name', 'woocommerce' ), + 'shipping_company' => __( 'Shipping Company', 'woocommerce' ), + 'shipping_address_1' => __( 'Shipping Address 1', 'woocommerce' ), + 'shipping_address_2' => __( 'Shipping Address 2', 'woocommerce' ), + 'shipping_city' => __( 'Shipping City', 'woocommerce' ), + 'shipping_postcode' => __( 'Shipping Postal/Zip Code', 'woocommerce' ), + 'shipping_state' => __( 'Shipping State', 'woocommerce' ), + 'shipping_country' => __( 'Shipping Country / Region', 'woocommerce' ), + 'shipping_phone' => __( 'Shipping Phone Number', 'woocommerce' ), + ), + $customer + ); + + foreach ( $props_to_export as $prop => $description ) { + $value = ''; + + if ( is_callable( array( $customer, 'get_' . $prop ) ) ) { + $value = $customer->{"get_$prop"}( 'edit' ); + } + + $value = apply_filters( 'woocommerce_privacy_export_customer_personal_data_prop_value', $value, $prop, $customer ); + + if ( $value ) { + $personal_data[] = array( + 'name' => $description, + 'value' => $value, + ); + } + } + + /** + * Allow extensions to register their own personal data for this customer for the export. + * + * @since 3.4.0 + * @param array $personal_data Array of name value pairs. + * @param WC_Order $order A customer object. + */ + $personal_data = apply_filters( 'woocommerce_privacy_export_customer_personal_data', $personal_data, $customer ); + + return $personal_data; + } + + /** + * Get personal data (key/value pairs) for an order object. + * + * @since 3.4.0 + * @param WC_Order $order Order object. + * @return array + */ + protected static function get_order_personal_data( $order ) { + $personal_data = array(); + $props_to_export = apply_filters( + 'woocommerce_privacy_export_order_personal_data_props', + array( + 'order_number' => __( 'Order Number', 'woocommerce' ), + 'date_created' => __( 'Order Date', 'woocommerce' ), + 'total' => __( 'Order Total', 'woocommerce' ), + 'items' => __( 'Items Purchased', 'woocommerce' ), + 'customer_ip_address' => __( 'IP Address', 'woocommerce' ), + 'customer_user_agent' => __( 'Browser User Agent', 'woocommerce' ), + 'formatted_billing_address' => __( 'Billing Address', 'woocommerce' ), + 'formatted_shipping_address' => __( 'Shipping Address', 'woocommerce' ), + 'billing_phone' => __( 'Phone Number', 'woocommerce' ), + 'billing_email' => __( 'Email Address', 'woocommerce' ), + 'shipping_phone' => __( 'Shipping Phone Number', 'woocommerce' ), + ), + $order + ); + + foreach ( $props_to_export as $prop => $name ) { + $value = ''; + + switch ( $prop ) { + case 'items': + $item_names = array(); + foreach ( $order->get_items() as $item ) { + $item_names[] = $item->get_name() . ' x ' . $item->get_quantity(); + } + $value = implode( ', ', $item_names ); + break; + case 'date_created': + $value = wc_format_datetime( $order->get_date_created(), get_option( 'date_format' ) . ', ' . get_option( 'time_format' ) ); + break; + case 'formatted_billing_address': + case 'formatted_shipping_address': + $value = preg_replace( '##i', ', ', $order->{"get_$prop"}() ); + break; + default: + if ( is_callable( array( $order, 'get_' . $prop ) ) ) { + $value = $order->{"get_$prop"}(); + } + break; + } + + $value = apply_filters( 'woocommerce_privacy_export_order_personal_data_prop', $value, $prop, $order ); + + if ( $value ) { + $personal_data[] = array( + 'name' => $name, + 'value' => $value, + ); + } + } + + // Export meta data. + $meta_to_export = apply_filters( + 'woocommerce_privacy_export_order_personal_data_meta', + array( + 'Payer first name' => __( 'Payer first name', 'woocommerce' ), + 'Payer last name' => __( 'Payer last name', 'woocommerce' ), + 'Payer PayPal address' => __( 'Payer PayPal address', 'woocommerce' ), + 'Transaction ID' => __( 'Transaction ID', 'woocommerce' ), + ) + ); + + if ( ! empty( $meta_to_export ) && is_array( $meta_to_export ) ) { + foreach ( $meta_to_export as $meta_key => $name ) { + $value = apply_filters( 'woocommerce_privacy_export_order_personal_data_meta_value', $order->get_meta( $meta_key ), $meta_key, $order ); + + if ( $value ) { + $personal_data[] = array( + 'name' => $name, + 'value' => $value, + ); + } + } + } + + /** + * Allow extensions to register their own personal data for this order for the export. + * + * @since 3.4.0 + * @param array $personal_data Array of name value pairs to expose in the export. + * @param WC_Order $order An order object. + */ + $personal_data = apply_filters( 'woocommerce_privacy_export_order_personal_data', $personal_data, $order ); + + return $personal_data; + } + + /** + * Get personal data (key/value pairs) for a download object. + * + * @since 3.4.0 + * @param WC_Order $download Download object. + * @return array + */ + protected static function get_download_personal_data( $download ) { + $personal_data = array( + array( + 'name' => __( 'Download ID', 'woocommerce' ), + 'value' => $download->get_id(), + ), + array( + 'name' => __( 'Order ID', 'woocommerce' ), + 'value' => $download->get_order_id(), + ), + array( + 'name' => __( 'Product', 'woocommerce' ), + 'value' => get_the_title( $download->get_product_id() ), + ), + array( + 'name' => __( 'User email', 'woocommerce' ), + 'value' => $download->get_user_email(), + ), + array( + 'name' => __( 'Downloads remaining', 'woocommerce' ), + 'value' => $download->get_downloads_remaining(), + ), + array( + 'name' => __( 'Download count', 'woocommerce' ), + 'value' => $download->get_download_count(), + ), + array( + 'name' => __( 'Access granted', 'woocommerce' ), + 'value' => gmdate( 'Y-m-d', $download->get_access_granted( 'edit' )->getTimestamp() ), + ), + array( + 'name' => __( 'Access expires', 'woocommerce' ), + 'value' => ! is_null( $download->get_access_expires( 'edit' ) ) ? gmdate( 'Y-m-d', $download->get_access_expires( 'edit' )->getTimestamp() ) : null, + ), + ); + + /** + * Allow extensions to register their own personal data for this download for the export. + * + * @since 3.4.0 + * @param array $personal_data Array of name value pairs to expose in the export. + * @param WC_Order $order An order object. + */ + $personal_data = apply_filters( 'woocommerce_privacy_export_download_personal_data', $personal_data, $download ); + + return $personal_data; + } + + /** + * Finds and exports payment tokens by email address for a customer. + * + * @since 3.4.0 + * @param string $email_address The user email address. + * @param int $page Page. + * @return array An array of personal data in name value pairs + */ + public static function customer_tokens_exporter( $email_address, $page ) { + $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. + $data_to_export = array(); + + if ( ! $user instanceof WP_User ) { + return array( + 'data' => $data_to_export, + 'done' => true, + ); + } + + $tokens = WC_Payment_Tokens::get_tokens( + array( + 'user_id' => $user->ID, + 'limit' => 10, + 'page' => $page, + ) + ); + + if ( 0 < count( $tokens ) ) { + foreach ( $tokens as $token ) { + $data_to_export[] = array( + 'group_id' => 'woocommerce_tokens', + 'group_label' => __( 'Payment Tokens', 'woocommerce' ), + 'group_description' => __( 'User’s WooCommerce payment tokens data.', 'woocommerce' ), + 'item_id' => 'token-' . $token->get_id(), + 'data' => array( + array( + 'name' => __( 'Token', 'woocommerce' ), + 'value' => $token->get_display_name(), + ), + ), + ); + } + $done = 10 > count( $tokens ); + } else { + $done = true; + } + + return array( + 'data' => $data_to_export, + 'done' => $done, + ); + } +} diff --git a/plugins/woocommerce/includes/class-wc-privacy.php b/plugins/woocommerce/includes/class-wc-privacy.php new file mode 100644 index 00000000000..62ca93f7281 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-privacy.php @@ -0,0 +1,393 @@ +name = __( 'WooCommerce', 'woocommerce' ); + + if ( ! self::$background_process ) { + self::$background_process = new WC_Privacy_Background_Process(); + } + + // Include supporting classes. + include_once __DIR__ . '/class-wc-privacy-erasers.php'; + include_once __DIR__ . '/class-wc-privacy-exporters.php'; + + // This hook registers WooCommerce data exporters. + $this->add_exporter( 'woocommerce-customer-data', __( 'WooCommerce Customer Data', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'customer_data_exporter' ) ); + $this->add_exporter( 'woocommerce-customer-orders', __( 'WooCommerce Customer Orders', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'order_data_exporter' ) ); + $this->add_exporter( 'woocommerce-customer-downloads', __( 'WooCommerce Customer Downloads', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'download_data_exporter' ) ); + $this->add_exporter( 'woocommerce-customer-tokens', __( 'WooCommerce Customer Payment Tokens', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'customer_tokens_exporter' ) ); + + // This hook registers WooCommerce data erasers. + $this->add_eraser( 'woocommerce-customer-data', __( 'WooCommerce Customer Data', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'customer_data_eraser' ) ); + $this->add_eraser( 'woocommerce-customer-orders', __( 'WooCommerce Customer Orders', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'order_data_eraser' ) ); + $this->add_eraser( 'woocommerce-customer-downloads', __( 'WooCommerce Customer Downloads', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'download_data_eraser' ) ); + $this->add_eraser( 'woocommerce-customer-tokens', __( 'WooCommerce Customer Payment Tokens', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'customer_tokens_eraser' ) ); + } + + /** + * Add privacy policy content for the privacy policy page. + * + * @since 3.4.0 + */ + public function get_privacy_message() { + $content = '
    ' . + '

    ' . + __( 'This sample language includes the basics around what personal data your store may be collecting, storing and sharing, as well as who may have access to that data. Depending on what settings are enabled and which additional plugins are used, the specific information shared by your store will vary. We recommend consulting with a lawyer when deciding what information to disclose on your privacy policy.', 'woocommerce' ) . + '

    ' . + '

    ' . __( 'We collect information about you during the checkout process on our store.', 'woocommerce' ) . '

    ' . + '

    ' . __( 'What we collect and store', 'woocommerce' ) . '

    ' . + '

    ' . __( 'While you visit our site, we’ll track:', 'woocommerce' ) . '

    ' . + '
      ' . + '
    • ' . __( 'Products you’ve viewed: we’ll use this to, for example, show you products you’ve recently viewed', 'woocommerce' ) . '
    • ' . + '
    • ' . __( 'Location, IP address and browser type: we’ll use this for purposes like estimating taxes and shipping', 'woocommerce' ) . '
    • ' . + '
    • ' . __( 'Shipping address: we’ll ask you to enter this so we can, for instance, estimate shipping before you place an order, and send you the order!', 'woocommerce' ) . '
    • ' . + '
    ' . + '

    ' . __( 'We’ll also use cookies to keep track of cart contents while you’re browsing our site.', 'woocommerce' ) . '

    ' . + '

    ' . + __( 'Note: you may want to further detail your cookie policy, and link to that section from here.', 'woocommerce' ) . + '

    ' . + '

    ' . __( 'When you purchase from us, we’ll ask you to provide information including your name, billing address, shipping address, email address, phone number, credit card/payment details and optional account information like username and password. We’ll use this information for purposes, such as, to:', 'woocommerce' ) . '

    ' . + '
      ' . + '
    • ' . __( 'Send you information about your account and order', 'woocommerce' ) . '
    • ' . + '
    • ' . __( 'Respond to your requests, including refunds and complaints', 'woocommerce' ) . '
    • ' . + '
    • ' . __( 'Process payments and prevent fraud', 'woocommerce' ) . '
    • ' . + '
    • ' . __( 'Set up your account for our store', 'woocommerce' ) . '
    • ' . + '
    • ' . __( 'Comply with any legal obligations we have, such as calculating taxes', 'woocommerce' ) . '
    • ' . + '
    • ' . __( 'Improve our store offerings', 'woocommerce' ) . '
    • ' . + '
    • ' . __( 'Send you marketing messages, if you choose to receive them', 'woocommerce' ) . '
    • ' . + '
    ' . + '

    ' . __( 'If you create an account, we will store your name, address, email and phone number, which will be used to populate the checkout for future orders.', 'woocommerce' ) . '

    ' . + '

    ' . __( 'We generally store information about you for as long as we need the information for the purposes for which we collect and use it, and we are not legally required to continue to keep it. For example, we will store order information for XXX years for tax and accounting purposes. This includes your name, email address and billing and shipping addresses.', 'woocommerce' ) . '

    ' . + '

    ' . __( 'We will also store comments or reviews, if you choose to leave them.', 'woocommerce' ) . '

    ' . + '

    ' . __( 'Who on our team has access', 'woocommerce' ) . '

    ' . + '

    ' . __( 'Members of our team have access to the information you provide us. For example, both Administrators and Shop Managers can access:', 'woocommerce' ) . '

    ' . + '
      ' . + '
    • ' . __( 'Order information like what was purchased, when it was purchased and where it should be sent, and', 'woocommerce' ) . '
    • ' . + '
    • ' . __( 'Customer information like your name, email address, and billing and shipping information.', 'woocommerce' ) . '
    • ' . + '
    ' . + '

    ' . __( 'Our team members have access to this information to help fulfill orders, process refunds and support you.', 'woocommerce' ) . '

    ' . + '

    ' . __( 'What we share with others', 'woocommerce' ) . '

    ' . + '

    ' . + __( 'In this section you should list who you’re sharing data with, and for what purpose. This could include, but may not be limited to, analytics, marketing, payment gateways, shipping providers, and third party embeds.', 'woocommerce' ) . + '

    ' . + '

    ' . __( 'We share information with third parties who help us provide our orders and store services to you; for example --', 'woocommerce' ) . '

    ' . + '

    ' . __( 'Payments', 'woocommerce' ) . '

    ' . + '

    ' . + __( 'In this subsection you should list which third party payment processors you’re using to take payments on your store since these may handle customer data. We’ve included PayPal as an example, but you should remove this if you’re not using PayPal.', 'woocommerce' ) . + '

    ' . + '

    ' . __( 'We accept payments through PayPal. When processing payments, some of your data will be passed to PayPal, including information required to process or support the payment, such as the purchase total and billing information.', 'woocommerce' ) . '

    ' . + '

    ' . __( 'Please see the PayPal Privacy Policy for more details.', 'woocommerce' ) . '

    ' . + '
    '; + + return apply_filters( 'wc_privacy_policy_content', $content ); + } + + /** + * Spawn events for order cleanup. + */ + public function queue_cleanup_personal_data() { + self::$background_process->push_to_queue( array( 'task' => 'trash_pending_orders' ) ); + self::$background_process->push_to_queue( array( 'task' => 'trash_failed_orders' ) ); + self::$background_process->push_to_queue( array( 'task' => 'trash_cancelled_orders' ) ); + self::$background_process->push_to_queue( array( 'task' => 'anonymize_completed_orders' ) ); + self::$background_process->push_to_queue( array( 'task' => 'delete_inactive_accounts' ) ); + self::$background_process->save()->dispatch(); + } + + /** + * Handle some custom types of data and anonymize them. + * + * @param string $anonymous Anonymized string. + * @param string $type Type of data. + * @param string $data The data being anonymized. + * @return string Anonymized string. + */ + public function anonymize_custom_data_types( $anonymous, $type, $data ) { + switch ( $type ) { + case 'address_state': + case 'address_country': + $anonymous = ''; // Empty string - we don't want to store anything after removal. + break; + case 'phone': + $anonymous = preg_replace( '/\d/u', '0', $data ); + break; + case 'numeric_id': + $anonymous = 0; + break; + } + return $anonymous; + } + + /** + * Find and trash old orders. + * + * @since 3.4.0 + * @param int $limit Limit orders to process per batch. + * @return int Number of orders processed. + */ + public static function trash_pending_orders( $limit = 20 ) { + $option = wc_parse_relative_date_option( get_option( 'woocommerce_trash_pending_orders' ) ); + + if ( empty( $option['number'] ) ) { + return 0; + } + + return self::trash_orders_query( + apply_filters( + 'woocommerce_trash_pending_orders_query_args', + array( + 'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), + 'limit' => $limit, // Batches of 20. + 'status' => 'wc-pending', + 'type' => 'shop_order', + ) + ) + ); + } + + /** + * Find and trash old orders. + * + * @since 3.4.0 + * @param int $limit Limit orders to process per batch. + * @return int Number of orders processed. + */ + public static function trash_failed_orders( $limit = 20 ) { + $option = wc_parse_relative_date_option( get_option( 'woocommerce_trash_failed_orders' ) ); + + if ( empty( $option['number'] ) ) { + return 0; + } + + return self::trash_orders_query( + apply_filters( + 'woocommerce_trash_failed_orders_query_args', + array( + 'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), + 'limit' => $limit, // Batches of 20. + 'status' => 'wc-failed', + 'type' => 'shop_order', + ) + ) + ); + } + + /** + * Find and trash old orders. + * + * @since 3.4.0 + * @param int $limit Limit orders to process per batch. + * @return int Number of orders processed. + */ + public static function trash_cancelled_orders( $limit = 20 ) { + $option = wc_parse_relative_date_option( get_option( 'woocommerce_trash_cancelled_orders' ) ); + + if ( empty( $option['number'] ) ) { + return 0; + } + + return self::trash_orders_query( + apply_filters( + 'woocommerce_trash_cancelled_orders_query_args', + array( + 'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), + 'limit' => $limit, // Batches of 20. + 'status' => 'wc-cancelled', + 'type' => 'shop_order', + ) + ) + ); + } + + /** + * For a given query trash all matches. + * + * @since 3.4.0 + * @param array $query Query array to pass to wc_get_orders(). + * @return int Count of orders that were trashed. + */ + protected static function trash_orders_query( $query ) { + $orders = wc_get_orders( $query ); + $count = 0; + + if ( $orders ) { + foreach ( $orders as $order ) { + $order->delete( false ); + $count ++; + } + } + + return $count; + } + + /** + * Anonymize old completed orders. + * + * @since 3.4.0 + * @param int $limit Limit orders to process per batch. + * @return int Number of orders processed. + */ + public static function anonymize_completed_orders( $limit = 20 ) { + $option = wc_parse_relative_date_option( get_option( 'woocommerce_anonymize_completed_orders' ) ); + + if ( empty( $option['number'] ) ) { + return 0; + } + + return self::anonymize_orders_query( + apply_filters( + 'woocommerce_anonymize_completed_orders_query_args', + array( + 'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), + 'limit' => $limit, // Batches of 20. + 'status' => 'wc-completed', + 'anonymized' => false, + 'type' => 'shop_order', + ) + ) + ); + } + + /** + * For a given query, anonymize all matches. + * + * @since 3.4.0 + * @param array $query Query array to pass to wc_get_orders(). + * @return int Count of orders that were anonymized. + */ + protected static function anonymize_orders_query( $query ) { + $orders = wc_get_orders( $query ); + $count = 0; + + if ( $orders ) { + foreach ( $orders as $order ) { + WC_Privacy_Erasers::remove_order_personal_data( $order ); + $count ++; + } + } + + return $count; + } + + /** + * Delete inactive accounts. + * + * @since 3.4.0 + * @param int $limit Limit users to process per batch. + * @return int Number of users processed. + */ + public static function delete_inactive_accounts( $limit = 20 ) { + $option = wc_parse_relative_date_option( get_option( 'woocommerce_delete_inactive_accounts' ) ); + + if ( empty( $option['number'] ) ) { + return 0; + } + + return self::delete_inactive_accounts_query( strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), $limit ); + } + + /** + * Delete inactive accounts. + * + * @since 3.4.0 + * @param int $timestamp Timestamp to delete customers before. + * @param int $limit Limit number of users to delete per run. + * @return int Count of customers that were deleted. + */ + protected static function delete_inactive_accounts_query( $timestamp, $limit = 20 ) { + $count = 0; + $user_query = new WP_User_Query( + array( + 'fields' => 'ID', + 'number' => $limit, + 'role__in' => apply_filters( + 'woocommerce_delete_inactive_account_roles', + array( + 'Customer', + 'Subscriber', + ) + ), + 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'relation' => 'AND', + array( + 'key' => 'wc_last_active', + 'value' => (string) $timestamp, + 'compare' => '<', + 'type' => 'NUMERIC', + ), + array( + 'key' => 'wc_last_active', + 'value' => '0', + 'compare' => '>', + 'type' => 'NUMERIC', + ), + ), + ) + ); + + $user_ids = $user_query->get_results(); + + if ( $user_ids ) { + if ( ! function_exists( 'wp_delete_user' ) ) { + require_once ABSPATH . 'wp-admin/includes/user.php'; + } + + foreach ( $user_ids as $user_id ) { + wp_delete_user( $user_id ); + $count ++; + } + } + + return $count; + } +} + +new WC_Privacy(); diff --git a/plugins/woocommerce/includes/class-wc-product-attribute.php b/plugins/woocommerce/includes/class-wc-product-attribute.php new file mode 100644 index 00000000000..3af608af8ae --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-product-attribute.php @@ -0,0 +1,333 @@ + 0, + 'name' => '', + 'options' => array(), + 'position' => 0, + 'visible' => false, + 'variation' => false, + ); + + /** + * Return if this attribute is a taxonomy. + * + * @return boolean + */ + public function is_taxonomy() { + return 0 < $this->get_id(); + } + + /** + * Get taxonomy name if applicable. + * + * @return string + */ + public function get_taxonomy() { + return $this->is_taxonomy() ? $this->get_name() : ''; + } + + /** + * Get taxonomy object. + * + * @return array|null + */ + public function get_taxonomy_object() { + global $wc_product_attributes; + return $this->is_taxonomy() ? $wc_product_attributes[ $this->get_name() ] : null; + } + + /** + * Gets terms from the stored options. + * + * @return array|null + */ + public function get_terms() { + if ( ! $this->is_taxonomy() || ! taxonomy_exists( $this->get_name() ) ) { + return null; + } + $terms = array(); + foreach ( $this->get_options() as $option ) { + if ( is_int( $option ) ) { + $term = get_term_by( 'id', $option, $this->get_name() ); + } else { + // Term names get escaped in WP. See sanitize_term_field. + $term = get_term_by( 'name', $option, $this->get_name() ); + + if ( ! $term || is_wp_error( $term ) ) { + $new_term = wp_insert_term( $option, $this->get_name() ); + $term = is_wp_error( $new_term ) ? false : get_term_by( 'id', $new_term['term_id'], $this->get_name() ); + } + } + if ( $term && ! is_wp_error( $term ) ) { + $terms[] = $term; + } + } + return $terms; + } + + /** + * Gets slugs from the stored options, or just the string if text based. + * + * @return array + */ + public function get_slugs() { + if ( ! $this->is_taxonomy() || ! taxonomy_exists( $this->get_name() ) ) { + return $this->get_options(); + } + $terms = array(); + foreach ( $this->get_options() as $option ) { + if ( is_int( $option ) ) { + $term = get_term_by( 'id', $option, $this->get_name() ); + } else { + $term = get_term_by( 'name', $option, $this->get_name() ); + + if ( ! $term || is_wp_error( $term ) ) { + $new_term = wp_insert_term( $option, $this->get_name() ); + $term = is_wp_error( $new_term ) ? false : get_term_by( 'id', $new_term['term_id'], $this->get_name() ); + } + } + if ( $term && ! is_wp_error( $term ) ) { + $terms[] = $term->slug; + } + } + return $terms; + } + + /** + * Returns all data for this object. + * + * @return array + */ + public function get_data() { + return array_merge( + $this->data, + array( + 'is_visible' => $this->get_visible() ? 1 : 0, + 'is_variation' => $this->get_variation() ? 1 : 0, + 'is_taxonomy' => $this->is_taxonomy() ? 1 : 0, + 'value' => $this->is_taxonomy() ? '' : wc_implode_text_attributes( $this->get_options() ), + ) + ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set ID (this is the attribute ID). + * + * @param int $value Attribute ID. + */ + public function set_id( $value ) { + $this->data['id'] = absint( $value ); + } + + /** + * Set name (this is the attribute name or taxonomy). + * + * @param string $value Attribute name. + */ + public function set_name( $value ) { + $this->data['name'] = $value; + } + + /** + * Set options. + * + * @param array $value Attribute options. + */ + public function set_options( $value ) { + $this->data['options'] = $value; + } + + /** + * Set position. + * + * @param int $value Attribute position. + */ + public function set_position( $value ) { + $this->data['position'] = absint( $value ); + } + + /** + * Set if visible. + * + * @param bool $value If is visible on Product's additional info tab. + */ + public function set_visible( $value ) { + $this->data['visible'] = wc_string_to_bool( $value ); + } + + /** + * Set if variation. + * + * @param bool $value If is used for variations. + */ + public function set_variation( $value ) { + $this->data['variation'] = wc_string_to_bool( $value ); + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get the ID. + * + * @return int + */ + public function get_id() { + return $this->data['id']; + } + + /** + * Get name. + * + * @return string + */ + public function get_name() { + return $this->data['name']; + } + + /** + * Get options. + * + * @return array + */ + public function get_options() { + return $this->data['options']; + } + + /** + * Get position. + * + * @return int + */ + public function get_position() { + return $this->data['position']; + } + + /** + * Get if visible. + * + * @return bool + */ + public function get_visible() { + return $this->data['visible']; + } + + /** + * Get if variation. + * + * @return bool + */ + public function get_variation() { + return $this->data['variation']; + } + + /* + |-------------------------------------------------------------------------- + | ArrayAccess/Backwards compatibility. + |-------------------------------------------------------------------------- + */ + + /** + * OffsetGet. + * + * @param string $offset Offset. + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet( $offset ) { + switch ( $offset ) { + case 'is_variation': + return $this->get_variation() ? 1 : 0; + case 'is_visible': + return $this->get_visible() ? 1 : 0; + case 'is_taxonomy': + return $this->is_taxonomy() ? 1 : 0; + case 'value': + return $this->is_taxonomy() ? '' : wc_implode_text_attributes( $this->get_options() ); + default: + if ( is_callable( array( $this, "get_$offset" ) ) ) { + return $this->{"get_$offset"}(); + } + break; + } + return ''; + } + + /** + * OffsetSet. + * + * @param string $offset Offset. + * @param mixed $value Value. + */ + #[\ReturnTypeWillChange] + public function offsetSet( $offset, $value ) { + switch ( $offset ) { + case 'is_variation': + $this->set_variation( $value ); + break; + case 'is_visible': + $this->set_visible( $value ); + break; + case 'value': + $this->set_options( $value ); + break; + default: + if ( is_callable( array( $this, "set_$offset" ) ) ) { + $this->{"set_$offset"}( $value ); + } + break; + } + } + + /** + * OffsetUnset. + * + * @param string $offset Offset. + */ + #[\ReturnTypeWillChange] + public function offsetUnset( $offset ) {} + + /** + * OffsetExists. + * + * @param string $offset Offset. + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists( $offset ) { + return in_array( $offset, array_merge( array( 'is_variation', 'is_visible', 'is_taxonomy', 'value' ), array_keys( $this->data ) ), true ); + } +} diff --git a/plugins/woocommerce/includes/class-wc-product-download.php b/plugins/woocommerce/includes/class-wc-product-download.php new file mode 100644 index 00000000000..6f4f234e594 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-product-download.php @@ -0,0 +1,434 @@ + '', + 'name' => '', + 'file' => '', + 'enabled' => true, + ); + + /** + * Returns all data for this object. + * + * @return array + */ + public function get_data() { + return $this->data; + } + + /** + * Get allowed mime types. + * + * @return array + */ + public function get_allowed_mime_types() { + return apply_filters( 'woocommerce_downloadable_file_allowed_mime_types', get_allowed_mime_types() ); + } + + /** + * Get type of file path set. + * + * @param string $file_path optional. + * @return string absolute, relative, or shortcode. + */ + public function get_type_of_file_path( $file_path = '' ) { + $file_path = $file_path ? $file_path : $this->get_file(); + $parsed_url = wp_parse_url( $file_path ); + if ( + $parsed_url && + isset( $parsed_url['host'] ) && // Absolute url means that it has a host. + ( // Theoretically we could permit any scheme (like ftp as well), but that has not been the case before. So we allow none or http(s). + ! isset( $parsed_url['scheme'] ) || + in_array( $parsed_url['scheme'], array( 'http', 'https' ), true ) + ) + ) { + return 'absolute'; + } elseif ( '[' === substr( $file_path, 0, 1 ) && ']' === substr( $file_path, -1 ) ) { + return 'shortcode'; + } else { + return 'relative'; + } + } + + /** + * Get file type. + * + * @return string + */ + public function get_file_type() { + $type = wp_check_filetype( strtok( $this->get_file(), '?' ), $this->get_allowed_mime_types() ); + return $type['type']; + } + + /** + * Get file extension. + * + * @return string + */ + public function get_file_extension() { + $parsed_url = wp_parse_url( $this->get_file(), PHP_URL_PATH ); + return pathinfo( $parsed_url, PATHINFO_EXTENSION ); + } + + /** + * Confirms that the download is of an allowed filetype, that it exists and that it is + * contained within an approved directory. Used before adding to a product's list of + * downloads. + * + * @internal + * @throws Exception If the download is determined to be invalid. + * + * @param bool $auto_add_to_approved_directory_list If the download is not already in the approved directory list, automatically add it if possible. + */ + public function check_is_valid( bool $auto_add_to_approved_directory_list = true ) { + $download_file = $this->get_file(); + + if ( ! $this->data['enabled'] ) { + throw new Exception( + sprintf( + /* translators: %s: Downloadable file. */ + __( 'The downloadable file %s cannot be used as it has been disabled.', 'woocommerce' ), + '' . basename( $download_file ) . '' + ) + ); + } + + if ( ! $this->is_allowed_filetype() ) { + throw new Exception( + sprintf( + /* translators: 1: Downloadable file, 2: List of allowed filetypes. */ + __( 'The downloadable file %1$s cannot be used as it does not have an allowed file type. Allowed types include: %2$s', 'woocommerce' ), + '' . basename( $download_file ) . '', + '' . implode( ', ', array_keys( $this->get_allowed_mime_types() ) ) . '' + ) + ); + } + + // Validate the file exists. + if ( ! $this->file_exists() ) { + throw new Exception( + sprintf( + /* translators: %s: Downloadable file */ + __( 'The downloadable file %s cannot be used as it does not exist on the server.', 'woocommerce' ), + '' . $download_file . '' + ) + ); + } + + $this->approved_directory_checks( $auto_add_to_approved_directory_list ); + } + + /** + * Check if file is allowed. + * + * @return boolean + */ + public function is_allowed_filetype() { + $file_path = $this->get_file(); + + // File types for URL-based files located on the server should get validated. + $parsed_file_path = WC_Download_Handler::parse_file_path( $file_path ); + $is_file_on_server = ! $parsed_file_path['remote_file']; + $file_path_type = $this->get_type_of_file_path( $file_path ); + + // Shortcodes are allowed, validations should be done by the shortcode provider in this case. + if ( 'shortcode' === $file_path_type ) { + return true; + } + + // Remote paths are allowed. + if ( ! $is_file_on_server && 'relative' !== $file_path_type ) { + return true; + } + + // On windows system, local files ending with `.` are not allowed. + // @link https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#naming-conventions. + if ( $is_file_on_server && ! $this->get_file_extension() && 'WIN' === strtoupper( substr( Constants::get_constant( 'PHP_OS' ), 0, 3 ) ) ) { + if ( '.' === substr( $file_path, -1 ) ) { + return false; + } + } + + return ! $this->get_file_extension() || in_array( $this->get_file_type(), $this->get_allowed_mime_types(), true ); + } + + /** + * Validate file exists. + * + * @return boolean + */ + public function file_exists() { + if ( 'relative' !== $this->get_type_of_file_path() ) { + return true; + } + $file_url = $this->get_file(); + if ( '..' === substr( $file_url, 0, 2 ) || '/' !== substr( $file_url, 0, 1 ) ) { + $file_url = realpath( ABSPATH . $file_url ); + } elseif ( substr( WP_CONTENT_DIR, strlen( untrailingslashit( ABSPATH ) ) ) === substr( $file_url, 0, strlen( substr( WP_CONTENT_DIR, strlen( untrailingslashit( ABSPATH ) ) ) ) ) ) { + $file_url = realpath( WP_CONTENT_DIR . substr( $file_url, 11 ) ); + } + return apply_filters( 'woocommerce_downloadable_file_exists', file_exists( $file_url ), $this->get_file() ); + } + + /** + * Confirms that the download exists within an approved directory. + * + * If it is not within an approved directory but the current user has sufficient + * capabilities, then the method will try to add the download's directory to the + * approved directory list. + * + * @throws Exception If the download is not in an approved directory. + * + * @param bool $auto_add_to_approved_directory_list If the download is not already in the approved directory list, automatically add it if possible. + */ + private function approved_directory_checks( bool $auto_add_to_approved_directory_list = true ) { + $download_directories = wc_get_container()->get( Download_Directories::class ); + + if ( $download_directories->get_mode() !== Download_Directories::MODE_ENABLED ) { + return; + } + + $download_file = $this->get_file(); + + /** + * Controls whether shortcodes should be resolved and validated using the Approved Download Directory feature. + * + * @param bool $should_validate + */ + if ( apply_filters( 'woocommerce_product_downloads_approved_directory_validation_for_shortcodes', true ) && 'shortcode' === $this->get_type_of_file_path() ) { + $download_file = do_shortcode( $download_file ); + } + + $is_site_administrator = is_multisite() ? current_user_can( 'manage_sites' ) : current_user_can( 'manage_options' ); + $valid_storage_directory = $download_directories->is_valid_path( $download_file ); + + if ( $valid_storage_directory ) { + return; + } + + if ( $auto_add_to_approved_directory_list ) { + try { + // Add the parent URL to the approved directories list, but *do not enable it* unless the current user is a site admin. + $download_directories->add_approved_directory( ( new URL( $download_file ) )->get_parent_url(), $is_site_administrator ); + $valid_storage_directory = $download_directories->is_valid_path( $download_file ); + } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch + // At this point, $valid_storage_directory will be false. Fall-through so the appropriate exception is + // triggered (same as if the storage directory was invalid and $auto_add_to_approved_directory_list was false. + } + } + + if ( ! $valid_storage_directory ) { + throw new Exception( + sprintf( + /* translators: %1$s is the downloadable file path, %2$s is an opening link tag, %3%s is a closing link tag. */ + __( 'The downloadable file %1$s cannot be used: it is not located in an approved directory. Please contact a site administrator for help. %2$sLearn more.%3$s', 'woocommerce' ), + '' . $download_file . '', + '', + '' + ) + ); + } + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set ID. + * + * @param string $value Download ID. + */ + public function set_id( $value ) { + $this->data['id'] = wc_clean( $value ); + } + + /** + * Set name. + * + * @param string $value Download name. + */ + public function set_name( $value ) { + $this->data['name'] = wc_clean( $value ); + } + + /** + * Set previous_hash. + * + * @deprecated 3.3.0 No longer using filename based hashing to keep track of files. + * @param string $value Previous hash. + */ + public function set_previous_hash( $value ) { + wc_deprecated_function( __FUNCTION__, '3.3' ); + $this->data['previous_hash'] = wc_clean( $value ); + } + + /** + * Set file. + * + * @param string $value File URL/Path. + */ + public function set_file( $value ) { + // A `///` is recognized as an "absolute", but on the filesystem, so it bypasses the mime check in `self::is_allowed_filetype`. + // This will strip extra prepending / to the maximum of 2. + if ( preg_match( '#^//+(/[^/].+)$#i', $value, $matches ) ) { + $value = $matches[1]; + } + switch ( $this->get_type_of_file_path( $value ) ) { + case 'absolute': + $this->data['file'] = esc_url_raw( $value ); + break; + default: + $this->data['file'] = wc_clean( $value ); + break; + } + } + + /** + * Sets the status of the download to enabled (true) or disabled (false). + * + * @param bool $enabled True indicates the downloadable file is enabled, false indicates it is disabled. + */ + public function set_enabled( bool $enabled = true ) { + $this->data['enabled'] = $enabled; + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get id. + * + * @return string + */ + public function get_id() { + return $this->data['id']; + } + + /** + * Get name. + * + * @return string + */ + public function get_name() { + return $this->data['name']; + } + + /** + * Get previous_hash. + * + * @deprecated 3.3.0 No longer using filename based hashing to keep track of files. + * @return string + */ + public function get_previous_hash() { + wc_deprecated_function( __FUNCTION__, '3.3' ); + return $this->data['previous_hash']; + } + + /** + * Get file. + * + * @return string + */ + public function get_file() { + return $this->data['file']; + } + + /** + * Get status of the download. + * + * @return bool + */ + public function get_enabled(): bool { + return $this->data['enabled']; + } + + /* + |-------------------------------------------------------------------------- + | ArrayAccess/Backwards compatibility. + |-------------------------------------------------------------------------- + */ + + /** + * OffsetGet. + * + * @param string $offset Offset. + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet( $offset ) { + switch ( $offset ) { + default: + if ( is_callable( array( $this, "get_$offset" ) ) ) { + return $this->{"get_$offset"}(); + } + break; + } + return ''; + } + + /** + * OffsetSet. + * + * @param string $offset Offset. + * @param mixed $value Offset value. + */ + #[\ReturnTypeWillChange] + public function offsetSet( $offset, $value ) { + switch ( $offset ) { + default: + if ( is_callable( array( $this, "set_$offset" ) ) ) { + $this->{"set_$offset"}( $value ); + } + break; + } + } + + /** + * OffsetUnset. + * + * @param string $offset Offset. + */ + #[\ReturnTypeWillChange] + public function offsetUnset( $offset ) {} + + /** + * OffsetExists. + * + * @param string $offset Offset. + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists( $offset ) { + return in_array( $offset, array_keys( $this->data ), true ); + } +} diff --git a/includes/class-wc-product-external.php b/plugins/woocommerce/includes/class-wc-product-external.php similarity index 100% rename from includes/class-wc-product-external.php rename to plugins/woocommerce/includes/class-wc-product-external.php diff --git a/includes/class-wc-product-factory.php b/plugins/woocommerce/includes/class-wc-product-factory.php similarity index 100% rename from includes/class-wc-product-factory.php rename to plugins/woocommerce/includes/class-wc-product-factory.php diff --git a/includes/class-wc-product-grouped.php b/plugins/woocommerce/includes/class-wc-product-grouped.php similarity index 100% rename from includes/class-wc-product-grouped.php rename to plugins/woocommerce/includes/class-wc-product-grouped.php diff --git a/includes/class-wc-product-query.php b/plugins/woocommerce/includes/class-wc-product-query.php similarity index 100% rename from includes/class-wc-product-query.php rename to plugins/woocommerce/includes/class-wc-product-query.php diff --git a/includes/class-wc-product-simple.php b/plugins/woocommerce/includes/class-wc-product-simple.php similarity index 100% rename from includes/class-wc-product-simple.php rename to plugins/woocommerce/includes/class-wc-product-simple.php diff --git a/plugins/woocommerce/includes/class-wc-product-variable.php b/plugins/woocommerce/includes/class-wc-product-variable.php new file mode 100644 index 00000000000..a240c406a08 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-product-variable.php @@ -0,0 +1,659 @@ +is_purchasable() ? __( 'Select options', 'woocommerce' ) : __( 'Read more', 'woocommerce' ), $this ); + } + + /** + * Get the add to cart button text description - used in aria tags. + * + * @since 3.3.0 + * @return string + */ + public function add_to_cart_description() { + /* translators: %s: Product title */ + return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( __( 'Select options for “%s”', 'woocommerce' ), $this->get_name() ), $this ); + } + + /** + * Get an array of all sale and regular prices from all variations. This is used for example when displaying the price range at variable product level or seeing if the variable product is on sale. + * + * @param bool $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes). + * @return array Array of RAW prices, regular prices, and sale prices with keys set to variation ID. + */ + public function get_variation_prices( $for_display = false ) { + $prices = $this->data_store->read_price_data( $this, $for_display ); + + foreach ( $prices as $price_key => $variation_prices ) { + $prices[ $price_key ] = $this->sort_variation_prices( $variation_prices ); + } + + return $prices; + } + + /** + * Get the min or max variation regular price. + * + * @param string $min_or_max Min or max price. + * @param boolean $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes). + * @return string + */ + public function get_variation_regular_price( $min_or_max = 'min', $for_display = false ) { + $prices = $this->get_variation_prices( $for_display ); + $price = 'min' === $min_or_max ? current( $prices['regular_price'] ) : end( $prices['regular_price'] ); + + return apply_filters( 'woocommerce_get_variation_regular_price', $price, $this, $min_or_max, $for_display ); + } + + /** + * Get the min or max variation sale price. + * + * @param string $min_or_max Min or max price. + * @param boolean $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes). + * @return string + */ + public function get_variation_sale_price( $min_or_max = 'min', $for_display = false ) { + $prices = $this->get_variation_prices( $for_display ); + $price = 'min' === $min_or_max ? current( $prices['sale_price'] ) : end( $prices['sale_price'] ); + + return apply_filters( 'woocommerce_get_variation_sale_price', $price, $this, $min_or_max, $for_display ); + } + + /** + * Get the min or max variation (active) price. + * + * @param string $min_or_max Min or max price. + * @param boolean $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes). + * @return string + */ + public function get_variation_price( $min_or_max = 'min', $for_display = false ) { + $prices = $this->get_variation_prices( $for_display ); + $price = 'min' === $min_or_max ? current( $prices['price'] ) : end( $prices['price'] ); + + return apply_filters( 'woocommerce_get_variation_price', $price, $this, $min_or_max, $for_display ); + } + + /** + * Returns the price in html format. + * + * Note: Variable prices do not show suffixes like other product types. This + * is due to some things like tax classes being set at variation level which + * could differ from the parent price. The only way to show accurate prices + * would be to load the variation and get it's price, which adds extra + * overhead and still has edge cases where the values would be inaccurate. + * + * Additionally, ranges of prices no longer show 'striked out' sale prices + * due to the strings being very long and unclear/confusing. A single range + * is shown instead. + * + * @param string $price Price (default: ''). + * @return string + */ + public function get_price_html( $price = '' ) { + $prices = $this->get_variation_prices( true ); + + if ( empty( $prices['price'] ) ) { + $price = apply_filters( 'woocommerce_variable_empty_price_html', '', $this ); + } else { + $min_price = current( $prices['price'] ); + $max_price = end( $prices['price'] ); + $min_reg_price = current( $prices['regular_price'] ); + $max_reg_price = end( $prices['regular_price'] ); + + if ( $min_price !== $max_price ) { + $price = wc_format_price_range( $min_price, $max_price ); + } elseif ( $this->is_on_sale() && $min_reg_price === $max_reg_price ) { + $price = wc_format_sale_price( wc_price( $max_reg_price ), wc_price( $min_price ) ); + } else { + $price = wc_price( $min_price ); + } + + $price = apply_filters( 'woocommerce_variable_price_html', $price . $this->get_price_suffix(), $this ); + } + + return apply_filters( 'woocommerce_get_price_html', $price, $this ); + } + + /** + * Get the suffix to display after prices > 0. + * + * This is skipped if the suffix + * has dynamic values such as {price_excluding_tax} for variable products. + * + * @see get_price_html for an explanation as to why. + * @param string $price Price to calculate, left blank to just use get_price(). + * @param integer $qty Quantity passed on to get_price_including_tax() or get_price_excluding_tax(). + * @return string + */ + public function get_price_suffix( $price = '', $qty = 1 ) { + $suffix = get_option( 'woocommerce_price_display_suffix' ); + + if ( strstr( $suffix, '{' ) ) { + return apply_filters( 'woocommerce_get_price_suffix', '', $this, $price, $qty ); + } else { + return parent::get_price_suffix( $price, $qty ); + } + } + + /** + * Return a products child ids. + * + * This is lazy loaded as it's not used often and does require several queries. + * + * @param bool|string $visible_only Visible only. + * @return array Children ids + */ + public function get_children( $visible_only = '' ) { + if ( is_bool( $visible_only ) ) { + wc_deprecated_argument( 'visible_only', '3.0', 'WC_Product_Variable::get_visible_children' ); + + return $visible_only ? $this->get_visible_children() : $this->get_children(); + } + + if ( null === $this->children ) { + $children = $this->data_store->read_children( $this ); + $this->set_children( $children['all'] ); + $this->set_visible_children( $children['visible'] ); + } + + return apply_filters( 'woocommerce_get_children', $this->children, $this, false ); + } + + /** + * Return a products child ids - visible only. + * + * This is lazy loaded as it's not used often and does require several queries. + * + * @since 3.0.0 + * @return array Children ids + */ + public function get_visible_children() { + if ( null === $this->visible_children ) { + $children = $this->data_store->read_children( $this ); + $this->set_children( $children['all'] ); + $this->set_visible_children( $children['visible'] ); + } + return apply_filters( 'woocommerce_get_children', $this->visible_children, $this, true ); + } + + /** + * Return an array of attributes used for variations, as well as their possible values. + * + * This is lazy loaded as it's not used often and does require several queries. + * + * @return array Attributes and their available values + */ + public function get_variation_attributes() { + if ( null === $this->variation_attributes ) { + $this->variation_attributes = $this->data_store->read_variation_attributes( $this ); + } + return $this->variation_attributes; + } + + /** + * If set, get the default attributes for a variable product. + * + * @param string $attribute_name Attribute name. + * @return string + */ + public function get_variation_default_attribute( $attribute_name ) { + $defaults = $this->get_default_attributes(); + $attribute_name = sanitize_title( $attribute_name ); + + return isset( $defaults[ $attribute_name ] ) ? $defaults[ $attribute_name ] : ''; + } + + /** + * Variable products themselves cannot be downloadable. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return bool + */ + public function get_downloadable( $context = 'view' ) { + return false; + } + + /** + * Variable products themselves cannot be virtual. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return bool + */ + public function get_virtual( $context = 'view' ) { + return false; + } + + /** + * Get an array of available variations for the current product. + * + * @param string $return Optional. The format to return the results in. Can be 'array' to return an array of variation data or 'objects' for the product objects. Default 'array'. + * + * @return array[]|WC_Product_Variation[] + */ + public function get_available_variations( $return = 'array' ) { + $variation_ids = $this->get_children(); + $available_variations = array(); + + if ( is_callable( '_prime_post_caches' ) ) { + _prime_post_caches( $variation_ids ); + } + + foreach ( $variation_ids as $variation_id ) { + + $variation = wc_get_product( $variation_id ); + + // Hide out of stock variations if 'Hide out of stock items from the catalog' is checked. + if ( ! $variation || ! $variation->exists() || ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $variation->is_in_stock() ) ) { + continue; + } + + // Filter 'woocommerce_hide_invisible_variations' to optionally hide invisible variations (disabled variations and variations with empty price). + if ( apply_filters( 'woocommerce_hide_invisible_variations', true, $this->get_id(), $variation ) && ! $variation->variation_is_visible() ) { + continue; + } + + if ( 'array' === $return ) { + $available_variations[] = $this->get_available_variation( $variation ); + } else { + $available_variations[] = $variation; + } + } + + if ( 'array' === $return ) { + $available_variations = array_values( array_filter( $available_variations ) ); + } + + return $available_variations; + } + + /** + * Check if a given variation is currently available. + * + * @param WC_Product_Variation $variation Variation to check. + * + * @return bool True if the variation is available, false otherwise. + */ + private function variation_is_available( WC_Product_Variation $variation ) { + // Hide out of stock variations if 'Hide out of stock items from the catalog' is checked. + if ( ! $variation || ! $variation->exists() || ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $variation->is_in_stock() ) ) { + return false; + } + + // Filter 'woocommerce_hide_invisible_variations' to optionally hide invisible variations (disabled variations and variations with empty price). + if ( apply_filters( 'woocommerce_hide_invisible_variations', true, $this->get_id(), $variation ) && ! $variation->variation_is_visible() ) { + return false; + } + + return true; + } + + /** + * Returns an array of data for a variation. Used in the add to cart form. + * + * @since 2.4.0 + * @param WC_Product $variation Variation product object or ID. + * @return array|bool + */ + public function get_available_variation( $variation ) { + if ( is_numeric( $variation ) ) { + $variation = wc_get_product( $variation ); + } + if ( ! $variation instanceof WC_Product_Variation ) { + return false; + } + // See if prices should be shown for each variation after selection. + $show_variation_price = apply_filters( 'woocommerce_show_variation_price', $variation->get_price() === '' || $this->get_variation_sale_price( 'min' ) !== $this->get_variation_sale_price( 'max' ) || $this->get_variation_regular_price( 'min' ) !== $this->get_variation_regular_price( 'max' ), $this, $variation ); + + return apply_filters( + 'woocommerce_available_variation', + array( + 'attributes' => $variation->get_variation_attributes(), + 'availability_html' => wc_get_stock_html( $variation ), + 'backorders_allowed' => $variation->backorders_allowed(), + 'dimensions' => $variation->get_dimensions( false ), + 'dimensions_html' => wc_format_dimensions( $variation->get_dimensions( false ) ), + 'display_price' => wc_get_price_to_display( $variation ), + 'display_regular_price' => wc_get_price_to_display( $variation, array( 'price' => $variation->get_regular_price() ) ), + 'image' => wc_get_product_attachment_props( $variation->get_image_id() ), + 'image_id' => $variation->get_image_id(), + 'is_downloadable' => $variation->is_downloadable(), + 'is_in_stock' => $variation->is_in_stock(), + 'is_purchasable' => $variation->is_purchasable(), + 'is_sold_individually' => $variation->is_sold_individually() ? 'yes' : 'no', + 'is_virtual' => $variation->is_virtual(), + 'max_qty' => 0 < $variation->get_max_purchase_quantity() ? $variation->get_max_purchase_quantity() : '', + 'min_qty' => $variation->get_min_purchase_quantity(), + 'price_html' => $show_variation_price ? '' . $variation->get_price_html() . '' : '', + 'sku' => $variation->get_sku(), + 'variation_description' => wc_format_content( $variation->get_description() ), + 'variation_id' => $variation->get_id(), + 'variation_is_active' => $variation->variation_is_active(), + 'variation_is_visible' => $variation->variation_is_visible(), + 'weight' => $variation->get_weight(), + 'weight_html' => wc_format_weight( $variation->get_weight() ), + ), + $this, + $variation + ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Sets an array of variation attributes. + * + * @since 3.0.0 + * @param array $variation_attributes Attributes list. + */ + public function set_variation_attributes( $variation_attributes ) { + $this->variation_attributes = $variation_attributes; + } + + /** + * Sets an array of children for the product. + * + * @since 3.0.0 + * @param array $children Children products. + */ + public function set_children( $children ) { + $this->children = array_filter( wp_parse_id_list( (array) $children ) ); + } + + /** + * Sets an array of visible children only. + * + * @since 3.0.0 + * @param array $visible_children List of visible children products. + */ + public function set_visible_children( $visible_children ) { + $this->visible_children = array_filter( wp_parse_id_list( (array) $visible_children ) ); + } + + /* + |-------------------------------------------------------------------------- + | CRUD methods + |-------------------------------------------------------------------------- + */ + + /** + * Ensure properties are set correctly before save. + * + * @since 3.0.0 + */ + public function validate_props() { + parent::validate_props(); + + if ( ! $this->get_manage_stock() ) { + $this->data_store->sync_stock_status( $this ); + } + } + + /** + * Do any extra processing needed before the actual product save + * (but after triggering the 'woocommerce_before_..._object_save' action) + * + * @return mixed A state value that will be passed to after_data_store_save_or_update. + */ + protected function before_data_store_save_or_update() { + // Get names before save. + $previous_name = $this->data['name']; + $new_name = $this->get_name( 'edit' ); + + return array( + 'previous_name' => $previous_name, + 'new_name' => $new_name, + ); + } + + /** + * Do any extra processing needed after the actual product save + * (but before triggering the 'woocommerce_after_..._object_save' action) + * + * @param mixed $state The state object that was returned by before_data_store_save_or_update. + */ + protected function after_data_store_save_or_update( $state ) { + $this->data_store->sync_variation_names( $this, $state['previous_name'], $state['new_name'] ); + $this->data_store->sync_managed_variation_stock_status( $this ); + } + + /* + |-------------------------------------------------------------------------- + | Conditionals + |-------------------------------------------------------------------------- + */ + + /** + * Returns whether or not the product is on sale. + * + * @param string $context What the value is for. Valid values are view and edit. What the value is for. Valid values are view and edit. + * @return bool + */ + public function is_on_sale( $context = 'view' ) { + $prices = $this->get_variation_prices(); + $on_sale = $prices['regular_price'] !== $prices['sale_price'] && $prices['sale_price'] === $prices['price']; + + return 'view' === $context ? apply_filters( 'woocommerce_product_is_on_sale', $on_sale, $this ) : $on_sale; + } + + /** + * Is a child in stock? + * + * @return boolean + */ + public function child_is_in_stock() { + return $this->data_store->child_is_in_stock( $this ); + } + + /** + * Is a child on backorder? + * + * @since 3.3.0 + * @return boolean + */ + public function child_is_on_backorder() { + return $this->data_store->child_has_stock_status( $this, 'onbackorder' ); + } + + /** + * Does a child have a weight set? + * + * @return boolean + */ + public function child_has_weight() { + $transient_name = 'wc_child_has_weight_' . $this->get_id(); + $has_weight = get_transient( $transient_name ); + + if ( false === $has_weight ) { + $has_weight = $this->data_store->child_has_weight( $this ); + set_transient( $transient_name, (int) $has_weight, DAY_IN_SECONDS * 30 ); + } + + return (bool) $has_weight; + } + + /** + * Does a child have dimensions set? + * + * @return boolean + */ + public function child_has_dimensions() { + $transient_name = 'wc_child_has_dimensions_' . $this->get_id(); + $has_dimension = get_transient( $transient_name ); + + if ( false === $has_dimension ) { + $has_dimension = $this->data_store->child_has_dimensions( $this ); + set_transient( $transient_name, (int) $has_dimension, DAY_IN_SECONDS * 30 ); + } + + return (bool) $has_dimension; + } + + /** + * Returns whether or not the product has dimensions set. + * + * @return bool + */ + public function has_dimensions() { + return parent::has_dimensions() || $this->child_has_dimensions(); + } + + /** + * Returns whether or not the product has weight set. + * + * @return bool + */ + public function has_weight() { + return parent::has_weight() || $this->child_has_weight(); + } + + /** + * Returns whether or not the product has additional options that need + * selecting before adding to cart. + * + * @since 3.0.0 + * @return boolean + */ + public function has_options() { + return apply_filters( 'woocommerce_product_has_options', true, $this ); + } + + + /* + |-------------------------------------------------------------------------- + | Sync with child variations. + |-------------------------------------------------------------------------- + */ + + /** + * Sync a variable product with it's children. These sync functions sync + * upwards (from child to parent) when the variation is saved. + * + * @param WC_Product|int $product Product object or ID for which you wish to sync. + * @param bool $save If true, the product object will be saved to the DB before returning it. + * @return WC_Product Synced product object. + */ + public static function sync( $product, $save = true ) { + if ( ! is_a( $product, 'WC_Product' ) ) { + $product = wc_get_product( $product ); + } + if ( is_a( $product, 'WC_Product_Variable' ) ) { + $data_store = WC_Data_Store::load( 'product-' . $product->get_type() ); + $data_store->sync_price( $product ); + $data_store->sync_stock_status( $product ); + self::sync_attributes( $product ); // Legacy update of attributes. + + do_action( 'woocommerce_variable_product_sync_data', $product ); + + if ( $save ) { + $product->save(); + } + + wc_do_deprecated_action( + 'woocommerce_variable_product_sync', + array( + $product->get_id(), + $product->get_visible_children(), + ), + '3.0', + 'woocommerce_variable_product_sync_data, woocommerce_new_product or woocommerce_update_product' + ); + } + + return $product; + } + + /** + * Sync parent stock status with the status of all children and save. + * + * @param WC_Product|int $product Product object or ID for which you wish to sync. + * @param bool $save If true, the product object will be saved to the DB before returning it. + * @return WC_Product Synced product object. + */ + public static function sync_stock_status( $product, $save = true ) { + if ( ! is_a( $product, 'WC_Product' ) ) { + $product = wc_get_product( $product ); + } + if ( is_a( $product, 'WC_Product_Variable' ) ) { + $data_store = WC_Data_Store::load( 'product-' . $product->get_type() ); + $data_store->sync_stock_status( $product ); + + if ( $save ) { + $product->save(); + } + } + + return $product; + } + + /** + * Sort an associative array of $variation_id => $price pairs in order of min and max prices. + * + * @param array $prices associative array of $variation_id => $price pairs. + * @return array + */ + protected function sort_variation_prices( $prices ) { + asort( $prices ); + + return $prices; + } +} diff --git a/plugins/woocommerce/includes/class-wc-product-variation.php b/plugins/woocommerce/includes/class-wc-product-variation.php new file mode 100644 index 00000000000..83b80fc206c --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-product-variation.php @@ -0,0 +1,586 @@ + '', + 'sku' => '', + 'manage_stock' => '', + 'backorders' => '', + 'stock_quantity' => '', + 'weight' => '', + 'length' => '', + 'width' => '', + 'height' => '', + 'tax_class' => '', + 'shipping_class_id' => '', + 'image_id' => '', + 'purchase_note' => '', + ); + + /** + * Override the default constructor to set custom defaults. + * + * @param int|WC_Product|object $product Product to init. + */ + public function __construct( $product = 0 ) { + $this->data['tax_class'] = 'parent'; + $this->data['attribute_summary'] = ''; + parent::__construct( $product ); + } + + /** + * Prefix for action and filter hooks on data. + * + * @since 3.0.0 + * @return string + */ + protected function get_hook_prefix() { + return 'woocommerce_product_variation_get_'; + } + + /** + * Get internal type. + * + * @return string + */ + public function get_type() { + return 'variation'; + } + + /** + * If the stock level comes from another product ID. + * + * @since 3.0.0 + * @return int + */ + public function get_stock_managed_by_id() { + return 'parent' === $this->get_manage_stock() ? $this->get_parent_id() : $this->get_id(); + } + + /** + * Get the product's title. For variations this is the parent product name. + * + * @return string + */ + public function get_title() { + return apply_filters( 'woocommerce_product_title', $this->parent_data['title'], $this ); + } + + /** + * Get product name with SKU or ID. Used within admin. + * + * @return string Formatted product name + */ + public function get_formatted_name() { + if ( $this->get_sku() ) { + $identifier = $this->get_sku(); + } else { + $identifier = '#' . $this->get_id(); + } + + $formatted_variation_list = wc_get_formatted_variation( $this, true, true, true ); + + return sprintf( '%2$s (%1$s)', $identifier, $this->get_name() ) . '' . $formatted_variation_list . ''; + } + + /** + * Get variation attribute values. Keys are prefixed with attribute_, as stored, unless $with_prefix is false. + * + * @param bool $with_prefix Whether keys should be prepended with attribute_ or not, default is true. + * @return array of attributes and their values for this variation. + */ + public function get_variation_attributes( $with_prefix = true ) { + $attributes = $this->get_attributes(); + $variation_attributes = array(); + $prefix = $with_prefix ? 'attribute_' : ''; + + foreach ( $attributes as $key => $value ) { + $variation_attributes[ $prefix . $key ] = $value; + } + return $variation_attributes; + } + + /** + * Returns a single product attribute as a string. + * + * @param string $attribute to get. + * @return string + */ + public function get_attribute( $attribute ) { + $attributes = $this->get_attributes(); + $attribute = sanitize_title( $attribute ); + + if ( isset( $attributes[ $attribute ] ) ) { + $value = $attributes[ $attribute ]; + $term = taxonomy_exists( $attribute ) ? get_term_by( 'slug', $value, $attribute ) : false; + return ! is_wp_error( $term ) && $term ? $term->name : $value; + } + + $att_str = 'pa_' . $attribute; + if ( isset( $attributes[ $att_str ] ) ) { + $value = $attributes[ $att_str ]; + $term = taxonomy_exists( $att_str ) ? get_term_by( 'slug', $value, $att_str ) : false; + return ! is_wp_error( $term ) && $term ? $term->name : $value; + } + + return ''; + } + + /** + * Wrapper for get_permalink. Adds this variations attributes to the URL. + * + * @param array|null $item_object item array If a cart or order item is passed, we can get a link containing the exact attributes selected for the variation, rather than the default attributes. + * @return string + */ + public function get_permalink( $item_object = null ) { + $url = get_permalink( $this->get_parent_id() ); + + if ( ! empty( $item_object['variation'] ) ) { + $data = $item_object['variation']; + } elseif ( ! empty( $item_object['item_meta_array'] ) ) { + $data_keys = array_map( 'wc_variation_attribute_name', wp_list_pluck( $item_object['item_meta_array'], 'key' ) ); + $data_values = wp_list_pluck( $item_object['item_meta_array'], 'value' ); + $data = array_intersect_key( array_combine( $data_keys, $data_values ), $this->get_variation_attributes() ); + } else { + $data = $this->get_variation_attributes(); + } + + $data = array_filter( $data, 'wc_array_filter_default_attributes' ); + + if ( empty( $data ) ) { + return $url; + } + + // Filter and encode keys and values so this is not broken by add_query_arg. + $data = array_map( 'urlencode', $data ); + $keys = array_map( 'urlencode', array_keys( $data ) ); + + return add_query_arg( array_combine( $keys, $data ), $url ); + } + + /** + * Get the add to url used mainly in loops. + * + * @return string + */ + public function add_to_cart_url() { + $url = $this->is_purchasable() ? remove_query_arg( + 'added-to-cart', + add_query_arg( + array( + 'variation_id' => $this->get_id(), + 'add-to-cart' => $this->get_parent_id(), + ), + $this->get_permalink() + ) + ) : $this->get_permalink(); + return apply_filters( 'woocommerce_product_add_to_cart_url', $url, $this ); + } + + /** + * Get SKU (Stock-keeping unit) - product unique ID. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_sku( $context = 'view' ) { + $value = $this->get_prop( 'sku', $context ); + + // Inherit value from parent. + if ( 'view' === $context && empty( $value ) ) { + $value = apply_filters( $this->get_hook_prefix() . 'sku', $this->parent_data['sku'], $this ); + } + return $value; + } + + /** + * Returns the product's weight. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_weight( $context = 'view' ) { + $value = $this->get_prop( 'weight', $context ); + + // Inherit value from parent. + if ( 'view' === $context && empty( $value ) ) { + $value = apply_filters( $this->get_hook_prefix() . 'weight', $this->parent_data['weight'], $this ); + } + return $value; + } + + /** + * Returns the product length. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_length( $context = 'view' ) { + $value = $this->get_prop( 'length', $context ); + + // Inherit value from parent. + if ( 'view' === $context && empty( $value ) ) { + $value = apply_filters( $this->get_hook_prefix() . 'length', $this->parent_data['length'], $this ); + } + return $value; + } + + /** + * Returns the product width. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_width( $context = 'view' ) { + $value = $this->get_prop( 'width', $context ); + + // Inherit value from parent. + if ( 'view' === $context && empty( $value ) ) { + $value = apply_filters( $this->get_hook_prefix() . 'width', $this->parent_data['width'], $this ); + } + return $value; + } + + /** + * Returns the product height. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_height( $context = 'view' ) { + $value = $this->get_prop( 'height', $context ); + + // Inherit value from parent. + if ( 'view' === $context && empty( $value ) ) { + $value = apply_filters( $this->get_hook_prefix() . 'height', $this->parent_data['height'], $this ); + } + return $value; + } + + /** + * Returns the tax class. + * + * Does not use get_prop so it can handle 'parent' inheritance correctly. + * + * @param string $context view, edit, or unfiltered. + * @return string + */ + public function get_tax_class( $context = 'view' ) { + $value = null; + + if ( array_key_exists( 'tax_class', $this->data ) ) { + $value = array_key_exists( 'tax_class', $this->changes ) ? $this->changes['tax_class'] : $this->data['tax_class']; + + if ( 'edit' !== $context && 'parent' === $value ) { + $value = $this->parent_data['tax_class']; + } + + if ( 'view' === $context ) { + $value = apply_filters( $this->get_hook_prefix() . 'tax_class', $value, $this ); + } + } + return $value; + } + + /** + * Return if product manage stock. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return boolean|string true, false, or parent. + */ + public function get_manage_stock( $context = 'view' ) { + $value = $this->get_prop( 'manage_stock', $context ); + + // Inherit value from parent. + if ( 'view' === $context && false === $value && true === wc_string_to_bool( $this->parent_data['manage_stock'] ) ) { + $value = 'parent'; + } + return $value; + } + + /** + * Returns number of items available for sale. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return int|null + */ + public function get_stock_quantity( $context = 'view' ) { + $value = $this->get_prop( 'stock_quantity', $context ); + + // Inherit value from parent. + if ( 'view' === $context && 'parent' === $this->get_manage_stock() ) { + $value = apply_filters( $this->get_hook_prefix() . 'stock_quantity', $this->parent_data['stock_quantity'], $this ); + } + return $value; + } + + /** + * Get backorders. + * + * @param string $context What the value is for. Valid values are view and edit. + * @since 3.0.0 + * @return string yes no or notify + */ + public function get_backorders( $context = 'view' ) { + $value = $this->get_prop( 'backorders', $context ); + + // Inherit value from parent. + if ( 'view' === $context && 'parent' === $this->get_manage_stock() ) { + $value = apply_filters( $this->get_hook_prefix() . 'backorders', $this->parent_data['backorders'], $this ); + } + return $value; + } + + /** + * Get main image ID. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_image_id( $context = 'view' ) { + $image_id = $this->get_prop( 'image_id', $context ); + + if ( 'view' === $context && ! $image_id ) { + $image_id = apply_filters( $this->get_hook_prefix() . 'image_id', $this->parent_data['image_id'], $this ); + } + + return $image_id; + } + + /** + * Get purchase note. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_purchase_note( $context = 'view' ) { + $value = $this->get_prop( 'purchase_note', $context ); + + // Inherit value from parent. + if ( 'view' === $context && empty( $value ) ) { + $value = apply_filters( $this->get_hook_prefix() . 'purchase_note', $this->parent_data['purchase_note'], $this ); + } + return $value; + } + + /** + * Get shipping class ID. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return int + */ + public function get_shipping_class_id( $context = 'view' ) { + $shipping_class_id = $this->get_prop( 'shipping_class_id', $context ); + + if ( 'view' === $context && ! $shipping_class_id ) { + $shipping_class_id = apply_filters( $this->get_hook_prefix() . 'shipping_class_id', $this->parent_data['shipping_class_id'], $this ); + } + + return $shipping_class_id; + } + + /** + * Get catalog visibility. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_catalog_visibility( $context = 'view' ) { + return apply_filters( $this->get_hook_prefix() . 'catalog_visibility', $this->parent_data['catalog_visibility'], $this ); + } + + /** + * Get attribute summary. + * + * By default, attribute summary contains comma-delimited 'attribute_name: attribute_value' pairs for all attributes. + * + * @param string $context What the value is for. Valid values are view and edit. + * + * @since 3.6.0 + * @return string + */ + public function get_attribute_summary( $context = 'view' ) { + return $this->get_prop( 'attribute_summary', $context ); + } + + + /** + * Set attribute summary. + * + * By default, attribute summary contains comma-delimited 'attribute_name: attribute_value' pairs for all attributes. + * + * @since 3.6.0 + * @param string $attribute_summary Summary of attribute names and values assigned to the variation. + */ + public function set_attribute_summary( $attribute_summary ) { + $this->set_prop( 'attribute_summary', $attribute_summary ); + } + + /* + |-------------------------------------------------------------------------- + | CRUD methods + |-------------------------------------------------------------------------- + */ + + /** + * Set the parent data array for this variation. + * + * @since 3.0.0 + * @param array $parent_data parent data array for this variation. + */ + public function set_parent_data( $parent_data ) { + $parent_data = wp_parse_args( + $parent_data, + array( + 'title' => '', + 'status' => '', + 'sku' => '', + 'manage_stock' => 'no', + 'backorders' => 'no', + 'stock_quantity' => '', + 'weight' => '', + 'length' => '', + 'width' => '', + 'height' => '', + 'tax_class' => '', + 'shipping_class_id' => 0, + 'image_id' => 0, + 'purchase_note' => '', + 'catalog_visibility' => 'visible', + ) + ); + + // Normalize tax class. + $parent_data['tax_class'] = sanitize_title( $parent_data['tax_class'] ); + $parent_data['tax_class'] = 'standard' === $parent_data['tax_class'] ? '' : $parent_data['tax_class']; + $valid_classes = $this->get_valid_tax_classes(); + + if ( ! in_array( $parent_data['tax_class'], $valid_classes, true ) ) { + $parent_data['tax_class'] = ''; + } + + $this->parent_data = $parent_data; + } + + /** + * Get the parent data array for this variation. + * + * @since 3.0.0 + * @return array + */ + public function get_parent_data() { + return $this->parent_data; + } + + /** + * Set attributes. Unlike the parent product which uses terms, variations are assigned + * specific attributes using name value pairs. + * + * @param array $raw_attributes array of raw attributes. + */ + public function set_attributes( $raw_attributes ) { + $raw_attributes = (array) $raw_attributes; + $attributes = array(); + + foreach ( $raw_attributes as $key => $value ) { + // Remove attribute prefix which meta gets stored with. + if ( 0 === strpos( $key, 'attribute_' ) ) { + $key = substr( $key, 10 ); + } + $attributes[ $key ] = $value; + } + $this->set_prop( 'attributes', $attributes ); + } + + /** + * Returns whether or not the product has any visible attributes. + * + * Variations are mapped to specific attributes unlike products, and the return + * value of ->get_attributes differs. Therefore this returns false. + * + * @return boolean + */ + public function has_attributes() { + return false; + } + + /* + |-------------------------------------------------------------------------- + | Conditionals + |-------------------------------------------------------------------------- + */ + + /** + * Returns false if the product cannot be bought. + * Override abstract method so that: i) Disabled variations are not be purchasable by admins. ii) Enabled variations are not purchasable if the parent product is not purchasable. + * + * @return bool + */ + public function is_purchasable() { + return apply_filters( 'woocommerce_variation_is_purchasable', $this->variation_is_visible() && parent::is_purchasable() && ( 'publish' === $this->parent_data['status'] || current_user_can( 'edit_post', $this->get_parent_id() ) ), $this ); + } + + /** + * Controls whether this particular variation will appear greyed-out (inactive) or not (active). + * Used by extensions to make incompatible variations appear greyed-out, etc. + * Other possible uses: prevent out-of-stock variations from being selected. + * + * @return bool + */ + public function variation_is_active() { + return apply_filters( 'woocommerce_variation_is_active', true, $this ); + } + + /** + * Checks if this particular variation is visible. Invisible variations are enabled and can be selected, but no price / stock info is displayed. + * Instead, a suitable 'unavailable' message is displayed. + * Invisible by default: Disabled variations and variations with an empty price. + * + * @return bool + */ + public function variation_is_visible() { + return apply_filters( 'woocommerce_variation_is_visible', 'publish' === get_post_status( $this->get_id() ) && '' !== $this->get_price(), $this->get_id(), $this->get_parent_id(), $this ); + } + + /** + * Return valid tax classes. Adds 'parent' to the default list of valid tax classes. + * + * @return array valid tax classes + */ + protected function get_valid_tax_classes() { + $valid_classes = WC_Tax::get_tax_class_slugs(); + $valid_classes[] = 'parent'; + + return $valid_classes; + } +} diff --git a/plugins/woocommerce/includes/class-wc-query.php b/plugins/woocommerce/includes/class-wc-query.php new file mode 100644 index 00000000000..4758db345fb --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-query.php @@ -0,0 +1,1010 @@ +filterer = wc_get_container()->get( Filterer::class ); + + add_action( 'init', array( $this, 'add_endpoints' ) ); + if ( ! is_admin() ) { + add_action( 'wp_loaded', array( $this, 'get_errors' ), 20 ); + add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 ); + add_action( 'parse_request', array( $this, 'parse_request' ), 0 ); + add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) ); + add_filter( 'get_pagenum_link', array( $this, 'remove_add_to_cart_pagination' ), 10, 1 ); + } + $this->init_query_vars(); + } + + /** + * Reset the chosen attributes so that get_layered_nav_chosen_attributes will get them from the query again. + */ + public static function reset_chosen_attributes() { + self::$chosen_attributes = null; + } + + /** + * Get any errors from querystring. + */ + public function get_errors() { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $error = ! empty( $_GET['wc_error'] ) ? sanitize_text_field( wp_unslash( $_GET['wc_error'] ) ) : ''; + + if ( $error && ! wc_has_notice( $error, 'error' ) ) { + wc_add_notice( $error, 'error' ); + } + } + + /** + * Init query vars by loading options. + */ + public function init_query_vars() { + // Query vars to add to WP. + $this->query_vars = array( + // Checkout actions. + 'order-pay' => get_option( 'woocommerce_checkout_pay_endpoint', 'order-pay' ), + 'order-received' => get_option( 'woocommerce_checkout_order_received_endpoint', 'order-received' ), + // My account actions. + 'orders' => get_option( 'woocommerce_myaccount_orders_endpoint', 'orders' ), + 'view-order' => get_option( 'woocommerce_myaccount_view_order_endpoint', 'view-order' ), + 'downloads' => get_option( 'woocommerce_myaccount_downloads_endpoint', 'downloads' ), + 'edit-account' => get_option( 'woocommerce_myaccount_edit_account_endpoint', 'edit-account' ), + 'edit-address' => get_option( 'woocommerce_myaccount_edit_address_endpoint', 'edit-address' ), + 'payment-methods' => get_option( 'woocommerce_myaccount_payment_methods_endpoint', 'payment-methods' ), + 'lost-password' => get_option( 'woocommerce_myaccount_lost_password_endpoint', 'lost-password' ), + 'customer-logout' => get_option( 'woocommerce_logout_endpoint', 'customer-logout' ), + 'add-payment-method' => get_option( 'woocommerce_myaccount_add_payment_method_endpoint', 'add-payment-method' ), + 'delete-payment-method' => get_option( 'woocommerce_myaccount_delete_payment_method_endpoint', 'delete-payment-method' ), + 'set-default-payment-method' => get_option( 'woocommerce_myaccount_set_default_payment_method_endpoint', 'set-default-payment-method' ), + ); + } + + /** + * Get page title for an endpoint. + * + * @param string $endpoint Endpoint key. + * @param string $action Optional action or variation within the endpoint. + * + * @since 2.3.0 + * @since 4.6.0 Added $action parameter. + * @return string The page title. + */ + public function get_endpoint_title( $endpoint, $action = '' ) { + global $wp; + + switch ( $endpoint ) { + case 'order-pay': + $title = __( 'Pay for order', 'woocommerce' ); + break; + case 'order-received': + $title = __( 'Order received', 'woocommerce' ); + break; + case 'orders': + if ( ! empty( $wp->query_vars['orders'] ) ) { + /* translators: %s: page */ + $title = sprintf( __( 'Orders (page %d)', 'woocommerce' ), intval( $wp->query_vars['orders'] ) ); + } else { + $title = __( 'Orders', 'woocommerce' ); + } + break; + case 'view-order': + $order = wc_get_order( $wp->query_vars['view-order'] ); + /* translators: %s: order number */ + $title = ( $order ) ? sprintf( __( 'Order #%s', 'woocommerce' ), $order->get_order_number() ) : ''; + break; + case 'downloads': + $title = __( 'Downloads', 'woocommerce' ); + break; + case 'edit-account': + $title = __( 'Account details', 'woocommerce' ); + break; + case 'edit-address': + $title = __( 'Addresses', 'woocommerce' ); + break; + case 'payment-methods': + $title = __( 'Payment methods', 'woocommerce' ); + break; + case 'add-payment-method': + $title = __( 'Add payment method', 'woocommerce' ); + break; + case 'lost-password': + if ( in_array( $action, array( 'rp', 'resetpass', 'newaccount' ), true ) ) { + $title = __( 'Set password', 'woocommerce' ); + } else { + $title = __( 'Lost password', 'woocommerce' ); + } + break; + default: + $title = ''; + break; + } + + /** + * Filters the page title used for my-account endpoints. + * + * @since 2.6.0 + * @since 4.6.0 Added $action parameter. + * + * @see get_endpoint_title() + * + * @param string $title Default title. + * @param string $endpoint Endpoint key. + * @param string $action Optional action or variation within the endpoint. + */ + return apply_filters( 'woocommerce_endpoint_' . $endpoint . '_title', $title, $endpoint, $action ); + } + + /** + * Endpoint mask describing the places the endpoint should be added. + * + * @since 2.6.2 + * @return int + */ + public function get_endpoints_mask() { + if ( 'page' === get_option( 'show_on_front' ) ) { + $page_on_front = get_option( 'page_on_front' ); + $myaccount_page_id = get_option( 'woocommerce_myaccount_page_id' ); + $checkout_page_id = get_option( 'woocommerce_checkout_page_id' ); + + if ( in_array( $page_on_front, array( $myaccount_page_id, $checkout_page_id ), true ) ) { + return EP_ROOT | EP_PAGES; + } + } + + return EP_PAGES; + } + + /** + * Add endpoints for query vars. + */ + public function add_endpoints() { + $mask = $this->get_endpoints_mask(); + + foreach ( $this->get_query_vars() as $key => $var ) { + if ( ! empty( $var ) ) { + add_rewrite_endpoint( $var, $mask ); + } + } + } + + /** + * Add query vars. + * + * @param array $vars Query vars. + * @return array + */ + public function add_query_vars( $vars ) { + foreach ( $this->get_query_vars() as $key => $var ) { + $vars[] = $key; + } + return $vars; + } + + /** + * Get query vars. + * + * @return array + */ + public function get_query_vars() { + return apply_filters( 'woocommerce_get_query_vars', $this->query_vars ); + } + + /** + * Get query current active query var. + * + * @return string + */ + public function get_current_endpoint() { + global $wp; + + foreach ( $this->get_query_vars() as $key => $value ) { + if ( isset( $wp->query_vars[ $key ] ) ) { + return $key; + } + } + return ''; + } + + /** + * Parse the request and look for query vars - endpoints may not be supported. + */ + public function parse_request() { + global $wp; + + // phpcs:disable WordPress.Security.NonceVerification.Recommended + // Map query vars to their keys, or get them if endpoints are not supported. + foreach ( $this->get_query_vars() as $key => $var ) { + if ( isset( $_GET[ $var ] ) ) { + $wp->query_vars[ $key ] = sanitize_text_field( wp_unslash( $_GET[ $var ] ) ); + } elseif ( isset( $wp->query_vars[ $var ] ) ) { + $wp->query_vars[ $key ] = $wp->query_vars[ $var ]; + } + } + // phpcs:enable WordPress.Security.NonceVerification.Recommended + } + + /** + * Are we currently on the front page? + * + * @param WP_Query $q Query instance. + * @return bool + */ + private function is_showing_page_on_front( $q ) { + return ( $q->is_home() && ! $q->is_posts_page ) && 'page' === get_option( 'show_on_front' ); + } + + /** + * Is the front page a page we define? + * + * @param int $page_id Page ID. + * @return bool + */ + private function page_on_front_is( $page_id ) { + return absint( get_option( 'page_on_front' ) ) === absint( $page_id ); + } + + /** + * Hook into pre_get_posts to do the main product query. + * + * @param WP_Query $q Query instance. + */ + public function pre_get_posts( $q ) { + // We only want to affect the main query. + if ( ! $q->is_main_query() ) { + return; + } + + // Fixes for queries on static homepages. + if ( $this->is_showing_page_on_front( $q ) ) { + + // Fix for endpoints on the homepage. + if ( ! $this->page_on_front_is( $q->get( 'page_id' ) ) ) { + $_query = wp_parse_args( $q->query ); + if ( ! empty( $_query ) && array_intersect( array_keys( $_query ), array_keys( $this->get_query_vars() ) ) ) { + $q->is_page = true; + $q->is_home = false; + $q->is_singular = true; + $q->set( 'page_id', (int) get_option( 'page_on_front' ) ); + add_filter( 'redirect_canonical', '__return_false' ); + } + } + + // When orderby is set, WordPress shows posts on the front-page. Get around that here. + if ( $this->page_on_front_is( wc_get_page_id( 'shop' ) ) ) { + $_query = wp_parse_args( $q->query ); + if ( empty( $_query ) || ! array_diff( array_keys( $_query ), array( 'preview', 'page', 'paged', 'cpage', 'orderby' ) ) ) { + $q->set( 'page_id', (int) get_option( 'page_on_front' ) ); + $q->is_page = true; + $q->is_home = false; + + // WP supporting themes show post type archive. + if ( current_theme_supports( 'woocommerce' ) ) { + $q->set( 'post_type', 'product' ); + } else { + $q->is_singular = true; + } + } + } elseif ( ! empty( $_GET['orderby'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $q->set( 'page_id', (int) get_option( 'page_on_front' ) ); + $q->is_page = true; + $q->is_home = false; + $q->is_singular = true; + } + } + + // Fix product feeds. + if ( $q->is_feed() && $q->is_post_type_archive( 'product' ) ) { + $q->is_comment_feed = false; + } + + // Special check for shops with the PRODUCT POST TYPE ARCHIVE on front. + if ( current_theme_supports( 'woocommerce' ) && $q->is_page() && 'page' === get_option( 'show_on_front' ) && absint( $q->get( 'page_id' ) ) === wc_get_page_id( 'shop' ) ) { + // This is a front-page shop. + $q->set( 'post_type', 'product' ); + $q->set( 'page_id', '' ); + + if ( isset( $q->query['paged'] ) ) { + $q->set( 'paged', $q->query['paged'] ); + } + + // Define a variable so we know this is the front page shop later on. + wc_maybe_define_constant( 'SHOP_IS_ON_FRONT', true ); + + // Get the actual WP page to avoid errors and let us use is_front_page(). + // This is hacky but works. Awaiting https://core.trac.wordpress.org/ticket/21096. + global $wp_post_types; + + $shop_page = get_post( wc_get_page_id( 'shop' ) ); + + $wp_post_types['product']->ID = $shop_page->ID; + $wp_post_types['product']->post_title = $shop_page->post_title; + $wp_post_types['product']->post_name = $shop_page->post_name; + $wp_post_types['product']->post_type = $shop_page->post_type; + $wp_post_types['product']->ancestors = get_ancestors( $shop_page->ID, $shop_page->post_type ); + + // Fix conditional Functions like is_front_page. + $q->is_singular = false; + $q->is_post_type_archive = true; + $q->is_archive = true; + $q->is_page = true; + + // Remove post type archive name from front page title tag. + add_filter( 'post_type_archive_title', '__return_empty_string', 5 ); + + // Fix WP SEO. + if ( class_exists( 'WPSEO_Meta' ) ) { + add_filter( 'wpseo_metadesc', array( $this, 'wpseo_metadesc' ) ); + add_filter( 'wpseo_metakey', array( $this, 'wpseo_metakey' ) ); + } + } elseif ( ! $q->is_post_type_archive( 'product' ) && ! $q->is_tax( get_object_taxonomies( 'product' ) ) ) { + // Only apply to product categories, the product post archive, the shop page, product tags, and product attribute taxonomies. + return; + } + + $this->product_query( $q ); + } + + /** + * Handler for the 'the_posts' WP filter. + * + * @param array $posts Posts from WP Query. + * @param WP_Query $query Current query. + * + * @return array + */ + public function handle_get_posts( $posts, $query ) { + if ( 'product_query' !== $query->get( 'wc_query' ) ) { + return $posts; + } + $this->remove_product_query_filters( $posts ); + return $posts; + } + + + /** + * Pre_get_posts above may adjust the main query to add WooCommerce logic. When this query is done, we need to ensure + * all custom filters are removed. + * + * This is done here during the_posts filter. The input is not changed. + * + * @param array $posts Posts from WP Query. + * @return array + */ + public function remove_product_query_filters( $posts ) { + $this->remove_ordering_args(); + remove_filter( 'posts_clauses', array( $this, 'price_filter_post_clauses' ), 10, 2 ); + return $posts; + } + + /** + * This function used to be hooked to found_posts and adjust the posts count when the filtering by attribute + * widget was used and variable products were present. Now it isn't hooked anymore and does nothing but return + * the input unchanged, since the pull request in which it was introduced has been reverted. + * + * @since 4.4.0 + * @param int $count Original posts count, as supplied by the found_posts filter. + * @param WP_Query $query The current WP_Query object. + * + * @return int Adjusted posts count. + */ + public function adjust_posts_count( $count, $query ) { + return $count; + } + + /** + * Instance version of get_layered_nav_chosen_attributes, needed for unit tests. + * + * @return array + */ + protected function get_layered_nav_chosen_attributes_inst() { + return self::get_layered_nav_chosen_attributes(); + } + + /** + * Get the posts (or the ids of the posts) found in the current WP loop. + * + * @return array Array of posts or post ids. + */ + protected function get_current_posts() { + return $GLOBALS['wp_query']->posts; + } + + /** + * WP SEO meta description. + * + * Hooked into wpseo_ hook already, so no need for function_exist. + * + * @return string + */ + public function wpseo_metadesc() { + return WPSEO_Meta::get_value( 'metadesc', wc_get_page_id( 'shop' ) ); + } + + /** + * WP SEO meta key. + * + * Hooked into wpseo_ hook already, so no need for function_exist. + * + * @return string + */ + public function wpseo_metakey() { + return WPSEO_Meta::get_value( 'metakey', wc_get_page_id( 'shop' ) ); + } + + /** + * Query the products, applying sorting/ordering etc. + * This applies to the main WordPress loop. + * + * @param WP_Query $q Query instance. + */ + public function product_query( $q ) { + if ( ! is_feed() ) { + $ordering = $this->get_catalog_ordering_args(); + $q->set( 'orderby', $ordering['orderby'] ); + $q->set( 'order', $ordering['order'] ); + + if ( isset( $ordering['meta_key'] ) ) { + $q->set( 'meta_key', $ordering['meta_key'] ); + } + } + + // Query vars that affect posts shown. + $q->set( 'meta_query', $this->get_meta_query( $q->get( 'meta_query' ), true ) ); + $q->set( 'tax_query', $this->get_tax_query( $q->get( 'tax_query' ), true ) ); + $q->set( 'wc_query', 'product_query' ); + $q->set( 'post__in', array_unique( (array) apply_filters( 'loop_shop_post_in', array() ) ) ); + + // Work out how many products to query. + $q->set( 'posts_per_page', $q->get( 'posts_per_page' ) ? $q->get( 'posts_per_page' ) : apply_filters( 'loop_shop_per_page', wc_get_default_products_per_row() * wc_get_default_product_rows_per_page() ) ); + + // Store reference to this query. + self::$product_query = $q; + + // Additonal hooks to change WP Query. + add_filter( + 'posts_clauses', + function( $args, $wp_query ) { + return $this->product_query_post_clauses( $args, $wp_query ); + }, + 10, + 2 + ); + add_filter( 'the_posts', array( $this, 'handle_get_posts' ), 10, 2 ); + + do_action( 'woocommerce_product_query', $q, $this ); + } + + /** + * Add extra clauses to the product query. + * + * @param array $args Product query clauses. + * @param WP_Query $wp_query The current product query. + * @return array The updated product query clauses array. + */ + private function product_query_post_clauses( $args, $wp_query ) { + $args = $this->price_filter_post_clauses( $args, $wp_query ); + $args = $this->filterer->filter_by_attribute_post_clauses( $args, $wp_query, $this->get_layered_nav_chosen_attributes() ); + + return $args; + } + + /** + * Remove the query. + */ + public function remove_product_query() { + remove_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) ); + } + + /** + * Remove ordering queries. + */ + public function remove_ordering_args() { + remove_filter( 'posts_clauses', array( $this, 'order_by_price_asc_post_clauses' ) ); + remove_filter( 'posts_clauses', array( $this, 'order_by_price_desc_post_clauses' ) ); + remove_filter( 'posts_clauses', array( $this, 'order_by_popularity_post_clauses' ) ); + remove_filter( 'posts_clauses', array( $this, 'order_by_rating_post_clauses' ) ); + } + + /** + * Returns an array of arguments for ordering products based on the selected values. + * + * @param string $orderby Order by param. + * @param string $order Order param. + * @return array + */ + public function get_catalog_ordering_args( $orderby = '', $order = '' ) { + // Get ordering from query string unless defined. + if ( ! $orderby ) { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $orderby_value = isset( $_GET['orderby'] ) ? wc_clean( (string) wp_unslash( $_GET['orderby'] ) ) : wc_clean( get_query_var( 'orderby' ) ); + + if ( ! $orderby_value ) { + if ( is_search() ) { + $orderby_value = 'relevance'; + } else { + $orderby_value = apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby', 'menu_order' ) ); + } + } + + // Get order + orderby args from string. + $orderby_value = is_array( $orderby_value ) ? $orderby_value : explode( '-', $orderby_value ); + $orderby = esc_attr( $orderby_value[0] ); + $order = ! empty( $orderby_value[1] ) ? $orderby_value[1] : $order; + } + + // Convert to correct format. + $orderby = strtolower( is_array( $orderby ) ? (string) current( $orderby ) : (string) $orderby ); + $order = strtoupper( is_array( $order ) ? (string) current( $order ) : (string) $order ); + $args = array( + 'orderby' => $orderby, + 'order' => ( 'DESC' === $order ) ? 'DESC' : 'ASC', + 'meta_key' => '', // @codingStandardsIgnoreLine + ); + + switch ( $orderby ) { + case 'id': + $args['orderby'] = 'ID'; + break; + case 'menu_order': + $args['orderby'] = 'menu_order title'; + break; + case 'title': + $args['orderby'] = 'title'; + $args['order'] = ( 'DESC' === $order ) ? 'DESC' : 'ASC'; + break; + case 'relevance': + $args['orderby'] = 'relevance'; + $args['order'] = 'DESC'; + break; + case 'rand': + $args['orderby'] = 'rand'; // @codingStandardsIgnoreLine + break; + case 'date': + $args['orderby'] = 'date ID'; + $args['order'] = ( 'ASC' === $order ) ? 'ASC' : 'DESC'; + break; + case 'price': + $callback = 'DESC' === $order ? 'order_by_price_desc_post_clauses' : 'order_by_price_asc_post_clauses'; + add_filter( 'posts_clauses', array( $this, $callback ) ); + break; + case 'popularity': + add_filter( 'posts_clauses', array( $this, 'order_by_popularity_post_clauses' ) ); + break; + case 'rating': + add_filter( 'posts_clauses', array( $this, 'order_by_rating_post_clauses' ) ); + break; + } + + return apply_filters( 'woocommerce_get_catalog_ordering_args', $args, $orderby, $order ); + } + + /** + * Custom query used to filter products by price. + * + * @since 3.6.0 + * + * @param array $args Query args. + * @param WP_Query $wp_query WP_Query object. + * + * @return array + */ + public function price_filter_post_clauses( $args, $wp_query ) { + global $wpdb; + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( ! $wp_query->is_main_query() || ( ! isset( $_GET['max_price'] ) && ! isset( $_GET['min_price'] ) ) ) { + return $args; + } + + // phpcs:disable WordPress.Security.NonceVerification.Recommended + $current_min_price = isset( $_GET['min_price'] ) ? floatval( wp_unslash( $_GET['min_price'] ) ) : 0; + $current_max_price = isset( $_GET['max_price'] ) ? floatval( wp_unslash( $_GET['max_price'] ) ) : PHP_INT_MAX; + // phpcs:enable WordPress.Security.NonceVerification.Recommended + + /** + * Adjust if the store taxes are not displayed how they are stored. + * Kicks in when prices excluding tax are displayed including tax. + */ + if ( wc_tax_enabled() && 'incl' === get_option( 'woocommerce_tax_display_shop' ) && ! wc_prices_include_tax() ) { + $tax_class = apply_filters( 'woocommerce_price_filter_widget_tax_class', '' ); // Uses standard tax class. + $tax_rates = WC_Tax::get_rates( $tax_class ); + + if ( $tax_rates ) { + $current_min_price -= WC_Tax::get_tax_total( WC_Tax::calc_inclusive_tax( $current_min_price, $tax_rates ) ); + $current_max_price -= WC_Tax::get_tax_total( WC_Tax::calc_inclusive_tax( $current_max_price, $tax_rates ) ); + } + } + + $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); + $args['where'] .= $wpdb->prepare( + ' AND NOT (%fwc_product_meta_lookup.max_price ) ', + $current_max_price, + $current_min_price + ); + return $args; + } + + /** + * Handle numeric price sorting. + * + * @param array $args Query args. + * @return array + */ + public function order_by_price_asc_post_clauses( $args ) { + $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); + $args['orderby'] = ' wc_product_meta_lookup.min_price ASC, wc_product_meta_lookup.product_id ASC '; + return $args; + } + + /** + * Handle numeric price sorting. + * + * @param array $args Query args. + * @return array + */ + public function order_by_price_desc_post_clauses( $args ) { + $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); + $args['orderby'] = ' wc_product_meta_lookup.max_price DESC, wc_product_meta_lookup.product_id DESC '; + return $args; + } + + /** + * WP Core does not let us change the sort direction for individual orderby params - https://core.trac.wordpress.org/ticket/17065. + * + * This lets us sort by meta value desc, and have a second orderby param. + * + * @param array $args Query args. + * @return array + */ + public function order_by_popularity_post_clauses( $args ) { + $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); + $args['orderby'] = ' wc_product_meta_lookup.total_sales DESC, wc_product_meta_lookup.product_id DESC '; + return $args; + } + + /** + * Order by rating post clauses. + * + * @param array $args Query args. + * @return array + */ + public function order_by_rating_post_clauses( $args ) { + $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); + $args['orderby'] = ' wc_product_meta_lookup.average_rating DESC, wc_product_meta_lookup.rating_count DESC, wc_product_meta_lookup.product_id DESC '; + return $args; + } + + /** + * Join wc_product_meta_lookup to posts if not already joined. + * + * @param string $sql SQL join. + * @return string + */ + private function append_product_sorting_table_join( $sql ) { + global $wpdb; + + if ( ! strstr( $sql, 'wc_product_meta_lookup' ) ) { + $sql .= " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON $wpdb->posts.ID = wc_product_meta_lookup.product_id "; + } + return $sql; + } + + /** + * Appends meta queries to an array. + * + * @param array $meta_query Meta query. + * @param bool $main_query If is main query. + * @return array + */ + public function get_meta_query( $meta_query = array(), $main_query = false ) { + if ( ! is_array( $meta_query ) ) { + $meta_query = array(); + } + return array_filter( apply_filters( 'woocommerce_product_query_meta_query', $meta_query, $this ) ); + } + + /** + * Appends tax queries to an array. + * + * @param array $tax_query Tax query. + * @param bool $main_query If is main query. + * @return array + */ + public function get_tax_query( $tax_query = array(), $main_query = false ) { + if ( ! is_array( $tax_query ) ) { + $tax_query = array( + 'relation' => 'AND', + ); + } + + if ( $main_query && ! $this->filterer->filtering_via_lookup_table_is_active() ) { + // Layered nav filters on terms. + foreach ( $this->get_layered_nav_chosen_attributes() as $taxonomy => $data ) { + $tax_query[] = array( + 'taxonomy' => $taxonomy, + 'field' => 'slug', + 'terms' => $data['terms'], + 'operator' => 'and' === $data['query_type'] ? 'AND' : 'IN', + 'include_children' => false, + ); + } + } + + $product_visibility_terms = wc_get_product_visibility_term_ids(); + $product_visibility_not_in = array( is_search() && $main_query ? $product_visibility_terms['exclude-from-search'] : $product_visibility_terms['exclude-from-catalog'] ); + + // Hide out of stock products. + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { + $product_visibility_not_in[] = $product_visibility_terms['outofstock']; + } + + // phpcs:disable WordPress.Security.NonceVerification.Recommended + // Filter by rating. + if ( isset( $_GET['rating_filter'] ) ) { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $rating_filter = array_filter( array_map( 'absint', explode( ',', wp_unslash( $_GET['rating_filter'] ) ) ) ); + $rating_terms = array(); + for ( $i = 1; $i <= 5; $i ++ ) { + if ( in_array( $i, $rating_filter, true ) && isset( $product_visibility_terms[ 'rated-' . $i ] ) ) { + $rating_terms[] = $product_visibility_terms[ 'rated-' . $i ]; + } + } + if ( ! empty( $rating_terms ) ) { + $tax_query[] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => $rating_terms, + 'operator' => 'IN', + 'rating_filter' => true, + ); + } + } + // phpcs:enable WordPress.Security.NonceVerification.Recommended + + if ( ! empty( $product_visibility_not_in ) ) { + $tax_query[] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => $product_visibility_not_in, + 'operator' => 'NOT IN', + ); + } + + return array_filter( apply_filters( 'woocommerce_product_query_tax_query', $tax_query, $this ) ); + } + + /** + * Get the main query which product queries ran against. + * + * @return WP_Query + */ + public static function get_main_query() { + return self::$product_query; + } + + /** + * Get the tax query which was used by the main query. + * + * @return array + */ + public static function get_main_tax_query() { + $tax_query = isset( self::$product_query->tax_query, self::$product_query->tax_query->queries ) ? self::$product_query->tax_query->queries : array(); + + return $tax_query; + } + + /** + * Get the meta query which was used by the main query. + * + * @return array + */ + public static function get_main_meta_query() { + $args = self::$product_query->query_vars; + $meta_query = isset( $args['meta_query'] ) ? $args['meta_query'] : array(); + + return $meta_query; + } + + /** + * Based on WP_Query::parse_search + */ + public static function get_main_search_query_sql() { + global $wpdb; + + $args = self::$product_query->query_vars; + $search_terms = isset( $args['search_terms'] ) ? $args['search_terms'] : array(); + $sql = array(); + + foreach ( $search_terms as $term ) { + // Terms prefixed with '-' should be excluded. + $include = '-' !== substr( $term, 0, 1 ); + + if ( $include ) { + $like_op = 'LIKE'; + $andor_op = 'OR'; + } else { + $like_op = 'NOT LIKE'; + $andor_op = 'AND'; + $term = substr( $term, 1 ); + } + + $like = '%' . $wpdb->esc_like( $term ) . '%'; + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $sql[] = $wpdb->prepare( "(($wpdb->posts.post_title $like_op %s) $andor_op ($wpdb->posts.post_excerpt $like_op %s) $andor_op ($wpdb->posts.post_content $like_op %s))", $like, $like, $like ); + } + + if ( ! empty( $sql ) && ! is_user_logged_in() ) { + $sql[] = "($wpdb->posts.post_password = '')"; + } + + return implode( ' AND ', $sql ); + } + + /** + * Get an array of attributes and terms selected with the layered nav widget. + * + * @return array + */ + public static function get_layered_nav_chosen_attributes() { + // phpcs:disable WordPress.Security.NonceVerification.Recommended + if ( ! is_array( self::$chosen_attributes ) ) { + self::$chosen_attributes = array(); + + if ( ! empty( $_GET ) ) { + foreach ( $_GET as $key => $value ) { + if ( 0 === strpos( $key, 'filter_' ) ) { + $attribute = wc_sanitize_taxonomy_name( str_replace( 'filter_', '', $key ) ); + $taxonomy = wc_attribute_taxonomy_name( $attribute ); + $filter_terms = ! empty( $value ) ? explode( ',', wc_clean( wp_unslash( $value ) ) ) : array(); + + if ( empty( $filter_terms ) || ! taxonomy_exists( $taxonomy ) || ! wc_attribute_taxonomy_id_by_name( $attribute ) ) { + continue; + } + + $query_type = ! empty( $_GET[ 'query_type_' . $attribute ] ) && in_array( $_GET[ 'query_type_' . $attribute ], array( 'and', 'or' ), true ) ? wc_clean( wp_unslash( $_GET[ 'query_type_' . $attribute ] ) ) : ''; + self::$chosen_attributes[ $taxonomy ]['terms'] = array_map( 'sanitize_title', $filter_terms ); // Ensures correct encoding. + self::$chosen_attributes[ $taxonomy ]['query_type'] = $query_type ? $query_type : apply_filters( 'woocommerce_layered_nav_default_query_type', 'and' ); + } + } + } + } + return self::$chosen_attributes; + // phpcs:disable WordPress.Security.NonceVerification.Recommended + } + + /** + * Remove the add-to-cart param from pagination urls. + * + * @param string $url URL. + * @return string + */ + public function remove_add_to_cart_pagination( $url ) { + return remove_query_arg( 'add-to-cart', $url ); + } + + /** + * Return a meta query for filtering by rating. + * + * @deprecated 3.0.0 Replaced with taxonomy. + * @return array + */ + public function rating_filter_meta_query() { + return array(); + } + + /** + * Returns a meta query to handle product visibility. + * + * @deprecated 3.0.0 Replaced with taxonomy. + * @param string $compare (default: 'IN'). + * @return array + */ + public function visibility_meta_query( $compare = 'IN' ) { + return array(); + } + + /** + * Returns a meta query to handle product stock status. + * + * @deprecated 3.0.0 Replaced with taxonomy. + * @param string $status (default: 'instock'). + * @return array + */ + public function stock_status_meta_query( $status = 'instock' ) { + return array(); + } + + /** + * Layered nav init. + * + * @deprecated 2.6.0 + */ + public function layered_nav_init() { + wc_deprecated_function( 'layered_nav_init', '2.6' ); + } + + /** + * Get an unpaginated list all product IDs (both filtered and unfiltered). Makes use of transients. + * + * @deprecated 2.6.0 due to performance concerns + */ + public function get_products_in_view() { + wc_deprecated_function( 'get_products_in_view', '2.6' ); + } + + /** + * Layered Nav post filter. + * + * @deprecated 2.6.0 due to performance concerns + * + * @param mixed $deprecated Deprecated. + */ + public function layered_nav_query( $deprecated ) { + wc_deprecated_function( 'layered_nav_query', '2.6' ); + } + + /** + * Search post excerpt. + * + * @param string $where Where clause. + * + * @deprecated 3.2.0 - Not needed anymore since WordPress 4.5. + */ + public function search_post_excerpt( $where = '' ) { + wc_deprecated_function( 'WC_Query::search_post_excerpt', '3.2.0', 'Excerpt added to search query by default since WordPress 4.5.' ); + return $where; + } + + /** + * Remove the posts_where filter. + * + * @deprecated 3.2.0 - Nothing to remove anymore because search_post_excerpt() is deprecated. + */ + public function remove_posts_where() { + wc_deprecated_function( 'WC_Query::remove_posts_where', '3.2.0', 'Nothing to remove anymore because search_post_excerpt() is deprecated.' ); + } +} diff --git a/plugins/woocommerce/includes/class-wc-rate-limiter.php b/plugins/woocommerce/includes/class-wc-rate-limiter.php new file mode 100644 index 00000000000..482f0e63da6 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-rate-limiter.php @@ -0,0 +1,173 @@ +get_var( + $wpdb->prepare( + " + SELECT rate_limit_expiry + FROM {$wpdb->prefix}wc_rate_limits + WHERE rate_limit_key = %s + ", + $action_id + ) + ); + + self::set_cache( $action_id, $next_try_allowed_at ); + } + + // No record of action running, so action is allowed to run. + if ( null === $next_try_allowed_at ) { + return false; + } + + // Before the next run is allowed, retry forbidden. + if ( time() <= $next_try_allowed_at ) { + return true; + } + + // After the next run is allowed, retry allowed. + return false; + } + + /** + * Sets the rate limit delay in seconds for action with identifier $id. + * + * @param string $action_id Identifier of the action. + * @param int $delay Delay in seconds. + * @return bool True if the option setting was successful, false otherwise. + */ + public static function set_rate_limit( $action_id, $delay ) { + global $wpdb; + + $next_try_allowed_at = time() + $delay; + + $result = $wpdb->replace( + $wpdb->prefix . 'wc_rate_limits', + array( + 'rate_limit_key' => $action_id, + 'rate_limit_expiry' => $next_try_allowed_at, + ), + array( '%s', '%d' ) + ); + + self::set_cache( $action_id, $next_try_allowed_at ); + + return false !== $result; + } + + /** + * Cleanup expired rate limits from the database and clear caches. + */ + public static function cleanup() { + global $wpdb; + + $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->prefix}wc_rate_limits WHERE rate_limit_expiry < %d", + time() + ) + ); + + if ( class_exists( 'WC_Cache_Helper' ) ) { + WC_Cache_Helper::invalidate_cache_group( self::CACHE_GROUP ); + } + } +} + +WC_Rate_Limiter::init(); diff --git a/includes/class-wc-regenerate-images-request.php b/plugins/woocommerce/includes/class-wc-regenerate-images-request.php similarity index 100% rename from includes/class-wc-regenerate-images-request.php rename to plugins/woocommerce/includes/class-wc-regenerate-images-request.php diff --git a/includes/class-wc-regenerate-images.php b/plugins/woocommerce/includes/class-wc-regenerate-images.php similarity index 100% rename from includes/class-wc-regenerate-images.php rename to plugins/woocommerce/includes/class-wc-regenerate-images.php diff --git a/plugins/woocommerce/includes/class-wc-register-wp-admin-settings.php b/plugins/woocommerce/includes/class-wc-register-wp-admin-settings.php new file mode 100644 index 00000000000..442cfb8d1fd --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-register-wp-admin-settings.php @@ -0,0 +1,185 @@ +object = $object; + + if ( 'page' === $type ) { + add_filter( 'woocommerce_settings_groups', array( $this, 'register_page_group' ) ); + add_filter( 'woocommerce_settings-' . $this->object->get_id(), array( $this, 'register_page_settings' ) ); + } elseif ( 'email' === $type ) { + add_filter( 'woocommerce_settings_groups', array( $this, 'register_email_group' ) ); + add_filter( 'woocommerce_settings-email_' . $this->object->id, array( $this, 'register_email_settings' ) ); + } + } + + /** + * Register's all of our different notification emails as sub groups + * of email settings. + * + * @since 3.0.0 + * @param array $groups Existing registered groups. + * @return array + */ + public function register_email_group( $groups ) { + $groups[] = array( + 'id' => 'email_' . $this->object->id, + 'label' => $this->object->title, + 'description' => $this->object->description, + 'parent_id' => 'email', + ); + return $groups; + } + + /** + * Registers all of the setting form fields for emails to each email type's group. + * + * @since 3.0.0 + * @param array $settings Existing registered settings. + * @return array + */ + public function register_email_settings( $settings ) { + foreach ( $this->object->form_fields as $id => $setting ) { + $setting['id'] = $id; + $setting['option_key'] = array( $this->object->get_option_key(), $id ); + $new_setting = $this->register_setting( $setting ); + if ( $new_setting ) { + $settings[] = $new_setting; + } + } + return $settings; + } + + /** + * Registers a setting group, based on admin page ID & label as parent group. + * + * @since 3.0.0 + * @param array $groups Array of previously registered groups. + * @return array + */ + public function register_page_group( $groups ) { + $groups[] = array( + 'id' => $this->object->get_id(), + 'label' => $this->object->get_label(), + ); + return $groups; + } + + /** + * Registers settings to a specific group. + * + * @since 3.0.0 + * @param array $settings Existing registered settings. + * @return array + */ + public function register_page_settings( $settings ) { + /** + * WP admin settings can be broken down into separate sections from + * a UI standpoint. This will grab all the sections associated with + * a particular setting group (like 'products') and register them + * to the REST API. + */ + $sections = $this->object->get_sections(); + if ( empty( $sections ) ) { + // Default section is just an empty string, per admin page classes. + $sections = array( '' => '' ); + } + + /** + * We are using 'WC_Settings_Page::get_settings' on purpose even thought it's deprecated. + * See the method documentation for an explanation. + */ + + foreach ( $sections as $section => $section_label ) { + $settings_from_section = $this->object->get_settings( $section ); + foreach ( $settings_from_section as $setting ) { + if ( ! isset( $setting['id'] ) ) { + continue; + } + $setting['option_key'] = $setting['id']; + $new_setting = $this->register_setting( $setting ); + if ( $new_setting ) { + $settings[] = $new_setting; + } + } + } + return $settings; + } + + /** + * Register a setting into the format expected for the Settings REST API. + * + * @since 3.0.0 + * @param array $setting Setting data. + * @return array|bool + */ + public function register_setting( $setting ) { + if ( ! isset( $setting['id'] ) ) { + return false; + } + + $description = ''; + if ( ! empty( $setting['desc'] ) ) { + $description = $setting['desc']; + } elseif ( ! empty( $setting['description'] ) ) { + $description = $setting['description']; + } + + $new_setting = array( + 'id' => $setting['id'], + 'label' => ( ! empty( $setting['title'] ) ? $setting['title'] : '' ), + 'description' => $description, + 'type' => $setting['type'], + 'option_key' => $setting['option_key'], + ); + + if ( isset( $setting['default'] ) ) { + $new_setting['default'] = $setting['default']; + } + if ( isset( $setting['options'] ) ) { + $new_setting['options'] = $setting['options']; + } + if ( isset( $setting['desc_tip'] ) ) { + if ( true === $setting['desc_tip'] ) { + $new_setting['tip'] = $description; + } elseif ( ! empty( $setting['desc_tip'] ) ) { + $new_setting['tip'] = $setting['desc_tip']; + } + } + + return $new_setting; + } + +} diff --git a/includes/class-wc-rest-authentication.php b/plugins/woocommerce/includes/class-wc-rest-authentication.php similarity index 100% rename from includes/class-wc-rest-authentication.php rename to plugins/woocommerce/includes/class-wc-rest-authentication.php diff --git a/includes/class-wc-rest-exception.php b/plugins/woocommerce/includes/class-wc-rest-exception.php similarity index 100% rename from includes/class-wc-rest-exception.php rename to plugins/woocommerce/includes/class-wc-rest-exception.php diff --git a/plugins/woocommerce/includes/class-wc-session-handler.php b/plugins/woocommerce/includes/class-wc-session-handler.php new file mode 100644 index 00000000000..332ecb5e7f6 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-session-handler.php @@ -0,0 +1,495 @@ +_cookie = apply_filters( 'woocommerce_cookie', 'wp_woocommerce_session_' . COOKIEHASH ); + $this->_table = $GLOBALS['wpdb']->prefix . 'woocommerce_sessions'; + } + + /** + * Init hooks and session data. + * + * @since 3.3.0 + */ + public function init() { + $this->init_session_cookie(); + + add_action( 'woocommerce_set_cart_cookies', array( $this, 'set_customer_session_cookie' ), 10 ); + add_action( 'shutdown', array( $this, 'save_data' ), 20 ); + add_action( 'wp_logout', array( $this, 'destroy_session' ) ); + + if ( ! is_user_logged_in() ) { + add_filter( 'nonce_user_logged_out', array( $this, 'maybe_update_nonce_user_logged_out' ), 10, 2 ); + } + } + + /** + * Setup cookie and customer ID. + * + * @since 3.6.0 + */ + public function init_session_cookie() { + $cookie = $this->get_session_cookie(); + + if ( $cookie ) { + // Customer ID will be an MD5 hash id this is a guest session. + $this->_customer_id = $cookie[0]; + $this->_session_expiration = $cookie[1]; + $this->_session_expiring = $cookie[2]; + $this->_has_cookie = true; + $this->_data = $this->get_session_data(); + + if ( ! $this->is_session_cookie_valid() ) { + $this->destroy_session(); + $this->set_session_expiration(); + } + + // If the user logs in, update session. + if ( is_user_logged_in() && strval( get_current_user_id() ) !== $this->_customer_id ) { + $guest_session_id = $this->_customer_id; + $this->_customer_id = strval( get_current_user_id() ); + $this->_dirty = true; + $this->save_data( $guest_session_id ); + $this->set_customer_session_cookie( true ); + } + + // Update session if its close to expiring. + if ( time() > $this->_session_expiring ) { + $this->set_session_expiration(); + $this->update_session_timestamp( $this->_customer_id, $this->_session_expiration ); + } + } else { + $this->set_session_expiration(); + $this->_customer_id = $this->generate_customer_id(); + $this->_data = $this->get_session_data(); + } + } + + /** + * Checks if session cookie is expired, or belongs to a logged out user. + * + * @return bool Whether session cookie is valid. + */ + private function is_session_cookie_valid() { + // If session is expired, session cookie is invalid. + if ( time() > $this->_session_expiration ) { + return false; + } + + // If user has logged out, session cookie is invalid. + if ( ! is_user_logged_in() && ! $this->is_customer_guest( $this->_customer_id ) ) { + return false; + } + + // Session from a different user is not valid. (Although from a guest user will be valid) + if ( is_user_logged_in() && ! $this->is_customer_guest( $this->_customer_id ) && strval( get_current_user_id() ) !== $this->_customer_id ) { + return false; + } + + return true; + } + + /** + * Sets the session cookie on-demand (usually after adding an item to the cart). + * + * Since the cookie name (as of 2.1) is prepended with wp, cache systems like batcache will not cache pages when set. + * + * Warning: Cookies will only be set if this is called before the headers are sent. + * + * @param bool $set Should the session cookie be set. + */ + public function set_customer_session_cookie( $set ) { + if ( $set ) { + $to_hash = $this->_customer_id . '|' . $this->_session_expiration; + $cookie_hash = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) ); + $cookie_value = $this->_customer_id . '||' . $this->_session_expiration . '||' . $this->_session_expiring . '||' . $cookie_hash; + $this->_has_cookie = true; + + if ( ! isset( $_COOKIE[ $this->_cookie ] ) || $_COOKIE[ $this->_cookie ] !== $cookie_value ) { + wc_setcookie( $this->_cookie, $cookie_value, $this->_session_expiration, $this->use_secure_cookie(), true ); + } + } + } + + /** + * Should the session cookie be secure? + * + * @since 3.6.0 + * @return bool + */ + protected function use_secure_cookie() { + return apply_filters( 'wc_session_use_secure_cookie', wc_site_is_https() && is_ssl() ); + } + + /** + * Return true if the current user has an active session, i.e. a cookie to retrieve values. + * + * @return bool + */ + public function has_session() { + return isset( $_COOKIE[ $this->_cookie ] ) || $this->_has_cookie || is_user_logged_in(); // @codingStandardsIgnoreLine. + } + + /** + * Set session expiration. + */ + public function set_session_expiration() { + $this->_session_expiring = time() + intval( apply_filters( 'wc_session_expiring', 60 * 60 * 47 ) ); // 47 Hours. + $this->_session_expiration = time() + intval( apply_filters( 'wc_session_expiration', 60 * 60 * 48 ) ); // 48 Hours. + } + + /** + * Generate a unique customer ID for guests, or return user ID if logged in. + * + * Uses Portable PHP password hashing framework to generate a unique cryptographically strong ID. + * + * @return string + */ + public function generate_customer_id() { + $customer_id = ''; + + if ( is_user_logged_in() ) { + $customer_id = strval( get_current_user_id() ); + } + + if ( empty( $customer_id ) ) { + require_once ABSPATH . 'wp-includes/class-phpass.php'; + $hasher = new PasswordHash( 8, false ); + $customer_id = 't_' . substr( md5( $hasher->get_random_bytes( 32 ) ), 2 ); + } + + return $customer_id; + } + + /** + * Checks if this is an auto-generated customer ID. + * + * @param string|int $customer_id Customer ID to check. + * + * @return bool Whether customer ID is randomly generated. + */ + private function is_customer_guest( $customer_id ) { + $customer_id = strval( $customer_id ); + + if ( empty( $customer_id ) ) { + return true; + } + + if ( 't_' === substr( $customer_id, 0, 2 ) ) { + return true; + } + + /** + * Legacy checks. This is to handle sessions that were created from a previous release. + * Maybe we can get rid of them after a few releases. + */ + + // Almost all random $customer_ids will have some letters in it, while all actual ids will be integers. + if ( strval( (int) $customer_id ) !== $customer_id ) { + return true; + } + + // Performance hack to potentially save a DB query, when same user as $customer_id is logged in. + if ( is_user_logged_in() && strval( get_current_user_id() ) === $customer_id ) { + return false; + } else { + $customer = new WC_Customer( $customer_id ); + + if ( 0 === $customer->get_id() ) { + return true; + } + } + + return false; + } + + /** + * Get session unique ID for requests if session is initialized or user ID if logged in. + * Introduced to help with unit tests. + * + * @since 5.3.0 + * @return string + */ + public function get_customer_unique_id() { + $customer_id = ''; + + if ( $this->has_session() && $this->_customer_id ) { + $customer_id = $this->_customer_id; + } elseif ( is_user_logged_in() ) { + $customer_id = (string) get_current_user_id(); + } + + return $customer_id; + } + + /** + * Get the session cookie, if set. Otherwise return false. + * + * Session cookies without a customer ID are invalid. + * + * @return bool|array + */ + public function get_session_cookie() { + $cookie_value = isset( $_COOKIE[ $this->_cookie ] ) ? wp_unslash( $_COOKIE[ $this->_cookie ] ) : false; // @codingStandardsIgnoreLine. + + if ( empty( $cookie_value ) || ! is_string( $cookie_value ) ) { + return false; + } + + list( $customer_id, $session_expiration, $session_expiring, $cookie_hash ) = explode( '||', $cookie_value ); + + if ( empty( $customer_id ) ) { + return false; + } + + // Validate hash. + $to_hash = $customer_id . '|' . $session_expiration; + $hash = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) ); + + if ( empty( $cookie_hash ) || ! hash_equals( $hash, $cookie_hash ) ) { + return false; + } + + return array( $customer_id, $session_expiration, $session_expiring, $cookie_hash ); + } + + /** + * Get session data. + * + * @return array + */ + public function get_session_data() { + return $this->has_session() ? (array) $this->get_session( $this->_customer_id, array() ) : array(); + } + + /** + * Gets a cache prefix. This is used in session names so the entire cache can be invalidated with 1 function call. + * + * @return string + */ + private function get_cache_prefix() { + return WC_Cache_Helper::get_cache_prefix( WC_SESSION_CACHE_GROUP ); + } + + /** + * Save data and delete guest session. + * + * @param int $old_session_key session ID before user logs in. + */ + public function save_data( $old_session_key = 0 ) { + // Dirty if something changed - prevents saving nothing new. + if ( $this->_dirty && $this->has_session() ) { + global $wpdb; + + $wpdb->query( + $wpdb->prepare( + "INSERT INTO {$wpdb->prefix}woocommerce_sessions (`session_key`, `session_value`, `session_expiry`) VALUES (%s, %s, %d) + ON DUPLICATE KEY UPDATE `session_value` = VALUES(`session_value`), `session_expiry` = VALUES(`session_expiry`)", + $this->_customer_id, + maybe_serialize( $this->_data ), + $this->_session_expiration + ) + ); + + wp_cache_set( $this->get_cache_prefix() . $this->_customer_id, $this->_data, WC_SESSION_CACHE_GROUP, $this->_session_expiration - time() ); + $this->_dirty = false; + if ( get_current_user_id() != $old_session_key && ! is_object( get_user_by( 'id', $old_session_key ) ) ) { + $this->delete_session( $old_session_key ); + } + } + } + + /** + * Destroy all session data. + */ + public function destroy_session() { + $this->delete_session( $this->_customer_id ); + $this->forget_session(); + } + + /** + * Forget all session data without destroying it. + */ + public function forget_session() { + wc_setcookie( $this->_cookie, '', time() - YEAR_IN_SECONDS, $this->use_secure_cookie(), true ); + + wc_empty_cart(); + + $this->_data = array(); + $this->_dirty = false; + $this->_customer_id = $this->generate_customer_id(); + } + + /** + * When a user is logged out, ensure they have a unique nonce by using the customer/session ID. + * + * @deprecated 5.3.0 + * @param int $uid User ID. + * @return int|string + */ + public function nonce_user_logged_out( $uid ) { + wc_deprecated_function( 'WC_Session_Handler::nonce_user_logged_out', '5.3', 'WC_Session_Handler::maybe_update_nonce_user_logged_out' ); + + return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid; + } + + /** + * When a user is logged out, ensure they have a unique nonce to manage cart and more using the customer/session ID. + * This filter runs everything `wp_verify_nonce()` and `wp_create_nonce()` gets called. + * + * @since 5.3.0 + * @param int $uid User ID. + * @param string $action The nonce action. + * @return int|string + */ + public function maybe_update_nonce_user_logged_out( $uid, $action ) { + if ( Automattic\WooCommerce\Utilities\StringUtil::starts_with( $action, 'woocommerce' ) ) { + return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid; + } + + return $uid; + } + + /** + * Cleanup session data from the database and clear caches. + */ + public function cleanup_sessions() { + global $wpdb; + + $wpdb->query( $wpdb->prepare( "DELETE FROM $this->_table WHERE session_expiry < %d", time() ) ); // @codingStandardsIgnoreLine. + + if ( class_exists( 'WC_Cache_Helper' ) ) { + WC_Cache_Helper::invalidate_cache_group( WC_SESSION_CACHE_GROUP ); + } + } + + /** + * Returns the session. + * + * @param string $customer_id Customer ID. + * @param mixed $default Default session value. + * @return string|array + */ + public function get_session( $customer_id, $default = false ) { + global $wpdb; + + if ( Constants::is_defined( 'WP_SETUP_CONFIG' ) ) { + return false; + } + + // Try to get it from the cache, it will return false if not present or if object cache not in use. + $value = wp_cache_get( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP ); + + if ( false === $value ) { + $value = $wpdb->get_var( $wpdb->prepare( "SELECT session_value FROM $this->_table WHERE session_key = %s", $customer_id ) ); // @codingStandardsIgnoreLine. + + if ( is_null( $value ) ) { + $value = $default; + } + + $cache_duration = $this->_session_expiration - time(); + if ( 0 < $cache_duration ) { + wp_cache_add( $this->get_cache_prefix() . $customer_id, $value, WC_SESSION_CACHE_GROUP, $cache_duration ); + } + } + + return maybe_unserialize( $value ); + } + + /** + * Delete the session from the cache and database. + * + * @param int $customer_id Customer ID. + */ + public function delete_session( $customer_id ) { + global $wpdb; + + wp_cache_delete( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP ); + + $wpdb->delete( + $this->_table, + array( + 'session_key' => $customer_id, + ) + ); + } + + /** + * Update the session expiry timestamp. + * + * @param string $customer_id Customer ID. + * @param int $timestamp Timestamp to expire the cookie. + */ + public function update_session_timestamp( $customer_id, $timestamp ) { + global $wpdb; + + $wpdb->update( + $this->_table, + array( + 'session_expiry' => $timestamp, + ), + array( + 'session_key' => $customer_id, + ), + array( + '%d', + ) + ); + } +} diff --git a/plugins/woocommerce/includes/class-wc-shipping-rate.php b/plugins/woocommerce/includes/class-wc-shipping-rate.php new file mode 100644 index 00000000000..f7eedbf3045 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-shipping-rate.php @@ -0,0 +1,255 @@ + '', + 'method_id' => '', + 'instance_id' => 0, + 'label' => '', + 'cost' => 0, + 'taxes' => array(), + ); + + /** + * Stores meta data for this rate. + * + * @since 2.6.0 + * @var array + */ + protected $meta_data = array(); + + /** + * Constructor. + * + * @param string $id Shipping rate ID. + * @param string $label Shipping rate label. + * @param integer $cost Cost. + * @param array $taxes Taxes applied to shipping rate. + * @param string $method_id Shipping method ID. + * @param int $instance_id Shipping instance ID. + */ + public function __construct( $id = '', $label = '', $cost = 0, $taxes = array(), $method_id = '', $instance_id = 0 ) { + $this->set_id( $id ); + $this->set_label( $label ); + $this->set_cost( $cost ); + $this->set_taxes( $taxes ); + $this->set_method_id( $method_id ); + $this->set_instance_id( $instance_id ); + } + + /** + * Magic methods to support direct access to props. + * + * @since 3.2.0 + * @param string $key Key. + * @return bool + */ + public function __isset( $key ) { + if ( 'meta_data' === $key ) { + wc_doing_it_wrong( __FUNCTION__, __( 'Use `array_key_exists` to check for meta_data on WC_Shipping_Rate to get the correct result.', 'woocommerce' ), '6.0' ); + } + return isset( $this->data[ $key ] ); + } + + /** + * Magic methods to support direct access to props. + * + * @since 3.2.0 + * @param string $key Key. + * @return mixed + */ + public function __get( $key ) { + if ( is_callable( array( $this, "get_{$key}" ) ) ) { + return $this->{"get_{$key}"}(); + } elseif ( isset( $this->data[ $key ] ) ) { + return $this->data[ $key ]; + } else { + return ''; + } + } + + /** + * Magic methods to support direct access to props. + * + * @since 3.2.0 + * @param string $key Key. + * @param mixed $value Value. + */ + public function __set( $key, $value ) { + if ( is_callable( array( $this, "set_{$key}" ) ) ) { + $this->{"set_{$key}"}( $value ); + } else { + $this->data[ $key ] = $value; + } + } + + /** + * Set ID for the rate. This is usually a combination of the method and instance IDs. + * + * @since 3.2.0 + * @param string $id Shipping rate ID. + */ + public function set_id( $id ) { + $this->data['id'] = (string) $id; + } + + /** + * Set shipping method ID the rate belongs to. + * + * @since 3.2.0 + * @param string $method_id Shipping method ID. + */ + public function set_method_id( $method_id ) { + $this->data['method_id'] = (string) $method_id; + } + + /** + * Set instance ID the rate belongs to. + * + * @since 3.2.0 + * @param int $instance_id Instance ID. + */ + public function set_instance_id( $instance_id ) { + $this->data['instance_id'] = absint( $instance_id ); + } + + /** + * Set rate label. + * + * @since 3.2.0 + * @param string $label Shipping rate label. + */ + public function set_label( $label ) { + $this->data['label'] = (string) $label; + } + + /** + * Set rate cost. + * + * @todo 4.0 Prevent negative value being set. #19293 + * @since 3.2.0 + * @param string $cost Shipping rate cost. + */ + public function set_cost( $cost ) { + $this->data['cost'] = $cost; + } + + /** + * Set rate taxes. + * + * @since 3.2.0 + * @param array $taxes List of taxes applied to shipping rate. + */ + public function set_taxes( $taxes ) { + $this->data['taxes'] = ! empty( $taxes ) && is_array( $taxes ) ? $taxes : array(); + } + + /** + * Get ID for the rate. This is usually a combination of the method and instance IDs. + * + * @since 3.2.0 + * @return string + */ + public function get_id() { + return apply_filters( 'woocommerce_shipping_rate_id', $this->data['id'], $this ); + } + + /** + * Get shipping method ID the rate belongs to. + * + * @since 3.2.0 + * @return string + */ + public function get_method_id() { + return apply_filters( 'woocommerce_shipping_rate_method_id', $this->data['method_id'], $this ); + } + + /** + * Get instance ID the rate belongs to. + * + * @since 3.2.0 + * @return int + */ + public function get_instance_id() { + return apply_filters( 'woocommerce_shipping_rate_instance_id', $this->data['instance_id'], $this ); + } + + /** + * Get rate label. + * + * @return string + */ + public function get_label() { + return apply_filters( 'woocommerce_shipping_rate_label', $this->data['label'], $this ); + } + + /** + * Get rate cost. + * + * @since 3.2.0 + * @return string + */ + public function get_cost() { + return apply_filters( 'woocommerce_shipping_rate_cost', $this->data['cost'], $this ); + } + + /** + * Get rate taxes. + * + * @since 3.2.0 + * @return array + */ + public function get_taxes() { + return apply_filters( 'woocommerce_shipping_rate_taxes', $this->data['taxes'], $this ); + } + + /** + * Get shipping tax. + * + * @return array + */ + public function get_shipping_tax() { + return apply_filters( 'woocommerce_get_shipping_tax', count( $this->taxes ) > 0 && ! WC()->customer->get_is_vat_exempt() ? array_sum( $this->taxes ) : 0, $this ); + } + + /** + * Add some meta data for this rate. + * + * @since 2.6.0 + * @param string $key Key. + * @param string $value Value. + */ + public function add_meta_data( $key, $value ) { + $this->meta_data[ wc_clean( $key ) ] = wc_clean( $value ); + } + + /** + * Get all meta data for this rate. + * + * @since 2.6.0 + * @return array + */ + public function get_meta_data() { + return $this->meta_data; + } +} diff --git a/includes/class-wc-shipping-zone.php b/plugins/woocommerce/includes/class-wc-shipping-zone.php similarity index 100% rename from includes/class-wc-shipping-zone.php rename to plugins/woocommerce/includes/class-wc-shipping-zone.php diff --git a/includes/class-wc-shipping-zones.php b/plugins/woocommerce/includes/class-wc-shipping-zones.php similarity index 100% rename from includes/class-wc-shipping-zones.php rename to plugins/woocommerce/includes/class-wc-shipping-zones.php diff --git a/plugins/woocommerce/includes/class-wc-shipping.php b/plugins/woocommerce/includes/class-wc-shipping.php new file mode 100644 index 00000000000..f4f9eba55f4 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-shipping.php @@ -0,0 +1,413 @@ +cart->get_shipping_total(); + } + if ( 'shipping_taxes' === $name ) { + return WC()->cart->get_shipping_taxes(); + } + } + + /** + * Initialize shipping. + */ + public function __construct() { + $this->enabled = wc_shipping_enabled(); + + if ( $this->enabled ) { + $this->init(); + } + } + + /** + * Initialize shipping. + */ + public function init() { + do_action( 'woocommerce_shipping_init' ); + } + + /** + * Shipping methods register themselves by returning their main class name through the woocommerce_shipping_methods filter. + * + * @return array + */ + public function get_shipping_method_class_names() { + // Unique Method ID => Method Class name. + $shipping_methods = array( + 'flat_rate' => 'WC_Shipping_Flat_Rate', + 'free_shipping' => 'WC_Shipping_Free_Shipping', + 'local_pickup' => 'WC_Shipping_Local_Pickup', + ); + + // For backwards compatibility with 2.5.x we load any ENABLED legacy shipping methods here. + $maybe_load_legacy_methods = array( 'flat_rate', 'free_shipping', 'international_delivery', 'local_delivery', 'local_pickup' ); + + foreach ( $maybe_load_legacy_methods as $method ) { + $options = get_option( 'woocommerce_' . $method . '_settings' ); + if ( $options && isset( $options['enabled'] ) && 'yes' === $options['enabled'] ) { + $shipping_methods[ 'legacy_' . $method ] = 'WC_Shipping_Legacy_' . $method; + } + } + + return apply_filters( 'woocommerce_shipping_methods', $shipping_methods ); + } + + /** + * Loads all shipping methods which are hooked in. + * If a $package is passed, some methods may add themselves conditionally and zones will be used. + * + * @param array $package Package information. + * @return WC_Shipping_Method[] + */ + public function load_shipping_methods( $package = array() ) { + if ( ! empty( $package ) ) { + $debug_mode = 'yes' === get_option( 'woocommerce_shipping_debug_mode', 'no' ); + $shipping_zone = WC_Shipping_Zones::get_zone_matching_package( $package ); + $this->shipping_methods = $shipping_zone->get_shipping_methods( true ); + + // translators: %s: shipping zone name. + $matched_zone_notice = sprintf( __( 'Customer matched zone "%s"', 'woocommerce' ), $shipping_zone->get_zone_name() ); + + // Debug output. + if ( $debug_mode && ! Constants::is_defined( 'WOOCOMMERCE_CHECKOUT' ) && ! Constants::is_defined( 'WC_DOING_AJAX' ) && ! wc_has_notice( $matched_zone_notice ) ) { + wc_add_notice( $matched_zone_notice ); + } + } else { + $this->shipping_methods = array(); + } + + // For the settings in the backend, and for non-shipping zone methods, we still need to load any registered classes here. + foreach ( $this->get_shipping_method_class_names() as $method_id => $method_class ) { + $this->register_shipping_method( $method_class ); + } + + // Methods can register themselves manually through this hook if necessary. + do_action( 'woocommerce_load_shipping_methods', $package ); + + // Return loaded methods. + return $this->get_shipping_methods(); + } + + /** + * Register a shipping method. + * + * @param object|string $method Either the name of the method's class, or an instance of the method's class. + * + * @return bool|void + */ + public function register_shipping_method( $method ) { + if ( ! is_object( $method ) ) { + if ( ! class_exists( $method ) ) { + return false; + } + $method = new $method(); + } + if ( is_null( $this->shipping_methods ) ) { + $this->shipping_methods = array(); + } + $this->shipping_methods[ $method->id ] = $method; + } + + /** + * Unregister shipping methods. + */ + public function unregister_shipping_methods() { + $this->shipping_methods = null; + } + + /** + * Returns all registered shipping methods for usage. + * + * @return WC_Shipping_Method[] + */ + public function get_shipping_methods() { + if ( is_null( $this->shipping_methods ) ) { + $this->load_shipping_methods(); + } + return $this->shipping_methods; + } + + /** + * Get an array of shipping classes. + * + * @return array + */ + public function get_shipping_classes() { + if ( empty( $this->shipping_classes ) ) { + $classes = get_terms( + 'product_shipping_class', + array( + 'hide_empty' => '0', + 'orderby' => 'name', + ) + ); + $this->shipping_classes = ! is_wp_error( $classes ) ? $classes : array(); + } + return apply_filters( 'woocommerce_get_shipping_classes', $this->shipping_classes ); + } + + /** + * Calculate shipping for (multiple) packages of cart items. + * + * @param array $packages multi-dimensional array of cart items to calc shipping for. + * @return array Array of calculated packages. + */ + public function calculate_shipping( $packages = array() ) { + $this->packages = array(); + + if ( ! $this->enabled || empty( $packages ) ) { + return array(); + } + + // Calculate costs for passed packages. + foreach ( $packages as $package_key => $package ) { + $this->packages[ $package_key ] = $this->calculate_shipping_for_package( $package, $package_key ); + } + + /** + * Allow packages to be reorganized after calculating the shipping. + * + * This filter can be used to apply some extra manipulation after the shipping costs are calculated for the packages + * but before WooCommerce does anything with them. A good example of usage is to merge the shipping methods for multiple + * packages for marketplaces. + * + * @since 2.6.0 + * + * @param array $packages The array of packages after shipping costs are calculated. + */ + $this->packages = array_filter( (array) apply_filters( 'woocommerce_shipping_packages', $this->packages ) ); + + return $this->packages; + } + + /** + * See if package is shippable. + * + * Packages are shippable until proven otherwise e.g. after getting a shipping country. + * + * @param array $package Package of cart items. + * @return bool + */ + public function is_package_shippable( $package ) { + // Packages are shippable until proven otherwise. + if ( empty( $package['destination']['country'] ) ) { + return true; + } + + $allowed = array_keys( WC()->countries->get_shipping_countries() ); + return in_array( $package['destination']['country'], $allowed, true ); + } + + /** + * Calculate shipping rates for a package, + * + * Calculates each shipping methods cost. Rates are stored in the session based on the package hash to avoid re-calculation every page load. + * + * @param array $package Package of cart items. + * @param int $package_key Index of the package being calculated. Used to cache multiple package rates. + * + * @return array|bool + */ + public function calculate_shipping_for_package( $package = array(), $package_key = 0 ) { + // If shipping is disabled or the package is invalid, return false. + if ( ! $this->enabled || empty( $package ) ) { + return false; + } + + $package['rates'] = array(); + + // If the package is not shippable, e.g. trying to ship to an invalid country, do not calculate rates. + if ( ! $this->is_package_shippable( $package ) ) { + return $package; + } + + // Check if we need to recalculate shipping for this package. + $package_to_hash = $package; + + // Remove data objects so hashes are consistent. + foreach ( $package_to_hash['contents'] as $item_id => $item ) { + unset( $package_to_hash['contents'][ $item_id ]['data'] ); + } + + // Get rates stored in the WC session data for this package. + $wc_session_key = 'shipping_for_package_' . $package_key; + $stored_rates = WC()->session->get( $wc_session_key ); + + // Calculate the hash for this package so we can tell if it's changed since last calculation. + $package_hash = 'wc_ship_' . md5( wp_json_encode( $package_to_hash ) . WC_Cache_Helper::get_transient_version( 'shipping' ) ); + + if ( ! is_array( $stored_rates ) || $package_hash !== $stored_rates['package_hash'] || 'yes' === get_option( 'woocommerce_shipping_debug_mode', 'no' ) ) { + foreach ( $this->load_shipping_methods( $package ) as $shipping_method ) { + if ( ! $shipping_method->supports( 'shipping-zones' ) || $shipping_method->get_instance_id() ) { + /** + * Fires before getting shipping rates for a package. + * + * @since 4.3.0 + * @param array $package Package of cart items. + * @param WC_Shipping_Method $shipping_method Shipping method instance. + */ + do_action( 'woocommerce_before_get_rates_for_package', $package, $shipping_method ); + + // Use + instead of array_merge to maintain numeric keys. + $package['rates'] = $package['rates'] + $shipping_method->get_rates_for_package( $package ); + + /** + * Fires after getting shipping rates for a package. + * + * @since 4.3.0 + * @param array $package Package of cart items. + * @param WC_Shipping_Method $shipping_method Shipping method instance. + */ + do_action( 'woocommerce_after_get_rates_for_package', $package, $shipping_method ); + } + } + + /** + * Filter the calculated shipping rates. + * + * @see https://gist.github.com/woogists/271654709e1d27648546e83253c1a813 for cache invalidation methods. + * @param array $package['rates'] Package rates. + * @param array $package Package of cart items. + */ + $package['rates'] = apply_filters( 'woocommerce_package_rates', $package['rates'], $package ); + + // Store in session to avoid recalculation. + WC()->session->set( + $wc_session_key, + array( + 'package_hash' => $package_hash, + 'rates' => $package['rates'], + ) + ); + } else { + $package['rates'] = $stored_rates['rates']; + } + + return $package; + } + + /** + * Get packages. + * + * @return array + */ + public function get_packages() { + return $this->packages; + } + + /** + * Reset shipping. + * + * Reset the totals for shipping as a whole. + */ + public function reset_shipping() { + unset( WC()->session->chosen_shipping_methods ); + $this->packages = array(); + } + + /** + * Deprecated + * + * @deprecated 2.6.0 Was previously used to determine sort order of methods, but this is now controlled by zones and thus unused. + */ + public function sort_shipping_methods() { + wc_deprecated_function( 'sort_shipping_methods', '2.6' ); + return $this->shipping_methods; + } +} diff --git a/includes/class-wc-shortcodes.php b/plugins/woocommerce/includes/class-wc-shortcodes.php similarity index 100% rename from includes/class-wc-shortcodes.php rename to plugins/woocommerce/includes/class-wc-shortcodes.php diff --git a/plugins/woocommerce/includes/class-wc-structured-data.php b/plugins/woocommerce/includes/class-wc-structured-data.php new file mode 100644 index 00000000000..1d3778864e7 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-structured-data.php @@ -0,0 +1,538 @@ +_data ) ) { + unset( $this->_data ); + } + + $this->_data[] = $data; + + return true; + } + + /** + * Gets data. + * + * @return array + */ + public function get_data() { + return $this->_data; + } + + /** + * Structures and returns data. + * + * List of types available by default for specific request: + * + * 'product', + * 'review', + * 'breadcrumblist', + * 'website', + * 'order', + * + * @param array $types Structured data types. + * @return array + */ + public function get_structured_data( $types ) { + $data = array(); + + // Put together the values of same type of structured data. + foreach ( $this->get_data() as $value ) { + $data[ strtolower( $value['@type'] ) ][] = $value; + } + + // Wrap the multiple values of each type inside a graph... Then add context to each type. + foreach ( $data as $type => $value ) { + $data[ $type ] = count( $value ) > 1 ? array( '@graph' => $value ) : $value[0]; + $data[ $type ] = apply_filters( 'woocommerce_structured_data_context', array( '@context' => 'https://schema.org/' ), $data, $type, $value ) + $data[ $type ]; + } + + // If requested types, pick them up... Finally change the associative array to an indexed one. + $data = $types ? array_values( array_intersect_key( $data, array_flip( $types ) ) ) : array_values( $data ); + + if ( ! empty( $data ) ) { + if ( 1 < count( $data ) ) { + $data = apply_filters( 'woocommerce_structured_data_context', array( '@context' => 'https://schema.org/' ), $data, '', '' ) + array( '@graph' => $data ); + } else { + $data = $data[0]; + } + } + + return $data; + } + + /** + * Get data types for pages. + * + * @return array + */ + protected function get_data_type_for_page() { + $types = array(); + $types[] = is_shop() || is_product_category() || is_product() ? 'product' : ''; + $types[] = is_shop() && is_front_page() ? 'website' : ''; + $types[] = is_product() ? 'review' : ''; + $types[] = 'breadcrumblist'; + $types[] = 'order'; + + return array_filter( apply_filters( 'woocommerce_structured_data_type_for_page', $types ) ); + } + + /** + * Makes sure email structured data only outputs on non-plain text versions. + * + * @param WP_Order $order Order data. + * @param bool $sent_to_admin Send to admin (default: false). + * @param bool $plain_text Plain text email (default: false). + */ + public function output_email_structured_data( $order, $sent_to_admin = false, $plain_text = false ) { + if ( $plain_text ) { + return; + } + echo '
    '; + $this->output_structured_data(); + echo '
    '; + } + + /** + * Sanitizes, encodes and outputs structured data. + * + * Hooked into `wp_footer` action hook. + * Hooked into `woocommerce_email_order_details` action hook. + */ + public function output_structured_data() { + $types = $this->get_data_type_for_page(); + $data = $this->get_structured_data( $types ); + + if ( $data ) { + echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } + + /* + |-------------------------------------------------------------------------- + | Generators + |-------------------------------------------------------------------------- + | + | Methods for generating specific structured data types: + | + | - Product + | - Review + | - BreadcrumbList + | - WebSite + | - Order + | + | The generated data is stored into `$this->_data`. + | See the methods above for handling `$this->_data`. + | + */ + + /** + * Generates Product structured data. + * + * Hooked into `woocommerce_single_product_summary` action hook. + * + * @param WC_Product $product Product data (default: null). + */ + public function generate_product_data( $product = null ) { + if ( ! is_object( $product ) ) { + global $product; + } + + if ( ! is_a( $product, 'WC_Product' ) ) { + return; + } + + $shop_name = get_bloginfo( 'name' ); + $shop_url = home_url(); + $currency = get_woocommerce_currency(); + $permalink = get_permalink( $product->get_id() ); + $image = wp_get_attachment_url( $product->get_image_id() ); + + $markup = array( + '@type' => 'Product', + '@id' => $permalink . '#product', // Append '#product' to differentiate between this @id and the @id generated for the Breadcrumblist. + 'name' => wp_kses_post( $product->get_name() ), + 'url' => $permalink, + 'description' => wp_strip_all_tags( do_shortcode( $product->get_short_description() ? $product->get_short_description() : $product->get_description() ) ), + ); + + if ( $image ) { + $markup['image'] = $image; + } + + // Declare SKU or fallback to ID. + if ( $product->get_sku() ) { + $markup['sku'] = $product->get_sku(); + } else { + $markup['sku'] = $product->get_id(); + } + + if ( '' !== $product->get_price() ) { + // Assume prices will be valid until the end of next year, unless on sale and there is an end date. + $price_valid_until = gmdate( 'Y-12-31', time() + YEAR_IN_SECONDS ); + + if ( $product->is_type( 'variable' ) ) { + $lowest = $product->get_variation_price( 'min', false ); + $highest = $product->get_variation_price( 'max', false ); + + if ( $lowest === $highest ) { + $markup_offer = array( + '@type' => 'Offer', + 'price' => wc_format_decimal( $lowest, wc_get_price_decimals() ), + 'priceValidUntil' => $price_valid_until, + 'priceSpecification' => array( + 'price' => wc_format_decimal( $lowest, wc_get_price_decimals() ), + 'priceCurrency' => $currency, + 'valueAddedTaxIncluded' => wc_prices_include_tax() ? 'true' : 'false', + ), + ); + } else { + $markup_offer = array( + '@type' => 'AggregateOffer', + 'lowPrice' => wc_format_decimal( $lowest, wc_get_price_decimals() ), + 'highPrice' => wc_format_decimal( $highest, wc_get_price_decimals() ), + 'offerCount' => count( $product->get_children() ), + ); + } + } else { + if ( $product->is_on_sale() && $product->get_date_on_sale_to() ) { + $price_valid_until = gmdate( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ); + } + $markup_offer = array( + '@type' => 'Offer', + 'price' => wc_format_decimal( $product->get_price(), wc_get_price_decimals() ), + 'priceValidUntil' => $price_valid_until, + 'priceSpecification' => array( + 'price' => wc_format_decimal( $product->get_price(), wc_get_price_decimals() ), + 'priceCurrency' => $currency, + 'valueAddedTaxIncluded' => wc_prices_include_tax() ? 'true' : 'false', + ), + ); + } + + $markup_offer += array( + 'priceCurrency' => $currency, + 'availability' => 'http://schema.org/' . ( $product->is_in_stock() ? 'InStock' : 'OutOfStock' ), + 'url' => $permalink, + 'seller' => array( + '@type' => 'Organization', + 'name' => $shop_name, + 'url' => $shop_url, + ), + ); + + $markup['offers'] = array( apply_filters( 'woocommerce_structured_data_product_offer', $markup_offer, $product ) ); + } + + if ( $product->get_rating_count() && wc_review_ratings_enabled() ) { + $markup['aggregateRating'] = array( + '@type' => 'AggregateRating', + 'ratingValue' => $product->get_average_rating(), + 'reviewCount' => $product->get_review_count(), + ); + + // Markup 5 most recent rating/review. + $comments = get_comments( + array( + 'number' => 5, + 'post_id' => $product->get_id(), + 'status' => 'approve', + 'post_status' => 'publish', + 'post_type' => 'product', + 'parent' => 0, + 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + array( + 'key' => 'rating', + 'type' => 'NUMERIC', + 'compare' => '>', + 'value' => 0, + ), + ), + ) + ); + + if ( $comments ) { + $markup['review'] = array(); + foreach ( $comments as $comment ) { + $markup['review'][] = array( + '@type' => 'Review', + 'reviewRating' => array( + '@type' => 'Rating', + 'bestRating' => '5', + 'ratingValue' => get_comment_meta( $comment->comment_ID, 'rating', true ), + 'worstRating' => '1', + ), + 'author' => array( + '@type' => 'Person', + 'name' => get_comment_author( $comment ), + ), + 'reviewBody' => get_comment_text( $comment ), + 'datePublished' => get_comment_date( 'c', $comment ), + ); + } + } + } + + // Check we have required data. + if ( empty( $markup['aggregateRating'] ) && empty( $markup['offers'] ) && empty( $markup['review'] ) ) { + return; + } + + $this->set_data( apply_filters( 'woocommerce_structured_data_product', $markup, $product ) ); + } + + /** + * Generates Review structured data. + * + * Hooked into `woocommerce_review_meta` action hook. + * + * @param WP_Comment $comment Comment data. + */ + public function generate_review_data( $comment ) { + $markup = array(); + $markup['@type'] = 'Review'; + $markup['@id'] = get_comment_link( $comment->comment_ID ); + $markup['datePublished'] = get_comment_date( 'c', $comment->comment_ID ); + $markup['description'] = get_comment_text( $comment->comment_ID ); + $markup['itemReviewed'] = array( + '@type' => 'Product', + 'name' => get_the_title( $comment->comment_post_ID ), + ); + + // Skip replies unless they have a rating. + $rating = get_comment_meta( $comment->comment_ID, 'rating', true ); + + if ( $rating ) { + $markup['reviewRating'] = array( + '@type' => 'Rating', + 'bestRating' => '5', + 'ratingValue' => $rating, + 'worstRating' => '1', + ); + } elseif ( $comment->comment_parent ) { + return; + } + + $markup['author'] = array( + '@type' => 'Person', + 'name' => get_comment_author( $comment->comment_ID ), + ); + + $this->set_data( apply_filters( 'woocommerce_structured_data_review', $markup, $comment ) ); + } + + /** + * Generates BreadcrumbList structured data. + * + * Hooked into `woocommerce_breadcrumb` action hook. + * + * @param WC_Breadcrumb $breadcrumbs Breadcrumb data. + */ + public function generate_breadcrumblist_data( $breadcrumbs ) { + $crumbs = $breadcrumbs->get_breadcrumb(); + + if ( empty( $crumbs ) || ! is_array( $crumbs ) ) { + return; + } + + $markup = array(); + $markup['@type'] = 'BreadcrumbList'; + $markup['itemListElement'] = array(); + + foreach ( $crumbs as $key => $crumb ) { + $markup['itemListElement'][ $key ] = array( + '@type' => 'ListItem', + 'position' => $key + 1, + 'item' => array( + 'name' => $crumb[0], + ), + ); + + if ( ! empty( $crumb[1] ) ) { + $markup['itemListElement'][ $key ]['item'] += array( '@id' => $crumb[1] ); + } elseif ( isset( $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'] ) ) { + $current_url = set_url_scheme( 'http://' . wp_unslash( $_SERVER['HTTP_HOST'] ) . wp_unslash( $_SERVER['REQUEST_URI'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + $markup['itemListElement'][ $key ]['item'] += array( '@id' => $current_url ); + } + } + + $this->set_data( apply_filters( 'woocommerce_structured_data_breadcrumblist', $markup, $breadcrumbs ) ); + } + + /** + * Generates WebSite structured data. + * + * Hooked into `woocommerce_before_main_content` action hook. + */ + public function generate_website_data() { + $markup = array(); + $markup['@type'] = 'WebSite'; + $markup['name'] = get_bloginfo( 'name' ); + $markup['url'] = home_url(); + $markup['potentialAction'] = array( + '@type' => 'SearchAction', + 'target' => home_url( '?s={search_term_string}&post_type=product' ), + 'query-input' => 'required name=search_term_string', + ); + + $this->set_data( apply_filters( 'woocommerce_structured_data_website', $markup ) ); + } + + /** + * Generates Order structured data. + * + * Hooked into `woocommerce_email_order_details` action hook. + * + * @param WP_Order $order Order data. + * @param bool $sent_to_admin Send to admin (default: false). + * @param bool $plain_text Plain text email (default: false). + */ + public function generate_order_data( $order, $sent_to_admin = false, $plain_text = false ) { + if ( $plain_text || ! is_a( $order, 'WC_Order' ) ) { + return; + } + + $shop_name = get_bloginfo( 'name' ); + $shop_url = home_url(); + $order_url = $sent_to_admin ? $order->get_edit_order_url() : $order->get_view_order_url(); + $order_statuses = array( + 'pending' => 'https://schema.org/OrderPaymentDue', + 'processing' => 'https://schema.org/OrderProcessing', + 'on-hold' => 'https://schema.org/OrderProblem', + 'completed' => 'https://schema.org/OrderDelivered', + 'cancelled' => 'https://schema.org/OrderCancelled', + 'refunded' => 'https://schema.org/OrderReturned', + 'failed' => 'https://schema.org/OrderProblem', + ); + + $markup_offers = array(); + foreach ( $order->get_items() as $item ) { + if ( ! apply_filters( 'woocommerce_order_item_visible', true, $item ) ) { + continue; + } + + $product = $item->get_product(); + $product_exists = is_object( $product ); + $is_visible = $product_exists && $product->is_visible(); + + $markup_offers[] = array( + '@type' => 'Offer', + 'price' => $order->get_line_subtotal( $item ), + 'priceCurrency' => $order->get_currency(), + 'priceSpecification' => array( + 'price' => $order->get_line_subtotal( $item ), + 'priceCurrency' => $order->get_currency(), + 'eligibleQuantity' => array( + '@type' => 'QuantitativeValue', + 'value' => apply_filters( 'woocommerce_email_order_item_quantity', $item->get_quantity(), $item ), + ), + ), + 'itemOffered' => array( + '@type' => 'Product', + 'name' => wp_kses_post( apply_filters( 'woocommerce_order_item_name', $item->get_name(), $item, $is_visible ) ), + 'sku' => $product_exists ? $product->get_sku() : '', + 'image' => $product_exists ? wp_get_attachment_image_url( $product->get_image_id() ) : '', + 'url' => $is_visible ? get_permalink( $product->get_id() ) : get_home_url(), + ), + 'seller' => array( + '@type' => 'Organization', + 'name' => $shop_name, + 'url' => $shop_url, + ), + ); + } + + $markup = array(); + $markup['@type'] = 'Order'; + $markup['url'] = $order_url; + $markup['orderStatus'] = isset( $order_statuses[ $order->get_status() ] ) ? $order_statuses[ $order->get_status() ] : ''; + $markup['orderNumber'] = $order->get_order_number(); + $markup['orderDate'] = $order->get_date_created()->format( 'c' ); + $markup['acceptedOffer'] = $markup_offers; + $markup['discount'] = $order->get_total_discount(); + $markup['discountCurrency'] = $order->get_currency(); + $markup['price'] = $order->get_total(); + $markup['priceCurrency'] = $order->get_currency(); + $markup['priceSpecification'] = array( + 'price' => $order->get_total(), + 'priceCurrency' => $order->get_currency(), + 'valueAddedTaxIncluded' => 'true', + ); + $markup['billingAddress'] = array( + '@type' => 'PostalAddress', + 'name' => $order->get_formatted_billing_full_name(), + 'streetAddress' => $order->get_billing_address_1(), + 'postalCode' => $order->get_billing_postcode(), + 'addressLocality' => $order->get_billing_city(), + 'addressRegion' => $order->get_billing_state(), + 'addressCountry' => $order->get_billing_country(), + 'email' => $order->get_billing_email(), + 'telephone' => $order->get_billing_phone(), + ); + $markup['customer'] = array( + '@type' => 'Person', + 'name' => $order->get_formatted_billing_full_name(), + ); + $markup['merchant'] = array( + '@type' => 'Organization', + 'name' => $shop_name, + 'url' => $shop_url, + ); + $markup['potentialAction'] = array( + '@type' => 'ViewAction', + 'name' => 'View Order', + 'url' => $order_url, + 'target' => $order_url, + ); + + $this->set_data( apply_filters( 'woocommerce_structured_data_order', $markup, $sent_to_admin, $order ), true ); + } +} diff --git a/plugins/woocommerce/includes/class-wc-tax.php b/plugins/woocommerce/includes/class-wc-tax.php new file mode 100644 index 00000000000..9cb4c67037a --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-tax.php @@ -0,0 +1,1248 @@ + $rate ) { + $taxes[ $key ] = 0; + + if ( 'yes' === $rate['compound'] ) { + $compound_rates[ $key ] = $rate['rate']; + } else { + $regular_rates[ $key ] = $rate['rate']; + } + } + + $compound_rates = array_reverse( $compound_rates, true ); // Working backwards. + + $non_compound_price = $price; + + foreach ( $compound_rates as $key => $compound_rate ) { + $tax_amount = apply_filters( 'woocommerce_price_inc_tax_amount', $non_compound_price - ( $non_compound_price / ( 1 + ( $compound_rate / 100 ) ) ), $key, $rates[ $key ], $price ); + $taxes[ $key ] += $tax_amount; + $non_compound_price = $non_compound_price - $tax_amount; + } + + // Regular taxes. + $regular_tax_rate = 1 + ( array_sum( $regular_rates ) / 100 ); + + foreach ( $regular_rates as $key => $regular_rate ) { + $the_rate = ( $regular_rate / 100 ) / $regular_tax_rate; + $net_price = $price - ( $the_rate * $non_compound_price ); + $tax_amount = apply_filters( 'woocommerce_price_inc_tax_amount', $price - $net_price, $key, $rates[ $key ], $price ); + $taxes[ $key ] += $tax_amount; + } + + /** + * Round all taxes to precision (4DP) before passing them back. Note, this is not the same rounding + * as in the cart calculation class which, depending on settings, will round to 2DP when calculating + * final totals. Also unlike that class, this rounds .5 up for all cases. + */ + $taxes = array_map( array( __CLASS__, 'round' ), $taxes ); + + return $taxes; + } + + /** + * Calc tax from exclusive price. + * + * @param float $price Price to calculate tax for. + * @param array $rates Array of tax rates. + * @return array + */ + public static function calc_exclusive_tax( $price, $rates ) { + $taxes = array(); + + if ( ! empty( $rates ) ) { + foreach ( $rates as $key => $rate ) { + if ( 'yes' === $rate['compound'] ) { + continue; + } + + $tax_amount = $price * ( $rate['rate'] / 100 ); + $tax_amount = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price ); // ADVANCED: Allow third parties to modify this rate. + + if ( ! isset( $taxes[ $key ] ) ) { + $taxes[ $key ] = $tax_amount; + } else { + $taxes[ $key ] += $tax_amount; + } + } + + $pre_compound_total = array_sum( $taxes ); + + // Compound taxes. + foreach ( $rates as $key => $rate ) { + if ( 'no' === $rate['compound'] ) { + continue; + } + $the_price_inc_tax = $price + ( $pre_compound_total ); + $tax_amount = $the_price_inc_tax * ( $rate['rate'] / 100 ); + $tax_amount = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price, $the_price_inc_tax, $pre_compound_total ); // ADVANCED: Allow third parties to modify this rate. + + if ( ! isset( $taxes[ $key ] ) ) { + $taxes[ $key ] = $tax_amount; + } else { + $taxes[ $key ] += $tax_amount; + } + + $pre_compound_total = array_sum( $taxes ); + } + } + + /** + * Round all taxes to precision (4DP) before passing them back. Note, this is not the same rounding + * as in the cart calculation class which, depending on settings, will round to 2DP when calculating + * final totals. Also unlike that class, this rounds .5 up for all cases. + */ + $taxes = array_map( array( __CLASS__, 'round' ), $taxes ); + + return $taxes; + } + + /** + * Searches for all matching country/state/postcode tax rates. + * + * @param array $args Args that determine the rate to find. + * @return array + */ + public static function find_rates( $args = array() ) { + $args = wp_parse_args( + $args, + array( + 'country' => '', + 'state' => '', + 'city' => '', + 'postcode' => '', + 'tax_class' => '', + ) + ); + + $country = $args['country']; + $state = $args['state']; + $city = $args['city']; + $postcode = wc_normalize_postcode( wc_clean( $args['postcode'] ) ); + $tax_class = $args['tax_class']; + + if ( ! $country ) { + return array(); + } + + $cache_key = WC_Cache_Helper::get_cache_prefix( 'taxes' ) . 'wc_tax_rates_' . md5( sprintf( '%s+%s+%s+%s+%s', $country, $state, $city, $postcode, $tax_class ) ); + $matched_tax_rates = wp_cache_get( $cache_key, 'taxes' ); + + if ( false === $matched_tax_rates ) { + $matched_tax_rates = self::get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class ); + wp_cache_set( $cache_key, $matched_tax_rates, 'taxes' ); + } + + return apply_filters( 'woocommerce_find_rates', $matched_tax_rates, $args ); + } + + /** + * Searches for all matching country/state/postcode tax rates. + * + * @param array $args Args that determine the rate to find. + * @return array + */ + public static function find_shipping_rates( $args = array() ) { + $rates = self::find_rates( $args ); + $shipping_rates = array(); + + if ( is_array( $rates ) ) { + foreach ( $rates as $key => $rate ) { + if ( 'yes' === $rate['shipping'] ) { + $shipping_rates[ $key ] = $rate; + } + } + } + + return $shipping_rates; + } + + /** + * Does the sort comparison. Compares (in this order): + * - Priority + * - Country + * - State + * - Number of postcodes + * - Number of cities + * - ID + * + * @param object $rate1 First rate to compare. + * @param object $rate2 Second rate to compare. + * @return int + */ + private static function sort_rates_callback( $rate1, $rate2 ) { + if ( $rate1->tax_rate_priority !== $rate2->tax_rate_priority ) { + return $rate1->tax_rate_priority < $rate2->tax_rate_priority ? -1 : 1; // ASC. + } + + if ( $rate1->tax_rate_country !== $rate2->tax_rate_country ) { + if ( '' === $rate1->tax_rate_country ) { + return 1; + } + if ( '' === $rate2->tax_rate_country ) { + return -1; + } + return strcmp( $rate1->tax_rate_country, $rate2->tax_rate_country ) > 0 ? 1 : -1; + } + + if ( $rate1->tax_rate_state !== $rate2->tax_rate_state ) { + if ( '' === $rate1->tax_rate_state ) { + return 1; + } + if ( '' === $rate2->tax_rate_state ) { + return -1; + } + return strcmp( $rate1->tax_rate_state, $rate2->tax_rate_state ) > 0 ? 1 : -1; + } + + if ( isset( $rate1->postcode_count, $rate2->postcode_count ) && $rate1->postcode_count !== $rate2->postcode_count ) { + return $rate1->postcode_count < $rate2->postcode_count ? 1 : -1; + } + + if ( isset( $rate1->city_count, $rate2->city_count ) && $rate1->city_count !== $rate2->city_count ) { + return $rate1->city_count < $rate2->city_count ? 1 : -1; + } + + return $rate1->tax_rate_id < $rate2->tax_rate_id ? -1 : 1; + } + + /** + * Logical sort order for tax rates based on the following in order of priority. + * + * @param array $rates Rates to be sorted. + * @return array + */ + private static function sort_rates( $rates ) { + uasort( $rates, __CLASS__ . '::sort_rates_callback' ); + $i = 0; + foreach ( $rates as $key => $rate ) { + $rates[ $key ]->tax_rate_order = $i++; + } + return $rates; + } + + /** + * Loop through a set of tax rates and get the matching rates (1 per priority). + * + * @param string $country Country code to match against. + * @param string $state State code to match against. + * @param string $postcode Postcode to match against. + * @param string $city City to match against. + * @param string $tax_class Tax class to match against. + * @return array + */ + private static function get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class ) { + global $wpdb; + + // Query criteria - these will be ANDed. + $criteria = array(); + $criteria[] = $wpdb->prepare( "tax_rate_country IN ( %s, '' )", strtoupper( $country ) ); + $criteria[] = $wpdb->prepare( "tax_rate_state IN ( %s, '' )", strtoupper( $state ) ); + $criteria[] = $wpdb->prepare( 'tax_rate_class = %s', sanitize_title( $tax_class ) ); + + // Pre-query postcode ranges for PHP based matching. + $postcode_search = wc_get_wildcard_postcodes( $postcode, $country ); + $postcode_ranges = $wpdb->get_results( "SELECT tax_rate_id, location_code FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE location_type = 'postcode' AND location_code LIKE '%...%';" ); + + if ( $postcode_ranges ) { + $matches = wc_postcode_location_matcher( $postcode, $postcode_ranges, 'tax_rate_id', 'location_code', $country ); + if ( ! empty( $matches ) ) { + foreach ( $matches as $matched_postcodes ) { + $postcode_search = array_merge( $postcode_search, $matched_postcodes ); + } + } + } + + $postcode_search = array_unique( $postcode_search ); + + /** + * Location matching criteria - ORed + * Needs to match: + * - rates with no postcodes and cities + * - rates with a matching postcode and city + * - rates with matching postcode, no city + * - rates with matching city, no postcode + */ + $locations_criteria = array(); + $locations_criteria[] = 'locations.location_type IS NULL'; + $locations_criteria[] = " + locations.location_type = 'postcode' AND locations.location_code IN ('" . implode( "','", array_map( 'esc_sql', $postcode_search ) ) . "') + AND ( + ( locations2.location_type = 'city' AND locations2.location_code = '" . esc_sql( strtoupper( $city ) ) . "' ) + OR NOT EXISTS ( + SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub + WHERE sub.location_type = 'city' + AND sub.tax_rate_id = tax_rates.tax_rate_id + ) + ) + "; + $locations_criteria[] = " + locations.location_type = 'city' AND locations.location_code = '" . esc_sql( strtoupper( $city ) ) . "' + AND NOT EXISTS ( + SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub + WHERE sub.location_type = 'postcode' + AND sub.tax_rate_id = tax_rates.tax_rate_id + ) + "; + + $criteria[] = '( ( ' . implode( ' ) OR ( ', $locations_criteria ) . ' ) )'; + + $criteria_string = implode( ' AND ', $criteria ); + + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $found_rates = $wpdb->get_results( + " + SELECT tax_rates.*, COUNT( locations.location_id ) as postcode_count, COUNT( locations2.location_id ) as city_count + FROM {$wpdb->prefix}woocommerce_tax_rates as tax_rates + LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations ON tax_rates.tax_rate_id = locations.tax_rate_id + LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations2 ON tax_rates.tax_rate_id = locations2.tax_rate_id + WHERE 1=1 AND {$criteria_string} + GROUP BY tax_rates.tax_rate_id + ORDER BY tax_rates.tax_rate_priority + " + ); + // phpcs:enable + + $found_rates = self::sort_rates( $found_rates ); + $matched_tax_rates = array(); + $found_priority = array(); + + foreach ( $found_rates as $found_rate ) { + if ( in_array( $found_rate->tax_rate_priority, $found_priority, true ) ) { + continue; + } + + $matched_tax_rates[ $found_rate->tax_rate_id ] = array( + 'rate' => (float) $found_rate->tax_rate, + 'label' => $found_rate->tax_rate_name, + 'shipping' => $found_rate->tax_rate_shipping ? 'yes' : 'no', + 'compound' => $found_rate->tax_rate_compound ? 'yes' : 'no', + ); + + $found_priority[] = $found_rate->tax_rate_priority; + } + + return apply_filters( 'woocommerce_matched_tax_rates', $matched_tax_rates, $country, $state, $postcode, $city, $tax_class ); + } + + /** + * Get the customer tax location based on their status and the current page. + * + * Used by get_rates(), get_shipping_rates(). + * + * @param string $tax_class string Optional, passed to the filter for advanced tax setups. + * @param object $customer Override the customer object to get their location. + * @return array + */ + public static function get_tax_location( $tax_class = '', $customer = null ) { + $location = array(); + + if ( is_null( $customer ) && WC()->customer ) { + $customer = WC()->customer; + } + + if ( ! empty( $customer ) ) { + $location = $customer->get_taxable_address(); + } elseif ( wc_prices_include_tax() || 'base' === get_option( 'woocommerce_default_customer_address' ) || 'base' === get_option( 'woocommerce_tax_based_on' ) ) { + $location = array( + WC()->countries->get_base_country(), + WC()->countries->get_base_state(), + WC()->countries->get_base_postcode(), + WC()->countries->get_base_city(), + ); + } + + return apply_filters( 'woocommerce_get_tax_location', $location, $tax_class, $customer ); + } + + /** + * Get's an array of matching rates for a tax class. + * + * @param string $tax_class Tax class to get rates for. + * @param object $customer Override the customer object to get their location. + * @return array + */ + public static function get_rates( $tax_class = '', $customer = null ) { + $tax_class = sanitize_title( $tax_class ); + $location = self::get_tax_location( $tax_class, $customer ); + return self::get_rates_from_location( $tax_class, $location, $customer ); + } + + /** + * Get's an array of matching rates from location and tax class. $customer parameter is used to preserve backward compatibility for filter. + * + * @param string $tax_class Tax class to get rates for. + * @param array $location Location to compute rates for. Should be in form: array( country, state, postcode, city). + * @param object $customer Only used to maintain backward compatibility for filter `woocommerce-matched_rates`. + * + * @return mixed|void Tax rates. + */ + public static function get_rates_from_location( $tax_class, $location, $customer = null ) { + $tax_class = sanitize_title( $tax_class ); + $matched_tax_rates = array(); + + if ( count( $location ) === 4 ) { + list( $country, $state, $postcode, $city ) = $location; + + $matched_tax_rates = self::find_rates( + array( + 'country' => $country, + 'state' => $state, + 'postcode' => $postcode, + 'city' => $city, + 'tax_class' => $tax_class, + ) + ); + } + + return apply_filters( 'woocommerce_matched_rates', $matched_tax_rates, $tax_class, $customer ); + } + + /** + * Get's an array of matching rates for the shop's base country. + * + * @param string $tax_class Tax Class. + * @return array + */ + public static function get_base_tax_rates( $tax_class = '' ) { + return apply_filters( + 'woocommerce_base_tax_rates', + self::find_rates( + array( + 'country' => WC()->countries->get_base_country(), + 'state' => WC()->countries->get_base_state(), + 'postcode' => WC()->countries->get_base_postcode(), + 'city' => WC()->countries->get_base_city(), + 'tax_class' => $tax_class, + ) + ), + $tax_class + ); + } + + /** + * Alias for get_base_tax_rates(). + * + * @deprecated 2.3 + * @param string $tax_class Tax Class. + * @return array + */ + public static function get_shop_base_rate( $tax_class = '' ) { + return self::get_base_tax_rates( $tax_class ); + } + + /** + * Gets an array of matching shipping tax rates for a given class. + * + * @param string $tax_class Tax class to get rates for. + * @param object $customer Override the customer object to get their location. + * @return mixed + */ + public static function get_shipping_tax_rates( $tax_class = null, $customer = null ) { + // See if we have an explicitly set shipping tax class. + $shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' ); + + if ( 'inherit' !== $shipping_tax_class ) { + $tax_class = $shipping_tax_class; + } + + $location = self::get_tax_location( $tax_class, $customer ); + $matched_tax_rates = array(); + + if ( 4 === count( $location ) ) { + list( $country, $state, $postcode, $city ) = $location; + + if ( ! is_null( $tax_class ) ) { + // This will be per item shipping. + $matched_tax_rates = self::find_shipping_rates( + array( + 'country' => $country, + 'state' => $state, + 'postcode' => $postcode, + 'city' => $city, + 'tax_class' => $tax_class, + ) + ); + + } elseif ( WC()->cart->get_cart() ) { + + // This will be per order shipping - loop through the order and find the highest tax class rate. + $cart_tax_classes = WC()->cart->get_cart_item_tax_classes_for_shipping(); + + // No tax classes = no taxable items. + if ( empty( $cart_tax_classes ) ) { + return array(); + } + + // If multiple classes are found, use the first one found unless a standard rate item is found. This will be the first listed in the 'additional tax class' section. + if ( count( $cart_tax_classes ) > 1 && ! in_array( '', $cart_tax_classes, true ) ) { + $tax_classes = self::get_tax_class_slugs(); + + foreach ( $tax_classes as $tax_class ) { + if ( in_array( $tax_class, $cart_tax_classes, true ) ) { + $matched_tax_rates = self::find_shipping_rates( + array( + 'country' => $country, + 'state' => $state, + 'postcode' => $postcode, + 'city' => $city, + 'tax_class' => $tax_class, + ) + ); + break; + } + } + } elseif ( 1 === count( $cart_tax_classes ) ) { + // If a single tax class is found, use it. + $matched_tax_rates = self::find_shipping_rates( + array( + 'country' => $country, + 'state' => $state, + 'postcode' => $postcode, + 'city' => $city, + 'tax_class' => $cart_tax_classes[0], + ) + ); + } + } + + // Get standard rate if no taxes were found. + if ( ! count( $matched_tax_rates ) ) { + $matched_tax_rates = self::find_shipping_rates( + array( + 'country' => $country, + 'state' => $state, + 'postcode' => $postcode, + 'city' => $city, + ) + ); + } + } + + return $matched_tax_rates; + } + + /** + * Return true/false depending on if a rate is a compound rate. + * + * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format. + * @return bool + */ + public static function is_compound( $key_or_rate ) { + global $wpdb; + + if ( is_object( $key_or_rate ) ) { + $key = $key_or_rate->tax_rate_id; + $compound = $key_or_rate->tax_rate_compound; + } else { + $key = $key_or_rate; + $compound = (bool) $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_compound FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); + } + + return (bool) apply_filters( 'woocommerce_rate_compound', $compound, $key ); + } + + /** + * Return a given rates label. + * + * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format. + * @return string + */ + public static function get_rate_label( $key_or_rate ) { + global $wpdb; + + if ( is_object( $key_or_rate ) ) { + $key = $key_or_rate->tax_rate_id; + $rate_name = $key_or_rate->tax_rate_name; + } else { + $key = $key_or_rate; + $rate_name = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_name FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); + } + + if ( ! $rate_name ) { + $rate_name = WC()->countries->tax_or_vat(); + } + + return apply_filters( 'woocommerce_rate_label', $rate_name, $key ); + } + + /** + * Return a given rates percent. + * + * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format. + * @return string + */ + public static function get_rate_percent( $key_or_rate ) { + $rate_percent_value = self::get_rate_percent_value( $key_or_rate ); + $tax_rate_id = is_object( $key_or_rate ) ? $key_or_rate->tax_rate_id : $key_or_rate; + return apply_filters( 'woocommerce_rate_percent', $rate_percent_value . '%', $tax_rate_id ); + } + + /** + * Return a given rates percent. + * + * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format. + * @return float + */ + public static function get_rate_percent_value( $key_or_rate ) { + global $wpdb; + + if ( is_object( $key_or_rate ) ) { + $tax_rate = $key_or_rate->tax_rate; + } else { + $key = $key_or_rate; + $tax_rate = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); + } + + return floatval( $tax_rate ); + } + + + /** + * Get a rates code. Code is made up of COUNTRY-STATE-NAME-Priority. E.g GB-VAT-1, US-AL-TAX-1. + * + * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format. + * @return string + */ + public static function get_rate_code( $key_or_rate ) { + global $wpdb; + + if ( is_object( $key_or_rate ) ) { + $key = $key_or_rate->tax_rate_id; + $rate = $key_or_rate; + } else { + $key = $key_or_rate; + $rate = $wpdb->get_row( $wpdb->prepare( "SELECT tax_rate_country, tax_rate_state, tax_rate_name, tax_rate_priority FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); + } + + $code_string = ''; + + if ( null !== $rate ) { + $code = array(); + $code[] = $rate->tax_rate_country; + $code[] = $rate->tax_rate_state; + $code[] = $rate->tax_rate_name ? $rate->tax_rate_name : 'TAX'; + $code[] = absint( $rate->tax_rate_priority ); + $code_string = strtoupper( implode( '-', array_filter( $code ) ) ); + } + + return apply_filters( 'woocommerce_rate_code', $code_string, $key ); + } + + /** + * Sums a set of taxes to form a single total. Values are pre-rounded to precision from 3.6.0. + * + * @param array $taxes Array of taxes. + * @return float + */ + public static function get_tax_total( $taxes ) { + return array_sum( $taxes ); + } + + /** + * Gets all tax rate classes from the database. + * + * @since 3.7.0 + * @return array Array of tax class objects consisting of tax_rate_class_id, name, and slug. + */ + public static function get_tax_rate_classes() { + global $wpdb; + + $cache_key = 'tax-rate-classes'; + $tax_rate_classes = wp_cache_get( $cache_key, 'taxes' ); + + if ( ! is_array( $tax_rate_classes ) ) { + $tax_rate_classes = $wpdb->get_results( + " + SELECT * FROM {$wpdb->wc_tax_rate_classes} ORDER BY name; + " + ); + wp_cache_set( $cache_key, $tax_rate_classes, 'taxes' ); + } + + return $tax_rate_classes; + } + + /** + * Get store tax class names. + * + * @return array Array of class names ("Reduced rate", "Zero rate", etc). + */ + public static function get_tax_classes() { + return wp_list_pluck( self::get_tax_rate_classes(), 'name' ); + } + + /** + * Get store tax classes as slugs. + * + * @since 3.0.0 + * @return array Array of class slugs ("reduced-rate", "zero-rate", etc). + */ + public static function get_tax_class_slugs() { + return wp_list_pluck( self::get_tax_rate_classes(), 'slug' ); + } + + /** + * Create a new tax class. + * + * @since 3.7.0 + * @param string $name Name of the tax class to add. + * @param string $slug (optional) Slug of the tax class to add. Defaults to sanitized name. + * @return WP_Error|array Returns name and slug (array) if the tax class is created, or WP_Error if something went wrong. + */ + public static function create_tax_class( $name, $slug = '' ) { + global $wpdb; + + if ( empty( $name ) ) { + return new WP_Error( 'tax_class_invalid_name', __( 'Tax class requires a valid name', 'woocommerce' ) ); + } + + $existing = self::get_tax_classes(); + $existing_slugs = self::get_tax_class_slugs(); + $name = wc_clean( $name ); + + if ( in_array( $name, $existing, true ) ) { + return new WP_Error( 'tax_class_exists', __( 'Tax class already exists', 'woocommerce' ) ); + } + + if ( ! $slug ) { + $slug = sanitize_title( $name ); + } + + // Stop if there's no slug. + if ( ! $slug ) { + return new WP_Error( 'tax_class_slug_invalid', __( 'Tax class slug is invalid', 'woocommerce' ) ); + } + + if ( in_array( $slug, $existing_slugs, true ) ) { + return new WP_Error( 'tax_class_slug_exists', __( 'Tax class slug already exists', 'woocommerce' ) ); + } + + $insert = $wpdb->insert( + $wpdb->wc_tax_rate_classes, + array( + 'name' => $name, + 'slug' => $slug, + ) + ); + + if ( is_wp_error( $insert ) ) { + return new WP_Error( 'tax_class_insert_error', $insert->get_error_message() ); + } + + wp_cache_delete( 'tax-rate-classes', 'taxes' ); + + return array( + 'name' => $name, + 'slug' => $slug, + ); + } + + /** + * Get an existing tax class. + * + * @since 3.7.0 + * @param string $field Field to get by. Valid values are id, name, or slug. + * @param string|int $item Item to get. + * @return array|bool Returns the tax class as an array. False if not found. + */ + public static function get_tax_class_by( $field, $item ) { + if ( ! in_array( $field, array( 'id', 'name', 'slug' ), true ) ) { + return new WP_Error( 'invalid_field', __( 'Invalid field', 'woocommerce' ) ); + } + + if ( 'id' === $field ) { + $field = 'tax_rate_class_id'; + } + + $matches = wp_list_filter( + self::get_tax_rate_classes(), + array( + $field => $item, + ) + ); + + if ( ! $matches ) { + return false; + } + + $tax_class = current( $matches ); + + return array( + 'name' => $tax_class->name, + 'slug' => $tax_class->slug, + ); + } + + /** + * Delete an existing tax class. + * + * @since 3.7.0 + * @param string $field Field to delete by. Valid values are id, name, or slug. + * @param string|int $item Item to delete. + * @return WP_Error|bool Returns true if deleted successfully, false if nothing was deleted, or WP_Error if there is an invalid request. + */ + public static function delete_tax_class_by( $field, $item ) { + global $wpdb; + + if ( ! in_array( $field, array( 'id', 'name', 'slug' ), true ) ) { + return new WP_Error( 'invalid_field', __( 'Invalid field', 'woocommerce' ) ); + } + + $tax_class = self::get_tax_class_by( $field, $item ); + + if ( ! $tax_class ) { + return new WP_Error( 'invalid_tax_class', __( 'Invalid tax class', 'woocommerce' ) ); + } + + if ( 'id' === $field ) { + $field = 'tax_rate_class_id'; + } + + $delete = $wpdb->delete( + $wpdb->wc_tax_rate_classes, + array( + $field => $item, + ) + ); + + if ( $delete ) { + // Delete associated tax rates. + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_class = %s;", $tax_class['slug'] ) ); + $wpdb->query( "DELETE locations FROM {$wpdb->prefix}woocommerce_tax_rate_locations locations LEFT JOIN {$wpdb->prefix}woocommerce_tax_rates rates ON rates.tax_rate_id = locations.tax_rate_id WHERE rates.tax_rate_id IS NULL;" ); + } + + wp_cache_delete( 'tax-rate-classes', 'taxes' ); + WC_Cache_Helper::invalidate_cache_group( 'taxes' ); + + return (bool) $delete; + } + + /** + * Format the city. + * + * @param string $city Value to format. + * @return string + */ + private static function format_tax_rate_city( $city ) { + return strtoupper( trim( $city ) ); + } + + /** + * Format the state. + * + * @param string $state Value to format. + * @return string + */ + private static function format_tax_rate_state( $state ) { + $state = strtoupper( $state ); + return ( '*' === $state ) ? '' : $state; + } + + /** + * Format the country. + * + * @param string $country Value to format. + * @return string + */ + private static function format_tax_rate_country( $country ) { + $country = strtoupper( $country ); + return ( '*' === $country ) ? '' : $country; + } + + /** + * Format the tax rate name. + * + * @param string $name Value to format. + * @return string + */ + private static function format_tax_rate_name( $name ) { + return $name ? $name : __( 'Tax', 'woocommerce' ); + } + + /** + * Format the rate. + * + * @param float $rate Value to format. + * @return string + */ + private static function format_tax_rate( $rate ) { + return number_format( (float) $rate, 4, '.', '' ); + } + + /** + * Format the priority. + * + * @param string $priority Value to format. + * @return int + */ + private static function format_tax_rate_priority( $priority ) { + return absint( $priority ); + } + + /** + * Format the class. + * + * @param string $class Value to format. + * @return string + */ + public static function format_tax_rate_class( $class ) { + $class = sanitize_title( $class ); + $classes = self::get_tax_class_slugs(); + if ( ! in_array( $class, $classes, true ) ) { + $class = ''; + } + return ( 'standard' === $class ) ? '' : $class; + } + + /** + * Prepare and format tax rate for DB insertion. + * + * @param array $tax_rate Tax rate to format. + * @return array + */ + private static function prepare_tax_rate( $tax_rate ) { + foreach ( $tax_rate as $key => $value ) { + if ( method_exists( __CLASS__, 'format_' . $key ) ) { + if ( 'tax_rate_state' === $key ) { + $tax_rate[ $key ] = call_user_func( array( __CLASS__, 'format_' . $key ), sanitize_key( $value ) ); + } else { + $tax_rate[ $key ] = call_user_func( array( __CLASS__, 'format_' . $key ), $value ); + } + } + } + return $tax_rate; + } + + /** + * Insert a new tax rate. + * + * Internal use only. + * + * @since 2.3.0 + * + * @param array $tax_rate Tax rate to insert. + * @return int tax rate id + */ + public static function _insert_tax_rate( $tax_rate ) { + global $wpdb; + + $wpdb->insert( $wpdb->prefix . 'woocommerce_tax_rates', self::prepare_tax_rate( $tax_rate ) ); + + $tax_rate_id = $wpdb->insert_id; + + WC_Cache_Helper::invalidate_cache_group( 'taxes' ); + + do_action( 'woocommerce_tax_rate_added', $tax_rate_id, $tax_rate ); + + return $tax_rate_id; + } + + /** + * Get tax rate. + * + * Internal use only. + * + * @since 2.5.0 + * + * @param int $tax_rate_id Tax rate ID. + * @param string $output_type Type of output. + * @return array|object + */ + public static function _get_tax_rate( $tax_rate_id, $output_type = ARRAY_A ) { + global $wpdb; + + return $wpdb->get_row( + $wpdb->prepare( + " + SELECT * + FROM {$wpdb->prefix}woocommerce_tax_rates + WHERE tax_rate_id = %d + ", + $tax_rate_id + ), + $output_type + ); + } + + /** + * Update a tax rate. + * + * Internal use only. + * + * @since 2.3.0 + * + * @param int $tax_rate_id Tax rate to update. + * @param array $tax_rate Tax rate values. + */ + public static function _update_tax_rate( $tax_rate_id, $tax_rate ) { + global $wpdb; + + $tax_rate_id = absint( $tax_rate_id ); + + $wpdb->update( + $wpdb->prefix . 'woocommerce_tax_rates', + self::prepare_tax_rate( $tax_rate ), + array( + 'tax_rate_id' => $tax_rate_id, + ) + ); + + WC_Cache_Helper::invalidate_cache_group( 'taxes' ); + + do_action( 'woocommerce_tax_rate_updated', $tax_rate_id, $tax_rate ); + } + + /** + * Delete a tax rate from the database. + * + * Internal use only. + * + * @since 2.3.0 + * @param int $tax_rate_id Tax rate to delete. + */ + public static function _delete_tax_rate( $tax_rate_id ) { + global $wpdb; + + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d;", $tax_rate_id ) ); + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d;", $tax_rate_id ) ); + + WC_Cache_Helper::invalidate_cache_group( 'taxes' ); + + do_action( 'woocommerce_tax_rate_deleted', $tax_rate_id ); + } + + /** + * Update postcodes for a tax rate in the DB. + * + * Internal use only. + * + * @since 2.3.0 + * + * @param int $tax_rate_id Tax rate to update. + * @param string $postcodes String of postcodes separated by ; characters. + */ + public static function _update_tax_rate_postcodes( $tax_rate_id, $postcodes ) { + if ( ! is_array( $postcodes ) ) { + $postcodes = explode( ';', $postcodes ); + } + // No normalization - postcodes are matched against both normal and formatted versions to support wildcards. + foreach ( $postcodes as $key => $postcode ) { + $postcodes[ $key ] = strtoupper( trim( str_replace( chr( 226 ) . chr( 128 ) . chr( 166 ), '...', $postcode ) ) ); + } + self::update_tax_rate_locations( $tax_rate_id, array_diff( array_filter( $postcodes ), array( '*' ) ), 'postcode' ); + } + + /** + * Update cities for a tax rate in the DB. + * + * Internal use only. + * + * @since 2.3.0 + * + * @param int $tax_rate_id Tax rate to update. + * @param string $cities Cities to set. + */ + public static function _update_tax_rate_cities( $tax_rate_id, $cities ) { + if ( ! is_array( $cities ) ) { + $cities = explode( ';', $cities ); + } + $cities = array_filter( array_diff( array_map( array( __CLASS__, 'format_tax_rate_city' ), $cities ), array( '*' ) ) ); + + self::update_tax_rate_locations( $tax_rate_id, $cities, 'city' ); + } + + /** + * Updates locations (postcode and city). + * + * Internal use only. + * + * @since 2.3.0 + * + * @param int $tax_rate_id Tax rate ID to update. + * @param array $values Values to set. + * @param string $type Location type. + */ + private static function update_tax_rate_locations( $tax_rate_id, $values, $type ) { + global $wpdb; + + $tax_rate_id = absint( $tax_rate_id ); + + $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d AND location_type = %s;", + $tax_rate_id, + $type + ) + ); + + if ( count( $values ) > 0 ) { + $sql = "( '" . implode( "', $tax_rate_id, '" . esc_sql( $type ) . "' ),( '", array_map( 'esc_sql', $values ) ) . "', $tax_rate_id, '" . esc_sql( $type ) . "' )"; + + $wpdb->query( "INSERT INTO {$wpdb->prefix}woocommerce_tax_rate_locations ( location_code, tax_rate_id, location_type ) VALUES $sql;" ); // @codingStandardsIgnoreLine. + } + + WC_Cache_Helper::invalidate_cache_group( 'taxes' ); + } + + /** + * Used by admin settings page. + * + * @param string $tax_class Tax class slug. + * + * @return array|null|object + */ + public static function get_rates_for_tax_class( $tax_class ) { + global $wpdb; + + $tax_class = self::format_tax_rate_class( $tax_class ); + + // Get all the rates and locations. Snagging all at once should significantly cut down on the number of queries. + $rates = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rates` WHERE `tax_rate_class` = %s;", $tax_class ) ); + $locations = $wpdb->get_results( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rate_locations`" ); + + if ( ! empty( $rates ) ) { + // Set the rates keys equal to their ids. + $rates = array_combine( wp_list_pluck( $rates, 'tax_rate_id' ), $rates ); + } + + // Drop the locations into the rates array. + foreach ( $locations as $location ) { + // Don't set them for nonexistent rates. + if ( ! isset( $rates[ $location->tax_rate_id ] ) ) { + continue; + } + // If the rate exists, initialize the array before appending to it. + if ( ! isset( $rates[ $location->tax_rate_id ]->{$location->location_type} ) ) { + $rates[ $location->tax_rate_id ]->{$location->location_type} = array(); + } + $rates[ $location->tax_rate_id ]->{$location->location_type}[] = $location->location_code; + } + + foreach ( $rates as $rate_id => $rate ) { + $rates[ $rate_id ]->postcode_count = isset( $rates[ $rate_id ]->postcode ) ? count( $rates[ $rate_id ]->postcode ) : 0; + $rates[ $rate_id ]->city_count = isset( $rates[ $rate_id ]->city ) ? count( $rates[ $rate_id ]->city ) : 0; + } + + $rates = self::sort_rates( $rates ); + + return $rates; + } +} +WC_Tax::init(); diff --git a/plugins/woocommerce/includes/class-wc-template-loader.php b/plugins/woocommerce/includes/class-wc-template-loader.php new file mode 100644 index 00000000000..c50a1526815 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-template-loader.php @@ -0,0 +1,668 @@ +plugin_path() . '/templates/' . $cs_template; + } else { + $template = WC()->plugin_path() . '/templates/' . $default_file; + } + } + } + + return $template; + } + + /** + * Checks whether a block template with that name exists. + * + * **Note: ** This checks both the `templates` and `block-templates` directories + * as both conventions should be supported. + * + * @since 5.5.0 + * @param string $template_name Template to check. + * @return boolean + */ + private static function has_block_template( $template_name ) { + if ( ! $template_name ) { + return false; + } + + $has_template = false; + $template_filename = $template_name . '.html'; + // Since Gutenberg 12.1.0, the conventions for block templates directories have changed, + // we should check both these possible directories for backwards-compatibility. + $possible_templates_dirs = array( 'templates', 'block-templates' ); + + // Combine the possible root directory names with either the template directory + // or the stylesheet directory for child themes, getting all possible block templates + // locations combinations. + $filepath = DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . $template_filename; + $legacy_filepath = DIRECTORY_SEPARATOR . 'block-templates' . DIRECTORY_SEPARATOR . $template_filename; + $possible_paths = array( + get_stylesheet_directory() . $filepath, + get_stylesheet_directory() . $legacy_filepath, + get_template_directory() . $filepath, + get_template_directory() . $legacy_filepath, + ); + + // Check the first matching one. + foreach ( $possible_paths as $path ) { + if ( is_readable( $path ) ) { + $has_template = true; + break; + } + } + + /** + * Filters the value of the result of the block template check. + * + * @since x.x.x + * + * @param boolean $has_template value to be filtered. + * @param string $template_name The name of the template. + */ + return (bool) apply_filters( 'woocommerce_has_block_template', $has_template, $template_name ); + } + + /** + * Get the default filename for a template except if a block template with + * the same name exists. + * + * @since 3.0.0 + * @since 5.5.0 If a block template with the same name exists, return an + * empty string. + * @since 6.3.0 It checks custom product taxonomies + * @return string + */ + private static function get_template_loader_default_file() { + if ( + is_singular( 'product' ) && + ! self::has_block_template( 'single-product' ) + ) { + $default_file = 'single-product.php'; + } elseif ( is_product_taxonomy() ) { + $object = get_queried_object(); + + if ( self::has_block_template( 'taxonomy-' . $object->taxonomy ) ) { + $default_file = ''; + } else { + if ( is_tax( 'product_cat' ) || is_tax( 'product_tag' ) ) { + $default_file = 'taxonomy-' . $object->taxonomy . '.php'; + } elseif ( ! self::has_block_template( 'archive-product' ) ) { + $default_file = 'archive-product.php'; + } else { + $default_file = ''; + } + } + } elseif ( + ( is_post_type_archive( 'product' ) || is_page( wc_get_page_id( 'shop' ) ) ) && + ! self::has_block_template( 'archive-product' ) + ) { + $default_file = self::$theme_support ? 'archive-product.php' : ''; + } else { + $default_file = ''; + } + return $default_file; + } + + /** + * Get an array of filenames to search for a given template. + * + * @since 3.0.0 + * @param string $default_file The default file name. + * @return string[] + */ + private static function get_template_loader_files( $default_file ) { + $templates = apply_filters( 'woocommerce_template_loader_files', array(), $default_file ); + $templates[] = 'woocommerce.php'; + + if ( is_page_template() ) { + $page_template = get_page_template_slug(); + + if ( $page_template ) { + $validated_file = validate_file( $page_template ); + if ( 0 === $validated_file ) { + $templates[] = $page_template; + } else { + error_log( "WooCommerce: Unable to validate template path: \"$page_template\". Error Code: $validated_file." ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + } + } + } + + if ( is_singular( 'product' ) ) { + $object = get_queried_object(); + $name_decoded = urldecode( $object->post_name ); + if ( $name_decoded !== $object->post_name ) { + $templates[] = "single-product-{$name_decoded}.php"; + } + $templates[] = "single-product-{$object->post_name}.php"; + } + + if ( is_product_taxonomy() ) { + $object = get_queried_object(); + + $templates[] = 'taxonomy-' . $object->taxonomy . '-' . $object->slug . '.php'; + $templates[] = WC()->template_path() . 'taxonomy-' . $object->taxonomy . '-' . $object->slug . '.php'; + $templates[] = 'taxonomy-' . $object->taxonomy . '.php'; + $templates[] = WC()->template_path() . 'taxonomy-' . $object->taxonomy . '.php'; + + if ( is_tax( 'product_cat' ) || is_tax( 'product_tag' ) ) { + $cs_taxonomy = str_replace( '_', '-', $object->taxonomy ); + $cs_default = str_replace( '_', '-', $default_file ); + $templates[] = 'taxonomy-' . $object->taxonomy . '-' . $object->slug . '.php'; + $templates[] = WC()->template_path() . 'taxonomy-' . $cs_taxonomy . '-' . $object->slug . '.php'; + $templates[] = 'taxonomy-' . $object->taxonomy . '.php'; + $templates[] = WC()->template_path() . 'taxonomy-' . $cs_taxonomy . '.php'; + $templates[] = $cs_default; + } + } + + $templates[] = $default_file; + if ( isset( $cs_default ) ) { + $templates[] = WC()->template_path() . $cs_default; + } + $templates[] = WC()->template_path() . $default_file; + + return array_unique( $templates ); + } + + /** + * Load comments template. + * + * @param string $template template to load. + * @return string + */ + public static function comments_template_loader( $template ) { + if ( get_post_type() !== 'product' ) { + return $template; + } + + $check_dirs = array( + trailingslashit( get_stylesheet_directory() ) . WC()->template_path(), + trailingslashit( get_template_directory() ) . WC()->template_path(), + trailingslashit( get_stylesheet_directory() ), + trailingslashit( get_template_directory() ), + trailingslashit( WC()->plugin_path() ) . 'templates/', + ); + + if ( WC_TEMPLATE_DEBUG_MODE ) { + $check_dirs = array( array_pop( $check_dirs ) ); + } + + foreach ( $check_dirs as $dir ) { + if ( file_exists( trailingslashit( $dir ) . 'single-product-reviews.php' ) ) { + return trailingslashit( $dir ) . 'single-product-reviews.php'; + } + } + } + + /** + * Unsupported theme compatibility methods. + */ + + /** + * Hook in methods to enhance the unsupported theme experience on pages. + * + * @since 3.3.0 + */ + public static function unsupported_theme_init() { + if ( 0 < self::$shop_page_id ) { + if ( is_product_taxonomy() ) { + self::unsupported_theme_tax_archive_init(); + } elseif ( is_product() ) { + self::unsupported_theme_product_page_init(); + } else { + self::unsupported_theme_shop_page_init(); + } + } + } + + /** + * Hook in methods to enhance the unsupported theme experience on the Shop page. + * + * @since 3.3.0 + */ + private static function unsupported_theme_shop_page_init() { + add_filter( 'the_content', array( __CLASS__, 'unsupported_theme_shop_content_filter' ), 10 ); + add_filter( 'the_title', array( __CLASS__, 'unsupported_theme_title_filter' ), 10, 2 ); + add_filter( 'comments_number', array( __CLASS__, 'unsupported_theme_comments_number_filter' ) ); + } + + /** + * Hook in methods to enhance the unsupported theme experience on Product pages. + * + * @since 3.3.0 + */ + private static function unsupported_theme_product_page_init() { + add_filter( 'the_content', array( __CLASS__, 'unsupported_theme_product_content_filter' ), 10 ); + add_filter( 'post_thumbnail_html', array( __CLASS__, 'unsupported_theme_single_featured_image_filter' ) ); + add_filter( 'woocommerce_product_tabs', array( __CLASS__, 'unsupported_theme_remove_review_tab' ) ); + remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10 ); + remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10 ); + self::add_support_for_product_page_gallery(); + } + + /** + * Add theme support for Product page gallery. + * + * @since x.x.x + */ + private static function add_support_for_product_page_gallery() { + add_theme_support( 'wc-product-gallery-zoom' ); + add_theme_support( 'wc-product-gallery-lightbox' ); + add_theme_support( 'wc-product-gallery-slider' ); + } + + /** + * Enhance the unsupported theme experience on Product Category and Attribute pages by rendering + * those pages using the single template and shortcode-based content. To do this we make a dummy + * post and set a shortcode as the post content. This approach is adapted from bbPress. + * + * @since 3.3.0 + */ + private static function unsupported_theme_tax_archive_init() { + global $wp_query, $post; + + $queried_object = get_queried_object(); + $args = self::get_current_shop_view_args(); + $shortcode_args = array( + 'page' => $args->page, + 'columns' => $args->columns, + 'rows' => $args->rows, + 'orderby' => '', + 'order' => '', + 'paginate' => true, + 'cache' => false, + ); + + if ( is_product_category() ) { + $shortcode_args['category'] = sanitize_title( $queried_object->slug ); + } elseif ( taxonomy_is_product_attribute( $queried_object->taxonomy ) ) { + $shortcode_args['attribute'] = sanitize_title( $queried_object->taxonomy ); + $shortcode_args['terms'] = sanitize_title( $queried_object->slug ); + } elseif ( is_product_tag() ) { + $shortcode_args['tag'] = sanitize_title( $queried_object->slug ); + } else { + // Default theme archive for all other taxonomies. + return; + } + + // Description handling. + if ( ! empty( $queried_object->description ) && ( empty( $_GET['product-page'] ) || 1 === absint( $_GET['product-page'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $prefix = '
    ' . wc_format_content( wp_kses_post( $queried_object->description ) ) . '
    '; + } else { + $prefix = ''; + } + + add_filter( 'woocommerce_shortcode_products_query', array( __CLASS__, 'unsupported_archive_layered_nav_compatibility' ) ); + $shortcode = new WC_Shortcode_Products( $shortcode_args ); + remove_filter( 'woocommerce_shortcode_products_query', array( __CLASS__, 'unsupported_archive_layered_nav_compatibility' ) ); + $shop_page = get_post( self::$shop_page_id ); + + $dummy_post_properties = array( + 'ID' => 0, + 'post_status' => 'publish', + 'post_author' => $shop_page->post_author, + 'post_parent' => 0, + 'post_type' => 'page', + 'post_date' => $shop_page->post_date, + 'post_date_gmt' => $shop_page->post_date_gmt, + 'post_modified' => $shop_page->post_modified, + 'post_modified_gmt' => $shop_page->post_modified_gmt, + 'post_content' => $prefix . $shortcode->get_content(), + 'post_title' => wc_clean( $queried_object->name ), + 'post_excerpt' => '', + 'post_content_filtered' => '', + 'post_mime_type' => '', + 'post_password' => '', + 'post_name' => $queried_object->slug, + 'guid' => '', + 'menu_order' => 0, + 'pinged' => '', + 'to_ping' => '', + 'ping_status' => '', + 'comment_status' => 'closed', + 'comment_count' => 0, + 'filter' => 'raw', + ); + + // Set the $post global. + $post = new WP_Post( (object) $dummy_post_properties ); // @codingStandardsIgnoreLine. + + // Copy the new post global into the main $wp_query. + $wp_query->post = $post; + $wp_query->posts = array( $post ); + + // Prevent comments form from appearing. + $wp_query->post_count = 1; + $wp_query->is_404 = false; + $wp_query->is_page = true; + $wp_query->is_single = true; + $wp_query->is_archive = false; + $wp_query->is_tax = true; + $wp_query->max_num_pages = 0; + + // Prepare everything for rendering. + setup_postdata( $post ); + remove_all_filters( 'the_content' ); + remove_all_filters( 'the_excerpt' ); + add_filter( 'template_include', array( __CLASS__, 'force_single_template_filter' ) ); + } + + /** + * Add layered nav args to WP_Query args generated by the 'products' shortcode. + * + * @since 3.3.4 + * @param array $query WP_Query args. + * @return array + */ + public static function unsupported_archive_layered_nav_compatibility( $query ) { + foreach ( WC()->query->get_layered_nav_chosen_attributes() as $taxonomy => $data ) { + $query['tax_query'][] = array( + 'taxonomy' => $taxonomy, + 'field' => 'slug', + 'terms' => $data['terms'], + 'operator' => 'and' === $data['query_type'] ? 'AND' : 'IN', + 'include_children' => false, + ); + } + return $query; + } + + /** + * Force the loading of one of the single templates instead of whatever template was about to be loaded. + * + * @since 3.3.0 + * @param string $template Path to template. + * @return string + */ + public static function force_single_template_filter( $template ) { + $possible_templates = array( + 'page', + 'single', + 'singular', + 'index', + ); + + foreach ( $possible_templates as $possible_template ) { + $path = get_query_template( $possible_template ); + if ( $path ) { + return $path; + } + } + + return $template; + } + + /** + * Get information about the current shop page view. + * + * @since 3.3.0 + * @return array + */ + private static function get_current_shop_view_args() { + return (object) array( + 'page' => absint( max( 1, absint( get_query_var( 'paged' ) ) ) ), + 'columns' => wc_get_default_products_per_row(), + 'rows' => wc_get_default_product_rows_per_page(), + ); + } + + /** + * Filter the title and insert WooCommerce content on the shop page. + * + * For non-WC themes, this will setup the main shop page to be shortcode based to improve default appearance. + * + * @since 3.3.0 + * @param string $title Existing title. + * @param int $id ID of the post being filtered. + * @return string + */ + public static function unsupported_theme_title_filter( $title, $id ) { + if ( self::$theme_support || ! $id !== self::$shop_page_id ) { + return $title; + } + + if ( is_page( self::$shop_page_id ) || ( is_home() && 'page' === get_option( 'show_on_front' ) && absint( get_option( 'page_on_front' ) ) === self::$shop_page_id ) ) { + $args = self::get_current_shop_view_args(); + $title_suffix = array(); + + if ( $args->page > 1 ) { + /* translators: %d: Page number. */ + $title_suffix[] = sprintf( esc_html__( 'Page %d', 'woocommerce' ), $args->page ); + } + + if ( $title_suffix ) { + $title = $title . ' – ' . implode( ', ', $title_suffix ); + } + } + return $title; + } + + /** + * Filter the content and insert WooCommerce content on the shop page. + * + * For non-WC themes, this will setup the main shop page to be shortcode based to improve default appearance. + * + * @since 3.3.0 + * @param string $content Existing post content. + * @return string + */ + public static function unsupported_theme_shop_content_filter( $content ) { + global $wp_query; + + if ( self::$theme_support || ! is_main_query() || ! in_the_loop() ) { + return $content; + } + + self::$in_content_filter = true; + + // Remove the filter we're in to avoid nested calls. + remove_filter( 'the_content', array( __CLASS__, 'unsupported_theme_shop_content_filter' ) ); + + // Unsupported theme shop page. + if ( is_page( self::$shop_page_id ) ) { + $args = self::get_current_shop_view_args(); + $shortcode = new WC_Shortcode_Products( + array_merge( + WC()->query->get_catalog_ordering_args(), + array( + 'page' => $args->page, + 'columns' => $args->columns, + 'rows' => $args->rows, + 'orderby' => '', + 'order' => '', + 'paginate' => true, + 'cache' => false, + ) + ), + 'products' + ); + + // Allow queries to run e.g. layered nav. + add_action( 'pre_get_posts', array( WC()->query, 'product_query' ) ); + + $content = $content . $shortcode->get_content(); + + // Remove actions and self to avoid nested calls. + remove_action( 'pre_get_posts', array( WC()->query, 'product_query' ) ); + WC()->query->remove_ordering_args(); + } + + self::$in_content_filter = false; + + return $content; + } + + /** + * Filter the content and insert WooCommerce content on the shop page. + * + * For non-WC themes, this will setup the main shop page to be shortcode based to improve default appearance. + * + * @since 3.3.0 + * @param string $content Existing post content. + * @return string + */ + public static function unsupported_theme_product_content_filter( $content ) { + global $wp_query; + + if ( self::$theme_support || ! is_main_query() || ! in_the_loop() ) { + return $content; + } + + self::$in_content_filter = true; + + // Remove the filter we're in to avoid nested calls. + remove_filter( 'the_content', array( __CLASS__, 'unsupported_theme_product_content_filter' ) ); + + if ( is_product() ) { + $content = do_shortcode( '[product_page id="' . get_the_ID() . '" show_title=0 status="any"]' ); + } + + self::$in_content_filter = false; + + return $content; + } + + /** + * Suppress the comments number on the Shop page for unsupported themes since there is no commenting on the Shop page. + * + * @since 3.4.5 + * @param string $comments_number The comments number text. + * @return string + */ + public static function unsupported_theme_comments_number_filter( $comments_number ) { + if ( is_page( self::$shop_page_id ) ) { + return ''; + } + + return $comments_number; + } + + /** + * Are we filtering content for unsupported themes? + * + * @since 3.3.2 + * @return bool + */ + public static function in_content_filter() { + return (bool) self::$in_content_filter; + } + + /** + * Prevent the main featured image on product pages because there will be another featured image + * in the gallery. + * + * @since 3.3.0 + * @param string $html Img element HTML. + * @return string + */ + public static function unsupported_theme_single_featured_image_filter( $html ) { + if ( self::in_content_filter() || ! is_product() || ! is_main_query() ) { + return $html; + } + + return ''; + } + + /** + * Remove the Review tab and just use the regular comment form. + * + * @param array $tabs Tab info. + * @return array + */ + public static function unsupported_theme_remove_review_tab( $tabs ) { + unset( $tabs['reviews'] ); + return $tabs; + } +} + +add_action( 'init', array( 'WC_Template_Loader', 'init' ) ); diff --git a/plugins/woocommerce/includes/class-wc-tracker.php b/plugins/woocommerce/includes/class-wc-tracker.php new file mode 100644 index 00000000000..dbae0b03978 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-tracker.php @@ -0,0 +1,789 @@ + apply_filters( 'woocommerce_tracker_last_send_interval', strtotime( '-1 week' ) ) ) { + return; + } + } else { + // Make sure there is at least a 1 hour delay between override sends, we don't want duplicate calls due to double clicking links. + $last_send = self::get_last_send_time(); + if ( $last_send && $last_send > strtotime( '-1 hours' ) ) { + return; + } + } + + // Update time first before sending to ensure it is set. + update_option( 'woocommerce_tracker_last_send', time() ); + + $params = self::get_tracking_data(); + wp_safe_remote_post( + self::$api_url, + array( + 'method' => 'POST', + 'timeout' => 45, + 'redirection' => 5, + 'httpversion' => '1.0', + 'blocking' => false, + 'headers' => array( 'user-agent' => 'WooCommerceTracker/' . md5( esc_url_raw( home_url( '/' ) ) ) . ';' ), + 'body' => wp_json_encode( $params ), + 'cookies' => array(), + ) + ); + } + + /** + * Get the last time tracking data was sent. + * + * @return int|bool + */ + private static function get_last_send_time() { + return apply_filters( 'woocommerce_tracker_last_send_time', get_option( 'woocommerce_tracker_last_send', false ) ); + } + + /** + * Test whether this site is a staging site according to the Jetpack criteria. + * + * With Jetpack 8.1+, Jetpack::is_staging_site has been deprecated. + * \Automattic\Jetpack\Status::is_staging_site is the replacement. + * However, there are version of JP where \Automattic\Jetpack\Status exists, but does *not* contain is_staging_site method, + * so with those, code still needs to use the previous check as a fallback. + * + * @return bool + */ + private static function is_jetpack_staging_site() { + if ( class_exists( '\Automattic\Jetpack\Status' ) ) { + // Preferred way of checking with Jetpack 8.1+. + $jp_status = new \Automattic\Jetpack\Status(); + if ( is_callable( array( $jp_status, 'is_staging_site' ) ) ) { + return $jp_status->is_staging_site(); + } + } + + return ( class_exists( 'Jetpack' ) && is_callable( 'Jetpack::is_staging_site' ) && Jetpack::is_staging_site() ); + } + + /** + * Get all the tracking data. + * + * @return array + */ + public static function get_tracking_data() { + $data = array(); + + // General site info. + $data['url'] = home_url(); + $data['email'] = apply_filters( 'woocommerce_tracker_admin_email', get_option( 'admin_email' ) ); + $data['theme'] = self::get_theme_info(); + + // WordPress Info. + $data['wp'] = self::get_wordpress_info(); + + // Server Info. + $data['server'] = self::get_server_info(); + + // Plugin info. + $all_plugins = self::get_all_plugins(); + $data['active_plugins'] = $all_plugins['active_plugins']; + $data['inactive_plugins'] = $all_plugins['inactive_plugins']; + + // Jetpack & WooCommerce Connect. + + $data['jetpack_version'] = Constants::is_defined( 'JETPACK__VERSION' ) ? Constants::get_constant( 'JETPACK__VERSION' ) : 'none'; + $data['jetpack_connected'] = ( class_exists( 'Jetpack' ) && is_callable( 'Jetpack::is_active' ) && Jetpack::is_active() ) ? 'yes' : 'no'; + $data['jetpack_is_staging'] = self::is_jetpack_staging_site() ? 'yes' : 'no'; + $data['connect_installed'] = class_exists( 'WC_Connect_Loader' ) ? 'yes' : 'no'; + $data['connect_active'] = ( class_exists( 'WC_Connect_Loader' ) && wp_next_scheduled( 'wc_connect_fetch_service_schemas' ) ) ? 'yes' : 'no'; + $data['helper_connected'] = self::get_helper_connected(); + + // Store count info. + $data['users'] = self::get_user_counts(); + $data['products'] = self::get_product_counts(); + $data['orders'] = self::get_orders(); + $data['reviews'] = self::get_review_counts(); + $data['categories'] = self::get_category_counts(); + + // Payment gateway info. + $data['gateways'] = self::get_active_payment_gateways(); + + // WcPay settings info. + $data['wcpay_settings'] = self::get_wcpay_settings(); + + // Shipping method info. + $data['shipping_methods'] = self::get_active_shipping_methods(); + + // Get all WooCommerce options info. + $data['settings'] = self::get_all_woocommerce_options_values(); + + // Template overrides. + $data['template_overrides'] = self::get_all_template_overrides(); + + // Cart & checkout tech (blocks or shortcodes). + $data['cart_checkout'] = self::get_cart_checkout_info(); + + // WooCommerce Admin info. + $data['wc_admin_disabled'] = apply_filters( 'woocommerce_admin_disabled', false ) ? 'yes' : 'no'; + + // Mobile info. + $data['wc_mobile_usage'] = self::get_woocommerce_mobile_usage(); + + return apply_filters( 'woocommerce_tracker_data', $data ); + } + + /** + * Get the current theme info, theme name and version. + * + * @return array + */ + public static function get_theme_info() { + $theme_data = wp_get_theme(); + $theme_child_theme = wc_bool_to_string( is_child_theme() ); + $theme_wc_support = wc_bool_to_string( current_theme_supports( 'woocommerce' ) ); + $theme_is_block_theme = wc_bool_to_string( wc_current_theme_is_fse_theme() ); + + return array( + 'name' => $theme_data->Name, // @phpcs:ignore + 'version' => $theme_data->Version, // @phpcs:ignore + 'child_theme' => $theme_child_theme, + 'wc_support' => $theme_wc_support, + 'block_theme' => $theme_is_block_theme, + ); + } + + /** + * Get WordPress related data. + * + * @return array + */ + private static function get_wordpress_info() { + $wp_data = array(); + + $memory = wc_let_to_num( WP_MEMORY_LIMIT ); + + if ( function_exists( 'memory_get_usage' ) ) { + $system_memory = wc_let_to_num( @ini_get( 'memory_limit' ) ); + $memory = max( $memory, $system_memory ); + } + + // WordPress 5.5+ environment type specification. + // 'production' is the default in WP, thus using it as a default here, too. + $environment_type = 'production'; + if ( function_exists( 'wp_get_environment_type' ) ) { + $environment_type = wp_get_environment_type(); + } + + $wp_data['memory_limit'] = size_format( $memory ); + $wp_data['debug_mode'] = ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ? 'Yes' : 'No'; + $wp_data['locale'] = get_locale(); + $wp_data['version'] = get_bloginfo( 'version' ); + $wp_data['multisite'] = is_multisite() ? 'Yes' : 'No'; + $wp_data['env_type'] = $environment_type; + $wp_data['dropins'] = array_keys( get_dropins() ); + + return $wp_data; + } + + /** + * Get server related info. + * + * @return array + */ + private static function get_server_info() { + $server_data = array(); + + if ( ! empty( $_SERVER['SERVER_SOFTWARE'] ) ) { + $server_data['software'] = $_SERVER['SERVER_SOFTWARE']; // @phpcs:ignore + } + + if ( function_exists( 'phpversion' ) ) { + $server_data['php_version'] = phpversion(); + } + + if ( function_exists( 'ini_get' ) ) { + $server_data['php_post_max_size'] = size_format( wc_let_to_num( ini_get( 'post_max_size' ) ) ); + $server_data['php_time_limt'] = ini_get( 'max_execution_time' ); + $server_data['php_max_input_vars'] = ini_get( 'max_input_vars' ); + $server_data['php_suhosin'] = extension_loaded( 'suhosin' ) ? 'Yes' : 'No'; + } + + $database_version = wc_get_server_database_version(); + $server_data['mysql_version'] = $database_version['number']; + + $server_data['php_max_upload_size'] = size_format( wp_max_upload_size() ); + $server_data['php_default_timezone'] = date_default_timezone_get(); + $server_data['php_soap'] = class_exists( 'SoapClient' ) ? 'Yes' : 'No'; + $server_data['php_fsockopen'] = function_exists( 'fsockopen' ) ? 'Yes' : 'No'; + $server_data['php_curl'] = function_exists( 'curl_init' ) ? 'Yes' : 'No'; + + return $server_data; + } + + /** + * Get all plugins grouped into activated or not. + * + * @return array + */ + private static function get_all_plugins() { + // Ensure get_plugins function is loaded. + if ( ! function_exists( 'get_plugins' ) ) { + include ABSPATH . '/wp-admin/includes/plugin.php'; + } + + $plugins = get_plugins(); + $active_plugins_keys = get_option( 'active_plugins', array() ); + $active_plugins = array(); + + foreach ( $plugins as $k => $v ) { + // Take care of formatting the data how we want it. + $formatted = array(); + $formatted['name'] = strip_tags( $v['Name'] ); + if ( isset( $v['Version'] ) ) { + $formatted['version'] = strip_tags( $v['Version'] ); + } + if ( isset( $v['Author'] ) ) { + $formatted['author'] = strip_tags( $v['Author'] ); + } + if ( isset( $v['Network'] ) ) { + $formatted['network'] = strip_tags( $v['Network'] ); + } + if ( isset( $v['PluginURI'] ) ) { + $formatted['plugin_uri'] = strip_tags( $v['PluginURI'] ); + } + if ( in_array( $k, $active_plugins_keys ) ) { + // Remove active plugins from list so we can show active and inactive separately. + unset( $plugins[ $k ] ); + $active_plugins[ $k ] = $formatted; + } else { + $plugins[ $k ] = $formatted; + } + } + + return array( + 'active_plugins' => $active_plugins, + 'inactive_plugins' => $plugins, + ); + } + + /** + * Get the settings of WooCommerce Payments plugin + * + * @return array + */ + private static function get_wcpay_settings() { + return get_option( 'woocommerce_woocommerce_payments_settings' ); + } + + /** + * Check to see if the helper is connected to woocommerce.com + * + * @return string + */ + private static function get_helper_connected() { + if ( class_exists( 'WC_Helper_Options' ) && is_callable( 'WC_Helper_Options::get' ) ) { + $authenticated = WC_Helper_Options::get( 'auth' ); + } else { + $authenticated = ''; + } + return ( ! empty( $authenticated ) ) ? 'yes' : 'no'; + } + + + /** + * Get user totals based on user role. + * + * @return array + */ + private static function get_user_counts() { + $user_count = array(); + $user_count_data = count_users(); + $user_count['total'] = $user_count_data['total_users']; + + // Get user count based on user role. + foreach ( $user_count_data['avail_roles'] as $role => $count ) { + $user_count[ $role ] = $count; + } + + return $user_count; + } + + /** + * Get product totals based on product type. + * + * @return array + */ + public static function get_product_counts() { + $product_count = array(); + $product_count_data = wp_count_posts( 'product' ); + $product_count['total'] = $product_count_data->publish; + + $product_statuses = get_terms( 'product_type', array( 'hide_empty' => 0 ) ); + foreach ( $product_statuses as $product_status ) { + $product_count[ $product_status->name ] = $product_status->count; + } + + return $product_count; + } + + /** + * Get order counts. + * + * @return array + */ + private static function get_order_counts() { + $order_count = array(); + $order_count_data = wp_count_posts( 'shop_order' ); + foreach ( wc_get_order_statuses() as $status_slug => $status_name ) { + $order_count[ $status_slug ] = $order_count_data->{ $status_slug }; + } + return $order_count; + } + + /** + * Combine all order data. + * + * @return array + */ + private static function get_orders() { + $order_dates = self::get_order_dates(); + $order_counts = self::get_order_counts(); + $order_totals = self::get_order_totals(); + $order_gateways = self::get_orders_by_gateway(); + + return array_merge( $order_dates, $order_counts, $order_totals, $order_gateways ); + } + + /** + * Get order totals. + * + * @since 5.4.0 + * @return array + */ + private static function get_order_totals() { + global $wpdb; + + $gross_total = $wpdb->get_var( + " + SELECT + SUM( order_meta.meta_value ) AS 'gross_total' + FROM {$wpdb->prefix}posts AS orders + LEFT JOIN {$wpdb->prefix}postmeta AS order_meta ON order_meta.post_id = orders.ID + WHERE order_meta.meta_key = '_order_total' + AND orders.post_status in ( 'wc-completed', 'wc-refunded' ) + GROUP BY order_meta.meta_key + " + ); + + if ( is_null( $gross_total ) ) { + $gross_total = 0; + } + + $processing_gross_total = $wpdb->get_var( + " + SELECT + SUM( order_meta.meta_value ) AS 'gross_total' + FROM {$wpdb->prefix}posts AS orders + LEFT JOIN {$wpdb->prefix}postmeta AS order_meta ON order_meta.post_id = orders.ID + WHERE order_meta.meta_key = '_order_total' + AND orders.post_status = 'wc-processing' + GROUP BY order_meta.meta_key + " + ); + + if ( is_null( $processing_gross_total ) ) { + $processing_gross_total = 0; + } + + return array( + 'gross' => $gross_total, + 'processing_gross' => $processing_gross_total, + ); + } + + /** + * Get last order date. + * + * @return string + */ + private static function get_order_dates() { + global $wpdb; + + $min_max = $wpdb->get_row( + " + SELECT + MIN( post_date_gmt ) as 'first', MAX( post_date_gmt ) as 'last' + FROM {$wpdb->prefix}posts + WHERE post_type = 'shop_order' + AND post_status = 'wc-completed' + ", + ARRAY_A + ); + + if ( is_null( $min_max ) ) { + $min_max = array( + 'first' => '-', + 'last' => '-', + ); + } + + $processing_min_max = $wpdb->get_row( + " + SELECT + MIN( post_date_gmt ) as 'processing_first', MAX( post_date_gmt ) as 'processing_last' + FROM {$wpdb->prefix}posts + WHERE post_type = 'shop_order' + AND post_status = 'wc-processing' + ", + ARRAY_A + ); + + if ( is_null( $processing_min_max ) ) { + $processing_min_max = array( + 'processing_first' => '-', + 'processing_last' => '-', + ); + } + + return array_merge( $min_max, $processing_min_max ); + } + + /** + * Get order details by gateway. + * + * @return array + */ + private static function get_orders_by_gateway() { + global $wpdb; + + $orders_by_gateway = $wpdb->get_results( + " + SELECT + gateway, currency, SUM(total) AS totals, COUNT(order_id) AS counts + FROM ( + SELECT + orders.id AS order_id, + MAX(CASE WHEN meta_key = '_payment_method' THEN meta_value END) gateway, + MAX(CASE WHEN meta_key = '_order_total' THEN meta_value END) total, + MAX(CASE WHEN meta_key = '_order_currency' THEN meta_value END) currency + FROM + {$wpdb->prefix}posts orders + LEFT JOIN + {$wpdb->prefix}postmeta order_meta ON order_meta.post_id = orders.id + WHERE orders.post_type = 'shop_order' + AND orders.post_status in ( 'wc-completed', 'wc-processing', 'wc-refunded' ) + AND meta_key in( '_payment_method','_order_total','_order_currency') + GROUP BY orders.id + ) order_gateways + GROUP BY gateway, currency + " + ); + + $orders_by_gateway_currency = array(); + foreach ( $orders_by_gateway as $orders_details ) { + $gateway = 'gateway_' . $orders_details->gateway; + $currency = $orders_details->currency; + $count = $gateway . '_' . $currency . '_count'; + $total = $gateway . '_' . $currency . '_total'; + + $orders_by_gateway_currency[ $count ] = $orders_details->counts; + $orders_by_gateway_currency[ $total ] = $orders_details->totals; + } + + return $orders_by_gateway_currency; + } + + /** + * Get review counts for different statuses. + * + * @return array + */ + private static function get_review_counts() { + global $wpdb; + $review_count = array( 'total' => 0 ); + $status_map = array( + '0' => 'pending', + '1' => 'approved', + 'trash' => 'trash', + 'spam' => 'spam', + ); + $counts = $wpdb->get_results( + " + SELECT comment_approved, COUNT(*) AS num_reviews + FROM {$wpdb->comments} + WHERE comment_type = 'review' + GROUP BY comment_approved + ", + ARRAY_A + ); + + if ( ! $counts ) { + return $review_count; + } + + foreach ( $counts as $count ) { + $status = $count['comment_approved']; + if ( array_key_exists( $status, $status_map ) ) { + $review_count[ $status_map[ $status ] ] = $count['num_reviews']; + } + $review_count['total'] += $count['num_reviews']; + } + + return $review_count; + } + + /** + * Get the number of product categories. + * + * @return int + */ + private static function get_category_counts() { + return wp_count_terms( 'product_cat' ); + } + + /** + * Get a list of all active payment gateways. + * + * @return array + */ + private static function get_active_payment_gateways() { + $active_gateways = array(); + $gateways = WC()->payment_gateways->payment_gateways(); + foreach ( $gateways as $id => $gateway ) { + if ( isset( $gateway->enabled ) && 'yes' === $gateway->enabled ) { + $active_gateways[ $id ] = array( + 'title' => $gateway->title, + 'supports' => $gateway->supports, + ); + } + } + + return $active_gateways; + } + + + /** + * Get a list of all active shipping methods. + * + * @return array + */ + private static function get_active_shipping_methods() { + $active_methods = array(); + $shipping_methods = WC()->shipping()->get_shipping_methods(); + foreach ( $shipping_methods as $id => $shipping_method ) { + if ( isset( $shipping_method->enabled ) && 'yes' === $shipping_method->enabled ) { + $active_methods[ $id ] = array( + 'title' => $shipping_method->title, + 'tax_status' => $shipping_method->tax_status, + ); + } + } + + return $active_methods; + } + + /** + * Get all options starting with woocommerce_ prefix. + * + * @return array + */ + private static function get_all_woocommerce_options_values() { + return array( + 'version' => WC()->version, + 'currency' => get_woocommerce_currency(), + 'base_location' => WC()->countries->get_base_country(), + 'base_state' => WC()->countries->get_base_state(), + 'base_postcode' => WC()->countries->get_base_postcode(), + 'selling_locations' => WC()->countries->get_allowed_countries(), + 'api_enabled' => get_option( 'woocommerce_api_enabled' ), + 'weight_unit' => get_option( 'woocommerce_weight_unit' ), + 'dimension_unit' => get_option( 'woocommerce_dimension_unit' ), + 'download_method' => get_option( 'woocommerce_file_download_method' ), + 'download_require_login' => get_option( 'woocommerce_downloads_require_login' ), + 'calc_taxes' => get_option( 'woocommerce_calc_taxes' ), + 'coupons_enabled' => get_option( 'woocommerce_enable_coupons' ), + 'guest_checkout' => get_option( 'woocommerce_enable_guest_checkout' ), + 'checkout_login_reminder' => get_option( 'woocommerce_enable_checkout_login_reminder' ), + 'secure_checkout' => get_option( 'woocommerce_force_ssl_checkout' ), + 'enable_signup_and_login_from_checkout' => get_option( 'woocommerce_enable_signup_and_login_from_checkout' ), + 'enable_myaccount_registration' => get_option( 'woocommerce_enable_myaccount_registration' ), + 'registration_generate_username' => get_option( 'woocommerce_registration_generate_username' ), + 'registration_generate_password' => get_option( 'woocommerce_registration_generate_password' ), + ); + } + + /** + * Look for any template override and return filenames. + * + * @return array + */ + private static function get_all_template_overrides() { + $override_data = array(); + $template_paths = apply_filters( 'woocommerce_template_overrides_scan_paths', array( 'WooCommerce' => WC()->plugin_path() . '/templates/' ) ); + $scanned_files = array(); + + require_once WC()->plugin_path() . '/includes/admin/class-wc-admin-status.php'; + + foreach ( $template_paths as $plugin_name => $template_path ) { + $scanned_files[ $plugin_name ] = WC_Admin_Status::scan_template_files( $template_path ); + } + + foreach ( $scanned_files as $plugin_name => $files ) { + foreach ( $files as $file ) { + if ( file_exists( get_stylesheet_directory() . '/' . $file ) ) { + $theme_file = get_stylesheet_directory() . '/' . $file; + } elseif ( file_exists( get_stylesheet_directory() . '/' . WC()->template_path() . $file ) ) { + $theme_file = get_stylesheet_directory() . '/' . WC()->template_path() . $file; + } elseif ( file_exists( get_template_directory() . '/' . $file ) ) { + $theme_file = get_template_directory() . '/' . $file; + } elseif ( file_exists( get_template_directory() . '/' . WC()->template_path() . $file ) ) { + $theme_file = get_template_directory() . '/' . WC()->template_path() . $file; + } else { + $theme_file = false; + } + + if ( false !== $theme_file ) { + $override_data[] = basename( $theme_file ); + } + } + } + return $override_data; + } + + /** + * Search a specific post for text content. + * + * @param integer $post_id The id of the post to search. + * @param string $text The text to search for. + * @return string 'Yes' if post contains $text (otherwise 'No'). + */ + public static function post_contains_text( $post_id, $text ) { + global $wpdb; + + // Search for the text anywhere in the post. + $wildcarded = "%{$text}%"; + + $result = $wpdb->get_var( + $wpdb->prepare( + " + SELECT COUNT( * ) FROM {$wpdb->prefix}posts + WHERE ID=%d + AND {$wpdb->prefix}posts.post_content LIKE %s + ", + array( $post_id, $wildcarded ) + ) + ); + + return ( '0' !== $result ) ? 'Yes' : 'No'; + } + + + /** + * Get tracker data for a specific block type on a woocommerce page. + * + * @param string $block_name The name (id) of a block, e.g. `woocommerce/cart`. + * @param string $woo_page_name The woo page to search, e.g. `cart`. + * @return array Associative array of tracker data with keys: + * - page_contains_block + * - block_attributes + */ + public static function get_block_tracker_data( $block_name, $woo_page_name ) { + $blocks = WC_Blocks_Utils::get_blocks_from_page( $block_name, $woo_page_name ); + + $block_present = false; + $attributes = array(); + if ( $blocks && count( $blocks ) ) { + // Return any customised attributes from the first block. + $block_present = true; + $attributes = $blocks[0]['attrs']; + } + + return array( + 'page_contains_block' => $block_present ? 'Yes' : 'No', + 'block_attributes' => $attributes, + ); + } + + /** + * Get info about the cart & checkout pages. + * + * @return array + */ + public static function get_cart_checkout_info() { + $cart_page_id = wc_get_page_id( 'cart' ); + $checkout_page_id = wc_get_page_id( 'checkout' ); + + $cart_block_data = self::get_block_tracker_data( 'woocommerce/cart', 'cart' ); + $checkout_block_data = self::get_block_tracker_data( 'woocommerce/checkout', 'checkout' ); + + return array( + 'cart_page_contains_cart_shortcode' => self::post_contains_text( + $cart_page_id, + '[woocommerce_cart]' + ), + 'checkout_page_contains_checkout_shortcode' => self::post_contains_text( + $checkout_page_id, + '[woocommerce_checkout]' + ), + + 'cart_page_contains_cart_block' => $cart_block_data['page_contains_block'], + 'cart_block_attributes' => $cart_block_data['block_attributes'], + 'checkout_page_contains_checkout_block' => $checkout_block_data['page_contains_block'], + 'checkout_block_attributes' => $checkout_block_data['block_attributes'], + ); + } + + /** + * Get info about WooCommerce Mobile App usage + * + * @return array + */ + public static function get_woocommerce_mobile_usage() { + return get_option( 'woocommerce_mobile_app_usage' ); + } +} + +WC_Tracker::init(); diff --git a/includes/class-wc-validation.php b/plugins/woocommerce/includes/class-wc-validation.php similarity index 100% rename from includes/class-wc-validation.php rename to plugins/woocommerce/includes/class-wc-validation.php diff --git a/includes/class-wc-webhook.php b/plugins/woocommerce/includes/class-wc-webhook.php similarity index 100% rename from includes/class-wc-webhook.php rename to plugins/woocommerce/includes/class-wc-webhook.php diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php new file mode 100644 index 00000000000..00bdccc6ba8 --- /dev/null +++ b/plugins/woocommerce/includes/class-woocommerce.php @@ -0,0 +1,1017 @@ +$key(); + } + } + + /** + * WooCommerce Constructor. + */ + public function __construct() { + $this->define_constants(); + $this->define_tables(); + $this->includes(); + $this->init_hooks(); + } + + /** + * When WP has loaded all plugins, trigger the `woocommerce_loaded` hook. + * + * This ensures `woocommerce_loaded` is called only after all other plugins + * are loaded, to avoid issues caused by plugin directory naming changing + * the load order. See #21524 for details. + * + * @since 3.6.0 + */ + public function on_plugins_loaded() { + do_action( 'woocommerce_loaded' ); + } + + /** + * Hook into actions and filters. + * + * @since 2.3 + */ + private function init_hooks() { + register_activation_hook( WC_PLUGIN_FILE, array( 'WC_Install', 'install' ) ); + register_shutdown_function( array( $this, 'log_errors' ) ); + + add_action( 'plugins_loaded', array( $this, 'on_plugins_loaded' ), -1 ); + add_action( 'admin_notices', array( $this, 'build_dependencies_notice' ) ); + add_action( 'after_setup_theme', array( $this, 'setup_environment' ) ); + add_action( 'after_setup_theme', array( $this, 'include_template_functions' ), 11 ); + add_action( 'init', array( $this, 'init' ), 0 ); + add_action( 'init', array( 'WC_Shortcodes', 'init' ) ); + add_action( 'init', array( 'WC_Emails', 'init_transactional_emails' ) ); + add_action( 'init', array( $this, 'add_image_sizes' ) ); + add_action( 'init', array( $this, 'load_rest_api' ) ); + add_action( 'switch_blog', array( $this, 'wpdb_table_fix' ), 0 ); + add_action( 'activated_plugin', array( $this, 'activated_plugin' ) ); + add_action( 'deactivated_plugin', array( $this, 'deactivated_plugin' ) ); + add_action( 'woocommerce_installed', array( $this, 'add_woocommerce_inbox_variant' ) ); + add_action( 'woocommerce_updated', array( $this, 'add_woocommerce_inbox_variant' ) ); + + // These classes set up hooks on instantiation. + wc_get_container()->get( ProductDownloadDirectories::class ); + wc_get_container()->get( DownloadPermissionsAdjuster::class ); + wc_get_container()->get( AssignDefaultCategory::class ); + wc_get_container()->get( DataRegenerator::class ); + wc_get_container()->get( LookupDataStore::class ); + wc_get_container()->get( RestockRefundedItemsAdjuster::class ); + wc_get_container()->get( CustomOrdersTableController::class ); + } + + /** + * Add woocommerce_inbox_variant for the Remote Inbox Notification. + * + * P2 post can be found at https://wp.me/paJDYF-1uJ. + */ + public function add_woocommerce_inbox_variant() { + $config_name = 'woocommerce_inbox_variant_assignment'; + if ( false === get_option( $config_name, false ) ) { + update_option( $config_name, wp_rand( 1, 12 ) ); + } + } + /** + * Ensures fatal errors are logged so they can be picked up in the status report. + * + * @since 3.2.0 + */ + public function log_errors() { + $error = error_get_last(); + if ( $error && in_array( $error['type'], array( E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ), true ) ) { + $logger = wc_get_logger(); + $logger->critical( + /* translators: 1: error message 2: file name and path 3: line number */ + sprintf( __( '%1$s in %2$s on line %3$s', 'woocommerce' ), $error['message'], $error['file'], $error['line'] ) . PHP_EOL, + array( + 'source' => 'fatal-errors', + ) + ); + do_action( 'woocommerce_shutdown_error', $error ); + } + } + + /** + * Define WC Constants. + */ + private function define_constants() { + $upload_dir = wp_upload_dir( null, false ); + + $this->define( 'WC_ABSPATH', dirname( WC_PLUGIN_FILE ) . '/' ); + $this->define( 'WC_PLUGIN_BASENAME', plugin_basename( WC_PLUGIN_FILE ) ); + $this->define( 'WC_VERSION', $this->version ); + $this->define( 'WOOCOMMERCE_VERSION', $this->version ); + $this->define( 'WC_ROUNDING_PRECISION', 6 ); + $this->define( 'WC_DISCOUNT_ROUNDING_MODE', 2 ); + $this->define( 'WC_TAX_ROUNDING_MODE', 'yes' === get_option( 'woocommerce_prices_include_tax', 'no' ) ? 2 : 1 ); + $this->define( 'WC_DELIMITER', '|' ); + $this->define( 'WC_LOG_DIR', $upload_dir['basedir'] . '/wc-logs/' ); + $this->define( 'WC_SESSION_CACHE_GROUP', 'wc_session_id' ); + $this->define( 'WC_TEMPLATE_DEBUG_MODE', false ); + $this->define( 'WC_NOTICE_MIN_PHP_VERSION', '7.2' ); + $this->define( 'WC_NOTICE_MIN_WP_VERSION', '5.2' ); + $this->define( 'WC_PHP_MIN_REQUIREMENTS_NOTICE', 'wp_php_min_requirements_' . WC_NOTICE_MIN_PHP_VERSION . '_' . WC_NOTICE_MIN_WP_VERSION ); + /** Define if we're checking against major, minor or no versions in the following places: + * - plugin screen in WP Admin (displaying extra warning when updating to new major versions) + * - System Status Report ('Installed version not tested with active version of WooCommerce' warning) + * - core update screen in WP Admin (displaying extra warning when updating to new major versions) + * - enable/disable automated updates in the plugin screen in WP Admin (if there are any plugins + * that don't declare compatibility, the auto-update is disabled) + * + * We dropped SemVer before WC 5.0, so all versions are backwards compatible now, thus no more check needed. + * The SSR in the name is preserved for bw compatibility, as this was initially used in System Status Report. + */ + $this->define( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE', 'none' ); + + } + + /** + * Register custom tables within $wpdb object. + */ + private function define_tables() { + global $wpdb; + + // List of tables without prefixes. + $tables = array( + 'payment_tokenmeta' => 'woocommerce_payment_tokenmeta', + 'order_itemmeta' => 'woocommerce_order_itemmeta', + 'wc_product_meta_lookup' => 'wc_product_meta_lookup', + 'wc_tax_rate_classes' => 'wc_tax_rate_classes', + 'wc_reserved_stock' => 'wc_reserved_stock', + ); + + foreach ( $tables as $name => $table ) { + $wpdb->$name = $wpdb->prefix . $table; + $wpdb->tables[] = $table; + } + } + + /** + * Define constant if not already set. + * + * @param string $name Constant name. + * @param string|bool $value Constant value. + */ + private function define( $name, $value ) { + if ( ! defined( $name ) ) { + define( $name, $value ); + } + } + + /** + * Returns true if the request is a non-legacy REST API request. + * + * Legacy REST requests should still run some extra code for backwards compatibility. + * + * @todo: replace this function once core WP function is available: https://core.trac.wordpress.org/ticket/42061. + * + * @return bool + */ + public function is_rest_api_request() { + if ( empty( $_SERVER['REQUEST_URI'] ) ) { + return false; + } + + $rest_prefix = trailingslashit( rest_get_url_prefix() ); + $is_rest_api_request = ( false !== strpos( $_SERVER['REQUEST_URI'], $rest_prefix ) ); // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + return apply_filters( 'woocommerce_is_rest_api_request', $is_rest_api_request ); + } + + /** + * Load REST API. + */ + public function load_rest_api() { + \Automattic\WooCommerce\RestApi\Server::instance()->init(); + } + + /** + * What type of request is this? + * + * @param string $type admin, ajax, cron or frontend. + * @return bool + */ + private function is_request( $type ) { + switch ( $type ) { + case 'admin': + return is_admin(); + case 'ajax': + return defined( 'DOING_AJAX' ); + case 'cron': + return defined( 'DOING_CRON' ); + case 'frontend': + return ( ! is_admin() || defined( 'DOING_AJAX' ) ) && ! defined( 'DOING_CRON' ) && ! $this->is_rest_api_request(); + } + } + + /** + * Include required core files used in admin and on the frontend. + */ + public function includes() { + /** + * Class autoloader. + */ + include_once WC_ABSPATH . 'includes/class-wc-autoloader.php'; + + /** + * Interfaces. + */ + include_once WC_ABSPATH . 'includes/interfaces/class-wc-abstract-order-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-coupon-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-customer-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-customer-download-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-customer-download-log-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-object-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-item-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-item-product-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-item-type-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-refund-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-payment-token-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-product-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-product-variable-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-shipping-zone-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-logger-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-log-handler-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-webhooks-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-queue-interface.php'; + + /** + * Core traits. + */ + include_once WC_ABSPATH . 'includes/traits/trait-wc-item-totals.php'; + + /** + * Abstract classes. + */ + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-data.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-object-query.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-payment-token.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-product.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-order.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-settings-api.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-shipping-method.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-payment-gateway.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-integration.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-log-handler.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-deprecated-hooks.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-session.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-privacy.php'; + + /** + * Core classes. + */ + include_once WC_ABSPATH . 'includes/wc-core-functions.php'; + include_once WC_ABSPATH . 'includes/class-wc-datetime.php'; + include_once WC_ABSPATH . 'includes/class-wc-post-types.php'; + include_once WC_ABSPATH . 'includes/class-wc-install.php'; + include_once WC_ABSPATH . 'includes/class-wc-geolocation.php'; + include_once WC_ABSPATH . 'includes/class-wc-download-handler.php'; + include_once WC_ABSPATH . 'includes/class-wc-comments.php'; + include_once WC_ABSPATH . 'includes/class-wc-post-data.php'; + include_once WC_ABSPATH . 'includes/class-wc-ajax.php'; + include_once WC_ABSPATH . 'includes/class-wc-emails.php'; + include_once WC_ABSPATH . 'includes/class-wc-data-exception.php'; + include_once WC_ABSPATH . 'includes/class-wc-query.php'; + include_once WC_ABSPATH . 'includes/class-wc-meta-data.php'; + include_once WC_ABSPATH . 'includes/class-wc-order-factory.php'; + include_once WC_ABSPATH . 'includes/class-wc-order-query.php'; + include_once WC_ABSPATH . 'includes/class-wc-product-factory.php'; + include_once WC_ABSPATH . 'includes/class-wc-product-query.php'; + include_once WC_ABSPATH . 'includes/class-wc-payment-tokens.php'; + include_once WC_ABSPATH . 'includes/class-wc-shipping-zone.php'; + include_once WC_ABSPATH . 'includes/gateways/class-wc-payment-gateway-cc.php'; + include_once WC_ABSPATH . 'includes/gateways/class-wc-payment-gateway-echeck.php'; + include_once WC_ABSPATH . 'includes/class-wc-countries.php'; + include_once WC_ABSPATH . 'includes/class-wc-integrations.php'; + include_once WC_ABSPATH . 'includes/class-wc-cache-helper.php'; + include_once WC_ABSPATH . 'includes/class-wc-https.php'; + include_once WC_ABSPATH . 'includes/class-wc-deprecated-action-hooks.php'; + include_once WC_ABSPATH . 'includes/class-wc-deprecated-filter-hooks.php'; + include_once WC_ABSPATH . 'includes/class-wc-background-emailer.php'; + include_once WC_ABSPATH . 'includes/class-wc-discounts.php'; + include_once WC_ABSPATH . 'includes/class-wc-cart-totals.php'; + include_once WC_ABSPATH . 'includes/customizer/class-wc-shop-customizer.php'; + include_once WC_ABSPATH . 'includes/class-wc-regenerate-images.php'; + include_once WC_ABSPATH . 'includes/class-wc-privacy.php'; + include_once WC_ABSPATH . 'includes/class-wc-structured-data.php'; + include_once WC_ABSPATH . 'includes/class-wc-shortcodes.php'; + include_once WC_ABSPATH . 'includes/class-wc-logger.php'; + include_once WC_ABSPATH . 'includes/queue/class-wc-action-queue.php'; + include_once WC_ABSPATH . 'includes/queue/class-wc-queue.php'; + include_once WC_ABSPATH . 'includes/admin/marketplace-suggestions/class-wc-marketplace-updater.php'; + include_once WC_ABSPATH . 'includes/blocks/class-wc-blocks-utils.php'; + + /** + * Data stores - used to store and retrieve CRUD object data from the database. + */ + include_once WC_ABSPATH . 'includes/class-wc-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-data-store-wp.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-coupon-data-store-cpt.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-data-store-cpt.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-grouped-data-store-cpt.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-variable-data-store-cpt.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-variation-data-store-cpt.php'; + include_once WC_ABSPATH . 'includes/data-stores/abstract-wc-order-item-type-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-coupon-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-fee-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-product-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-shipping-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-tax-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-payment-token-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-data-store-session.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-download-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-download-log-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-shipping-zone-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/abstract-wc-order-data-store-cpt.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-data-store-cpt.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-refund-data-store-cpt.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-webhook-data-store.php'; + + /** + * REST API. + */ + include_once WC_ABSPATH . 'includes/legacy/class-wc-legacy-api.php'; + include_once WC_ABSPATH . 'includes/class-wc-api.php'; + include_once WC_ABSPATH . 'includes/class-wc-rest-authentication.php'; + include_once WC_ABSPATH . 'includes/class-wc-rest-exception.php'; + include_once WC_ABSPATH . 'includes/class-wc-auth.php'; + include_once WC_ABSPATH . 'includes/class-wc-register-wp-admin-settings.php'; + + /** + * WCCOM Site. + */ + include_once WC_ABSPATH . 'includes/wccom-site/class-wc-wccom-site.php'; + + /** + * Libraries and packages. + */ + include_once WC_ABSPATH . 'packages/action-scheduler/action-scheduler.php'; + + if ( defined( 'WP_CLI' ) && WP_CLI ) { + include_once WC_ABSPATH . 'includes/class-wc-cli.php'; + } + + if ( $this->is_request( 'admin' ) ) { + include_once WC_ABSPATH . 'includes/admin/class-wc-admin.php'; + } + + if ( $this->is_request( 'frontend' ) ) { + $this->frontend_includes(); + } + + if ( $this->is_request( 'cron' ) && 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ) ) { + include_once WC_ABSPATH . 'includes/class-wc-tracker.php'; + } + + $this->theme_support_includes(); + $this->query = new WC_Query(); + $this->api = new WC_API(); + $this->api->init(); + } + + /** + * Include classes for theme support. + * + * @since 3.3.0 + */ + private function theme_support_includes() { + if ( wc_is_wp_default_theme_active() ) { + switch ( get_template() ) { + case 'twentyten': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-ten.php'; + break; + case 'twentyeleven': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-eleven.php'; + break; + case 'twentytwelve': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twelve.php'; + break; + case 'twentythirteen': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-thirteen.php'; + break; + case 'twentyfourteen': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-fourteen.php'; + break; + case 'twentyfifteen': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-fifteen.php'; + break; + case 'twentysixteen': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-sixteen.php'; + break; + case 'twentyseventeen': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-seventeen.php'; + break; + case 'twentynineteen': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-nineteen.php'; + break; + case 'twentytwenty': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twenty.php'; + break; + case 'twentytwentyone': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twenty-one.php'; + break; + case 'twentytwentytwo': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twenty-two.php'; + break; + } + } + } + + /** + * Include required frontend files. + */ + public function frontend_includes() { + include_once WC_ABSPATH . 'includes/wc-cart-functions.php'; + include_once WC_ABSPATH . 'includes/wc-notice-functions.php'; + include_once WC_ABSPATH . 'includes/wc-template-hooks.php'; + include_once WC_ABSPATH . 'includes/class-wc-template-loader.php'; + include_once WC_ABSPATH . 'includes/class-wc-frontend-scripts.php'; + include_once WC_ABSPATH . 'includes/class-wc-form-handler.php'; + include_once WC_ABSPATH . 'includes/class-wc-cart.php'; + include_once WC_ABSPATH . 'includes/class-wc-tax.php'; + include_once WC_ABSPATH . 'includes/class-wc-shipping-zones.php'; + include_once WC_ABSPATH . 'includes/class-wc-customer.php'; + include_once WC_ABSPATH . 'includes/class-wc-embed.php'; + include_once WC_ABSPATH . 'includes/class-wc-session-handler.php'; + } + + /** + * Function used to Init WooCommerce Template Functions - This makes them pluggable by plugins and themes. + */ + public function include_template_functions() { + include_once WC_ABSPATH . 'includes/wc-template-functions.php'; + } + + /** + * Init WooCommerce when WordPress Initialises. + */ + public function init() { + // Before init action. + do_action( 'before_woocommerce_init' ); + + // Set up localisation. + $this->load_plugin_textdomain(); + + // Load class instances. + $this->product_factory = new WC_Product_Factory(); + $this->order_factory = new WC_Order_Factory(); + $this->countries = new WC_Countries(); + $this->integrations = new WC_Integrations(); + $this->structured_data = new WC_Structured_Data(); + $this->deprecated_hook_handlers['actions'] = new WC_Deprecated_Action_Hooks(); + $this->deprecated_hook_handlers['filters'] = new WC_Deprecated_Filter_Hooks(); + + // Classes/actions loaded for the frontend and for ajax requests. + if ( $this->is_request( 'frontend' ) ) { + wc_load_cart(); + } + + $this->load_webhooks(); + + // Init action. + do_action( 'woocommerce_init' ); + } + + /** + * Load Localisation files. + * + * Note: the first-loaded translation file overrides any following ones if the same translation is present. + * + * Locales found in: + * - WP_LANG_DIR/woocommerce/woocommerce-LOCALE.mo + * - WP_LANG_DIR/plugins/woocommerce-LOCALE.mo + */ + public function load_plugin_textdomain() { + $locale = determine_locale(); + $locale = apply_filters( 'plugin_locale', $locale, 'woocommerce' ); + + unload_textdomain( 'woocommerce' ); + load_textdomain( 'woocommerce', WP_LANG_DIR . '/woocommerce/woocommerce-' . $locale . '.mo' ); + load_plugin_textdomain( 'woocommerce', false, plugin_basename( dirname( WC_PLUGIN_FILE ) ) . '/i18n/languages' ); + } + + /** + * Ensure theme and server variable compatibility and setup image sizes. + */ + public function setup_environment() { + /** + * WC_TEMPLATE_PATH constant. + * + * @deprecated 2.2 Use WC()->template_path() instead. + */ + $this->define( 'WC_TEMPLATE_PATH', $this->template_path() ); + + $this->add_thumbnail_support(); + } + + /** + * Ensure post thumbnail support is turned on. + */ + private function add_thumbnail_support() { + if ( ! current_theme_supports( 'post-thumbnails' ) ) { + add_theme_support( 'post-thumbnails' ); + } + add_post_type_support( 'product', 'thumbnail' ); + } + + /** + * Add WC Image sizes to WP. + * + * As of 3.3, image sizes can be registered via themes using add_theme_support for woocommerce + * and defining an array of args. If these are not defined, we will use defaults. This is + * handled in wc_get_image_size function. + * + * 3.3 sizes: + * + * woocommerce_thumbnail - Used in product listings. We assume these work for a 3 column grid layout. + * woocommerce_single - Used on single product pages for the main image. + * + * @since 2.3 + */ + public function add_image_sizes() { + $thumbnail = wc_get_image_size( 'thumbnail' ); + $single = wc_get_image_size( 'single' ); + $gallery_thumbnail = wc_get_image_size( 'gallery_thumbnail' ); + + add_image_size( 'woocommerce_thumbnail', $thumbnail['width'], $thumbnail['height'], $thumbnail['crop'] ); + add_image_size( 'woocommerce_single', $single['width'], $single['height'], $single['crop'] ); + add_image_size( 'woocommerce_gallery_thumbnail', $gallery_thumbnail['width'], $gallery_thumbnail['height'], $gallery_thumbnail['crop'] ); + + /** + * Legacy image sizes. + * + * @deprecated 3.3.0 These sizes will be removed in 4.6.0. + */ + add_image_size( 'shop_catalog', $thumbnail['width'], $thumbnail['height'], $thumbnail['crop'] ); + add_image_size( 'shop_single', $single['width'], $single['height'], $single['crop'] ); + add_image_size( 'shop_thumbnail', $gallery_thumbnail['width'], $gallery_thumbnail['height'], $gallery_thumbnail['crop'] ); + } + + /** + * Get the plugin url. + * + * @return string + */ + public function plugin_url() { + return untrailingslashit( plugins_url( '/', WC_PLUGIN_FILE ) ); + } + + /** + * Get the plugin path. + * + * @return string + */ + public function plugin_path() { + return untrailingslashit( plugin_dir_path( WC_PLUGIN_FILE ) ); + } + + /** + * Get the template path. + * + * @return string + */ + public function template_path() { + return apply_filters( 'woocommerce_template_path', 'woocommerce/' ); + } + + /** + * Get Ajax URL. + * + * @return string + */ + public function ajax_url() { + return admin_url( 'admin-ajax.php', 'relative' ); + } + + /** + * Return the WC API URL for a given request. + * + * @param string $request Requested endpoint. + * @param bool|null $ssl If should use SSL, null if should auto detect. Default: null. + * @return string + */ + public function api_request_url( $request, $ssl = null ) { + if ( is_null( $ssl ) ) { + $scheme = wp_parse_url( home_url(), PHP_URL_SCHEME ); + } elseif ( $ssl ) { + $scheme = 'https'; + } else { + $scheme = 'http'; + } + + if ( strstr( get_option( 'permalink_structure' ), '/index.php/' ) ) { + $api_request_url = trailingslashit( home_url( '/index.php/wc-api/' . $request, $scheme ) ); + } elseif ( get_option( 'permalink_structure' ) ) { + $api_request_url = trailingslashit( home_url( '/wc-api/' . $request, $scheme ) ); + } else { + $api_request_url = add_query_arg( 'wc-api', $request, trailingslashit( home_url( '', $scheme ) ) ); + } + + return esc_url_raw( apply_filters( 'woocommerce_api_request_url', $api_request_url, $request, $ssl ) ); + } + + /** + * Load & enqueue active webhooks. + * + * @since 2.2 + */ + private function load_webhooks() { + + if ( ! is_blog_installed() ) { + return; + } + + /** + * Hook: woocommerce_load_webhooks_limit. + * + * @since 3.6.0 + * @param int $limit Used to limit how many webhooks are loaded. Default: no limit. + */ + $limit = apply_filters( 'woocommerce_load_webhooks_limit', null ); + + wc_load_webhooks( 'active', $limit ); + } + + /** + * Initialize the customer and cart objects and setup customer saving on shutdown. + * + * @since 3.6.4 + * @return void + */ + public function initialize_cart() { + // Cart needs customer info. + if ( is_null( $this->customer ) || ! $this->customer instanceof WC_Customer ) { + $this->customer = new WC_Customer( get_current_user_id(), true ); + // Customer should be saved during shutdown. + add_action( 'shutdown', array( $this->customer, 'save' ), 10 ); + } + if ( is_null( $this->cart ) || ! $this->cart instanceof WC_Cart ) { + $this->cart = new WC_Cart(); + } + } + + /** + * Initialize the session class. + * + * @since 3.6.4 + * @return void + */ + public function initialize_session() { + // Session class, handles session data for users - can be overwritten if custom handler is needed. + $session_class = apply_filters( 'woocommerce_session_handler', 'WC_Session_Handler' ); + if ( is_null( $this->session ) || ! $this->session instanceof $session_class ) { + $this->session = new $session_class(); + $this->session->init(); + } + } + + /** + * Set tablenames inside WPDB object. + */ + public function wpdb_table_fix() { + $this->define_tables(); + } + + /** + * Ran when any plugin is activated. + * + * @since 3.6.0 + * @param string $filename The filename of the activated plugin. + */ + public function activated_plugin( $filename ) { + include_once dirname( __FILE__ ) . '/admin/helper/class-wc-helper.php'; + + if ( '/woocommerce.php' === substr( $filename, -16 ) ) { + set_transient( 'woocommerce_activated_plugin', $filename ); + } + + WC_Helper::activated_plugin( $filename ); + } + + /** + * Ran when any plugin is deactivated. + * + * @since 3.6.0 + * @param string $filename The filename of the deactivated plugin. + */ + public function deactivated_plugin( $filename ) { + include_once dirname( __FILE__ ) . '/admin/helper/class-wc-helper.php'; + + WC_Helper::deactivated_plugin( $filename ); + } + + /** + * Get queue instance. + * + * @return WC_Queue_Interface + */ + public function queue() { + return WC_Queue::instance(); + } + + /** + * Get Checkout Class. + * + * @return WC_Checkout + */ + public function checkout() { + return WC_Checkout::instance(); + } + + /** + * Get gateways class. + * + * @return WC_Payment_Gateways + */ + public function payment_gateways() { + return WC_Payment_Gateways::instance(); + } + + /** + * Get shipping class. + * + * @return WC_Shipping + */ + public function shipping() { + return WC_Shipping::instance(); + } + + /** + * Email Class. + * + * @return WC_Emails + */ + public function mailer() { + return WC_Emails::instance(); + } + + /** + * Check if plugin assets are built and minified + * + * @return bool + */ + public function build_dependencies_satisfied() { + // Check if we have compiled CSS. + if ( ! file_exists( WC()->plugin_path() . '/assets/css/admin.css' ) ) { + return false; + } + + // Check if we have minified JS. + if ( ! file_exists( WC()->plugin_path() . '/assets/js/admin/woocommerce_admin.min.js' ) ) { + return false; + } + + return true; + } + + /** + * Output a admin notice when build dependencies not met. + * + * @return void + */ + public function build_dependencies_notice() { + if ( $this->build_dependencies_satisfied() ) { + return; + } + + $message_one = __( 'You have installed a development version of WooCommerce which requires files to be built and minified. From the plugin directory, run pnpm install and then pnpm nx build woocommerce-legacy-assets to build and minify assets.', 'woocommerce' ); + $message_two = sprintf( + /* translators: 1: URL of WordPress.org Repository 2: URL of the GitHub Repository release page */ + __( 'Or you can download a pre-built version of the plugin from the WordPress.org repository or by visiting the releases page in the GitHub repository.', 'woocommerce' ), + 'https://wordpress.org/plugins/woocommerce/', + 'https://github.com/woocommerce/woocommerce/releases' + ); + printf( '

    %s %s

    ', $message_one, $message_two ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * Is the WooCommerce Admin actively included in the WooCommerce core? + * Based on presence of a basic WC Admin function. + * + * @return boolean + */ + public function is_wc_admin_active() { + return function_exists( 'wc_admin_url' ); + } + + /** + * Call a user function. This should be used to execute any non-idempotent function, especially + * those in the `includes` directory or provided by WordPress. + * + * This method can be useful for unit tests, since functions called using this method + * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_function_mocks. + * + * @param string $function_name The function to execute. + * @param mixed ...$parameters The parameters to pass to the function. + * + * @return mixed The result from the function. + * + * @since 4.4 + */ + public function call_function( $function_name, ...$parameters ) { + return wc_get_container()->get( LegacyProxy::class )->call_function( $function_name, ...$parameters ); + } + + /** + * Call a static method in a class. This should be used to execute any non-idempotent method in classes + * from the `includes` directory. + * + * This method can be useful for unit tests, since methods called using this method + * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_static_mocks. + * + * @param string $class_name The name of the class containing the method. + * @param string $method_name The name of the method. + * @param mixed ...$parameters The parameters to pass to the method. + * + * @return mixed The result from the method. + * + * @since 4.4 + */ + public function call_static( $class_name, $method_name, ...$parameters ) { + return wc_get_container()->get( LegacyProxy::class )->call_static( $class_name, $method_name, ...$parameters ); + } + + /** + * Gets an instance of a given legacy class. + * This must not be used to get instances of classes in the `src` directory. + * + * This method can be useful for unit tests, since objects obtained using this method + * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_class_mocks. + * + * @param string $class_name The name of the class to get an instance for. + * @param mixed ...$args Parameters to be passed to the class constructor or to the appropriate internal 'get_instance_of_' method. + * + * @return object The instance of the class. + * @throws \Exception The requested class belongs to the `src` directory, or there was an error creating an instance of the class. + * + * @since 4.4 + */ + public function get_instance_of( string $class_name, ...$args ) { + return wc_get_container()->get( LegacyProxy::class )->get_instance_of( $class_name, ...$args ); + } +} diff --git a/includes/cli/class-wc-cli-rest-command.php b/plugins/woocommerce/includes/cli/class-wc-cli-rest-command.php similarity index 100% rename from includes/cli/class-wc-cli-rest-command.php rename to plugins/woocommerce/includes/cli/class-wc-cli-rest-command.php diff --git a/includes/cli/class-wc-cli-runner.php b/plugins/woocommerce/includes/cli/class-wc-cli-runner.php similarity index 100% rename from includes/cli/class-wc-cli-runner.php rename to plugins/woocommerce/includes/cli/class-wc-cli-runner.php diff --git a/includes/cli/class-wc-cli-tool-command.php b/plugins/woocommerce/includes/cli/class-wc-cli-tool-command.php similarity index 100% rename from includes/cli/class-wc-cli-tool-command.php rename to plugins/woocommerce/includes/cli/class-wc-cli-tool-command.php diff --git a/plugins/woocommerce/includes/cli/class-wc-cli-tracker-command.php b/plugins/woocommerce/includes/cli/class-wc-cli-tracker-command.php new file mode 100644 index 00000000000..d5b8155b57c --- /dev/null +++ b/plugins/woocommerce/includes/cli/class-wc-cli-tracker-command.php @@ -0,0 +1,55 @@ +] + * : Render output in a particular format, see WP_CLI\Formatter for details. + * + * @see \WP_CLI\Formatter + * @see WC_Tracker::get_tracking_data() + * @param array $args WP-CLI positional arguments. + * @param array $assoc_args WP-CLI associative arguments. + */ + public static function show_tracker_snapshot( $args, $assoc_args ) { + $snapshot_data = WC_Tracker::get_tracking_data(); + + $formatter = new \WP_CLI\Formatter( + $assoc_args, + array_keys( $snapshot_data ) + ); + + $formatter->display_items( array( $snapshot_data ) ); + } +} diff --git a/includes/cli/class-wc-cli-update-command.php b/plugins/woocommerce/includes/cli/class-wc-cli-update-command.php similarity index 100% rename from includes/cli/class-wc-cli-update-command.php rename to plugins/woocommerce/includes/cli/class-wc-cli-update-command.php diff --git a/includes/customizer/class-wc-customizer-control-cropping.php b/plugins/woocommerce/includes/customizer/class-wc-customizer-control-cropping.php similarity index 100% rename from includes/customizer/class-wc-customizer-control-cropping.php rename to plugins/woocommerce/includes/customizer/class-wc-customizer-control-cropping.php diff --git a/plugins/woocommerce/includes/customizer/class-wc-shop-customizer.php b/plugins/woocommerce/includes/customizer/class-wc-shop-customizer.php new file mode 100644 index 00000000000..2170ff6d210 --- /dev/null +++ b/plugins/woocommerce/includes/customizer/class-wc-shop-customizer.php @@ -0,0 +1,888 @@ +add_panel( + 'woocommerce', + array( + 'priority' => 200, + 'capability' => 'manage_woocommerce', + 'theme_supports' => '', + 'title' => __( 'WooCommerce', 'woocommerce' ), + ) + ); + + $this->add_store_notice_section( $wp_customize ); + $this->add_product_catalog_section( $wp_customize ); + $this->add_product_images_section( $wp_customize ); + $this->add_checkout_section( $wp_customize ); + } + + /** + * Frontend CSS styles. + */ + public function add_frontend_scripts() { + if ( ! is_customize_preview() || ! is_store_notice_showing() ) { + return; + } + + $css = '.woocommerce-store-notice, p.demo_store { display: block !important; }'; + wp_add_inline_style( 'customize-preview', $css ); + } + + /** + * CSS styles to improve our form. + */ + public function add_styles() { + ?> + + + + __( 'Default sorting (custom ordering + name)', 'woocommerce' ), + 'popularity' => __( 'Popularity (sales)', 'woocommerce' ), + 'rating' => __( 'Average rating', 'woocommerce' ), + 'date' => __( 'Sort by most recent', 'woocommerce' ), + 'price' => __( 'Sort by price (asc)', 'woocommerce' ), + 'price-desc' => __( 'Sort by price (desc)', 'woocommerce' ), + ) + ); + + return array_key_exists( $value, $options ) ? $value : 'menu_order'; + } + + /** + * Store notice section. + * + * @param WP_Customize_Manager $wp_customize Theme Customizer object. + */ + private function add_store_notice_section( $wp_customize ) { + $wp_customize->add_section( + 'woocommerce_store_notice', + array( + 'title' => __( 'Store Notice', 'woocommerce' ), + 'priority' => 10, + 'panel' => 'woocommerce', + ) + ); + + $wp_customize->add_setting( + 'woocommerce_demo_store', + array( + 'default' => 'no', + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'wc_bool_to_string', + 'sanitize_js_callback' => 'wc_string_to_bool', + ) + ); + + $wp_customize->add_setting( + 'woocommerce_demo_store_notice', + array( + 'default' => __( 'This is a demo store for testing purposes — no orders shall be fulfilled.', 'woocommerce' ), + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'wp_kses_post', + 'transport' => 'postMessage', + ) + ); + + $wp_customize->add_control( + 'woocommerce_demo_store_notice', + array( + 'label' => __( 'Store notice', 'woocommerce' ), + 'description' => __( 'If enabled, this text will be shown site-wide. You can use it to show events or promotions to visitors!', 'woocommerce' ), + 'section' => 'woocommerce_store_notice', + 'settings' => 'woocommerce_demo_store_notice', + 'type' => 'textarea', + ) + ); + + $wp_customize->add_control( + 'woocommerce_demo_store', + array( + 'label' => __( 'Enable store notice', 'woocommerce' ), + 'section' => 'woocommerce_store_notice', + 'settings' => 'woocommerce_demo_store', + 'type' => 'checkbox', + ) + ); + + if ( isset( $wp_customize->selective_refresh ) ) { + $wp_customize->selective_refresh->add_partial( + 'woocommerce_demo_store_notice', + array( + 'selector' => '.woocommerce-store-notice', + 'container_inclusive' => true, + 'render_callback' => 'woocommerce_demo_store', + ) + ); + } + } + + /** + * Product catalog section. + * + * @param WP_Customize_Manager $wp_customize Theme Customizer object. + */ + public function add_product_catalog_section( $wp_customize ) { + $wp_customize->add_section( + 'woocommerce_product_catalog', + array( + 'title' => __( 'Product Catalog', 'woocommerce' ), + 'priority' => 10, + 'panel' => 'woocommerce', + ) + ); + + $wp_customize->add_setting( + 'woocommerce_shop_page_display', + array( + 'default' => '', + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => array( $this, 'sanitize_archive_display' ), + ) + ); + + $wp_customize->add_control( + 'woocommerce_shop_page_display', + array( + 'label' => __( 'Shop page display', 'woocommerce' ), + 'description' => __( 'Choose what to display on the main shop page.', 'woocommerce' ), + 'section' => 'woocommerce_product_catalog', + 'settings' => 'woocommerce_shop_page_display', + 'type' => 'select', + 'choices' => array( + '' => __( 'Show products', 'woocommerce' ), + 'subcategories' => __( 'Show categories', 'woocommerce' ), + 'both' => __( 'Show categories & products', 'woocommerce' ), + ), + ) + ); + + $wp_customize->add_setting( + 'woocommerce_category_archive_display', + array( + 'default' => '', + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => array( $this, 'sanitize_archive_display' ), + ) + ); + + $wp_customize->add_control( + 'woocommerce_category_archive_display', + array( + 'label' => __( 'Category display', 'woocommerce' ), + 'description' => __( 'Choose what to display on product category pages.', 'woocommerce' ), + 'section' => 'woocommerce_product_catalog', + 'settings' => 'woocommerce_category_archive_display', + 'type' => 'select', + 'choices' => array( + '' => __( 'Show products', 'woocommerce' ), + 'subcategories' => __( 'Show subcategories', 'woocommerce' ), + 'both' => __( 'Show subcategories & products', 'woocommerce' ), + ), + ) + ); + + $wp_customize->add_setting( + 'woocommerce_default_catalog_orderby', + array( + 'default' => 'menu_order', + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => array( $this, 'sanitize_default_catalog_orderby' ), + ) + ); + + $wp_customize->add_control( + 'woocommerce_default_catalog_orderby', + array( + 'label' => __( 'Default product sorting', 'woocommerce' ), + 'description' => __( 'How should products be sorted in the catalog by default?', 'woocommerce' ), + 'section' => 'woocommerce_product_catalog', + 'settings' => 'woocommerce_default_catalog_orderby', + 'type' => 'select', + 'choices' => apply_filters( + 'woocommerce_default_catalog_orderby_options', + array( + 'menu_order' => __( 'Default sorting (custom ordering + name)', 'woocommerce' ), + 'popularity' => __( 'Popularity (sales)', 'woocommerce' ), + 'rating' => __( 'Average rating', 'woocommerce' ), + 'date' => __( 'Sort by most recent', 'woocommerce' ), + 'price' => __( 'Sort by price (asc)', 'woocommerce' ), + 'price-desc' => __( 'Sort by price (desc)', 'woocommerce' ), + ) + ), + ) + ); + + // The following settings should be hidden if the theme is declaring the values. + if ( has_filter( 'loop_shop_columns' ) ) { + return; + } + + $wp_customize->add_setting( + 'woocommerce_catalog_columns', + array( + 'default' => 4, + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'absint', + 'sanitize_js_callback' => 'absint', + ) + ); + + $wp_customize->add_control( + 'woocommerce_catalog_columns', + array( + 'label' => __( 'Products per row', 'woocommerce' ), + 'description' => __( 'How many products should be shown per row?', 'woocommerce' ), + 'section' => 'woocommerce_product_catalog', + 'settings' => 'woocommerce_catalog_columns', + 'type' => 'number', + 'input_attrs' => array( + 'min' => wc_get_theme_support( 'product_grid::min_columns', 1 ), + 'max' => wc_get_theme_support( 'product_grid::max_columns', '' ), + 'step' => 1, + ), + ) + ); + + // Only add this setting if something else isn't managing the number of products per page. + if ( ! has_filter( 'loop_shop_per_page' ) ) { + $wp_customize->add_setting( + 'woocommerce_catalog_rows', + array( + 'default' => 4, + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'absint', + 'sanitize_js_callback' => 'absint', + ) + ); + } + + $wp_customize->add_control( + 'woocommerce_catalog_rows', + array( + 'label' => __( 'Rows per page', 'woocommerce' ), + 'description' => __( 'How many rows of products should be shown per page?', 'woocommerce' ), + 'section' => 'woocommerce_product_catalog', + 'settings' => 'woocommerce_catalog_rows', + 'type' => 'number', + 'input_attrs' => array( + 'min' => wc_get_theme_support( 'product_grid::min_rows', 1 ), + 'max' => wc_get_theme_support( 'product_grid::max_rows', '' ), + 'step' => 1, + ), + ) + ); + } + + /** + * Product images section. + * + * @param WP_Customize_Manager $wp_customize Theme Customizer object. + */ + private function add_product_images_section( $wp_customize ) { + if ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'photon' ) ) { + $regen_description = ''; // Nothing to report; Jetpack will handle magically. + } elseif ( apply_filters( 'woocommerce_background_image_regeneration', true ) && ! is_multisite() ) { + $regen_description = __( 'After publishing your changes, new image sizes will be generated automatically.', 'woocommerce' ); + } elseif ( apply_filters( 'woocommerce_background_image_regeneration', true ) && is_multisite() ) { + /* translators: 1: tools URL 2: regen thumbs url */ + $regen_description = sprintf( __( 'After publishing your changes, new image sizes may not be shown until you regenerate thumbnails. You can do this from the tools section in WooCommerce or by using a plugin such as Regenerate Thumbnails.', 'woocommerce' ), admin_url( 'admin.php?page=wc-status&tab=tools' ), 'https://en-gb.wordpress.org/plugins/regenerate-thumbnails/' ); + } else { + /* translators: %s: regen thumbs url */ + $regen_description = sprintf( __( 'After publishing your changes, new image sizes may not be shown until you Regenerate Thumbnails.', 'woocommerce' ), 'https://en-gb.wordpress.org/plugins/regenerate-thumbnails/' ); + } + + $wp_customize->add_section( + 'woocommerce_product_images', + array( + 'title' => __( 'Product Images', 'woocommerce' ), + 'description' => $regen_description, + 'priority' => 20, + 'panel' => 'woocommerce', + ) + ); + + if ( ! wc_get_theme_support( 'single_image_width' ) ) { + $wp_customize->add_setting( + 'woocommerce_single_image_width', + array( + 'default' => 600, + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'absint', + 'sanitize_js_callback' => 'absint', + ) + ); + + $wp_customize->add_control( + 'woocommerce_single_image_width', + array( + 'label' => __( 'Main image width', 'woocommerce' ), + 'description' => __( 'Image size used for the main image on single product pages. These images will remain uncropped.', 'woocommerce' ), + 'section' => 'woocommerce_product_images', + 'settings' => 'woocommerce_single_image_width', + 'type' => 'number', + 'input_attrs' => array( + 'min' => 0, + 'step' => 1, + ), + ) + ); + } + + if ( ! wc_get_theme_support( 'thumbnail_image_width' ) ) { + $wp_customize->add_setting( + 'woocommerce_thumbnail_image_width', + array( + 'default' => 300, + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'absint', + 'sanitize_js_callback' => 'absint', + ) + ); + + $wp_customize->add_control( + 'woocommerce_thumbnail_image_width', + array( + 'label' => __( 'Thumbnail width', 'woocommerce' ), + 'description' => __( 'Image size used for products in the catalog.', 'woocommerce' ), + 'section' => 'woocommerce_product_images', + 'settings' => 'woocommerce_thumbnail_image_width', + 'type' => 'number', + 'input_attrs' => array( + 'min' => 0, + 'step' => 1, + ), + ) + ); + } + + include_once WC_ABSPATH . 'includes/customizer/class-wc-customizer-control-cropping.php'; + + $wp_customize->add_setting( + 'woocommerce_thumbnail_cropping', + array( + 'default' => '1:1', + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'wc_clean', + ) + ); + + $wp_customize->add_setting( + 'woocommerce_thumbnail_cropping_custom_width', + array( + 'default' => '4', + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'absint', + 'sanitize_js_callback' => 'absint', + ) + ); + + $wp_customize->add_setting( + 'woocommerce_thumbnail_cropping_custom_height', + array( + 'default' => '3', + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'absint', + 'sanitize_js_callback' => 'absint', + ) + ); + + $wp_customize->add_control( + new WC_Customizer_Control_Cropping( + $wp_customize, + 'woocommerce_thumbnail_cropping', + array( + 'section' => 'woocommerce_product_images', + 'settings' => array( + 'cropping' => 'woocommerce_thumbnail_cropping', + 'custom_width' => 'woocommerce_thumbnail_cropping_custom_width', + 'custom_height' => 'woocommerce_thumbnail_cropping_custom_height', + ), + 'label' => __( 'Thumbnail cropping', 'woocommerce' ), + 'choices' => array( + '1:1' => array( + 'label' => __( '1:1', 'woocommerce' ), + 'description' => __( 'Images will be cropped into a square', 'woocommerce' ), + ), + 'custom' => array( + 'label' => __( 'Custom', 'woocommerce' ), + 'description' => __( 'Images will be cropped to a custom aspect ratio', 'woocommerce' ), + ), + 'uncropped' => array( + 'label' => __( 'Uncropped', 'woocommerce' ), + 'description' => __( 'Images will display using the aspect ratio in which they were uploaded', 'woocommerce' ), + ), + ), + ) + ) + ); + } + + /** + * Checkout section. + * + * @param WP_Customize_Manager $wp_customize Theme Customizer object. + */ + public function add_checkout_section( $wp_customize ) { + $wp_customize->add_section( + 'woocommerce_checkout', + array( + 'title' => __( 'Checkout', 'woocommerce' ), + 'priority' => 20, + 'panel' => 'woocommerce', + 'description' => __( 'These options let you change the appearance of the WooCommerce checkout.', 'woocommerce' ), + ) + ); + + // Checkout field controls. + $fields = array( + 'company' => __( 'Company name', 'woocommerce' ), + 'address_2' => __( 'Address line 2', 'woocommerce' ), + 'phone' => __( 'Phone', 'woocommerce' ), + ); + foreach ( $fields as $field => $label ) { + $wp_customize->add_setting( + 'woocommerce_checkout_' . $field . '_field', + array( + 'default' => 'phone' === $field ? 'required' : 'optional', + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => array( $this, 'sanitize_checkout_field_display' ), + ) + ); + $wp_customize->add_control( + 'woocommerce_checkout_' . $field . '_field', + array( + /* Translators: %s field name. */ + 'label' => sprintf( __( '%s field', 'woocommerce' ), $label ), + 'section' => 'woocommerce_checkout', + 'settings' => 'woocommerce_checkout_' . $field . '_field', + 'type' => 'select', + 'choices' => array( + 'hidden' => __( 'Hidden', 'woocommerce' ), + 'optional' => __( 'Optional', 'woocommerce' ), + 'required' => __( 'Required', 'woocommerce' ), + ), + ) + ); + } + + // Register settings. + $wp_customize->add_setting( + 'woocommerce_checkout_highlight_required_fields', + array( + 'default' => 'yes', + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'wc_bool_to_string', + 'sanitize_js_callback' => 'wc_string_to_bool', + ) + ); + + $wp_customize->add_setting( + 'woocommerce_checkout_terms_and_conditions_checkbox_text', + array( + /* translators: %s terms and conditions page name and link */ + 'default' => sprintf( __( 'I have read and agree to the website %s', 'woocommerce' ), '[terms]' ), + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'wp_kses_post', + 'transport' => 'postMessage', + ) + ); + + $wp_customize->add_setting( + 'woocommerce_checkout_privacy_policy_text', + array( + /* translators: %s privacy policy page name and link */ + 'default' => sprintf( __( 'Your personal data will be used to process your order, support your experience throughout this website, and for other purposes described in our %s.', 'woocommerce' ), '[privacy_policy]' ), + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'wp_kses_post', + 'transport' => 'postMessage', + ) + ); + + // Register controls. + $wp_customize->add_control( + 'woocommerce_checkout_highlight_required_fields', + array( + 'label' => __( 'Highlight required fields with an asterisk', 'woocommerce' ), + 'section' => 'woocommerce_checkout', + 'settings' => 'woocommerce_checkout_highlight_required_fields', + 'type' => 'checkbox', + ) + ); + + if ( current_user_can( 'manage_privacy_options' ) ) { + $choose_pages = array( + 'wp_page_for_privacy_policy' => __( 'Privacy policy', 'woocommerce' ), + 'woocommerce_terms_page_id' => __( 'Terms and conditions', 'woocommerce' ), + ); + } else { + $choose_pages = array( + 'woocommerce_terms_page_id' => __( 'Terms and conditions', 'woocommerce' ), + ); + } + $pages = get_pages( + array( + 'post_type' => 'page', + 'post_status' => 'publish,private,draft', + 'child_of' => 0, + 'parent' => -1, + 'exclude' => array( + wc_get_page_id( 'cart' ), + wc_get_page_id( 'checkout' ), + wc_get_page_id( 'myaccount' ), + ), + 'sort_order' => 'asc', + 'sort_column' => 'post_title', + ) + ); + $page_choices = array( '' => __( 'No page set', 'woocommerce' ) ) + array_combine( array_map( 'strval', wp_list_pluck( $pages, 'ID' ) ), wp_list_pluck( $pages, 'post_title' ) ); + + foreach ( $choose_pages as $id => $name ) { + $wp_customize->add_setting( + $id, + array( + 'default' => '', + 'type' => 'option', + 'capability' => 'manage_woocommerce', + ) + ); + $wp_customize->add_control( + $id, + array( + /* Translators: %s: page name. */ + 'label' => sprintf( __( '%s page', 'woocommerce' ), $name ), + 'section' => 'woocommerce_checkout', + 'settings' => $id, + 'type' => 'select', + 'choices' => $page_choices, + ) + ); + } + + $wp_customize->add_control( + 'woocommerce_checkout_privacy_policy_text', + array( + 'label' => __( 'Privacy policy', 'woocommerce' ), + 'description' => __( 'Optionally add some text about your store privacy policy to show during checkout.', 'woocommerce' ), + 'section' => 'woocommerce_checkout', + 'settings' => 'woocommerce_checkout_privacy_policy_text', + 'active_callback' => array( $this, 'has_privacy_policy_page_id' ), + 'type' => 'textarea', + ) + ); + + $wp_customize->add_control( + 'woocommerce_checkout_terms_and_conditions_checkbox_text', + array( + 'label' => __( 'Terms and conditions', 'woocommerce' ), + 'description' => __( 'Optionally add some text for the terms checkbox that customers must accept.', 'woocommerce' ), + 'section' => 'woocommerce_checkout', + 'settings' => 'woocommerce_checkout_terms_and_conditions_checkbox_text', + 'active_callback' => array( $this, 'has_terms_and_conditions_page_id' ), + 'type' => 'text', + ) + ); + + if ( isset( $wp_customize->selective_refresh ) ) { + $wp_customize->selective_refresh->add_partial( + 'woocommerce_checkout_privacy_policy_text', + array( + 'selector' => '.woocommerce-privacy-policy-text', + 'container_inclusive' => true, + 'render_callback' => 'wc_checkout_privacy_policy_text', + ) + ); + $wp_customize->selective_refresh->add_partial( + 'woocommerce_checkout_terms_and_conditions_checkbox_text', + array( + 'selector' => '.woocommerce-terms-and-conditions-checkbox-text', + 'container_inclusive' => false, + 'render_callback' => 'wc_terms_and_conditions_checkbox_text', + ) + ); + } + } + + /** + * Sanitize field display. + * + * @param string $value '', 'subcategories', or 'both'. + * @return string + */ + public function sanitize_checkout_field_display( $value ) { + $options = array( 'hidden', 'optional', 'required' ); + return in_array( $value, $options, true ) ? $value : ''; + } + + /** + * Whether or not a page has been chose for the privacy policy. + * + * @return bool + */ + public function has_privacy_policy_page_id() { + return wc_privacy_policy_page_id() > 0; + } + + /** + * Whether or not a page has been chose for the terms and conditions. + * + * @return bool + */ + public function has_terms_and_conditions_page_id() { + return wc_terms_and_conditions_page_id() > 0; + } +} + +new WC_Shop_Customizer(); diff --git a/includes/data-stores/abstract-wc-order-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php similarity index 100% rename from includes/data-stores/abstract-wc-order-data-store-cpt.php rename to plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php diff --git a/includes/data-stores/abstract-wc-order-item-type-data-store.php b/plugins/woocommerce/includes/data-stores/abstract-wc-order-item-type-data-store.php similarity index 100% rename from includes/data-stores/abstract-wc-order-item-type-data-store.php rename to plugins/woocommerce/includes/data-stores/abstract-wc-order-item-type-data-store.php diff --git a/plugins/woocommerce/includes/data-stores/class-wc-coupon-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-coupon-data-store-cpt.php new file mode 100644 index 00000000000..9b5cef4e948 --- /dev/null +++ b/plugins/woocommerce/includes/data-stores/class-wc-coupon-data-store-cpt.php @@ -0,0 +1,730 @@ +get_date_created( 'edit' ) ) { + $coupon->set_date_created( time() ); + } + + $coupon_id = wp_insert_post( + apply_filters( + 'woocommerce_new_coupon_data', + array( + 'post_type' => 'shop_coupon', + 'post_status' => 'publish', + 'post_author' => get_current_user_id(), + 'post_title' => $coupon->get_code( 'edit' ), + 'post_content' => '', + 'post_excerpt' => $coupon->get_description( 'edit' ), + 'post_date' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getOffsetTimestamp() ), + 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getTimestamp() ), + ) + ), + true + ); + + if ( $coupon_id ) { + $coupon->set_id( $coupon_id ); + $this->update_post_meta( $coupon ); + $coupon->save_meta_data(); + $coupon->apply_changes(); + delete_transient( 'rest_api_coupons_type_count' ); + do_action( 'woocommerce_new_coupon', $coupon_id, $coupon ); + } + } + + /** + * Method to read a coupon. + * + * @since 3.0.0 + * + * @param WC_Coupon $coupon Coupon object. + * + * @throws Exception If invalid coupon. + */ + public function read( &$coupon ) { + $coupon->set_defaults(); + + $post_object = get_post( $coupon->get_id() ); + + if ( ! $coupon->get_id() || ! $post_object || 'shop_coupon' !== $post_object->post_type ) { + throw new Exception( __( 'Invalid coupon.', 'woocommerce' ) ); + } + + $coupon_id = $coupon->get_id(); + $coupon->set_props( + array( + 'code' => $post_object->post_title, + 'description' => $post_object->post_excerpt, + 'status' => $post_object->post_status, + 'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ), + 'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ), + 'date_expires' => metadata_exists( 'post', $coupon_id, 'date_expires' ) ? get_post_meta( $coupon_id, 'date_expires', true ) : get_post_meta( $coupon_id, 'expiry_date', true ), // @todo: Migrate expiry_date meta to date_expires in upgrade routine. + 'discount_type' => get_post_meta( $coupon_id, 'discount_type', true ), + 'amount' => get_post_meta( $coupon_id, 'coupon_amount', true ), + 'usage_count' => get_post_meta( $coupon_id, 'usage_count', true ), + 'individual_use' => 'yes' === get_post_meta( $coupon_id, 'individual_use', true ), + 'product_ids' => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'product_ids', true ) ) ), + 'excluded_product_ids' => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'exclude_product_ids', true ) ) ), + 'usage_limit' => get_post_meta( $coupon_id, 'usage_limit', true ), + 'usage_limit_per_user' => get_post_meta( $coupon_id, 'usage_limit_per_user', true ), + 'limit_usage_to_x_items' => 0 < get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) ? get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) : null, + 'free_shipping' => 'yes' === get_post_meta( $coupon_id, 'free_shipping', true ), + 'product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'product_categories', true ) ), + 'excluded_product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'exclude_product_categories', true ) ), + 'exclude_sale_items' => 'yes' === get_post_meta( $coupon_id, 'exclude_sale_items', true ), + 'minimum_amount' => get_post_meta( $coupon_id, 'minimum_amount', true ), + 'maximum_amount' => get_post_meta( $coupon_id, 'maximum_amount', true ), + 'email_restrictions' => array_filter( (array) get_post_meta( $coupon_id, 'customer_email', true ) ), + 'used_by' => array_filter( (array) get_post_meta( $coupon_id, '_used_by' ) ), + ) + ); + $coupon->read_meta_data(); + $coupon->set_object_read( true ); + do_action( 'woocommerce_coupon_loaded', $coupon ); + } + + /** + * Updates a coupon in the database. + * + * @since 3.0.0 + * @param WC_Coupon $coupon Coupon object. + */ + public function update( &$coupon ) { + $coupon->save_meta_data(); + $changes = $coupon->get_changes(); + + if ( array_intersect( array( 'code', 'description', 'date_created', 'date_modified' ), array_keys( $changes ) ) ) { + $post_data = array( + 'post_title' => $coupon->get_code( 'edit' ), + 'post_excerpt' => $coupon->get_description( 'edit' ), + 'post_date' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created( 'edit' )->getOffsetTimestamp() ), + 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created( 'edit' )->getTimestamp() ), + 'post_modified' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $coupon->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ), + 'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $coupon->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ), + ); + + /** + * When updating this object, to prevent infinite loops, use $wpdb + * to update data, since wp_update_post spawns more calls to the + * save_post action. + * + * This ensures hooks are fired by either WP itself (admin screen save), + * or an update purely from CRUD. + */ + if ( doing_action( 'save_post' ) ) { + $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $coupon->get_id() ) ); + clean_post_cache( $coupon->get_id() ); + } else { + wp_update_post( array_merge( array( 'ID' => $coupon->get_id() ), $post_data ) ); + } + $coupon->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook. + } + $this->update_post_meta( $coupon ); + $coupon->apply_changes(); + delete_transient( 'rest_api_coupons_type_count' ); + do_action( 'woocommerce_update_coupon', $coupon->get_id(), $coupon ); + } + + /** + * Deletes a coupon from the database. + * + * @since 3.0.0 + * + * @param WC_Coupon $coupon Coupon object. + * @param array $args Array of args to pass to the delete method. + */ + public function delete( &$coupon, $args = array() ) { + $args = wp_parse_args( + $args, + array( + 'force_delete' => false, + ) + ); + + $id = $coupon->get_id(); + + if ( ! $id ) { + return; + } + + if ( $args['force_delete'] ) { + wp_delete_post( $id ); + + wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $coupon->get_code(), 'coupons' ); + + $coupon->set_id( 0 ); + do_action( 'woocommerce_delete_coupon', $id ); + } else { + wp_trash_post( $id ); + do_action( 'woocommerce_trash_coupon', $id ); + } + } + + /** + * Helper method that updates all the post meta for a coupon based on it's settings in the WC_Coupon class. + * + * @param WC_Coupon $coupon Coupon object. + * @since 3.0.0 + */ + private function update_post_meta( &$coupon ) { + $meta_key_to_props = array( + 'discount_type' => 'discount_type', + 'coupon_amount' => 'amount', + 'individual_use' => 'individual_use', + 'product_ids' => 'product_ids', + 'exclude_product_ids' => 'excluded_product_ids', + 'usage_limit' => 'usage_limit', + 'usage_limit_per_user' => 'usage_limit_per_user', + 'limit_usage_to_x_items' => 'limit_usage_to_x_items', + 'usage_count' => 'usage_count', + 'date_expires' => 'date_expires', + 'free_shipping' => 'free_shipping', + 'product_categories' => 'product_categories', + 'exclude_product_categories' => 'excluded_product_categories', + 'exclude_sale_items' => 'exclude_sale_items', + 'minimum_amount' => 'minimum_amount', + 'maximum_amount' => 'maximum_amount', + 'customer_email' => 'email_restrictions', + ); + + $props_to_update = $this->get_props_to_update( $coupon, $meta_key_to_props ); + foreach ( $props_to_update as $meta_key => $prop ) { + $value = $coupon->{"get_$prop"}( 'edit' ); + $value = is_string( $value ) ? wp_slash( $value ) : $value; + switch ( $prop ) { + case 'individual_use': + case 'free_shipping': + case 'exclude_sale_items': + $value = wc_bool_to_string( $value ); + break; + case 'product_ids': + case 'excluded_product_ids': + $value = implode( ',', array_filter( array_map( 'intval', $value ) ) ); + break; + case 'product_categories': + case 'excluded_product_categories': + $value = array_filter( array_map( 'intval', $value ) ); + break; + case 'email_restrictions': + $value = array_filter( array_map( 'sanitize_email', $value ) ); + break; + case 'date_expires': + $value = $value ? $value->getTimestamp() : null; + break; + } + + $updated = $this->update_or_delete_post_meta( $coupon, $meta_key, $value ); + + if ( $updated ) { + $this->updated_props[] = $prop; + } + } + + do_action( 'woocommerce_coupon_object_updated_props', $coupon, $this->updated_props ); + } + + /** + * Increase usage count for current coupon. + * + * @since 3.0.0 + * @param WC_Coupon $coupon Coupon object. + * @param string $used_by Either user ID or billing email. + * @param WC_Order $order (Optional) If passed, clears the hold record associated with order. + + * @return int New usage count. + */ + public function increase_usage_count( &$coupon, $used_by = '', $order = null ) { + $coupon_held_key_for_user = ''; + if ( $order instanceof WC_Order ) { + $coupon_held_key_for_user = $order->get_data_store()->get_coupon_held_keys_for_users( $order, $coupon->get_id() ); + } + + $new_count = $this->update_usage_count_meta( $coupon, 'increase' ); + + if ( $used_by ) { + $this->add_coupon_used_by( $coupon, $used_by, $coupon_held_key_for_user ); + $coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) ); + } + + do_action( 'woocommerce_increase_coupon_usage_count', $coupon, $new_count, $used_by ); + + return $new_count; + } + + /** + * Helper function to add a `_used_by` record to track coupons used by the user. + * + * @param WC_Coupon $coupon Coupon object. + * @param string $used_by Either user ID or billing email. + * @param string $coupon_held_key (Optional) Update meta key to `_used_by` instead of adding a new record. + */ + private function add_coupon_used_by( $coupon, $used_by, $coupon_held_key ) { + global $wpdb; + if ( $coupon_held_key && '' !== $coupon_held_key ) { + // Looks like we added a tentative record for this coupon getting used. + // Lets change the tentative record to a permanent one. + $result = $wpdb->query( + $wpdb->prepare( + " + UPDATE $wpdb->postmeta SET meta_key = %s, meta_value = %s WHERE meta_key = %s LIMIT 1", + '_used_by', + $used_by, + $coupon_held_key + ) + ); + if ( ! $result ) { + // If no rows were updated, then insert a `_used_by` row manually to maintain consistency. + add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) ); + } + } else { + add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) ); + } + } + + /** + * Decrease usage count for current coupon. + * + * @since 3.0.0 + * @param WC_Coupon $coupon Coupon object. + * @param string $used_by Either user ID or billing email. + * @return int New usage count. + */ + public function decrease_usage_count( &$coupon, $used_by = '' ) { + global $wpdb; + $new_count = $this->update_usage_count_meta( $coupon, 'decrease' ); + if ( $used_by ) { + /** + * We're doing this the long way because `delete_post_meta( $id, $key, $value )` deletes. + * all instances where the key and value match, and we only want to delete one. + */ + $meta_id = $wpdb->get_var( + $wpdb->prepare( + "SELECT meta_id FROM $wpdb->postmeta WHERE meta_key = '_used_by' AND meta_value = %s AND post_id = %d LIMIT 1;", + $used_by, + $coupon->get_id() + ) + ); + if ( $meta_id ) { + delete_metadata_by_mid( 'post', $meta_id ); + $coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) ); + } + } + + do_action( 'woocommerce_decrease_coupon_usage_count', $coupon, $new_count, $used_by ); + + return $new_count; + } + + /** + * Increase or decrease the usage count for a coupon by 1. + * + * @since 3.0.0 + * @param WC_Coupon $coupon Coupon object. + * @param string $operation 'increase' or 'decrease'. + * @return int New usage count + */ + private function update_usage_count_meta( &$coupon, $operation = 'increase' ) { + global $wpdb; + $id = $coupon->get_id(); + $operator = ( 'increase' === $operation ) ? '+' : '-'; + + add_post_meta( $id, 'usage_count', $coupon->get_usage_count( 'edit' ), true ); + $wpdb->query( + $wpdb->prepare( + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "UPDATE $wpdb->postmeta SET meta_value = meta_value {$operator} 1 WHERE meta_key = 'usage_count' AND post_id = %d;", + $id + ) + ); + + // Get the latest value direct from the DB, instead of possibly the WP meta cache. + return (int) $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = 'usage_count' AND post_id = %d;", $id ) ); + } + + /** + * Returns tentative usage count for coupon. + * + * @param int $coupon_id Coupon ID. + * + * @return int Tentative usage count. + */ + public function get_tentative_usage_count( $coupon_id ) { + global $wpdb; + return $wpdb->get_var( + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $this->get_tentative_usage_query( $coupon_id ) + ); + } + + /** + * Get the number of uses for a coupon by user ID. + * + * @since 3.0.0 + * @param WC_Coupon $coupon Coupon object. + * @param int $user_id User ID. + * @return int + */ + public function get_usage_by_user_id( &$coupon, $user_id ) { + global $wpdb; + $usage_count = $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %d;", + $coupon->get_id(), + $user_id + ) + ); + $tentative_usage_count = $this->get_tentative_usages_for_user( $coupon->get_id(), array( $user_id ) ); + return $tentative_usage_count + $usage_count; + } + + /** + * Get the number of uses for a coupon by email address + * + * @since 3.6.4 + * @param WC_Coupon $coupon Coupon object. + * @param string $email Email address. + * @return int + */ + public function get_usage_by_email( &$coupon, $email ) { + global $wpdb; + $usage_count = $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %s;", + $coupon->get_id(), + $email + ) + ); + $tentative_usage_count = $this->get_tentative_usages_for_user( $coupon->get_id(), array( $email ) ); + return $tentative_usage_count + $usage_count; + } + + /** + * Get tentative coupon usages for user. + * + * @param int $coupon_id Coupon ID. + * @param array $user_aliases Array of user aliases to check tentative usages for. + * + * @return string|null + */ + public function get_tentative_usages_for_user( $coupon_id, $user_aliases ) { + global $wpdb; + return $wpdb->get_var( + $this->get_tentative_usage_query_for_user( $coupon_id, $user_aliases ) + ); // WPCS: unprepared SQL ok. + + } + + /** + * Get held time for resources before cancelling the order. Use 60 minutes as sane default. + * Note that the filter `woocommerce_coupon_hold_minutes` only support minutes because it's getting used elsewhere as well, however this function returns in seconds. + * + * @return int + */ + private function get_tentative_held_time() { + return apply_filters( 'woocommerce_coupon_hold_minutes', ( (int) get_option( 'woocommerce_hold_stock_minutes', 60 ) ) ) * 60; + } + + /** + * Check and records coupon usage tentatively for short period of time so that counts validation is correct. Returns early if there is no limit defined for the coupon. + * + * @param WC_Coupon $coupon Coupon object. + * + * @return bool|int|string|null Returns meta key if coupon was held, null if returned early. + */ + public function check_and_hold_coupon( $coupon ) { + global $wpdb; + + $usage_limit = $coupon->get_usage_limit(); + $held_time = $this->get_tentative_held_time(); + + if ( 0 >= $usage_limit || 0 >= $held_time ) { + return null; + } + + if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', true ) ) { + return null; + } + + // Make sure we have usage_count meta key for this coupon because its required for `$query_for_usages`. + // We are not directly modifying `$query_for_usages` to allow for `usage_count` not present only keep that query simple. + if ( ! metadata_exists( 'post', $coupon->get_id(), 'usage_count' ) ) { + $coupon->set_usage_count( $coupon->get_usage_count() ); // Use `get_usage_count` here to write default value, which may changed by a filter. + $coupon->save(); + } + + $query_for_usages = $wpdb->prepare( + " + SELECT meta_value from $wpdb->postmeta + WHERE {$wpdb->postmeta}.meta_key = 'usage_count' + AND {$wpdb->postmeta}.post_id = %d + LIMIT 1 + FOR UPDATE + ", + $coupon->get_id() + ); + + $query_for_tentative_usages = $this->get_tentative_usage_query( $coupon->get_id() ); + $db_timestamp = $wpdb->get_var( 'SELECT UNIX_TIMESTAMP() FROM DUAL' ); + + $coupon_usage_key = '_coupon_held_' . ( (int) $db_timestamp + $held_time ) . '_' . wp_generate_password( 6, false ); + + $insert_statement = $wpdb->prepare( + " + INSERT INTO $wpdb->postmeta ( post_id, meta_key, meta_value ) + SELECT %d, %s, %s FROM DUAL + WHERE ( $query_for_usages ) + ( $query_for_tentative_usages ) < %d + ", + $coupon->get_id(), + $coupon_usage_key, + '', + $usage_limit + ); // WPCS: unprepared SQL ok. + + /** + * In some cases, specifically when there is a combined index on post_id,meta_key, the insert statement above could end up in a deadlock. + * We will try to insert 3 times before giving up to recover from deadlock. + */ + for ( $count = 0; $count < 3; $count++ ) { + $result = $wpdb->query( $insert_statement ); // WPCS: unprepared SQL ok. + if ( false !== $result ) { + break; + } + } + + return $result > 0 ? $coupon_usage_key : $result; + } + + /** + * Generate query to calculate tentative usages for the coupon. + * + * @param int $coupon_id Coupon ID to get tentative usage query for. + * + * @return string Query for tentative usages. + */ + private function get_tentative_usage_query( $coupon_id ) { + global $wpdb; + return $wpdb->prepare( + " + SELECT COUNT(meta_id) FROM $wpdb->postmeta + WHERE {$wpdb->postmeta}.meta_key like %s + AND {$wpdb->postmeta}.meta_key > %s + AND {$wpdb->postmeta}.post_id = %d + FOR UPDATE + ", + array( + '_coupon_held_%', + '_coupon_held_' . time(), + $coupon_id, + ) + ); // WPCS: unprepared SQL ok. + } + + /** + * Check and records coupon usage tentatively for passed user aliases for short period of time so that counts validation is correct. Returns early if there is no limit per user for the coupon. + * + * @param WC_Coupon $coupon Coupon object. + * @param array $user_aliases Emails or Ids to check for user. + * @param string $user_alias Email/ID to use as `used_by` value. + * + * @return null|false|int + */ + public function check_and_hold_coupon_for_user( $coupon, $user_aliases, $user_alias ) { + global $wpdb; + $limit_per_user = $coupon->get_usage_limit_per_user(); + $held_time = $this->get_tentative_held_time(); + + if ( 0 >= $limit_per_user || 0 >= $held_time ) { + // This coupon do not have any restriction for usage per customer. No need to check further, lets bail. + return null; + } + + if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', true ) ) { + return null; + } + + $format = implode( "','", array_fill( 0, count( $user_aliases ), '%s' ) ); + + $query_for_usages = $wpdb->prepare( + " + SELECT COUNT(*) FROM $wpdb->postmeta + WHERE {$wpdb->postmeta}.meta_key = '_used_by' + AND {$wpdb->postmeta}.meta_value IN ('$format') + AND {$wpdb->postmeta}.post_id = %d + FOR UPDATE + ", + array_merge( + $user_aliases, + array( $coupon->get_id() ) + ) + ); // WPCS: unprepared SQL ok. + + $query_for_tentative_usages = $this->get_tentative_usage_query_for_user( $coupon->get_id(), $user_aliases ); + $db_timestamp = $wpdb->get_var( 'SELECT UNIX_TIMESTAMP() FROM DUAL' ); + + $coupon_used_by_meta_key = '_maybe_used_by_' . ( (int) $db_timestamp + $held_time ) . '_' . wp_generate_password( 6, false ); + $insert_statement = $wpdb->prepare( + " + INSERT INTO $wpdb->postmeta ( post_id, meta_key, meta_value ) + SELECT %d, %s, %s FROM DUAL + WHERE ( $query_for_usages ) + ( $query_for_tentative_usages ) < %d + ", + $coupon->get_id(), + $coupon_used_by_meta_key, + $user_alias, + $limit_per_user + ); // WPCS: unprepared SQL ok. + + // This query can potentially be deadlocked if a combined index on post_id and meta_key is present and there is + // high concurrency, in which case DB will abort the query which has done less work to resolve deadlock. + // We will try up to 3 times before giving up. + for ( $count = 0; $count < 3; $count++ ) { + $result = $wpdb->query( $insert_statement ); // WPCS: unprepared SQL ok. + if ( false !== $result ) { + break; + } + } + + return $result > 0 ? $coupon_used_by_meta_key : $result; + } + + /** + * Generate query to calculate tentative usages for the coupon by the user. + * + * @param int $coupon_id Coupon ID. + * @param array $user_aliases List of user aliases to check for usages. + * + * @return string Tentative usages query. + */ + private function get_tentative_usage_query_for_user( $coupon_id, $user_aliases ) { + global $wpdb; + + $format = implode( "','", array_fill( 0, count( $user_aliases ), '%s' ) ); + + // Note that if you are debugging, `_maybe_used_by_%` will be converted to `_maybe_used_by_{...very long str...}` to very long string. This is expected, and is automatically corrected while running the insert query. + return $wpdb->prepare( + " + SELECT COUNT( meta_id ) FROM $wpdb->postmeta + WHERE {$wpdb->postmeta}.meta_key like %s + AND {$wpdb->postmeta}.meta_key > %s + AND {$wpdb->postmeta}.post_id = %d + AND {$wpdb->postmeta}.meta_value IN ('$format') + FOR UPDATE + ", + array_merge( + array( + '_maybe_used_by_%', + '_maybe_used_by_' . time(), + $coupon_id, + ), + $user_aliases + ) + ); // WPCS: unprepared SQL ok. + } + + /** + * Return a coupon code for a specific ID. + * + * @since 3.0.0 + * @param int $id Coupon ID. + * @return string Coupon Code + */ + public function get_code_by_id( $id ) { + global $wpdb; + return $wpdb->get_var( + $wpdb->prepare( + "SELECT post_title + FROM $wpdb->posts + WHERE ID = %d + AND post_type = 'shop_coupon' + AND post_status = 'publish'", + $id + ) + ); + } + + /** + * Return an array of IDs for for a specific coupon code. + * Can return multiple to check for existence. + * + * @since 3.0.0 + * @param string $code Coupon code. + * @return array Array of IDs. + */ + public function get_ids_by_code( $code ) { + global $wpdb; + return $wpdb->get_col( + $wpdb->prepare( + "SELECT ID FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC", + wc_sanitize_coupon_code( $code ) + ) + ); + } +} diff --git a/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store-session.php b/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store-session.php new file mode 100644 index 00000000000..048e09372ca --- /dev/null +++ b/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store-session.php @@ -0,0 +1,199 @@ +save_to_session( $customer ); + } + + /** + * Simply update the session. + * + * @param WC_Customer $customer Customer object. + */ + public function update( &$customer ) { + $this->save_to_session( $customer ); + } + + /** + * Saves all customer data to the session. + * + * @param WC_Customer $customer Customer object. + */ + public function save_to_session( $customer ) { + $data = array(); + foreach ( $this->session_keys as $session_key ) { + $function_key = $session_key; + if ( 'billing_' === substr( $session_key, 0, 8 ) ) { + $session_key = str_replace( 'billing_', '', $session_key ); + } + $data[ $session_key ] = (string) $customer->{"get_$function_key"}( 'edit' ); + } + WC()->session->set( 'customer', $data ); + } + + /** + * Read customer data from the session unless the user has logged in, in + * which case the stored ID will differ from the actual ID. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + */ + public function read( &$customer ) { + $data = (array) WC()->session->get( 'customer' ); + + /** + * There is a valid session if $data is not empty, and the ID matches the logged in user ID. + * + * If the user object has been updated since the session was created (based on date_modified) we should not load the session - data should be reloaded. + */ + if ( isset( $data['id'], $data['date_modified'] ) && $data['id'] === (string) $customer->get_id() && $data['date_modified'] === (string) $customer->get_date_modified( 'edit' ) ) { + foreach ( $this->session_keys as $session_key ) { + if ( in_array( $session_key, array( 'id', 'date_modified' ), true ) ) { + continue; + } + $function_key = $session_key; + if ( 'billing_' === substr( $session_key, 0, 8 ) ) { + $session_key = str_replace( 'billing_', '', $session_key ); + } + if ( isset( $data[ $session_key ] ) && is_callable( array( $customer, "set_{$function_key}" ) ) ) { + $customer->{"set_{$function_key}"}( wp_unslash( $data[ $session_key ] ) ); + } + } + } + $this->set_defaults( $customer ); + $customer->set_object_read( true ); + } + + /** + * Load default values if props are unset. + * + * @param WC_Customer $customer Customer object. + */ + protected function set_defaults( &$customer ) { + try { + $default = wc_get_customer_default_location(); + $has_shipping_address = $customer->has_shipping_address(); + + if ( ! $customer->get_billing_country() ) { + $customer->set_billing_country( $default['country'] ); + } + + if ( ! $customer->get_shipping_country() && ! $has_shipping_address ) { + $customer->set_shipping_country( $customer->get_billing_country() ); + } + + if ( ! $customer->get_billing_state() ) { + $customer->set_billing_state( $default['state'] ); + } + + if ( ! $customer->get_shipping_state() && ! $has_shipping_address ) { + $customer->set_shipping_state( $customer->get_billing_state() ); + } + + if ( ! $customer->get_billing_email() && is_user_logged_in() ) { + $current_user = wp_get_current_user(); + $customer->set_billing_email( $current_user->user_email ); + } + } catch ( WC_Data_Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch + } + } + + /** + * Deletes a customer from the database. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + * @param array $args Array of args to pass to the delete method. + */ + public function delete( &$customer, $args = array() ) { + WC()->session->set( 'customer', null ); + } + + /** + * Gets the customers last order. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + * @return WC_Order|false + */ + public function get_last_order( &$customer ) { + return false; + } + + /** + * Return the number of orders this customer has. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + * @return integer + */ + public function get_order_count( &$customer ) { + return 0; + } + + /** + * Return how much money this customer has spent. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + * @return float + */ + public function get_total_spent( &$customer ) { + return 0; + } +} diff --git a/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store.php new file mode 100644 index 00000000000..7bbfe8319db --- /dev/null +++ b/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store.php @@ -0,0 +1,529 @@ +prefix ? $wpdb->prefix : 'wp_'; + + return ! in_array( $meta->meta_key, $this->internal_meta_keys, true ) + && 0 !== strpos( $meta->meta_key, '_woocommerce_persistent_cart' ) + && 0 !== strpos( $meta->meta_key, 'closedpostboxes_' ) + && 0 !== strpos( $meta->meta_key, 'metaboxhidden_' ) + && 0 !== strpos( $meta->meta_key, 'manageedit-' ) + && ! strstr( $meta->meta_key, $table_prefix ) + && 0 !== stripos( $meta->meta_key, 'wp_' ); + } + + /** + * Method to create a new customer in the database. + * + * @since 3.0.0 + * + * @param WC_Customer $customer Customer object. + * + * @throws WC_Data_Exception If unable to create new customer. + */ + public function create( &$customer ) { + $id = wc_create_new_customer( $customer->get_email(), $customer->get_username(), $customer->get_password() ); + + if ( is_wp_error( $id ) ) { + throw new WC_Data_Exception( $id->get_error_code(), $id->get_error_message() ); + } + + $customer->set_id( $id ); + $this->update_user_meta( $customer ); + + // Prevent wp_update_user calls in the same request and customer trigger the 'Notice of Password Changed' email. + $customer->set_password( '' ); + + wp_update_user( + apply_filters( + 'woocommerce_update_customer_args', + array( + 'ID' => $customer->get_id(), + 'role' => $customer->get_role(), + 'display_name' => $customer->get_display_name(), + ), + $customer + ) + ); + $wp_user = new WP_User( $customer->get_id() ); + $customer->set_date_created( $wp_user->user_registered ); + $customer->set_date_modified( get_user_meta( $customer->get_id(), 'last_update', true ) ); + $customer->save_meta_data(); + $customer->apply_changes(); + do_action( 'woocommerce_new_customer', $customer->get_id(), $customer ); + } + + /** + * Method to read a customer object. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + * @throws Exception If invalid customer. + */ + public function read( &$customer ) { + $user_object = $customer->get_id() ? get_user_by( 'id', $customer->get_id() ) : false; + + // User object is required. + if ( ! $user_object || empty( $user_object->ID ) ) { + throw new Exception( __( 'Invalid customer.', 'woocommerce' ) ); + } + + $customer_id = $customer->get_id(); + + // Load meta but exclude deprecated props and parent keys. + $user_meta = array_diff_key( + array_change_key_case( array_map( 'wc_flatten_meta_callback', get_user_meta( $customer_id ) ) ), + array_flip( array( 'country', 'state', 'postcode', 'city', 'address', 'address_2', 'default', 'location' ) ), + array_change_key_case( (array) $user_object->data ) + ); + + $customer->set_props( $user_meta ); + $customer->set_props( + array( + 'is_paying_customer' => get_user_meta( $customer_id, 'paying_customer', true ), + 'email' => $user_object->user_email, + 'username' => $user_object->user_login, + 'display_name' => $user_object->display_name, + 'date_created' => $user_object->user_registered, // Mysql string in local format. + 'date_modified' => get_user_meta( $customer_id, 'last_update', true ), + 'role' => ! empty( $user_object->roles[0] ) ? $user_object->roles[0] : 'customer', + ) + ); + $customer->read_meta_data(); + $customer->set_object_read( true ); + do_action( 'woocommerce_customer_loaded', $customer ); + } + + /** + * Updates a customer in the database. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + */ + public function update( &$customer ) { + wp_update_user( + apply_filters( + 'woocommerce_update_customer_args', + array( + 'ID' => $customer->get_id(), + 'user_email' => $customer->get_email(), + 'display_name' => $customer->get_display_name(), + ), + $customer + ) + ); + + // Only update password if a new one was set with set_password. + if ( $customer->get_password() ) { + wp_update_user( + array( + 'ID' => $customer->get_id(), + 'user_pass' => $customer->get_password(), + ) + ); + $customer->set_password( '' ); + } + + $this->update_user_meta( $customer ); + $customer->set_date_modified( get_user_meta( $customer->get_id(), 'last_update', true ) ); + $customer->save_meta_data(); + $customer->apply_changes(); + do_action( 'woocommerce_update_customer', $customer->get_id(), $customer ); + } + + /** + * Deletes a customer from the database. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + * @param array $args Array of args to pass to the delete method. + */ + public function delete( &$customer, $args = array() ) { + if ( ! $customer->get_id() ) { + return; + } + + $args = wp_parse_args( + $args, + array( + 'reassign' => 0, + ) + ); + + $id = $customer->get_id(); + wp_delete_user( $id, $args['reassign'] ); + + do_action( 'woocommerce_delete_customer', $id ); + } + + /** + * Helper method that updates all the meta for a customer. Used for update & create. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + */ + private function update_user_meta( $customer ) { + $updated_props = array(); + $changed_props = $customer->get_changes(); + + $meta_key_to_props = array( + 'paying_customer' => 'is_paying_customer', + 'first_name' => 'first_name', + 'last_name' => 'last_name', + ); + + foreach ( $meta_key_to_props as $meta_key => $prop ) { + if ( ! array_key_exists( $prop, $changed_props ) ) { + continue; + } + + if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) { + $updated_props[] = $prop; + } + } + + $billing_address_props = array( + 'billing_first_name' => 'billing_first_name', + 'billing_last_name' => 'billing_last_name', + 'billing_company' => 'billing_company', + 'billing_address_1' => 'billing_address_1', + 'billing_address_2' => 'billing_address_2', + 'billing_city' => 'billing_city', + 'billing_state' => 'billing_state', + 'billing_postcode' => 'billing_postcode', + 'billing_country' => 'billing_country', + 'billing_email' => 'billing_email', + 'billing_phone' => 'billing_phone', + ); + + foreach ( $billing_address_props as $meta_key => $prop ) { + $prop_key = substr( $prop, 8 ); + + if ( ! isset( $changed_props['billing'] ) || ! array_key_exists( $prop_key, $changed_props['billing'] ) ) { + continue; + } + + if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) { + $updated_props[] = $prop; + } + } + + $shipping_address_props = array( + 'shipping_first_name' => 'shipping_first_name', + 'shipping_last_name' => 'shipping_last_name', + 'shipping_company' => 'shipping_company', + 'shipping_address_1' => 'shipping_address_1', + 'shipping_address_2' => 'shipping_address_2', + 'shipping_city' => 'shipping_city', + 'shipping_state' => 'shipping_state', + 'shipping_postcode' => 'shipping_postcode', + 'shipping_country' => 'shipping_country', + 'shipping_phone' => 'shipping_phone', + ); + + foreach ( $shipping_address_props as $meta_key => $prop ) { + $prop_key = substr( $prop, 9 ); + + if ( ! isset( $changed_props['shipping'] ) || ! array_key_exists( $prop_key, $changed_props['shipping'] ) ) { + continue; + } + + if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) { + $updated_props[] = $prop; + } + } + + do_action( 'woocommerce_customer_object_updated_props', $customer, $updated_props ); + } + + /** + * Gets the customers last order. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + * @return WC_Order|false + */ + public function get_last_order( &$customer ) { + $last_order = apply_filters( + 'woocommerce_customer_get_last_order', + get_user_meta( $customer->get_id(), '_last_order', true ), + $customer + ); + + if ( '' === $last_order ) { + global $wpdb; + + $last_order = $wpdb->get_var( + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + "SELECT posts.ID + FROM $wpdb->posts AS posts + LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id + WHERE meta.meta_key = '_customer_user' + AND meta.meta_value = '" . esc_sql( $customer->get_id() ) . "' + AND posts.post_type = 'shop_order' + AND posts.post_status IN ( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' ) + ORDER BY posts.ID DESC" + // phpcs:enable + ); + update_user_meta( $customer->get_id(), '_last_order', $last_order ); + } + + if ( ! $last_order ) { + return false; + } + + return wc_get_order( absint( $last_order ) ); + } + + /** + * Return the number of orders this customer has. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + * @return integer + */ + public function get_order_count( &$customer ) { + $count = apply_filters( + 'woocommerce_customer_get_order_count', + get_user_meta( $customer->get_id(), '_order_count', true ), + $customer + ); + + if ( '' === $count ) { + global $wpdb; + + $count = $wpdb->get_var( + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + "SELECT COUNT(*) + FROM $wpdb->posts as posts + LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id + WHERE meta.meta_key = '_customer_user' + AND posts.post_type = 'shop_order' + AND posts.post_status IN ( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' ) + AND meta_value = '" . esc_sql( $customer->get_id() ) . "'" + // phpcs:enable + ); + update_user_meta( $customer->get_id(), '_order_count', $count ); + } + + return absint( $count ); + } + + /** + * Return how much money this customer has spent. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + * @return float + */ + public function get_total_spent( &$customer ) { + $spent = apply_filters( + 'woocommerce_customer_get_total_spent', + get_user_meta( $customer->get_id(), '_money_spent', true ), + $customer + ); + + if ( '' === $spent ) { + global $wpdb; + + $statuses = array_map( 'esc_sql', wc_get_is_paid_statuses() ); + $spent = $wpdb->get_var( + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + apply_filters( + 'woocommerce_customer_get_total_spent_query', + "SELECT SUM(meta2.meta_value) + FROM $wpdb->posts as posts + LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id + LEFT JOIN {$wpdb->postmeta} AS meta2 ON posts.ID = meta2.post_id + WHERE meta.meta_key = '_customer_user' + AND meta.meta_value = '" . esc_sql( $customer->get_id() ) . "' + AND posts.post_type = 'shop_order' + AND posts.post_status IN ( 'wc-" . implode( "','wc-", $statuses ) . "' ) + AND meta2.meta_key = '_order_total'", + $customer + ) + // phpcs:enable + ); + + if ( ! $spent ) { + $spent = 0; + } + update_user_meta( $customer->get_id(), '_money_spent', $spent ); + } + + return wc_format_decimal( $spent, 2 ); + } + + /** + * Search customers and return customer IDs. + * + * @param string $term Search term. + * @param int|string $limit Limit search results. + * @since 3.0.7 + * + * @return array + */ + public function search_customers( $term, $limit = '' ) { + $results = apply_filters( 'woocommerce_customer_pre_search_customers', false, $term, $limit ); + if ( is_array( $results ) ) { + return $results; + } + + $query = new WP_User_Query( + apply_filters( + 'woocommerce_customer_search_customers', + array( + 'search' => '*' . esc_attr( $term ) . '*', + 'search_columns' => array( 'user_login', 'user_url', 'user_email', 'user_nicename', 'display_name' ), + 'fields' => 'ID', + 'number' => $limit, + ), + $term, + $limit, + 'main_query' + ) + ); + + $query2 = new WP_User_Query( + apply_filters( + 'woocommerce_customer_search_customers', + array( + 'fields' => 'ID', + 'number' => $limit, + 'meta_query' => array( + 'relation' => 'OR', + array( + 'key' => 'first_name', + 'value' => $term, + 'compare' => 'LIKE', + ), + array( + 'key' => 'last_name', + 'value' => $term, + 'compare' => 'LIKE', + ), + ), + ), + $term, + $limit, + 'meta_query' + ) + ); + + $results = wp_parse_id_list( array_merge( (array) $query->get_results(), (array) $query2->get_results() ) ); + + if ( $limit && count( $results ) > $limit ) { + $results = array_slice( $results, 0, $limit ); + } + + return $results; + } + + /** + * Get all user ids who have `billing_email` set to any of the email passed in array. + * + * @param array $emails List of emails to check against. + * + * @return array + */ + public function get_user_ids_for_billing_email( $emails ) { + $emails = array_unique( array_map( 'strtolower', array_map( 'sanitize_email', $emails ) ) ); + $users_query = new WP_User_Query( + array( + 'fields' => 'ID', + 'meta_query' => array( + array( + 'key' => 'billing_email', + 'value' => $emails, + 'compare' => 'IN', + ), + ), + ) + ); + return array_unique( $users_query->get_results() ); + } +} diff --git a/plugins/woocommerce/includes/data-stores/class-wc-customer-download-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-customer-download-data-store.php new file mode 100644 index 00000000000..c28a0826780 --- /dev/null +++ b/plugins/woocommerce/includes/data-stores/class-wc-customer-download-data-store.php @@ -0,0 +1,521 @@ +insert_new_download_permission( $data ); + + do_action( 'woocommerce_grant_product_download_access', $data ); + + return $id; + } + + /** + * Create download permission for a user. + * + * @param WC_Customer_Download $download WC_Customer_Download object. + */ + public function create( &$download ) { + global $wpdb; + + // Always set a access granted date. + if ( is_null( $download->get_access_granted( 'edit' ) ) ) { + $download->set_access_granted( time() ); + } + + $data = array(); + foreach ( self::DOWNLOAD_PERMISSION_DB_FIELDS as $db_field_name ) { + $value = call_user_func( array( $download, 'get_' . $db_field_name ), 'edit' ); + $data[ $db_field_name ] = $value; + } + + $inserted_id = $this->insert_new_download_permission( $data ); + if ( $inserted_id ) { + $download->set_id( $inserted_id ); + $download->apply_changes(); + } + + do_action( 'woocommerce_grant_product_download_access', $data ); + } + + /** + * Create download permission for a user, from an array of data. + * Assumes that all the keys in the passed data are valid. + * + * @param array $data Data to create the permission for. + * @return int The database id of the created permission, or false if the permission creation failed. + */ + private function insert_new_download_permission( $data ) { + global $wpdb; + + // Always set a access granted date. + if ( ! isset( $data['access_granted'] ) ) { + $data['access_granted'] = time(); + } + + $data['access_granted'] = $this->adjust_date_for_db( $data['access_granted'] ); + + if ( isset( $data['access_expires'] ) ) { + $data['access_expires'] = $this->adjust_date_for_db( $data['access_expires'] ); + } + + $format = array( + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%d', + '%s', + ); + + $result = $wpdb->insert( + $wpdb->prefix . 'woocommerce_downloadable_product_permissions', + apply_filters( 'woocommerce_downloadable_file_permission_data', $data ), + apply_filters( 'woocommerce_downloadable_file_permission_format', $format, $data ) + ); + + return $result ? $wpdb->insert_id : false; + } + + /** + * Adjust a date value to be inserted in the database. + * + * @param mixed $date The date value. Can be a WC_DateTime, a timestamp, or anything else that "date" recognizes. + * @return string The date converted to 'Y-m-d' format. + * @throws Exception The passed value can't be converted to a date. + */ + private function adjust_date_for_db( $date ) { + if ( 'WC_DateTime' === get_class( $date ) ) { + $date = $date->getTimestamp(); + } + + $adjusted_date = date( 'Y-m-d', $date ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + + if ( $adjusted_date ) { + return $adjusted_date; + } + + $msg = sprintf( __( "I don't know how to get a date from a %s", 'woocommerce' ), is_object( $date ) ? get_class( $date ) : gettype( $date ) ); + throw new Exception( $msg ); + } + + /** + * Method to read a download permission from the database. + * + * @param WC_Customer_Download $download WC_Customer_Download object. + * + * @throws Exception Throw exception if invalid download is passed. + */ + public function read( &$download ) { + global $wpdb; + + if ( ! $download->get_id() ) { + throw new Exception( __( 'Invalid download.', 'woocommerce' ) ); + } + + $download->set_defaults(); + $raw_download = $wpdb->get_row( + $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE permission_id = %d", + $download->get_id() + ) + ); + + if ( ! $raw_download ) { + throw new Exception( __( 'Invalid download.', 'woocommerce' ) ); + } + + $download->set_props( + array( + 'download_id' => $raw_download->download_id, + 'product_id' => $raw_download->product_id, + 'user_id' => $raw_download->user_id, + 'user_email' => $raw_download->user_email, + 'order_id' => $raw_download->order_id, + 'order_key' => $raw_download->order_key, + 'downloads_remaining' => $raw_download->downloads_remaining, + 'access_granted' => strtotime( $raw_download->access_granted ), + 'download_count' => $raw_download->download_count, + 'access_expires' => is_null( $raw_download->access_expires ) ? null : strtotime( $raw_download->access_expires ), + ) + ); + $download->set_object_read( true ); + } + + /** + * Method to update a download in the database. + * + * @param WC_Customer_Download $download WC_Customer_Download object. + */ + public function update( &$download ) { + global $wpdb; + + $data = array( + 'download_id' => $download->get_download_id( 'edit' ), + 'product_id' => $download->get_product_id( 'edit' ), + 'user_id' => $download->get_user_id( 'edit' ), + 'user_email' => $download->get_user_email( 'edit' ), + 'order_id' => $download->get_order_id( 'edit' ), + 'order_key' => $download->get_order_key( 'edit' ), + 'downloads_remaining' => $download->get_downloads_remaining( 'edit' ), + // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + 'access_granted' => date( 'Y-m-d', $download->get_access_granted( 'edit' )->getTimestamp() ), + 'download_count' => $download->get_download_count( 'edit' ), + // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + 'access_expires' => ! is_null( $download->get_access_expires( 'edit' ) ) ? date( 'Y-m-d', $download->get_access_expires( 'edit' )->getTimestamp() ) : null, + ); + + $format = array( + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%d', + '%s', + ); + + $wpdb->update( + $wpdb->prefix . 'woocommerce_downloadable_product_permissions', + $data, + array( + 'permission_id' => $download->get_id(), + ), + $format + ); + $download->apply_changes(); + } + + /** + * Method to delete a download permission from the database. + * + * @param WC_Customer_Download $download WC_Customer_Download object. + * @param array $args Array of args to pass to the delete method. + */ + public function delete( &$download, $args = array() ) { + global $wpdb; + + $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE permission_id = %d", + $download->get_id() + ) + ); + + $download->set_id( 0 ); + } + + /** + * Method to delete a download permission from the database by ID. + * + * @param int $id permission_id of the download to be deleted. + */ + public function delete_by_id( $id ) { + global $wpdb; + $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE permission_id = %d", + $id + ) + ); + } + + /** + * Method to delete a download permission from the database by order ID. + * + * @param int $id Order ID of the downloads that will be deleted. + */ + public function delete_by_order_id( $id ) { + global $wpdb; + $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE order_id = %d", + $id + ) + ); + } + + /** + * Method to delete a download permission from the database by download ID. + * + * @param int $id download_id of the downloads that will be deleted. + */ + public function delete_by_download_id( $id ) { + global $wpdb; + $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE download_id = %s", + $id + ) + ); + } + + /** + * Method to delete a download permission from the database by user ID. + * + * @since 3.4.0 + * @param int $id user ID of the downloads that will be deleted. + * @return bool True if deleted rows. + */ + public function delete_by_user_id( $id ) { + global $wpdb; + return (bool) $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE user_id = %d", + $id + ) + ); + } + + /** + * Method to delete a download permission from the database by user email. + * + * @since 3.4.0 + * @param string $email email of the downloads that will be deleted. + * @return bool True if deleted rows. + */ + public function delete_by_user_email( $email ) { + global $wpdb; + return (bool) $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE user_email = %s", + $email + ) + ); + } + + /** + * Get a download object. + * + * @param array $data From the DB. + * @return WC_Customer_Download + */ + private function get_download( $data ) { + return new WC_Customer_Download( $data ); + } + + /** + * Get array of download ids by specified args. + * + * @param array $args Arguments to filter downloads. $args['return'] accepts the following values: 'objects' (default), 'ids' or a comma separated list of fields (for example: 'order_id,user_id,user_email'). + * @return array Can be an array of permission_ids, an array of WC_Customer_Download objects or an array of arrays containing specified fields depending on the value of $args['return']. + */ + public function get_downloads( $args = array() ) { + global $wpdb; + + $args = wp_parse_args( + $args, + array( + 'user_email' => '', + 'user_id' => '', + 'order_id' => '', + 'order_key' => '', + 'product_id' => '', + 'download_id' => '', + 'orderby' => 'permission_id', + 'order' => 'ASC', + 'limit' => -1, + 'page' => 1, + 'return' => 'objects', + ) + ); + + $valid_fields = array( 'permission_id', 'download_id', 'product_id', 'order_id', 'order_key', 'user_email', 'user_id', 'downloads_remaining', 'access_granted', 'access_expires', 'download_count' ); + $get_results_output = ARRAY_A; + + if ( 'ids' === $args['return'] ) { + $fields = 'permission_id'; + } elseif ( 'objects' === $args['return'] ) { + $fields = '*'; + $get_results_output = OBJECT; + } else { + $fields = explode( ',', (string) $args['return'] ); + $fields = implode( ', ', array_intersect( $fields, $valid_fields ) ); + } + + $query = array(); + $query[] = "SELECT {$fields} FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE 1=1"; + + if ( $args['user_email'] ) { + $query[] = $wpdb->prepare( 'AND user_email = %s', sanitize_email( $args['user_email'] ) ); + } + + if ( $args['user_id'] ) { + $query[] = $wpdb->prepare( 'AND user_id = %d', absint( $args['user_id'] ) ); + } + + if ( $args['order_id'] ) { + $query[] = $wpdb->prepare( 'AND order_id = %d', $args['order_id'] ); + } + + if ( $args['order_key'] ) { + $query[] = $wpdb->prepare( 'AND order_key = %s', $args['order_key'] ); + } + + if ( $args['product_id'] ) { + $query[] = $wpdb->prepare( 'AND product_id = %d', $args['product_id'] ); + } + + if ( $args['download_id'] ) { + $query[] = $wpdb->prepare( 'AND download_id = %s', $args['download_id'] ); + } + + $orderby = in_array( $args['orderby'], $valid_fields, true ) ? $args['orderby'] : 'permission_id'; + $order = 'DESC' === strtoupper( $args['order'] ) ? 'DESC' : 'ASC'; + $orderby_sql = sanitize_sql_orderby( "{$orderby} {$order}" ); + $query[] = "ORDER BY {$orderby_sql}"; + + if ( 0 < $args['limit'] ) { + $query[] = $wpdb->prepare( 'LIMIT %d, %d', absint( $args['limit'] ) * absint( $args['page'] - 1 ), absint( $args['limit'] ) ); + } + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $results = $wpdb->get_results( implode( ' ', $query ), $get_results_output ); + + switch ( $args['return'] ) { + case 'ids': + return wp_list_pluck( $results, 'permission_id' ); + case 'objects': + return array_map( array( $this, 'get_download' ), $results ); + default: + return $results; + } + } + + /** + * Update download ids if the hash changes. + * + * @deprecated 3.3.0 Download id is now a static UUID and should not be changed based on file hash. + * + * @param int $product_id Product ID. + * @param string $old_id Old download_id. + * @param string $new_id New download_id. + */ + public function update_download_id( $product_id, $old_id, $new_id ) { + global $wpdb; + + wc_deprecated_function( __METHOD__, '3.3' ); + + $wpdb->update( + $wpdb->prefix . 'woocommerce_downloadable_product_permissions', + array( + 'download_id' => $new_id, + ), + array( + 'download_id' => $old_id, + 'product_id' => $product_id, + ) + ); + } + + /** + * Get a customers downloads. + * + * @param int $customer_id Customer ID. + * @return array + */ + public function get_downloads_for_customer( $customer_id ) { + global $wpdb; + + return $wpdb->get_results( + $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions as permissions + WHERE user_id = %d + AND permissions.order_id > 0 + AND + ( + permissions.downloads_remaining > 0 + OR permissions.downloads_remaining = '' + ) + AND + ( + permissions.access_expires IS NULL + OR permissions.access_expires >= %s + OR permissions.access_expires = '0000-00-00 00:00:00' + ) + ORDER BY permissions.order_id, permissions.product_id, permissions.permission_id;", + $customer_id, + date( 'Y-m-d', current_time( 'timestamp' ) ) // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + ) + ); + } + + /** + * Update user prop for downloads based on order id. + * + * @param int $order_id Order ID. + * @param int $customer_id Customer ID. + * @param string $email Customer email address. + */ + public function update_user_by_order_id( $order_id, $customer_id, $email ) { + global $wpdb; + $wpdb->update( + $wpdb->prefix . 'woocommerce_downloadable_product_permissions', + array( + 'user_id' => $customer_id, + 'user_email' => $email, + ), + array( + 'order_id' => $order_id, + ), + array( + '%d', + '%s', + ), + array( + '%d', + ) + ); + } +} diff --git a/includes/data-stores/class-wc-customer-download-log-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-customer-download-log-data-store.php similarity index 100% rename from includes/data-stores/class-wc-customer-download-log-data-store.php rename to plugins/woocommerce/includes/data-stores/class-wc-customer-download-log-data-store.php diff --git a/includes/data-stores/class-wc-data-store-wp.php b/plugins/woocommerce/includes/data-stores/class-wc-data-store-wp.php similarity index 100% rename from includes/data-stores/class-wc-data-store-wp.php rename to plugins/woocommerce/includes/data-stores/class-wc-data-store-wp.php diff --git a/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php new file mode 100644 index 00000000000..bc055291c45 --- /dev/null +++ b/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php @@ -0,0 +1,1117 @@ +get_order_key() ) { + $order->set_order_key( wc_generate_order_key() ); + } + parent::create( $order ); + do_action( 'woocommerce_new_order', $order->get_id(), $order ); + } + + /** + * Read order data. Can be overridden by child classes to load other props. + * + * @param WC_Order $order Order object. + * @param object $post_object Post object. + * @since 3.0.0 + */ + protected function read_order_data( &$order, $post_object ) { + parent::read_order_data( $order, $post_object ); + $id = $order->get_id(); + $date_completed = get_post_meta( $id, '_date_completed', true ); + $date_paid = get_post_meta( $id, '_date_paid', true ); + + if ( ! $date_completed ) { + $date_completed = get_post_meta( $id, '_completed_date', true ); + } + + if ( ! $date_paid ) { + $date_paid = get_post_meta( $id, '_paid_date', true ); + } + + $order->set_props( + array( + 'order_key' => get_post_meta( $id, '_order_key', true ), + 'customer_id' => get_post_meta( $id, '_customer_user', true ), + 'billing_first_name' => get_post_meta( $id, '_billing_first_name', true ), + 'billing_last_name' => get_post_meta( $id, '_billing_last_name', true ), + 'billing_company' => get_post_meta( $id, '_billing_company', true ), + 'billing_address_1' => get_post_meta( $id, '_billing_address_1', true ), + 'billing_address_2' => get_post_meta( $id, '_billing_address_2', true ), + 'billing_city' => get_post_meta( $id, '_billing_city', true ), + 'billing_state' => get_post_meta( $id, '_billing_state', true ), + 'billing_postcode' => get_post_meta( $id, '_billing_postcode', true ), + 'billing_country' => get_post_meta( $id, '_billing_country', true ), + 'billing_email' => get_post_meta( $id, '_billing_email', true ), + 'billing_phone' => get_post_meta( $id, '_billing_phone', true ), + 'shipping_first_name' => get_post_meta( $id, '_shipping_first_name', true ), + 'shipping_last_name' => get_post_meta( $id, '_shipping_last_name', true ), + 'shipping_company' => get_post_meta( $id, '_shipping_company', true ), + 'shipping_address_1' => get_post_meta( $id, '_shipping_address_1', true ), + 'shipping_address_2' => get_post_meta( $id, '_shipping_address_2', true ), + 'shipping_city' => get_post_meta( $id, '_shipping_city', true ), + 'shipping_state' => get_post_meta( $id, '_shipping_state', true ), + 'shipping_postcode' => get_post_meta( $id, '_shipping_postcode', true ), + 'shipping_country' => get_post_meta( $id, '_shipping_country', true ), + 'shipping_phone' => get_post_meta( $id, '_shipping_phone', true ), + 'payment_method' => get_post_meta( $id, '_payment_method', true ), + 'payment_method_title' => get_post_meta( $id, '_payment_method_title', true ), + 'transaction_id' => get_post_meta( $id, '_transaction_id', true ), + 'customer_ip_address' => get_post_meta( $id, '_customer_ip_address', true ), + 'customer_user_agent' => get_post_meta( $id, '_customer_user_agent', true ), + 'created_via' => get_post_meta( $id, '_created_via', true ), + 'date_completed' => $date_completed, + 'date_paid' => $date_paid, + 'cart_hash' => get_post_meta( $id, '_cart_hash', true ), + 'customer_note' => $post_object->post_excerpt, + ) + ); + } + + /** + * Method to update an order in the database. + * + * @param WC_Order $order Order object. + */ + public function update( &$order ) { + // Before updating, ensure date paid is set if missing. + if ( ! $order->get_date_paid( 'edit' ) && version_compare( $order->get_version( 'edit' ), '3.0', '<' ) && $order->has_status( apply_filters( 'woocommerce_payment_complete_order_status', $order->needs_processing() ? 'processing' : 'completed', $order->get_id(), $order ) ) ) { + $order->set_date_paid( $order->get_date_created( 'edit' ) ); + } + + // Also grab the current status so we can compare. + $previous_status = get_post_status( $order->get_id() ); + + // Update the order. + parent::update( $order ); + + // Fire a hook depending on the status - this should be considered a creation if it was previously draft status. + $new_status = $order->get_status( 'edit' ); + + if ( $new_status !== $previous_status && in_array( $previous_status, array( 'new', 'auto-draft', 'draft' ), true ) ) { + do_action( 'woocommerce_new_order', $order->get_id(), $order ); + } else { + do_action( 'woocommerce_update_order', $order->get_id(), $order ); + } + } + + /** + * Helper method that updates all the post meta for an order based on it's settings in the WC_Order class. + * + * @param WC_Order $order Order object. + * @since 3.0.0 + */ + protected function update_post_meta( &$order ) { + $updated_props = array(); + $id = $order->get_id(); + $meta_key_to_props = array( + '_order_key' => 'order_key', + '_customer_user' => 'customer_id', + '_payment_method' => 'payment_method', + '_payment_method_title' => 'payment_method_title', + '_transaction_id' => 'transaction_id', + '_customer_ip_address' => 'customer_ip_address', + '_customer_user_agent' => 'customer_user_agent', + '_created_via' => 'created_via', + '_date_completed' => 'date_completed', + '_date_paid' => 'date_paid', + '_cart_hash' => 'cart_hash', + ); + + $props_to_update = $this->get_props_to_update( $order, $meta_key_to_props ); + + foreach ( $props_to_update as $meta_key => $prop ) { + $value = $order->{"get_$prop"}( 'edit' ); + $value = is_string( $value ) ? wp_slash( $value ) : $value; + switch ( $prop ) { + case 'date_paid': + case 'date_completed': + $value = ! is_null( $value ) ? $value->getTimestamp() : ''; + break; + } + + $updated = $this->update_or_delete_post_meta( $order, $meta_key, $value ); + + if ( $updated ) { + $updated_props[] = $prop; + } + } + + $address_props = array( + 'billing' => array( + '_billing_first_name' => 'billing_first_name', + '_billing_last_name' => 'billing_last_name', + '_billing_company' => 'billing_company', + '_billing_address_1' => 'billing_address_1', + '_billing_address_2' => 'billing_address_2', + '_billing_city' => 'billing_city', + '_billing_state' => 'billing_state', + '_billing_postcode' => 'billing_postcode', + '_billing_country' => 'billing_country', + '_billing_email' => 'billing_email', + '_billing_phone' => 'billing_phone', + ), + 'shipping' => array( + '_shipping_first_name' => 'shipping_first_name', + '_shipping_last_name' => 'shipping_last_name', + '_shipping_company' => 'shipping_company', + '_shipping_address_1' => 'shipping_address_1', + '_shipping_address_2' => 'shipping_address_2', + '_shipping_city' => 'shipping_city', + '_shipping_state' => 'shipping_state', + '_shipping_postcode' => 'shipping_postcode', + '_shipping_country' => 'shipping_country', + '_shipping_phone' => 'shipping_phone', + ), + ); + + foreach ( $address_props as $props_key => $props ) { + $props_to_update = $this->get_props_to_update( $order, $props ); + foreach ( $props_to_update as $meta_key => $prop ) { + $value = $order->{"get_$prop"}( 'edit' ); + $value = is_string( $value ) ? wp_slash( $value ) : $value; + $updated = $this->update_or_delete_post_meta( $order, $meta_key, $value ); + + if ( $updated ) { + $updated_props[] = $prop; + $updated_props[] = $props_key; + } + } + } + + parent::update_post_meta( $order ); + + // If address changed, store concatenated version to make searches faster. + if ( in_array( 'billing', $updated_props, true ) || ! metadata_exists( 'post', $id, '_billing_address_index' ) ) { + update_post_meta( $id, '_billing_address_index', implode( ' ', $order->get_address( 'billing' ) ) ); + } + if ( in_array( 'shipping', $updated_props, true ) || ! metadata_exists( 'post', $id, '_shipping_address_index' ) ) { + update_post_meta( $id, '_shipping_address_index', implode( ' ', $order->get_address( 'shipping' ) ) ); + } + + // Legacy date handling. @todo remove in 4.0. + if ( in_array( 'date_paid', $updated_props, true ) ) { + $value = $order->get_date_paid( 'edit' ); + // In 2.6.x date_paid was stored as _paid_date in local mysql format. + update_post_meta( $id, '_paid_date', ! is_null( $value ) ? $value->date( 'Y-m-d H:i:s' ) : '' ); + } + + if ( in_array( 'date_completed', $updated_props, true ) ) { + $value = $order->get_date_completed( 'edit' ); + // In 2.6.x date_completed was stored as _completed_date in local mysql format. + update_post_meta( $id, '_completed_date', ! is_null( $value ) ? $value->date( 'Y-m-d H:i:s' ) : '' ); + } + + // If customer changed, update any downloadable permissions. + if ( in_array( 'customer_id', $updated_props ) || in_array( 'billing_email', $updated_props ) ) { + $data_store = WC_Data_Store::load( 'customer-download' ); + $data_store->update_user_by_order_id( $id, $order->get_customer_id(), $order->get_billing_email() ); + } + + // Mark user account as active. + if ( in_array( 'customer_id', $updated_props, true ) ) { + wc_update_user_last_active( $order->get_customer_id() ); + } + + do_action( 'woocommerce_order_object_updated_props', $order, $updated_props ); + } + + /** + * Excerpt for post. + * + * @param WC_Order $order Order object. + * @return string + */ + protected function get_post_excerpt( $order ) { + return $order->get_customer_note(); + } + + /** + * Get order key. + * + * @since 4.3.0 + * @param WC_order $order Order object. + * @return string + */ + protected function get_order_key( $order ) { + if ( '' !== $order->get_order_key() ) { + return $order->get_order_key(); + } + + return parent::get_order_key( $order ); + } + + /** + * Get amount already refunded. + * + * @param WC_Order $order Order object. + * @return float + */ + public function get_total_refunded( $order ) { + global $wpdb; + + $total = $wpdb->get_var( + $wpdb->prepare( + "SELECT SUM( postmeta.meta_value ) + FROM $wpdb->postmeta AS postmeta + INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) + WHERE postmeta.meta_key = '_refund_amount' + AND postmeta.post_id = posts.ID", + $order->get_id() + ) + ); + + return floatval( $total ); + } + + /** + * Get the total tax refunded. + * + * @param WC_Order $order Order object. + * @return float + */ + public function get_total_tax_refunded( $order ) { + global $wpdb; + + $total = $wpdb->get_var( + $wpdb->prepare( + "SELECT SUM( order_itemmeta.meta_value ) + FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta + INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) + INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'tax' ) + WHERE order_itemmeta.order_item_id = order_items.order_item_id + AND order_itemmeta.meta_key IN ('tax_amount', 'shipping_tax_amount')", + $order->get_id() + ) + ); + + return abs( $total ); + } + + /** + * Get the total shipping refunded. + * + * @param WC_Order $order Order object. + * @return float + */ + public function get_total_shipping_refunded( $order ) { + global $wpdb; + + $total = $wpdb->get_var( + $wpdb->prepare( + "SELECT SUM( order_itemmeta.meta_value ) + FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta + INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) + INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'shipping' ) + WHERE order_itemmeta.order_item_id = order_items.order_item_id + AND order_itemmeta.meta_key IN ('cost')", + $order->get_id() + ) + ); + + return abs( $total ); + } + + /** + * Finds an Order ID based on an order key. + * + * @param string $order_key An order key has generated by. + * @return int The ID of an order, or 0 if the order could not be found + */ + public function get_order_id_by_order_key( $order_key ) { + global $wpdb; + return $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->prefix}postmeta WHERE meta_key = '_order_key' AND meta_value = %s", $order_key ) ); + } + + /** + * Return count of orders with a specific status. + * + * @param string $status Order status. Function wc_get_order_statuses() returns a list of valid statuses. + * @return int + */ + public function get_order_count( $status ) { + global $wpdb; + return absint( $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( * ) FROM {$wpdb->posts} WHERE post_type = 'shop_order' AND post_status = %s", $status ) ) ); + } + + /** + * Get all orders matching the passed in args. + * + * @deprecated 3.1.0 - Use wc_get_orders instead. + * @see wc_get_orders() + * + * @param array $args List of args passed to wc_get_orders(). + * + * @return array|object + */ + public function get_orders( $args = array() ) { + wc_deprecated_function( 'WC_Order_Data_Store_CPT::get_orders', '3.1.0', 'Use wc_get_orders instead.' ); + return wc_get_orders( $args ); + } + + /** + * Generate meta query for wc_get_orders. + * + * @param array $values List of customers ids or emails. + * @param string $relation 'or' or 'and' relation used to build the WP meta_query. + * @return array + */ + private function get_orders_generate_customer_meta_query( $values, $relation = 'or' ) { + $meta_query = array( + 'relation' => strtoupper( $relation ), + 'customer_emails' => array( + 'key' => '_billing_email', + 'value' => array(), + 'compare' => 'IN', + ), + 'customer_ids' => array( + 'key' => '_customer_user', + 'value' => array(), + 'compare' => 'IN', + ), + ); + foreach ( $values as $value ) { + if ( is_array( $value ) ) { + $query_part = $this->get_orders_generate_customer_meta_query( $value, 'and' ); + if ( is_wp_error( $query_part ) ) { + return $query_part; + } + $meta_query[] = $query_part; + } elseif ( is_email( $value ) ) { + $meta_query['customer_emails']['value'][] = sanitize_email( $value ); + } elseif ( is_numeric( $value ) ) { + $meta_query['customer_ids']['value'][] = strval( absint( $value ) ); + } else { + return new WP_Error( 'woocommerce_query_invalid', __( 'Invalid customer query.', 'woocommerce' ), $values ); + } + } + + if ( empty( $meta_query['customer_emails']['value'] ) ) { + unset( $meta_query['customer_emails'] ); + unset( $meta_query['relation'] ); + } + + if ( empty( $meta_query['customer_ids']['value'] ) ) { + unset( $meta_query['customer_ids'] ); + unset( $meta_query['relation'] ); + } + + return $meta_query; + } + + /** + * Get unpaid orders after a certain date, + * + * @param int $date Timestamp. + * @return array + */ + public function get_unpaid_orders( $date ) { + global $wpdb; + + $unpaid_orders = $wpdb->get_col( + $wpdb->prepare( + // @codingStandardsIgnoreStart + "SELECT posts.ID + FROM {$wpdb->posts} AS posts + WHERE posts.post_type IN ('" . implode( "','", wc_get_order_types() ) . "') + AND posts.post_status = 'wc-pending' + AND posts.post_modified < %s", + // @codingStandardsIgnoreEnd + gmdate( 'Y-m-d H:i:s', absint( $date ) ) + ) + ); + + return $unpaid_orders; + } + + /** + * Search order data for a term and return ids. + * + * @param string $term Searched term. + * @return array of ids + */ + public function search_orders( $term ) { + global $wpdb; + + /** + * Searches on meta data can be slow - this lets you choose what fields to search. + * 3.0.0 added _billing_address and _shipping_address meta which contains all address data to make this faster. + * This however won't work on older orders unless updated, so search a few others (expand this using the filter if needed). + * + * @var array + */ + $search_fields = array_map( + 'wc_clean', + apply_filters( + 'woocommerce_shop_order_search_fields', + array( + '_billing_address_index', + '_shipping_address_index', + '_billing_last_name', + '_billing_email', + ) + ) + ); + $order_ids = array(); + + if ( is_numeric( $term ) ) { + $order_ids[] = absint( $term ); + } + + if ( ! empty( $search_fields ) ) { + $order_ids = array_unique( + array_merge( + $order_ids, + $wpdb->get_col( + $wpdb->prepare( + "SELECT DISTINCT p1.post_id FROM {$wpdb->postmeta} p1 WHERE p1.meta_value LIKE %s AND p1.meta_key IN ('" . implode( "','", array_map( 'esc_sql', $search_fields ) ) . "')", // @codingStandardsIgnoreLine + '%' . $wpdb->esc_like( wc_clean( $term ) ) . '%' + ) + ), + $wpdb->get_col( + $wpdb->prepare( + "SELECT order_id + FROM {$wpdb->prefix}woocommerce_order_items as order_items + WHERE order_item_name LIKE %s", + '%' . $wpdb->esc_like( wc_clean( $term ) ) . '%' + ) + ) + ) + ); + } + + return apply_filters( 'woocommerce_shop_order_search_results', $order_ids, $term, $search_fields ); + } + + /** + * Gets information about whether permissions were generated yet. + * + * @param WC_Order|int $order Order ID or order object. + * @return bool + */ + public function get_download_permissions_granted( $order ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + return wc_string_to_bool( get_post_meta( $order_id, '_download_permissions_granted', true ) ); + } + + /** + * Stores information about whether permissions were generated yet. + * + * @param WC_Order|int $order Order ID or order object. + * @param bool $set True or false. + */ + public function set_download_permissions_granted( $order, $set ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + update_post_meta( $order_id, '_download_permissions_granted', wc_bool_to_string( $set ) ); + } + + /** + * Gets information about whether sales were recorded. + * + * @param WC_Order|int $order Order ID or order object. + * @return bool + */ + public function get_recorded_sales( $order ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + return wc_string_to_bool( get_post_meta( $order_id, '_recorded_sales', true ) ); + } + + /** + * Stores information about whether sales were recorded. + * + * @param WC_Order|int $order Order ID or order object. + * @param bool $set True or false. + */ + public function set_recorded_sales( $order, $set ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + update_post_meta( $order_id, '_recorded_sales', wc_bool_to_string( $set ) ); + } + + /** + * Gets information about whether coupon counts were updated. + * + * @param WC_Order|int $order Order ID or order object. + * @return bool + */ + public function get_recorded_coupon_usage_counts( $order ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + return wc_string_to_bool( get_post_meta( $order_id, '_recorded_coupon_usage_counts', true ) ); + } + + /** + * Stores information about whether coupon counts were updated. + * + * @param WC_Order|int $order Order ID or order object. + * @param bool $set True or false. + */ + public function set_recorded_coupon_usage_counts( $order, $set ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + update_post_meta( $order_id, '_recorded_coupon_usage_counts', wc_bool_to_string( $set ) ); + } + + /** + * Return array of coupon_code => meta_key for coupon which have usage limit and have tentative keys. + * Pass $coupon_id if key for only one of the coupon is needed. + * + * @param WC_Order $order Order object. + * @param int $coupon_id If passed, will return held key for that coupon. + * + * @return array|string Key value pair for coupon code and meta key name. If $coupon_id is passed, returns meta_key for only that coupon. + */ + public function get_coupon_held_keys( $order, $coupon_id = null ) { + $held_keys = $order->get_meta( '_coupon_held_keys' ); + if ( $coupon_id ) { + return isset( $held_keys[ $coupon_id ] ) ? $held_keys[ $coupon_id ] : null; + } + return $held_keys; + } + + /** + * Return array of coupon_code => meta_key for coupon which have usage limit per customer and have tentative keys. + * + * @param WC_Order $order Order object. + * @param int $coupon_id If passed, will return held key for that coupon. + * + * @return mixed + */ + public function get_coupon_held_keys_for_users( $order, $coupon_id = null ) { + $held_keys_for_user = $order->get_meta( '_coupon_held_keys_for_users' ); + if ( $coupon_id ) { + return isset( $held_keys_for_user[ $coupon_id ] ) ? $held_keys_for_user[ $coupon_id ] : null; + } + return $held_keys_for_user; + } + + /** + * Add/Update list of meta keys that are currently being used by this order to hold a coupon. + * This is used to figure out what all meta entries we should delete when order is cancelled/completed. + * + * @param WC_Order $order Order object. + * @param array $held_keys Array of coupon_code => meta_key. + * @param array $held_keys_for_user Array of coupon_code => meta_key for held coupon for user. + * + * @return mixed + */ + public function set_coupon_held_keys( $order, $held_keys, $held_keys_for_user ) { + if ( is_array( $held_keys ) && 0 < count( $held_keys ) ) { + $order->update_meta_data( '_coupon_held_keys', $held_keys ); + } + if ( is_array( $held_keys_for_user ) && 0 < count( $held_keys_for_user ) ) { + $order->update_meta_data( '_coupon_held_keys_for_users', $held_keys_for_user ); + } + } + + /** + * Release all coupons held by this order. + * + * @param WC_Order $order Current order object. + * @param bool $save Whether to delete keys from DB right away. Could be useful to pass `false` if you are building a bulk request. + */ + public function release_held_coupons( $order, $save = true ) { + $coupon_held_keys = $this->get_coupon_held_keys( $order ); + if ( is_array( $coupon_held_keys ) ) { + foreach ( $coupon_held_keys as $coupon_id => $meta_key ) { + delete_post_meta( $coupon_id, $meta_key ); + } + } + $order->delete_meta_data( '_coupon_held_keys' ); + + $coupon_held_keys_for_users = $this->get_coupon_held_keys_for_users( $order ); + if ( is_array( $coupon_held_keys_for_users ) ) { + foreach ( $coupon_held_keys_for_users as $coupon_id => $meta_key ) { + delete_post_meta( $coupon_id, $meta_key ); + } + } + $order->delete_meta_data( '_coupon_held_keys_for_users' ); + + if ( $save ) { + $order->save_meta_data(); + } + + } + + /** + * Gets information about whether stock was reduced. + * + * @param WC_Order|int $order Order ID or order object. + * @return bool + */ + public function get_stock_reduced( $order ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + return wc_string_to_bool( get_post_meta( $order_id, '_order_stock_reduced', true ) ); + } + + /** + * Stores information about whether stock was reduced. + * + * @param WC_Order|int $order Order ID or order object. + * @param bool $set True or false. + */ + public function set_stock_reduced( $order, $set ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + update_post_meta( $order_id, '_order_stock_reduced', wc_bool_to_string( $set ) ); + } + + /** + * Get the order type based on Order ID. + * + * @since 3.0.0 + * @param int|WP_Post $order Order | Order id. + * + * @return string + */ + public function get_order_type( $order ) { + return get_post_type( $order ); + } + + /** + * Get valid WP_Query args from a WC_Order_Query's query variables. + * + * @since 3.1.0 + * @param array $query_vars query vars from a WC_Order_Query. + * @return array + */ + protected function get_wp_query_args( $query_vars ) { + + // Map query vars to ones that get_wp_query_args or WP_Query recognize. + $key_mapping = array( + 'customer_id' => 'customer_user', + 'status' => 'post_status', + 'currency' => 'order_currency', + 'version' => 'order_version', + 'discount_total' => 'cart_discount', + 'discount_tax' => 'cart_discount_tax', + 'shipping_total' => 'order_shipping', + 'shipping_tax' => 'order_shipping_tax', + 'cart_tax' => 'order_tax', + 'total' => 'order_total', + 'page' => 'paged', + ); + + foreach ( $key_mapping as $query_key => $db_key ) { + if ( isset( $query_vars[ $query_key ] ) ) { + $query_vars[ $db_key ] = $query_vars[ $query_key ]; + unset( $query_vars[ $query_key ] ); + } + } + + // Add the 'wc-' prefix to status if needed. + if ( ! empty( $query_vars['post_status'] ) ) { + if ( is_array( $query_vars['post_status'] ) ) { + foreach ( $query_vars['post_status'] as &$status ) { + $status = wc_is_order_status( 'wc-' . $status ) ? 'wc-' . $status : $status; + } + } else { + $query_vars['post_status'] = wc_is_order_status( 'wc-' . $query_vars['post_status'] ) ? 'wc-' . $query_vars['post_status'] : $query_vars['post_status']; + } + } + + $wp_query_args = parent::get_wp_query_args( $query_vars ); + + if ( ! isset( $wp_query_args['date_query'] ) ) { + $wp_query_args['date_query'] = array(); + } + if ( ! isset( $wp_query_args['meta_query'] ) ) { + $wp_query_args['meta_query'] = array(); + } + + $date_queries = array( + 'date_created' => 'post_date', + 'date_modified' => 'post_modified', + 'date_completed' => '_date_completed', + 'date_paid' => '_date_paid', + ); + foreach ( $date_queries as $query_var_key => $db_key ) { + if ( isset( $query_vars[ $query_var_key ] ) && '' !== $query_vars[ $query_var_key ] ) { + + // Remove any existing meta queries for the same keys to prevent conflicts. + $existing_queries = wp_list_pluck( $wp_query_args['meta_query'], 'key', true ); + $meta_query_index = array_search( $db_key, $existing_queries, true ); + if ( false !== $meta_query_index ) { + unset( $wp_query_args['meta_query'][ $meta_query_index ] ); + } + + $wp_query_args = $this->parse_date_for_wp_query( $query_vars[ $query_var_key ], $db_key, $wp_query_args ); + } + } + + if ( isset( $query_vars['customer'] ) && '' !== $query_vars['customer'] && array() !== $query_vars['customer'] ) { + $values = is_array( $query_vars['customer'] ) ? $query_vars['customer'] : array( $query_vars['customer'] ); + $customer_query = $this->get_orders_generate_customer_meta_query( $values ); + if ( is_wp_error( $customer_query ) ) { + $wp_query_args['errors'][] = $customer_query; + } else { + $wp_query_args['meta_query'][] = $customer_query; + } + } + + if ( isset( $query_vars['anonymized'] ) ) { + if ( $query_vars['anonymized'] ) { + $wp_query_args['meta_query'][] = array( + 'key' => '_anonymized', + 'value' => 'yes', + ); + } else { + $wp_query_args['meta_query'][] = array( + 'key' => '_anonymized', + 'compare' => 'NOT EXISTS', + ); + } + } + + if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) { + $wp_query_args['no_found_rows'] = true; + } + + return apply_filters( 'woocommerce_order_data_store_cpt_get_orders_query', $wp_query_args, $query_vars, $this ); + } + + /** + * Query for Orders matching specific criteria. + * + * @since 3.1.0 + * + * @param array $query_vars query vars from a WC_Order_Query. + * + * @return array|object + */ + public function query( $query_vars ) { + $args = $this->get_wp_query_args( $query_vars ); + + if ( ! empty( $args['errors'] ) ) { + $query = (object) array( + 'posts' => array(), + 'found_posts' => 0, + 'max_num_pages' => 0, + ); + } else { + $query = new WP_Query( $args ); + } + + if ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) { + $orders = $query->posts; + } else { + update_post_caches( $query->posts ); // We already fetching posts, might as well hydrate some caches. + $order_ids = wp_list_pluck( $query->posts, 'ID' ); + $orders = $this->compile_orders( $order_ids, $query_vars, $query ); + } + + if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) { + return (object) array( + 'orders' => $orders, + 'total' => $query->found_posts, + 'max_num_pages' => $query->max_num_pages, + ); + } + + return $orders; + } + + /** + * Compile order response and set caches as needed for order ids. + * + * @param array $order_ids List of order IDS to compile. + * @param array $query_vars Original query arguments. + * @param WP_Query $query Query object. + * + * @return array Orders. + */ + private function compile_orders( $order_ids, $query_vars, $query ) { + if ( empty( $order_ids ) ) { + return array(); + } + $orders = array(); + + // Lets do some cache hydrations so that we don't have to fetch data from DB for every order. + $this->prime_raw_meta_cache_for_orders( $order_ids, $query_vars ); + $this->prime_refund_caches_for_order( $order_ids, $query_vars ); + $this->prime_order_item_caches_for_orders( $order_ids, $query_vars ); + + foreach ( $query->posts as $post ) { + $order = wc_get_order( $post ); + + // If the order returns false, don't add it to the list. + if ( false === $order ) { + continue; + } + + $orders[] = $order; + } + + return $orders; + } + + /** + * Prime refund cache for orders. + * + * @param array $order_ids Order Ids to prime cache for. + * @param array $query_vars Query vars for the query. + */ + private function prime_refund_caches_for_order( $order_ids, $query_vars ) { + if ( ! isset( $query_vars['type'] ) || ! ( 'shop_order' === $query_vars['type'] ) ) { + return; + } + if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) { + if ( is_array( $query_vars['fields'] ) && ! in_array( 'refunds', $query_vars['fields'] ) ) { + return; + } + } + $cache_keys_mapping = array(); + foreach ( $order_ids as $order_id ) { + $cache_keys_mapping[ $order_id ] = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $order_id; + } + $non_cached_ids = array(); + $cache_values = wc_cache_get_multiple( array_values( $cache_keys_mapping ), 'orders' ); + foreach ( $order_ids as $order_id ) { + if ( false === $cache_values[ $cache_keys_mapping[ $order_id ] ] ) { + $non_cached_ids[] = $order_id; + } + } + if ( empty( $non_cached_ids ) ) { + return; + } + + $refunds = wc_get_orders( + array( + 'type' => 'shop_order_refund', + 'post_parent__in' => $non_cached_ids, + 'limit' => - 1, + ) + ); + $order_refunds = array_reduce( + $refunds, + function ( $order_refunds_array, WC_Order_Refund $refund ) { + if ( ! isset( $order_refunds_array[ $refund->get_parent_id() ] ) ) { + $order_refunds_array[ $refund->get_parent_id() ] = array(); + } + $order_refunds_array[ $refund->get_parent_id() ][] = $refund; + return $order_refunds_array; + }, + array() + ); + foreach ( $non_cached_ids as $order_id ) { + $refunds = array(); + if ( isset( $order_refunds[ $order_id ] ) ) { + $refunds = $order_refunds[ $order_id ]; + } + wp_cache_set( $cache_keys_mapping[ $order_id ], $refunds, 'orders' ); + } + } + + /** + * Prime following caches: + * 1. item-$order_item_id For individual items. + * 2. order-items-$order-id For fetching items associated with an order. + * 3. order-item meta. + * + * @param array $order_ids Order Ids to prime cache for. + * @param array $query_vars Query vars for the query. + */ + private function prime_order_item_caches_for_orders( $order_ids, $query_vars ) { + global $wpdb; + if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) { + $line_items = array( + 'line_items', + 'shipping_lines', + 'fee_lines', + 'coupon_lines', + ); + + if ( is_array( $query_vars['fields'] ) && 0 === count( array_intersect( $line_items, $query_vars['fields'] ) ) ) { + return; + } + } + $cache_keys = array_map( + function ( $order_id ) { + return 'order-items-' . $order_id; + }, + $order_ids + ); + $cache_values = wc_cache_get_multiple( $cache_keys, 'orders' ); + $non_cached_ids = array(); + foreach ( $order_ids as $order_id ) { + if ( false === $cache_values[ 'order-items-' . $order_id ] ) { + $non_cached_ids[] = $order_id; + } + } + if ( empty( $non_cached_ids ) ) { + return; + } + + $non_cached_ids = esc_sql( $non_cached_ids ); + $non_cached_ids_string = implode( ',', $non_cached_ids ); + $order_items = $wpdb->get_results( + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "SELECT order_item_type, order_item_id, order_id, order_item_name FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id in ( $non_cached_ids_string ) ORDER BY order_item_id;" + ); + if ( empty( $order_items ) ) { + return; + } + + $order_items_for_all_orders = array_reduce( + $order_items, + function ( $order_items_collection, $order_item ) { + if ( ! isset( $order_items_collection[ $order_item->order_id ] ) ) { + $order_items_collection[ $order_item->order_id ] = array(); + } + $order_items_collection[ $order_item->order_id ][] = $order_item; + return $order_items_collection; + } + ); + foreach ( $order_items_for_all_orders as $order_id => $items ) { + wp_cache_set( 'order-items-' . $order_id, $items, 'orders' ); + } + foreach ( $order_items as $item ) { + wp_cache_set( 'item-' . $item->order_item_id, $item, 'order-items' ); + } + $order_item_ids = wp_list_pluck( $order_items, 'order_item_id' ); + update_meta_cache( 'order_item', $order_item_ids ); + } + + /** + * Prime cache for raw meta data for orders in bulk. Difference between this and WP built-in metadata is that this method also fetches `meta_id` field which we use and cache it. + * + * @param array $order_ids Order Ids to prime cache for. + * @param array $query_vars Query vars for the query. + */ + private function prime_raw_meta_cache_for_orders( $order_ids, $query_vars ) { + global $wpdb; + + if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) { + if ( is_array( $query_vars['fields'] ) && ! in_array( 'meta_data', $query_vars['fields'] ) ) { + return; + } + } + + $cache_keys_mapping = array(); + foreach ( $order_ids as $order_id ) { + $cache_keys_mapping[ $order_id ] = WC_Order::generate_meta_cache_key( $order_id, 'orders' ); + } + $cache_values = wc_cache_get_multiple( array_values( $cache_keys_mapping ), 'orders' ); + $non_cached_ids = array(); + foreach ( $order_ids as $order_id ) { + if ( false === $cache_values[ $cache_keys_mapping[ $order_id ] ] ) { + $non_cached_ids[] = $order_id; + } + } + if ( empty( $non_cached_ids ) ) { + return; + } + $order_ids = esc_sql( $non_cached_ids ); + $order_ids_in = "'" . implode( "', '", $order_ids ) . "'"; + $raw_meta_data_array = $wpdb->get_results( + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "SELECT post_id as object_id, meta_id, meta_key, meta_value + FROM {$wpdb->postmeta} + WHERE post_id IN ( $order_ids_in ) + ORDER BY post_id" + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + ); + $raw_meta_data_collection = array_reduce( + $raw_meta_data_array, + function ( $collection, $raw_meta_data ) { + if ( ! isset( $collection[ $raw_meta_data->object_id ] ) ) { + $collection[ $raw_meta_data->object_id ] = array(); + } + $collection[ $raw_meta_data->object_id ][] = $raw_meta_data; + return $collection; + }, + array() + ); + WC_Order::prime_raw_meta_data_cache( $raw_meta_data_collection, 'orders' ); + } + + /** + * Return the order type of a given item which belongs to WC_Order. + * + * @since 3.2.0 + * @param WC_Order $order Order Object. + * @param int $order_item_id Order item id. + * @return string Order Item type + */ + public function get_order_item_type( $order, $order_item_id ) { + global $wpdb; + return $wpdb->get_var( $wpdb->prepare( "SELECT DISTINCT order_item_type FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d and order_item_id = %d;", $order->get_id(), $order_item_id ) ); + } +} diff --git a/includes/data-stores/class-wc-order-item-coupon-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-order-item-coupon-data-store.php similarity index 100% rename from includes/data-stores/class-wc-order-item-coupon-data-store.php rename to plugins/woocommerce/includes/data-stores/class-wc-order-item-coupon-data-store.php diff --git a/includes/data-stores/class-wc-order-item-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-order-item-data-store.php similarity index 100% rename from includes/data-stores/class-wc-order-item-data-store.php rename to plugins/woocommerce/includes/data-stores/class-wc-order-item-data-store.php diff --git a/includes/data-stores/class-wc-order-item-fee-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-order-item-fee-data-store.php similarity index 100% rename from includes/data-stores/class-wc-order-item-fee-data-store.php rename to plugins/woocommerce/includes/data-stores/class-wc-order-item-fee-data-store.php diff --git a/includes/data-stores/class-wc-order-item-product-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-order-item-product-data-store.php similarity index 100% rename from includes/data-stores/class-wc-order-item-product-data-store.php rename to plugins/woocommerce/includes/data-stores/class-wc-order-item-product-data-store.php diff --git a/includes/data-stores/class-wc-order-item-shipping-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-order-item-shipping-data-store.php similarity index 100% rename from includes/data-stores/class-wc-order-item-shipping-data-store.php rename to plugins/woocommerce/includes/data-stores/class-wc-order-item-shipping-data-store.php diff --git a/includes/data-stores/class-wc-order-item-tax-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-order-item-tax-data-store.php similarity index 100% rename from includes/data-stores/class-wc-order-item-tax-data-store.php rename to plugins/woocommerce/includes/data-stores/class-wc-order-item-tax-data-store.php diff --git a/includes/data-stores/class-wc-order-refund-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-order-refund-data-store-cpt.php similarity index 100% rename from includes/data-stores/class-wc-order-refund-data-store-cpt.php rename to plugins/woocommerce/includes/data-stores/class-wc-order-refund-data-store-cpt.php diff --git a/plugins/woocommerce/includes/data-stores/class-wc-payment-token-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-payment-token-data-store.php new file mode 100644 index 00000000000..11e510a8724 --- /dev/null +++ b/plugins/woocommerce/includes/data-stores/class-wc-payment-token-data-store.php @@ -0,0 +1,372 @@ +validate() ) { + throw new Exception( __( 'Invalid or missing payment token fields.', 'woocommerce' ) ); + } + + global $wpdb; + if ( ! $token->is_default() && $token->get_user_id() > 0 ) { + $default_token = WC_Payment_Tokens::get_customer_default_token( $token->get_user_id() ); + if ( is_null( $default_token ) ) { + $token->set_default( true ); + } + } + + $payment_token_data = array( + 'gateway_id' => $token->get_gateway_id( 'edit' ), + 'token' => $token->get_token( 'edit' ), + 'user_id' => $token->get_user_id( 'edit' ), + 'type' => $token->get_type( 'edit' ), + ); + + $wpdb->insert( $wpdb->prefix . 'woocommerce_payment_tokens', $payment_token_data ); + $token_id = $wpdb->insert_id; + $token->set_id( $token_id ); + $this->save_extra_data( $token, true ); + $token->save_meta_data(); + $token->apply_changes(); + + // Make sure all other tokens are not set to default. + if ( $token->is_default() && $token->get_user_id() > 0 ) { + WC_Payment_Tokens::set_users_default( $token->get_user_id(), $token_id ); + } + + do_action( 'woocommerce_new_payment_token', $token_id, $token ); + } + + /** + * Update a payment token. + * + * @since 3.0.0 + * + * @param WC_Payment_Token $token Payment token object. + * + * @throws Exception Throw exception if invalid or missing payment token fields. + */ + public function update( &$token ) { + if ( false === $token->validate() ) { + throw new Exception( __( 'Invalid or missing payment token fields.', 'woocommerce' ) ); + } + + global $wpdb; + + $updated_props = array(); + $core_props = array( 'gateway_id', 'token', 'user_id', 'type' ); + $changed_props = array_keys( $token->get_changes() ); + + foreach ( $changed_props as $prop ) { + if ( ! in_array( $prop, $core_props, true ) ) { + continue; + } + $updated_props[] = $prop; + $payment_token_data[ $prop ] = $token->{'get_' . $prop}( 'edit' ); + } + + if ( ! empty( $payment_token_data ) ) { + $wpdb->update( + $wpdb->prefix . 'woocommerce_payment_tokens', + $payment_token_data, + array( 'token_id' => $token->get_id() ) + ); + } + + $updated_extra_props = $this->save_extra_data( $token ); + $updated_props = array_merge( $updated_props, $updated_extra_props ); + $token->save_meta_data(); + $token->apply_changes(); + + // Make sure all other tokens are not set to default. + if ( $token->is_default() && $token->get_user_id() > 0 ) { + WC_Payment_Tokens::set_users_default( $token->get_user_id(), $token->get_id() ); + } + + do_action( 'woocommerce_payment_token_object_updated_props', $token, $updated_props ); + do_action( 'woocommerce_payment_token_updated', $token->get_id() ); + } + + /** + * Remove a payment token from the database. + * + * @since 3.0.0 + * @param WC_Payment_Token $token Payment token object. + * @param bool $force_delete Unused param. + */ + public function delete( &$token, $force_delete = false ) { + global $wpdb; + $wpdb->delete( $wpdb->prefix . 'woocommerce_payment_tokens', array( 'token_id' => $token->get_id() ), array( '%d' ) ); + $wpdb->delete( $wpdb->prefix . 'woocommerce_payment_tokenmeta', array( 'payment_token_id' => $token->get_id() ), array( '%d' ) ); + do_action( 'woocommerce_payment_token_deleted', $token->get_id(), $token ); + } + + /** + * Read a token from the database. + * + * @since 3.0.0 + * + * @param WC_Payment_Token $token Payment token object. + * + * @throws Exception Throw exception if invalid payment token. + */ + public function read( &$token ) { + global $wpdb; + + $data = $wpdb->get_row( + $wpdb->prepare( + "SELECT token, user_id, gateway_id, is_default FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d LIMIT 1", + $token->get_id() + ) + ); + + if ( $data ) { + $token->set_props( + array( + 'token' => $data->token, + 'user_id' => $data->user_id, + 'gateway_id' => $data->gateway_id, + 'default' => $data->is_default, + ) + ); + $this->read_extra_data( $token ); + $token->read_meta_data(); + $token->set_object_read( true ); + do_action( 'woocommerce_payment_token_loaded', $token ); + } else { + throw new Exception( __( 'Invalid payment token.', 'woocommerce' ) ); + } + } + + /** + * Read extra data associated with the token (like last4 digits of a card for expiry dates). + * + * @param WC_Payment_Token $token Payment token object. + * @since 3.0.0 + */ + protected function read_extra_data( &$token ) { + foreach ( $token->get_extra_data_keys() as $key ) { + $function = 'set_' . $key; + if ( is_callable( array( $token, $function ) ) ) { + $token->{$function}( get_metadata( 'payment_token', $token->get_id(), $key, true ) ); + } + } + } + + /** + * Saves extra token data as meta. + * + * @since 3.0.0 + * @param WC_Payment_Token $token Payment token object. + * @param bool $force By default, only changed props are updated. When this param is true all props are updated. + * @return array List of updated props. + */ + protected function save_extra_data( &$token, $force = false ) { + if ( $this->extra_data_saved ) { + return array(); + } + + $updated_props = array(); + $extra_data_keys = $token->get_extra_data_keys(); + $meta_key_to_props = ! empty( $extra_data_keys ) ? array_combine( $extra_data_keys, $extra_data_keys ) : array(); + $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $token, $meta_key_to_props ); + + foreach ( $extra_data_keys as $key ) { + if ( ! array_key_exists( $key, $props_to_update ) ) { + continue; + } + $function = 'get_' . $key; + if ( is_callable( array( $token, $function ) ) ) { + if ( update_metadata( 'payment_token', $token->get_id(), $key, $token->{$function}( 'edit' ) ) ) { + $updated_props[] = $key; + } + } + } + + return $updated_props; + } + + /** + * Returns an array of objects (stdObject) matching specific token criteria. + * Accepts token_id, user_id, gateway_id, and type. + * Each object should contain the fields token_id, gateway_id, token, user_id, type, is_default. + * + * @since 3.0.0 + * @param array $args List of accepted args: token_id, gateway_id, user_id, type. + * @return array + */ + public function get_tokens( $args ) { + global $wpdb; + $args = wp_parse_args( + $args, + array( + 'token_id' => '', + 'user_id' => '', + 'gateway_id' => '', + 'type' => '', + ) + ); + + $sql = "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens"; + $where = array( '1=1' ); + + if ( $args['token_id'] ) { + $token_ids = array_map( 'absint', is_array( $args['token_id'] ) ? $args['token_id'] : array( $args['token_id'] ) ); + $where[] = "token_id IN ('" . implode( "','", array_map( 'esc_sql', $token_ids ) ) . "')"; + } + + if ( $args['user_id'] ) { + $where[] = $wpdb->prepare( 'user_id = %d', absint( $args['user_id'] ) ); + } + + if ( $args['gateway_id'] ) { + $gateway_ids = array( $args['gateway_id'] ); + } else { + $gateways = WC_Payment_Gateways::instance(); + $gateway_ids = $gateways->get_payment_gateway_ids(); + } + + $page = isset( $args['page'] ) ? absint( $args['page'] ) : 1; + $posts_per_page = absint( isset( $args['limit'] ) ? $args['limit'] : get_option( 'posts_per_page' ) ); + + $pgstrt = absint( ( $page - 1 ) * $posts_per_page ) . ', '; + $limits = 'LIMIT ' . $pgstrt . $posts_per_page; + + $gateway_ids[] = ''; + $where[] = "gateway_id IN ('" . implode( "','", array_map( 'esc_sql', $gateway_ids ) ) . "')"; + + if ( $args['type'] ) { + $where[] = $wpdb->prepare( 'type = %s', $args['type'] ); + } + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $token_results = $wpdb->get_results( $sql . ' WHERE ' . implode( ' AND ', $where ) . ' ' . $limits ); + + return $token_results; + } + + /** + * Returns an stdObject of a token for a user's default token. + * Should contain the fields token_id, gateway_id, token, user_id, type, is_default. + * + * @since 3.0.0 + * @param int $user_id User ID. + * @return object + */ + public function get_users_default_token( $user_id ) { + global $wpdb; + return $wpdb->get_row( + $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE user_id = %d AND is_default = 1", + $user_id + ) + ); + } + + /** + * Returns an stdObject of a token. + * Should contain the fields token_id, gateway_id, token, user_id, type, is_default. + * + * @since 3.0.0 + * @param int $token_id Token ID. + * @return object + */ + public function get_token_by_id( $token_id ) { + global $wpdb; + return $wpdb->get_row( + $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d", + $token_id + ) + ); + } + + /** + * Returns metadata for a specific payment token. + * + * @since 3.0.0 + * @param int $token_id Token ID. + * @return array + */ + public function get_metadata( $token_id ) { + return get_metadata( 'payment_token', $token_id ); + } + + /** + * Get a token's type by ID. + * + * @since 3.0.0 + * @param int $token_id Token ID. + * @return string + */ + public function get_token_type_by_id( $token_id ) { + global $wpdb; + return $wpdb->get_var( + $wpdb->prepare( + "SELECT type FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d", + $token_id + ) + ); + } + + /** + * Update's a tokens default status in the database. Used for quickly + * looping through tokens and setting their statuses instead of creating a bunch + * of objects. + * + * @since 3.0.0 + * + * @param int $token_id Token ID. + * @param bool $status Whether given payment token is the default payment token or not. + * + * @return void + */ + public function set_default_status( $token_id, $status = true ) { + global $wpdb; + $wpdb->update( + $wpdb->prefix . 'woocommerce_payment_tokens', + array( 'is_default' => (int) $status ), + array( + 'token_id' => $token_id, + ) + ); + } + +} diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php new file mode 100644 index 00000000000..190749d7db3 --- /dev/null +++ b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php @@ -0,0 +1,2130 @@ +get_date_created( 'edit' ) ) { + $product->set_date_created( time() ); + } + + $id = wp_insert_post( + apply_filters( + 'woocommerce_new_product_data', + array( + 'post_type' => 'product', + 'post_status' => $product->get_status() ? $product->get_status() : 'publish', + 'post_author' => get_current_user_id(), + 'post_title' => $product->get_name() ? $product->get_name() : __( 'Product', 'woocommerce' ), + 'post_content' => $product->get_description(), + 'post_excerpt' => $product->get_short_description(), + 'post_parent' => $product->get_parent_id(), + 'comment_status' => $product->get_reviews_allowed() ? 'open' : 'closed', + 'ping_status' => 'closed', + 'menu_order' => $product->get_menu_order(), + 'post_password' => $product->get_post_password( 'edit' ), + 'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ), + 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ), + 'post_name' => $product->get_slug( 'edit' ), + ) + ), + true + ); + + if ( $id && ! is_wp_error( $id ) ) { + $product->set_id( $id ); + + $this->update_post_meta( $product, true ); + $this->update_terms( $product, true ); + $this->update_visibility( $product, true ); + $this->update_attributes( $product, true ); + $this->update_version_and_type( $product ); + $this->handle_updated_props( $product ); + $this->clear_caches( $product ); + + $product->save_meta_data(); + $product->apply_changes(); + + do_action( 'woocommerce_new_product', $id, $product ); + } + } + + /** + * Method to read a product from the database. + * + * @param WC_Product $product Product object. + * @throws Exception If invalid product. + */ + public function read( &$product ) { + $product->set_defaults(); + $post_object = get_post( $product->get_id() ); + + if ( ! $product->get_id() || ! $post_object || 'product' !== $post_object->post_type ) { + throw new Exception( __( 'Invalid product.', 'woocommerce' ) ); + } + + $product->set_props( + array( + 'name' => $post_object->post_title, + 'slug' => $post_object->post_name, + 'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ), + 'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ), + 'status' => $post_object->post_status, + 'description' => $post_object->post_content, + 'short_description' => $post_object->post_excerpt, + 'parent_id' => $post_object->post_parent, + 'menu_order' => $post_object->menu_order, + 'post_password' => $post_object->post_password, + 'reviews_allowed' => 'open' === $post_object->comment_status, + ) + ); + + $this->read_attributes( $product ); + $this->read_downloads( $product ); + $this->read_visibility( $product ); + $this->read_product_data( $product ); + $this->read_extra_data( $product ); + $product->set_object_read( true ); + + do_action( 'woocommerce_product_read', $product->get_id() ); + } + + /** + * Method to update a product in the database. + * + * @param WC_Product $product Product object. + */ + public function update( &$product ) { + $product->save_meta_data(); + $changes = $product->get_changes(); + + // Only update the post when the post data changes. + if ( array_intersect( array( 'description', 'short_description', 'name', 'parent_id', 'reviews_allowed', 'status', 'menu_order', 'date_created', 'date_modified', 'slug' ), array_keys( $changes ) ) ) { + $post_data = array( + 'post_content' => $product->get_description( 'edit' ), + 'post_excerpt' => $product->get_short_description( 'edit' ), + 'post_title' => $product->get_name( 'edit' ), + 'post_parent' => $product->get_parent_id( 'edit' ), + 'comment_status' => $product->get_reviews_allowed( 'edit' ) ? 'open' : 'closed', + 'post_status' => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish', + 'menu_order' => $product->get_menu_order( 'edit' ), + 'post_password' => $product->get_post_password( 'edit' ), + 'post_name' => $product->get_slug( 'edit' ), + 'post_type' => 'product', + ); + if ( $product->get_date_created( 'edit' ) ) { + $post_data['post_date'] = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ); + $post_data['post_date_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ); + } + if ( isset( $changes['date_modified'] ) && $product->get_date_modified( 'edit' ) ) { + $post_data['post_modified'] = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() ); + $post_data['post_modified_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getTimestamp() ); + } else { + $post_data['post_modified'] = current_time( 'mysql' ); + $post_data['post_modified_gmt'] = current_time( 'mysql', 1 ); + } + + /** + * When updating this object, to prevent infinite loops, use $wpdb + * to update data, since wp_update_post spawns more calls to the + * save_post action. + * + * This ensures hooks are fired by either WP itself (admin screen save), + * or an update purely from CRUD. + */ + if ( doing_action( 'save_post' ) ) { + $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) ); + clean_post_cache( $product->get_id() ); + } else { + wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) ); + } + $product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook. + + } else { // Only update post modified time to record this save event. + $GLOBALS['wpdb']->update( + $GLOBALS['wpdb']->posts, + array( + 'post_modified' => current_time( 'mysql' ), + 'post_modified_gmt' => current_time( 'mysql', 1 ), + ), + array( + 'ID' => $product->get_id(), + ) + ); + clean_post_cache( $product->get_id() ); + } + + $this->update_post_meta( $product ); + $this->update_terms( $product ); + $this->update_visibility( $product ); + $this->update_attributes( $product ); + $this->update_version_and_type( $product ); + $this->handle_updated_props( $product ); + $this->clear_caches( $product ); + + wc_get_container() + ->get( DownloadPermissionsAdjuster::class ) + ->maybe_schedule_adjust_download_permissions( $product ); + + $product->apply_changes(); + + do_action( 'woocommerce_update_product', $product->get_id(), $product ); + } + + /** + * Method to delete a product from the database. + * + * @param WC_Product $product Product object. + * @param array $args Array of args to pass to the delete method. + */ + public function delete( &$product, $args = array() ) { + $id = $product->get_id(); + $post_type = $product->is_type( 'variation' ) ? 'product_variation' : 'product'; + + $args = wp_parse_args( + $args, + array( + 'force_delete' => false, + ) + ); + + if ( ! $id ) { + return; + } + + if ( $args['force_delete'] ) { + do_action( 'woocommerce_before_delete_' . $post_type, $id ); + wp_delete_post( $id ); + $product->set_id( 0 ); + do_action( 'woocommerce_delete_' . $post_type, $id ); + } else { + wp_trash_post( $id ); + $product->set_status( 'trash' ); + do_action( 'woocommerce_trash_' . $post_type, $id ); + } + } + + /* + |-------------------------------------------------------------------------- + | Additional Methods + |-------------------------------------------------------------------------- + */ + + /** + * Read product data. Can be overridden by child classes to load other props. + * + * @param WC_Product $product Product object. + * @since 3.0.0 + */ + protected function read_product_data( &$product ) { + $id = $product->get_id(); + $post_meta_values = get_post_meta( $id ); + $meta_key_to_props = array( + '_sku' => 'sku', + '_regular_price' => 'regular_price', + '_sale_price' => 'sale_price', + '_price' => 'price', + '_sale_price_dates_from' => 'date_on_sale_from', + '_sale_price_dates_to' => 'date_on_sale_to', + 'total_sales' => 'total_sales', + '_tax_status' => 'tax_status', + '_tax_class' => 'tax_class', + '_manage_stock' => 'manage_stock', + '_backorders' => 'backorders', + '_low_stock_amount' => 'low_stock_amount', + '_sold_individually' => 'sold_individually', + '_weight' => 'weight', + '_length' => 'length', + '_width' => 'width', + '_height' => 'height', + '_upsell_ids' => 'upsell_ids', + '_crosssell_ids' => 'cross_sell_ids', + '_purchase_note' => 'purchase_note', + '_default_attributes' => 'default_attributes', + '_virtual' => 'virtual', + '_downloadable' => 'downloadable', + '_download_limit' => 'download_limit', + '_download_expiry' => 'download_expiry', + '_thumbnail_id' => 'image_id', + '_stock' => 'stock_quantity', + '_stock_status' => 'stock_status', + '_wc_average_rating' => 'average_rating', + '_wc_rating_count' => 'rating_counts', + '_wc_review_count' => 'review_count', + '_product_image_gallery' => 'gallery_image_ids', + ); + + $set_props = array(); + + foreach ( $meta_key_to_props as $meta_key => $prop ) { + $meta_value = isset( $post_meta_values[ $meta_key ][0] ) ? $post_meta_values[ $meta_key ][0] : null; + $set_props[ $prop ] = maybe_unserialize( $meta_value ); // get_post_meta only unserializes single values. + } + + $set_props['category_ids'] = $this->get_term_ids( $product, 'product_cat' ); + $set_props['tag_ids'] = $this->get_term_ids( $product, 'product_tag' ); + $set_props['shipping_class_id'] = current( $this->get_term_ids( $product, 'product_shipping_class' ) ); + $set_props['gallery_image_ids'] = array_filter( explode( ',', $set_props['gallery_image_ids'] ) ); + + $product->set_props( $set_props ); + } + + /** + * Re-reads stock from the DB ignoring changes. + * + * @param WC_Product $product Product object. + * @param int|float $new_stock New stock level if already read. + */ + public function read_stock_quantity( &$product, $new_stock = null ) { + $object_read = $product->get_object_read(); + $product->set_object_read( false ); // This makes update of qty go directly to data- instead of changes-array of the product object (which is needed as the data should hold status of the object as it was read from the db). + $product->set_stock_quantity( is_null( $new_stock ) ? get_post_meta( $product->get_id(), '_stock', true ) : $new_stock ); + $product->set_object_read( $object_read ); + } + + /** + * Read extra data associated with the product, like button text or product URL for external products. + * + * @param WC_Product $product Product object. + * @since 3.0.0 + */ + protected function read_extra_data( &$product ) { + foreach ( $product->get_extra_data_keys() as $key ) { + $function = 'set_' . $key; + if ( is_callable( array( $product, $function ) ) ) { + $product->{$function}( get_post_meta( $product->get_id(), '_' . $key, true ) ); + } + } + } + + /** + * Convert visibility terms to props. + * Catalog visibility valid values are 'visible', 'catalog', 'search', and 'hidden'. + * + * @param WC_Product $product Product object. + * @since 3.0.0 + */ + protected function read_visibility( &$product ) { + $terms = get_the_terms( $product->get_id(), 'product_visibility' ); + $term_names = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array(); + $featured = in_array( 'featured', $term_names, true ); + $exclude_search = in_array( 'exclude-from-search', $term_names, true ); + $exclude_catalog = in_array( 'exclude-from-catalog', $term_names, true ); + + if ( $exclude_search && $exclude_catalog ) { + $catalog_visibility = 'hidden'; + } elseif ( $exclude_search ) { + $catalog_visibility = 'catalog'; + } elseif ( $exclude_catalog ) { + $catalog_visibility = 'search'; + } else { + $catalog_visibility = 'visible'; + } + + $product->set_props( + array( + 'featured' => $featured, + 'catalog_visibility' => $catalog_visibility, + ) + ); + } + + /** + * Read attributes from post meta. + * + * @param WC_Product $product Product object. + */ + protected function read_attributes( &$product ) { + $meta_attributes = get_post_meta( $product->get_id(), '_product_attributes', true ); + + if ( ! empty( $meta_attributes ) && is_array( $meta_attributes ) ) { + $attributes = array(); + foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) { + $meta_value = array_merge( + array( + 'name' => '', + 'value' => '', + 'position' => 0, + 'is_visible' => 0, + 'is_variation' => 0, + 'is_taxonomy' => 0, + ), + (array) $meta_attribute_value + ); + + // Check if is a taxonomy attribute. + if ( ! empty( $meta_value['is_taxonomy'] ) ) { + if ( ! taxonomy_exists( $meta_value['name'] ) ) { + continue; + } + $id = wc_attribute_taxonomy_id_by_name( $meta_value['name'] ); + $options = wc_get_object_terms( $product->get_id(), $meta_value['name'], 'term_id' ); + } else { + $id = 0; + $options = wc_get_text_attributes( $meta_value['value'] ); + } + + $attribute = new WC_Product_Attribute(); + $attribute->set_id( $id ); + $attribute->set_name( $meta_value['name'] ); + $attribute->set_options( $options ); + $attribute->set_position( $meta_value['position'] ); + $attribute->set_visible( $meta_value['is_visible'] ); + $attribute->set_variation( $meta_value['is_variation'] ); + $attributes[] = $attribute; + } + $product->set_attributes( $attributes ); + } + } + + /** + * Read downloads from post meta. + * + * @param WC_Product $product Product object. + * @since 3.0.0 + */ + protected function read_downloads( &$product ) { + $meta_values = array_filter( (array) get_post_meta( $product->get_id(), '_downloadable_files', true ) ); + + if ( $meta_values ) { + $downloads = array(); + foreach ( $meta_values as $key => $value ) { + if ( ! isset( $value['name'], $value['file'] ) ) { + continue; + } + $download = new WC_Product_Download(); + $download->set_id( $key ); + $download->set_name( $value['name'] ? $value['name'] : wc_get_filename_from_url( $value['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $value['file'], $product, $key ) ); + $downloads[] = $download; + } + $product->set_downloads( $downloads ); + } + } + + /** + * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class. + * + * @param WC_Product $product Product object. + * @param bool $force Force update. Used during create. + * @since 3.0.0 + */ + protected function update_post_meta( &$product, $force = false ) { + $meta_key_to_props = array( + '_sku' => 'sku', + '_regular_price' => 'regular_price', + '_sale_price' => 'sale_price', + '_sale_price_dates_from' => 'date_on_sale_from', + '_sale_price_dates_to' => 'date_on_sale_to', + 'total_sales' => 'total_sales', + '_tax_status' => 'tax_status', + '_tax_class' => 'tax_class', + '_manage_stock' => 'manage_stock', + '_backorders' => 'backorders', + '_low_stock_amount' => 'low_stock_amount', + '_sold_individually' => 'sold_individually', + '_weight' => 'weight', + '_length' => 'length', + '_width' => 'width', + '_height' => 'height', + '_upsell_ids' => 'upsell_ids', + '_crosssell_ids' => 'cross_sell_ids', + '_purchase_note' => 'purchase_note', + '_default_attributes' => 'default_attributes', + '_virtual' => 'virtual', + '_downloadable' => 'downloadable', + '_product_image_gallery' => 'gallery_image_ids', + '_download_limit' => 'download_limit', + '_download_expiry' => 'download_expiry', + '_thumbnail_id' => 'image_id', + '_stock' => 'stock_quantity', + '_stock_status' => 'stock_status', + '_wc_average_rating' => 'average_rating', + '_wc_rating_count' => 'rating_counts', + '_wc_review_count' => 'review_count', + ); + + // Make sure to take extra data (like product url or text for external products) into account. + $extra_data_keys = $product->get_extra_data_keys(); + + foreach ( $extra_data_keys as $key ) { + $meta_key_to_props[ '_' . $key ] = $key; + } + + $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props ); + + foreach ( $props_to_update as $meta_key => $prop ) { + $value = $product->{"get_$prop"}( 'edit' ); + $value = is_string( $value ) ? wp_slash( $value ) : $value; + switch ( $prop ) { + case 'virtual': + case 'downloadable': + case 'manage_stock': + case 'sold_individually': + $value = wc_bool_to_string( $value ); + break; + case 'gallery_image_ids': + $value = implode( ',', $value ); + break; + case 'date_on_sale_from': + case 'date_on_sale_to': + $value = $value ? $value->getTimestamp() : ''; + break; + case 'stock_quantity': + // Fire actions to let 3rd parties know the stock is about to be changed. + if ( $product->is_type( 'variation' ) ) { + /** + * Action to signal that the value of 'stock_quantity' for a variation is about to change. + * + * @since 4.9 + * + * @param int $product The variation whose stock is about to change. + */ + do_action( 'woocommerce_variation_before_set_stock', $product ); + } else { + /** + * Action to signal that the value of 'stock_quantity' for a product is about to change. + * + * @since 4.9 + * + * @param int $product The product whose stock is about to change. + */ + do_action( 'woocommerce_product_before_set_stock', $product ); + } + break; + } + + $updated = $this->update_or_delete_post_meta( $product, $meta_key, $value ); + + if ( $updated ) { + $this->updated_props[] = $prop; + } + } + + // Update extra data associated with the product like button text or product URL for external products. + if ( ! $this->extra_data_saved ) { + foreach ( $extra_data_keys as $key ) { + $meta_key = '_' . $key; + $function = 'get_' . $key; + if ( ! array_key_exists( $meta_key, $props_to_update ) ) { + continue; + } + if ( is_callable( array( $product, $function ) ) ) { + $value = $product->{$function}( 'edit' ); + $value = is_string( $value ) ? wp_slash( $value ) : $value; + $updated = $this->update_or_delete_post_meta( $product, $meta_key, $value ); + + if ( $updated ) { + $this->updated_props[] = $key; + } + } + } + } + + if ( $this->update_downloads( $product, $force ) ) { + $this->updated_props[] = 'downloads'; + } + } + + /** + * Handle updated meta props after updating meta data. + * + * @since 3.0.0 + * @param WC_Product $product Product Object. + */ + protected function handle_updated_props( &$product ) { + $price_is_synced = $product->is_type( array( 'variable', 'grouped' ) ); + + if ( ! $price_is_synced ) { + if ( in_array( 'regular_price', $this->updated_props, true ) || in_array( 'sale_price', $this->updated_props, true ) ) { + if ( $product->get_sale_price( 'edit' ) >= $product->get_regular_price( 'edit' ) ) { + update_post_meta( $product->get_id(), '_sale_price', '' ); + $product->set_sale_price( '' ); + } + } + + if ( in_array( 'date_on_sale_from', $this->updated_props, true ) || in_array( 'date_on_sale_to', $this->updated_props, true ) || in_array( 'regular_price', $this->updated_props, true ) || in_array( 'sale_price', $this->updated_props, true ) || in_array( 'product_type', $this->updated_props, true ) ) { + if ( $product->is_on_sale( 'edit' ) ) { + update_post_meta( $product->get_id(), '_price', $product->get_sale_price( 'edit' ) ); + $product->set_price( $product->get_sale_price( 'edit' ) ); + } else { + update_post_meta( $product->get_id(), '_price', $product->get_regular_price( 'edit' ) ); + $product->set_price( $product->get_regular_price( 'edit' ) ); + } + } + } + + if ( in_array( 'stock_quantity', $this->updated_props, true ) ) { + if ( $product->is_type( 'variation' ) ) { + do_action( 'woocommerce_variation_set_stock', $product ); + } else { + do_action( 'woocommerce_product_set_stock', $product ); + } + } + + if ( in_array( 'stock_status', $this->updated_props, true ) ) { + if ( $product->is_type( 'variation' ) ) { + do_action( 'woocommerce_variation_set_stock_status', $product->get_id(), $product->get_stock_status(), $product ); + } else { + do_action( 'woocommerce_product_set_stock_status', $product->get_id(), $product->get_stock_status(), $product ); + } + } + + if ( array_intersect( $this->updated_props, array( 'sku', 'regular_price', 'sale_price', 'date_on_sale_from', 'date_on_sale_to', 'total_sales', 'average_rating', 'stock_quantity', 'stock_status', 'manage_stock', 'downloadable', 'virtual', 'tax_status', 'tax_class' ) ) ) { + $this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' ); + } + + // Trigger action so 3rd parties can deal with updated props. + do_action( 'woocommerce_product_object_updated_props', $product, $this->updated_props ); + + // After handling, we can reset the props array. + $this->updated_props = array(); + } + + /** + * For all stored terms in all taxonomies, save them to the DB. + * + * @param WC_Product $product Product object. + * @param bool $force Force update. Used during create. + * @since 3.0.0 + */ + protected function update_terms( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_key_exists( 'category_ids', $changes ) ) { + $categories = $product->get_category_ids( 'edit' ); + + if ( empty( $categories ) && get_option( 'default_product_cat', 0 ) ) { + $categories = array( get_option( 'default_product_cat', 0 ) ); + } + + wp_set_post_terms( $product->get_id(), $categories, 'product_cat', false ); + } + if ( $force || array_key_exists( 'tag_ids', $changes ) ) { + wp_set_post_terms( $product->get_id(), $product->get_tag_ids( 'edit' ), 'product_tag', false ); + } + if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) { + wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false ); + } + + _wc_recount_terms_by_product( $product->get_id() ); + } + + /** + * Update visibility terms based on props. + * + * @since 3.0.0 + * + * @param WC_Product $product Product object. + * @param bool $force Force update. Used during create. + */ + protected function update_visibility( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_intersect( array( 'featured', 'stock_status', 'average_rating', 'catalog_visibility' ), array_keys( $changes ) ) ) { + $terms = array(); + + if ( $product->get_featured() ) { + $terms[] = 'featured'; + } + + if ( 'outofstock' === $product->get_stock_status() ) { + $terms[] = 'outofstock'; + } + + $rating = min( 5, NumberUtil::round( $product->get_average_rating(), 0 ) ); + + if ( $rating > 0 ) { + $terms[] = 'rated-' . $rating; + } + + switch ( $product->get_catalog_visibility() ) { + case 'hidden': + $terms[] = 'exclude-from-search'; + $terms[] = 'exclude-from-catalog'; + break; + case 'catalog': + $terms[] = 'exclude-from-search'; + break; + case 'search': + $terms[] = 'exclude-from-catalog'; + break; + } + + if ( ! is_wp_error( wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false ) ) ) { + do_action( 'woocommerce_product_set_visibility', $product->get_id(), $product->get_catalog_visibility() ); + } + } + } + + /** + * Update attributes which are a mix of terms and meta data. + * + * @param WC_Product $product Product object. + * @param bool $force Force update. Used during create. + * @since 3.0.0 + */ + protected function update_attributes( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_key_exists( 'attributes', $changes ) ) { + $attributes = $product->get_attributes(); + $meta_values = array(); + + if ( $attributes ) { + foreach ( $attributes as $attribute_key => $attribute ) { + $value = ''; + + if ( is_null( $attribute ) ) { + if ( taxonomy_exists( $attribute_key ) ) { + // Handle attributes that have been unset. + wp_set_object_terms( $product->get_id(), array(), $attribute_key ); + } elseif ( taxonomy_exists( urldecode( $attribute_key ) ) ) { + // Handle attributes that have been unset. + wp_set_object_terms( $product->get_id(), array(), urldecode( $attribute_key ) ); + } + continue; + + } elseif ( $attribute->is_taxonomy() ) { + wp_set_object_terms( $product->get_id(), wp_list_pluck( (array) $attribute->get_terms(), 'term_id' ), $attribute->get_name() ); + } else { + $value = wc_implode_text_attributes( $attribute->get_options() ); + } + + // Store in format WC uses in meta. + $meta_values[ $attribute_key ] = array( + 'name' => $attribute->get_name(), + 'value' => $value, + 'position' => $attribute->get_position(), + 'is_visible' => $attribute->get_visible() ? 1 : 0, + 'is_variation' => $attribute->get_variation() ? 1 : 0, + 'is_taxonomy' => $attribute->is_taxonomy() ? 1 : 0, + ); + } + } + // Note, we use wp_slash to add extra level of escaping. See https://codex.wordpress.org/Function_Reference/update_post_meta#Workaround. + $this->update_or_delete_post_meta( $product, '_product_attributes', wp_slash( $meta_values ) ); + } + } + + /** + * Update downloads. + * + * @since 3.0.0 + * @param WC_Product $product Product object. + * @param bool $force Force update. Used during create. + * @return bool If updated or not. + */ + protected function update_downloads( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_key_exists( 'downloads', $changes ) ) { + $downloads = $product->get_downloads(); + $meta_values = array(); + + if ( $downloads ) { + foreach ( $downloads as $key => $download ) { + // Store in format WC uses in meta. + $meta_values[ $key ] = $download->get_data(); + } + } + + if ( $product->is_type( 'variation' ) ) { + do_action( 'woocommerce_process_product_file_download_paths', $product->get_parent_id(), $product->get_id(), $downloads ); + } else { + do_action( 'woocommerce_process_product_file_download_paths', $product->get_id(), 0, $downloads ); + } + + return $this->update_or_delete_post_meta( $product, '_downloadable_files', wp_slash( $meta_values ) ); + } + return false; + } + + /** + * Make sure we store the product type and version (to track data changes). + * + * @param WC_Product $product Product object. + * @since 3.0.0 + */ + protected function update_version_and_type( &$product ) { + $old_type = WC_Product_Factory::get_product_type( $product->get_id() ); + $new_type = $product->get_type(); + + wp_set_object_terms( $product->get_id(), $new_type, 'product_type' ); + update_post_meta( $product->get_id(), '_product_version', Constants::get_constant( 'WC_VERSION' ) ); + + // Action for the transition. + if ( $old_type !== $new_type ) { + $this->updated_props[] = 'product_type'; + do_action( 'woocommerce_product_type_changed', $product, $old_type, $new_type ); + } + } + + /** + * Clear any caches. + * + * @param WC_Product $product Product object. + * @since 3.0.0 + */ + protected function clear_caches( &$product ) { + wc_delete_product_transients( $product->get_id() ); + if ( $product->get_parent_id( 'edit' ) ) { + wc_delete_product_transients( $product->get_parent_id( 'edit' ) ); + WC_Cache_Helper::invalidate_cache_group( 'product_' . $product->get_parent_id( 'edit' ) ); + } + WC_Cache_Helper::invalidate_attribute_count( array_keys( $product->get_attributes() ) ); + WC_Cache_Helper::invalidate_cache_group( 'product_' . $product->get_id() ); + } + + /* + |-------------------------------------------------------------------------- + | wc-product-functions.php methods + |-------------------------------------------------------------------------- + */ + + /** + * Returns an array of on sale products, as an array of objects with an + * ID and parent_id present. Example: $return[0]->id, $return[0]->parent_id. + * + * @return array + * @since 3.0.0 + */ + public function get_on_sale_products() { + global $wpdb; + + $exclude_term_ids = array(); + $outofstock_join = ''; + $outofstock_where = ''; + $non_published_where = ''; + $product_visibility_term_ids = wc_get_product_visibility_term_ids(); + + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) { + $exclude_term_ids[] = $product_visibility_term_ids['outofstock']; + } + + if ( count( $exclude_term_ids ) ) { + $outofstock_join = " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = id'; + $outofstock_where = ' AND exclude_join.object_id IS NULL'; + } + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + return $wpdb->get_results( + " + SELECT posts.ID as id, posts.post_parent as parent_id + FROM {$wpdb->posts} AS posts + INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id + $outofstock_join + WHERE posts.post_type IN ( 'product', 'product_variation' ) + AND posts.post_status = 'publish' + AND lookup.onsale = 1 + $outofstock_where + AND posts.post_parent NOT IN ( + SELECT ID FROM `$wpdb->posts` as posts + WHERE posts.post_type = 'product' + AND posts.post_parent = 0 + AND posts.post_status != 'publish' + ) + GROUP BY posts.ID + " + ); + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } + + /** + * Returns a list of product IDs ( id as key => parent as value) that are + * featured. Uses get_posts instead of wc_get_products since we want + * some extra meta queries and ALL products (posts_per_page = -1). + * + * @return array + * @since 3.0.0 + */ + public function get_featured_product_ids() { + $product_visibility_term_ids = wc_get_product_visibility_term_ids(); + + return get_posts( + array( + 'post_type' => array( 'product', 'product_variation' ), + 'posts_per_page' => -1, + 'post_status' => 'publish', + 'tax_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + 'relation' => 'AND', + array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => array( $product_visibility_term_ids['featured'] ), + ), + array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => array( $product_visibility_term_ids['exclude-from-catalog'] ), + 'operator' => 'NOT IN', + ), + ), + 'fields' => 'id=>parent', + ) + ); + } + + /** + * Check if product sku is found for any other product IDs. + * + * @since 3.0.0 + * @param int $product_id Product ID. + * @param string $sku Will be slashed to work around https://core.trac.wordpress.org/ticket/27421. + * @return bool + */ + public function is_existing_sku( $product_id, $sku ) { + global $wpdb; + + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + return (bool) $wpdb->get_var( + $wpdb->prepare( + " + SELECT posts.ID + FROM {$wpdb->posts} as posts + INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id + WHERE + posts.post_type IN ( 'product', 'product_variation' ) + AND posts.post_status != 'trash' + AND lookup.sku = %s + AND lookup.product_id <> %d + LIMIT 1 + ", + wp_slash( $sku ), + $product_id + ) + ); + } + + /** + * Return product ID based on SKU. + * + * @since 3.0.0 + * @param string $sku Product SKU. + * @return int + */ + public function get_product_id_by_sku( $sku ) { + global $wpdb; + + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $id = $wpdb->get_var( + $wpdb->prepare( + " + SELECT posts.ID + FROM {$wpdb->posts} as posts + INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id + WHERE + posts.post_type IN ( 'product', 'product_variation' ) + AND posts.post_status != 'trash' + AND lookup.sku = %s + LIMIT 1 + ", + $sku + ) + ); + + return (int) apply_filters( 'woocommerce_get_product_id_by_sku', $id, $sku ); + } + + /** + * Returns an array of IDs of products that have sales starting soon. + * + * @since 3.0.0 + * @return array + */ + public function get_starting_sales() { + global $wpdb; + + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + return $wpdb->get_col( + $wpdb->prepare( + "SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta + LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id + LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id + WHERE postmeta.meta_key = '_sale_price_dates_from' + AND postmeta_2.meta_key = '_price' + AND postmeta_3.meta_key = '_sale_price' + AND postmeta.meta_value > 0 + AND postmeta.meta_value < %s + AND postmeta_2.meta_value != postmeta_3.meta_value", + time() + ) + ); + } + + /** + * Returns an array of IDs of products that have sales which are due to end. + * + * @since 3.0.0 + * @return array + */ + public function get_ending_sales() { + global $wpdb; + + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + return $wpdb->get_col( + $wpdb->prepare( + "SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta + LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id + LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id + WHERE postmeta.meta_key = '_sale_price_dates_to' + AND postmeta_2.meta_key = '_price' + AND postmeta_3.meta_key = '_regular_price' + AND postmeta.meta_value > 0 + AND postmeta.meta_value < %s + AND postmeta_2.meta_value != postmeta_3.meta_value", + time() + ) + ); + } + + /** + * Find a matching (enabled) variation within a variable product. + * + * @since 3.0.0 + * @param WC_Product $product Variable product. + * @param array $match_attributes Array of attributes we want to try to match. + * @return int Matching variation ID or 0. + */ + public function find_matching_product_variation( $product, $match_attributes = array() ) { + global $wpdb; + + $meta_attribute_names = array(); + + // Get attributes to match in meta. + foreach ( $product->get_attributes() as $attribute ) { + if ( ! $attribute->get_variation() ) { + continue; + } + $meta_attribute_names[] = 'attribute_' . sanitize_title( $attribute->get_name() ); + } + + // Get the attributes of the variations. + $query = $wpdb->prepare( + " + SELECT postmeta.post_id, postmeta.meta_key, postmeta.meta_value, posts.menu_order FROM {$wpdb->postmeta} as postmeta + LEFT JOIN {$wpdb->posts} as posts ON postmeta.post_id=posts.ID + WHERE postmeta.post_id IN ( + SELECT ID FROM {$wpdb->posts} + WHERE {$wpdb->posts}.post_parent = %d + AND {$wpdb->posts}.post_status = 'publish' + AND {$wpdb->posts}.post_type = 'product_variation' + ) + ", + $product->get_id() + ); + + $query .= " AND postmeta.meta_key IN ( '" . implode( "','", array_map( 'esc_sql', $meta_attribute_names ) ) . "' )"; + + $query .= ' ORDER BY posts.menu_order ASC, postmeta.post_id ASC;'; + + $attributes = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + if ( ! $attributes ) { + return 0; + } + + $sorted_meta = array(); + + foreach ( $attributes as $m ) { + $sorted_meta[ $m->post_id ][ $m->meta_key ] = $m->meta_value; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + } + + /** + * Check each variation to find the one that matches the $match_attributes. + * + * Note: Not all meta fields will be set which is why we check existance. + */ + foreach ( $sorted_meta as $variation_id => $variation ) { + $match = true; + + // Loop over the variation meta keys and values i.e. what is saved to the products. Note: $attribute_value is empty when 'any' is in use. + foreach ( $variation as $attribute_key => $attribute_value ) { + $match_any_value = '' === $attribute_value; + + if ( ! $match_any_value && ! array_key_exists( $attribute_key, $match_attributes ) ) { + $match = false; // Requires a selection but no value was provide. + } + + if ( array_key_exists( $attribute_key, $match_attributes ) ) { // Value to match was provided. + if ( ! $match_any_value && $match_attributes[ $attribute_key ] !== $attribute_value ) { + $match = false; // Provided value does not match variation. + } + } + } + + if ( true === $match ) { + return $variation_id; + } + } + + if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) { + /** + * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute. + * Fallback is here because there are cases where data will be 'synced' but the product version will remain the same. + */ + return ( array_map( 'sanitize_title', $match_attributes ) === $match_attributes ) ? 0 : $this->find_matching_product_variation( $product, array_map( 'sanitize_title', $match_attributes ) ); + } + + return 0; + } + + /** + * Creates all possible combinations of variations from the attributes, without creating duplicates. + * + * @since 3.6.0 + * @todo Add to interface in 4.0. + * @param WC_Product $product Variable product. + * @param int $limit Limit the number of created variations. + * @return int Number of created variations. + */ + public function create_all_product_variations( $product, $limit = -1 ) { + $count = 0; + + if ( ! $product ) { + return $count; + } + + $attributes = wc_list_pluck( array_filter( $product->get_attributes(), 'wc_attributes_array_filter_variation' ), 'get_slugs' ); + + if ( empty( $attributes ) ) { + return $count; + } + + // Get existing variations so we don't create duplicates. + $existing_variations = array_map( 'wc_get_product', $product->get_children() ); + $existing_attributes = array(); + + foreach ( $existing_variations as $existing_variation ) { + $existing_attributes[] = $existing_variation->get_attributes(); + } + + $possible_attributes = array_reverse( wc_array_cartesian( $attributes ) ); + + foreach ( $possible_attributes as $possible_attribute ) { + // Allow any order if key/values -- do not use strict mode. + if ( in_array( $possible_attribute, $existing_attributes ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict + continue; + } + $variation = wc_get_product_object( 'variation' ); + $variation->set_parent_id( $product->get_id() ); + $variation->set_attributes( $possible_attribute ); + $variation_id = $variation->save(); + + do_action( 'product_variation_linked', $variation_id ); + + $count ++; + + if ( $limit > 0 && $count >= $limit ) { + break; + } + } + + return $count; + } + + /** + * Make sure all variations have a sort order set so they can be reordered correctly. + * + * @param int $parent_id Product ID. + */ + public function sort_all_product_variations( $parent_id ) { + global $wpdb; + + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $ids = $wpdb->get_col( + $wpdb->prepare( + "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product_variation' AND post_parent = %d AND post_status = 'publish' ORDER BY menu_order ASC, ID ASC", + $parent_id + ) + ); + $index = 1; + + foreach ( $ids as $id ) { + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $wpdb->update( $wpdb->posts, array( 'menu_order' => ( $index++ ) ), array( 'ID' => absint( $id ) ) ); + } + } + + /** + * Return a list of related products (using data like categories and IDs). + * + * @since 3.0.0 + * @param array $cats_array List of categories IDs. + * @param array $tags_array List of tags IDs. + * @param array $exclude_ids Excluded IDs. + * @param int $limit Limit of results. + * @param int $product_id Product ID. + * @return array + */ + public function get_related_products( $cats_array, $tags_array, $exclude_ids, $limit, $product_id ) { + global $wpdb; + + $args = array( + 'categories' => $cats_array, + 'tags' => $tags_array, + 'exclude_ids' => $exclude_ids, + 'limit' => $limit + 10, + ); + + $related_product_query = (array) apply_filters( 'woocommerce_product_related_posts_query', $this->get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit + 10 ), $product_id, $args ); + + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared + return $wpdb->get_col( implode( ' ', $related_product_query ) ); + } + + /** + * Builds the related posts query. + * + * @since 3.0.0 + * + * @param array $cats_array List of categories IDs. + * @param array $tags_array List of tags IDs. + * @param array $exclude_ids Excluded IDs. + * @param int $limit Limit of results. + * + * @return array + */ + public function get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit ) { + global $wpdb; + + $include_term_ids = array_merge( $cats_array, $tags_array ); + $exclude_term_ids = array(); + $product_visibility_term_ids = wc_get_product_visibility_term_ids(); + + if ( $product_visibility_term_ids['exclude-from-catalog'] ) { + $exclude_term_ids[] = $product_visibility_term_ids['exclude-from-catalog']; + } + + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) { + $exclude_term_ids[] = $product_visibility_term_ids['outofstock']; + } + + $query = array( + 'fields' => " + SELECT DISTINCT ID FROM {$wpdb->posts} p + ", + 'join' => '', + 'where' => " + WHERE 1=1 + AND p.post_status = 'publish' + AND p.post_type = 'product' + + ", + 'limits' => ' + LIMIT ' . absint( $limit ) . ' + ', + ); + + if ( count( $exclude_term_ids ) ) { + $query['join'] .= " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = p.ID'; + $query['where'] .= ' AND exclude_join.object_id IS NULL'; + } + + if ( count( $include_term_ids ) ) { + $query['join'] .= " INNER JOIN ( SELECT object_id FROM {$wpdb->term_relationships} INNER JOIN {$wpdb->term_taxonomy} using( term_taxonomy_id ) WHERE term_id IN ( " . implode( ',', array_map( 'absint', $include_term_ids ) ) . ' ) ) AS include_join ON include_join.object_id = p.ID'; + } + + if ( count( $exclude_ids ) ) { + $query['where'] .= ' AND p.ID NOT IN ( ' . implode( ',', array_map( 'absint', $exclude_ids ) ) . ' )'; + } + + return $query; + } + + /** + * Update a product's stock amount directly in the database. + * + * Updates both post meta and lookup tables. Ignores manage stock setting on the product. + * + * @param int $product_id_with_stock Product ID. + * @param int|float|null $stock_quantity Stock quantity. + */ + protected function set_product_stock( $product_id_with_stock, $stock_quantity ) { + global $wpdb; + + // Generate SQL. + $sql = $wpdb->prepare( + "UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'", + $stock_quantity, + $product_id_with_stock + ); + + $sql = apply_filters( 'woocommerce_update_product_stock_query', $sql, $product_id_with_stock, $stock_quantity, 'set' ); + + $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared + + // Cache delete is required (not only) to set correct data for lookup table (which reads from cache). + // Sometimes I wonder if it shouldn't be part of update_lookup_table. + wp_cache_delete( $product_id_with_stock, 'post_meta' ); + + $this->update_lookup_table( $product_id_with_stock, 'wc_product_meta_lookup' ); + } + + /** + * Update a product's stock amount directly. + * + * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues). + * Ignores manage stock setting on the product and sets quantities directly in the db: post meta and lookup tables. + * Uses locking to update the quantity. If the lock is not acquired, change is lost. + * + * @since 3.0.0 this supports set, increase and decrease. + * @param int $product_id_with_stock Product ID. + * @param int|float|null $stock_quantity Stock quantity. + * @param string $operation Set, increase and decrease. + * @return int|float New stock level. + */ + public function update_product_stock( $product_id_with_stock, $stock_quantity = null, $operation = 'set' ) { + global $wpdb; + + // Ensures a row exists to update. + add_post_meta( $product_id_with_stock, '_stock', 0, true ); + + if ( 'set' === $operation ) { + $new_stock = wc_stock_amount( $stock_quantity ); + + // Generate SQL. + $sql = $wpdb->prepare( + "UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'", + $new_stock, + $product_id_with_stock + ); + } else { + $current_stock = wc_stock_amount( + $wpdb->get_var( + $wpdb->prepare( + "SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key='_stock';", + $product_id_with_stock + ) + ) + ); + + // Calculate new value for filter below. Set multiplier to subtract or add the meta_value. + switch ( $operation ) { + case 'increase': + $new_stock = $current_stock + wc_stock_amount( $stock_quantity ); + $multiplier = 1; + break; + default: + $new_stock = $current_stock - wc_stock_amount( $stock_quantity ); + $multiplier = -1; + break; + } + + // Generate SQL. + $sql = $wpdb->prepare( + "UPDATE {$wpdb->postmeta} SET meta_value = meta_value %+f WHERE post_id = %d AND meta_key='_stock'", + wc_stock_amount( $stock_quantity ) * $multiplier, // This will either subtract or add depending on operation. + $product_id_with_stock + ); + } + + $sql = apply_filters( 'woocommerce_update_product_stock_query', $sql, $product_id_with_stock, $new_stock, $operation ); + + $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared + + // Cache delete is required (not only) to set correct data for lookup table (which reads from cache). + // Sometimes I wonder if it shouldn't be part of update_lookup_table. + wp_cache_delete( $product_id_with_stock, 'post_meta' ); + + $this->update_lookup_table( $product_id_with_stock, 'wc_product_meta_lookup' ); + + /** + * Fire an action for this direct update so it can be detected by other code. + * + * @since 3.6 + * @param int $product_id_with_stock Product ID that was updated directly. + */ + do_action( 'woocommerce_updated_product_stock', $product_id_with_stock ); + + return $new_stock; + } + + /** + * Update a product's sale count directly. + * + * Uses queries rather than update_post_meta so we can do this in one query for performance. + * + * @since 3.0.0 this supports set, increase and decrease. + * @param int $product_id Product ID. + * @param int|null $quantity Quantity. + * @param string $operation set, increase and decrease. + */ + public function update_product_sales( $product_id, $quantity = null, $operation = 'set' ) { + global $wpdb; + add_post_meta( $product_id, 'total_sales', 0, true ); + + // Update stock in DB directly. + switch ( $operation ) { + case 'increase': + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->postmeta} SET meta_value = meta_value + %f WHERE post_id = %d AND meta_key='total_sales'", + $quantity, + $product_id + ) + ); + break; + case 'decrease': + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->postmeta} SET meta_value = meta_value - %f WHERE post_id = %d AND meta_key='total_sales'", + $quantity, + $product_id + ) + ); + break; + default: + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='total_sales'", + $quantity, + $product_id + ) + ); + break; + } + + wp_cache_delete( $product_id, 'post_meta' ); + + $this->update_lookup_table( $product_id, 'wc_product_meta_lookup' ); + + /** + * Fire an action for this direct update so it can be detected by other code. + * + * @since 3.6 + * @param int $product_id Product ID that was updated directly. + */ + do_action( 'woocommerce_updated_product_sales', $product_id ); + } + + /** + * Update a products average rating meta. + * + * @since 3.0.0 + * @todo Deprecate unused function? + * @param WC_Product $product Product object. + */ + public function update_average_rating( $product ) { + update_post_meta( $product->get_id(), '_wc_average_rating', $product->get_average_rating( 'edit' ) ); + self::update_visibility( $product, true ); + } + + /** + * Update a products review count meta. + * + * @since 3.0.0 + * @todo Deprecate unused function? + * @param WC_Product $product Product object. + */ + public function update_review_count( $product ) { + update_post_meta( $product->get_id(), '_wc_review_count', $product->get_review_count( 'edit' ) ); + } + + /** + * Update a products rating counts. + * + * @since 3.0.0 + * @todo Deprecate unused function? + * @param WC_Product $product Product object. + */ + public function update_rating_counts( $product ) { + update_post_meta( $product->get_id(), '_wc_rating_count', $product->get_rating_counts( 'edit' ) ); + } + + /** + * Get shipping class ID by slug. + * + * @since 3.0.0 + * @param string $slug Product shipping class slug. + * @return int|false + */ + public function get_shipping_class_id_by_slug( $slug ) { + $shipping_class_term = get_term_by( 'slug', $slug, 'product_shipping_class' ); + if ( $shipping_class_term ) { + return $shipping_class_term->term_id; + } else { + return false; + } + } + + /** + * Returns an array of products. + * + * @param array $args Args to pass to WC_Product_Query(). + * @return array|object + * @see wc_get_products + */ + public function get_products( $args = array() ) { + $query = new WC_Product_Query( $args ); + return $query->get_products(); + } + + /** + * Search product data for a term and return ids. + * + * @param string $term Search term. + * @param string $type Type of product. + * @param bool $include_variations Include variations in search or not. + * @param bool $all_statuses Should we search all statuses or limit to published. + * @param null|int $limit Limit returned results. @since 3.5.0. + * @param null|array $include Keep specific results. @since 3.6.0. + * @param null|array $exclude Discard specific results. @since 3.6.0. + * @return array of ids + */ + public function search_products( $term, $type = '', $include_variations = false, $all_statuses = false, $limit = null, $include = null, $exclude = null ) { + global $wpdb; + + $custom_results = apply_filters( 'woocommerce_product_pre_search_products', false, $term, $type, $include_variations, $all_statuses, $limit ); + + if ( is_array( $custom_results ) ) { + return $custom_results; + } + + $post_types = $include_variations ? array( 'product', 'product_variation' ) : array( 'product' ); + $join_query = ''; + $type_where = ''; + $status_where = ''; + $limit_query = ''; + + // When searching variations we should include the parent's meta table for use in searches. + if ( $include_variations ) { + $join_query = " LEFT JOIN {$wpdb->wc_product_meta_lookup} parent_wc_product_meta_lookup + ON posts.post_type = 'product_variation' AND parent_wc_product_meta_lookup.product_id = posts.post_parent "; + } + + /** + * Hook woocommerce_search_products_post_statuses. + * + * @since 3.7.0 + * @param array $post_statuses List of post statuses. + */ + $post_statuses = apply_filters( + 'woocommerce_search_products_post_statuses', + current_user_can( 'edit_private_products' ) ? array( 'private', 'publish' ) : array( 'publish' ) + ); + + // See if search term contains OR keywords. + if ( stristr( $term, ' or ' ) ) { + $term_groups = preg_split( '/\s+or\s+/i', $term ); + } else { + $term_groups = array( $term ); + } + + $search_where = ''; + $search_queries = array(); + + foreach ( $term_groups as $term_group ) { + // Parse search terms. + if ( preg_match_all( '/".*?("|$)|((?<=[\t ",+])|^)[^\t ",+]+/', $term_group, $matches ) ) { + $search_terms = $this->get_valid_search_terms( $matches[0] ); + $count = count( $search_terms ); + + // if the search string has only short terms or stopwords, or is 10+ terms long, match it as sentence. + if ( 9 < $count || 0 === $count ) { + $search_terms = array( $term_group ); + } + } else { + $search_terms = array( $term_group ); + } + + $term_group_query = ''; + $searchand = ''; + + foreach ( $search_terms as $search_term ) { + $like = '%' . $wpdb->esc_like( $search_term ) . '%'; + + // Variations should also search the parent's meta table for fallback fields. + if ( $include_variations ) { + $variation_query = $wpdb->prepare( " OR ( wc_product_meta_lookup.sku = '' AND parent_wc_product_meta_lookup.sku LIKE %s ) ", $like ); + } else { + $variation_query = ''; + } + + $term_group_query .= $wpdb->prepare( " {$searchand} ( ( posts.post_title LIKE %s) OR ( posts.post_excerpt LIKE %s) OR ( posts.post_content LIKE %s ) OR ( wc_product_meta_lookup.sku LIKE %s ) $variation_query)", $like, $like, $like, $like ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $searchand = ' AND '; + } + + if ( $term_group_query ) { + $search_queries[] = $term_group_query; + } + } + + if ( ! empty( $search_queries ) ) { + $search_where = ' AND (' . implode( ') OR (', $search_queries ) . ') '; + } + + if ( ! empty( $include ) && is_array( $include ) ) { + $search_where .= ' AND posts.ID IN(' . implode( ',', array_map( 'absint', $include ) ) . ') '; + } + + if ( ! empty( $exclude ) && is_array( $exclude ) ) { + $search_where .= ' AND posts.ID NOT IN(' . implode( ',', array_map( 'absint', $exclude ) ) . ') '; + } + + if ( 'virtual' === $type ) { + $type_where = ' AND ( wc_product_meta_lookup.virtual = 1 ) '; + } elseif ( 'downloadable' === $type ) { + $type_where = ' AND ( wc_product_meta_lookup.downloadable = 1 ) '; + } + + if ( ! $all_statuses ) { + $status_where = " AND posts.post_status IN ('" . implode( "','", $post_statuses ) . "') "; + } + + if ( $limit ) { + $limit_query = $wpdb->prepare( ' LIMIT %d ', $limit ); + } + + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $search_results = $wpdb->get_results( + // phpcs:disable + "SELECT DISTINCT posts.ID as product_id, posts.post_parent as parent_id FROM {$wpdb->posts} posts + LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON posts.ID = wc_product_meta_lookup.product_id + $join_query + WHERE posts.post_type IN ('" . implode( "','", $post_types ) . "') + $search_where + $status_where + $type_where + ORDER BY posts.post_parent ASC, posts.post_title ASC + $limit_query + " + // phpcs:enable + ); + + $product_ids = wp_parse_id_list( array_merge( wp_list_pluck( $search_results, 'product_id' ), wp_list_pluck( $search_results, 'parent_id' ) ) ); + + if ( is_numeric( $term ) ) { + $post_id = absint( $term ); + $post_type = get_post_type( $post_id ); + + if ( 'product_variation' === $post_type && $include_variations ) { + $product_ids[] = $post_id; + } elseif ( 'product' === $post_type ) { + $product_ids[] = $post_id; + } + + $product_ids[] = wp_get_post_parent_id( $post_id ); + } + + return wp_parse_id_list( $product_ids ); + } + + /** + * Get the product type based on product ID. + * + * @since 3.0.0 + * @param int $product_id Product ID. + * @return bool|string + */ + public function get_product_type( $product_id ) { + $cache_key = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . '_type_' . $product_id; + $product_type = wp_cache_get( $cache_key, 'products' ); + + if ( $product_type ) { + return $product_type; + } + + $post_type = get_post_type( $product_id ); + + if ( 'product_variation' === $post_type ) { + $product_type = 'variation'; + } elseif ( 'product' === $post_type ) { + $terms = get_the_terms( $product_id, 'product_type' ); + $product_type = ! empty( $terms ) && ! is_wp_error( $terms ) ? sanitize_title( current( $terms )->name ) : 'simple'; + } else { + $product_type = false; + } + + wp_cache_set( $cache_key, $product_type, 'products' ); + + return $product_type; + } + + /** + * Add ability to get products by 'reviews_allowed' in WC_Product_Query. + * + * @since 3.2.0 + * @param string $where Where clause. + * @param WP_Query $wp_query WP_Query instance. + * @return string + */ + public function reviews_allowed_query_where( $where, $wp_query ) { + global $wpdb; + + if ( isset( $wp_query->query_vars['reviews_allowed'] ) && is_bool( $wp_query->query_vars['reviews_allowed'] ) ) { + if ( $wp_query->query_vars['reviews_allowed'] ) { + $where .= " AND $wpdb->posts.comment_status = 'open'"; + } else { + $where .= " AND $wpdb->posts.comment_status = 'closed'"; + } + } + + return $where; + } + + /** + * Get valid WP_Query args from a WC_Product_Query's query variables. + * + * @since 3.2.0 + * @param array $query_vars Query vars from a WC_Product_Query. + * @return array + */ + protected function get_wp_query_args( $query_vars ) { + + // Map query vars to ones that get_wp_query_args or WP_Query recognize. + $key_mapping = array( + 'status' => 'post_status', + 'page' => 'paged', + 'include' => 'post__in', + 'stock_quantity' => 'stock', + 'average_rating' => 'wc_average_rating', + 'review_count' => 'wc_review_count', + ); + foreach ( $key_mapping as $query_key => $db_key ) { + if ( isset( $query_vars[ $query_key ] ) ) { + $query_vars[ $db_key ] = $query_vars[ $query_key ]; + unset( $query_vars[ $query_key ] ); + } + } + + // Map boolean queries that are stored as 'yes'/'no' in the DB to 'yes' or 'no'. + $boolean_queries = array( + 'virtual', + 'downloadable', + 'sold_individually', + 'manage_stock', + ); + foreach ( $boolean_queries as $boolean_query ) { + if ( isset( $query_vars[ $boolean_query ] ) && '' !== $query_vars[ $boolean_query ] ) { + $query_vars[ $boolean_query ] = $query_vars[ $boolean_query ] ? 'yes' : 'no'; + } + } + + // These queries cannot be auto-generated so we have to remove them and build them manually. + $manual_queries = array( + 'sku' => '', + 'featured' => '', + 'visibility' => '', + ); + foreach ( $manual_queries as $key => $manual_query ) { + if ( isset( $query_vars[ $key ] ) ) { + $manual_queries[ $key ] = $query_vars[ $key ]; + unset( $query_vars[ $key ] ); + } + } + + $wp_query_args = parent::get_wp_query_args( $query_vars ); + + if ( ! isset( $wp_query_args['date_query'] ) ) { + $wp_query_args['date_query'] = array(); + } + if ( ! isset( $wp_query_args['meta_query'] ) ) { + $wp_query_args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + } + + // Handle product types. + if ( 'variation' === $query_vars['type'] ) { + $wp_query_args['post_type'] = 'product_variation'; + } elseif ( is_array( $query_vars['type'] ) && in_array( 'variation', $query_vars['type'], true ) ) { + $wp_query_args['post_type'] = array( 'product_variation', 'product' ); + $wp_query_args['tax_query'][] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + 'relation' => 'OR', + array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $query_vars['type'], + ), + array( + 'taxonomy' => 'product_type', + 'field' => 'id', + 'operator' => 'NOT EXISTS', + ), + ); + } else { + $wp_query_args['post_type'] = 'product'; + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $query_vars['type'], + ); + } + + // Handle product categories. + if ( ! empty( $query_vars['category'] ) ) { + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_cat', + 'field' => 'slug', + 'terms' => $query_vars['category'], + ); + } + + // Handle product tags. + if ( ! empty( $query_vars['tag'] ) ) { + unset( $wp_query_args['tag'] ); + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_tag', + 'field' => 'slug', + 'terms' => $query_vars['tag'], + ); + } + + // Handle shipping classes. + if ( ! empty( $query_vars['shipping_class'] ) ) { + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_shipping_class', + 'field' => 'slug', + 'terms' => $query_vars['shipping_class'], + ); + } + + // Handle total_sales. + // This query doesn't get auto-generated since the meta key doesn't have the underscore prefix. + if ( isset( $query_vars['total_sales'] ) && '' !== $query_vars['total_sales'] ) { + $wp_query_args['meta_query'][] = array( + 'key' => 'total_sales', + 'value' => absint( $query_vars['total_sales'] ), + 'compare' => '=', + ); + } + + // Handle SKU. + if ( $manual_queries['sku'] ) { + // Check for existing values if wildcard is used. + if ( '*' === $manual_queries['sku'] ) { + $wp_query_args['meta_query'][] = array( + array( + 'key' => '_sku', + 'compare' => 'EXISTS', + ), + array( + 'key' => '_sku', + 'value' => '', + 'compare' => '!=', + ), + ); + } else { + $wp_query_args['meta_query'][] = array( + 'key' => '_sku', + 'value' => $manual_queries['sku'], + 'compare' => 'LIKE', + ); + } + } + + // Handle featured. + if ( '' !== $manual_queries['featured'] ) { + $product_visibility_term_ids = wc_get_product_visibility_term_ids(); + if ( $manual_queries['featured'] ) { + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => array( $product_visibility_term_ids['featured'] ), + ); + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => array( $product_visibility_term_ids['exclude-from-catalog'] ), + 'operator' => 'NOT IN', + ); + } else { + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => array( $product_visibility_term_ids['featured'] ), + 'operator' => 'NOT IN', + ); + } + } + + // Handle visibility. + if ( $manual_queries['visibility'] ) { + switch ( $manual_queries['visibility'] ) { + case 'search': + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'slug', + 'terms' => array( 'exclude-from-search' ), + 'operator' => 'NOT IN', + ); + break; + case 'catalog': + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'slug', + 'terms' => array( 'exclude-from-catalog' ), + 'operator' => 'NOT IN', + ); + break; + case 'visible': + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'slug', + 'terms' => array( 'exclude-from-catalog', 'exclude-from-search' ), + 'operator' => 'NOT IN', + ); + break; + case 'hidden': + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'slug', + 'terms' => array( 'exclude-from-catalog', 'exclude-from-search' ), + 'operator' => 'AND', + ); + break; + } + } + + // Handle date queries. + $date_queries = array( + 'date_created' => 'post_date', + 'date_modified' => 'post_modified', + 'date_on_sale_from' => '_sale_price_dates_from', + 'date_on_sale_to' => '_sale_price_dates_to', + ); + foreach ( $date_queries as $query_var_key => $db_key ) { + if ( isset( $query_vars[ $query_var_key ] ) && '' !== $query_vars[ $query_var_key ] ) { + + // Remove any existing meta queries for the same keys to prevent conflicts. + $existing_queries = wp_list_pluck( $wp_query_args['meta_query'], 'key', true ); + foreach ( $existing_queries as $query_index => $query_contents ) { + unset( $wp_query_args['meta_query'][ $query_index ] ); + } + + $wp_query_args = $this->parse_date_for_wp_query( $query_vars[ $query_var_key ], $db_key, $wp_query_args ); + } + } + + // Handle paginate. + if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) { + $wp_query_args['no_found_rows'] = true; + } + + // Handle reviews_allowed. + if ( isset( $query_vars['reviews_allowed'] ) && is_bool( $query_vars['reviews_allowed'] ) ) { + add_filter( 'posts_where', array( $this, 'reviews_allowed_query_where' ), 10, 2 ); + } + + // Handle orderby. + if ( isset( $query_vars['orderby'] ) && 'include' === $query_vars['orderby'] ) { + $wp_query_args['orderby'] = 'post__in'; + } + + return apply_filters( 'woocommerce_product_data_store_cpt_get_products_query', $wp_query_args, $query_vars, $this ); + } + + /** + * Query for Products matching specific criteria. + * + * @since 3.2.0 + * + * @param array $query_vars Query vars from a WC_Product_Query. + * + * @return array|object + */ + public function query( $query_vars ) { + $args = $this->get_wp_query_args( $query_vars ); + + if ( ! empty( $args['errors'] ) ) { + $query = (object) array( + 'posts' => array(), + 'found_posts' => 0, + 'max_num_pages' => 0, + ); + } else { + $query = new WP_Query( $args ); + } + + if ( isset( $query_vars['return'] ) && 'objects' === $query_vars['return'] && ! empty( $query->posts ) ) { + // Prime caches before grabbing objects. + update_post_caches( $query->posts, array( 'product', 'product_variation' ) ); + } + + $products = ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) ? $query->posts : array_filter( array_map( 'wc_get_product', $query->posts ) ); + + if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) { + return (object) array( + 'products' => $products, + 'total' => $query->found_posts, + 'max_num_pages' => $query->max_num_pages, + ); + } + + return $products; + } + + /** + * Get data to save to a lookup table. + * + * @since 3.6.0 + * @param int $id ID of object to update. + * @param string $table Lookup table name. + * @return array + */ + protected function get_data_for_lookup_table( $id, $table ) { + if ( 'wc_product_meta_lookup' === $table ) { + $price_meta = (array) get_post_meta( $id, '_price', false ); + $manage_stock = get_post_meta( $id, '_manage_stock', true ); + $stock = 'yes' === $manage_stock ? wc_stock_amount( get_post_meta( $id, '_stock', true ) ) : null; + $price = wc_format_decimal( get_post_meta( $id, '_price', true ) ); + $sale_price = wc_format_decimal( get_post_meta( $id, '_sale_price', true ) ); + return array( + 'product_id' => absint( $id ), + 'sku' => get_post_meta( $id, '_sku', true ), + 'virtual' => 'yes' === get_post_meta( $id, '_virtual', true ) ? 1 : 0, + 'downloadable' => 'yes' === get_post_meta( $id, '_downloadable', true ) ? 1 : 0, + 'min_price' => reset( $price_meta ), + 'max_price' => end( $price_meta ), + 'onsale' => $sale_price && $price === $sale_price ? 1 : 0, + 'stock_quantity' => $stock, + 'stock_status' => get_post_meta( $id, '_stock_status', true ), + 'rating_count' => array_sum( (array) get_post_meta( $id, '_wc_rating_count', true ) ), + 'average_rating' => get_post_meta( $id, '_wc_average_rating', true ), + 'total_sales' => get_post_meta( $id, 'total_sales', true ), + 'tax_status' => get_post_meta( $id, '_tax_status', true ), + 'tax_class' => get_post_meta( $id, '_tax_class', true ), + ); + } + return array(); + } + + /** + * Get primary key name for lookup table. + * + * @since 3.6.0 + * @param string $table Lookup table name. + * @return string + */ + protected function get_primary_key_for_lookup_table( $table ) { + if ( 'wc_product_meta_lookup' === $table ) { + return 'product_id'; + } + return ''; + } + + /** + * Returns query statement for getting current `_stock` of a product. + * + * @internal MAX function below is used to make sure result is a scalar. + * @param int $product_id Product ID. + * @return string|void Query statement. + */ + public function get_query_for_stock( $product_id ) { + global $wpdb; + return $wpdb->prepare( + " + SELECT COALESCE ( MAX( meta_value ), 0 ) FROM $wpdb->postmeta as meta_table + WHERE meta_table.meta_key = '_stock' + AND meta_table.post_id = %d + ", + $product_id + ); + } +} diff --git a/includes/data-stores/class-wc-product-grouped-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-grouped-data-store-cpt.php similarity index 100% rename from includes/data-stores/class-wc-product-grouped-data-store-cpt.php rename to plugins/woocommerce/includes/data-stores/class-wc-product-grouped-data-store-cpt.php diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php new file mode 100644 index 00000000000..5b91e548251 --- /dev/null +++ b/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php @@ -0,0 +1,710 @@ +get_id(), '_product_attributes', true ); + + if ( ! empty( $meta_attributes ) && is_array( $meta_attributes ) ) { + $attributes = array(); + $force_update = false; + foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) { + $meta_value = array_merge( + array( + 'name' => '', + 'value' => '', + 'position' => 0, + 'is_visible' => 0, + 'is_variation' => 0, + 'is_taxonomy' => 0, + ), + (array) $meta_attribute_value + ); + + // Maintain data integrity. 4.9 changed sanitization functions - update the values here so variations function correctly. + if ( $meta_value['is_variation'] && strstr( $meta_value['name'], '/' ) && sanitize_title( $meta_value['name'] ) !== $meta_attribute_key ) { + global $wpdb; + + $old_slug = 'attribute_' . $meta_attribute_key; + $new_slug = 'attribute_' . sanitize_title( $meta_value['name'] ); + $old_meta_rows = $wpdb->get_results( $wpdb->prepare( "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s;", $old_slug ) ); // WPCS: db call ok, cache ok. + + if ( $old_meta_rows ) { + foreach ( $old_meta_rows as $old_meta_row ) { + update_post_meta( $old_meta_row->post_id, $new_slug, $old_meta_row->meta_value ); + } + } + + $force_update = true; + } + + // Check if is a taxonomy attribute. + if ( ! empty( $meta_value['is_taxonomy'] ) ) { + if ( ! taxonomy_exists( $meta_value['name'] ) ) { + continue; + } + $id = wc_attribute_taxonomy_id_by_name( $meta_value['name'] ); + $options = wc_get_object_terms( $product->get_id(), $meta_value['name'], 'term_id' ); + } else { + $id = 0; + $options = wc_get_text_attributes( $meta_value['value'] ); + } + + $attribute = new WC_Product_Attribute(); + $attribute->set_id( $id ); + $attribute->set_name( $meta_value['name'] ); + $attribute->set_options( $options ); + $attribute->set_position( $meta_value['position'] ); + $attribute->set_visible( $meta_value['is_visible'] ); + $attribute->set_variation( $meta_value['is_variation'] ); + $attributes[] = $attribute; + } + $product->set_attributes( $attributes ); + + if ( $force_update ) { + $this->update_attributes( $product, true ); + } + } + } + + /** + * Read product data. + * + * @param WC_Product $product Product object. + * + * @since 3.0.0 + */ + protected function read_product_data( &$product ) { + parent::read_product_data( $product ); + + // Make sure data which does not apply to variables is unset. + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + } + + /** + * Loads variation child IDs. + * + * @param WC_Product $product Product object. + * @param bool $force_read True to bypass the transient. + * + * @return array + */ + public function read_children( &$product, $force_read = false ) { + $children_transient_name = 'wc_product_children_' . $product->get_id(); + $children = get_transient( $children_transient_name ); + + if ( empty( $children ) || ! is_array( $children ) || ! isset( $children['all'] ) || ! isset( $children['visible'] ) || $force_read ) { + $all_args = array( + 'post_parent' => $product->get_id(), + 'post_type' => 'product_variation', + 'orderby' => array( + 'menu_order' => 'ASC', + 'ID' => 'ASC', + ), + 'fields' => 'ids', + 'post_status' => array( 'publish', 'private' ), + 'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts + ); + + $visible_only_args = $all_args; + $visible_only_args['post_status'] = 'publish'; + + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { + $visible_only_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'name', + 'terms' => 'outofstock', + 'operator' => 'NOT IN', + ); + } + $children['all'] = get_posts( apply_filters( 'woocommerce_variable_children_args', $all_args, $product, false ) ); + $children['visible'] = get_posts( apply_filters( 'woocommerce_variable_children_args', $visible_only_args, $product, true ) ); + + set_transient( $children_transient_name, $children, DAY_IN_SECONDS * 30 ); + } + + $children['all'] = wp_parse_id_list( (array) $children['all'] ); + $children['visible'] = wp_parse_id_list( (array) $children['visible'] ); + + return $children; + } + + /** + * Loads an array of attributes used for variations, as well as their possible values. + * + * @param WC_Product $product Product object. + * + * @return array + */ + public function read_variation_attributes( &$product ) { + global $wpdb; + + $variation_attributes = array(); + $attributes = $product->get_attributes(); + $child_ids = $product->get_children(); + $cache_key = WC_Cache_Helper::get_cache_prefix( 'product_' . $product->get_id() ) . 'product_variation_attributes_' . $product->get_id(); + $cache_group = 'products'; + $cached_data = wp_cache_get( $cache_key, $cache_group ); + + if ( false !== $cached_data ) { + return $cached_data; + } + + if ( ! empty( $attributes ) ) { + foreach ( $attributes as $attribute ) { + if ( empty( $attribute['is_variation'] ) ) { + continue; + } + + // Get possible values for this attribute, for only visible variations. + if ( ! empty( $child_ids ) ) { + $format = array_fill( 0, count( $child_ids ), '%d' ); + $query_in = '(' . implode( ',', $format ) . ')'; + $query_args = array( 'attribute_name' => wc_variation_attribute_name( $attribute['name'] ) ) + $child_ids; + $values = array_unique( + $wpdb->get_col( + $wpdb->prepare( + "SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN {$query_in}", // @codingStandardsIgnoreLine. + $query_args + ) + ) + ); + } else { + $values = array(); + } + + // Empty value indicates that all options for given attribute are available. + if ( in_array( null, $values, true ) || in_array( '', $values, true ) || empty( $values ) ) { + $values = $attribute['is_taxonomy'] ? wc_get_object_terms( $product->get_id(), $attribute['name'], 'slug' ) : wc_get_text_attributes( $attribute['value'] ); + // Get custom attributes (non taxonomy) as defined. + } elseif ( ! $attribute['is_taxonomy'] ) { + $text_attributes = wc_get_text_attributes( $attribute['value'] ); + $assigned_text_attributes = $values; + $values = array(); + + // Pre 2.4 handling where 'slugs' were saved instead of the full text attribute. + if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) { + $assigned_text_attributes = array_map( 'sanitize_title', $assigned_text_attributes ); + foreach ( $text_attributes as $text_attribute ) { + if ( in_array( sanitize_title( $text_attribute ), $assigned_text_attributes, true ) ) { + $values[] = $text_attribute; + } + } + } else { + foreach ( $text_attributes as $text_attribute ) { + if ( in_array( $text_attribute, $assigned_text_attributes, true ) ) { + $values[] = $text_attribute; + } + } + } + } + $variation_attributes[ $attribute['name'] ] = array_unique( $values ); + } + } + + wp_cache_set( $cache_key, $variation_attributes, $cache_group ); + + return $variation_attributes; + } + + /** + * Get an array of all sale and regular prices from all variations. This is used for example when displaying the price range at variable product level or seeing if the variable product is on sale. + * + * Can be filtered by plugins which modify costs, but otherwise will include the raw meta costs unlike get_price() which runs costs through the woocommerce_get_price filter. + * This is to ensure modified prices are not cached, unless intended. + * + * @param WC_Product $product Product object. + * @param bool $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes). + * + * @return array of prices + * @since 3.0.0 + */ + public function read_price_data( &$product, $for_display = false ) { + + /** + * Transient name for storing prices for this product (note: Max transient length is 45) + * + * @since 2.5.0 a single transient is used per product for all prices, rather than many transients per product. + */ + $transient_name = 'wc_var_prices_' . $product->get_id(); + $transient_version = WC_Cache_Helper::get_transient_version( 'product' ); + $price_hash = $this->get_price_hash( $product, $for_display ); + + // Check if prices array is stale. + if ( ! isset( $this->prices_array['version'] ) || $this->prices_array['version'] !== $transient_version ) { + $this->prices_array = array( + 'version' => $transient_version, + ); + } + + /** + * $this->prices_array is an array of values which may have been modified from what is stored in transients - this may not match $transient_cached_prices_array. + * If the value has already been generated, we don't need to grab the values again so just return them. They are already filtered. + */ + if ( empty( $this->prices_array[ $price_hash ] ) ) { + $transient_cached_prices_array = array_filter( (array) json_decode( strval( get_transient( $transient_name ) ), true ) ); + + // If the product version has changed since the transient was last saved, reset the transient cache. + if ( ! isset( $transient_cached_prices_array['version'] ) || $transient_version !== $transient_cached_prices_array['version'] ) { + $transient_cached_prices_array = array( + 'version' => $transient_version, + ); + } + + // If the prices are not stored for this hash, generate them and add to the transient. + if ( empty( $transient_cached_prices_array[ $price_hash ] ) ) { + $prices_array = array( + 'price' => array(), + 'regular_price' => array(), + 'sale_price' => array(), + ); + + $variation_ids = $product->get_visible_children(); + + if ( is_callable( '_prime_post_caches' ) ) { + _prime_post_caches( $variation_ids ); + } + + foreach ( $variation_ids as $variation_id ) { + $variation = wc_get_product( $variation_id ); + + if ( $variation ) { + $price = apply_filters( 'woocommerce_variation_prices_price', $variation->get_price( 'edit' ), $variation, $product ); + $regular_price = apply_filters( 'woocommerce_variation_prices_regular_price', $variation->get_regular_price( 'edit' ), $variation, $product ); + $sale_price = apply_filters( 'woocommerce_variation_prices_sale_price', $variation->get_sale_price( 'edit' ), $variation, $product ); + + // Skip empty prices. + if ( '' === $price ) { + continue; + } + + // If sale price does not equal price, the product is not yet on sale. + if ( $sale_price === $regular_price || $sale_price !== $price ) { + $sale_price = $regular_price; + } + + // If we are getting prices for display, we need to account for taxes. + if ( $for_display ) { + if ( 'incl' === get_option( 'woocommerce_tax_display_shop' ) ) { + $price = '' === $price ? '' : wc_get_price_including_tax( + $variation, + array( + 'qty' => 1, + 'price' => $price, + ) + ); + $regular_price = '' === $regular_price ? '' : wc_get_price_including_tax( + $variation, + array( + 'qty' => 1, + 'price' => $regular_price, + ) + ); + $sale_price = '' === $sale_price ? '' : wc_get_price_including_tax( + $variation, + array( + 'qty' => 1, + 'price' => $sale_price, + ) + ); + } else { + $price = '' === $price ? '' : wc_get_price_excluding_tax( + $variation, + array( + 'qty' => 1, + 'price' => $price, + ) + ); + $regular_price = '' === $regular_price ? '' : wc_get_price_excluding_tax( + $variation, + array( + 'qty' => 1, + 'price' => $regular_price, + ) + ); + $sale_price = '' === $sale_price ? '' : wc_get_price_excluding_tax( + $variation, + array( + 'qty' => 1, + 'price' => $sale_price, + ) + ); + } + } + + $prices_array['price'][ $variation_id ] = wc_format_decimal( $price, wc_get_price_decimals() ); + $prices_array['regular_price'][ $variation_id ] = wc_format_decimal( $regular_price, wc_get_price_decimals() ); + $prices_array['sale_price'][ $variation_id ] = wc_format_decimal( $sale_price, wc_get_price_decimals() ); + + $prices_array = apply_filters( 'woocommerce_variation_prices_array', $prices_array, $variation, $for_display ); + } + } + + // Add all pricing data to the transient array. + foreach ( $prices_array as $key => $values ) { + $transient_cached_prices_array[ $price_hash ][ $key ] = $values; + } + + set_transient( $transient_name, wp_json_encode( $transient_cached_prices_array ), DAY_IN_SECONDS * 30 ); + } + + /** + * Give plugins one last chance to filter the variation prices array which has been generated and store locally to the class. + * This value may differ from the transient cache. It is filtered once before storing locally. + */ + $this->prices_array[ $price_hash ] = apply_filters( 'woocommerce_variation_prices', $transient_cached_prices_array[ $price_hash ], $product, $for_display ); + } + return $this->prices_array[ $price_hash ]; + } + + /** + * Create unique cache key based on the tax location (affects displayed/cached prices), product version and active price filters. + * DEVELOPERS should filter this hash if offering conditional pricing to keep it unique. + * + * @param WC_Product $product Product object. + * @param bool $for_display If taxes should be calculated or not. + * + * @since 3.0.0 + * @return string + */ + protected function get_price_hash( &$product, $for_display = false ) { + global $wp_filter; + + $price_hash = array( false ); + + if ( $for_display && wc_tax_enabled() ) { + $price_hash = array( + get_option( 'woocommerce_tax_display_shop', 'excl' ), + WC_Tax::get_rates(), + empty( WC()->customer ) ? false : WC()->customer->is_vat_exempt(), + ); + } + + $filter_names = array( 'woocommerce_variation_prices_price', 'woocommerce_variation_prices_regular_price', 'woocommerce_variation_prices_sale_price' ); + + foreach ( $filter_names as $filter_name ) { + if ( ! empty( $wp_filter[ $filter_name ] ) ) { + $price_hash[ $filter_name ] = array(); + + foreach ( $wp_filter[ $filter_name ] as $priority => $callbacks ) { + $price_hash[ $filter_name ][] = array_values( wp_list_pluck( $callbacks, 'function' ) ); + } + } + } + + return md5( wp_json_encode( apply_filters( 'woocommerce_get_variation_prices_hash', $price_hash, $product, $for_display ) ) ); + } + + /** + * Does a child have a weight set? + * + * @param WC_Product $product Product object. + * + * @since 3.0.0 + * @return boolean + */ + public function child_has_weight( $product ) { + global $wpdb; + $children = $product->get_visible_children(); + if ( ! $children ) { + return false; + } + + $format = array_fill( 0, count( $children ), '%d' ); + $query_in = '(' . implode( ',', $format ) . ')'; + + return null !== $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_weight' AND meta_value > 0 AND post_id IN {$query_in}", $children ) ); // @codingStandardsIgnoreLine. + } + + /** + * Does a child have dimensions set? + * + * @param WC_Product $product Product object. + * + * @since 3.0.0 + * @return boolean + */ + public function child_has_dimensions( $product ) { + global $wpdb; + $children = $product->get_visible_children(); + if ( ! $children ) { + return false; + } + + $format = array_fill( 0, count( $children ), '%d' ); + $query_in = '(' . implode( ',', $format ) . ')'; + + return null !== $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key IN ( '_length', '_width', '_height' ) AND meta_value > 0 AND post_id IN {$query_in}", $children ) ); // @codingStandardsIgnoreLine. + } + + /** + * Is a child in stock? + * + * @param WC_Product $product Product object. + * + * @since 3.0.0 + * @return boolean + */ + public function child_is_in_stock( $product ) { + return $this->child_has_stock_status( $product, 'instock' ); + } + + /** + * Does a child have a stock status? + * + * @param WC_Product $product Product object. + * @param string $status 'instock', 'outofstock', or 'onbackorder'. + * + * @since 3.3.0 + * @return boolean + */ + public function child_has_stock_status( $product, $status ) { + global $wpdb; + + $children = $product->get_children(); + + if ( $children ) { + $format = array_fill( 0, count( $children ), '%d' ); + $query_in = '(' . implode( ',', $format ) . ')'; + $query_args = array( 'stock_status' => $status ) + $children; + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + if ( get_option( 'woocommerce_product_lookup_table_is_generating' ) ) { + $query = "SELECT COUNT( post_id ) FROM {$wpdb->postmeta} WHERE meta_key = '_stock_status' AND meta_value = %s AND post_id IN {$query_in}"; + } else { + $query = "SELECT COUNT( product_id ) FROM {$wpdb->wc_product_meta_lookup} WHERE stock_status = %s AND product_id IN {$query_in}"; + } + $children_with_status = $wpdb->get_var( + $wpdb->prepare( + $query, + $query_args + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared + } else { + $children_with_status = 0; + } + + return (bool) $children_with_status; + } + + /** + * Syncs all variation names if the parent name is changed. + * + * @param WC_Product $product Product object. + * @param string $previous_name Variation previous name. + * @param string $new_name Variation new name. + * + * @since 3.0.0 + */ + public function sync_variation_names( &$product, $previous_name = '', $new_name = '' ) { + if ( $new_name !== $previous_name ) { + global $wpdb; + + $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->posts} + SET post_title = REPLACE( post_title, %s, %s ) + WHERE post_type = 'product_variation' + AND post_parent = %d", + $previous_name ? $previous_name : 'AUTO-DRAFT', + $new_name, + $product->get_id() + ) + ); + } + } + + /** + * Stock managed at the parent level - update children being managed by this product. + * This sync function syncs downwards (from parent to child) when the variable product is saved. + * + * @param WC_Product $product Product object. + * + * @since 3.0.0 + */ + public function sync_managed_variation_stock_status( &$product ) { + global $wpdb; + + if ( $product->get_manage_stock() ) { + $children = $product->get_children(); + $changed = false; + + if ( $children ) { + $status = $product->get_stock_status(); + $format = array_fill( 0, count( $children ), '%d' ); + $query_in = '(' . implode( ',', $format ) . ')'; + $managed_children = array_unique( $wpdb->get_col( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_manage_stock' AND meta_value != 'yes' AND post_id IN {$query_in}", $children ) ) ); // @codingStandardsIgnoreLine. + foreach ( $managed_children as $managed_child ) { + if ( update_post_meta( $managed_child, '_stock_status', $status ) ) { + $this->update_lookup_table( $managed_child, 'wc_product_meta_lookup' ); + $changed = true; + } + } + } + + if ( $changed ) { + $children = $this->read_children( $product, true ); + $product->set_children( $children['all'] ); + $product->set_visible_children( $children['visible'] ); + } + } + } + + /** + * Sync variable product prices with children. + * + * @param WC_Product $product Product object. + * + * @since 3.0.0 + */ + public function sync_price( &$product ) { + global $wpdb; + + $children = $product->get_visible_children(); + if ( $children ) { + $format = array_fill( 0, count( $children ), '%d' ); + $query_in = '(' . implode( ',', $format ) . ')'; + $prices = array_unique( $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = '_price' AND post_id IN {$query_in}", $children ) ) ); // @codingStandardsIgnoreLine. + } else { + $prices = array(); + } + + delete_post_meta( $product->get_id(), '_price' ); + delete_post_meta( $product->get_id(), '_sale_price' ); + delete_post_meta( $product->get_id(), '_regular_price' ); + + if ( $prices ) { + sort( $prices, SORT_NUMERIC ); + // To allow sorting and filtering by multiple values, we have no choice but to store child prices in this manner. + foreach ( $prices as $price ) { + if ( is_null( $price ) || '' === $price ) { + continue; + } + add_post_meta( $product->get_id(), '_price', $price, false ); + } + } + + $this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' ); + + /** + * Fire an action for this direct update so it can be detected by other code. + * + * @since 3.6 + * @param int $product_id Product ID that was updated directly. + */ + do_action( 'woocommerce_updated_product_price', $product->get_id() ); + } + + /** + * Sync variable product stock status with children. + * Change does not persist unless saved by caller. + * + * @param WC_Product $product Product object. + * + * @since 3.0.0 + */ + public function sync_stock_status( &$product ) { + if ( $product->child_is_in_stock() ) { + $product->set_stock_status( 'instock' ); + } elseif ( $product->child_is_on_backorder() ) { + $product->set_stock_status( 'onbackorder' ); + } else { + $product->set_stock_status( 'outofstock' ); + } + } + + /** + * Delete variations of a product. + * + * @param int $product_id Product ID. + * @param bool $force_delete False to trash. + * + * @since 3.0.0 + */ + public function delete_variations( $product_id, $force_delete = false ) { + if ( ! is_numeric( $product_id ) || 0 >= $product_id ) { + return; + } + + $variation_ids = wp_parse_id_list( + get_posts( + array( + 'post_parent' => $product_id, + 'post_type' => 'product_variation', + 'fields' => 'ids', + 'post_status' => array( 'any', 'trash', 'auto-draft' ), + 'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts + ) + ) + ); + + if ( ! empty( $variation_ids ) ) { + foreach ( $variation_ids as $variation_id ) { + if ( $force_delete ) { + do_action( 'woocommerce_before_delete_product_variation', $variation_id ); + wp_delete_post( $variation_id, true ); + do_action( 'woocommerce_delete_product_variation', $variation_id ); + } else { + wp_trash_post( $variation_id ); + do_action( 'woocommerce_trash_product_variation', $variation_id ); + } + } + } + + delete_transient( 'wc_product_children_' . $product_id ); + } + + /** + * Untrash variations. + * + * @param int $product_id Product ID. + */ + public function untrash_variations( $product_id ) { + $variation_ids = wp_parse_id_list( + get_posts( + array( + 'post_parent' => $product_id, + 'post_type' => 'product_variation', + 'fields' => 'ids', + 'post_status' => 'trash', + 'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts + ) + ) + ); + + if ( ! empty( $variation_ids ) ) { + foreach ( $variation_ids as $variation_id ) { + wp_untrash_post( $variation_id ); + } + } + + delete_transient( 'wc_product_children_' . $product_id ); + } +} diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-variation-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-variation-data-store-cpt.php new file mode 100644 index 00000000000..cf481afc41a --- /dev/null +++ b/plugins/woocommerce/includes/data-stores/class-wc-product-variation-data-store-cpt.php @@ -0,0 +1,547 @@ +meta_key, $this->internal_meta_keys, true ) && 0 !== stripos( $meta->meta_key, 'attribute_' ) && 0 !== stripos( $meta->meta_key, 'wp_' ); + } + + /* + |-------------------------------------------------------------------------- + | CRUD Methods + |-------------------------------------------------------------------------- + */ + + /** + * Reads a product from the database and sets its data to the class. + * + * @since 3.0.0 + * @param WC_Product_Variation $product Product object. + * @throws WC_Data_Exception If WC_Product::set_tax_status() is called with an invalid tax status (via read_product_data), or when passing an invalid ID. + */ + public function read( &$product ) { + $product->set_defaults(); + + if ( ! $product->get_id() ) { + return; + } + + $post_object = get_post( $product->get_id() ); + + if ( ! $post_object ) { + return; + } + + if ( 'product_variation' !== $post_object->post_type ) { + throw new WC_Data_Exception( 'variation_invalid_id', __( 'Invalid product type: passed ID does not correspond to a product variation.', 'woocommerce' ) ); + } + + $product->set_props( + array( + 'name' => $post_object->post_title, + 'slug' => $post_object->post_name, + 'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ), + 'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ), + 'status' => $post_object->post_status, + 'menu_order' => $post_object->menu_order, + 'reviews_allowed' => 'open' === $post_object->comment_status, + 'parent_id' => $post_object->post_parent, + 'attribute_summary' => $post_object->post_excerpt, + ) + ); + + // The post parent is not a valid variable product so we should prevent this. + if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) { + $product->set_parent_id( 0 ); + } + + $this->read_downloads( $product ); + $this->read_product_data( $product ); + $this->read_extra_data( $product ); + $product->set_attributes( wc_get_product_variation_attributes( $product->get_id() ) ); + + $updates = array(); + /** + * If a variation title is not in sync with the parent e.g. saved prior to 3.0, or if the parent title has changed, detect here and update. + */ + $new_title = $this->generate_product_title( $product ); + + if ( $post_object->post_title !== $new_title ) { + $product->set_name( $new_title ); + $updates = array_merge( $updates, array( 'post_title' => $new_title ) ); + } + + /** + * If the attribute summary is not in sync, update here. Used when searching for variations by attribute values. + * This is meant to also cover the case when global attribute name or value is updated, then the attribute summary is updated + * for respective products when they're read. + */ + $new_attribute_summary = $this->generate_attribute_summary( $product ); + + if ( $new_attribute_summary !== $post_object->post_excerpt ) { + $product->set_attribute_summary( $new_attribute_summary ); + $updates = array_merge( $updates, array( 'post_excerpt' => $new_attribute_summary ) ); + } + + if ( ! empty( $updates ) ) { + $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $updates, array( 'ID' => $product->get_id() ) ); + clean_post_cache( $product->get_id() ); + } + + // Set object_read true once all data is read. + $product->set_object_read( true ); + } + + /** + * Create a new product. + * + * @since 3.0.0 + * @param WC_Product_Variation $product Product object. + */ + public function create( &$product ) { + if ( ! $product->get_date_created() ) { + $product->set_date_created( time() ); + } + + $new_title = $this->generate_product_title( $product ); + + if ( $product->get_name( 'edit' ) !== $new_title ) { + $product->set_name( $new_title ); + } + + $attribute_summary = $this->generate_attribute_summary( $product ); + $product->set_attribute_summary( $attribute_summary ); + + // The post parent is not a valid variable product so we should prevent this. + if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) { + $product->set_parent_id( 0 ); + } + + $id = wp_insert_post( + apply_filters( + 'woocommerce_new_product_variation_data', + array( + 'post_type' => 'product_variation', + 'post_status' => $product->get_status() ? $product->get_status() : 'publish', + 'post_author' => get_current_user_id(), + 'post_title' => $product->get_name( 'edit' ), + 'post_excerpt' => $product->get_attribute_summary( 'edit' ), + 'post_content' => '', + 'post_parent' => $product->get_parent_id(), + 'comment_status' => 'closed', + 'ping_status' => 'closed', + 'menu_order' => $product->get_menu_order(), + 'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ), + 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ), + 'post_name' => $product->get_slug( 'edit' ), + ) + ), + true + ); + + if ( $id && ! is_wp_error( $id ) ) { + $product->set_id( $id ); + + $this->update_post_meta( $product, true ); + $this->update_terms( $product, true ); + $this->update_visibility( $product, true ); + $this->update_attributes( $product, true ); + $this->handle_updated_props( $product ); + + $product->save_meta_data(); + $product->apply_changes(); + + $this->update_version_and_type( $product ); + $this->update_guid( $product ); + + $this->clear_caches( $product ); + + do_action( 'woocommerce_new_product_variation', $id, $product ); + } + } + + /** + * Updates an existing product. + * + * @since 3.0.0 + * @param WC_Product_Variation $product Product object. + */ + public function update( &$product ) { + $product->save_meta_data(); + + if ( ! $product->get_date_created() ) { + $product->set_date_created( time() ); + } + + $new_title = $this->generate_product_title( $product ); + + if ( $product->get_name( 'edit' ) !== $new_title ) { + $product->set_name( $new_title ); + } + + // The post parent is not a valid variable product so we should prevent this. + if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) { + $product->set_parent_id( 0 ); + } + + $changes = $product->get_changes(); + + if ( array_intersect( array( 'attributes' ), array_keys( $changes ) ) ) { + $product->set_attribute_summary( $this->generate_attribute_summary( $product ) ); + } + + // Only update the post when the post data changes. + if ( array_intersect( array( 'name', 'parent_id', 'status', 'menu_order', 'date_created', 'date_modified', 'attributes' ), array_keys( $changes ) ) ) { + $post_data = array( + 'post_title' => $product->get_name( 'edit' ), + 'post_excerpt' => $product->get_attribute_summary( 'edit' ), + 'post_parent' => $product->get_parent_id( 'edit' ), + 'comment_status' => 'closed', + 'post_status' => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish', + 'menu_order' => $product->get_menu_order( 'edit' ), + 'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ), + 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ), + 'post_modified' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ), + 'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ), + 'post_type' => 'product_variation', + 'post_name' => $product->get_slug( 'edit' ), + ); + + /** + * When updating this object, to prevent infinite loops, use $wpdb + * to update data, since wp_update_post spawns more calls to the + * save_post action. + * + * This ensures hooks are fired by either WP itself (admin screen save), + * or an update purely from CRUD. + */ + if ( doing_action( 'save_post' ) ) { + $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) ); + clean_post_cache( $product->get_id() ); + } else { + wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) ); + } + $product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook. + + } else { // Only update post modified time to record this save event. + $GLOBALS['wpdb']->update( + $GLOBALS['wpdb']->posts, + array( + 'post_modified' => current_time( 'mysql' ), + 'post_modified_gmt' => current_time( 'mysql', 1 ), + ), + array( + 'ID' => $product->get_id(), + ) + ); + clean_post_cache( $product->get_id() ); + } + + $this->update_post_meta( $product ); + $this->update_terms( $product ); + $this->update_visibility( $product, true ); + $this->update_attributes( $product ); + $this->handle_updated_props( $product ); + + $product->apply_changes(); + + $this->update_version_and_type( $product ); + + $this->clear_caches( $product ); + + do_action( 'woocommerce_update_product_variation', $product->get_id(), $product ); + } + + /* + |-------------------------------------------------------------------------- + | Additional Methods + |-------------------------------------------------------------------------- + */ + + /** + * Generates a title with attribute information for a variation. + * Products will get a title of the form "Name - Value, Value" or just "Name". + * + * @since 3.0.0 + * @param WC_Product $product Product object. + * @return string + */ + protected function generate_product_title( $product ) { + $attributes = (array) $product->get_attributes(); + + // Do not include attributes if the product has 3+ attributes. + $should_include_attributes = count( $attributes ) < 3; + + // Do not include attributes if an attribute name has 2+ words and the + // product has multiple attributes. + if ( $should_include_attributes && 1 < count( $attributes ) ) { + foreach ( $attributes as $name => $value ) { + if ( false !== strpos( $name, '-' ) ) { + $should_include_attributes = false; + break; + } + } + } + + $should_include_attributes = apply_filters( 'woocommerce_product_variation_title_include_attributes', $should_include_attributes, $product ); + $separator = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', $product ); + $title_base = get_post_field( 'post_title', $product->get_parent_id() ); + $title_suffix = $should_include_attributes ? wc_get_formatted_variation( $product, true, false ) : ''; + + return apply_filters( 'woocommerce_product_variation_title', $title_suffix ? $title_base . $separator . $title_suffix : $title_base, $product, $title_base, $title_suffix ); + } + + /** + * Generates attribute summary for the variation. + * + * Attribute summary contains comma-delimited 'attribute_name: attribute_value' pairs for all attributes. + * + * @since 3.6.0 + * @param WC_Product_Variation $product Product variation to generate the attribute summary for. + * + * @return string + */ + protected function generate_attribute_summary( $product ) { + return wc_get_formatted_variation( $product, true, true ); + } + + /** + * Make sure we store the product version (to track data changes). + * + * @param WC_Product $product Product object. + * @since 3.0.0 + */ + protected function update_version_and_type( &$product ) { + wp_set_object_terms( $product->get_id(), '', 'product_type' ); + update_post_meta( $product->get_id(), '_product_version', Constants::get_constant( 'WC_VERSION' ) ); + } + + /** + * Read post data. + * + * @since 3.0.0 + * @param WC_Product_Variation $product Product object. + * @throws WC_Data_Exception If WC_Product::set_tax_status() is called with an invalid tax status. + */ + protected function read_product_data( &$product ) { + $id = $product->get_id(); + + $product->set_props( + array( + 'description' => get_post_meta( $id, '_variation_description', true ), + 'regular_price' => get_post_meta( $id, '_regular_price', true ), + 'sale_price' => get_post_meta( $id, '_sale_price', true ), + 'date_on_sale_from' => get_post_meta( $id, '_sale_price_dates_from', true ), + 'date_on_sale_to' => get_post_meta( $id, '_sale_price_dates_to', true ), + 'manage_stock' => get_post_meta( $id, '_manage_stock', true ), + 'stock_status' => get_post_meta( $id, '_stock_status', true ), + 'low_stock_amount' => get_post_meta( $id, '_low_stock_amount', true ), + 'shipping_class_id' => current( $this->get_term_ids( $id, 'product_shipping_class' ) ), + 'virtual' => get_post_meta( $id, '_virtual', true ), + 'downloadable' => get_post_meta( $id, '_downloadable', true ), + 'gallery_image_ids' => array_filter( explode( ',', get_post_meta( $id, '_product_image_gallery', true ) ) ), + 'download_limit' => get_post_meta( $id, '_download_limit', true ), + 'download_expiry' => get_post_meta( $id, '_download_expiry', true ), + 'image_id' => get_post_thumbnail_id( $id ), + 'backorders' => get_post_meta( $id, '_backorders', true ), + 'sku' => get_post_meta( $id, '_sku', true ), + 'stock_quantity' => get_post_meta( $id, '_stock', true ), + 'weight' => get_post_meta( $id, '_weight', true ), + 'length' => get_post_meta( $id, '_length', true ), + 'width' => get_post_meta( $id, '_width', true ), + 'height' => get_post_meta( $id, '_height', true ), + 'tax_class' => ! metadata_exists( 'post', $id, '_tax_class' ) ? 'parent' : get_post_meta( $id, '_tax_class', true ), + ) + ); + + if ( $product->is_on_sale( 'edit' ) ) { + $product->set_price( $product->get_sale_price( 'edit' ) ); + } else { + $product->set_price( $product->get_regular_price( 'edit' ) ); + } + + $parent_object = get_post( $product->get_parent_id() ); + $terms = get_the_terms( $product->get_parent_id(), 'product_visibility' ); + $term_names = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array(); + $exclude_search = in_array( 'exclude-from-search', $term_names, true ); + $exclude_catalog = in_array( 'exclude-from-catalog', $term_names, true ); + + if ( $exclude_search && $exclude_catalog ) { + $catalog_visibility = 'hidden'; + } elseif ( $exclude_search ) { + $catalog_visibility = 'catalog'; + } elseif ( $exclude_catalog ) { + $catalog_visibility = 'search'; + } else { + $catalog_visibility = 'visible'; + } + + $product->set_parent_data( + array( + 'title' => $parent_object ? $parent_object->post_title : '', + 'status' => $parent_object ? $parent_object->post_status : '', + 'sku' => get_post_meta( $product->get_parent_id(), '_sku', true ), + 'manage_stock' => get_post_meta( $product->get_parent_id(), '_manage_stock', true ), + 'backorders' => get_post_meta( $product->get_parent_id(), '_backorders', true ), + 'stock_quantity' => wc_stock_amount( get_post_meta( $product->get_parent_id(), '_stock', true ) ), + 'weight' => get_post_meta( $product->get_parent_id(), '_weight', true ), + 'length' => get_post_meta( $product->get_parent_id(), '_length', true ), + 'width' => get_post_meta( $product->get_parent_id(), '_width', true ), + 'height' => get_post_meta( $product->get_parent_id(), '_height', true ), + 'tax_class' => get_post_meta( $product->get_parent_id(), '_tax_class', true ), + 'shipping_class_id' => absint( current( $this->get_term_ids( $product->get_parent_id(), 'product_shipping_class' ) ) ), + 'image_id' => get_post_thumbnail_id( $product->get_parent_id() ), + 'purchase_note' => get_post_meta( $product->get_parent_id(), '_purchase_note', true ), + 'catalog_visibility' => $catalog_visibility, + ) + ); + + // Pull data from the parent when there is no user-facing way to set props. + $product->set_sold_individually( get_post_meta( $product->get_parent_id(), '_sold_individually', true ) ); + $product->set_tax_status( get_post_meta( $product->get_parent_id(), '_tax_status', true ) ); + $product->set_cross_sell_ids( get_post_meta( $product->get_parent_id(), '_crosssell_ids', true ) ); + } + + /** + * For all stored terms in all taxonomies, save them to the DB. + * + * @since 3.0.0 + * @param WC_Product $product Product object. + * @param bool $force Force update. Used during create. + */ + protected function update_terms( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) { + wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false ); + } + } + + /** + * Update visibility terms based on props. + * + * @since 3.0.0 + * + * @param WC_Product $product Product object. + * @param bool $force Force update. Used during create. + */ + protected function update_visibility( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_intersect( array( 'stock_status' ), array_keys( $changes ) ) ) { + $terms = array(); + + if ( 'outofstock' === $product->get_stock_status() ) { + $terms[] = 'outofstock'; + } + + wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false ); + } + } + + /** + * Update attribute meta values. + * + * @since 3.0.0 + * @param WC_Product $product Product object. + * @param bool $force Force update. Used during create. + */ + protected function update_attributes( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_key_exists( 'attributes', $changes ) ) { + global $wpdb; + + $product_id = $product->get_id(); + $attributes = $product->get_attributes(); + $updated_attribute_keys = array(); + foreach ( $attributes as $key => $value ) { + update_post_meta( $product_id, 'attribute_' . $key, wp_slash( $value ) ); + $updated_attribute_keys[] = 'attribute_' . $key; + } + + // Remove old taxonomies attributes so data is kept up to date - first get attribute key names. + $delete_attribute_keys = $wpdb->get_col( + $wpdb->prepare( + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration + "SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE %s AND meta_key NOT IN ( '" . implode( "','", array_map( 'esc_sql', $updated_attribute_keys ) ) . "' ) AND post_id = %d", + $wpdb->esc_like( 'attribute_' ) . '%', + $product_id + ) + ); + + foreach ( $delete_attribute_keys as $key ) { + delete_post_meta( $product_id, $key ); + } + } + } + + /** + * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class. + * + * @since 3.0.0 + * @param WC_Product $product Product object. + * @param bool $force Force update. Used during create. + */ + public function update_post_meta( &$product, $force = false ) { + $meta_key_to_props = array( + '_variation_description' => 'description', + ); + + $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props ); + + foreach ( $props_to_update as $meta_key => $prop ) { + $value = $product->{"get_$prop"}( 'edit' ); + $updated = update_post_meta( $product->get_id(), $meta_key, $value ); + if ( $updated ) { + $this->updated_props[] = $prop; + } + } + + parent::update_post_meta( $product, $force ); + } + + /** + * Update product variation guid. + * + * @param WC_Product_Variation $product Product variation object. + * + * @since 3.6.0 + */ + protected function update_guid( $product ) { + global $wpdb; + + $guid = home_url( + add_query_arg( + array( + 'post_type' => 'product_variation', + 'p' => $product->get_id(), + ), + '' + ) + ); + $wpdb->update( $wpdb->posts, array( 'guid' => $guid ), array( 'ID' => $product->get_id() ) ); + } +} diff --git a/plugins/woocommerce/includes/data-stores/class-wc-shipping-zone-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-shipping-zone-data-store.php new file mode 100644 index 00000000000..6d795611651 --- /dev/null +++ b/plugins/woocommerce/includes/data-stores/class-wc-shipping-zone-data-store.php @@ -0,0 +1,373 @@ +insert( + $wpdb->prefix . 'woocommerce_shipping_zones', + array( + 'zone_name' => $zone->get_zone_name(), + 'zone_order' => $zone->get_zone_order(), + ) + ); + $zone->set_id( $wpdb->insert_id ); + $zone->save_meta_data(); + $this->save_locations( $zone ); + $zone->apply_changes(); + WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' ); + WC_Cache_Helper::get_transient_version( 'shipping', true ); + } + + /** + * Update zone in the database. + * + * @since 3.0.0 + * @param WC_Shipping_Zone $zone Shipping zone object. + */ + public function update( &$zone ) { + global $wpdb; + if ( $zone->get_id() ) { + $wpdb->update( + $wpdb->prefix . 'woocommerce_shipping_zones', + array( + 'zone_name' => $zone->get_zone_name(), + 'zone_order' => $zone->get_zone_order(), + ), + array( 'zone_id' => $zone->get_id() ) + ); + } + $zone->save_meta_data(); + $this->save_locations( $zone ); + $zone->apply_changes(); + WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' ); + WC_Cache_Helper::get_transient_version( 'shipping', true ); + } + + /** + * Method to read a shipping zone from the database. + * + * @since 3.0.0 + * @param WC_Shipping_Zone $zone Shipping zone object. + * @throws Exception If invalid data store. + */ + public function read( &$zone ) { + global $wpdb; + + // Zone 0 is used as a default if no other zones fit. + if ( 0 === $zone->get_id() || '0' === $zone->get_id() ) { + $this->read_zone_locations( $zone ); + $zone->set_zone_name( __( 'Locations not covered by your other zones', 'woocommerce' ) ); + $zone->read_meta_data(); + $zone->set_object_read( true ); + + /** + * Indicate that the WooCommerce shipping zone has been loaded. + * + * @param WC_Shipping_Zone $zone The shipping zone that has been loaded. + */ + do_action( 'woocommerce_shipping_zone_loaded', $zone ); + return; + } + + $zone_data = $wpdb->get_row( + $wpdb->prepare( + "SELECT zone_name, zone_order FROM {$wpdb->prefix}woocommerce_shipping_zones WHERE zone_id = %d LIMIT 1", + $zone->get_id() + ) + ); + + if ( ! $zone_data ) { + throw new Exception( __( 'Invalid data store.', 'woocommerce' ) ); + } + + $zone->set_zone_name( $zone_data->zone_name ); + $zone->set_zone_order( $zone_data->zone_order ); + $this->read_zone_locations( $zone ); + $zone->read_meta_data(); + $zone->set_object_read( true ); + + /** This action is documented in includes/datastores/class-wc-shipping-zone-data-store.php. */ + do_action( 'woocommerce_shipping_zone_loaded', $zone ); + } + + /** + * Deletes a shipping zone from the database. + * + * @since 3.0.0 + * @param WC_Shipping_Zone $zone Shipping zone object. + * @param array $args Array of args to pass to the delete method. + * @return void + */ + public function delete( &$zone, $args = array() ) { + $zone_id = $zone->get_id(); + + if ( $zone_id ) { + global $wpdb; + + // Delete methods and their settings. + $methods = $this->get_methods( $zone_id, false ); + + if ( $methods ) { + foreach ( $methods as $method ) { + $this->delete_method( $method->instance_id ); + } + } + + // Delete zone. + $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array( 'zone_id' => $zone_id ) ); + $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zones', array( 'zone_id' => $zone_id ) ); + + $zone->set_id( null ); + + WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' ); + WC_Cache_Helper::get_transient_version( 'shipping', true ); + + do_action( 'woocommerce_delete_shipping_zone', $zone_id ); + } + } + + /** + * Get a list of shipping methods for a specific zone. + * + * @since 3.0.0 + * @param int $zone_id Zone ID. + * @param bool $enabled_only True to request enabled methods only. + * @return array Array of objects containing method_id, method_order, instance_id, is_enabled + */ + public function get_methods( $zone_id, $enabled_only ) { + global $wpdb; + + if ( $enabled_only ) { + $raw_methods_sql = "SELECT method_id, method_order, instance_id, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d AND is_enabled = 1"; + } else { + $raw_methods_sql = "SELECT method_id, method_order, instance_id, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d"; + } + + return $wpdb->get_results( $wpdb->prepare( $raw_methods_sql, $zone_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + + /** + * Get count of methods for a zone. + * + * @since 3.0.0 + * @param int $zone_id Zone ID. + * @return int Method Count + */ + public function get_method_count( $zone_id ) { + global $wpdb; + return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d", $zone_id ) ); + } + + /** + * Add a shipping method to a zone. + * + * @since 3.0.0 + * @param int $zone_id Zone ID. + * @param string $type Method Type/ID. + * @param int $order Method Order. + * @return int Instance ID + */ + public function add_method( $zone_id, $type, $order ) { + global $wpdb; + $wpdb->insert( + $wpdb->prefix . 'woocommerce_shipping_zone_methods', + array( + 'method_id' => $type, + 'zone_id' => $zone_id, + 'method_order' => $order, + ), + array( + '%s', + '%d', + '%d', + ) + ); + return $wpdb->insert_id; + } + + /** + * Delete a method instance. + * + * @since 3.0.0 + * @param int $instance_id Instance ID. + */ + public function delete_method( $instance_id ) { + global $wpdb; + + $method = $this->get_method( $instance_id ); + + if ( ! $method ) { + return; + } + + delete_option( 'woocommerce_' . $method->method_id . '_' . $instance_id . '_settings' ); + + $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_methods', array( 'instance_id' => $instance_id ) ); + + do_action( 'woocommerce_delete_shipping_zone_method', $instance_id ); + } + + /** + * Get a shipping zone method instance. + * + * @since 3.0.0 + * @param int $instance_id Instance ID. + * @return object + */ + public function get_method( $instance_id ) { + global $wpdb; + return $wpdb->get_row( $wpdb->prepare( "SELECT zone_id, method_id, instance_id, method_order, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE instance_id = %d LIMIT 1;", $instance_id ) ); + } + + /** + * Find a matching zone ID for a given package. + * + * @since 3.0.0 + * @param object $package Package information. + * @return int + */ + public function get_zone_id_from_package( $package ) { + global $wpdb; + + $country = strtoupper( wc_clean( $package['destination']['country'] ) ); + $state = strtoupper( wc_clean( $package['destination']['state'] ) ); + $continent = strtoupper( wc_clean( WC()->countries->get_continent_code_for_country( $country ) ) ); + $postcode = wc_normalize_postcode( wc_clean( $package['destination']['postcode'] ) ); + + // Work out criteria for our zone search. + $criteria = array(); + $criteria[] = $wpdb->prepare( "( ( location_type = 'country' AND location_code = %s )", $country ); + $criteria[] = $wpdb->prepare( "OR ( location_type = 'state' AND location_code = %s )", $country . ':' . $state ); + $criteria[] = $wpdb->prepare( "OR ( location_type = 'continent' AND location_code = %s )", $continent ); + $criteria[] = 'OR ( location_type IS NULL ) )'; + + // Postcode range and wildcard matching. + $postcode_locations = $wpdb->get_results( "SELECT zone_id, location_code FROM {$wpdb->prefix}woocommerce_shipping_zone_locations WHERE location_type = 'postcode';" ); + + if ( $postcode_locations ) { + $zone_ids_with_postcode_rules = array_map( 'absint', wp_list_pluck( $postcode_locations, 'zone_id' ) ); + $matches = wc_postcode_location_matcher( $postcode, $postcode_locations, 'zone_id', 'location_code', $country ); + $do_not_match = array_unique( array_diff( $zone_ids_with_postcode_rules, array_keys( $matches ) ) ); + + if ( ! empty( $do_not_match ) ) { + $criteria[] = 'AND zones.zone_id NOT IN (' . implode( ',', $do_not_match ) . ')'; + } + } + + /** + * Get shipping zone criteria + * + * @since 3.6.6 + * @param array $criteria Get zone criteria. + * @param array $package Package information. + * @param array $postcode_locations Postcode range and wildcard matching. + */ + $criteria = apply_filters( 'woocommerce_get_zone_criteria', $criteria, $package, $postcode_locations ); + + // Get matching zones. + return $wpdb->get_var( + "SELECT zones.zone_id FROM {$wpdb->prefix}woocommerce_shipping_zones as zones + LEFT OUTER JOIN {$wpdb->prefix}woocommerce_shipping_zone_locations as locations ON zones.zone_id = locations.zone_id AND location_type != 'postcode' + WHERE " . implode( ' ', $criteria ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + . ' ORDER BY zone_order ASC, zones.zone_id ASC LIMIT 1' + ); + } + + /** + * Return an ordered list of zones. + * + * @since 3.0.0 + * @return array An array of objects containing a zone_id, zone_name, and zone_order. + */ + public function get_zones() { + global $wpdb; + return $wpdb->get_results( "SELECT zone_id, zone_name, zone_order FROM {$wpdb->prefix}woocommerce_shipping_zones order by zone_order ASC, zone_id ASC;" ); + } + + + /** + * Return a zone ID from an instance ID. + * + * @since 3.0.0 + * @param int $id Instance ID. + * @return int + */ + public function get_zone_id_by_instance_id( $id ) { + global $wpdb; + return $wpdb->get_var( $wpdb->prepare( "SELECT zone_id FROM {$wpdb->prefix}woocommerce_shipping_zone_methods as methods WHERE methods.instance_id = %d LIMIT 1;", $id ) ); + } + + /** + * Read location data from the database. + * + * @param WC_Shipping_Zone $zone Shipping zone object. + */ + private function read_zone_locations( &$zone ) { + global $wpdb; + + $locations = $wpdb->get_results( + $wpdb->prepare( + "SELECT location_code, location_type FROM {$wpdb->prefix}woocommerce_shipping_zone_locations WHERE zone_id = %d", + $zone->get_id() + ) + ); + + if ( $locations ) { + foreach ( $locations as $location ) { + $zone->add_location( $location->location_code, $location->location_type ); + } + } + } + + /** + * Save locations to the DB. + * This function clears old locations, then re-inserts new if any changes are found. + * + * @since 3.0.0 + * + * @param WC_Shipping_Zone $zone Shipping zone object. + * + * @return bool|void + */ + private function save_locations( &$zone ) { + $changed_props = array_keys( $zone->get_changes() ); + if ( ! in_array( 'zone_locations', $changed_props, true ) ) { + return false; + } + + global $wpdb; + $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array( 'zone_id' => $zone->get_id() ) ); + + foreach ( $zone->get_zone_locations( 'edit' ) as $location ) { + $wpdb->insert( + $wpdb->prefix . 'woocommerce_shipping_zone_locations', + array( + 'zone_id' => $zone->get_id(), + 'location_code' => $location->code, + 'location_type' => $location->type, + ) + ); + } + } +} diff --git a/plugins/woocommerce/includes/data-stores/class-wc-webhook-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-webhook-data-store.php new file mode 100644 index 00000000000..e8b417e3a8f --- /dev/null +++ b/plugins/woocommerce/includes/data-stores/class-wc-webhook-data-store.php @@ -0,0 +1,448 @@ +get_changes(); + if ( isset( $changes['date_created'] ) ) { + $date_created = $webhook->get_date_created()->date( 'Y-m-d H:i:s' ); + $date_created_gmt = gmdate( 'Y-m-d H:i:s', $webhook->get_date_created()->getTimestamp() ); + } else { + $date_created = current_time( 'mysql' ); + $date_created_gmt = current_time( 'mysql', 1 ); + $webhook->set_date_created( $date_created ); + } + + // Pending delivery by default if not set while creating a new webhook. + if ( ! isset( $changes['pending_delivery'] ) ) { + $webhook->set_pending_delivery( true ); + } + + $data = array( + 'status' => $webhook->get_status( 'edit' ), + 'name' => $webhook->get_name( 'edit' ), + 'user_id' => $webhook->get_user_id( 'edit' ), + 'delivery_url' => $webhook->get_delivery_url( 'edit' ), + 'secret' => $webhook->get_secret( 'edit' ), + 'topic' => $webhook->get_topic( 'edit' ), + 'date_created' => $date_created, + 'date_created_gmt' => $date_created_gmt, + 'api_version' => $this->get_api_version_number( $webhook->get_api_version( 'edit' ) ), + 'failure_count' => $webhook->get_failure_count( 'edit' ), + 'pending_delivery' => $webhook->get_pending_delivery( 'edit' ), + ); + + $wpdb->insert( $wpdb->prefix . 'wc_webhooks', $data ); // WPCS: DB call ok. + + $webhook_id = $wpdb->insert_id; + $webhook->set_id( $webhook_id ); + $webhook->apply_changes(); + + $this->delete_transients( $webhook->get_status( 'edit' ) ); + WC_Cache_Helper::invalidate_cache_group( 'webhooks' ); + do_action( 'woocommerce_new_webhook', $webhook_id, $webhook ); + } + + /** + * Read a webhook from the database. + * + * @since 3.3.0 + * @param WC_Webhook $webhook Webhook instance. + * @throws Exception When webhook is invalid. + */ + public function read( &$webhook ) { + global $wpdb; + + $data = wp_cache_get( $webhook->get_id(), 'webhooks' ); + + if ( false === $data ) { + $data = $wpdb->get_row( $wpdb->prepare( "SELECT webhook_id, status, name, user_id, delivery_url, secret, topic, date_created, date_modified, api_version, failure_count, pending_delivery FROM {$wpdb->prefix}wc_webhooks WHERE webhook_id = %d LIMIT 1;", $webhook->get_id() ), ARRAY_A ); // WPCS: cache ok, DB call ok. + + wp_cache_add( $webhook->get_id(), $data, 'webhooks' ); + } + + if ( is_array( $data ) ) { + $webhook->set_props( + array( + 'id' => $data['webhook_id'], + 'status' => $data['status'], + 'name' => $data['name'], + 'user_id' => $data['user_id'], + 'delivery_url' => $data['delivery_url'], + 'secret' => $data['secret'], + 'topic' => $data['topic'], + 'date_created' => '0000-00-00 00:00:00' === $data['date_created'] ? null : $data['date_created'], + 'date_modified' => '0000-00-00 00:00:00' === $data['date_modified'] ? null : $data['date_modified'], + 'api_version' => $data['api_version'], + 'failure_count' => $data['failure_count'], + 'pending_delivery' => $data['pending_delivery'], + ) + ); + $webhook->set_object_read( true ); + + do_action( 'woocommerce_webhook_loaded', $webhook ); + } else { + throw new Exception( __( 'Invalid webhook.', 'woocommerce' ) ); + } + } + + /** + * Update a webhook. + * + * @since 3.3.0 + * @param WC_Webhook $webhook Webhook instance. + */ + public function update( &$webhook ) { + global $wpdb; + + $changes = $webhook->get_changes(); + $trigger = isset( $changes['delivery_url'] ); + + if ( isset( $changes['date_modified'] ) ) { + $date_modified = $webhook->get_date_modified()->date( 'Y-m-d H:i:s' ); + $date_modified_gmt = gmdate( 'Y-m-d H:i:s', $webhook->get_date_modified()->getTimestamp() ); + } else { + $date_modified = current_time( 'mysql' ); + $date_modified_gmt = current_time( 'mysql', 1 ); + $webhook->set_date_modified( $date_modified ); + } + + $data = array( + 'status' => $webhook->get_status( 'edit' ), + 'name' => $webhook->get_name( 'edit' ), + 'user_id' => $webhook->get_user_id( 'edit' ), + 'delivery_url' => $webhook->get_delivery_url( 'edit' ), + 'secret' => $webhook->get_secret( 'edit' ), + 'topic' => $webhook->get_topic( 'edit' ), + 'date_modified' => $date_modified, + 'date_modified_gmt' => $date_modified_gmt, + 'api_version' => $this->get_api_version_number( $webhook->get_api_version( 'edit' ) ), + 'failure_count' => $webhook->get_failure_count( 'edit' ), + 'pending_delivery' => $webhook->get_pending_delivery( 'edit' ), + ); + + $wpdb->update( + $wpdb->prefix . 'wc_webhooks', + $data, + array( + 'webhook_id' => $webhook->get_id(), + ) + ); // WPCS: DB call ok. + + $webhook->apply_changes(); + + if ( isset( $changes['status'] ) ) { + // We need to delete all transients, because we can't be sure of the old status. + $this->delete_transients( 'all' ); + } + wp_cache_delete( $webhook->get_id(), 'webhooks' ); + WC_Cache_Helper::invalidate_cache_group( 'webhooks' ); + + if ( 'active' === $webhook->get_status() && ( $trigger || $webhook->get_pending_delivery() ) ) { + $webhook->deliver_ping(); + } + + do_action( 'woocommerce_webhook_updated', $webhook->get_id() ); + } + + /** + * Remove a webhook from the database. + * + * @since 3.3.0 + * @param WC_Webhook $webhook Webhook instance. + */ + public function delete( &$webhook ) { + global $wpdb; + + $wpdb->delete( + $wpdb->prefix . 'wc_webhooks', + array( + 'webhook_id' => $webhook->get_id(), + ), + array( '%d' ) + ); // WPCS: cache ok, DB call ok. + + $this->delete_transients( 'all' ); + wp_cache_delete( $webhook->get_id(), 'webhooks' ); + WC_Cache_Helper::invalidate_cache_group( 'webhooks' ); + do_action( 'woocommerce_webhook_deleted', $webhook->get_id(), $webhook ); + } + + /** + * Get API version number. + * + * @since 3.3.0 + * @param string $api_version REST API version. + * @return int + */ + public function get_api_version_number( $api_version ) { + return 'legacy_v3' === $api_version ? -1 : intval( substr( $api_version, -1 ) ); + } + + /** + * Get webhooks IDs from the database. + * + * @since 3.3.0 + * @throws InvalidArgumentException If a $status value is passed in that is not in the known wc_get_webhook_statuses() keys. + * @param string $status Optional - status to filter results by. Must be a key in return value of @see wc_get_webhook_statuses(). @since 3.6.0. + * @return int[] + */ + public function get_webhooks_ids( $status = '' ) { + if ( ! empty( $status ) ) { + $this->validate_status( $status ); + } + + $ids = get_transient( $this->get_transient_key( $status ) ); + + if ( false === $ids ) { + $ids = $this->search_webhooks( + array( + 'limit' => -1, + 'status' => $status, + ) + ); + $ids = array_map( 'absint', $ids ); + set_transient( $this->get_transient_key( $status ), $ids ); + } + + return $ids; + } + + /** + * Search webhooks. + * + * @param array $args Search arguments. + * @return array|object + */ + public function search_webhooks( $args ) { + global $wpdb; + + $args = wp_parse_args( + $args, + array( + 'limit' => 10, + 'offset' => 0, + 'order' => 'DESC', + 'orderby' => 'id', + 'paginate' => false, + ) + ); + + // Map post statuses. + $statuses = array( + 'publish' => 'active', + 'draft' => 'paused', + 'pending' => 'disabled', + ); + + // Map orderby to support a few post keys. + $orderby_mapping = array( + 'ID' => 'webhook_id', + 'id' => 'webhook_id', + 'name' => 'name', + 'title' => 'name', + 'post_title' => 'name', + 'post_name' => 'name', + 'date_created' => 'date_created_gmt', + 'date' => 'date_created_gmt', + 'post_date' => 'date_created_gmt', + 'date_modified' => 'date_modified_gmt', + 'modified' => 'date_modified_gmt', + 'post_modified' => 'date_modified_gmt', + ); + $orderby = isset( $orderby_mapping[ $args['orderby'] ] ) ? $orderby_mapping[ $args['orderby'] ] : 'webhook_id'; + $sort = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC'; + $order = "ORDER BY {$orderby} {$sort}"; + $limit = -1 < $args['limit'] ? $wpdb->prepare( 'LIMIT %d', $args['limit'] ) : ''; + $offset = 0 < $args['offset'] ? $wpdb->prepare( 'OFFSET %d', $args['offset'] ) : ''; + $status = ! empty( $args['status'] ) ? $wpdb->prepare( 'AND `status` = %s', isset( $statuses[ $args['status'] ] ) ? $statuses[ $args['status'] ] : $args['status'] ) : ''; + $search = ! empty( $args['search'] ) ? $wpdb->prepare( 'AND `name` LIKE %s', '%' . $wpdb->esc_like( sanitize_text_field( $args['search'] ) ) . '%' ) : ''; + $include = ''; + $exclude = ''; + $date_created = ''; + $date_modified = ''; + + if ( ! empty( $args['include'] ) ) { + $args['include'] = implode( ',', wp_parse_id_list( $args['include'] ) ); + $include = 'AND webhook_id IN (' . $args['include'] . ')'; + } + + if ( ! empty( $args['exclude'] ) ) { + $args['exclude'] = implode( ',', wp_parse_id_list( $args['exclude'] ) ); + $exclude = 'AND webhook_id NOT IN (' . $args['exclude'] . ')'; + } + + if ( ! empty( $args['after'] ) || ! empty( $args['before'] ) ) { + $args['after'] = empty( $args['after'] ) ? '0000-00-00' : $args['after']; + $args['before'] = empty( $args['before'] ) ? current_time( 'mysql', 1 ) : $args['before']; + + $date_created = "AND `date_created_gmt` BETWEEN STR_TO_DATE('" . esc_sql( $args['after'] ) . "', '%Y-%m-%d %H:%i:%s') and STR_TO_DATE('" . esc_sql( $args['before'] ) . "', '%Y-%m-%d %H:%i:%s')"; + } + + if ( ! empty( $args['modified_after'] ) || ! empty( $args['modified_before'] ) ) { + $args['modified_after'] = empty( $args['modified_after'] ) ? '0000-00-00' : $args['modified_after']; + $args['modified_before'] = empty( $args['modified_before'] ) ? current_time( 'mysql', 1 ) : $args['modified_before']; + + $date_modified = "AND `date_modified_gmt` BETWEEN STR_TO_DATE('" . esc_sql( $args['modified_after'] ) . "', '%Y-%m-%d %H:%i:%s') and STR_TO_DATE('" . esc_sql( $args['modified_before'] ) . "', '%Y-%m-%d %H:%i:%s')"; + } + + // Check for cache. + $cache_key = WC_Cache_Helper::get_cache_prefix( 'webhooks' ) . 'search_webhooks' . md5( implode( ',', $args ) ); + $cache_value = wp_cache_get( $cache_key, 'webhook_search_results' ); + + if ( $cache_value ) { + return $cache_value; + } + + if ( $args['paginate'] ) { + $query = trim( + "SELECT SQL_CALC_FOUND_ROWS webhook_id + FROM {$wpdb->prefix}wc_webhooks + WHERE 1=1 + {$status} + {$search} + {$include} + {$exclude} + {$date_created} + {$date_modified} + {$order} + {$limit} + {$offset}" + ); + + $webhook_ids = wp_parse_id_list( $wpdb->get_col( $query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $total = (int) $wpdb->get_var( 'SELECT FOUND_ROWS();' ); + $return_value = (object) array( + 'webhooks' => $webhook_ids, + 'total' => $total, + 'max_num_pages' => $args['limit'] > 1 ? ceil( $total / $args['limit'] ) : 1, + ); + } else { + $query = trim( + "SELECT webhook_id + FROM {$wpdb->prefix}wc_webhooks + WHERE 1=1 + {$status} + {$search} + {$include} + {$exclude} + {$date_created} + {$date_modified} + {$order} + {$limit} + {$offset}" + ); + + $webhook_ids = wp_parse_id_list( $wpdb->get_col( $query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $return_value = $webhook_ids; + } + + wp_cache_set( $cache_key, $return_value, 'webhook_search_results' ); + + return $return_value; + } + + /** + * Count webhooks. + * + * @since 3.6.0 + * @param string $status Status to count. + * @return int + */ + protected function get_webhook_count( $status = 'active' ) { + global $wpdb; + $cache_key = WC_Cache_Helper::get_cache_prefix( 'webhooks' ) . $status . '_count'; + $count = wp_cache_get( $cache_key, 'webhooks' ); + + if ( false === $count ) { + $count = absint( $wpdb->get_var( $wpdb->prepare( "SELECT count( webhook_id ) FROM {$wpdb->prefix}wc_webhooks WHERE `status` = %s;", $status ) ) ); + + wp_cache_add( $cache_key, $count, 'webhooks' ); + } + + return $count; + } + + /** + * Get total webhook counts by status. + * + * @return array + */ + public function get_count_webhooks_by_status() { + $statuses = array_keys( wc_get_webhook_statuses() ); + $counts = array(); + + foreach ( $statuses as $status ) { + $counts[ $status ] = $this->get_webhook_count( $status ); + } + + return $counts; + } + + /** + * Check if a given string is in known statuses, based on return value of @see wc_get_webhook_statuses(). + * + * @since 3.6.0 + * @throws InvalidArgumentException If $status is not empty and not in the known wc_get_webhook_statuses() keys. + * @param string $status Status to check. + */ + private function validate_status( $status ) { + if ( ! array_key_exists( $status, wc_get_webhook_statuses() ) ) { + throw new InvalidArgumentException( sprintf( 'Invalid status given: %s. Status must be one of: %s.', $status, implode( ', ', array_keys( wc_get_webhook_statuses() ) ) ) ); + } + } + + /** + * Get the transient key used to cache a set of webhook IDs, optionally filtered by status. + * + * @since 3.6.0 + * @param string $status Optional - status of cache key. + * @return string + */ + private function get_transient_key( $status = '' ) { + return empty( $status ) ? 'woocommerce_webhook_ids' : sprintf( 'woocommerce_webhook_ids_status_%s', $status ); + } + + /** + * Delete the transients used to cache a set of webhook IDs, optionally filtered by status. + * + * @since 3.6.0 + * @param string $status Optional - status of cache to delete, or 'all' to delete all caches. + */ + private function delete_transients( $status = '' ) { + + // Always delete the non-filtered cache. + delete_transient( $this->get_transient_key( '' ) ); + + if ( ! empty( $status ) ) { + if ( 'all' === $status ) { + foreach ( wc_get_webhook_statuses() as $status_key => $status_string ) { + delete_transient( $this->get_transient_key( $status_key ) ); + } + } else { + delete_transient( $this->get_transient_key( $status ) ); + } + } + } +} diff --git a/includes/emails/class-wc-email-cancelled-order.php b/plugins/woocommerce/includes/emails/class-wc-email-cancelled-order.php similarity index 100% rename from includes/emails/class-wc-email-cancelled-order.php rename to plugins/woocommerce/includes/emails/class-wc-email-cancelled-order.php diff --git a/includes/emails/class-wc-email-customer-completed-order.php b/plugins/woocommerce/includes/emails/class-wc-email-customer-completed-order.php similarity index 100% rename from includes/emails/class-wc-email-customer-completed-order.php rename to plugins/woocommerce/includes/emails/class-wc-email-customer-completed-order.php diff --git a/includes/emails/class-wc-email-customer-invoice.php b/plugins/woocommerce/includes/emails/class-wc-email-customer-invoice.php similarity index 100% rename from includes/emails/class-wc-email-customer-invoice.php rename to plugins/woocommerce/includes/emails/class-wc-email-customer-invoice.php diff --git a/plugins/woocommerce/includes/emails/class-wc-email-customer-new-account.php b/plugins/woocommerce/includes/emails/class-wc-email-customer-new-account.php new file mode 100644 index 00000000000..f660d88ebdf --- /dev/null +++ b/plugins/woocommerce/includes/emails/class-wc-email-customer-new-account.php @@ -0,0 +1,203 @@ +id = 'customer_new_account'; + $this->customer_email = true; + $this->title = __( 'New account', 'woocommerce' ); + $this->description = __( 'Customer "new account" emails are sent to the customer when a customer signs up via checkout or account pages.', 'woocommerce' ); + $this->template_html = 'emails/customer-new-account.php'; + $this->template_plain = 'emails/plain/customer-new-account.php'; + + // Call parent constructor. + parent::__construct(); + } + + /** + * Get email subject. + * + * @since 3.1.0 + * @return string + */ + public function get_default_subject() { + return __( 'Your {site_title} account has been created!', 'woocommerce' ); + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'Welcome to {site_title}', 'woocommerce' ); + } + + /** + * Trigger. + * + * @param int $user_id User ID. + * @param string $user_pass User password. + * @param bool $password_generated Whether the password was generated automatically or not. + */ + public function trigger( $user_id, $user_pass = '', $password_generated = false ) { + $this->setup_locale(); + + if ( $user_id ) { + $this->object = new WP_User( $user_id ); + + $this->user_pass = $user_pass; + $this->user_login = stripslashes( $this->object->user_login ); + $this->user_email = stripslashes( $this->object->user_email ); + $this->recipient = $this->user_email; + $this->password_generated = $password_generated; + $this->set_password_url = $this->generate_set_password_url(); + } + + if ( $this->is_enabled() && $this->get_recipient() ) { + $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + } + + $this->restore_locale(); + } + + /** + * Get content html. + * + * @return string + */ + public function get_content_html() { + return wc_get_template_html( + $this->template_html, + array( + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'user_login' => $this->user_login, + 'user_pass' => $this->user_pass, + 'blogname' => $this->get_blogname(), + 'password_generated' => $this->password_generated, + 'sent_to_admin' => false, + 'plain_text' => false, + 'email' => $this, + 'set_password_url' => $this->set_password_url, + ) + ); + } + + /** + * Get content plain. + * + * @return string + */ + public function get_content_plain() { + return wc_get_template_html( + $this->template_plain, + array( + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'user_login' => $this->user_login, + 'user_pass' => $this->user_pass, + 'blogname' => $this->get_blogname(), + 'password_generated' => $this->password_generated, + 'sent_to_admin' => false, + 'plain_text' => true, + 'email' => $this, + 'set_password_url' => $this->set_password_url, + ) + ); + } + + /** + * Default content to show below main email content. + * + * @since 3.7.0 + * @return string + */ + public function get_default_additional_content() { + return __( 'We look forward to seeing you soon.', 'woocommerce' ); + } + + /** + * Generate set password URL link for a new user. + * + * See also Automattic\WooCommerce\Blocks\Domain\Services\Email\CustomerNewAccount and wp_new_user_notification. + * + * @since 6.0.0 + * @return string + */ + protected function generate_set_password_url() { + // Generate a magic link so user can set initial password. + $key = get_password_reset_key( $this->object ); + if ( ! is_wp_error( $key ) ) { + $action = 'newaccount'; + return wc_get_account_endpoint_url( 'lost-password' ) . "?action=$action&key=$key&login=" . rawurlencode( $this->object->user_login ); + } else { + // Something went wrong while getting the key for new password URL, send customer to the generic password reset. + return wc_get_account_endpoint_url( 'lost-password' ); + } + } + } + +endif; + +return new WC_Email_Customer_New_Account(); diff --git a/includes/emails/class-wc-email-customer-note.php b/plugins/woocommerce/includes/emails/class-wc-email-customer-note.php similarity index 100% rename from includes/emails/class-wc-email-customer-note.php rename to plugins/woocommerce/includes/emails/class-wc-email-customer-note.php diff --git a/plugins/woocommerce/includes/emails/class-wc-email-customer-on-hold-order.php b/plugins/woocommerce/includes/emails/class-wc-email-customer-on-hold-order.php new file mode 100644 index 00000000000..8be3f17c09d --- /dev/null +++ b/plugins/woocommerce/includes/emails/class-wc-email-customer-on-hold-order.php @@ -0,0 +1,148 @@ +id = 'customer_on_hold_order'; + $this->customer_email = true; + $this->title = __( 'Order on-hold', 'woocommerce' ); + $this->description = __( 'This is an order notification sent to customers containing order details after an order is placed on-hold from Pending, Cancelled or Failed order status.', 'woocommerce' ); + $this->template_html = 'emails/customer-on-hold-order.php'; + $this->template_plain = 'emails/plain/customer-on-hold-order.php'; + $this->placeholders = array( + '{order_date}' => '', + '{order_number}' => '', + ); + + // Triggers for this email. + add_action( 'woocommerce_order_status_pending_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_failed_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_cancelled_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); + + // Call parent constructor. + parent::__construct(); + } + + /** + * Get email subject. + * + * @since 3.1.0 + * @return string + */ + public function get_default_subject() { + return __( 'Your {site_title} order has been received!', 'woocommerce' ); + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'Thank you for your order', 'woocommerce' ); + } + + /** + * Trigger the sending of this email. + * + * @param int $order_id The order ID. + * @param WC_Order|false $order Order object. + */ + public function trigger( $order_id, $order = false ) { + $this->setup_locale(); + + if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { + $order = wc_get_order( $order_id ); + } + + if ( is_a( $order, 'WC_Order' ) ) { + $this->object = $order; + $this->recipient = $this->object->get_billing_email(); + $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); + $this->placeholders['{order_number}'] = $this->object->get_order_number(); + } + + if ( $this->is_enabled() && $this->get_recipient() ) { + $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + } + + $this->restore_locale(); + } + + /** + * Get content html. + * + * @return string + */ + public function get_content_html() { + return wc_get_template_html( + $this->template_html, + array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => false, + 'plain_text' => false, + 'email' => $this, + ) + ); + } + + /** + * Get content plain. + * + * @return string + */ + public function get_content_plain() { + return wc_get_template_html( + $this->template_plain, + array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => false, + 'plain_text' => true, + 'email' => $this, + ) + ); + } + + /** + * Default content to show below main email content. + * + * @since 3.7.0 + * @return string + */ + public function get_default_additional_content() { + return __( 'We look forward to fulfilling your order soon.', 'woocommerce' ); + } + } + +endif; + +return new WC_Email_Customer_On_Hold_Order(); diff --git a/includes/emails/class-wc-email-customer-processing-order.php b/plugins/woocommerce/includes/emails/class-wc-email-customer-processing-order.php similarity index 100% rename from includes/emails/class-wc-email-customer-processing-order.php rename to plugins/woocommerce/includes/emails/class-wc-email-customer-processing-order.php diff --git a/includes/emails/class-wc-email-customer-refunded-order.php b/plugins/woocommerce/includes/emails/class-wc-email-customer-refunded-order.php similarity index 100% rename from includes/emails/class-wc-email-customer-refunded-order.php rename to plugins/woocommerce/includes/emails/class-wc-email-customer-refunded-order.php diff --git a/includes/emails/class-wc-email-customer-reset-password.php b/plugins/woocommerce/includes/emails/class-wc-email-customer-reset-password.php similarity index 100% rename from includes/emails/class-wc-email-customer-reset-password.php rename to plugins/woocommerce/includes/emails/class-wc-email-customer-reset-password.php diff --git a/includes/emails/class-wc-email-failed-order.php b/plugins/woocommerce/includes/emails/class-wc-email-failed-order.php similarity index 100% rename from includes/emails/class-wc-email-failed-order.php rename to plugins/woocommerce/includes/emails/class-wc-email-failed-order.php diff --git a/plugins/woocommerce/includes/emails/class-wc-email-new-order.php b/plugins/woocommerce/includes/emails/class-wc-email-new-order.php new file mode 100644 index 00000000000..f4b2f383d5f --- /dev/null +++ b/plugins/woocommerce/includes/emails/class-wc-email-new-order.php @@ -0,0 +1,229 @@ +id = 'new_order'; + $this->title = __( 'New order', 'woocommerce' ); + $this->description = __( 'New order emails are sent to chosen recipient(s) when a new order is received.', 'woocommerce' ); + $this->template_html = 'emails/admin-new-order.php'; + $this->template_plain = 'emails/plain/admin-new-order.php'; + $this->placeholders = array( + '{order_date}' => '', + '{order_number}' => '', + ); + + // Triggers for this email. + add_action( 'woocommerce_order_status_pending_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_pending_to_completed_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_pending_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_failed_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_failed_to_completed_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_failed_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_cancelled_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_cancelled_to_completed_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_cancelled_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); + + // Call parent constructor. + parent::__construct(); + + // Other settings. + $this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) ); + } + + /** + * Get email subject. + * + * @since 3.1.0 + * @return string + */ + public function get_default_subject() { + return __( '[{site_title}]: New order #{order_number}', 'woocommerce' ); + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'New Order: #{order_number}', 'woocommerce' ); + } + + /** + * Trigger the sending of this email. + * + * @param int $order_id The order ID. + * @param WC_Order|false $order Order object. + */ + public function trigger( $order_id, $order = false ) { + $this->setup_locale(); + + if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { + $order = wc_get_order( $order_id ); + } + + if ( is_a( $order, 'WC_Order' ) ) { + $this->object = $order; + $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); + $this->placeholders['{order_number}'] = $this->object->get_order_number(); + + $email_already_sent = $order->get_meta( '_new_order_email_sent' ); + } + + /** + * Controls if new order emails can be resend multiple times. + * + * @since 5.0.0 + * @param bool $allows Defaults to false. + */ + if ( 'true' === $email_already_sent && ! apply_filters( 'woocommerce_new_order_email_allows_resend', false ) ) { + return; + } + + if ( $this->is_enabled() && $this->get_recipient() ) { + $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + + $order->update_meta_data( '_new_order_email_sent', 'true' ); + $order->save(); + } + + $this->restore_locale(); + } + + /** + * Get content html. + * + * @return string + */ + public function get_content_html() { + return wc_get_template_html( + $this->template_html, + array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => true, + 'plain_text' => false, + 'email' => $this, + ) + ); + } + + /** + * Get content plain. + * + * @return string + */ + public function get_content_plain() { + return wc_get_template_html( + $this->template_plain, + array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => true, + 'plain_text' => true, + 'email' => $this, + ) + ); + } + + /** + * Default content to show below main email content. + * + * @since 3.7.0 + * @return string + */ + public function get_default_additional_content() { + return __( 'Congratulations on the sale.', 'woocommerce' ); + } + + /** + * Initialise settings form fields. + */ + public function init_form_fields() { + /* translators: %s: list of placeholders */ + $placeholder_text = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '' . implode( ', ', array_keys( $this->placeholders ) ) . '' ); + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable this email notification', 'woocommerce' ), + 'default' => 'yes', + ), + 'recipient' => array( + 'title' => __( 'Recipient(s)', 'woocommerce' ), + 'type' => 'text', + /* translators: %s: WP admin email */ + 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce' ), '' . esc_attr( get_option( 'admin_email' ) ) . '' ), + 'placeholder' => '', + 'default' => '', + 'desc_tip' => true, + ), + 'subject' => array( + 'title' => __( 'Subject', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_subject(), + 'default' => '', + ), + 'heading' => array( + 'title' => __( 'Email heading', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_heading(), + 'default' => '', + ), + 'additional_content' => array( + 'title' => __( 'Additional content', 'woocommerce' ), + 'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text, + 'css' => 'width:400px; height: 75px;', + 'placeholder' => __( 'N/A', 'woocommerce' ), + 'type' => 'textarea', + 'default' => $this->get_default_additional_content(), + 'desc_tip' => true, + ), + 'email_type' => array( + 'title' => __( 'Email type', 'woocommerce' ), + 'type' => 'select', + 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), + 'default' => 'html', + 'class' => 'email_type wc-enhanced-select', + 'options' => $this->get_email_type_options(), + 'desc_tip' => true, + ), + ); + } + } + +endif; + +return new WC_Email_New_Order(); diff --git a/plugins/woocommerce/includes/emails/class-wc-email.php b/plugins/woocommerce/includes/emails/class-wc-email.php new file mode 100644 index 00000000000..18bd88c02c9 --- /dev/null +++ b/plugins/woocommerce/includes/emails/class-wc-email.php @@ -0,0 +1,1098 @@ +', // Greater-than. + '<', // Less-than. + '&', // Ampersand. + '&', // Ampersand. + '(c)', // Copyright. + '(tm)', // Trademark. + '(R)', // Registered. + '--', // mdash. + '-', // ndash. + '*', // Bullet. + '£', // Pound sign. + 'EUR', // Euro sign. € ?. + '$', // Dollar sign. + '', // Unknown/unhandled entities. + ' ', // Runs of spaces, post-handling. + ); + + /** + * Strings to find/replace in subjects/headings. + * + * @var array + */ + protected $placeholders = array(); + + /** + * Strings to find in subjects/headings. + * + * @deprecated 3.2.0 in favour of placeholders + * @var array + */ + public $find = array(); + + /** + * Strings to replace in subjects/headings. + * + * @deprecated 3.2.0 in favour of placeholders + * @var array + */ + public $replace = array(); + + /** + * Constructor. + */ + public function __construct() { + // Find/replace. + $this->placeholders = array_merge( + array( + '{site_title}' => $this->get_blogname(), + '{site_address}' => wp_parse_url( home_url(), PHP_URL_HOST ), + '{site_url}' => wp_parse_url( home_url(), PHP_URL_HOST ), + ), + $this->placeholders + ); + + // Init settings. + $this->init_form_fields(); + $this->init_settings(); + + // Default template base if not declared in child constructor. + if ( is_null( $this->template_base ) ) { + $this->template_base = WC()->plugin_path() . '/templates/'; + } + + $this->email_type = $this->get_option( 'email_type' ); + $this->enabled = $this->get_option( 'enabled' ); + + add_action( 'phpmailer_init', array( $this, 'handle_multipart' ) ); + add_action( 'woocommerce_update_options_email_' . $this->id, array( $this, 'process_admin_options' ) ); + } + + /** + * Handle multipart mail. + * + * @param PHPMailer $mailer PHPMailer object. + * @return PHPMailer + */ + public function handle_multipart( $mailer ) { + if ( $this->sending && 'multipart' === $this->get_email_type() ) { + $mailer->AltBody = wordwrap( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + preg_replace( $this->plain_search, $this->plain_replace, wp_strip_all_tags( $this->get_content_plain() ) ) + ); + $this->sending = false; + } + return $mailer; + } + + /** + * Format email string. + * + * @param mixed $string Text to replace placeholders in. + * @return string + */ + public function format_string( $string ) { + $find = array_keys( $this->placeholders ); + $replace = array_values( $this->placeholders ); + + // If using legacy find replace, add those to our find/replace arrays first. @todo deprecate in 4.0.0. + $find = array_merge( (array) $this->find, $find ); + $replace = array_merge( (array) $this->replace, $replace ); + + // Take care of blogname which is no longer defined as a valid placeholder. + $find[] = '{blogname}'; + $replace[] = $this->get_blogname(); + + // If using the older style filters for find and replace, ensure the array is associative and then pass through filters. @todo deprecate in 4.0.0. + if ( has_filter( 'woocommerce_email_format_string_replace' ) || has_filter( 'woocommerce_email_format_string_find' ) ) { + $legacy_find = $this->find; + $legacy_replace = $this->replace; + + foreach ( $this->placeholders as $find => $replace ) { + $legacy_key = sanitize_title( str_replace( '_', '-', trim( $find, '{}' ) ) ); + $legacy_find[ $legacy_key ] = $find; + $legacy_replace[ $legacy_key ] = $replace; + } + + $string = str_replace( apply_filters( 'woocommerce_email_format_string_find', $legacy_find, $this ), apply_filters( 'woocommerce_email_format_string_replace', $legacy_replace, $this ), $string ); + } + + /** + * Filter for main find/replace. + * + * @since 3.2.0 + */ + return apply_filters( 'woocommerce_email_format_string', str_replace( $find, $replace, $string ), $this ); + } + + /** + * Set the locale to the store locale for customer emails to make sure emails are in the store language. + */ + public function setup_locale() { + if ( $this->is_customer_email() && apply_filters( 'woocommerce_email_setup_locale', true ) ) { + wc_switch_to_site_locale(); + } + } + + /** + * Restore the locale to the default locale. Use after finished with setup_locale. + */ + public function restore_locale() { + if ( $this->is_customer_email() && apply_filters( 'woocommerce_email_restore_locale', true ) ) { + wc_restore_locale(); + } + } + + /** + * Get email subject. + * + * @since 3.1.0 + * @return string + */ + public function get_default_subject() { + return $this->subject; + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return $this->heading; + } + + /** + * Default content to show below main email content. + * + * @since 3.7.0 + * @return string + */ + public function get_default_additional_content() { + return ''; + } + + /** + * Return content from the additional_content field. + * + * Displayed above the footer. + * + * @since 3.7.0 + * @return string + */ + public function get_additional_content() { + $content = $this->get_option( 'additional_content', '' ); + + return apply_filters( 'woocommerce_email_additional_content_' . $this->id, $this->format_string( $content ), $this->object, $this ); + } + + /** + * Get email subject. + * + * @return string + */ + public function get_subject() { + return apply_filters( 'woocommerce_email_subject_' . $this->id, $this->format_string( $this->get_option( 'subject', $this->get_default_subject() ) ), $this->object, $this ); + } + + /** + * Get email heading. + * + * @return string + */ + public function get_heading() { + return apply_filters( 'woocommerce_email_heading_' . $this->id, $this->format_string( $this->get_option( 'heading', $this->get_default_heading() ) ), $this->object, $this ); + } + + /** + * Get valid recipients. + * + * @return string + */ + public function get_recipient() { + $recipient = apply_filters( 'woocommerce_email_recipient_' . $this->id, $this->recipient, $this->object, $this ); + $recipients = array_map( 'trim', explode( ',', $recipient ) ); + $recipients = array_filter( $recipients, 'is_email' ); + return implode( ', ', $recipients ); + } + + /** + * Get email headers. + * + * @return string + */ + public function get_headers() { + $header = 'Content-Type: ' . $this->get_content_type() . "\r\n"; + + if ( in_array( $this->id, array( 'new_order', 'cancelled_order', 'failed_order' ), true ) ) { + if ( $this->object && $this->object->get_billing_email() && ( $this->object->get_billing_first_name() || $this->object->get_billing_last_name() ) ) { + $header .= 'Reply-to: ' . $this->object->get_billing_first_name() . ' ' . $this->object->get_billing_last_name() . ' <' . $this->object->get_billing_email() . ">\r\n"; + } + } elseif ( $this->get_from_address() && $this->get_from_name() ) { + $header .= 'Reply-to: ' . $this->get_from_name() . ' <' . $this->get_from_address() . ">\r\n"; + } + + return apply_filters( 'woocommerce_email_headers', $header, $this->id, $this->object, $this ); + } + + /** + * Get email attachments. + * + * @return array + */ + public function get_attachments() { + return apply_filters( 'woocommerce_email_attachments', array(), $this->id, $this->object, $this ); + } + + /** + * Return email type. + * + * @return string + */ + public function get_email_type() { + return $this->email_type && class_exists( 'DOMDocument' ) ? $this->email_type : 'plain'; + } + + /** + * Get email content type. + * + * @param string $default_content_type Default wp_mail() content type. + * @return string + */ + public function get_content_type( $default_content_type = '' ) { + switch ( $this->get_email_type() ) { + case 'html': + $content_type = 'text/html'; + break; + case 'multipart': + $content_type = 'multipart/alternative'; + break; + default: + $content_type = 'text/plain'; + break; + } + + return apply_filters( 'woocommerce_email_content_type', $content_type, $this, $default_content_type ); + } + + /** + * Return the email's title + * + * @return string + */ + public function get_title() { + return apply_filters( 'woocommerce_email_title', $this->title, $this ); + } + + /** + * Return the email's description + * + * @return string + */ + public function get_description() { + return apply_filters( 'woocommerce_email_description', $this->description, $this ); + } + + /** + * Proxy to parent's get_option and attempt to localize the result using gettext. + * + * @param string $key Option key. + * @param mixed $empty_value Value to use when option is empty. + * @return string + */ + public function get_option( $key, $empty_value = null ) { + $value = parent::get_option( $key, $empty_value ); + return apply_filters( 'woocommerce_email_get_option', $value, $this, $value, $key, $empty_value ); + } + + /** + * Checks if this email is enabled and will be sent. + * + * @return bool + */ + public function is_enabled() { + return apply_filters( 'woocommerce_email_enabled_' . $this->id, 'yes' === $this->enabled, $this->object, $this ); + } + + /** + * Checks if this email is manually sent + * + * @return bool + */ + public function is_manual() { + return $this->manual; + } + + /** + * Checks if this email is customer focussed. + * + * @return bool + */ + public function is_customer_email() { + return $this->customer_email; + } + + /** + * Get WordPress blog name. + * + * @return string + */ + public function get_blogname() { + return wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); + } + + /** + * Get email content. + * + * @return string + */ + public function get_content() { + $this->sending = true; + + if ( 'plain' === $this->get_email_type() ) { + $email_content = wordwrap( preg_replace( $this->plain_search, $this->plain_replace, wp_strip_all_tags( $this->get_content_plain() ) ), 70 ); + } else { + $email_content = $this->get_content_html(); + } + + return $email_content; + } + + /** + * Apply inline styles to dynamic content. + * + * We only inline CSS for html emails, and to do so we use Emogrifier library (if supported). + * + * @version 4.0.0 + * @param string|null $content Content that will receive inline styles. + * @return string + */ + public function style_inline( $content ) { + if ( in_array( $this->get_content_type(), array( 'text/html', 'multipart/alternative' ), true ) ) { + ob_start(); + wc_get_template( 'emails/email-styles.php' ); + $css = apply_filters( 'woocommerce_email_styles', ob_get_clean(), $this ); + + $css_inliner_class = CssInliner::class; + + if ( $this->supports_emogrifier() && class_exists( $css_inliner_class ) ) { + try { + $css_inliner = CssInliner::fromHtml( $content )->inlineCss( $css ); + + do_action( 'woocommerce_emogrifier', $css_inliner, $this ); + + $dom_document = $css_inliner->getDomDocument(); + + HtmlPruner::fromDomDocument( $dom_document )->removeElementsWithDisplayNone(); + $content = CssToAttributeConverter::fromDomDocument( $dom_document ) + ->convertCssToVisualAttributes() + ->render(); + } catch ( Exception $e ) { + $logger = wc_get_logger(); + $logger->error( $e->getMessage(), array( 'source' => 'emogrifier' ) ); + } + } else { + $content = '' . $content; + } + } + + return $content; + } + + /** + * Return if emogrifier library is supported. + * + * @version 4.0.0 + * @since 3.5.0 + * @return bool + */ + protected function supports_emogrifier() { + return class_exists( 'DOMDocument' ); + } + + /** + * Get the email content in plain text format. + * + * @return string + */ + public function get_content_plain() { + return ''; + } + + /** + * Get the email content in HTML format. + * + * @return string + */ + public function get_content_html() { + return ''; + } + + /** + * Get the from name for outgoing emails. + * + * @param string $from_name Default wp_mail() name associated with the "from" email address. + * @return string + */ + public function get_from_name( $from_name = '' ) { + $from_name = apply_filters( 'woocommerce_email_from_name', get_option( 'woocommerce_email_from_name' ), $this, $from_name ); + return wp_specialchars_decode( esc_html( $from_name ), ENT_QUOTES ); + } + + /** + * Get the from address for outgoing emails. + * + * @param string $from_email Default wp_mail() email address to send from. + * @return string + */ + public function get_from_address( $from_email = '' ) { + $from_email = apply_filters( 'woocommerce_email_from_address', get_option( 'woocommerce_email_from_address' ), $this, $from_email ); + return sanitize_email( $from_email ); + } + + /** + * Send an email. + * + * @param string $to Email to. + * @param string $subject Email subject. + * @param string $message Email message. + * @param string $headers Email headers. + * @param array $attachments Email attachments. + * @return bool success + */ + public function send( $to, $subject, $message, $headers, $attachments ) { + add_filter( 'wp_mail_from', array( $this, 'get_from_address' ) ); + add_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) ); + add_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) ); + + $message = apply_filters( 'woocommerce_mail_content', $this->style_inline( $message ) ); + $mail_callback = apply_filters( 'woocommerce_mail_callback', 'wp_mail', $this ); + $mail_callback_params = apply_filters( 'woocommerce_mail_callback_params', array( $to, wp_specialchars_decode( $subject ), $message, $headers, $attachments ), $this ); + $return = $mail_callback( ...$mail_callback_params ); + + remove_filter( 'wp_mail_from', array( $this, 'get_from_address' ) ); + remove_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) ); + remove_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) ); + + /** + * Action hook fired when an email is sent. + * + * @since 5.6.0 + * @param bool $return Whether the email was sent successfully. + * @param int $id Email ID. + * @param WC_Email $this WC_Email instance. + */ + do_action( 'woocommerce_email_sent', $return, $this->id, $this ); + + return $return; + } + + /** + * Initialise Settings Form Fields - these are generic email options most will use. + */ + public function init_form_fields() { + /* translators: %s: list of placeholders */ + $placeholder_text = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '' . esc_html( implode( ', ', array_keys( $this->placeholders ) ) ) . '' ); + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable this email notification', 'woocommerce' ), + 'default' => 'yes', + ), + 'subject' => array( + 'title' => __( 'Subject', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_subject(), + 'default' => '', + ), + 'heading' => array( + 'title' => __( 'Email heading', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_heading(), + 'default' => '', + ), + 'additional_content' => array( + 'title' => __( 'Additional content', 'woocommerce' ), + 'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text, + 'css' => 'width:400px; height: 75px;', + 'placeholder' => __( 'N/A', 'woocommerce' ), + 'type' => 'textarea', + 'default' => $this->get_default_additional_content(), + 'desc_tip' => true, + ), + 'email_type' => array( + 'title' => __( 'Email type', 'woocommerce' ), + 'type' => 'select', + 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), + 'default' => 'html', + 'class' => 'email_type wc-enhanced-select', + 'options' => $this->get_email_type_options(), + 'desc_tip' => true, + ), + ); + } + + /** + * Email type options. + * + * @return array + */ + public function get_email_type_options() { + $types = array( 'plain' => __( 'Plain text', 'woocommerce' ) ); + + if ( class_exists( 'DOMDocument' ) ) { + $types['html'] = __( 'HTML', 'woocommerce' ); + $types['multipart'] = __( 'Multipart', 'woocommerce' ); + } + + return $types; + } + + /** + * Admin Panel Options Processing. + */ + public function process_admin_options() { + // Save regular options. + parent::process_admin_options(); + + $post_data = $this->get_post_data(); + + // Save templates. + if ( isset( $post_data['template_html_code'] ) ) { + $this->save_template( $post_data['template_html_code'], $this->template_html ); + } + if ( isset( $post_data['template_plain_code'] ) ) { + $this->save_template( $post_data['template_plain_code'], $this->template_plain ); + } + } + + /** + * Get template. + * + * @param string $type Template type. Can be either 'template_html' or 'template_plain'. + * @return string + */ + public function get_template( $type ) { + $type = basename( $type ); + + if ( 'template_html' === $type ) { + return $this->template_html; + } elseif ( 'template_plain' === $type ) { + return $this->template_plain; + } + return ''; + } + + /** + * Save the email templates. + * + * @since 2.4.0 + * @param string $template_code Template code. + * @param string $template_path Template path. + */ + protected function save_template( $template_code, $template_path ) { + if ( current_user_can( 'edit_themes' ) && ! empty( $template_code ) && ! empty( $template_path ) ) { + $saved = false; + $file = $this->get_theme_template_file( $template_path ); + $code = wp_unslash( $template_code ); + + if ( is_writeable( $file ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writeable + $f = fopen( $file, 'w+' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen + + if ( false !== $f ) { + fwrite( $f, $code ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite + fclose( $f ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose + $saved = true; + } + } + + if ( ! $saved ) { + $redirect = add_query_arg( 'wc_error', rawurlencode( __( 'Could not write to template file.', 'woocommerce' ) ) ); + wp_safe_redirect( $redirect ); + exit; + } + } + } + + /** + * Get the template file in the current theme. + * + * @param string $template Template name. + * + * @return string + */ + public function get_theme_template_file( $template ) { + return get_stylesheet_directory() . '/' . apply_filters( 'woocommerce_template_directory', 'woocommerce', $template ) . '/' . $template; + } + + /** + * Move template action. + * + * @param string $template_type Template type. + */ + protected function move_template_action( $template_type ) { + $template = $this->get_template( $template_type ); + if ( ! empty( $template ) ) { + $theme_file = $this->get_theme_template_file( $template ); + + if ( wp_mkdir_p( dirname( $theme_file ) ) && ! file_exists( $theme_file ) ) { + + // Locate template file. + $core_file = $this->template_base . $template; + $template_file = apply_filters( 'woocommerce_locate_core_template', $core_file, $template, $this->template_base, $this->id ); + + // Copy template file. + copy( $template_file, $theme_file ); + + /** + * Action hook fired after copying email template file. + * + * @param string $template_type The copied template type + * @param string $email The email object + */ + do_action( 'woocommerce_copy_email_template', $template_type, $this ); + + ?> +
    +

    +
    + get_template( $template_type ); + + if ( $template ) { + if ( ! empty( $template ) ) { + $theme_file = $this->get_theme_template_file( $template ); + + if ( file_exists( $theme_file ) ) { + unlink( $theme_file ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink + + /** + * Action hook fired after deleting template file. + * + * @param string $template The deleted template type + * @param string $email The email object + */ + do_action( 'woocommerce_delete_email_template', $template_type, $this ); + ?> +
    +

    +
    + template_html ) || ! empty( $this->template_plain ) ) + && ( ! empty( $_GET['move_template'] ) || ! empty( $_GET['delete_template'] ) ) + && 'GET' === $_SERVER['REQUEST_METHOD'] // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated + ) { + if ( empty( $_GET['_wc_email_nonce'] ) || ! wp_verify_nonce( wc_clean( wp_unslash( $_GET['_wc_email_nonce'] ) ), 'woocommerce_email_template_nonce' ) ) { + wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); + } + + if ( ! current_user_can( 'edit_themes' ) ) { + wp_die( esc_html__( 'You don’t have permission to do this.', 'woocommerce' ) ); + } + + if ( ! empty( $_GET['move_template'] ) ) { + $this->move_template_action( wc_clean( wp_unslash( $_GET['move_template'] ) ) ); + } + + if ( ! empty( $_GET['delete_template'] ) ) { + $this->delete_template_action( wc_clean( wp_unslash( $_GET['delete_template'] ) ) ); + } + } + } + + /** + * Admin Options. + * + * Setup the email settings screen. + * Override this in your email. + * + * @since 1.0.0 + */ + public function admin_options() { + // Do admin actions. + $this->admin_actions(); + ?> +

    get_title() ); ?>

    + + get_description() ) ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?> + + + + + generate_settings_html(); ?> +
    + + + + template_html ) || ! empty( $this->template_plain ) ) ) { + ?> +
    + __( 'HTML template', 'woocommerce' ), + 'template_plain' => __( 'Plain text template', 'woocommerce' ), + ); + + foreach ( $templates as $template_type => $title ) : + $template = $this->get_template( $template_type ); + + if ( empty( $template ) ) { + continue; + } + + $local_file = $this->get_theme_template_file( $template ); + $core_file = $this->template_base . $template; + $template_file = apply_filters( 'woocommerce_locate_core_template', $core_file, $template, $this->template_base, $this->id ); + $template_dir = apply_filters( 'woocommerce_template_directory', 'woocommerce', $template ); + ?> +
    +

    + + +

    + + + + + + + + + ' . esc_html( trailingslashit( basename( get_stylesheet_directory() ) ) . $template_dir . '/' . $template ) . '' ); + ?> +

    + + + +

    + + + + + + + + + ' . esc_html( plugin_basename( $template_file ) ) . '', '' . esc_html( trailingslashit( basename( get_stylesheet_directory() ) ) . $template_dir . '/' . $template ) . '' ); + ?> +

    + + + +

    + +
    + +
    + + column_names = $this->get_default_column_names(); + } + + /** + * Get file path to export to. + * + * @return string + */ + protected function get_file_path() { + $upload_dir = wp_upload_dir(); + return trailingslashit( $upload_dir['basedir'] ) . $this->get_filename(); + } + + /** + * Get CSV headers row file path to export to. + * + * @return string + */ + protected function get_headers_row_file_path() { + return $this->get_file_path() . '.headers'; + } + + /** + * Get the contents of the CSV headers row file. Defaults to the original known headers. + * + * @since 3.1.0 + * @return string + */ + public function get_headers_row_file() { + + $file = chr( 239 ) . chr( 187 ) . chr( 191 ) . $this->export_column_headers(); + + if ( @file_exists( $this->get_headers_row_file_path() ) ) { // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged + $file = @file_get_contents( $this->get_headers_row_file_path() ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.WP.AlternativeFunctions.file_system_read_file_get_contents + } + + return $file; + } + + /** + * Get the file contents. + * + * @since 3.1.0 + * @return string + */ + public function get_file() { + $file = ''; + if ( @file_exists( $this->get_file_path() ) ) { // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged + $file = @file_get_contents( $this->get_file_path() ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.WP.AlternativeFunctions.file_system_read_file_get_contents + } else { + @file_put_contents( $this->get_file_path(), '' ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_file_put_contents, Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents + @chmod( $this->get_file_path(), 0664 ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.chmod_chmod, WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents, Generic.PHP.NoSilencedErrors.Discouraged + } + return $file; + } + + /** + * Serve the file and remove once sent to the client. + * + * @since 3.1.0 + */ + public function export() { + $this->send_headers(); + $this->send_content( $this->get_headers_row_file() . $this->get_file() ); + @unlink( $this->get_file_path() ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink, Generic.PHP.NoSilencedErrors.Discouraged + @unlink( $this->get_headers_row_file_path() ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink, Generic.PHP.NoSilencedErrors.Discouraged + die(); + } + + /** + * Generate the CSV file. + * + * @since 3.1.0 + */ + public function generate_file() { + if ( 1 === $this->get_page() ) { + @unlink( $this->get_file_path() ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink, Generic.PHP.NoSilencedErrors.Discouraged, + + // We need to initialize the file here. + $this->get_file(); + } + $this->prepare_data_to_export(); + $this->write_csv_data( $this->get_csv_data() ); + } + + /** + * Write data to the file. + * + * @since 3.1.0 + * @param string $data Data. + */ + protected function write_csv_data( $data ) { + + if ( ! file_exists( $this->get_file_path() ) || ! is_writeable( $this->get_file_path() ) ) { + return false; + } + + $fp = fopen( $this->get_file_path(), 'a+' ); + + if ( $fp ) { + fwrite( $fp, $data ); + fclose( $fp ); + } + + // Add all columns when finished. + if ( 100 === $this->get_percent_complete() ) { + $header = chr( 239 ) . chr( 187 ) . chr( 191 ) . $this->export_column_headers(); + + // We need to use a temporary file to store headers, this will make our life so much easier. + @file_put_contents( $this->get_headers_row_file_path(), $header ); //phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_file_put_contents, Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents + } + + } + + /** + * Get page. + * + * @since 3.1.0 + * @return int + */ + public function get_page() { + return $this->page; + } + + /** + * Set page. + * + * @since 3.1.0 + * @param int $page Page Nr. + */ + public function set_page( $page ) { + $this->page = absint( $page ); + } + + /** + * Get count of records exported. + * + * @since 3.1.0 + * @return int + */ + public function get_total_exported() { + return ( ( $this->get_page() - 1 ) * $this->get_limit() ) + $this->exported_row_count; + } + + /** + * Get total % complete. + * + * @since 3.1.0 + * @return int + */ + public function get_percent_complete() { + return $this->total_rows ? (int) floor( ( $this->get_total_exported() / $this->total_rows ) * 100 ) : 100; + } +} diff --git a/includes/export/abstract-wc-csv-exporter.php b/plugins/woocommerce/includes/export/abstract-wc-csv-exporter.php similarity index 100% rename from includes/export/abstract-wc-csv-exporter.php rename to plugins/woocommerce/includes/export/abstract-wc-csv-exporter.php diff --git a/plugins/woocommerce/includes/export/class-wc-product-csv-exporter.php b/plugins/woocommerce/includes/export/class-wc-product-csv-exporter.php new file mode 100644 index 00000000000..0be4abb9c4f --- /dev/null +++ b/plugins/woocommerce/includes/export/class-wc-product-csv-exporter.php @@ -0,0 +1,733 @@ +set_product_types_to_export( array_keys( WC_Admin_Exporters::get_product_types() ) ); + } + + /** + * Should meta be exported? + * + * @param bool $enable_meta_export Should meta be exported. + * + * @since 3.1.0 + */ + public function enable_meta_export( $enable_meta_export ) { + $this->enable_meta_export = (bool) $enable_meta_export; + } + + /** + * Product types to export. + * + * @param array $product_types_to_export List of types to export. + * + * @since 3.1.0 + */ + public function set_product_types_to_export( $product_types_to_export ) { + $this->product_types_to_export = array_map( 'wc_clean', $product_types_to_export ); + } + + /** + * Product category to export + * + * @param string $product_category_to_export Product category slug to export, empty string exports all. + * + * @since 3.5.0 + * @return void + */ + public function set_product_category_to_export( $product_category_to_export ) { + $this->product_category_to_export = array_map( 'sanitize_title_with_dashes', $product_category_to_export ); + } + + /** + * Return an array of columns to export. + * + * @since 3.1.0 + * @return array + */ + public function get_default_column_names() { + return apply_filters( + "woocommerce_product_export_{$this->export_type}_default_columns", + array( + 'id' => __( 'ID', 'woocommerce' ), + 'type' => __( 'Type', 'woocommerce' ), + 'sku' => __( 'SKU', 'woocommerce' ), + 'name' => __( 'Name', 'woocommerce' ), + 'published' => __( 'Published', 'woocommerce' ), + 'featured' => __( 'Is featured?', 'woocommerce' ), + 'catalog_visibility' => __( 'Visibility in catalog', 'woocommerce' ), + 'short_description' => __( 'Short description', 'woocommerce' ), + 'description' => __( 'Description', 'woocommerce' ), + 'date_on_sale_from' => __( 'Date sale price starts', 'woocommerce' ), + 'date_on_sale_to' => __( 'Date sale price ends', 'woocommerce' ), + 'tax_status' => __( 'Tax status', 'woocommerce' ), + 'tax_class' => __( 'Tax class', 'woocommerce' ), + 'stock_status' => __( 'In stock?', 'woocommerce' ), + 'stock' => __( 'Stock', 'woocommerce' ), + 'low_stock_amount' => __( 'Low stock amount', 'woocommerce' ), + 'backorders' => __( 'Backorders allowed?', 'woocommerce' ), + 'sold_individually' => __( 'Sold individually?', 'woocommerce' ), + /* translators: %s: weight */ + 'weight' => sprintf( __( 'Weight (%s)', 'woocommerce' ), get_option( 'woocommerce_weight_unit' ) ), + /* translators: %s: length */ + 'length' => sprintf( __( 'Length (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + /* translators: %s: width */ + 'width' => sprintf( __( 'Width (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + /* translators: %s: Height */ + 'height' => sprintf( __( 'Height (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'reviews_allowed' => __( 'Allow customer reviews?', 'woocommerce' ), + 'purchase_note' => __( 'Purchase note', 'woocommerce' ), + 'sale_price' => __( 'Sale price', 'woocommerce' ), + 'regular_price' => __( 'Regular price', 'woocommerce' ), + 'category_ids' => __( 'Categories', 'woocommerce' ), + 'tag_ids' => __( 'Tags', 'woocommerce' ), + 'shipping_class_id' => __( 'Shipping class', 'woocommerce' ), + 'images' => __( 'Images', 'woocommerce' ), + 'download_limit' => __( 'Download limit', 'woocommerce' ), + 'download_expiry' => __( 'Download expiry days', 'woocommerce' ), + 'parent_id' => __( 'Parent', 'woocommerce' ), + 'grouped_products' => __( 'Grouped products', 'woocommerce' ), + 'upsell_ids' => __( 'Upsells', 'woocommerce' ), + 'cross_sell_ids' => __( 'Cross-sells', 'woocommerce' ), + 'product_url' => __( 'External URL', 'woocommerce' ), + 'button_text' => __( 'Button text', 'woocommerce' ), + 'menu_order' => __( 'Position', 'woocommerce' ), + ) + ); + } + + /** + * Prepare data for export. + * + * @since 3.1.0 + */ + public function prepare_data_to_export() { + $args = array( + 'status' => array( 'private', 'publish', 'draft', 'future', 'pending' ), + 'type' => $this->product_types_to_export, + 'limit' => $this->get_limit(), + 'page' => $this->get_page(), + 'orderby' => array( + 'ID' => 'ASC', + ), + 'return' => 'objects', + 'paginate' => true, + ); + + if ( ! empty( $this->product_category_to_export ) ) { + $args['category'] = $this->product_category_to_export; + } + $products = wc_get_products( apply_filters( "woocommerce_product_export_{$this->export_type}_query_args", $args ) ); + + $this->total_rows = $products->total; + $this->row_data = array(); + $variable_products = array(); + + foreach ( $products->products as $product ) { + // Check if the category is set, this means we need to fetch variations seperately as they are not tied to a category. + if ( ! empty( $args['category'] ) && $product->is_type( 'variable' ) ) { + $variable_products[] = $product->get_id(); + } + + $this->row_data[] = $this->generate_row_data( $product ); + } + + // If a category was selected we loop through the variations as they are not tied to a category so will be excluded by default. + if ( ! empty( $variable_products ) ) { + foreach ( $variable_products as $parent_id ) { + $products = wc_get_products( + array( + 'parent' => $parent_id, + 'type' => array( 'variation' ), + 'return' => 'objects', + 'limit' => -1, + ) + ); + + if ( ! $products ) { + continue; + } + + foreach ( $products as $product ) { + $this->row_data[] = $this->generate_row_data( $product ); + } + } + } + } + + /** + * Take a product and generate row data from it for export. + * + * @param WC_Product $product WC_Product object. + * + * @return array + */ + protected function generate_row_data( $product ) { + $columns = $this->get_column_names(); + $row = array(); + foreach ( $columns as $column_id => $column_name ) { + $column_id = strstr( $column_id, ':' ) ? current( explode( ':', $column_id ) ) : $column_id; + $value = ''; + + // Skip some columns if dynamically handled later or if we're being selective. + if ( in_array( $column_id, array( 'downloads', 'attributes', 'meta' ), true ) || ! $this->is_column_exporting( $column_id ) ) { + continue; + } + + if ( has_filter( "woocommerce_product_export_{$this->export_type}_column_{$column_id}" ) ) { + // Filter for 3rd parties. + $value = apply_filters( "woocommerce_product_export_{$this->export_type}_column_{$column_id}", '', $product, $column_id ); + + } elseif ( is_callable( array( $this, "get_column_value_{$column_id}" ) ) ) { + // Handle special columns which don't map 1:1 to product data. + $value = $this->{"get_column_value_{$column_id}"}( $product ); + + } elseif ( is_callable( array( $product, "get_{$column_id}" ) ) ) { + // Default and custom handling. + $value = $product->{"get_{$column_id}"}( 'edit' ); + } + + if ( 'description' === $column_id || 'short_description' === $column_id ) { + $value = $this->filter_description_field( $value ); + } + + $row[ $column_id ] = $value; + } + + $this->prepare_downloads_for_export( $product, $row ); + $this->prepare_attributes_for_export( $product, $row ); + $this->prepare_meta_for_export( $product, $row ); + return apply_filters( 'woocommerce_product_export_row_data', $row, $product ); + } + + /** + * Get published value. + * + * @param WC_Product $product Product being exported. + * + * @since 3.1.0 + * @return int + */ + protected function get_column_value_published( $product ) { + $statuses = array( + 'draft' => -1, + 'private' => 0, + 'publish' => 1, + ); + + // Fix display for variations when parent product is a draft. + if ( 'variation' === $product->get_type() ) { + $parent = $product->get_parent_data(); + $status = 'draft' === $parent['status'] ? $parent['status'] : $product->get_status( 'edit' ); + } else { + $status = $product->get_status( 'edit' ); + } + + return isset( $statuses[ $status ] ) ? $statuses[ $status ] : -1; + } + + /** + * Get formatted sale price. + * + * @param WC_Product $product Product being exported. + * + * @return string + */ + protected function get_column_value_sale_price( $product ) { + return wc_format_localized_price( $product->get_sale_price( 'view' ) ); + } + + /** + * Get formatted regular price. + * + * @param WC_Product $product Product being exported. + * + * @return string + */ + protected function get_column_value_regular_price( $product ) { + return wc_format_localized_price( $product->get_regular_price() ); + } + + /** + * Get product_cat value. + * + * @param WC_Product $product Product being exported. + * + * @since 3.1.0 + * @return string + */ + protected function get_column_value_category_ids( $product ) { + $term_ids = $product->get_category_ids( 'edit' ); + return $this->format_term_ids( $term_ids, 'product_cat' ); + } + + /** + * Get product_tag value. + * + * @param WC_Product $product Product being exported. + * + * @since 3.1.0 + * @return string + */ + protected function get_column_value_tag_ids( $product ) { + $term_ids = $product->get_tag_ids( 'edit' ); + return $this->format_term_ids( $term_ids, 'product_tag' ); + } + + /** + * Get product_shipping_class value. + * + * @param WC_Product $product Product being exported. + * + * @since 3.1.0 + * @return string + */ + protected function get_column_value_shipping_class_id( $product ) { + $term_ids = $product->get_shipping_class_id( 'edit' ); + return $this->format_term_ids( $term_ids, 'product_shipping_class' ); + } + + /** + * Get images value. + * + * @param WC_Product $product Product being exported. + * + * @since 3.1.0 + * @return string + */ + protected function get_column_value_images( $product ) { + $image_ids = array_merge( array( $product->get_image_id( 'edit' ) ), $product->get_gallery_image_ids( 'edit' ) ); + $images = array(); + + foreach ( $image_ids as $image_id ) { + $image = wp_get_attachment_image_src( $image_id, 'full' ); + + if ( $image ) { + $images[] = $image[0]; + } + } + + return $this->implode_values( $images ); + } + + /** + * Prepare linked products for export. + * + * @param int[] $linked_products Array of linked product ids. + * + * @since 3.1.0 + * @return string + */ + protected function prepare_linked_products_for_export( $linked_products ) { + $product_list = array(); + + foreach ( $linked_products as $linked_product ) { + if ( $linked_product->get_sku() ) { + $product_list[] = $linked_product->get_sku(); + } else { + $product_list[] = 'id:' . $linked_product->get_id(); + } + } + + return $this->implode_values( $product_list ); + } + + /** + * Get cross_sell_ids value. + * + * @param WC_Product $product Product being exported. + * + * @since 3.1.0 + * @return string + */ + protected function get_column_value_cross_sell_ids( $product ) { + return $this->prepare_linked_products_for_export( array_filter( array_map( 'wc_get_product', (array) $product->get_cross_sell_ids( 'edit' ) ) ) ); + } + + /** + * Get upsell_ids value. + * + * @param WC_Product $product Product being exported. + * + * @since 3.1.0 + * @return string + */ + protected function get_column_value_upsell_ids( $product ) { + return $this->prepare_linked_products_for_export( array_filter( array_map( 'wc_get_product', (array) $product->get_upsell_ids( 'edit' ) ) ) ); + } + + /** + * Get parent_id value. + * + * @param WC_Product $product Product being exported. + * + * @since 3.1.0 + * @return string + */ + protected function get_column_value_parent_id( $product ) { + if ( $product->get_parent_id( 'edit' ) ) { + $parent = wc_get_product( $product->get_parent_id( 'edit' ) ); + if ( ! $parent ) { + return ''; + } + + return $parent->get_sku( 'edit' ) ? $parent->get_sku( 'edit' ) : 'id:' . $parent->get_id(); + } + return ''; + } + + /** + * Get grouped_products value. + * + * @param WC_Product $product Product being exported. + * + * @since 3.1.0 + * @return string + */ + protected function get_column_value_grouped_products( $product ) { + if ( 'grouped' !== $product->get_type() ) { + return ''; + } + + $grouped_products = array(); + $child_ids = $product->get_children( 'edit' ); + foreach ( $child_ids as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! $child ) { + continue; + } + + $grouped_products[] = $child->get_sku( 'edit' ) ? $child->get_sku( 'edit' ) : 'id:' . $child_id; + } + return $this->implode_values( $grouped_products ); + } + + /** + * Get download_limit value. + * + * @param WC_Product $product Product being exported. + * + * @since 3.1.0 + * @return string + */ + protected function get_column_value_download_limit( $product ) { + return $product->is_downloadable() && $product->get_download_limit( 'edit' ) ? $product->get_download_limit( 'edit' ) : ''; + } + + /** + * Get download_expiry value. + * + * @param WC_Product $product Product being exported. + * + * @since 3.1.0 + * @return string + */ + protected function get_column_value_download_expiry( $product ) { + return $product->is_downloadable() && $product->get_download_expiry( 'edit' ) ? $product->get_download_expiry( 'edit' ) : ''; + } + + /** + * Get stock value. + * + * @param WC_Product $product Product being exported. + * + * @since 3.1.0 + * @return string + */ + protected function get_column_value_stock( $product ) { + $manage_stock = $product->get_manage_stock( 'edit' ); + $stock_quantity = $product->get_stock_quantity( 'edit' ); + + if ( $product->is_type( 'variation' ) && 'parent' === $manage_stock ) { + return 'parent'; + } elseif ( $manage_stock ) { + return $stock_quantity; + } else { + return ''; + } + } + + /** + * Get stock status value. + * + * @param WC_Product $product Product being exported. + * + * @since 3.1.0 + * @return string + */ + protected function get_column_value_stock_status( $product ) { + $status = $product->get_stock_status( 'edit' ); + + if ( 'onbackorder' === $status ) { + return 'backorder'; + } + + return 'instock' === $status ? 1 : 0; + } + + /** + * Get backorders. + * + * @param WC_Product $product Product being exported. + * + * @since 3.1.0 + * @return string + */ + protected function get_column_value_backorders( $product ) { + $backorders = $product->get_backorders( 'edit' ); + + switch ( $backorders ) { + case 'notify': + return 'notify'; + default: + return wc_string_to_bool( $backorders ) ? 1 : 0; + } + } + + /** + * Get low stock amount value. + * + * @param WC_Product $product Product being exported. + * + * @since 3.5.0 + * @return int|string Empty string if value not set + */ + protected function get_column_value_low_stock_amount( $product ) { + return $product->managing_stock() && $product->get_low_stock_amount( 'edit' ) ? $product->get_low_stock_amount( 'edit' ) : ''; + } + + /** + * Get type value. + * + * @param WC_Product $product Product being exported. + * + * @since 3.1.0 + * @return string + */ + protected function get_column_value_type( $product ) { + $types = array(); + $types[] = $product->get_type(); + + if ( $product->is_downloadable() ) { + $types[] = 'downloadable'; + } + + if ( $product->is_virtual() ) { + $types[] = 'virtual'; + } + + return $this->implode_values( $types ); + } + + /** + * Filter description field for export. + * Convert newlines to '\n'. + * + * @param string $description Product description text to filter. + * + * @since 3.5.4 + * @return string + */ + protected function filter_description_field( $description ) { + $description = str_replace( '\n', "\\\\n", $description ); + $description = str_replace( "\n", '\n', $description ); + return $description; + } + /** + * Export downloads. + * + * @param WC_Product $product Product being exported. + * @param array $row Row being exported. + * + * @since 3.1.0 + */ + protected function prepare_downloads_for_export( $product, &$row ) { + if ( $product->is_downloadable() && $this->is_column_exporting( 'downloads' ) ) { + $downloads = $product->get_downloads( 'edit' ); + + if ( $downloads ) { + $i = 1; + foreach ( $downloads as $download ) { + /* translators: %s: download number */ + $this->column_names[ 'downloads:id' . $i ] = sprintf( __( 'Download %d ID', 'woocommerce' ), $i ); + /* translators: %s: download number */ + $this->column_names[ 'downloads:name' . $i ] = sprintf( __( 'Download %d name', 'woocommerce' ), $i ); + /* translators: %s: download number */ + $this->column_names[ 'downloads:url' . $i ] = sprintf( __( 'Download %d URL', 'woocommerce' ), $i ); + $row[ 'downloads:id' . $i ] = $download->get_id(); + $row[ 'downloads:name' . $i ] = $download->get_name(); + $row[ 'downloads:url' . $i ] = $download->get_file(); + $i++; + } + } + } + } + + /** + * Export attributes data. + * + * @param WC_Product $product Product being exported. + * @param array $row Row being exported. + * + * @since 3.1.0 + */ + protected function prepare_attributes_for_export( $product, &$row ) { + if ( $this->is_column_exporting( 'attributes' ) ) { + $attributes = $product->get_attributes(); + $default_attributes = $product->get_default_attributes(); + + if ( count( $attributes ) ) { + $i = 1; + foreach ( $attributes as $attribute_name => $attribute ) { + /* translators: %s: attribute number */ + $this->column_names[ 'attributes:name' . $i ] = sprintf( __( 'Attribute %d name', 'woocommerce' ), $i ); + /* translators: %s: attribute number */ + $this->column_names[ 'attributes:value' . $i ] = sprintf( __( 'Attribute %d value(s)', 'woocommerce' ), $i ); + /* translators: %s: attribute number */ + $this->column_names[ 'attributes:visible' . $i ] = sprintf( __( 'Attribute %d visible', 'woocommerce' ), $i ); + /* translators: %s: attribute number */ + $this->column_names[ 'attributes:taxonomy' . $i ] = sprintf( __( 'Attribute %d global', 'woocommerce' ), $i ); + + if ( is_a( $attribute, 'WC_Product_Attribute' ) ) { + $row[ 'attributes:name' . $i ] = wc_attribute_label( $attribute->get_name(), $product ); + + if ( $attribute->is_taxonomy() ) { + $terms = $attribute->get_terms(); + $values = array(); + + foreach ( $terms as $term ) { + $values[] = $term->name; + } + + $row[ 'attributes:value' . $i ] = $this->implode_values( $values ); + $row[ 'attributes:taxonomy' . $i ] = 1; + } else { + $row[ 'attributes:value' . $i ] = $this->implode_values( $attribute->get_options() ); + $row[ 'attributes:taxonomy' . $i ] = 0; + } + + $row[ 'attributes:visible' . $i ] = $attribute->get_visible(); + } else { + $row[ 'attributes:name' . $i ] = wc_attribute_label( $attribute_name, $product ); + + if ( 0 === strpos( $attribute_name, 'pa_' ) ) { + $option_term = get_term_by( 'slug', $attribute, $attribute_name ); // @codingStandardsIgnoreLine. + $row[ 'attributes:value' . $i ] = $option_term && ! is_wp_error( $option_term ) ? str_replace( ',', '\\,', $option_term->name ) : str_replace( ',', '\\,', $attribute ); + $row[ 'attributes:taxonomy' . $i ] = 1; + } else { + $row[ 'attributes:value' . $i ] = str_replace( ',', '\\,', $attribute ); + $row[ 'attributes:taxonomy' . $i ] = 0; + } + + $row[ 'attributes:visible' . $i ] = ''; + } + + if ( $product->is_type( 'variable' ) && isset( $default_attributes[ sanitize_title( $attribute_name ) ] ) ) { + /* translators: %s: attribute number */ + $this->column_names[ 'attributes:default' . $i ] = sprintf( __( 'Attribute %d default', 'woocommerce' ), $i ); + $default_value = $default_attributes[ sanitize_title( $attribute_name ) ]; + + if ( 0 === strpos( $attribute_name, 'pa_' ) ) { + $option_term = get_term_by( 'slug', $default_value, $attribute_name ); // @codingStandardsIgnoreLine. + $row[ 'attributes:default' . $i ] = $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $default_value; + } else { + $row[ 'attributes:default' . $i ] = $default_value; + } + } + $i++; + } + } + } + } + + /** + * Export meta data. + * + * @param WC_Product $product Product being exported. + * @param array $row Row data. + * + * @since 3.1.0 + */ + protected function prepare_meta_for_export( $product, &$row ) { + if ( $this->enable_meta_export ) { + $meta_data = $product->get_meta_data(); + + if ( count( $meta_data ) ) { + $meta_keys_to_skip = apply_filters( 'woocommerce_product_export_skip_meta_keys', array(), $product ); + + $i = 1; + foreach ( $meta_data as $meta ) { + if ( in_array( $meta->key, $meta_keys_to_skip, true ) ) { + continue; + } + + // Allow 3rd parties to process the meta, e.g. to transform non-scalar values to scalar. + $meta_value = apply_filters( 'woocommerce_product_export_meta_value', $meta->value, $meta, $product, $row ); + + if ( ! is_scalar( $meta_value ) ) { + continue; + } + + $column_key = 'meta:' . esc_attr( $meta->key ); + /* translators: %s: meta data name */ + $this->column_names[ $column_key ] = sprintf( __( 'Meta: %s', 'woocommerce' ), $meta->key ); + $row[ $column_key ] = $meta_value; + $i ++; + } + } + } + } +} diff --git a/plugins/woocommerce/includes/gateways/bacs/class-wc-gateway-bacs.php b/plugins/woocommerce/includes/gateways/bacs/class-wc-gateway-bacs.php new file mode 100644 index 00000000000..661e84483fc --- /dev/null +++ b/plugins/woocommerce/includes/gateways/bacs/class-wc-gateway-bacs.php @@ -0,0 +1,442 @@ +id = 'bacs'; + $this->icon = apply_filters( 'woocommerce_bacs_icon', '' ); + $this->has_fields = false; + $this->method_title = __( 'Direct bank transfer', 'woocommerce' ); + $this->method_description = __( 'Take payments in person via BACS. More commonly known as direct bank/wire transfer.', 'woocommerce' ); + + // Load the settings. + $this->init_form_fields(); + $this->init_settings(); + + // Define user set variables. + $this->title = $this->get_option( 'title' ); + $this->description = $this->get_option( 'description' ); + $this->instructions = $this->get_option( 'instructions' ); + + // BACS account fields shown on the thanks page and in emails. + $this->account_details = get_option( + 'woocommerce_bacs_accounts', + array( + array( + 'account_name' => $this->get_option( 'account_name' ), + 'account_number' => $this->get_option( 'account_number' ), + 'sort_code' => $this->get_option( 'sort_code' ), + 'bank_name' => $this->get_option( 'bank_name' ), + 'iban' => $this->get_option( 'iban' ), + 'bic' => $this->get_option( 'bic' ), + ), + ) + ); + + // Actions. + add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) ); + add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'save_account_details' ) ); + add_action( 'woocommerce_thankyou_bacs', array( $this, 'thankyou_page' ) ); + + // Customer Emails. + add_action( 'woocommerce_email_before_order_table', array( $this, 'email_instructions' ), 10, 3 ); + } + + /** + * Initialise Gateway Settings Form Fields. + */ + public function init_form_fields() { + + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable bank transfer', 'woocommerce' ), + 'default' => 'no', + ), + 'title' => array( + 'title' => __( 'Title', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), + 'default' => __( 'Direct bank transfer', 'woocommerce' ), + 'desc_tip' => true, + ), + 'description' => array( + 'title' => __( 'Description', 'woocommerce' ), + 'type' => 'textarea', + 'description' => __( 'Payment method description that the customer will see on your checkout.', 'woocommerce' ), + 'default' => __( 'Make your payment directly into our bank account. Please use your Order ID as the payment reference. Your order will not be shipped until the funds have cleared in our account.', 'woocommerce' ), + 'desc_tip' => true, + ), + 'instructions' => array( + 'title' => __( 'Instructions', 'woocommerce' ), + 'type' => 'textarea', + 'description' => __( 'Instructions that will be added to the thank you page and emails.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + ), + 'account_details' => array( + 'type' => 'account_details', + ), + ); + + } + + /** + * Generate account details html. + * + * @return string + */ + public function generate_account_details_html() { + + ob_start(); + + $country = WC()->countries->get_base_country(); + $locale = $this->get_country_locale(); + + // Get sortcode label in the $locale array and use appropriate one. + $sortcode = isset( $locale[ $country ]['sortcode']['label'] ) ? $locale[ $country ]['sortcode']['label'] : __( 'Sort code', 'woocommerce' ); + + ?> + + + +
    + + + + + + + + + + + + + + account_details ) { + foreach ( $this->account_details as $account ) { + $i++; + + echo ' + + + + + + + + '; + } + } + ?> + + + + + + +
     
    +
    + + + + $name ) { + if ( ! isset( $account_names[ $i ] ) ) { + continue; + } + + $accounts[] = array( + 'account_name' => $account_names[ $i ], + 'account_number' => $account_numbers[ $i ], + 'bank_name' => $bank_names[ $i ], + 'sort_code' => $sort_codes[ $i ], + 'iban' => $ibans[ $i ], + 'bic' => $bics[ $i ], + ); + } + } + // phpcs:enable + + do_action( 'woocommerce_update_option', array( 'id' => 'woocommerce_bacs_accounts' ) ); + update_option( 'woocommerce_bacs_accounts', $accounts ); + } + + /** + * Output for the order received page. + * + * @param int $order_id Order ID. + */ + public function thankyou_page( $order_id ) { + + if ( $this->instructions ) { + echo wp_kses_post( wpautop( wptexturize( wp_kses_post( $this->instructions ) ) ) ); + } + $this->bank_details( $order_id ); + + } + + /** + * Add content to the WC emails. + * + * @param WC_Order $order Order object. + * @param bool $sent_to_admin Sent to admin. + * @param bool $plain_text Email format: plain text or HTML. + */ + public function email_instructions( $order, $sent_to_admin, $plain_text = false ) { + + if ( ! $sent_to_admin && 'bacs' === $order->get_payment_method() && $order->has_status( 'on-hold' ) ) { + if ( $this->instructions ) { + echo wp_kses_post( wpautop( wptexturize( $this->instructions ) ) . PHP_EOL ); + } + $this->bank_details( $order->get_id() ); + } + + } + + /** + * Get bank details and place into a list format. + * + * @param int $order_id Order ID. + */ + private function bank_details( $order_id = '' ) { + + if ( empty( $this->account_details ) ) { + return; + } + + // Get order and store in $order. + $order = wc_get_order( $order_id ); + + // Get the order country and country $locale. + $country = $order->get_billing_country(); + $locale = $this->get_country_locale(); + + // Get sortcode label in the $locale array and use appropriate one. + $sortcode = isset( $locale[ $country ]['sortcode']['label'] ) ? $locale[ $country ]['sortcode']['label'] : __( 'Sort code', 'woocommerce' ); + + $bacs_accounts = apply_filters( 'woocommerce_bacs_accounts', $this->account_details, $order_id ); + + if ( ! empty( $bacs_accounts ) ) { + $account_html = ''; + $has_details = false; + + foreach ( $bacs_accounts as $bacs_account ) { + $bacs_account = (object) $bacs_account; + + if ( $bacs_account->account_name ) { + $account_html .= '' . PHP_EOL; + } + + $account_html .= '
      ' . PHP_EOL; + + // BACS account fields shown on the thanks page and in emails. + $account_fields = apply_filters( + 'woocommerce_bacs_account_fields', + array( + 'bank_name' => array( + 'label' => __( 'Bank', 'woocommerce' ), + 'value' => $bacs_account->bank_name, + ), + 'account_number' => array( + 'label' => __( 'Account number', 'woocommerce' ), + 'value' => $bacs_account->account_number, + ), + 'sort_code' => array( + 'label' => $sortcode, + 'value' => $bacs_account->sort_code, + ), + 'iban' => array( + 'label' => __( 'IBAN', 'woocommerce' ), + 'value' => $bacs_account->iban, + ), + 'bic' => array( + 'label' => __( 'BIC', 'woocommerce' ), + 'value' => $bacs_account->bic, + ), + ), + $order_id + ); + + foreach ( $account_fields as $field_key => $field ) { + if ( ! empty( $field['value'] ) ) { + $account_html .= '
    • ' . wp_kses_post( $field['label'] ) . ': ' . wp_kses_post( wptexturize( $field['value'] ) ) . '
    • ' . PHP_EOL; + $has_details = true; + } + } + + $account_html .= '
    '; + } + + if ( $has_details ) { + echo '

    ' . esc_html__( 'Our bank details', 'woocommerce' ) . '

    ' . wp_kses_post( PHP_EOL . $account_html ) . '
    '; + } + } + + } + + /** + * Process the payment and return the result. + * + * @param int $order_id Order ID. + * @return array + */ + public function process_payment( $order_id ) { + + $order = wc_get_order( $order_id ); + + if ( $order->get_total() > 0 ) { + // Mark as on-hold (we're awaiting the payment). + $order->update_status( apply_filters( 'woocommerce_bacs_process_payment_order_status', 'on-hold', $order ), __( 'Awaiting BACS payment', 'woocommerce' ) ); + } else { + $order->payment_complete(); + } + + // Remove cart. + WC()->cart->empty_cart(); + + // Return thankyou redirect. + return array( + 'result' => 'success', + 'redirect' => $this->get_return_url( $order ), + ); + + } + + /** + * Get country locale if localized. + * + * @return array + */ + public function get_country_locale() { + + if ( empty( $this->locale ) ) { + + // Locale information to be used - only those that are not 'Sort Code'. + $this->locale = apply_filters( + 'woocommerce_get_bacs_locale', + array( + 'AU' => array( + 'sortcode' => array( + 'label' => __( 'BSB', 'woocommerce' ), + ), + ), + 'CA' => array( + 'sortcode' => array( + 'label' => __( 'Bank transit number', 'woocommerce' ), + ), + ), + 'IN' => array( + 'sortcode' => array( + 'label' => __( 'IFSC', 'woocommerce' ), + ), + ), + 'IT' => array( + 'sortcode' => array( + 'label' => __( 'Branch sort', 'woocommerce' ), + ), + ), + 'NZ' => array( + 'sortcode' => array( + 'label' => __( 'Bank code', 'woocommerce' ), + ), + ), + 'SE' => array( + 'sortcode' => array( + 'label' => __( 'Bank code', 'woocommerce' ), + ), + ), + 'US' => array( + 'sortcode' => array( + 'label' => __( 'Routing number', 'woocommerce' ), + ), + ), + 'ZA' => array( + 'sortcode' => array( + 'label' => __( 'Branch code', 'woocommerce' ), + ), + ), + ) + ); + + } + + return $this->locale; + + } +} diff --git a/includes/gateways/cheque/class-wc-gateway-cheque.php b/plugins/woocommerce/includes/gateways/cheque/class-wc-gateway-cheque.php similarity index 100% rename from includes/gateways/cheque/class-wc-gateway-cheque.php rename to plugins/woocommerce/includes/gateways/cheque/class-wc-gateway-cheque.php diff --git a/includes/gateways/class-wc-payment-gateway-cc.php b/plugins/woocommerce/includes/gateways/class-wc-payment-gateway-cc.php similarity index 100% rename from includes/gateways/class-wc-payment-gateway-cc.php rename to plugins/woocommerce/includes/gateways/class-wc-payment-gateway-cc.php diff --git a/includes/gateways/class-wc-payment-gateway-echeck.php b/plugins/woocommerce/includes/gateways/class-wc-payment-gateway-echeck.php similarity index 100% rename from includes/gateways/class-wc-payment-gateway-echeck.php rename to plugins/woocommerce/includes/gateways/class-wc-payment-gateway-echeck.php diff --git a/plugins/woocommerce/includes/gateways/cod/class-wc-gateway-cod.php b/plugins/woocommerce/includes/gateways/cod/class-wc-gateway-cod.php new file mode 100644 index 00000000000..e4b3e65c908 --- /dev/null +++ b/plugins/woocommerce/includes/gateways/cod/class-wc-gateway-cod.php @@ -0,0 +1,379 @@ +setup_properties(); + + // Load the settings. + $this->init_form_fields(); + $this->init_settings(); + + // Get settings. + $this->title = $this->get_option( 'title' ); + $this->description = $this->get_option( 'description' ); + $this->instructions = $this->get_option( 'instructions' ); + $this->enable_for_methods = $this->get_option( 'enable_for_methods', array() ); + $this->enable_for_virtual = $this->get_option( 'enable_for_virtual', 'yes' ) === 'yes'; + + // Actions. + add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) ); + add_action( 'woocommerce_thankyou_' . $this->id, array( $this, 'thankyou_page' ) ); + add_filter( 'woocommerce_payment_complete_order_status', array( $this, 'change_payment_complete_order_status' ), 10, 3 ); + + // Customer Emails. + add_action( 'woocommerce_email_before_order_table', array( $this, 'email_instructions' ), 10, 3 ); + } + + /** + * Setup general properties for the gateway. + */ + protected function setup_properties() { + $this->id = 'cod'; + $this->icon = apply_filters( 'woocommerce_cod_icon', '' ); + $this->method_title = __( 'Cash on delivery', 'woocommerce' ); + $this->method_description = __( 'Have your customers pay with cash (or by other means) upon delivery.', 'woocommerce' ); + $this->has_fields = false; + } + + /** + * Initialise Gateway Settings Form Fields. + */ + public function init_form_fields() { + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'label' => __( 'Enable cash on delivery', 'woocommerce' ), + 'type' => 'checkbox', + 'description' => '', + 'default' => 'no', + ), + 'title' => array( + 'title' => __( 'Title', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Payment method description that the customer will see on your checkout.', 'woocommerce' ), + 'default' => __( 'Cash on delivery', 'woocommerce' ), + 'desc_tip' => true, + ), + 'description' => array( + 'title' => __( 'Description', 'woocommerce' ), + 'type' => 'textarea', + 'description' => __( 'Payment method description that the customer will see on your website.', 'woocommerce' ), + 'default' => __( 'Pay with cash upon delivery.', 'woocommerce' ), + 'desc_tip' => true, + ), + 'instructions' => array( + 'title' => __( 'Instructions', 'woocommerce' ), + 'type' => 'textarea', + 'description' => __( 'Instructions that will be added to the thank you page.', 'woocommerce' ), + 'default' => __( 'Pay with cash upon delivery.', 'woocommerce' ), + 'desc_tip' => true, + ), + 'enable_for_methods' => array( + 'title' => __( 'Enable for shipping methods', 'woocommerce' ), + 'type' => 'multiselect', + 'class' => 'wc-enhanced-select', + 'css' => 'width: 400px;', + 'default' => '', + 'description' => __( 'If COD is only available for certain methods, set it up here. Leave blank to enable for all methods.', 'woocommerce' ), + 'options' => $this->load_shipping_method_options(), + 'desc_tip' => true, + 'custom_attributes' => array( + 'data-placeholder' => __( 'Select shipping methods', 'woocommerce' ), + ), + ), + 'enable_for_virtual' => array( + 'title' => __( 'Accept for virtual orders', 'woocommerce' ), + 'label' => __( 'Accept COD if the order is virtual', 'woocommerce' ), + 'type' => 'checkbox', + 'default' => 'yes', + ), + ); + } + + /** + * Check If The Gateway Is Available For Use. + * + * @return bool + */ + public function is_available() { + $order = null; + $needs_shipping = false; + + // Test if shipping is needed first. + if ( WC()->cart && WC()->cart->needs_shipping() ) { + $needs_shipping = true; + } elseif ( is_page( wc_get_page_id( 'checkout' ) ) && 0 < get_query_var( 'order-pay' ) ) { + $order_id = absint( get_query_var( 'order-pay' ) ); + $order = wc_get_order( $order_id ); + + // Test if order needs shipping. + if ( $order && 0 < count( $order->get_items() ) ) { + foreach ( $order->get_items() as $item ) { + $_product = $item->get_product(); + if ( $_product && $_product->needs_shipping() ) { + $needs_shipping = true; + break; + } + } + } + } + + $needs_shipping = apply_filters( 'woocommerce_cart_needs_shipping', $needs_shipping ); + + // Virtual order, with virtual disabled. + if ( ! $this->enable_for_virtual && ! $needs_shipping ) { + return false; + } + + // Only apply if all packages are being shipped via chosen method, or order is virtual. + if ( ! empty( $this->enable_for_methods ) && $needs_shipping ) { + $order_shipping_items = is_object( $order ) ? $order->get_shipping_methods() : false; + $chosen_shipping_methods_session = WC()->session->get( 'chosen_shipping_methods' ); + + if ( $order_shipping_items ) { + $canonical_rate_ids = $this->get_canonical_order_shipping_item_rate_ids( $order_shipping_items ); + } else { + $canonical_rate_ids = $this->get_canonical_package_rate_ids( $chosen_shipping_methods_session ); + } + + if ( ! count( $this->get_matching_rates( $canonical_rate_ids ) ) ) { + return false; + } + } + + return parent::is_available(); + } + + /** + * Checks to see whether or not the admin settings are being accessed by the current request. + * + * @return bool + */ + private function is_accessing_settings() { + if ( is_admin() ) { + // phpcs:disable WordPress.Security.NonceVerification + if ( ! isset( $_REQUEST['page'] ) || 'wc-settings' !== $_REQUEST['page'] ) { + return false; + } + if ( ! isset( $_REQUEST['tab'] ) || 'checkout' !== $_REQUEST['tab'] ) { + return false; + } + if ( ! isset( $_REQUEST['section'] ) || 'cod' !== $_REQUEST['section'] ) { + return false; + } + // phpcs:enable WordPress.Security.NonceVerification + + return true; + } + + if ( Constants::is_true( 'REST_REQUEST' ) ) { + global $wp; + if ( isset( $wp->query_vars['rest_route'] ) && false !== strpos( $wp->query_vars['rest_route'], '/payment_gateways' ) ) { + return true; + } + } + + return false; + } + + /** + * Loads all of the shipping method options for the enable_for_methods field. + * + * @return array + */ + private function load_shipping_method_options() { + // Since this is expensive, we only want to do it if we're actually on the settings page. + if ( ! $this->is_accessing_settings() ) { + return array(); + } + + $data_store = WC_Data_Store::load( 'shipping-zone' ); + $raw_zones = $data_store->get_zones(); + + foreach ( $raw_zones as $raw_zone ) { + $zones[] = new WC_Shipping_Zone( $raw_zone ); + } + + $zones[] = new WC_Shipping_Zone( 0 ); + + $options = array(); + foreach ( WC()->shipping()->load_shipping_methods() as $method ) { + + $options[ $method->get_method_title() ] = array(); + + // Translators: %1$s shipping method name. + $options[ $method->get_method_title() ][ $method->id ] = sprintf( __( 'Any "%1$s" method', 'woocommerce' ), $method->get_method_title() ); + + foreach ( $zones as $zone ) { + + $shipping_method_instances = $zone->get_shipping_methods(); + + foreach ( $shipping_method_instances as $shipping_method_instance_id => $shipping_method_instance ) { + + if ( $shipping_method_instance->id !== $method->id ) { + continue; + } + + $option_id = $shipping_method_instance->get_rate_id(); + + // Translators: %1$s shipping method title, %2$s shipping method id. + $option_instance_title = sprintf( __( '%1$s (#%2$s)', 'woocommerce' ), $shipping_method_instance->get_title(), $shipping_method_instance_id ); + + // Translators: %1$s zone name, %2$s shipping method instance name. + $option_title = sprintf( __( '%1$s – %2$s', 'woocommerce' ), $zone->get_id() ? $zone->get_zone_name() : __( 'Other locations', 'woocommerce' ), $option_instance_title ); + + $options[ $method->get_method_title() ][ $option_id ] = $option_title; + } + } + } + + return $options; + } + + /** + * Converts the chosen rate IDs generated by Shipping Methods to a canonical 'method_id:instance_id' format. + * + * @since 3.4.0 + * + * @param array $order_shipping_items Array of WC_Order_Item_Shipping objects. + * @return array $canonical_rate_ids Rate IDs in a canonical format. + */ + private function get_canonical_order_shipping_item_rate_ids( $order_shipping_items ) { + + $canonical_rate_ids = array(); + + foreach ( $order_shipping_items as $order_shipping_item ) { + $canonical_rate_ids[] = $order_shipping_item->get_method_id() . ':' . $order_shipping_item->get_instance_id(); + } + + return $canonical_rate_ids; + } + + /** + * Converts the chosen rate IDs generated by Shipping Methods to a canonical 'method_id:instance_id' format. + * + * @since 3.4.0 + * + * @param array $chosen_package_rate_ids Rate IDs as generated by shipping methods. Can be anything if a shipping method doesn't honor WC conventions. + * @return array $canonical_rate_ids Rate IDs in a canonical format. + */ + private function get_canonical_package_rate_ids( $chosen_package_rate_ids ) { + + $shipping_packages = WC()->shipping()->get_packages(); + $canonical_rate_ids = array(); + + if ( ! empty( $chosen_package_rate_ids ) && is_array( $chosen_package_rate_ids ) ) { + foreach ( $chosen_package_rate_ids as $package_key => $chosen_package_rate_id ) { + if ( ! empty( $shipping_packages[ $package_key ]['rates'][ $chosen_package_rate_id ] ) ) { + $chosen_rate = $shipping_packages[ $package_key ]['rates'][ $chosen_package_rate_id ]; + $canonical_rate_ids[] = $chosen_rate->get_method_id() . ':' . $chosen_rate->get_instance_id(); + } + } + } + + return $canonical_rate_ids; + } + + /** + * Indicates whether a rate exists in an array of canonically-formatted rate IDs that activates this gateway. + * + * @since 3.4.0 + * + * @param array $rate_ids Rate ids to check. + * @return boolean + */ + private function get_matching_rates( $rate_ids ) { + // First, match entries in 'method_id:instance_id' format. Then, match entries in 'method_id' format by stripping off the instance ID from the candidates. + return array_unique( array_merge( array_intersect( $this->enable_for_methods, $rate_ids ), array_intersect( $this->enable_for_methods, array_unique( array_map( 'wc_get_string_before_colon', $rate_ids ) ) ) ) ); + } + + /** + * Process the payment and return the result. + * + * @param int $order_id Order ID. + * @return array + */ + public function process_payment( $order_id ) { + $order = wc_get_order( $order_id ); + + if ( $order->get_total() > 0 ) { + // Mark as processing or on-hold (payment won't be taken until delivery). + $order->update_status( apply_filters( 'woocommerce_cod_process_payment_order_status', $order->has_downloadable_item() ? 'on-hold' : 'processing', $order ), __( 'Payment to be made upon delivery.', 'woocommerce' ) ); + } else { + $order->payment_complete(); + } + + // Remove cart. + WC()->cart->empty_cart(); + + // Return thankyou redirect. + return array( + 'result' => 'success', + 'redirect' => $this->get_return_url( $order ), + ); + } + + /** + * Output for the order received page. + */ + public function thankyou_page() { + if ( $this->instructions ) { + echo wp_kses_post( wpautop( wptexturize( $this->instructions ) ) ); + } + } + + /** + * Change payment complete order status to completed for COD orders. + * + * @since 3.1.0 + * @param string $status Current order status. + * @param int $order_id Order ID. + * @param WC_Order|false $order Order object. + * @return string + */ + public function change_payment_complete_order_status( $status, $order_id = 0, $order = false ) { + if ( $order && 'cod' === $order->get_payment_method() ) { + $status = 'completed'; + } + return $status; + } + + /** + * Add content to the WC emails. + * + * @param WC_Order $order Order object. + * @param bool $sent_to_admin Sent to admin. + * @param bool $plain_text Email format: plain text or HTML. + */ + public function email_instructions( $order, $sent_to_admin, $plain_text = false ) { + if ( $this->instructions && ! $sent_to_admin && $this->id === $order->get_payment_method() ) { + echo wp_kses_post( wpautop( wptexturize( $this->instructions ) ) . PHP_EOL ); + } + } +} diff --git a/includes/gateways/paypal/assets/images/paypal.png b/plugins/woocommerce/includes/gateways/paypal/assets/images/paypal.png similarity index 100% rename from includes/gateways/paypal/assets/images/paypal.png rename to plugins/woocommerce/includes/gateways/paypal/assets/images/paypal.png diff --git a/plugins/woocommerce/includes/gateways/paypal/assets/js/paypal-admin.js b/plugins/woocommerce/includes/gateways/paypal/assets/js/paypal-admin.js new file mode 100644 index 00000000000..8fb28c3f059 --- /dev/null +++ b/plugins/woocommerce/includes/gateways/paypal/assets/js/paypal-admin.js @@ -0,0 +1,46 @@ +jQuery( function( $ ) { + 'use strict'; + + /** + * Object to handle PayPal admin functions. + */ + var wc_paypal_admin = { + isTestMode: function() { + return $( '#woocommerce_paypal_testmode' ).is( ':checked' ); + }, + + /** + * Initialize. + */ + init: function() { + $( document.body ).on( 'change', '#woocommerce_paypal_testmode', function() { + var test_api_username = $( '#woocommerce_paypal_sandbox_api_username' ).parents( 'tr' ).eq( 0 ), + test_api_password = $( '#woocommerce_paypal_sandbox_api_password' ).parents( 'tr' ).eq( 0 ), + test_api_signature = $( '#woocommerce_paypal_sandbox_api_signature' ).parents( 'tr' ).eq( 0 ), + live_api_username = $( '#woocommerce_paypal_api_username' ).parents( 'tr' ).eq( 0 ), + live_api_password = $( '#woocommerce_paypal_api_password' ).parents( 'tr' ).eq( 0 ), + live_api_signature = $( '#woocommerce_paypal_api_signature' ).parents( 'tr' ).eq( 0 ); + + if ( $( this ).is( ':checked' ) ) { + test_api_username.show(); + test_api_password.show(); + test_api_signature.show(); + live_api_username.hide(); + live_api_password.hide(); + live_api_signature.hide(); + } else { + test_api_username.hide(); + test_api_password.hide(); + test_api_signature.hide(); + live_api_username.show(); + live_api_password.show(); + live_api_signature.show(); + } + } ); + + $( '#woocommerce_paypal_testmode' ).trigger( 'change' ); + } + }; + + wc_paypal_admin.init(); +}); diff --git a/plugins/woocommerce/includes/gateways/paypal/assets/js/paypal-admin.min.js b/plugins/woocommerce/includes/gateways/paypal/assets/js/paypal-admin.min.js new file mode 100644 index 00000000000..f91eadf31e4 --- /dev/null +++ b/plugins/woocommerce/includes/gateways/paypal/assets/js/paypal-admin.min.js @@ -0,0 +1 @@ +jQuery(function(t){"use strict";(function(){t(document.body).on("change","#woocommerce_paypal_testmode",function(){var e=t("#woocommerce_paypal_sandbox_api_username").parents("tr").eq(0),o=t("#woocommerce_paypal_sandbox_api_password").parents("tr").eq(0),a=t("#woocommerce_paypal_sandbox_api_signature").parents("tr").eq(0),r=t("#woocommerce_paypal_api_username").parents("tr").eq(0),p=t("#woocommerce_paypal_api_password").parents("tr").eq(0),s=t("#woocommerce_paypal_api_signature").parents("tr").eq(0);t(this).is(":checked")?(e.show(),o.show(),a.show(),r.hide(),p.hide(),s.hide()):(e.hide(),o.hide(),a.hide(),r.show(),p.show(),s.show())}),t("#woocommerce_paypal_testmode").trigger("change")})()}); \ No newline at end of file diff --git a/plugins/woocommerce/includes/gateways/paypal/class-wc-gateway-paypal.php b/plugins/woocommerce/includes/gateways/paypal/class-wc-gateway-paypal.php new file mode 100644 index 00000000000..658cec00094 --- /dev/null +++ b/plugins/woocommerce/includes/gateways/paypal/class-wc-gateway-paypal.php @@ -0,0 +1,516 @@ +id = 'paypal'; + $this->has_fields = false; + $this->order_button_text = __( 'Proceed to PayPal', 'woocommerce' ); + $this->method_title = __( 'PayPal Standard', 'woocommerce' ); + /* translators: %s: Link to WC system status page */ + $this->method_description = __( 'PayPal Standard redirects customers to PayPal to enter their payment information.', 'woocommerce' ); + $this->supports = array( + 'products', + 'refunds', + ); + + // Load the settings. + $this->init_form_fields(); + $this->init_settings(); + + // Define user set variables. + $this->title = $this->get_option( 'title' ); + $this->description = $this->get_option( 'description' ); + $this->testmode = 'yes' === $this->get_option( 'testmode', 'no' ); + $this->debug = 'yes' === $this->get_option( 'debug', 'no' ); + $this->email = $this->get_option( 'email' ); + $this->receiver_email = $this->get_option( 'receiver_email', $this->email ); + $this->identity_token = $this->get_option( 'identity_token' ); + self::$log_enabled = $this->debug; + + if ( $this->testmode ) { + /* translators: %s: Link to PayPal sandbox testing guide page */ + $this->description .= ' ' . sprintf( __( 'SANDBOX ENABLED. You can use sandbox testing accounts only. See the PayPal Sandbox Testing Guide for more details.', 'woocommerce' ), 'https://developer.paypal.com/docs/classic/lifecycle/ug_sandbox/' ); + $this->description = trim( $this->description ); + } + + // Actions. + add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) ); + add_action( 'woocommerce_order_status_processing', array( $this, 'capture_payment' ) ); + add_action( 'woocommerce_order_status_completed', array( $this, 'capture_payment' ) ); + add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) ); + + if ( ! $this->is_valid_for_use() ) { + $this->enabled = 'no'; + } else { + include_once dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-ipn-handler.php'; + new WC_Gateway_Paypal_IPN_Handler( $this->testmode, $this->receiver_email ); + + if ( $this->identity_token ) { + include_once dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-pdt-handler.php'; + $pdt_handler = new WC_Gateway_Paypal_PDT_Handler( $this->testmode, $this->identity_token ); + $pdt_handler->set_receiver_email( $this->receiver_email ); + } + } + + if ( 'yes' === $this->enabled ) { + add_filter( 'woocommerce_thankyou_order_received_text', array( $this, 'order_received_text' ), 10, 2 ); + } + } + + /** + * Return whether or not this gateway still requires setup to function. + * + * When this gateway is toggled on via AJAX, if this returns true a + * redirect will occur to the settings page instead. + * + * @since 3.4.0 + * @return bool + */ + public function needs_setup() { + return ! is_email( $this->email ); + } + + /** + * Logging method. + * + * @param string $message Log message. + * @param string $level Optional. Default 'info'. Possible values: + * emergency|alert|critical|error|warning|notice|info|debug. + */ + public static function log( $message, $level = 'info' ) { + if ( self::$log_enabled ) { + if ( empty( self::$log ) ) { + self::$log = wc_get_logger(); + } + self::$log->log( $level, $message, array( 'source' => 'paypal' ) ); + } + } + + /** + * Processes and saves options. + * If there is an error thrown, will continue to save and validate fields, but will leave the erroring field out. + * + * @return bool was anything saved? + */ + public function process_admin_options() { + $saved = parent::process_admin_options(); + + // Maybe clear logs. + if ( 'yes' !== $this->get_option( 'debug', 'no' ) ) { + if ( empty( self::$log ) ) { + self::$log = wc_get_logger(); + } + self::$log->clear( 'paypal' ); + } + + return $saved; + } + + /** + * Get gateway icon. + * + * @return string + */ + public function get_icon() { + // We need a base country for the link to work, bail if in the unlikely event no country is set. + $base_country = WC()->countries->get_base_country(); + if ( empty( $base_country ) ) { + return ''; + } + $icon_html = ''; + $icon = (array) $this->get_icon_image( $base_country ); + + foreach ( $icon as $i ) { + $icon_html .= '' . esc_attr__( 'PayPal acceptance mark', 'woocommerce' ) . ''; + } + + $icon_html .= sprintf( '' . esc_attr__( 'What is PayPal?', 'woocommerce' ) . '', esc_url( $this->get_icon_url( $base_country ) ) ); + + return apply_filters( 'woocommerce_gateway_icon', $icon_html, $this->id ); + } + + /** + * Get the link for an icon based on country. + * + * @param string $country Country two letter code. + * @return string + */ + protected function get_icon_url( $country ) { + $url = 'https://www.paypal.com/' . strtolower( $country ); + $home_counties = array( 'BE', 'CZ', 'DK', 'HU', 'IT', 'JP', 'NL', 'NO', 'ES', 'SE', 'TR', 'IN' ); + $countries = array( 'DZ', 'AU', 'BH', 'BQ', 'BW', 'CA', 'CN', 'CW', 'FI', 'FR', 'DE', 'GR', 'HK', 'ID', 'JO', 'KE', 'KW', 'LU', 'MY', 'MA', 'OM', 'PH', 'PL', 'PT', 'QA', 'IE', 'RU', 'BL', 'SX', 'MF', 'SA', 'SG', 'SK', 'KR', 'SS', 'TW', 'TH', 'AE', 'GB', 'US', 'VN' ); + + if ( in_array( $country, $home_counties, true ) ) { + return $url . '/webapps/mpp/home'; + } elseif ( in_array( $country, $countries, true ) ) { + return $url . '/webapps/mpp/paypal-popup'; + } else { + return $url . '/cgi-bin/webscr?cmd=xpt/Marketing/general/WIPaypal-outside'; + } + } + + /** + * Get PayPal images for a country. + * + * @param string $country Country code. + * @return array of image URLs + */ + protected function get_icon_image( $country ) { + switch ( $country ) { + case 'US': + case 'NZ': + case 'CZ': + case 'HU': + case 'MY': + $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo/AM_mc_vs_dc_ae.jpg'; + break; + case 'TR': + $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/logo_paypal_odeme_secenekleri.jpg'; + break; + case 'GB': + $icon = 'https://www.paypalobjects.com/webstatic/mktg/Logo/AM_mc_vs_ms_ae_UK.png'; + break; + case 'MX': + $icon = array( + 'https://www.paypal.com/es_XC/Marketing/i/banner/paypal_visa_mastercard_amex.png', + 'https://www.paypal.com/es_XC/Marketing/i/banner/paypal_debit_card_275x60.gif', + ); + break; + case 'FR': + $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/logo_paypal_moyens_paiement_fr.jpg'; + break; + case 'AU': + $icon = 'https://www.paypalobjects.com/webstatic/en_AU/mktg/logo/Solutions-graphics-1-184x80.jpg'; + break; + case 'DK': + $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/logo_PayPal_betalingsmuligheder_dk.jpg'; + break; + case 'RU': + $icon = 'https://www.paypalobjects.com/webstatic/ru_RU/mktg/business/pages/logo-center/AM_mc_vs_dc_ae.jpg'; + break; + case 'NO': + $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/banner_pl_just_pp_319x110.jpg'; + break; + case 'CA': + $icon = 'https://www.paypalobjects.com/webstatic/en_CA/mktg/logo-image/AM_mc_vs_dc_ae.jpg'; + break; + case 'HK': + $icon = 'https://www.paypalobjects.com/webstatic/en_HK/mktg/logo/AM_mc_vs_dc_ae.jpg'; + break; + case 'SG': + $icon = 'https://www.paypalobjects.com/webstatic/en_SG/mktg/Logos/AM_mc_vs_dc_ae.jpg'; + break; + case 'TW': + $icon = 'https://www.paypalobjects.com/webstatic/en_TW/mktg/logos/AM_mc_vs_dc_ae.jpg'; + break; + case 'TH': + $icon = 'https://www.paypalobjects.com/webstatic/en_TH/mktg/Logos/AM_mc_vs_dc_ae.jpg'; + break; + case 'JP': + $icon = 'https://www.paypal.com/ja_JP/JP/i/bnr/horizontal_solution_4_jcb.gif'; + break; + case 'IN': + $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo/AM_mc_vs_dc_ae.jpg'; + break; + default: + $icon = WC_HTTPS::force_https_url( WC()->plugin_url() . '/includes/gateways/paypal/assets/images/paypal.png' ); + break; + } + return apply_filters( 'woocommerce_paypal_icon', $icon ); + } + + /** + * Check if this gateway is available in the user's country based on currency. + * + * @return bool + */ + public function is_valid_for_use() { + return in_array( + get_woocommerce_currency(), + apply_filters( + 'woocommerce_paypal_supported_currencies', + array( 'AUD', 'BRL', 'CAD', 'MXN', 'NZD', 'HKD', 'SGD', 'USD', 'EUR', 'JPY', 'TRY', 'NOK', 'CZK', 'DKK', 'HUF', 'ILS', 'MYR', 'PHP', 'PLN', 'SEK', 'CHF', 'TWD', 'THB', 'GBP', 'RMB', 'RUB', 'INR' ) + ), + true + ); + } + + /** + * Admin Panel Options. + * - Options for bits like 'title' and availability on a country-by-country basis. + * + * @since 1.0.0 + */ + public function admin_options() { + if ( $this->is_valid_for_use() ) { + parent::admin_options(); + } else { + ?> +
    +

    + : +

    +
    + form_fields = include __DIR__ . '/includes/settings-paypal.php'; + } + + /** + * Get the transaction URL. + * + * @param WC_Order $order Order object. + * @return string + */ + public function get_transaction_url( $order ) { + if ( $this->testmode ) { + $this->view_transaction_url = 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s'; + } else { + $this->view_transaction_url = 'https://www.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s'; + } + return parent::get_transaction_url( $order ); + } + + /** + * Process the payment and return the result. + * + * @param int $order_id Order ID. + * @return array + */ + public function process_payment( $order_id ) { + include_once dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-request.php'; + + $order = wc_get_order( $order_id ); + $paypal_request = new WC_Gateway_Paypal_Request( $this ); + + return array( + 'result' => 'success', + 'redirect' => $paypal_request->get_request_url( $order, $this->testmode ), + ); + } + + /** + * Can the order be refunded via PayPal? + * + * @param WC_Order $order Order object. + * @return bool + */ + public function can_refund_order( $order ) { + $has_api_creds = false; + + if ( $this->testmode ) { + $has_api_creds = $this->get_option( 'sandbox_api_username' ) && $this->get_option( 'sandbox_api_password' ) && $this->get_option( 'sandbox_api_signature' ); + } else { + $has_api_creds = $this->get_option( 'api_username' ) && $this->get_option( 'api_password' ) && $this->get_option( 'api_signature' ); + } + + return $order && $order->get_transaction_id() && $has_api_creds; + } + + /** + * Init the API class and set the username/password etc. + */ + protected function init_api() { + include_once dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-api-handler.php'; + + WC_Gateway_Paypal_API_Handler::$api_username = $this->testmode ? $this->get_option( 'sandbox_api_username' ) : $this->get_option( 'api_username' ); + WC_Gateway_Paypal_API_Handler::$api_password = $this->testmode ? $this->get_option( 'sandbox_api_password' ) : $this->get_option( 'api_password' ); + WC_Gateway_Paypal_API_Handler::$api_signature = $this->testmode ? $this->get_option( 'sandbox_api_signature' ) : $this->get_option( 'api_signature' ); + WC_Gateway_Paypal_API_Handler::$sandbox = $this->testmode; + } + + /** + * Process a refund if supported. + * + * @param int $order_id Order ID. + * @param float $amount Refund amount. + * @param string $reason Refund reason. + * @return bool|WP_Error + */ + public function process_refund( $order_id, $amount = null, $reason = '' ) { + $order = wc_get_order( $order_id ); + + if ( ! $this->can_refund_order( $order ) ) { + return new WP_Error( 'error', __( 'Refund failed.', 'woocommerce' ) ); + } + + $this->init_api(); + + $result = WC_Gateway_Paypal_API_Handler::refund_transaction( $order, $amount, $reason ); + + if ( is_wp_error( $result ) ) { + $this->log( 'Refund Failed: ' . $result->get_error_message(), 'error' ); + return new WP_Error( 'error', $result->get_error_message() ); + } + + $this->log( 'Refund Result: ' . wc_print_r( $result, true ) ); + + switch ( strtolower( $result->ACK ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + case 'success': + case 'successwithwarning': + $order->add_order_note( + /* translators: 1: Refund amount, 2: Refund ID */ + sprintf( __( 'Refunded %1$s - Refund ID: %2$s', 'woocommerce' ), $result->GROSSREFUNDAMT, $result->REFUNDTRANSACTIONID ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + ); + return true; + } + + return isset( $result->L_LONGMESSAGE0 ) ? new WP_Error( 'error', $result->L_LONGMESSAGE0 ) : false; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + } + + /** + * Capture payment when the order is changed from on-hold to complete or processing + * + * @param int $order_id Order ID. + */ + public function capture_payment( $order_id ) { + $order = wc_get_order( $order_id ); + + if ( 'paypal' === $order->get_payment_method() && 'pending' === $order->get_meta( '_paypal_status', true ) && $order->get_transaction_id() ) { + $this->init_api(); + $result = WC_Gateway_Paypal_API_Handler::do_capture( $order ); + + if ( is_wp_error( $result ) ) { + $this->log( 'Capture Failed: ' . $result->get_error_message(), 'error' ); + /* translators: %s: Paypal gateway error message */ + $order->add_order_note( sprintf( __( 'Payment could not be captured: %s', 'woocommerce' ), $result->get_error_message() ) ); + return; + } + + $this->log( 'Capture Result: ' . wc_print_r( $result, true ) ); + + // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + if ( ! empty( $result->PAYMENTSTATUS ) ) { + switch ( $result->PAYMENTSTATUS ) { + case 'Completed': + /* translators: 1: Amount, 2: Authorization ID, 3: Transaction ID */ + $order->add_order_note( sprintf( __( 'Payment of %1$s was captured - Auth ID: %2$s, Transaction ID: %3$s', 'woocommerce' ), $result->AMT, $result->AUTHORIZATIONID, $result->TRANSACTIONID ) ); + update_post_meta( $order->get_id(), '_paypal_status', $result->PAYMENTSTATUS ); + update_post_meta( $order->get_id(), '_transaction_id', $result->TRANSACTIONID ); + break; + default: + /* translators: 1: Authorization ID, 2: Payment status */ + $order->add_order_note( sprintf( __( 'Payment could not be captured - Auth ID: %1$s, Status: %2$s', 'woocommerce' ), $result->AUTHORIZATIONID, $result->PAYMENTSTATUS ) ); + break; + } + } + // phpcs:enable + } + } + + /** + * Load admin scripts. + * + * @since 3.3.0 + */ + public function admin_scripts() { + $screen = get_current_screen(); + $screen_id = $screen ? $screen->id : ''; + + if ( 'woocommerce_page_wc-settings' !== $screen_id ) { + return; + } + + $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; + $version = Constants::get_constant( 'WC_VERSION' ); + + wp_enqueue_script( 'woocommerce_paypal_admin', WC()->plugin_url() . '/includes/gateways/paypal/assets/js/paypal-admin' . $suffix . '.js', array(), $version, true ); + } + + /** + * Custom PayPal order received text. + * + * @since 3.9.0 + * @param string $text Default text. + * @param WC_Order $order Order data. + * @return string + */ + public function order_received_text( $text, $order ) { + if ( $order && $this->id === $order->get_payment_method() ) { + return esc_html__( 'Thank you for your payment. Your transaction has been completed, and a receipt for your purchase has been emailed to you. Log into your PayPal account to view transaction details.', 'woocommerce' ); + } + + return $text; + } + + /** + * Determines whether PayPal Standard should be loaded or not. + * + * By default PayPal Standard isn't loaded on new installs or on existing sites which haven't set up the gateway. + * + * @since 5.5.0 + * + * @return bool Whether PayPal Standard should be loaded. + */ + public function should_load() { + $option_key = '_should_load'; + $should_load = $this->get_option( $option_key ); + + if ( '' === $should_load ) { + + // New installs without PayPal Standard enabled don't load it. + if ( 'no' === $this->enabled && WC_Install::is_new_install() ) { + $should_load = false; + } else { + $should_load = true; + } + + $this->update_option( $option_key, wc_bool_to_string( $should_load ) ); + } else { + $should_load = wc_string_to_bool( $should_load ); + } + + /** + * Allow third-parties to filter whether PayPal Standard should be loaded or not. + * + * @since 5.5.0 + * + * @param bool $should_load Whether PayPal Standard should be loaded. + * @param WC_Gateway_Paypal $this The WC_Gateway_Paypal instance. + */ + return apply_filters( 'woocommerce_should_load_paypal_standard', $should_load, $this ); + } +} diff --git a/includes/gateways/paypal/includes/class-wc-gateway-paypal-api-handler.php b/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-api-handler.php similarity index 100% rename from includes/gateways/paypal/includes/class-wc-gateway-paypal-api-handler.php rename to plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-api-handler.php diff --git a/includes/gateways/paypal/includes/class-wc-gateway-paypal-ipn-handler.php b/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-ipn-handler.php similarity index 100% rename from includes/gateways/paypal/includes/class-wc-gateway-paypal-ipn-handler.php rename to plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-ipn-handler.php diff --git a/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-pdt-handler.php b/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-pdt-handler.php new file mode 100644 index 00000000000..b356f66a7de --- /dev/null +++ b/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-pdt-handler.php @@ -0,0 +1,189 @@ +identity_token = $identity_token; + $this->sandbox = $sandbox; + } + + /** + * Set receiver email to enable more strict validation. + * + * @param string $receiver_email Email to receive PDT notification from. + */ + public function set_receiver_email( $receiver_email = '' ) { + $this->receiver_email = $receiver_email; + } + + /** + * Validate a PDT transaction to ensure its authentic. + * + * @param string $transaction TX ID. + * @return bool|array False or result array if successful and valid. + */ + protected function validate_transaction( $transaction ) { + $pdt = array( + 'body' => array( + 'cmd' => '_notify-synch', + 'tx' => $transaction, + 'at' => $this->identity_token, + ), + 'timeout' => 60, + 'httpversion' => '1.1', + 'user-agent' => 'WooCommerce/' . Constants::get_constant( 'WC_VERSION' ), + ); + + // Post back to get a response. + $response = wp_safe_remote_post( $this->sandbox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr', $pdt ); + + if ( is_wp_error( $response ) || strpos( $response['body'], 'SUCCESS' ) !== 0 ) { + return false; + } + + // Parse transaction result data. + $transaction_result = array_map( 'wc_clean', array_map( 'urldecode', explode( "\n", $response['body'] ) ) ); + $transaction_results = array(); + + foreach ( $transaction_result as $line ) { + $line = explode( '=', $line ); + $transaction_results[ $line[0] ] = isset( $line[1] ) ? $line[1] : ''; + } + + if ( ! empty( $transaction_results['charset'] ) && function_exists( 'iconv' ) ) { + foreach ( $transaction_results as $key => $value ) { + $transaction_results[ $key ] = iconv( $transaction_results['charset'], 'utf-8', $value ); + } + } + + return $transaction_results; + } + + /** + * Check Response for PDT, taking the order id from the request. + * + * @deprecated 6.4 Use check_response_for_order instead. + */ + public function check_response() { + global $wp; + $order_id = apply_filters( 'woocommerce_thankyou_order_id', absint( $wp->query_vars['order-received'] ) ); + + $this->check_response_for_order( $order_id ); + } + + /** + * Check Response for PDT. + * + * @since 6.4 + * + * @param mixed $wc_order_id The order id to check the response against. + */ + public function check_response_for_order( $wc_order_id ) { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( empty( $_REQUEST['tx'] ) ) { + return; + } + + $wc_order = wc_get_order( $wc_order_id ); + if ( ! $wc_order->needs_payment() ) { + return; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $transaction = wc_clean( wp_unslash( $_REQUEST['tx'] ) ); + $transaction_result = $this->validate_transaction( $transaction ); + + if ( $transaction_result ) { + $status = strtolower( $transaction_result['payment_status'] ); + $amount = isset( $transaction_result['mc_gross'] ) ? $transaction_result['mc_gross'] : 0; + $order = $this->get_paypal_order( $transaction_result['custom'] ); + + if ( ! $order ) { + // No valid WC order found on tx data. + return; + } + + if ( $wc_order->get_id() !== $order->get_id() ) { + /* translators: 1: order ID, 2: order ID. */ + WC_Gateway_Paypal::log( sprintf( __( 'Received PDT notification for order %1$d on endpoint for order %2$d.', 'woocommerce' ), $order->get_id(), $wc_order_id ), 'error' ); + return; + } + + if ( 0 !== strcasecmp( trim( $transaction_result['receiver_email'] ), trim( $this->receiver_email ) ) ) { + /* translators: 1: email address, 2: order ID . */ + WC_Gateway_Paypal::log( sprintf( __( 'Received PDT notification for another account: %1$s. Order ID: %2$d.', 'woocommerce' ), $transaction_result['receiver_email'], $order->get_id() ), 'error' ); + return; + } + + // We have a valid response from PayPal. + WC_Gateway_Paypal::log( 'PDT Transaction Status: ' . wc_print_r( $status, true ) ); + + $order->add_meta_data( '_paypal_status', $status ); + $order->set_transaction_id( $transaction ); + + if ( 'completed' === $status ) { + if ( number_format( $order->get_total(), 2, '.', '' ) !== number_format( $amount, 2, '.', '' ) ) { + WC_Gateway_Paypal::log( 'Payment error: Amounts do not match (amt ' . $amount . ')', 'error' ); + /* translators: 1: Payment amount */ + $this->payment_on_hold( $order, sprintf( __( 'Validation error: PayPal amounts do not match (amt %s).', 'woocommerce' ), $amount ) ); + } else { + // Log paypal transaction fee and payment type. + if ( ! empty( $transaction_result['mc_fee'] ) ) { + $order->add_meta_data( 'PayPal Transaction Fee', wc_clean( $transaction_result['mc_fee'] ) ); + } + if ( ! empty( $transaction_result['payment_type'] ) ) { + $order->add_meta_data( 'Payment type', wc_clean( $transaction_result['payment_type'] ) ); + } + + $this->payment_complete( $order, $transaction, __( 'PDT payment completed', 'woocommerce' ) ); + } + } else { + if ( 'authorization' === $transaction_result['pending_reason'] ) { + $this->payment_on_hold( $order, __( 'Payment authorized. Change payment status to processing or complete to capture funds.', 'woocommerce' ) ); + } else { + /* translators: 1: Pending reason */ + $this->payment_on_hold( $order, sprintf( __( 'Payment pending (%s).', 'woocommerce' ), $transaction_result['pending_reason'] ) ); + } + } + } else { + WC_Gateway_Paypal::log( 'Received invalid response from PayPal PDT' ); + } + } +} diff --git a/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php b/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php similarity index 100% rename from includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php rename to plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php diff --git a/includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php b/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php similarity index 100% rename from includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php rename to plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php diff --git a/plugins/woocommerce/includes/gateways/paypal/includes/settings-paypal.php b/plugins/woocommerce/includes/gateways/paypal/includes/settings-paypal.php new file mode 100644 index 00000000000..53729429e8e --- /dev/null +++ b/plugins/woocommerce/includes/gateways/paypal/includes/settings-paypal.php @@ -0,0 +1,178 @@ + array( + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable PayPal Standard', 'woocommerce' ), + 'default' => 'no', + ), + 'title' => array( + 'title' => __( 'Title', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), + 'default' => __( 'PayPal', 'woocommerce' ), + 'desc_tip' => true, + ), + 'description' => array( + 'title' => __( 'Description', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce' ), + 'default' => __( "Pay via PayPal; you can pay with your credit card if you don't have a PayPal account.", 'woocommerce' ), + ), + 'email' => array( + 'title' => __( 'PayPal email', 'woocommerce' ), + 'type' => 'email', + 'description' => __( 'Please enter your PayPal email address; this is needed in order to take payment.', 'woocommerce' ), + 'default' => get_option( 'admin_email' ), + 'desc_tip' => true, + 'placeholder' => 'you@youremail.com', + ), + 'advanced' => array( + 'title' => __( 'Advanced options', 'woocommerce' ), + 'type' => 'title', + 'description' => '', + ), + 'testmode' => array( + 'title' => __( 'PayPal sandbox', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable PayPal sandbox', 'woocommerce' ), + 'default' => 'no', + /* translators: %s: URL */ + 'description' => sprintf( __( 'PayPal sandbox can be used to test payments. Sign up for a developer account.', 'woocommerce' ), 'https://developer.paypal.com/' ), + ), + 'debug' => array( + 'title' => __( 'Debug log', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable logging', 'woocommerce' ), + 'default' => 'no', + /* translators: %s: URL */ + 'description' => sprintf( __( 'Log PayPal events, such as IPN requests, inside %s Note: this may log personal information. We recommend using this for debugging purposes only and deleting the logs when finished.', 'woocommerce' ), '' . WC_Log_Handler_File::get_log_file_path( 'paypal' ) . '' ), + ), + 'ipn_notification' => array( + 'title' => __( 'IPN email notifications', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable IPN email notifications', 'woocommerce' ), + 'default' => 'yes', + 'description' => __( 'Send notifications when an IPN is received from PayPal indicating refunds, chargebacks and cancellations.', 'woocommerce' ), + ), + 'receiver_email' => array( + 'title' => __( 'Receiver email', 'woocommerce' ), + 'type' => 'email', + 'description' => __( 'If your main PayPal email differs from the PayPal email entered above, input your main receiver email for your PayPal account here. This is used to validate IPN requests.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => 'you@youremail.com', + ), + 'identity_token' => array( + 'title' => __( 'PayPal identity token', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Optionally enable "Payment Data Transfer" (Profile > Profile and Settings > My Selling Tools > Website Preferences) and then copy your identity token here. This will allow payments to be verified without the need for PayPal IPN.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => '', + ), + 'invoice_prefix' => array( + 'title' => __( 'Invoice prefix', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Please enter a prefix for your invoice numbers. If you use your PayPal account for multiple stores ensure this prefix is unique as PayPal will not allow orders with the same invoice number.', 'woocommerce' ), + 'default' => 'WC-', + 'desc_tip' => true, + ), + 'send_shipping' => array( + 'title' => __( 'Shipping details', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Send shipping details to PayPal instead of billing.', 'woocommerce' ), + 'description' => __( 'PayPal allows us to send one address. If you are using PayPal for shipping labels you may prefer to send the shipping address rather than billing. Turning this option off may prevent PayPal Seller protection from applying.', 'woocommerce' ), + 'default' => 'yes', + ), + 'address_override' => array( + 'title' => __( 'Address override', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable "address_override" to prevent address information from being changed.', 'woocommerce' ), + 'description' => __( 'PayPal verifies addresses therefore this setting can cause errors (we recommend keeping it disabled).', 'woocommerce' ), + 'default' => 'no', + ), + 'paymentaction' => array( + 'title' => __( 'Payment action', 'woocommerce' ), + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'description' => __( 'Choose whether you wish to capture funds immediately or authorize payment only.', 'woocommerce' ), + 'default' => 'sale', + 'desc_tip' => true, + 'options' => array( + 'sale' => __( 'Capture', 'woocommerce' ), + 'authorization' => __( 'Authorize', 'woocommerce' ), + ), + ), + 'image_url' => array( + 'title' => __( 'Image url', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Optionally enter the URL to a 150x50px image displayed as your logo in the upper left corner of the PayPal checkout pages.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => __( 'Optional', 'woocommerce' ), + ), + 'api_details' => array( + 'title' => __( 'API credentials', 'woocommerce' ), + 'type' => 'title', + /* translators: %s: URL */ + 'description' => sprintf( __( 'Enter your PayPal API credentials to process refunds via PayPal. Learn how to access your PayPal API Credentials.', 'woocommerce' ), 'https://developer.paypal.com/webapps/developer/docs/classic/api/apiCredentials/#create-an-api-signature' ), + ), + 'api_username' => array( + 'title' => __( 'Live API username', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => __( 'Optional', 'woocommerce' ), + ), + 'api_password' => array( + 'title' => __( 'Live API password', 'woocommerce' ), + 'type' => 'password', + 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => __( 'Optional', 'woocommerce' ), + ), + 'api_signature' => array( + 'title' => __( 'Live API signature', 'woocommerce' ), + 'type' => 'password', + 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => __( 'Optional', 'woocommerce' ), + ), + 'sandbox_api_username' => array( + 'title' => __( 'Sandbox API username', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => __( 'Optional', 'woocommerce' ), + ), + 'sandbox_api_password' => array( + 'title' => __( 'Sandbox API password', 'woocommerce' ), + 'type' => 'password', + 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => __( 'Optional', 'woocommerce' ), + ), + 'sandbox_api_signature' => array( + 'title' => __( 'Sandbox API signature', 'woocommerce' ), + 'type' => 'password', + 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => __( 'Optional', 'woocommerce' ), + ), +); diff --git a/includes/import/abstract-wc-product-importer.php b/plugins/woocommerce/includes/import/abstract-wc-product-importer.php similarity index 100% rename from includes/import/abstract-wc-product-importer.php rename to plugins/woocommerce/includes/import/abstract-wc-product-importer.php diff --git a/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php b/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php new file mode 100644 index 00000000000..31a97d379ea --- /dev/null +++ b/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php @@ -0,0 +1,1135 @@ + 0, // File pointer start. + 'end_pos' => -1, // File pointer end. + 'lines' => -1, // Max lines to read. + 'mapping' => array(), // Column mapping. csv_heading => schema_heading. + 'parse' => false, // Whether to sanitize and format data. + 'update_existing' => false, // Whether to update existing items. + 'delimiter' => ',', // CSV delimiter. + 'prevent_timeouts' => true, // Check memory and time usage and abort if reaching limit. + 'enclosure' => '"', // The character used to wrap text in the CSV. + 'escape' => "\0", // PHP uses '\' as the default escape character. This is not RFC-4180 compliant. This disables the escape character. + ); + + $this->params = wp_parse_args( $params, $default_args ); + $this->file = $file; + + if ( isset( $this->params['mapping']['from'], $this->params['mapping']['to'] ) ) { + $this->params['mapping'] = array_combine( $this->params['mapping']['from'], $this->params['mapping']['to'] ); + } + + // Import mappings for CSV data. + include_once dirname( dirname( __FILE__ ) ) . '/admin/importers/mappings/mappings.php'; + + $this->read_file(); + } + + /** + * Read file. + */ + protected function read_file() { + if ( ! WC_Product_CSV_Importer_Controller::is_file_valid_csv( $this->file ) ) { + wp_die( esc_html__( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); + } + + $handle = fopen( $this->file, 'r' ); // @codingStandardsIgnoreLine. + + if ( false !== $handle ) { + $this->raw_keys = version_compare( PHP_VERSION, '5.3', '>=' ) ? array_map( 'trim', fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'], $this->params['escape'] ) ) : array_map( 'trim', fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'] ) ); // @codingStandardsIgnoreLine + + // Remove BOM signature from the first item. + if ( isset( $this->raw_keys[0] ) ) { + $this->raw_keys[0] = $this->remove_utf8_bom( $this->raw_keys[0] ); + } + + if ( 0 !== $this->params['start_pos'] ) { + fseek( $handle, (int) $this->params['start_pos'] ); + } + + while ( 1 ) { + $row = version_compare( PHP_VERSION, '5.3', '>=' ) ? fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'], $this->params['escape'] ) : fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'] ); // @codingStandardsIgnoreLine + + if ( false !== $row ) { + $this->raw_data[] = $row; + $this->file_positions[ count( $this->raw_data ) ] = ftell( $handle ); + + if ( ( $this->params['end_pos'] > 0 && ftell( $handle ) >= $this->params['end_pos'] ) || 0 === --$this->params['lines'] ) { + break; + } + } else { + break; + } + } + + $this->file_position = ftell( $handle ); + } + + if ( ! empty( $this->params['mapping'] ) ) { + $this->set_mapped_keys(); + } + + if ( $this->params['parse'] ) { + $this->set_parsed_data(); + } + } + + /** + * Remove UTF-8 BOM signature. + * + * @param string $string String to handle. + * + * @return string + */ + protected function remove_utf8_bom( $string ) { + if ( 'efbbbf' === substr( bin2hex( $string ), 0, 6 ) ) { + $string = substr( $string, 3 ); + } + + return $string; + } + + /** + * Set file mapped keys. + */ + protected function set_mapped_keys() { + $mapping = $this->params['mapping']; + + foreach ( $this->raw_keys as $key ) { + $this->mapped_keys[] = isset( $mapping[ $key ] ) ? $mapping[ $key ] : $key; + } + } + + /** + * Parse relative field and return product ID. + * + * Handles `id:xx` and SKUs. + * + * If mapping to an id: and the product ID does not exist, this link is not + * valid. + * + * If mapping to a SKU and the product ID does not exist, a temporary object + * will be created so it can be updated later. + * + * @param string $value Field value. + * + * @return int|string + */ + public function parse_relative_field( $value ) { + global $wpdb; + + if ( empty( $value ) ) { + return ''; + } + + // IDs are prefixed with id:. + if ( preg_match( '/^id:(\d+)$/', $value, $matches ) ) { + $id = intval( $matches[1] ); + + // If original_id is found, use that instead of the given ID since a new placeholder must have been created already. + $original_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_original_id' AND meta_value = %s;", $id ) ); // WPCS: db call ok, cache ok. + + if ( $original_id ) { + return absint( $original_id ); + } + + // See if the given ID maps to a valid product allready. + $existing_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type IN ( 'product', 'product_variation' ) AND ID = %d;", $id ) ); // WPCS: db call ok, cache ok. + + if ( $existing_id ) { + return absint( $existing_id ); + } + + // If we're not updating existing posts, we may need a placeholder product to map to. + if ( ! $this->params['update_existing'] ) { + $product = wc_get_product_object( 'simple' ); + $product->set_name( 'Import placeholder for ' . $id ); + $product->set_status( 'importing' ); + $product->add_meta_data( '_original_id', $id, true ); + $id = $product->save(); + } + + return $id; + } + + $id = wc_get_product_id_by_sku( $value ); + + if ( $id ) { + return $id; + } + + try { + $product = wc_get_product_object( 'simple' ); + $product->set_name( 'Import placeholder for ' . $value ); + $product->set_status( 'importing' ); + $product->set_sku( $value ); + $id = $product->save(); + + if ( $id && ! is_wp_error( $id ) ) { + return $id; + } + } catch ( Exception $e ) { + return ''; + } + + return ''; + } + + /** + * Parse the ID field. + * + * If we're not doing an update, create a placeholder product so mapping works + * for rows following this one. + * + * @param string $value Field value. + * + * @return int + */ + public function parse_id_field( $value ) { + global $wpdb; + + $id = absint( $value ); + + if ( ! $id ) { + return 0; + } + + // See if this maps to an ID placeholder already. + $original_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_original_id' AND meta_value = %s;", $id ) ); // WPCS: db call ok, cache ok. + + if ( $original_id ) { + return absint( $original_id ); + } + + // Not updating? Make sure we have a new placeholder for this ID. + if ( ! $this->params['update_existing'] ) { + $mapped_keys = $this->get_mapped_keys(); + $sku_column_index = absint( array_search( 'sku', $mapped_keys, true ) ); + $row_sku = isset( $this->raw_data[ $this->parsing_raw_data_index ][ $sku_column_index ] ) ? $this->raw_data[ $this->parsing_raw_data_index ][ $sku_column_index ] : ''; + $id_from_sku = $row_sku ? wc_get_product_id_by_sku( $row_sku ) : ''; + + // If row has a SKU, make sure placeholder was not made already. + if ( $id_from_sku ) { + return $id_from_sku; + } + + $product = wc_get_product_object( 'simple' ); + $product->set_name( 'Import placeholder for ' . $id ); + $product->set_status( 'importing' ); + $product->add_meta_data( '_original_id', $id, true ); + + // If row has a SKU, make sure placeholder has it too. + if ( $row_sku ) { + $product->set_sku( $row_sku ); + } + $id = $product->save(); + } + + return $id && ! is_wp_error( $id ) ? $id : 0; + } + + /** + * Parse relative comma-delineated field and return product ID. + * + * @param string $value Field value. + * + * @return array + */ + public function parse_relative_comma_field( $value ) { + if ( empty( $value ) ) { + return array(); + } + + return array_filter( array_map( array( $this, 'parse_relative_field' ), $this->explode_values( $value ) ) ); + } + + /** + * Parse a comma-delineated field from a CSV. + * + * @param string $value Field value. + * + * @return array + */ + public function parse_comma_field( $value ) { + if ( empty( $value ) && '0' !== $value ) { + return array(); + } + + $value = $this->unescape_data( $value ); + return array_map( 'wc_clean', $this->explode_values( $value ) ); + } + + /** + * Parse a field that is generally '1' or '0' but can be something else. + * + * @param string $value Field value. + * + * @return bool|string + */ + public function parse_bool_field( $value ) { + if ( '0' === $value ) { + return false; + } + + if ( '1' === $value ) { + return true; + } + + // Don't return explicit true or false for empty fields or values like 'notify'. + return wc_clean( $value ); + } + + /** + * Parse a float value field. + * + * @param string $value Field value. + * + * @return float|string + */ + public function parse_float_field( $value ) { + if ( '' === $value ) { + return $value; + } + + // Remove the ' prepended to fields that start with - if needed. + $value = $this->unescape_data( $value ); + + return floatval( $value ); + } + + /** + * Parse the stock qty field. + * + * @param string $value Field value. + * + * @return float|string + */ + public function parse_stock_quantity_field( $value ) { + if ( '' === $value ) { + return $value; + } + + // Remove the ' prepended to fields that start with - if needed. + $value = $this->unescape_data( $value ); + + return wc_stock_amount( $value ); + } + + /** + * Parse the tax status field. + * + * @param string $value Field value. + * + * @return string + */ + public function parse_tax_status_field( $value ) { + if ( '' === $value ) { + return $value; + } + + // Remove the ' prepended to fields that start with - if needed. + $value = $this->unescape_data( $value ); + + if ( 'true' === strtolower( $value ) || 'false' === strtolower( $value ) ) { + $value = wc_string_to_bool( $value ) ? 'taxable' : 'none'; + } + + return wc_clean( $value ); + } + + /** + * Parse a category field from a CSV. + * Categories are separated by commas and subcategories are "parent > subcategory". + * + * @param string $value Field value. + * + * @return array of arrays with "parent" and "name" keys. + */ + public function parse_categories_field( $value ) { + if ( empty( $value ) ) { + return array(); + } + + $row_terms = $this->explode_values( $value ); + $categories = array(); + + foreach ( $row_terms as $row_term ) { + $parent = null; + $_terms = array_map( 'trim', explode( '>', $row_term ) ); + $total = count( $_terms ); + + foreach ( $_terms as $index => $_term ) { + // Don't allow users without capabilities to create new categories. + if ( ! current_user_can( 'manage_product_terms' ) ) { + break; + } + + $term = wp_insert_term( $_term, 'product_cat', array( 'parent' => intval( $parent ) ) ); + + if ( is_wp_error( $term ) ) { + if ( $term->get_error_code() === 'term_exists' ) { + // When term exists, error data should contain existing term id. + $term_id = $term->get_error_data(); + } else { + break; // We cannot continue on any other error. + } + } else { + // New term. + $term_id = $term['term_id']; + } + + // Only requires assign the last category. + if ( ( 1 + $index ) === $total ) { + $categories[] = $term_id; + } else { + // Store parent to be able to insert or query categories based in parent ID. + $parent = $term_id; + } + } + } + + return $categories; + } + + /** + * Parse a tag field from a CSV. + * + * @param string $value Field value. + * + * @return array + */ + public function parse_tags_field( $value ) { + if ( empty( $value ) ) { + return array(); + } + + $value = $this->unescape_data( $value ); + $names = $this->explode_values( $value ); + $tags = array(); + + foreach ( $names as $name ) { + $term = get_term_by( 'name', $name, 'product_tag' ); + + if ( ! $term || is_wp_error( $term ) ) { + $term = (object) wp_insert_term( $name, 'product_tag' ); + } + + if ( ! is_wp_error( $term ) ) { + $tags[] = $term->term_id; + } + } + + return $tags; + } + + /** + * Parse a tag field from a CSV with space separators. + * + * @param string $value Field value. + * + * @return array + */ + public function parse_tags_spaces_field( $value ) { + if ( empty( $value ) ) { + return array(); + } + + $value = $this->unescape_data( $value ); + $names = $this->explode_values( $value, ' ' ); + $tags = array(); + + foreach ( $names as $name ) { + $term = get_term_by( 'name', $name, 'product_tag' ); + + if ( ! $term || is_wp_error( $term ) ) { + $term = (object) wp_insert_term( $name, 'product_tag' ); + } + + if ( ! is_wp_error( $term ) ) { + $tags[] = $term->term_id; + } + } + + return $tags; + } + + /** + * Parse a shipping class field from a CSV. + * + * @param string $value Field value. + * + * @return int + */ + public function parse_shipping_class_field( $value ) { + if ( empty( $value ) ) { + return 0; + } + + $term = get_term_by( 'name', $value, 'product_shipping_class' ); + + if ( ! $term || is_wp_error( $term ) ) { + $term = (object) wp_insert_term( $value, 'product_shipping_class' ); + } + + if ( is_wp_error( $term ) ) { + return 0; + } + + return $term->term_id; + } + + /** + * Parse images list from a CSV. Images can be filenames or URLs. + * + * @param string $value Field value. + * + * @return array + */ + public function parse_images_field( $value ) { + if ( empty( $value ) ) { + return array(); + } + + $images = array(); + $separator = apply_filters( 'woocommerce_product_import_image_separator', ',' ); + + foreach ( $this->explode_values( $value, $separator ) as $image ) { + if ( stristr( $image, '://' ) ) { + $images[] = esc_url_raw( $image ); + } else { + $images[] = sanitize_file_name( $image ); + } + } + + return $images; + } + + /** + * Parse dates from a CSV. + * Dates requires the format YYYY-MM-DD and time is optional. + * + * @param string $value Field value. + * + * @return string|null + */ + public function parse_date_field( $value ) { + if ( empty( $value ) ) { + return null; + } + + if ( preg_match( '/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])([ 01-9:]*)$/', $value ) ) { + // Don't include the time if the field had time in it. + return current( explode( ' ', $value ) ); + } + + return null; + } + + /** + * Parse backorders from a CSV. + * + * @param string $value Field value. + * + * @return string + */ + public function parse_backorders_field( $value ) { + if ( empty( $value ) ) { + return 'no'; + } + + $value = $this->parse_bool_field( $value ); + + if ( 'notify' === $value ) { + return 'notify'; + } elseif ( is_bool( $value ) ) { + return $value ? 'yes' : 'no'; + } + + return 'no'; + } + + /** + * Just skip current field. + * + * By default is applied wc_clean() to all not listed fields + * in self::get_formatting_callback(), use this method to skip any formatting. + * + * @param string $value Field value. + * + * @return string + */ + public function parse_skip_field( $value ) { + return $value; + } + + /** + * Parse download file urls, we should allow shortcodes here. + * + * Allow shortcodes if present, otherwise esc_url the value. + * + * @param string $value Field value. + * + * @return string + */ + public function parse_download_file_field( $value ) { + // Absolute file paths. + if ( 0 === strpos( $value, 'http' ) ) { + return esc_url_raw( $value ); + } + // Relative and shortcode paths. + return wc_clean( $value ); + } + + /** + * Parse an int value field + * + * @param int $value field value. + * + * @return int + */ + public function parse_int_field( $value ) { + // Remove the ' prepended to fields that start with - if needed. + $value = $this->unescape_data( $value ); + + return intval( $value ); + } + + /** + * Parse a description value field + * + * @param string $description field value. + * + * @return string + */ + public function parse_description_field( $description ) { + $parts = explode( "\\\\n", $description ); + foreach ( $parts as $key => $part ) { + $parts[ $key ] = str_replace( '\n', "\n", $part ); + } + + return implode( '\\\n', $parts ); + } + + /** + * Parse the published field. 1 is published, 0 is private, -1 is draft. + * Alternatively, 'true' can be used for published and 'false' for draft. + * + * @param string $value Field value. + * + * @return float|string + */ + public function parse_published_field( $value ) { + if ( '' === $value ) { + return $value; + } + + // Remove the ' prepended to fields that start with - if needed. + $value = $this->unescape_data( $value ); + + if ( 'true' === strtolower( $value ) || 'false' === strtolower( $value ) ) { + return wc_string_to_bool( $value ) ? 1 : -1; + } + + return floatval( $value ); + } + + /** + * Deprecated get formatting callback method. + * + * @deprecated 4.3.0 + * @return array + */ + protected function get_formating_callback() { + return $this->get_formatting_callback(); + } + + /** + * Get formatting callback. + * + * @since 4.3.0 + * @return array + */ + protected function get_formatting_callback() { + + /** + * Columns not mentioned here will get parsed with 'wc_clean'. + * column_name => callback. + */ + $data_formatting = array( + 'id' => array( $this, 'parse_id_field' ), + 'type' => array( $this, 'parse_comma_field' ), + 'published' => array( $this, 'parse_published_field' ), + 'featured' => array( $this, 'parse_bool_field' ), + 'date_on_sale_from' => array( $this, 'parse_date_field' ), + 'date_on_sale_to' => array( $this, 'parse_date_field' ), + 'name' => array( $this, 'parse_skip_field' ), + 'short_description' => array( $this, 'parse_description_field' ), + 'description' => array( $this, 'parse_description_field' ), + 'manage_stock' => array( $this, 'parse_bool_field' ), + 'low_stock_amount' => array( $this, 'parse_stock_quantity_field' ), + 'backorders' => array( $this, 'parse_backorders_field' ), + 'stock_status' => array( $this, 'parse_bool_field' ), + 'sold_individually' => array( $this, 'parse_bool_field' ), + 'width' => array( $this, 'parse_float_field' ), + 'length' => array( $this, 'parse_float_field' ), + 'height' => array( $this, 'parse_float_field' ), + 'weight' => array( $this, 'parse_float_field' ), + 'reviews_allowed' => array( $this, 'parse_bool_field' ), + 'purchase_note' => 'wp_filter_post_kses', + 'price' => 'wc_format_decimal', + 'regular_price' => 'wc_format_decimal', + 'stock_quantity' => array( $this, 'parse_stock_quantity_field' ), + 'category_ids' => array( $this, 'parse_categories_field' ), + 'tag_ids' => array( $this, 'parse_tags_field' ), + 'tag_ids_spaces' => array( $this, 'parse_tags_spaces_field' ), + 'shipping_class_id' => array( $this, 'parse_shipping_class_field' ), + 'images' => array( $this, 'parse_images_field' ), + 'parent_id' => array( $this, 'parse_relative_field' ), + 'grouped_products' => array( $this, 'parse_relative_comma_field' ), + 'upsell_ids' => array( $this, 'parse_relative_comma_field' ), + 'cross_sell_ids' => array( $this, 'parse_relative_comma_field' ), + 'download_limit' => array( $this, 'parse_int_field' ), + 'download_expiry' => array( $this, 'parse_int_field' ), + 'product_url' => 'esc_url_raw', + 'menu_order' => 'intval', + 'tax_status' => array( $this, 'parse_tax_status_field' ), + ); + + /** + * Match special column names. + */ + $regex_match_data_formatting = array( + '/attributes:value*/' => array( $this, 'parse_comma_field' ), + '/attributes:visible*/' => array( $this, 'parse_bool_field' ), + '/attributes:taxonomy*/' => array( $this, 'parse_bool_field' ), + '/downloads:url*/' => array( $this, 'parse_download_file_field' ), + '/meta:*/' => 'wp_kses_post', // Allow some HTML in meta fields. + ); + + $callbacks = array(); + + // Figure out the parse function for each column. + foreach ( $this->get_mapped_keys() as $index => $heading ) { + $callback = 'wc_clean'; + + if ( isset( $data_formatting[ $heading ] ) ) { + $callback = $data_formatting[ $heading ]; + } else { + foreach ( $regex_match_data_formatting as $regex => $callback ) { + if ( preg_match( $regex, $heading ) ) { + $callback = $callback; + break; + } + } + } + + $callbacks[] = $callback; + } + + return apply_filters( 'woocommerce_product_importer_formatting_callbacks', $callbacks, $this ); + } + + /** + * Check if strings starts with determined word. + * + * @param string $haystack Complete sentence. + * @param string $needle Excerpt. + * + * @return bool + */ + protected function starts_with( $haystack, $needle ) { + return substr( $haystack, 0, strlen( $needle ) ) === $needle; + } + + /** + * Expand special and internal data into the correct formats for the product CRUD. + * + * @param array $data Data to import. + * + * @return array + */ + protected function expand_data( $data ) { + $data = apply_filters( 'woocommerce_product_importer_pre_expand_data', $data ); + + // Images field maps to image and gallery id fields. + if ( isset( $data['images'] ) ) { + $images = $data['images']; + $data['raw_image_id'] = array_shift( $images ); + + if ( ! empty( $images ) ) { + $data['raw_gallery_image_ids'] = $images; + } + unset( $data['images'] ); + } + + // Type, virtual and downloadable are all stored in the same column. + if ( isset( $data['type'] ) ) { + $data['type'] = array_map( 'strtolower', $data['type'] ); + $data['virtual'] = in_array( 'virtual', $data['type'], true ); + $data['downloadable'] = in_array( 'downloadable', $data['type'], true ); + + // Convert type to string. + $data['type'] = current( array_diff( $data['type'], array( 'virtual', 'downloadable' ) ) ); + + if ( ! $data['type'] ) { + $data['type'] = 'simple'; + } + } + + // Status is mapped from a special published field. + if ( isset( $data['published'] ) ) { + $statuses = array( + -1 => 'draft', + 0 => 'private', + 1 => 'publish', + ); + $data['status'] = isset( $statuses[ $data['published'] ] ) ? $statuses[ $data['published'] ] : 'draft'; + + // Fix draft status of variations. + if ( isset( $data['type'] ) && 'variation' === $data['type'] && -1 === $data['published'] ) { + $data['status'] = 'publish'; + } + + unset( $data['published'] ); + } + + if ( isset( $data['stock_quantity'] ) ) { + if ( '' === $data['stock_quantity'] ) { + $data['manage_stock'] = false; + $data['stock_status'] = isset( $data['stock_status'] ) ? $data['stock_status'] : true; + } else { + $data['manage_stock'] = true; + } + } + + // Stock is bool or 'backorder'. + if ( isset( $data['stock_status'] ) ) { + if ( 'backorder' === $data['stock_status'] ) { + $data['stock_status'] = 'onbackorder'; + } else { + $data['stock_status'] = $data['stock_status'] ? 'instock' : 'outofstock'; + } + } + + // Prepare grouped products. + if ( isset( $data['grouped_products'] ) ) { + $data['children'] = $data['grouped_products']; + unset( $data['grouped_products'] ); + } + + // Tag ids. + if ( isset( $data['tag_ids_spaces'] ) ) { + $data['tag_ids'] = $data['tag_ids_spaces']; + unset( $data['tag_ids_spaces'] ); + } + + // Handle special column names which span multiple columns. + $attributes = array(); + $downloads = array(); + $meta_data = array(); + + foreach ( $data as $key => $value ) { + if ( $this->starts_with( $key, 'attributes:name' ) ) { + if ( ! empty( $value ) ) { + $attributes[ str_replace( 'attributes:name', '', $key ) ]['name'] = $value; + } + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'attributes:value' ) ) { + $attributes[ str_replace( 'attributes:value', '', $key ) ]['value'] = $value; + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'attributes:taxonomy' ) ) { + $attributes[ str_replace( 'attributes:taxonomy', '', $key ) ]['taxonomy'] = wc_string_to_bool( $value ); + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'attributes:visible' ) ) { + $attributes[ str_replace( 'attributes:visible', '', $key ) ]['visible'] = wc_string_to_bool( $value ); + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'attributes:default' ) ) { + if ( ! empty( $value ) ) { + $attributes[ str_replace( 'attributes:default', '', $key ) ]['default'] = $value; + } + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'downloads:id' ) ) { + if ( ! empty( $value ) ) { + $downloads[ str_replace( 'downloads:id', '', $key ) ]['id'] = $value; + } + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'downloads:name' ) ) { + if ( ! empty( $value ) ) { + $downloads[ str_replace( 'downloads:name', '', $key ) ]['name'] = $value; + } + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'downloads:url' ) ) { + if ( ! empty( $value ) ) { + $downloads[ str_replace( 'downloads:url', '', $key ) ]['url'] = $value; + } + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'meta:' ) ) { + $meta_data[] = array( + 'key' => str_replace( 'meta:', '', $key ), + 'value' => $value, + ); + unset( $data[ $key ] ); + } + } + + if ( ! empty( $attributes ) ) { + // Remove empty attributes and clear indexes. + foreach ( $attributes as $attribute ) { + if ( empty( $attribute['name'] ) ) { + continue; + } + + $data['raw_attributes'][] = $attribute; + } + } + + if ( ! empty( $downloads ) ) { + $data['downloads'] = array(); + + foreach ( $downloads as $key => $file ) { + if ( empty( $file['url'] ) ) { + continue; + } + + $data['downloads'][] = array( + 'download_id' => isset( $file['id'] ) ? $file['id'] : null, + 'name' => $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['url'] ), + 'file' => $file['url'], + ); + } + } + + if ( ! empty( $meta_data ) ) { + $data['meta_data'] = $meta_data; + } + + return $data; + } + + /** + * Map and format raw data to known fields. + */ + protected function set_parsed_data() { + $parse_functions = $this->get_formatting_callback(); + $mapped_keys = $this->get_mapped_keys(); + $use_mb = function_exists( 'mb_convert_encoding' ); + + // Parse the data. + foreach ( $this->raw_data as $row_index => $row ) { + // Skip empty rows. + if ( ! count( array_filter( $row ) ) ) { + continue; + } + + $this->parsing_raw_data_index = $row_index; + + $data = array(); + + do_action( 'woocommerce_product_importer_before_set_parsed_data', $row, $mapped_keys ); + + foreach ( $row as $id => $value ) { + // Skip ignored columns. + if ( empty( $mapped_keys[ $id ] ) ) { + continue; + } + + // Convert UTF8. + if ( $use_mb ) { + $encoding = mb_detect_encoding( $value, mb_detect_order(), true ); + if ( $encoding ) { + $value = mb_convert_encoding( $value, 'UTF-8', $encoding ); + } else { + $value = mb_convert_encoding( $value, 'UTF-8', 'UTF-8' ); + } + } else { + $value = wp_check_invalid_utf8( $value, true ); + } + + $data[ $mapped_keys[ $id ] ] = call_user_func( $parse_functions[ $id ], $value ); + } + + /** + * Filter product importer parsed data. + * + * @param array $parsed_data Parsed data. + * @param WC_Product_Importer $importer Importer instance. + */ + $this->parsed_data[] = apply_filters( 'woocommerce_product_importer_parsed_data', $this->expand_data( $data ), $this ); + } + } + + /** + * Get a string to identify the row from parsed data. + * + * @param array $parsed_data Parsed data. + * + * @return string + */ + protected function get_row_id( $parsed_data ) { + $id = isset( $parsed_data['id'] ) ? absint( $parsed_data['id'] ) : 0; + $sku = isset( $parsed_data['sku'] ) ? esc_attr( $parsed_data['sku'] ) : ''; + $name = isset( $parsed_data['name'] ) ? esc_attr( $parsed_data['name'] ) : ''; + $row_data = array(); + + if ( $name ) { + $row_data[] = $name; + } + if ( $id ) { + /* translators: %d: product ID */ + $row_data[] = sprintf( __( 'ID %d', 'woocommerce' ), $id ); + } + if ( $sku ) { + /* translators: %s: product SKU */ + $row_data[] = sprintf( __( 'SKU %s', 'woocommerce' ), $sku ); + } + + return implode( ', ', $row_data ); + } + + /** + * Process importer. + * + * Do not import products with IDs or SKUs that already exist if option + * update existing is false, and likewise, if updating products, do not + * process rows which do not exist if an ID/SKU is provided. + * + * @return array + */ + public function import() { + $this->start_time = time(); + $index = 0; + $update_existing = $this->params['update_existing']; + $data = array( + 'imported' => array(), + 'failed' => array(), + 'updated' => array(), + 'skipped' => array(), + ); + + foreach ( $this->parsed_data as $parsed_data_key => $parsed_data ) { + do_action( 'woocommerce_product_import_before_import', $parsed_data ); + + $id = isset( $parsed_data['id'] ) ? absint( $parsed_data['id'] ) : 0; + $sku = isset( $parsed_data['sku'] ) ? $parsed_data['sku'] : ''; + $id_exists = false; + $sku_exists = false; + + if ( $id ) { + $product = wc_get_product( $id ); + $id_exists = $product && 'importing' !== $product->get_status(); + } + + if ( $sku ) { + $id_from_sku = wc_get_product_id_by_sku( $sku ); + $product = $id_from_sku ? wc_get_product( $id_from_sku ) : false; + $sku_exists = $product && 'importing' !== $product->get_status(); + } + + if ( $id_exists && ! $update_existing ) { + $data['skipped'][] = new WP_Error( + 'woocommerce_product_importer_error', + esc_html__( 'A product with this ID already exists.', 'woocommerce' ), + array( + 'id' => $id, + 'row' => $this->get_row_id( $parsed_data ), + ) + ); + continue; + } + + if ( $sku_exists && ! $update_existing ) { + $data['skipped'][] = new WP_Error( + 'woocommerce_product_importer_error', + esc_html__( 'A product with this SKU already exists.', 'woocommerce' ), + array( + 'sku' => esc_attr( $sku ), + 'row' => $this->get_row_id( $parsed_data ), + ) + ); + continue; + } + + if ( $update_existing && ( isset( $parsed_data['id'] ) || isset( $parsed_data['sku'] ) ) && ! $id_exists && ! $sku_exists ) { + $data['skipped'][] = new WP_Error( + 'woocommerce_product_importer_error', + esc_html__( 'No matching product exists to update.', 'woocommerce' ), + array( + 'id' => $id, + 'sku' => esc_attr( $sku ), + 'row' => $this->get_row_id( $parsed_data ), + ) + ); + continue; + } + + $result = $this->process_item( $parsed_data ); + + if ( is_wp_error( $result ) ) { + $result->add_data( array( 'row' => $this->get_row_id( $parsed_data ) ) ); + $data['failed'][] = $result; + } elseif ( $result['updated'] ) { + $data['updated'][] = $result['id']; + } else { + $data['imported'][] = $result['id']; + } + + $index ++; + + if ( $this->params['prevent_timeouts'] && ( $this->time_exceeded() || $this->memory_exceeded() ) ) { + $this->file_position = $this->file_positions[ $index ]; + break; + } + } + + return $data; + } +} diff --git a/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-database-service.php b/plugins/woocommerce/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-database-service.php similarity index 100% rename from includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-database-service.php rename to plugins/woocommerce/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-database-service.php diff --git a/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-geolocation.php b/plugins/woocommerce/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-geolocation.php similarity index 100% rename from includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-geolocation.php rename to plugins/woocommerce/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-geolocation.php diff --git a/includes/integrations/maxmind-geolocation/views/html-admin-options.php b/plugins/woocommerce/includes/integrations/maxmind-geolocation/views/html-admin-options.php similarity index 100% rename from includes/integrations/maxmind-geolocation/views/html-admin-options.php rename to plugins/woocommerce/includes/integrations/maxmind-geolocation/views/html-admin-options.php diff --git a/plugins/woocommerce/includes/interfaces/class-wc-abstract-order-data-store-interface.php b/plugins/woocommerce/includes/interfaces/class-wc-abstract-order-data-store-interface.php new file mode 100644 index 00000000000..6e2ca1f224d --- /dev/null +++ b/plugins/woocommerce/includes/interfaces/class-wc-abstract-order-data-store-interface.php @@ -0,0 +1,50 @@ + [], 'failed' => []] + * + * @return array + */ + public function import(); + + /** + * Get file raw keys. + * + * CSV - Headers. + * XML - Element names. + * JSON - Keys + * + * @return array + */ + public function get_raw_keys(); + + /** + * Get file mapped headers. + * + * @return array + */ + public function get_mapped_keys(); + + /** + * Get raw data. + * + * @return array + */ + public function get_raw_data(); + + /** + * Get parsed data. + * + * @return array + */ + public function get_parsed_data(); + + /** + * Get file pointer position from the last read. + * + * @return int + */ + public function get_file_position(); + + /** + * Get file pointer position as a percentage of file size. + * + * @return int + */ + public function get_percent_complete(); +} diff --git a/plugins/woocommerce/includes/interfaces/class-wc-log-handler-interface.php b/plugins/woocommerce/includes/interfaces/class-wc-log-handler-interface.php new file mode 100644 index 00000000000..f83828165e5 --- /dev/null +++ b/plugins/woocommerce/includes/interfaces/class-wc-log-handler-interface.php @@ -0,0 +1,29 @@ +id). + * @return array + */ + public function delete_meta( &$data, $meta ); + + /** + * Add new piece of meta. + * + * @param WC_Data $data Data object. + * @param object $meta Meta object (containing ->key and ->value). + * @return int meta ID + */ + public function add_meta( &$data, $meta ); + + /** + * Update meta. + * + * @param WC_Data $data Data object. + * @param object $meta Meta object (containing ->id, ->key and ->value). + */ + public function update_meta( &$data, $meta ); +} diff --git a/plugins/woocommerce/includes/interfaces/class-wc-order-data-store-interface.php b/plugins/woocommerce/includes/interfaces/class-wc-order-data-store-interface.php new file mode 100644 index 00000000000..18745176414 --- /dev/null +++ b/plugins/woocommerce/includes/interfaces/class-wc-order-data-store-interface.php @@ -0,0 +1,137 @@ +get_id() will be set. + * + * @param WC_Order_Item $item Item object. + */ + public function save_item_data( &$item ); +} diff --git a/plugins/woocommerce/includes/interfaces/class-wc-order-refund-data-store-interface.php b/plugins/woocommerce/includes/interfaces/class-wc-order-refund-data-store-interface.php new file mode 100644 index 00000000000..1167e830943 --- /dev/null +++ b/plugins/woocommerce/includes/interfaces/class-wc-order-refund-data-store-interface.php @@ -0,0 +1,17 @@ +id, $return[0]->parent_id. + * + * @return array + */ + public function get_on_sale_products(); + + /** + * Returns a list of product IDs ( id as key => parent as value) that are + * featured. Uses get_posts instead of wc_get_products since we want + * some extra meta queries and ALL products (posts_per_page = -1). + * + * @return array + */ + public function get_featured_product_ids(); + + /** + * Check if product sku is found for any other product IDs. + * + * @param int $product_id Product ID. + * @param string $sku SKU. + * @return bool + */ + public function is_existing_sku( $product_id, $sku ); + + /** + * Return product ID based on SKU. + * + * @param string $sku SKU. + * @return int + */ + public function get_product_id_by_sku( $sku ); + + /** + * Returns an array of IDs of products that have sales starting soon. + * + * @return array + */ + public function get_starting_sales(); + + /** + * Returns an array of IDs of products that have sales which are due to end. + * + * @return array + */ + public function get_ending_sales(); + + /** + * Find a matching (enabled) variation within a variable product. + * + * @param WC_Product $product Variable product object. + * @param array $match_attributes Array of attributes we want to try to match. + * @return int Matching variation ID or 0. + */ + public function find_matching_product_variation( $product, $match_attributes = array() ); + + /** + * Make sure all variations have a sort order set so they can be reordered correctly. + * + * @param int $parent_id Parent ID. + */ + public function sort_all_product_variations( $parent_id ); + + /** + * Return a list of related products (using data like categories and IDs). + * + * @param array $cats_array List of categories IDs. + * @param array $tags_array List of tags IDs. + * @param array $exclude_ids Excluded IDs. + * @param int $limit Limit of results. + * @param int $product_id Product ID. + * @return array + */ + public function get_related_products( $cats_array, $tags_array, $exclude_ids, $limit, $product_id ); + + /** + * Update a product's stock amount directly. + * + * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues). + * + * @param int $product_id_with_stock Product ID. + * @param int|null $stock_quantity Stock quantity to update to. + * @param string $operation Either set, increase or decrease. + */ + public function update_product_stock( $product_id_with_stock, $stock_quantity = null, $operation = 'set' ); + + /** + * Update a product's sale count directly. + * + * Uses queries rather than update_post_meta so we can do this in one query for performance. + * + * @param int $product_id Product ID. + * @param int|null $quantity Stock quantity to use for update. + * @param string $operation Either set, increase or decrease. + */ + public function update_product_sales( $product_id, $quantity = null, $operation = 'set' ); + + /** + * Get shipping class ID by slug. + * + * @param string $slug Shipping class slug. + * @return int|false + */ + public function get_shipping_class_id_by_slug( $slug ); + + /** + * Returns an array of products. + * + * @param array $args @see wc_get_products. + * @return array + */ + public function get_products( $args = array() ); + + /** + * Get the product type based on product ID. + * + * @param int $product_id Product ID. + * @return bool|string + */ + public function get_product_type( $product_id ); +} diff --git a/plugins/woocommerce/includes/interfaces/class-wc-product-variable-data-store-interface.php b/plugins/woocommerce/includes/interfaces/class-wc-product-variable-data-store-interface.php new file mode 100644 index 00000000000..03b72b48a3e --- /dev/null +++ b/plugins/woocommerce/includes/interfaces/class-wc-product-variable-data-store-interface.php @@ -0,0 +1,79 @@ + '' - the name of the action that will be triggered. + * 'args' => null - the args array that will be passed with the action. + * 'date' => null - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. + * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '='. + * 'modified' => null - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. + * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '='. + * 'group' => '' - the group the action belongs to. + * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING. + * 'claimed' => null - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID. + * 'per_page' => 5 - Number of results to return. + * 'offset' => 0. + * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', or 'date'. + * 'order' => 'ASC'. + * @param string $return_format OBJECT, ARRAY_A, or ids. + * @return array + */ + public function search( $args = array(), $return_format = OBJECT ); +} diff --git a/plugins/woocommerce/includes/interfaces/class-wc-shipping-zone-data-store-interface.php b/plugins/woocommerce/includes/interfaces/class-wc-shipping-zone-data-store-interface.php new file mode 100644 index 00000000000..65d7243e2c7 --- /dev/null +++ b/plugins/woocommerce/includes/interfaces/class-wc-shipping-zone-data-store-interface.php @@ -0,0 +1,81 @@ + + * GET /orders//notes + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET|POST /orders + $routes[ $this->base ] = array( + array( array( $this, 'get_orders' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /orders/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ), + ); + + # GET /orders/statuses + $routes[ $this->base . '/statuses' ] = array( + array( array( $this, 'get_order_statuses' ), WC_API_Server::READABLE ), + ); + + # GET|PUT|DELETE /orders/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_order' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order' ), WC_API_Server::DELETABLE ), + ); + + # GET|POST /orders//notes + $routes[ $this->base . '/(?P\d+)/notes' ] = array( + array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order_note' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET|PUT|DELETE /orders//notes/ + $routes[ $this->base . '/(?P\d+)/notes/(?P\d+)' ] = array( + array( array( $this, 'get_order_note' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order_note' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order_note' ), WC_API_SERVER::DELETABLE ), + ); + + # GET|POST /orders//refunds + $routes[ $this->base . '/(?P\d+)/refunds' ] = array( + array( array( $this, 'get_order_refunds' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order_refund' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET|PUT|DELETE /orders//refunds/ + $routes[ $this->base . '/(?P\d+)/refunds/(?P\d+)' ] = array( + array( array( $this, 'get_order_refund' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order_refund' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order_refund' ), WC_API_SERVER::DELETABLE ), + ); + + # POST|PUT /orders/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + return $routes; + } + + /** + * Get all orders + * + * @since 2.1 + * @param string $fields + * @param array $filter + * @param string $status + * @param int $page + * @return array + */ + public function get_orders( $fields = null, $filter = array(), $status = null, $page = 1 ) { + + if ( ! empty( $status ) ) { + $filter['status'] = $status; + } + + $filter['page'] = $page; + + $query = $this->query_orders( $filter ); + + $orders = array(); + + foreach ( $query->posts as $order_id ) { + + if ( ! $this->is_readable( $order_id ) ) { + continue; + } + + $orders[] = current( $this->get_order( $order_id, $fields, $filter ) ); + } + + $this->server->add_pagination_headers( $query ); + + return array( 'orders' => $orders ); + } + + + /** + * Get the order for the given ID + * + * @since 2.1 + * @param int $id the order ID + * @param array $fields + * @param array $filter + * @return array|WP_Error + */ + public function get_order( $id, $fields = null, $filter = array() ) { + + // ensure order ID is valid & user has permission to read + $id = $this->validate_request( $id, $this->post_type, 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + // Get the decimal precession + $dp = ( isset( $filter['dp'] ) ? intval( $filter['dp'] ) : 2 ); + $order = wc_get_order( $id ); + $order_data = array( + 'id' => $order->get_id(), + 'order_number' => $order->get_order_number(), + 'created_at' => $this->server->format_datetime( $order->get_date_created() ? $order->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'updated_at' => $this->server->format_datetime( $order->get_date_modified() ? $order->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'completed_at' => $this->server->format_datetime( $order->get_date_completed() ? $order->get_date_completed()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'status' => $order->get_status(), + 'currency' => $order->get_currency(), + 'total' => wc_format_decimal( $order->get_total(), $dp ), + 'subtotal' => wc_format_decimal( $order->get_subtotal(), $dp ), + 'total_line_items_quantity' => $order->get_item_count(), + 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), + 'total_shipping' => wc_format_decimal( $order->get_shipping_total(), $dp ), + 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), + 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), + 'total_discount' => wc_format_decimal( $order->get_total_discount(), $dp ), + 'shipping_methods' => $order->get_shipping_method(), + 'payment_details' => array( + 'method_id' => $order->get_payment_method(), + 'method_title' => $order->get_payment_method_title(), + 'paid' => ! is_null( $order->get_date_paid() ), + ), + 'billing_address' => array( + 'first_name' => $order->get_billing_first_name(), + 'last_name' => $order->get_billing_last_name(), + 'company' => $order->get_billing_company(), + 'address_1' => $order->get_billing_address_1(), + 'address_2' => $order->get_billing_address_2(), + 'city' => $order->get_billing_city(), + 'state' => $order->get_billing_state(), + 'postcode' => $order->get_billing_postcode(), + 'country' => $order->get_billing_country(), + 'email' => $order->get_billing_email(), + 'phone' => $order->get_billing_phone(), + ), + 'shipping_address' => array( + 'first_name' => $order->get_shipping_first_name(), + 'last_name' => $order->get_shipping_last_name(), + 'company' => $order->get_shipping_company(), + 'address_1' => $order->get_shipping_address_1(), + 'address_2' => $order->get_shipping_address_2(), + 'city' => $order->get_shipping_city(), + 'state' => $order->get_shipping_state(), + 'postcode' => $order->get_shipping_postcode(), + 'country' => $order->get_shipping_country(), + ), + 'note' => $order->get_customer_note(), + 'customer_ip' => $order->get_customer_ip_address(), + 'customer_user_agent' => $order->get_customer_user_agent(), + 'customer_id' => $order->get_user_id(), + 'view_order_url' => $order->get_view_order_url(), + 'line_items' => array(), + 'shipping_lines' => array(), + 'tax_lines' => array(), + 'fee_lines' => array(), + 'coupon_lines' => array(), + ); + + // add line items + foreach ( $order->get_items() as $item_id => $item ) { + $product = $item->get_product(); + $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; + $item_meta = $item->get_all_formatted_meta_data( $hideprefix ); + + foreach ( $item_meta as $key => $values ) { + $item_meta[ $key ]->label = $values->display_key; + unset( $item_meta[ $key ]->display_key ); + unset( $item_meta[ $key ]->display_value ); + } + + $order_data['line_items'][] = array( + 'id' => $item_id, + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), + 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), $dp ), + 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), + 'total_tax' => wc_format_decimal( $item->get_total_tax(), $dp ), + 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), + 'quantity' => $item->get_quantity(), + 'tax_class' => $item->get_tax_class(), + 'name' => $item->get_name(), + 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), + 'sku' => is_object( $product ) ? $product->get_sku() : null, + 'meta' => array_values( $item_meta ), + ); + } + + // add shipping + foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { + $order_data['shipping_lines'][] = array( + 'id' => $shipping_item_id, + 'method_id' => $shipping_item->get_method_id(), + 'method_title' => $shipping_item->get_name(), + 'total' => wc_format_decimal( $shipping_item->get_total(), $dp ), + ); + } + + // add taxes + foreach ( $order->get_tax_totals() as $tax_code => $tax ) { + $order_data['tax_lines'][] = array( + 'id' => $tax->id, + 'rate_id' => $tax->rate_id, + 'code' => $tax_code, + 'title' => $tax->label, + 'total' => wc_format_decimal( $tax->amount, $dp ), + 'compound' => (bool) $tax->is_compound, + ); + } + + // add fees + foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { + $order_data['fee_lines'][] = array( + 'id' => $fee_item_id, + 'title' => $fee_item->get_name(), + 'tax_class' => $fee_item->get_tax_class(), + 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), + ); + } + + // add coupons + foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { + $order_data['coupon_lines'][] = array( + 'id' => $coupon_item_id, + 'code' => $coupon_item->get_code(), + 'amount' => wc_format_decimal( $coupon_item->get_discount(), $dp ), + ); + } + + return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) ); + } + + /** + * Get the total number of orders + * + * @since 2.4 + * + * @param string $status + * @param array $filter + * + * @return array|WP_Error + */ + public function get_orders_count( $status = null, $filter = array() ) { + + try { + if ( ! current_user_can( 'read_private_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), 401 ); + } + + if ( ! empty( $status ) ) { + + if ( 'any' === $status ) { + + $order_statuses = array(); + + foreach ( wc_get_order_statuses() as $slug => $name ) { + $filter['status'] = str_replace( 'wc-', '', $slug ); + $query = $this->query_orders( $filter ); + $order_statuses[ str_replace( 'wc-', '', $slug ) ] = (int) $query->found_posts; + } + + return array( 'count' => $order_statuses ); + + } else { + $filter['status'] = $status; + } + } + + $query = $this->query_orders( $filter ); + + return array( 'count' => (int) $query->found_posts ); + + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a list of valid order statuses + * + * Note this requires no specific permissions other than being an authenticated + * API user. Order statuses (particularly custom statuses) could be considered + * private information which is why it's not in the API index. + * + * @since 2.1 + * @return array + */ + public function get_order_statuses() { + + $order_statuses = array(); + + foreach ( wc_get_order_statuses() as $slug => $name ) { + $order_statuses[ str_replace( 'wc-', '', $slug ) ] = $name; + } + + return array( 'order_statuses' => apply_filters( 'woocommerce_api_order_statuses_response', $order_statuses, $this ) ); + } + + /** + * Create an order + * + * @since 2.2 + * + * @param array $data raw order data + * + * @return array|WP_Error + */ + public function create_order( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['order'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order' ), 400 ); + } + + $data = $data['order']; + + // permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order', __( 'You do not have permission to create orders', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_order_data', $data, $this ); + + // default order args, note that status is checked for validity in wc_create_order() + $default_order_args = array( + 'status' => isset( $data['status'] ) ? $data['status'] : '', + 'customer_note' => isset( $data['note'] ) ? $data['note'] : null, + ); + + // if creating order for existing customer + if ( ! empty( $data['customer_id'] ) ) { + + // make sure customer exists + if ( false === get_user_by( 'id', $data['customer_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + $default_order_args['customer_id'] = $data['customer_id']; + } + + // create the pending order + $order = $this->create_base_order( $default_order_args, $data ); + + if ( is_wp_error( $order ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order', sprintf( __( 'Cannot create order: %s', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 ); + } + + // billing/shipping addresses + $this->set_order_addresses( $order, $data ); + + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + + if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { + + $set_item = "set_{$line_type}"; + + foreach ( $data[ $line ] as $item ) { + + $this->$set_item( $order, $item, 'create' ); + } + } + } + + // calculate totals and set them + $order->calculate_totals(); + + // payment method (and payment_complete() if `paid` == true) + if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { + + // method ID & title are required + if ( empty( $data['payment_details']['method_id'] ) || empty( $data['payment_details']['method_title'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_payment_details', __( 'Payment method ID and title are required', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); + update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); + + // mark as paid if set + if ( isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { + $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); + } + } + + // set order currency + if ( isset( $data['currency'] ) ) { + + if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); + } + + // set order meta + if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { + $this->set_order_meta( $order->get_id(), $data['order_meta'] ); + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + wc_delete_shop_order_transients( $order ); + + do_action( 'woocommerce_api_create_order', $order->get_id(), $data, $this ); + do_action( 'woocommerce_new_order', $order->get_id() ); + + return $this->get_order( $order->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Creates new WC_Order. + * + * Requires a separate function for classes that extend WC_API_Orders. + * + * @since 2.3 + * + * @param $args array + * @param $data + * + * @return WC_Order + */ + protected function create_base_order( $args, $data ) { + return wc_create_order( $args ); + } + + /** + * Edit an order + * + * @since 2.2 + * + * @param int $id the order ID + * @param array $data + * + * @return array|WP_Error + */ + public function edit_order( $id, $data ) { + try { + if ( ! isset( $data['order'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order' ), 400 ); + } + + $data = $data['order']; + + $update_totals = false; + + $id = $this->validate_request( $id, $this->post_type, 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $data = apply_filters( 'woocommerce_api_edit_order_data', $data, $id, $this ); + $order = wc_get_order( $id ); + + if ( empty( $order ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); + } + + $order_args = array( 'order_id' => $order->get_id() ); + + // Customer note. + if ( isset( $data['note'] ) ) { + $order_args['customer_note'] = $data['note']; + } + + // Customer ID. + if ( isset( $data['customer_id'] ) && $data['customer_id'] != $order->get_user_id() ) { + // Make sure customer exists. + if ( false === get_user_by( 'id', $data['customer_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_customer_user', $data['customer_id'] ); + } + + // Billing/shipping address. + $this->set_order_addresses( $order, $data ); + + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + + if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { + + $update_totals = true; + + foreach ( $data[ $line ] as $item ) { + + // Item ID is always required. + if ( ! array_key_exists( 'id', $item ) ) { + $item['id'] = null; + } + + // Create item. + if ( is_null( $item['id'] ) ) { + $this->set_item( $order, $line_type, $item, 'create' ); + } elseif ( $this->item_is_null( $item ) ) { + // Delete item. + wc_delete_order_item( $item['id'] ); + } else { + // Update item. + $this->set_item( $order, $line_type, $item, 'update' ); + } + } + } + } + + // Payment method (and payment_complete() if `paid` == true and order needs payment). + if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { + + // Method ID. + if ( isset( $data['payment_details']['method_id'] ) ) { + update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); + } + + // Method title. + if ( isset( $data['payment_details']['method_title'] ) ) { + update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); + } + + // Mark as paid if set. + if ( $order->needs_payment() && isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { + $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); + } + } + + // Set order currency. + if ( isset( $data['currency'] ) ) { + if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); + } + + // If items have changed, recalculate order totals. + if ( $update_totals ) { + $order->calculate_totals(); + } + + // Update order meta. + if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { + $this->set_order_meta( $order->get_id(), $data['order_meta'] ); + } + + // Update the order post to set customer note/modified date. + wc_update_order( $order_args ); + + // Order status. + if ( ! empty( $data['status'] ) ) { + // Refresh the order instance. + $order = wc_get_order( $order->get_id() ); + $order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true ); + } + + wc_delete_shop_order_transients( $order ); + + do_action( 'woocommerce_api_edit_order', $order->get_id(), $data, $this ); + do_action( 'woocommerce_update_order', $order->get_id() ); + + return $this->get_order( $id ); + + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete an order + * + * @param int $id the order ID + * @param bool $force true to permanently delete order, false to move to trash + * @return array|WP_Error + */ + public function delete_order( $id, $force = false ) { + + $id = $this->validate_request( $id, $this->post_type, 'delete' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + wc_delete_shop_order_transients( $id ); + + do_action( 'woocommerce_api_delete_order', $id, $this ); + + return $this->delete( $id, 'order', ( 'true' === $force ) ); + } + + /** + * Helper method to get order post objects + * + * @since 2.1 + * @param array $args request arguments for filtering query + * @return WP_Query + */ + protected function query_orders( $args ) { + + // set base query arguments + $query_args = array( + 'fields' => 'ids', + 'post_type' => $this->post_type, + 'post_status' => array_keys( wc_get_order_statuses() ), + ); + + // add status argument + if ( ! empty( $args['status'] ) ) { + + $statuses = 'wc-' . str_replace( ',', ',wc-', $args['status'] ); + $statuses = explode( ',', $statuses ); + $query_args['post_status'] = $statuses; + + unset( $args['status'] ); + + } + + $query_args = $this->merge_query_args( $query_args, $args ); + + return new WP_Query( $query_args ); + } + + /** + * Helper method to set/update the billing & shipping addresses for + * an order + * + * @since 2.1 + * @param \WC_Order $order + * @param array $data + */ + protected function set_order_addresses( $order, $data ) { + + $address_fields = array( + 'first_name', + 'last_name', + 'company', + 'email', + 'phone', + 'address_1', + 'address_2', + 'city', + 'state', + 'postcode', + 'country', + ); + + $billing_address = $shipping_address = array(); + + // billing address + if ( isset( $data['billing_address'] ) && is_array( $data['billing_address'] ) ) { + + foreach ( $address_fields as $field ) { + + if ( isset( $data['billing_address'][ $field ] ) ) { + $billing_address[ $field ] = wc_clean( $data['billing_address'][ $field ] ); + } + } + + unset( $address_fields['email'] ); + unset( $address_fields['phone'] ); + } + + // shipping address + if ( isset( $data['shipping_address'] ) && is_array( $data['shipping_address'] ) ) { + + foreach ( $address_fields as $field ) { + + if ( isset( $data['shipping_address'][ $field ] ) ) { + $shipping_address[ $field ] = wc_clean( $data['shipping_address'][ $field ] ); + } + } + } + + $this->update_address( $order, $billing_address, 'billing' ); + $this->update_address( $order, $shipping_address, 'shipping' ); + + // update user meta + if ( $order->get_user_id() ) { + foreach ( $billing_address as $key => $value ) { + update_user_meta( $order->get_user_id(), 'billing_' . $key, $value ); + } + foreach ( $shipping_address as $key => $value ) { + update_user_meta( $order->get_user_id(), 'shipping_' . $key, $value ); + } + } + } + + /** + * Update address. + * + * @param WC_Order $order + * @param array $posted + * @param string $type + */ + protected function update_address( $order, $posted, $type = 'billing' ) { + foreach ( $posted as $key => $value ) { + if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { + $order->{"set_{$type}_{$key}"}( $value ); + } + } + } + + /** + * Helper method to add/update order meta, with two restrictions: + * + * 1) Only non-protected meta (no leading underscore) can be set + * 2) Meta values must be scalar (int, string, bool) + * + * @since 2.2 + * @param int $order_id valid order ID + * @param array $order_meta order meta in array( 'meta_key' => 'meta_value' ) format + */ + protected function set_order_meta( $order_id, $order_meta ) { + + foreach ( $order_meta as $meta_key => $meta_value ) { + + if ( is_string( $meta_key ) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) { + update_post_meta( $order_id, $meta_key, $meta_value ); + } + } + } + + /** + * Helper method to check if the resource ID associated with the provided item is null + * + * Items can be deleted by setting the resource ID to null + * + * @since 2.2 + * @param array $item item provided in the request body + * @return bool true if the item resource ID is null, false otherwise + */ + protected function item_is_null( $item ) { + + $keys = array( 'product_id', 'method_id', 'title', 'code' ); + + foreach ( $keys as $key ) { + if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { + return true; + } + } + + return false; + } + + /** + * Wrapper method to create/update order items + * + * When updating, the item ID provided is checked to ensure it is associated + * with the order. + * + * @since 2.2 + * @param \WC_Order $order order + * @param string $item_type + * @param array $item item provided in the request body + * @param string $action either 'create' or 'update' + * @throws WC_API_Exception if item ID is not associated with order + */ + protected function set_item( $order, $item_type, $item, $action ) { + global $wpdb; + + $set_method = "set_{$item_type}"; + + // verify provided line item ID is associated with order + if ( 'update' === $action ) { + + $result = $wpdb->get_row( + $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", + absint( $item['id'] ), + absint( $order->get_id() ) + ) ); + + if ( is_null( $result ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); + } + } + + $this->$set_method( $order, $item, $action ); + } + + /** + * Create or update a line item + * + * @since 2.2 + * @param \WC_Order $order + * @param array $item line item data + * @param string $action 'create' to add line item or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_line_item( $order, $item, $action ) { + $creating = ( 'create' === $action ); + + // product is always required + if ( ! isset( $item['product_id'] ) && ! isset( $item['sku'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID or SKU is required', 'woocommerce' ), 400 ); + } + + // when updating, ensure product ID provided matches + if ( 'update' === $action ) { + + $item_product_id = wc_get_order_item_meta( $item['id'], '_product_id' ); + $item_variation_id = wc_get_order_item_meta( $item['id'], '_variation_id' ); + + if ( $item['product_id'] != $item_product_id && $item['product_id'] != $item_variation_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID provided does not match this line item', 'woocommerce' ), 400 ); + } + } + + if ( isset( $item['product_id'] ) ) { + $product_id = $item['product_id']; + } elseif ( isset( $item['sku'] ) ) { + $product_id = wc_get_product_id_by_sku( $item['sku'] ); + } + + // variations must each have a key & value + $variation_id = 0; + if ( isset( $item['variations'] ) && is_array( $item['variations'] ) ) { + foreach ( $item['variations'] as $key => $value ) { + if ( ! $key || ! $value ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_variation', __( 'The product variation is invalid', 'woocommerce' ), 400 ); + } + } + $variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item['variations'] ); + } + + $product = wc_get_product( $variation_id ? $variation_id : $product_id ); + + // must be a valid WC_Product + if ( ! is_object( $product ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 ); + } + + // quantity must be positive float + if ( isset( $item['quantity'] ) && floatval( $item['quantity'] ) <= 0 ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity must be a positive float.', 'woocommerce' ), 400 ); + } + + // quantity is required when creating + if ( $creating && ! isset( $item['quantity'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 ); + } + + if ( $creating ) { + $line_item = new WC_Order_Item_Product(); + } else { + $line_item = new WC_Order_Item_Product( $item['id'] ); + } + + $line_item->set_product( $product ); + $line_item->set_order_id( $order->get_id() ); + + if ( isset( $item['quantity'] ) ) { + $line_item->set_quantity( $item['quantity'] ); + } + if ( isset( $item['total'] ) ) { + $line_item->set_total( floatval( $item['total'] ) ); + } elseif ( $creating ) { + $total = wc_get_price_excluding_tax( $product, array( 'qty' => $line_item->get_quantity() ) ); + $line_item->set_total( $total ); + $line_item->set_subtotal( $total ); + } + if ( isset( $item['total_tax'] ) ) { + $line_item->set_total_tax( floatval( $item['total_tax'] ) ); + } + if ( isset( $item['subtotal'] ) ) { + $line_item->set_subtotal( floatval( $item['subtotal'] ) ); + } + if ( isset( $item['subtotal_tax'] ) ) { + $line_item->set_subtotal_tax( floatval( $item['subtotal_tax'] ) ); + } + if ( $variation_id ) { + $line_item->set_variation_id( $variation_id ); + $line_item->set_variation( $item['variations'] ); + } + + // Save or add to order. + if ( $creating ) { + $order->add_item( $line_item ); + } else { + $item_id = $line_item->save(); + + if ( ! $item_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Given a product ID & API provided variations, find the correct variation ID to use for calculation + * We can't just trust input from the API to pass a variation_id manually, otherwise you could pass + * the cheapest variation ID but provide other information so we have to look up the variation ID. + * + * @param WC_Product $product + * @param array $variations + * + * @return int returns an ID if a valid variation was found for this product + */ + function get_variation_id( $product, $variations = array() ) { + $variation_id = null; + $variations_normalized = array(); + + if ( $product->is_type( 'variable' ) && $product->has_child() ) { + if ( isset( $variations ) && is_array( $variations ) ) { + // start by normalizing the passed variations + foreach ( $variations as $key => $value ) { + $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); // from get_attributes in class-wc-api-products.php + $variations_normalized[ $key ] = strtolower( $value ); + } + // now search through each product child and see if our passed variations match anything + foreach ( $product->get_children() as $variation ) { + $meta = array(); + foreach ( get_post_meta( $variation ) as $key => $value ) { + $value = $value[0]; + $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); + $meta[ $key ] = strtolower( $value ); + } + // if the variation array is a part of the $meta array, we found our match + if ( $this->array_contains( $variations_normalized, $meta ) ) { + $variation_id = $variation; + break; + } + } + } + } + + return $variation_id; + } + + /** + * Utility function to see if the meta array contains data from variations + * + * @param array $needles + * @param array $haystack + * + * @return bool + */ + protected function array_contains( $needles, $haystack ) { + foreach ( $needles as $key => $value ) { + if ( $haystack[ $key ] !== $value ) { + return false; + } + } + return true; + } + + /** + * Create or update an order shipping method + * + * @since 2.2 + * @param \WC_Order $order + * @param array $shipping item data + * @param string $action 'create' to add shipping or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_shipping( $order, $shipping, $action ) { + + // total must be a positive float + if ( isset( $shipping['total'] ) && floatval( $shipping['total'] ) < 0 ) { + throw new WC_API_Exception( 'woocommerce_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + + // method ID is required + if ( ! isset( $shipping['method_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); + } + + $rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] ); + $item = new WC_Order_Item_Shipping(); + $item->set_order_id( $order->get_id() ); + $item->set_shipping_rate( $rate ); + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Shipping( $shipping['id'] ); + + if ( isset( $shipping['method_id'] ) ) { + $item->set_method_id( $shipping['method_id'] ); + } + + if ( isset( $shipping['method_title'] ) ) { + $item->set_method_title( $shipping['method_title'] ); + } + + if ( isset( $shipping['total'] ) ) { + $item->set_total( floatval( $shipping['total'] ) ); + } + + $shipping_id = $item->save(); + + if ( ! $shipping_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order fee + * + * @since 2.2 + * @param \WC_Order $order + * @param array $fee item data + * @param string $action 'create' to add fee or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_fee( $order, $fee, $action ) { + + if ( 'create' === $action ) { + + // fee title is required + if ( ! isset( $fee['title'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee title is required', 'woocommerce' ), 400 ); + } + + $item = new WC_Order_Item_Fee(); + $item->set_order_id( $order->get_id() ); + $item->set_name( wc_clean( $fee['title'] ) ); + $item->set_total( isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0 ); + + // if taxable, tax class and total are required + if ( ! empty( $fee['taxable'] ) ) { + if ( ! isset( $fee['tax_class'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 ); + } + + $item->set_tax_status( 'taxable' ); + $item->set_tax_class( $fee['tax_class'] ); + + if ( isset( $fee['total_tax'] ) ) { + $item->set_total_tax( isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0 ); + } + + if ( isset( $fee['tax_data'] ) ) { + $item->set_total_tax( wc_format_refund_total( array_sum( $fee['tax_data'] ) ) ); + $item->set_taxes( array_map( 'wc_format_refund_total', $fee['tax_data'] ) ); + } + } + + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Fee( $fee['id'] ); + + if ( isset( $fee['title'] ) ) { + $item->set_name( wc_clean( $fee['title'] ) ); + } + + if ( isset( $fee['tax_class'] ) ) { + $item->set_tax_class( $fee['tax_class'] ); + } + + if ( isset( $fee['total'] ) ) { + $item->set_total( floatval( $fee['total'] ) ); + } + + if ( isset( $fee['total_tax'] ) ) { + $item->set_total_tax( floatval( $fee['total_tax'] ) ); + } + + $fee_id = $item->save(); + + if ( ! $fee_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order coupon + * + * @since 2.2 + * @param \WC_Order $order + * @param array $coupon item data + * @param string $action 'create' to add coupon or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_coupon( $order, $coupon, $action ) { + + // coupon amount must be positive float + if ( isset( $coupon['amount'] ) && floatval( $coupon['amount'] ) < 0 ) { + throw new WC_API_Exception( 'woocommerce_invalid_coupon_total', __( 'Coupon discount total must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + + // coupon code is required + if ( empty( $coupon['code'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + + $item = new WC_Order_Item_Coupon(); + $item->set_props( array( + 'code' => $coupon['code'], + 'discount' => isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0, + 'discount_tax' => 0, + 'order_id' => $order->get_id(), + ) ); + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Coupon( $coupon['id'] ); + + if ( isset( $coupon['code'] ) ) { + $item->set_code( $coupon['code'] ); + } + + if ( isset( $coupon['amount'] ) ) { + $item->set_discount( floatval( $coupon['amount'] ) ); + } + + $coupon_id = $item->save(); + + if ( ! $coupon_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Get the admin order notes for an order + * + * @since 2.1 + * @param string $order_id order ID + * @param string|null $fields fields to include in response + * @return array|WP_Error + */ + public function get_order_notes( $order_id, $fields = null ) { + + // ensure ID is valid order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $args = array( + 'post_id' => $order_id, + 'approve' => 'approve', + 'type' => 'order_note', + ); + + remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $notes = get_comments( $args ); + + add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $order_notes = array(); + + foreach ( $notes as $note ) { + + $order_notes[] = current( $this->get_order_note( $order_id, $note->comment_ID, $fields ) ); + } + + return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $order_id, $fields, $notes, $this->server ) ); + } + + /** + * Get an order note for the given order ID and ID + * + * @since 2.2 + * + * @param string $order_id order ID + * @param string $id order note ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_order_note( $order_id, $id, $fields = null ) { + try { + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $order_note = array( + 'id' => $note->comment_ID, + 'created_at' => $this->server->format_datetime( $note->comment_date_gmt ), + 'note' => $note->comment_content, + 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), + ); + + return array( 'order_note' => apply_filters( 'woocommerce_api_order_note_response', $order_note, $id, $fields, $note, $order_id, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new order note for the given order + * + * @since 2.2 + * @param string $order_id order ID + * @param array $data raw request data + * @return WP_Error|array error or created note response data + */ + public function create_order_note( $order_id, $data ) { + try { + if ( ! isset( $data['order_note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_note' ), 400 ); + } + + $data = $data['order_note']; + + // permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_note', __( 'You do not have permission to create order notes', 'woocommerce' ), 401 ); + } + + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $order = wc_get_order( $order_id ); + + $data = apply_filters( 'woocommerce_api_create_order_note_data', $data, $order_id, $this ); + + // note content is required + if ( ! isset( $data['note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note', __( 'Order note is required', 'woocommerce' ), 400 ); + } + + $is_customer_note = ( isset( $data['customer_note'] ) && true === $data['customer_note'] ); + + // create the note + $note_id = $order->add_order_note( $data['note'], $is_customer_note ); + + if ( ! $note_id ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), 500 ); + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + do_action( 'woocommerce_api_create_order_note', $note_id, $order_id, $this ); + + return $this->get_order_note( $order->get_id(), $note_id ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit the order note + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id note ID + * @param array $data parsed request data + * @return WP_Error|array error or edited note response data + */ + public function edit_order_note( $order_id, $id, $data ) { + try { + if ( ! isset( $data['order_note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_note' ), 400 ); + } + + $data = $data['order_note']; + + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $order = wc_get_order( $order_id ); + + // Validate note ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + // Ensure note ID is valid + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + // Ensure note ID is associated with given order + if ( $note->comment_post_ID != $order->get_id() ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_edit_order_note_data', $data, $note->comment_ID, $order->get_id(), $this ); + + // Note content + if ( isset( $data['note'] ) ) { + + wp_update_comment( + array( + 'comment_ID' => $note->comment_ID, + 'comment_content' => $data['note'], + ) + ); + } + + // Customer note + if ( isset( $data['customer_note'] ) ) { + + update_comment_meta( $note->comment_ID, 'is_customer_note', true === $data['customer_note'] ? 1 : 0 ); + } + + do_action( 'woocommerce_api_edit_order_note', $note->comment_ID, $order->get_id(), $this ); + + return $this->get_order_note( $order->get_id(), $note->comment_ID ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete order note + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id note ID + * @return WP_Error|array error or deleted message + */ + public function delete_order_note( $order_id, $id ) { + try { + $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate note ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + // Ensure note ID is valid + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + // Ensure note ID is associated with given order + if ( $note->comment_post_ID != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); + } + + // Force delete since trashed order notes could not be managed through comments list table + $result = wc_delete_order_note( $note->comment_ID ); + + if ( ! $result ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_order_note', __( 'This order note cannot be deleted', 'woocommerce' ), 500 ); + } + + do_action( 'woocommerce_api_delete_order_note', $note->comment_ID, $order_id, $this ); + + return array( 'message' => __( 'Permanently deleted order note', 'woocommerce' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the order refunds for an order + * + * @since 2.2 + * @param string $order_id order ID + * @param string|null $fields fields to include in response + * @return array|WP_Error + */ + public function get_order_refunds( $order_id, $fields = null ) { + + // Ensure ID is valid order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $refund_items = wc_get_orders( array( + 'type' => 'shop_order_refund', + 'parent' => $order_id, + 'limit' => -1, + 'return' => 'ids', + ) ); + $order_refunds = array(); + + foreach ( $refund_items as $refund_id ) { + $order_refunds[] = current( $this->get_order_refund( $order_id, $refund_id, $fields ) ); + } + + return array( 'order_refunds' => apply_filters( 'woocommerce_api_order_refunds_response', $order_refunds, $order_id, $fields, $refund_items, $this ) ); + } + + /** + * Get an order refund for the given order ID and ID + * + * @since 2.2 + * + * @param string $order_id order ID + * @param int $id + * @param string|null $fields fields to limit response to + * @param array $filter + * + * @return array|WP_Error + */ + public function get_order_refund( $order_id, $id, $fields = null, $filter = array() ) { + try { + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + $order = wc_get_order( $order_id ); + $refund = wc_get_order( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + $line_items = array(); + + // Add line items + foreach ( $refund->get_items( 'line_item' ) as $item_id => $item ) { + $product = $item->get_product(); + $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; + $item_meta = $item->get_all_formatted_meta_data( $hideprefix ); + + foreach ( $item_meta as $key => $values ) { + $item_meta[ $key ]->label = $values->display_key; + unset( $item_meta[ $key ]->display_key ); + unset( $item_meta[ $key ]->display_value ); + } + + $line_items[] = array( + 'id' => $item_id, + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ), + 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), 2 ), + 'total' => wc_format_decimal( $order->get_line_total( $item ), 2 ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $item ), 2 ), + 'price' => wc_format_decimal( $order->get_item_total( $item ), 2 ), + 'quantity' => $item->get_quantity(), + 'tax_class' => $item->get_tax_class(), + 'name' => $item->get_name(), + 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), + 'sku' => is_object( $product ) ? $product->get_sku() : null, + 'meta' => array_values( $item_meta ), + 'refunded_item_id' => (int) $item->get_meta( 'refunded_item_id' ), + ); + } + + $order_refund = array( + 'id' => $refund->get_id(), + 'created_at' => $this->server->format_datetime( $refund->get_date_created() ? $refund->get_date_created()->getTimestamp() : 0, false, false ), + 'amount' => wc_format_decimal( $refund->get_amount(), 2 ), + 'reason' => $refund->get_reason(), + 'line_items' => $line_items, + ); + + return array( 'order_refund' => apply_filters( 'woocommerce_api_order_refund_response', $order_refund, $id, $fields, $refund, $order_id, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new order refund for the given order + * + * @since 2.2 + * @param string $order_id order ID + * @param array $data raw request data + * @param bool $api_refund do refund using a payment gateway API + * @return WP_Error|array error or created refund response data + */ + public function create_order_refund( $order_id, $data, $api_refund = true ) { + try { + if ( ! isset( $data['order_refund'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_refund' ), 400 ); + } + + $data = $data['order_refund']; + + // Permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_refund', __( 'You do not have permission to create order refunds', 'woocommerce' ), 401 ); + } + + $order_id = absint( $order_id ); + + if ( empty( $order_id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_create_order_refund_data', $data, $order_id, $this ); + + // Refund amount is required + if ( ! isset( $data['amount'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount is required.', 'woocommerce' ), 400 ); + } elseif ( 0 > $data['amount'] ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount must be positive.', 'woocommerce' ), 400 ); + } + + $data['order_id'] = $order_id; + $data['refund_id'] = 0; + + // Create the refund + $refund = wc_create_refund( $data ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); + } + + // Refund via API + if ( $api_refund ) { + if ( WC()->payment_gateways() ) { + $payment_gateways = WC()->payment_gateways->payment_gateways(); + } + + $order = wc_get_order( $order_id ); + + if ( isset( $payment_gateways[ $order->get_payment_method() ] ) && $payment_gateways[ $order->get_payment_method() ]->supports( 'refunds' ) ) { + $result = $payment_gateways[ $order->get_payment_method() ]->process_refund( $order_id, $refund->get_amount(), $refund->get_reason() ); + + if ( is_wp_error( $result ) ) { + return $result; + } elseif ( ! $result ) { + throw new WC_API_Exception( 'woocommerce_api_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 ); + } + } + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + do_action( 'woocommerce_api_create_order_refund', $refund->get_id(), $order_id, $this ); + + return $this->get_order_refund( $order_id, $refund->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit an order refund + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id refund ID + * @param array $data parsed request data + * @return WP_Error|array error or edited refund response data + */ + public function edit_order_refund( $order_id, $id, $data ) { + try { + if ( ! isset( $data['order_refund'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_refund' ), 400 ); + } + + $data = $data['order_refund']; + + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate refund ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + // Ensure order ID is valid + $refund = get_post( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + // Ensure refund ID is associated with given order + if ( $refund->post_parent != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_edit_order_refund_data', $data, $refund->ID, $order_id, $this ); + + // Update reason + if ( isset( $data['reason'] ) ) { + $updated_refund = wp_update_post( array( 'ID' => $refund->ID, 'post_excerpt' => $data['reason'] ) ); + + if ( is_wp_error( $updated_refund ) ) { + return $updated_refund; + } + } + + // Update refund amount + if ( isset( $data['amount'] ) && 0 < $data['amount'] ) { + update_post_meta( $refund->ID, '_refund_amount', wc_format_decimal( $data['amount'] ) ); + } + + do_action( 'woocommerce_api_edit_order_refund', $refund->ID, $order_id, $this ); + + return $this->get_order_refund( $order_id, $refund->ID ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete order refund + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id refund ID + * @return WP_Error|array error or deleted message + */ + public function delete_order_refund( $order_id, $id ) { + try { + $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate refund ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + // Ensure refund ID is valid + $refund = get_post( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + // Ensure refund ID is associated with given order + if ( $refund->post_parent != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); + } + + wc_delete_shop_order_transients( $order_id ); + + do_action( 'woocommerce_api_delete_order_refund', $refund->ID, $order_id, $this ); + + return $this->delete( $refund->ID, 'refund', true ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Bulk update or insert orders + * Accepts an array with orders in the formats supported by + * WC_API_Orders->create_order() and WC_API_Orders->edit_order() + * + * @since 2.4.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + + try { + if ( ! isset( $data['orders'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_orders_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'orders' ), 400 ); + } + + $data = $data['orders']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'orders' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_orders_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $orders = array(); + + foreach ( $data as $_order ) { + $order_id = 0; + + // Try to get the order ID + if ( isset( $_order['id'] ) ) { + $order_id = intval( $_order['id'] ); + } + + // Order exists / edit order + if ( $order_id ) { + $edit = $this->edit_order( $order_id, array( 'order' => $_order ) ); + + if ( is_wp_error( $edit ) ) { + $orders[] = array( + 'id' => $order_id, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $orders[] = $edit['order']; + } + } else { + // Order don't exists / create order + $new = $this->create_order( array( 'order' => $_order ) ); + + if ( is_wp_error( $new ) ) { + $orders[] = array( + 'id' => $order_id, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $orders[] = $new['order']; + } + } + } + + return array( 'orders' => apply_filters( 'woocommerce_api_orders_bulk_response', $orders, $this ) ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/legacy/api/v2/class-wc-api-products.php b/plugins/woocommerce/includes/legacy/api/v2/class-wc-api-products.php similarity index 100% rename from includes/legacy/api/v2/class-wc-api-products.php rename to plugins/woocommerce/includes/legacy/api/v2/class-wc-api-products.php diff --git a/includes/legacy/api/v2/class-wc-api-reports.php b/plugins/woocommerce/includes/legacy/api/v2/class-wc-api-reports.php similarity index 100% rename from includes/legacy/api/v2/class-wc-api-reports.php rename to plugins/woocommerce/includes/legacy/api/v2/class-wc-api-reports.php diff --git a/includes/legacy/api/v2/class-wc-api-resource.php b/plugins/woocommerce/includes/legacy/api/v2/class-wc-api-resource.php similarity index 100% rename from includes/legacy/api/v2/class-wc-api-resource.php rename to plugins/woocommerce/includes/legacy/api/v2/class-wc-api-resource.php diff --git a/includes/legacy/api/v2/class-wc-api-server.php b/plugins/woocommerce/includes/legacy/api/v2/class-wc-api-server.php similarity index 100% rename from includes/legacy/api/v2/class-wc-api-server.php rename to plugins/woocommerce/includes/legacy/api/v2/class-wc-api-server.php diff --git a/includes/legacy/api/v2/class-wc-api-webhooks.php b/plugins/woocommerce/includes/legacy/api/v2/class-wc-api-webhooks.php similarity index 100% rename from includes/legacy/api/v2/class-wc-api-webhooks.php rename to plugins/woocommerce/includes/legacy/api/v2/class-wc-api-webhooks.php diff --git a/includes/legacy/api/v2/interface-wc-api-handler.php b/plugins/woocommerce/includes/legacy/api/v2/interface-wc-api-handler.php similarity index 100% rename from includes/legacy/api/v2/interface-wc-api-handler.php rename to plugins/woocommerce/includes/legacy/api/v2/interface-wc-api-handler.php diff --git a/includes/legacy/api/v3/class-wc-api-authentication.php b/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-authentication.php similarity index 100% rename from includes/legacy/api/v3/class-wc-api-authentication.php rename to plugins/woocommerce/includes/legacy/api/v3/class-wc-api-authentication.php diff --git a/includes/legacy/api/v3/class-wc-api-coupons.php b/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-coupons.php similarity index 100% rename from includes/legacy/api/v3/class-wc-api-coupons.php rename to plugins/woocommerce/includes/legacy/api/v3/class-wc-api-coupons.php diff --git a/includes/legacy/api/v3/class-wc-api-customers.php b/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-customers.php similarity index 100% rename from includes/legacy/api/v3/class-wc-api-customers.php rename to plugins/woocommerce/includes/legacy/api/v3/class-wc-api-customers.php diff --git a/includes/legacy/api/v3/class-wc-api-exception.php b/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-exception.php similarity index 100% rename from includes/legacy/api/v3/class-wc-api-exception.php rename to plugins/woocommerce/includes/legacy/api/v3/class-wc-api-exception.php diff --git a/includes/legacy/api/v3/class-wc-api-json-handler.php b/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-json-handler.php similarity index 100% rename from includes/legacy/api/v3/class-wc-api-json-handler.php rename to plugins/woocommerce/includes/legacy/api/v3/class-wc-api-json-handler.php diff --git a/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-orders.php b/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-orders.php new file mode 100644 index 00000000000..4d602a94edb --- /dev/null +++ b/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-orders.php @@ -0,0 +1,1877 @@ + + * GET /orders//notes + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET|POST /orders + $routes[ $this->base ] = array( + array( array( $this, 'get_orders' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /orders/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ), + ); + + # GET /orders/statuses + $routes[ $this->base . '/statuses' ] = array( + array( array( $this, 'get_order_statuses' ), WC_API_Server::READABLE ), + ); + + # GET|PUT|DELETE /orders/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_order' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order' ), WC_API_Server::DELETABLE ), + ); + + # GET|POST /orders//notes + $routes[ $this->base . '/(?P\d+)/notes' ] = array( + array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order_note' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET|PUT|DELETE /orders//notes/ + $routes[ $this->base . '/(?P\d+)/notes/(?P\d+)' ] = array( + array( array( $this, 'get_order_note' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order_note' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order_note' ), WC_API_SERVER::DELETABLE ), + ); + + # GET|POST /orders//refunds + $routes[ $this->base . '/(?P\d+)/refunds' ] = array( + array( array( $this, 'get_order_refunds' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order_refund' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET|PUT|DELETE /orders//refunds/ + $routes[ $this->base . '/(?P\d+)/refunds/(?P\d+)' ] = array( + array( array( $this, 'get_order_refund' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order_refund' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order_refund' ), WC_API_SERVER::DELETABLE ), + ); + + # POST|PUT /orders/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + return $routes; + } + + /** + * Get all orders + * + * @since 2.1 + * @param string $fields + * @param array $filter + * @param string $status + * @param int $page + * @return array + */ + public function get_orders( $fields = null, $filter = array(), $status = null, $page = 1 ) { + + if ( ! empty( $status ) ) { + $filter['status'] = $status; + } + + $filter['page'] = $page; + + $query = $this->query_orders( $filter ); + + $orders = array(); + + foreach ( $query->posts as $order_id ) { + + if ( ! $this->is_readable( $order_id ) ) { + continue; + } + + $orders[] = current( $this->get_order( $order_id, $fields, $filter ) ); + } + + $this->server->add_pagination_headers( $query ); + + return array( 'orders' => $orders ); + } + + + /** + * Get the order for the given ID. + * + * @since 2.1 + * @param int $id The order ID. + * @param array $fields Request fields. + * @param array $filter Request filters. + * @return array|WP_Error + */ + public function get_order( $id, $fields = null, $filter = array() ) { + + // Ensure order ID is valid & user has permission to read. + $id = $this->validate_request( $id, $this->post_type, 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + // Get the decimal precession. + $dp = ( isset( $filter['dp'] ) ? intval( $filter['dp'] ) : 2 ); + $order = wc_get_order( $id ); + $expand = array(); + + if ( ! empty( $filter['expand'] ) ) { + $expand = explode( ',', $filter['expand'] ); + } + + $order_data = array( + 'id' => $order->get_id(), + 'order_number' => $order->get_order_number(), + 'order_key' => $order->get_order_key(), + 'created_at' => $this->server->format_datetime( $order->get_date_created() ? $order->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'updated_at' => $this->server->format_datetime( $order->get_date_modified() ? $order->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'completed_at' => $this->server->format_datetime( $order->get_date_completed() ? $order->get_date_completed()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'status' => $order->get_status(), + 'currency' => $order->get_currency(), + 'total' => wc_format_decimal( $order->get_total(), $dp ), + 'subtotal' => wc_format_decimal( $order->get_subtotal(), $dp ), + 'total_line_items_quantity' => $order->get_item_count(), + 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), + 'total_shipping' => wc_format_decimal( $order->get_shipping_total(), $dp ), + 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), + 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), + 'total_discount' => wc_format_decimal( $order->get_total_discount(), $dp ), + 'shipping_methods' => $order->get_shipping_method(), + 'payment_details' => array( + 'method_id' => $order->get_payment_method(), + 'method_title' => $order->get_payment_method_title(), + 'paid' => ! is_null( $order->get_date_paid() ), + ), + 'billing_address' => array( + 'first_name' => $order->get_billing_first_name(), + 'last_name' => $order->get_billing_last_name(), + 'company' => $order->get_billing_company(), + 'address_1' => $order->get_billing_address_1(), + 'address_2' => $order->get_billing_address_2(), + 'city' => $order->get_billing_city(), + 'state' => $order->get_billing_state(), + 'postcode' => $order->get_billing_postcode(), + 'country' => $order->get_billing_country(), + 'email' => $order->get_billing_email(), + 'phone' => $order->get_billing_phone(), + ), + 'shipping_address' => array( + 'first_name' => $order->get_shipping_first_name(), + 'last_name' => $order->get_shipping_last_name(), + 'company' => $order->get_shipping_company(), + 'address_1' => $order->get_shipping_address_1(), + 'address_2' => $order->get_shipping_address_2(), + 'city' => $order->get_shipping_city(), + 'state' => $order->get_shipping_state(), + 'postcode' => $order->get_shipping_postcode(), + 'country' => $order->get_shipping_country(), + ), + 'note' => $order->get_customer_note(), + 'customer_ip' => $order->get_customer_ip_address(), + 'customer_user_agent' => $order->get_customer_user_agent(), + 'customer_id' => $order->get_user_id(), + 'view_order_url' => $order->get_view_order_url(), + 'line_items' => array(), + 'shipping_lines' => array(), + 'tax_lines' => array(), + 'fee_lines' => array(), + 'coupon_lines' => array(), + ); + + // Add line items. + foreach ( $order->get_items() as $item_id => $item ) { + $product = $item->get_product(); + $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; + $item_meta = $item->get_all_formatted_meta_data( $hideprefix ); + + foreach ( $item_meta as $key => $values ) { + $item_meta[ $key ]->label = $values->display_key; + unset( $item_meta[ $key ]->display_key ); + unset( $item_meta[ $key ]->display_value ); + } + + $line_item = array( + 'id' => $item_id, + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), + 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), $dp ), + 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), + 'total_tax' => wc_format_decimal( $item->get_total_tax(), $dp ), + 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), + 'quantity' => $item->get_quantity(), + 'tax_class' => $item->get_tax_class(), + 'name' => $item->get_name(), + 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), + 'sku' => is_object( $product ) ? $product->get_sku() : null, + 'meta' => array_values( $item_meta ), + ); + + if ( in_array( 'products', $expand ) && is_object( $product ) ) { + $_product_data = WC()->api->WC_API_Products->get_product( $product->get_id() ); + + if ( isset( $_product_data['product'] ) ) { + $line_item['product_data'] = $_product_data['product']; + } + } + + $order_data['line_items'][] = $line_item; + } + + // Add shipping. + foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { + $order_data['shipping_lines'][] = array( + 'id' => $shipping_item_id, + 'method_id' => $shipping_item->get_method_id(), + 'method_title' => $shipping_item->get_name(), + 'total' => wc_format_decimal( $shipping_item->get_total(), $dp ), + ); + } + + // Add taxes. + foreach ( $order->get_tax_totals() as $tax_code => $tax ) { + $tax_line = array( + 'id' => $tax->id, + 'rate_id' => $tax->rate_id, + 'code' => $tax_code, + 'title' => $tax->label, + 'total' => wc_format_decimal( $tax->amount, $dp ), + 'compound' => (bool) $tax->is_compound, + ); + + if ( in_array( 'taxes', $expand ) ) { + $_rate_data = WC()->api->WC_API_Taxes->get_tax( $tax->rate_id ); + + if ( isset( $_rate_data['tax'] ) ) { + $tax_line['rate_data'] = $_rate_data['tax']; + } + } + + $order_data['tax_lines'][] = $tax_line; + } + + // Add fees. + foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { + $order_data['fee_lines'][] = array( + 'id' => $fee_item_id, + 'title' => $fee_item->get_name(), + 'tax_class' => $fee_item->get_tax_class(), + 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), + ); + } + + // Add coupons. + foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { + $coupon_line = array( + 'id' => $coupon_item_id, + 'code' => $coupon_item->get_code(), + 'amount' => wc_format_decimal( $coupon_item->get_discount(), $dp ), + ); + + if ( in_array( 'coupons', $expand ) ) { + $_coupon_data = WC()->api->WC_API_Coupons->get_coupon_by_code( $coupon_item->get_code() ); + + if ( ! is_wp_error( $_coupon_data ) && isset( $_coupon_data['coupon'] ) ) { + $coupon_line['coupon_data'] = $_coupon_data['coupon']; + } + } + + $order_data['coupon_lines'][] = $coupon_line; + } + + return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) ); + } + + /** + * Get the total number of orders + * + * @since 2.4 + * + * @param string $status + * @param array $filter + * + * @return array|WP_Error + */ + public function get_orders_count( $status = null, $filter = array() ) { + + try { + if ( ! current_user_can( 'read_private_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), 401 ); + } + + if ( ! empty( $status ) ) { + + if ( 'any' === $status ) { + + $order_statuses = array(); + + foreach ( wc_get_order_statuses() as $slug => $name ) { + $filter['status'] = str_replace( 'wc-', '', $slug ); + $query = $this->query_orders( $filter ); + $order_statuses[ str_replace( 'wc-', '', $slug ) ] = (int) $query->found_posts; + } + + return array( 'count' => $order_statuses ); + + } else { + $filter['status'] = $status; + } + } + + $query = $this->query_orders( $filter ); + + return array( 'count' => (int) $query->found_posts ); + + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a list of valid order statuses + * + * Note this requires no specific permissions other than being an authenticated + * API user. Order statuses (particularly custom statuses) could be considered + * private information which is why it's not in the API index. + * + * @since 2.1 + * @return array + */ + public function get_order_statuses() { + + $order_statuses = array(); + + foreach ( wc_get_order_statuses() as $slug => $name ) { + $order_statuses[ str_replace( 'wc-', '', $slug ) ] = $name; + } + + return array( 'order_statuses' => apply_filters( 'woocommerce_api_order_statuses_response', $order_statuses, $this ) ); + } + + /** + * Create an order + * + * @since 2.2 + * @param array $data raw order data + * @return array|WP_Error + */ + public function create_order( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['order'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order' ), 400 ); + } + + $data = $data['order']; + + // permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order', __( 'You do not have permission to create orders', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_order_data', $data, $this ); + + // default order args, note that status is checked for validity in wc_create_order() + $default_order_args = array( + 'status' => isset( $data['status'] ) ? $data['status'] : '', + 'customer_note' => isset( $data['note'] ) ? $data['note'] : null, + ); + + // if creating order for existing customer + if ( ! empty( $data['customer_id'] ) ) { + + // make sure customer exists + if ( false === get_user_by( 'id', $data['customer_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + $default_order_args['customer_id'] = $data['customer_id']; + } + + // create the pending order + $order = $this->create_base_order( $default_order_args, $data ); + + if ( is_wp_error( $order ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order', sprintf( __( 'Cannot create order: %s', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 ); + } + + // billing/shipping addresses + $this->set_order_addresses( $order, $data ); + + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + + if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { + + $set_item = "set_{$line_type}"; + + foreach ( $data[ $line ] as $item ) { + + $this->$set_item( $order, $item, 'create' ); + } + } + } + + // set is vat exempt + if ( isset( $data['is_vat_exempt'] ) ) { + update_post_meta( $order->get_id(), '_is_vat_exempt', $data['is_vat_exempt'] ? 'yes' : 'no' ); + } + + // calculate totals and set them + $order->calculate_totals(); + + // payment method (and payment_complete() if `paid` == true) + if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { + + // method ID & title are required + if ( empty( $data['payment_details']['method_id'] ) || empty( $data['payment_details']['method_title'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_payment_details', __( 'Payment method ID and title are required', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); + update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); + + // mark as paid if set + if ( isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { + $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); + } + } + + // set order currency + if ( isset( $data['currency'] ) ) { + + if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); + } + + // set order meta + if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { + $this->set_order_meta( $order->get_id(), $data['order_meta'] ); + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + wc_delete_shop_order_transients( $order ); + + do_action( 'woocommerce_api_create_order', $order->get_id(), $data, $this ); + do_action( 'woocommerce_new_order', $order->get_id() ); + + return $this->get_order( $order->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Creates new WC_Order. + * + * Requires a separate function for classes that extend WC_API_Orders. + * + * @since 2.3 + * + * @param $args array + * @param $data + * + * @return WC_Order + */ + protected function create_base_order( $args, $data ) { + return wc_create_order( $args ); + } + + /** + * Edit an order + * + * @since 2.2 + * @param int $id the order ID + * @param array $data + * @return array|WP_Error + */ + public function edit_order( $id, $data ) { + try { + if ( ! isset( $data['order'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order' ), 400 ); + } + + $data = $data['order']; + + $update_totals = false; + + $id = $this->validate_request( $id, $this->post_type, 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $data = apply_filters( 'woocommerce_api_edit_order_data', $data, $id, $this ); + $order = wc_get_order( $id ); + + if ( empty( $order ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); + } + + $order_args = array( 'order_id' => $order->get_id() ); + + // Customer note. + if ( isset( $data['note'] ) ) { + $order_args['customer_note'] = $data['note']; + } + + // Customer ID. + if ( isset( $data['customer_id'] ) && $data['customer_id'] != $order->get_user_id() ) { + // Make sure customer exists. + if ( false === get_user_by( 'id', $data['customer_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_customer_user', $data['customer_id'] ); + } + + // Billing/shipping address. + $this->set_order_addresses( $order, $data ); + + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + + if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { + + $update_totals = true; + + foreach ( $data[ $line ] as $item ) { + // Item ID is always required. + if ( ! array_key_exists( 'id', $item ) ) { + $item['id'] = null; + } + + // Create item. + if ( is_null( $item['id'] ) ) { + $this->set_item( $order, $line_type, $item, 'create' ); + } elseif ( $this->item_is_null( $item ) ) { + // Delete item. + wc_delete_order_item( $item['id'] ); + } else { + // Update item. + $this->set_item( $order, $line_type, $item, 'update' ); + } + } + } + } + + // Payment method (and payment_complete() if `paid` == true and order needs payment). + if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { + + // Method ID. + if ( isset( $data['payment_details']['method_id'] ) ) { + update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); + } + + // Method title. + if ( isset( $data['payment_details']['method_title'] ) ) { + update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); + } + + // Mark as paid if set. + if ( $order->needs_payment() && isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { + $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); + } + } + + // Set order currency. + if ( isset( $data['currency'] ) ) { + if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); + } + + // If items have changed, recalculate order totals. + if ( $update_totals ) { + $order->calculate_totals(); + } + + // Update order meta. + if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { + $this->set_order_meta( $order->get_id(), $data['order_meta'] ); + } + + // Update the order post to set customer note/modified date. + wc_update_order( $order_args ); + + // Order status. + if ( ! empty( $data['status'] ) ) { + // Refresh the order instance. + $order = wc_get_order( $order->get_id() ); + $order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true ); + } + + wc_delete_shop_order_transients( $order ); + + do_action( 'woocommerce_api_edit_order', $order->get_id(), $data, $this ); + do_action( 'woocommerce_update_order', $order->get_id() ); + + return $this->get_order( $id ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete an order + * + * @param int $id the order ID + * @param bool $force true to permanently delete order, false to move to trash + * @return array|WP_Error + */ + public function delete_order( $id, $force = false ) { + + $id = $this->validate_request( $id, $this->post_type, 'delete' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + wc_delete_shop_order_transients( $id ); + + do_action( 'woocommerce_api_delete_order', $id, $this ); + + return $this->delete( $id, 'order', ( 'true' === $force ) ); + } + + /** + * Helper method to get order post objects + * + * @since 2.1 + * @param array $args request arguments for filtering query + * @return WP_Query + */ + protected function query_orders( $args ) { + + // set base query arguments + $query_args = array( + 'fields' => 'ids', + 'post_type' => $this->post_type, + 'post_status' => array_keys( wc_get_order_statuses() ), + ); + + // add status argument + if ( ! empty( $args['status'] ) ) { + $statuses = 'wc-' . str_replace( ',', ',wc-', $args['status'] ); + $statuses = explode( ',', $statuses ); + $query_args['post_status'] = $statuses; + + unset( $args['status'] ); + } + + if ( ! empty( $args['customer_id'] ) ) { + $query_args['meta_query'] = array( + array( + 'key' => '_customer_user', + 'value' => absint( $args['customer_id'] ), + 'compare' => '=', + ), + ); + } + + $query_args = $this->merge_query_args( $query_args, $args ); + + return new WP_Query( $query_args ); + } + + /** + * Helper method to set/update the billing & shipping addresses for + * an order + * + * @since 2.1 + * @param \WC_Order $order + * @param array $data + */ + protected function set_order_addresses( $order, $data ) { + + $address_fields = array( + 'first_name', + 'last_name', + 'company', + 'email', + 'phone', + 'address_1', + 'address_2', + 'city', + 'state', + 'postcode', + 'country', + ); + + $billing_address = $shipping_address = array(); + + // billing address + if ( isset( $data['billing_address'] ) && is_array( $data['billing_address'] ) ) { + + foreach ( $address_fields as $field ) { + + if ( isset( $data['billing_address'][ $field ] ) ) { + $billing_address[ $field ] = wc_clean( $data['billing_address'][ $field ] ); + } + } + + unset( $address_fields['email'] ); + unset( $address_fields['phone'] ); + } + + // shipping address + if ( isset( $data['shipping_address'] ) && is_array( $data['shipping_address'] ) ) { + + foreach ( $address_fields as $field ) { + + if ( isset( $data['shipping_address'][ $field ] ) ) { + $shipping_address[ $field ] = wc_clean( $data['shipping_address'][ $field ] ); + } + } + } + + $this->update_address( $order, $billing_address, 'billing' ); + $this->update_address( $order, $shipping_address, 'shipping' ); + + // update user meta + if ( $order->get_user_id() ) { + foreach ( $billing_address as $key => $value ) { + update_user_meta( $order->get_user_id(), 'billing_' . $key, $value ); + } + foreach ( $shipping_address as $key => $value ) { + update_user_meta( $order->get_user_id(), 'shipping_' . $key, $value ); + } + } + } + + /** + * Update address. + * + * @param WC_Order $order + * @param array $posted + * @param string $type + */ + protected function update_address( $order, $posted, $type = 'billing' ) { + foreach ( $posted as $key => $value ) { + if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { + $order->{"set_{$type}_{$key}"}( $value ); + } + } + } + + /** + * Helper method to add/update order meta, with two restrictions: + * + * 1) Only non-protected meta (no leading underscore) can be set + * 2) Meta values must be scalar (int, string, bool) + * + * @since 2.2 + * @param int $order_id valid order ID + * @param array $order_meta order meta in array( 'meta_key' => 'meta_value' ) format + */ + protected function set_order_meta( $order_id, $order_meta ) { + + foreach ( $order_meta as $meta_key => $meta_value ) { + + if ( is_string( $meta_key ) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) { + update_post_meta( $order_id, $meta_key, $meta_value ); + } + } + } + + /** + * Helper method to check if the resource ID associated with the provided item is null + * + * Items can be deleted by setting the resource ID to null + * + * @since 2.2 + * @param array $item item provided in the request body + * @return bool true if the item resource ID is null, false otherwise + */ + protected function item_is_null( $item ) { + + $keys = array( 'product_id', 'method_id', 'title', 'code' ); + + foreach ( $keys as $key ) { + if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { + return true; + } + } + + return false; + } + + /** + * Wrapper method to create/update order items + * + * When updating, the item ID provided is checked to ensure it is associated + * with the order. + * + * @since 2.2 + * @param \WC_Order $order order + * @param string $item_type + * @param array $item item provided in the request body + * @param string $action either 'create' or 'update' + * @throws WC_API_Exception if item ID is not associated with order + */ + protected function set_item( $order, $item_type, $item, $action ) { + global $wpdb; + + $set_method = "set_{$item_type}"; + + // verify provided line item ID is associated with order + if ( 'update' === $action ) { + + $result = $wpdb->get_row( + $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", + absint( $item['id'] ), + absint( $order->get_id() ) + ) ); + + if ( is_null( $result ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); + } + } + + $this->$set_method( $order, $item, $action ); + } + + /** + * Create or update a line item + * + * @since 2.2 + * @param \WC_Order $order + * @param array $item line item data + * @param string $action 'create' to add line item or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_line_item( $order, $item, $action ) { + $creating = ( 'create' === $action ); + + // product is always required + if ( ! isset( $item['product_id'] ) && ! isset( $item['sku'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID or SKU is required', 'woocommerce' ), 400 ); + } + + // when updating, ensure product ID provided matches + if ( 'update' === $action ) { + + $item_product_id = wc_get_order_item_meta( $item['id'], '_product_id' ); + $item_variation_id = wc_get_order_item_meta( $item['id'], '_variation_id' ); + + if ( $item['product_id'] != $item_product_id && $item['product_id'] != $item_variation_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID provided does not match this line item', 'woocommerce' ), 400 ); + } + } + + if ( isset( $item['product_id'] ) ) { + $product_id = $item['product_id']; + } elseif ( isset( $item['sku'] ) ) { + $product_id = wc_get_product_id_by_sku( $item['sku'] ); + } + + // variations must each have a key & value + $variation_id = 0; + if ( isset( $item['variations'] ) && is_array( $item['variations'] ) ) { + foreach ( $item['variations'] as $key => $value ) { + if ( ! $key || ! $value ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_variation', __( 'The product variation is invalid', 'woocommerce' ), 400 ); + } + } + $variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item['variations'] ); + } + + $product = wc_get_product( $variation_id ? $variation_id : $product_id ); + + // must be a valid WC_Product + if ( ! is_object( $product ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 ); + } + + // quantity must be positive float + if ( isset( $item['quantity'] ) && floatval( $item['quantity'] ) <= 0 ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity must be a positive float.', 'woocommerce' ), 400 ); + } + + // quantity is required when creating + if ( $creating && ! isset( $item['quantity'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 ); + } + + // quantity + if ( $creating ) { + $line_item = new WC_Order_Item_Product(); + } else { + $line_item = new WC_Order_Item_Product( $item['id'] ); + } + + $line_item->set_product( $product ); + $line_item->set_order_id( $order->get_id() ); + + if ( isset( $item['quantity'] ) ) { + $line_item->set_quantity( $item['quantity'] ); + } + if ( isset( $item['total'] ) ) { + $line_item->set_total( floatval( $item['total'] ) ); + } elseif ( $creating ) { + $total = wc_get_price_excluding_tax( $product, array( 'qty' => $line_item->get_quantity() ) ); + $line_item->set_total( $total ); + $line_item->set_subtotal( $total ); + } + if ( isset( $item['total_tax'] ) ) { + $line_item->set_total_tax( floatval( $item['total_tax'] ) ); + } + if ( isset( $item['subtotal'] ) ) { + $line_item->set_subtotal( floatval( $item['subtotal'] ) ); + } + if ( isset( $item['subtotal_tax'] ) ) { + $line_item->set_subtotal_tax( floatval( $item['subtotal_tax'] ) ); + } + if ( $variation_id ) { + $line_item->set_variation_id( $variation_id ); + $line_item->set_variation( $item['variations'] ); + } + + // Save or add to order. + if ( $creating ) { + $order->add_item( $line_item ); + } else { + $item_id = $line_item->save(); + + if ( ! $item_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Given a product ID & API provided variations, find the correct variation ID to use for calculation + * We can't just trust input from the API to pass a variation_id manually, otherwise you could pass + * the cheapest variation ID but provide other information so we have to look up the variation ID. + * + * @param WC_Product $product Product instance + * @param array $variations + * + * @return int Returns an ID if a valid variation was found for this product + */ + public function get_variation_id( $product, $variations = array() ) { + $variation_id = null; + $variations_normalized = array(); + + if ( $product->is_type( 'variable' ) && $product->has_child() ) { + if ( isset( $variations ) && is_array( $variations ) ) { + // start by normalizing the passed variations + foreach ( $variations as $key => $value ) { + $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); // from get_attributes in class-wc-api-products.php + $variations_normalized[ $key ] = strtolower( $value ); + } + // now search through each product child and see if our passed variations match anything + foreach ( $product->get_children() as $variation ) { + $meta = array(); + foreach ( get_post_meta( $variation ) as $key => $value ) { + $value = $value[0]; + $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); + $meta[ $key ] = strtolower( $value ); + } + // if the variation array is a part of the $meta array, we found our match + if ( $this->array_contains( $variations_normalized, $meta ) ) { + $variation_id = $variation; + break; + } + } + } + } + + return $variation_id; + } + + /** + * Utility function to see if the meta array contains data from variations + * + * @param array $needles + * @param array $haystack + * + * @return bool + */ + protected function array_contains( $needles, $haystack ) { + foreach ( $needles as $key => $value ) { + if ( $haystack[ $key ] !== $value ) { + return false; + } + } + return true; + } + + /** + * Create or update an order shipping method + * + * @since 2.2 + * @param \WC_Order $order + * @param array $shipping item data + * @param string $action 'create' to add shipping or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_shipping( $order, $shipping, $action ) { + + // total must be a positive float + if ( isset( $shipping['total'] ) && floatval( $shipping['total'] ) < 0 ) { + throw new WC_API_Exception( 'woocommerce_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + + // method ID is required + if ( ! isset( $shipping['method_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); + } + + $rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] ); + $item = new WC_Order_Item_Shipping(); + $item->set_order_id( $order->get_id() ); + $item->set_shipping_rate( $rate ); + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Shipping( $shipping['id'] ); + + if ( isset( $shipping['method_id'] ) ) { + $item->set_method_id( $shipping['method_id'] ); + } + + if ( isset( $shipping['method_title'] ) ) { + $item->set_method_title( $shipping['method_title'] ); + } + + if ( isset( $shipping['total'] ) ) { + $item->set_total( floatval( $shipping['total'] ) ); + } + + $shipping_id = $item->save(); + + if ( ! $shipping_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order fee + * + * @since 2.2 + * @param \WC_Order $order + * @param array $fee item data + * @param string $action 'create' to add fee or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_fee( $order, $fee, $action ) { + + if ( 'create' === $action ) { + + // fee title is required + if ( ! isset( $fee['title'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee title is required', 'woocommerce' ), 400 ); + } + + $item = new WC_Order_Item_Fee(); + $item->set_order_id( $order->get_id() ); + $item->set_name( wc_clean( $fee['title'] ) ); + $item->set_total( isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0 ); + + // if taxable, tax class and total are required + if ( ! empty( $fee['taxable'] ) ) { + if ( ! isset( $fee['tax_class'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 ); + } + + $item->set_tax_status( 'taxable' ); + $item->set_tax_class( $fee['tax_class'] ); + + if ( isset( $fee['total_tax'] ) ) { + $item->set_total_tax( isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0 ); + } + + if ( isset( $fee['tax_data'] ) ) { + $item->set_total_tax( wc_format_refund_total( array_sum( $fee['tax_data'] ) ) ); + $item->set_taxes( array_map( 'wc_format_refund_total', $fee['tax_data'] ) ); + } + } + + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Fee( $fee['id'] ); + + if ( isset( $fee['title'] ) ) { + $item->set_name( wc_clean( $fee['title'] ) ); + } + + if ( isset( $fee['tax_class'] ) ) { + $item->set_tax_class( $fee['tax_class'] ); + } + + if ( isset( $fee['total'] ) ) { + $item->set_total( floatval( $fee['total'] ) ); + } + + if ( isset( $fee['total_tax'] ) ) { + $item->set_total_tax( floatval( $fee['total_tax'] ) ); + } + + $fee_id = $item->save(); + + if ( ! $fee_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order coupon + * + * @since 2.2 + * @param \WC_Order $order + * @param array $coupon item data + * @param string $action 'create' to add coupon or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_coupon( $order, $coupon, $action ) { + + // coupon amount must be positive float + if ( isset( $coupon['amount'] ) && floatval( $coupon['amount'] ) < 0 ) { + throw new WC_API_Exception( 'woocommerce_invalid_coupon_total', __( 'Coupon discount total must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + + // coupon code is required + if ( empty( $coupon['code'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + + $item = new WC_Order_Item_Coupon(); + $item->set_props( array( + 'code' => $coupon['code'], + 'discount' => isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0, + 'discount_tax' => 0, + 'order_id' => $order->get_id(), + ) ); + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Coupon( $coupon['id'] ); + + if ( isset( $coupon['code'] ) ) { + $item->set_code( $coupon['code'] ); + } + + if ( isset( $coupon['amount'] ) ) { + $item->set_discount( floatval( $coupon['amount'] ) ); + } + + $coupon_id = $item->save(); + + if ( ! $coupon_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Get the admin order notes for an order + * + * @since 2.1 + * @param string $order_id order ID + * @param string|null $fields fields to include in response + * @return array|WP_Error + */ + public function get_order_notes( $order_id, $fields = null ) { + + // ensure ID is valid order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $args = array( + 'post_id' => $order_id, + 'approve' => 'approve', + 'type' => 'order_note', + ); + + remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $notes = get_comments( $args ); + + add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $order_notes = array(); + + foreach ( $notes as $note ) { + + $order_notes[] = current( $this->get_order_note( $order_id, $note->comment_ID, $fields ) ); + } + + return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $order_id, $fields, $notes, $this->server ) ); + } + + /** + * Get an order note for the given order ID and ID + * + * @since 2.2 + * + * @param string $order_id order ID + * @param string $id order note ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_order_note( $order_id, $id, $fields = null ) { + try { + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $order_note = array( + 'id' => $note->comment_ID, + 'created_at' => $this->server->format_datetime( $note->comment_date_gmt ), + 'note' => $note->comment_content, + 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), + ); + + return array( 'order_note' => apply_filters( 'woocommerce_api_order_note_response', $order_note, $id, $fields, $note, $order_id, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new order note for the given order + * + * @since 2.2 + * @param string $order_id order ID + * @param array $data raw request data + * @return WP_Error|array error or created note response data + */ + public function create_order_note( $order_id, $data ) { + try { + if ( ! isset( $data['order_note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_note' ), 400 ); + } + + $data = $data['order_note']; + + // permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_note', __( 'You do not have permission to create order notes', 'woocommerce' ), 401 ); + } + + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $order = wc_get_order( $order_id ); + + $data = apply_filters( 'woocommerce_api_create_order_note_data', $data, $order_id, $this ); + + // note content is required + if ( ! isset( $data['note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note', __( 'Order note is required', 'woocommerce' ), 400 ); + } + + $is_customer_note = ( isset( $data['customer_note'] ) && true === $data['customer_note'] ); + + // create the note + $note_id = $order->add_order_note( $data['note'], $is_customer_note ); + + if ( ! $note_id ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), 500 ); + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + do_action( 'woocommerce_api_create_order_note', $note_id, $order_id, $this ); + + return $this->get_order_note( $order->get_id(), $note_id ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit the order note + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id note ID + * @param array $data parsed request data + * @return WP_Error|array error or edited note response data + */ + public function edit_order_note( $order_id, $id, $data ) { + try { + if ( ! isset( $data['order_note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_note' ), 400 ); + } + + $data = $data['order_note']; + + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $order = wc_get_order( $order_id ); + + // Validate note ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + // Ensure note ID is valid + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + // Ensure note ID is associated with given order + if ( $note->comment_post_ID != $order->get_id() ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_edit_order_note_data', $data, $note->comment_ID, $order->get_id(), $this ); + + // Note content + if ( isset( $data['note'] ) ) { + + wp_update_comment( + array( + 'comment_ID' => $note->comment_ID, + 'comment_content' => $data['note'], + ) + ); + } + + // Customer note + if ( isset( $data['customer_note'] ) ) { + + update_comment_meta( $note->comment_ID, 'is_customer_note', true === $data['customer_note'] ? 1 : 0 ); + } + + do_action( 'woocommerce_api_edit_order_note', $note->comment_ID, $order->get_id(), $this ); + + return $this->get_order_note( $order->get_id(), $note->comment_ID ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete order note + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id note ID + * @return WP_Error|array error or deleted message + */ + public function delete_order_note( $order_id, $id ) { + try { + $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate note ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + // Ensure note ID is valid + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + // Ensure note ID is associated with given order + if ( $note->comment_post_ID != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); + } + + // Force delete since trashed order notes could not be managed through comments list table + $result = wc_delete_order_note( $note->comment_ID ); + + if ( ! $result ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_order_note', __( 'This order note cannot be deleted', 'woocommerce' ), 500 ); + } + + do_action( 'woocommerce_api_delete_order_note', $note->comment_ID, $order_id, $this ); + + return array( 'message' => __( 'Permanently deleted order note', 'woocommerce' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the order refunds for an order + * + * @since 2.2 + * @param string $order_id order ID + * @param string|null $fields fields to include in response + * @return array|WP_Error + */ + public function get_order_refunds( $order_id, $fields = null ) { + + // Ensure ID is valid order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $refund_items = wc_get_orders( array( + 'type' => 'shop_order_refund', + 'parent' => $order_id, + 'limit' => -1, + 'return' => 'ids', + ) ); + $order_refunds = array(); + + foreach ( $refund_items as $refund_id ) { + $order_refunds[] = current( $this->get_order_refund( $order_id, $refund_id, $fields ) ); + } + + return array( 'order_refunds' => apply_filters( 'woocommerce_api_order_refunds_response', $order_refunds, $order_id, $fields, $refund_items, $this ) ); + } + + /** + * Get an order refund for the given order ID and ID + * + * @since 2.2 + * + * @param string $order_id order ID + * @param int $id + * @param string|null $fields fields to limit response to + * @param array $filter + * + * @return array|WP_Error + */ + public function get_order_refund( $order_id, $id, $fields = null, $filter = array() ) { + try { + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + $order = wc_get_order( $order_id ); + $refund = wc_get_order( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + $line_items = array(); + + // Add line items + foreach ( $refund->get_items( 'line_item' ) as $item_id => $item ) { + $product = $item->get_product(); + $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; + $item_meta = $item->get_all_formatted_meta_data( $hideprefix ); + + foreach ( $item_meta as $key => $values ) { + $item_meta[ $key ]->label = $values->display_key; + unset( $item_meta[ $key ]->display_key ); + unset( $item_meta[ $key ]->display_value ); + } + + $line_items[] = array( + 'id' => $item_id, + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ), + 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), 2 ), + 'total' => wc_format_decimal( $order->get_line_total( $item ), 2 ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $item ), 2 ), + 'price' => wc_format_decimal( $order->get_item_total( $item ), 2 ), + 'quantity' => $item->get_quantity(), + 'tax_class' => $item->get_tax_class(), + 'name' => $item->get_name(), + 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), + 'sku' => is_object( $product ) ? $product->get_sku() : null, + 'meta' => array_values( $item_meta ), + 'refunded_item_id' => (int) $item->get_meta( 'refunded_item_id' ), + ); + } + + $order_refund = array( + 'id' => $refund->get_id(), + 'created_at' => $this->server->format_datetime( $refund->get_date_created() ? $refund->get_date_created()->getTimestamp() : 0, false, false ), + 'amount' => wc_format_decimal( $refund->get_amount(), 2 ), + 'reason' => $refund->get_reason(), + 'line_items' => $line_items, + ); + + return array( 'order_refund' => apply_filters( 'woocommerce_api_order_refund_response', $order_refund, $id, $fields, $refund, $order_id, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new order refund for the given order + * + * @since 2.2 + * @param string $order_id order ID + * @param array $data raw request data + * @param bool $api_refund do refund using a payment gateway API + * @return WP_Error|array error or created refund response data + */ + public function create_order_refund( $order_id, $data, $api_refund = true ) { + try { + if ( ! isset( $data['order_refund'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_refund' ), 400 ); + } + + $data = $data['order_refund']; + + // Permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_refund', __( 'You do not have permission to create order refunds', 'woocommerce' ), 401 ); + } + + $order_id = absint( $order_id ); + + if ( empty( $order_id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_create_order_refund_data', $data, $order_id, $this ); + + // Refund amount is required + if ( ! isset( $data['amount'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount is required.', 'woocommerce' ), 400 ); + } elseif ( 0 > $data['amount'] ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount must be positive.', 'woocommerce' ), 400 ); + } + + $data['order_id'] = $order_id; + $data['refund_id'] = 0; + + // Create the refund + $refund = wc_create_refund( $data ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); + } + + // Refund via API + if ( $api_refund ) { + if ( WC()->payment_gateways() ) { + $payment_gateways = WC()->payment_gateways->payment_gateways(); + } + + $order = wc_get_order( $order_id ); + + if ( isset( $payment_gateways[ $order->get_payment_method() ] ) && $payment_gateways[ $order->get_payment_method() ]->supports( 'refunds' ) ) { + $result = $payment_gateways[ $order->get_payment_method() ]->process_refund( $order_id, $refund->get_amount(), $refund->get_reason() ); + + if ( is_wp_error( $result ) ) { + return $result; + } elseif ( ! $result ) { + throw new WC_API_Exception( 'woocommerce_api_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 ); + } + } + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + do_action( 'woocommerce_api_create_order_refund', $refund->get_id(), $order_id, $this ); + + return $this->get_order_refund( $order_id, $refund->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit an order refund + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id refund ID + * @param array $data parsed request data + * @return WP_Error|array error or edited refund response data + */ + public function edit_order_refund( $order_id, $id, $data ) { + try { + if ( ! isset( $data['order_refund'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_refund' ), 400 ); + } + + $data = $data['order_refund']; + + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate refund ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + // Ensure order ID is valid + $refund = get_post( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + // Ensure refund ID is associated with given order + if ( $refund->post_parent != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_edit_order_refund_data', $data, $refund->ID, $order_id, $this ); + + // Update reason + if ( isset( $data['reason'] ) ) { + $updated_refund = wp_update_post( array( 'ID' => $refund->ID, 'post_excerpt' => $data['reason'] ) ); + + if ( is_wp_error( $updated_refund ) ) { + return $updated_refund; + } + } + + // Update refund amount + if ( isset( $data['amount'] ) && 0 < $data['amount'] ) { + update_post_meta( $refund->ID, '_refund_amount', wc_format_decimal( $data['amount'] ) ); + } + + do_action( 'woocommerce_api_edit_order_refund', $refund->ID, $order_id, $this ); + + return $this->get_order_refund( $order_id, $refund->ID ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete order refund + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id refund ID + * @return WP_Error|array error or deleted message + */ + public function delete_order_refund( $order_id, $id ) { + try { + $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate refund ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + // Ensure refund ID is valid + $refund = get_post( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + // Ensure refund ID is associated with given order + if ( $refund->post_parent != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); + } + + wc_delete_shop_order_transients( $order_id ); + + do_action( 'woocommerce_api_delete_order_refund', $refund->ID, $order_id, $this ); + + return $this->delete( $refund->ID, 'refund', true ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Bulk update or insert orders + * Accepts an array with orders in the formats supported by + * WC_API_Orders->create_order() and WC_API_Orders->edit_order() + * + * @since 2.4.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + + try { + if ( ! isset( $data['orders'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_orders_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'orders' ), 400 ); + } + + $data = $data['orders']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'orders' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_orders_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $orders = array(); + + foreach ( $data as $_order ) { + $order_id = 0; + + // Try to get the order ID + if ( isset( $_order['id'] ) ) { + $order_id = intval( $_order['id'] ); + } + + if ( $order_id ) { + + // Order exists / edit order + $edit = $this->edit_order( $order_id, array( 'order' => $_order ) ); + + if ( is_wp_error( $edit ) ) { + $orders[] = array( + 'id' => $order_id, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $orders[] = $edit['order']; + } + } else { + + // Order don't exists / create order + $new = $this->create_order( array( 'order' => $_order ) ); + + if ( is_wp_error( $new ) ) { + $orders[] = array( + 'id' => $order_id, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $orders[] = $new['order']; + } + } + } + + return array( 'orders' => apply_filters( 'woocommerce_api_orders_bulk_response', $orders, $this ) ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-products.php b/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-products.php new file mode 100644 index 00000000000..fb5a37ed92b --- /dev/null +++ b/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-products.php @@ -0,0 +1,3310 @@ + + * GET /products//reviews + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET/POST /products + $routes[ $this->base ] = array( + array( array( $this, 'get_products' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /products/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ), + ); + + # GET/PUT/DELETE /products/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_product' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product' ), WC_API_Server::DELETABLE ), + ); + + # GET /products//reviews + $routes[ $this->base . '/(?P\d+)/reviews' ] = array( + array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ), + ); + + # GET /products//orders + $routes[ $this->base . '/(?P\d+)/orders' ] = array( + array( array( $this, 'get_product_orders' ), WC_API_Server::READABLE ), + ); + + # GET/POST /products/categories + $routes[ $this->base . '/categories' ] = array( + array( array( $this, 'get_product_categories' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_category' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /products/categories/ + $routes[ $this->base . '/categories/(?P\d+)' ] = array( + array( array( $this, 'get_product_category' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_category' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_category' ), WC_API_Server::DELETABLE ), + ); + + # GET/POST /products/tags + $routes[ $this->base . '/tags' ] = array( + array( array( $this, 'get_product_tags' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_tag' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /products/tags/ + $routes[ $this->base . '/tags/(?P\d+)' ] = array( + array( array( $this, 'get_product_tag' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_tag' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_tag' ), WC_API_Server::DELETABLE ), + ); + + # GET/POST /products/shipping_classes + $routes[ $this->base . '/shipping_classes' ] = array( + array( array( $this, 'get_product_shipping_classes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_shipping_class' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /products/shipping_classes/ + $routes[ $this->base . '/shipping_classes/(?P\d+)' ] = array( + array( array( $this, 'get_product_shipping_class' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_shipping_class' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_shipping_class' ), WC_API_Server::DELETABLE ), + ); + + # GET/POST /products/attributes + $routes[ $this->base . '/attributes' ] = array( + array( array( $this, 'get_product_attributes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_attribute' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /products/attributes/ + $routes[ $this->base . '/attributes/(?P\d+)' ] = array( + array( array( $this, 'get_product_attribute' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_attribute' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_attribute' ), WC_API_Server::DELETABLE ), + ); + + # GET/POST /products/attributes//terms + $routes[ $this->base . '/attributes/(?P\d+)/terms' ] = array( + array( array( $this, 'get_product_attribute_terms' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_attribute_term' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /products/attributes//terms/ + $routes[ $this->base . '/attributes/(?P\d+)/terms/(?P\d+)' ] = array( + array( array( $this, 'get_product_attribute_term' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_attribute_term' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_attribute_term' ), WC_API_Server::DELETABLE ), + ); + + # POST|PUT /products/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + return $routes; + } + + /** + * Get all products + * + * @since 2.1 + * @param string $fields + * @param string $type + * @param array $filter + * @param int $page + * @return array + */ + public function get_products( $fields = null, $type = null, $filter = array(), $page = 1 ) { + + if ( ! empty( $type ) ) { + $filter['type'] = $type; + } + + $filter['page'] = $page; + + $query = $this->query_products( $filter ); + + $products = array(); + + foreach ( $query->posts as $product_id ) { + + if ( ! $this->is_readable( $product_id ) ) { + continue; + } + + $products[] = current( $this->get_product( $product_id, $fields ) ); + } + + $this->server->add_pagination_headers( $query ); + + return array( 'products' => $products ); + } + + /** + * Get the product for the given ID + * + * @since 2.1 + * @param int $id the product ID + * @param string $fields + * @return array|WP_Error + */ + public function get_product( $id, $fields = null ) { + + $id = $this->validate_request( $id, 'product', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $product = wc_get_product( $id ); + + // add data that applies to every product type + $product_data = $this->get_product_data( $product ); + + // add variations to variable products + if ( $product->is_type( 'variable' ) && $product->has_child() ) { + $product_data['variations'] = $this->get_variation_data( $product ); + } + + // add the parent product data to an individual variation + if ( $product->is_type( 'variation' ) && $product->get_parent_id() ) { + $product_data['parent'] = $this->get_product_data( $product->get_parent_id() ); + } + + // Add grouped products data + if ( $product->is_type( 'grouped' ) && $product->has_child() ) { + $product_data['grouped_products'] = $this->get_grouped_products_data( $product ); + } + + if ( $product->is_type( 'simple' ) ) { + $parent_id = $product->get_parent_id(); + if ( ! empty( $parent_id ) ) { + $_product = wc_get_product( $parent_id ); + $product_data['parent'] = $this->get_product_data( $_product ); + } + } + + return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) ); + } + + /** + * Get the total number of products + * + * @since 2.1 + * + * @param string $type + * @param array $filter + * + * @return array|WP_Error + */ + public function get_products_count( $type = null, $filter = array() ) { + try { + if ( ! current_user_can( 'read_private_products' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), 401 ); + } + + if ( ! empty( $type ) ) { + $filter['type'] = $type; + } + + $query = $this->query_products( $filter ); + + return array( 'count' => (int) $query->found_posts ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new product. + * + * @since 2.2 + * + * @param array $data posted data + * + * @return array|WP_Error + */ + public function create_product( $data ) { + $id = 0; + + try { + if ( ! isset( $data['product'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product' ), 400 ); + } + + $data = $data['product']; + + // Check permissions. + if ( ! current_user_can( 'publish_products' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product', __( 'You do not have permission to create products', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_product_data', $data, $this ); + + // Check if product title is specified. + if ( ! isset( $data['title'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_title', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'title' ), 400 ); + } + + // Check product type. + if ( ! isset( $data['type'] ) ) { + $data['type'] = 'simple'; + } + + // Set visible visibility when not sent. + if ( ! isset( $data['catalog_visibility'] ) ) { + $data['catalog_visibility'] = 'visible'; + } + + // Validate the product type. + if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); + } + + // Enable description html tags. + $post_content = isset( $data['description'] ) ? wc_clean( $data['description'] ) : ''; + if ( $post_content && isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) { + + $post_content = wp_filter_post_kses( $data['description'] ); + } + + // Enable short description html tags. + $post_excerpt = isset( $data['short_description'] ) ? wc_clean( $data['short_description'] ) : ''; + if ( $post_excerpt && isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) { + $post_excerpt = wp_filter_post_kses( $data['short_description'] ); + } + + $classname = WC_Product_Factory::get_classname_from_product_type( $data['type'] ); + if ( ! class_exists( $classname ) ) { + $classname = 'WC_Product_Simple'; + } + $product = new $classname(); + + $product->set_name( wc_clean( $data['title'] ) ); + $product->set_status( isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish' ); + $product->set_short_description( isset( $data['short_description'] ) ? $post_excerpt : '' ); + $product->set_description( isset( $data['description'] ) ? $post_content : '' ); + $product->set_menu_order( isset( $data['menu_order'] ) ? intval( $data['menu_order'] ) : 0 ); + + if ( ! empty( $data['name'] ) ) { + $product->set_slug( sanitize_title( $data['name'] ) ); + } + + // Attempts to create the new product. + $product->save(); + $id = $product->get_id(); + + // Checks for an error in the product creation. + if ( 0 >= $id ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product', $id->get_error_message(), 400 ); + } + + // Check for featured/gallery images, upload it and set it. + if ( isset( $data['images'] ) ) { + $product = $this->save_product_images( $product, $data['images'] ); + } + + // Save product meta fields. + $product = $this->save_product_meta( $product, $data ); + $product->save(); + + // Save variations. + if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) { + $this->save_variations( $product, $data ); + } + + do_action( 'woocommerce_api_create_product', $id, $data ); + + // Clear cache/transients. + wc_delete_product_transients( $id ); + + $this->server->send_status( 201 ); + + return $this->get_product( $id ); + } catch ( WC_Data_Exception $e ) { + $this->clear_product( $id ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } catch ( WC_API_Exception $e ) { + $this->clear_product( $id ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product + * + * @since 2.2 + * + * @param int $id the product ID + * @param array $data + * + * @return array|WP_Error + */ + public function edit_product( $id, $data ) { + try { + if ( ! isset( $data['product'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product' ), 400 ); + } + + $data = $data['product']; + + $id = $this->validate_request( $id, 'product', 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $product = wc_get_product( $id ); + + $data = apply_filters( 'woocommerce_api_edit_product_data', $data, $this ); + + // Product title. + if ( isset( $data['title'] ) ) { + $product->set_name( wc_clean( $data['title'] ) ); + } + + // Product name (slug). + if ( isset( $data['name'] ) ) { + $product->set_slug( wc_clean( $data['name'] ) ); + } + + // Product status. + if ( isset( $data['status'] ) ) { + $product->set_status( wc_clean( $data['status'] ) ); + } + + // Product short description. + if ( isset( $data['short_description'] ) ) { + // Enable short description html tags. + $post_excerpt = ( isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) ? wp_filter_post_kses( $data['short_description'] ) : wc_clean( $data['short_description'] ); + $product->set_short_description( $post_excerpt ); + } + + // Product description. + if ( isset( $data['description'] ) ) { + // Enable description html tags. + $post_content = ( isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) ? wp_filter_post_kses( $data['description'] ) : wc_clean( $data['description'] ); + $product->set_description( $post_content ); + } + + // Validate the product type. + if ( isset( $data['type'] ) && ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); + } + + // Menu order. + if ( isset( $data['menu_order'] ) ) { + $product->set_menu_order( intval( $data['menu_order'] ) ); + } + + // Check for featured/gallery images, upload it and set it. + if ( isset( $data['images'] ) ) { + $product = $this->save_product_images( $product, $data['images'] ); + } + + // Save product meta fields. + $product = $this->save_product_meta( $product, $data ); + + // Save variations. + if ( $product->is_type( 'variable' ) ) { + if ( isset( $data['variations'] ) && is_array( $data['variations'] ) ) { + $this->save_variations( $product, $data ); + } else { + // Just sync variations. + $product = WC_Product_Variable::sync( $product, false ); + } + } + + $product->save(); + + do_action( 'woocommerce_api_edit_product', $id, $data ); + + // Clear cache/transients. + wc_delete_product_transients( $id ); + + return $this->get_product( $id ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product. + * + * @since 2.2 + * + * @param int $id the product ID. + * @param bool $force true to permanently delete order, false to move to trash. + * + * @return array|WP_Error + */ + public function delete_product( $id, $force = false ) { + + $id = $this->validate_request( $id, 'product', 'delete' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $product = wc_get_product( $id ); + + do_action( 'woocommerce_api_delete_product', $id, $this ); + + // If we're forcing, then delete permanently. + if ( $force ) { + if ( $product->is_type( 'variable' ) ) { + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->delete( true ); + } + } + } else { + // For other product types, if the product has children, remove the relationship. + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->set_parent_id( 0 ); + $child->save(); + } + } + } + + $product->delete( true ); + $result = ! ( $product->get_id() > 0 ); + } else { + $product->delete(); + $result = 'trash' === $product->get_status(); + } + + if ( ! $result ) { + return new WP_Error( 'woocommerce_api_cannot_delete_product', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product' ), array( 'status' => 500 ) ); + } + + // Delete parent product transients. + if ( $parent_id = wp_get_post_parent_id( $id ) ) { + wc_delete_product_transients( $parent_id ); + } + + if ( $force ) { + return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), 'product' ) ); + } else { + $this->server->send_status( '202' ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product' ) ); + } + } + + /** + * Get the reviews for a product + * + * @since 2.1 + * @param int $id the product ID to get reviews for + * @param string $fields fields to include in response + * @return array|WP_Error + */ + public function get_product_reviews( $id, $fields = null ) { + + $id = $this->validate_request( $id, 'product', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $comments = get_approved_comments( $id ); + $reviews = array(); + + foreach ( $comments as $comment ) { + + $reviews[] = array( + 'id' => intval( $comment->comment_ID ), + 'created_at' => $this->server->format_datetime( $comment->comment_date_gmt ), + 'review' => $comment->comment_content, + 'rating' => get_comment_meta( $comment->comment_ID, 'rating', true ), + 'reviewer_name' => $comment->comment_author, + 'reviewer_email' => $comment->comment_author_email, + 'verified' => wc_review_is_from_verified_owner( $comment->comment_ID ), + ); + } + + return array( 'product_reviews' => apply_filters( 'woocommerce_api_product_reviews_response', $reviews, $id, $fields, $comments, $this->server ) ); + } + + /** + * Get the orders for a product + * + * @since 2.4.0 + * @param int $id the product ID to get orders for + * @param string fields fields to retrieve + * @param array $filter filters to include in response + * @param string $status the order status to retrieve + * @param $page $page page to retrieve + * @return array|WP_Error + */ + public function get_product_orders( $id, $fields = null, $filter = array(), $status = null, $page = 1 ) { + global $wpdb; + + $id = $this->validate_request( $id, 'product', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $order_ids = $wpdb->get_col( $wpdb->prepare( " + SELECT order_id + FROM {$wpdb->prefix}woocommerce_order_items + WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) + AND order_item_type = 'line_item' + ", $id ) ); + + if ( empty( $order_ids ) ) { + return array( 'orders' => array() ); + } + + $filter = array_merge( $filter, array( + 'in' => implode( ',', $order_ids ), + ) ); + + $orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, $status, $page ); + + return array( 'orders' => apply_filters( 'woocommerce_api_product_orders_response', $orders['orders'], $id, $filter, $fields, $this->server ) ); + } + + /** + * Get a listing of product categories + * + * @since 2.2 + * + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_categories( $fields = null ) { + try { + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); + } + + $product_categories = array(); + + $terms = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'ids' ) ); + + foreach ( $terms as $term_id ) { + $product_categories[] = current( $this->get_product_category( $term_id, $fields ) ); + } + + return array( 'product_categories' => apply_filters( 'woocommerce_api_product_categories_response', $product_categories, $terms, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product category for the given ID + * + * @since 2.2 + * + * @param string $id product category term ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_category( $id, $fields = null ) { + try { + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'Invalid product category ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); + } + + $term = get_term( $id, 'product_cat' ); + + if ( is_wp_error( $term ) || is_null( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'A product category with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $term_id = intval( $term->term_id ); + + // Get category display type + $display_type = get_term_meta( $term_id, 'display_type', true ); + + // Get category image + $image = ''; + if ( $image_id = get_term_meta( $term_id, 'thumbnail_id', true ) ) { + $image = wp_get_attachment_url( $image_id ); + } + + $product_category = array( + 'id' => $term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'parent' => $term->parent, + 'description' => $term->description, + 'display' => $display_type ? $display_type : 'default', + 'image' => $image ? esc_url( $image ) : '', + 'count' => intval( $term->count ), + ); + + return array( 'product_category' => apply_filters( 'woocommerce_api_product_category_response', $product_category, $id, $fields, $term, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new product category. + * + * @since 2.5.0 + * @param array $data Posted data + * @return array|WP_Error Product category if succeed, otherwise WP_Error + * will be returned + */ + public function create_product_category( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_category'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_category_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_category' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_category', __( 'You do not have permission to create product categories', 'woocommerce' ), 401 ); + } + + $defaults = array( + 'name' => '', + 'slug' => '', + 'description' => '', + 'parent' => 0, + 'display' => 'default', + 'image' => '', + ); + + $data = wp_parse_args( $data['product_category'], $defaults ); + $data = apply_filters( 'woocommerce_api_create_product_category_data', $data, $this ); + + // Check parent. + $data['parent'] = absint( $data['parent'] ); + if ( $data['parent'] ) { + $parent = get_term_by( 'id', $data['parent'], 'product_cat' ); + if ( ! $parent ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_parent', __( 'Product category parent is invalid', 'woocommerce' ), 400 ); + } + } + + // If value of image is numeric, assume value as image_id. + $image = $data['image']; + $image_id = 0; + if ( is_numeric( $image ) ) { + $image_id = absint( $image ); + } elseif ( ! empty( $image ) ) { + $upload = $this->upload_product_category_image( esc_url_raw( $image ) ); + $image_id = $this->set_product_category_image_as_attachment( $upload ); + } + + $insert = wp_insert_term( $data['name'], 'product_cat', $data ); + if ( is_wp_error( $insert ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_category', $insert->get_error_message(), 400 ); + } + + $id = $insert['term_id']; + + update_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) ); + + // Check if image_id is a valid image attachment before updating the term meta. + if ( $image_id && wp_attachment_is_image( $image_id ) ) { + update_term_meta( $id, 'thumbnail_id', $image_id ); + } + + do_action( 'woocommerce_api_create_product_category', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_product_category( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product category. + * + * @since 2.5.0 + * @param int $id Product category term ID + * @param array $data Posted data + * @return array|WP_Error Product category if succeed, otherwise WP_Error + * will be returned + */ + public function edit_product_category( $id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_category'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_category', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_category' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_category']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_category', __( 'You do not have permission to edit product categories', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_category_data', $data, $this ); + $category = $this->get_product_category( $id ); + + if ( is_wp_error( $category ) ) { + return $category; + } + + if ( isset( $data['image'] ) ) { + $image_id = 0; + + // If value of image is numeric, assume value as image_id. + $image = $data['image']; + if ( is_numeric( $image ) ) { + $image_id = absint( $image ); + } elseif ( ! empty( $image ) ) { + $upload = $this->upload_product_category_image( esc_url_raw( $image ) ); + $image_id = $this->set_product_category_image_as_attachment( $upload ); + } + + // In case client supplies invalid image or wants to unset category image. + if ( ! wp_attachment_is_image( $image_id ) ) { + $image_id = ''; + } + } + + $update = wp_update_term( $id, 'product_cat', $data ); + if ( is_wp_error( $update ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_category', __( 'Could not edit the category', 'woocommerce' ), 400 ); + } + + if ( ! empty( $data['display'] ) ) { + update_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) ); + } + + if ( isset( $image_id ) ) { + update_term_meta( $id, 'thumbnail_id', $image_id ); + } + + do_action( 'woocommerce_api_edit_product_category', $id, $data ); + + return $this->get_product_category( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product category. + * + * @since 2.5.0 + * @param int $id Product category term ID + * @return array|WP_Error Success message if succeed, otherwise WP_Error + * will be returned + */ + public function delete_product_category( $id ) { + global $wpdb; + + try { + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_category', __( 'You do not have permission to delete product category', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + $deleted = wp_delete_term( $id, 'product_cat' ); + if ( ! $deleted || is_wp_error( $deleted ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_category', __( 'Could not delete the category', 'woocommerce' ), 401 ); + } + + do_action( 'woocommerce_api_delete_product_category', $id, $this ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_category' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a listing of product tags. + * + * @since 2.5.0 + * + * @param string|null $fields Fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_tags( $fields = null ) { + try { + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_tags', __( 'You do not have permission to read product tags', 'woocommerce' ), 401 ); + } + + $product_tags = array(); + + $terms = get_terms( 'product_tag', array( 'hide_empty' => false, 'fields' => 'ids' ) ); + + foreach ( $terms as $term_id ) { + $product_tags[] = current( $this->get_product_tag( $term_id, $fields ) ); + } + + return array( 'product_tags' => apply_filters( 'woocommerce_api_product_tags_response', $product_tags, $terms, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product tag for the given ID. + * + * @since 2.5.0 + * + * @param string $id Product tag term ID + * @param string|null $fields Fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_tag( $id, $fields = null ) { + try { + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_tag_id', __( 'Invalid product tag ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_tags', __( 'You do not have permission to read product tags', 'woocommerce' ), 401 ); + } + + $term = get_term( $id, 'product_tag' ); + + if ( is_wp_error( $term ) || is_null( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_tag_id', __( 'A product tag with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $term_id = intval( $term->term_id ); + + $tag = array( + 'id' => $term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'description' => $term->description, + 'count' => intval( $term->count ), + ); + + return array( 'product_tag' => apply_filters( 'woocommerce_api_product_tag_response', $tag, $id, $fields, $term, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new product tag. + * + * @since 2.5.0 + * @param array $data Posted data + * @return array|WP_Error Product tag if succeed, otherwise WP_Error + * will be returned + */ + public function create_product_tag( $data ) { + try { + if ( ! isset( $data['product_tag'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_tag_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_tag' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_tag', __( 'You do not have permission to create product tags', 'woocommerce' ), 401 ); + } + + $defaults = array( + 'name' => '', + 'slug' => '', + 'description' => '', + ); + + $data = wp_parse_args( $data['product_tag'], $defaults ); + $data = apply_filters( 'woocommerce_api_create_product_tag_data', $data, $this ); + + $insert = wp_insert_term( $data['name'], 'product_tag', $data ); + if ( is_wp_error( $insert ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_tag', $insert->get_error_message(), 400 ); + } + $id = $insert['term_id']; + + do_action( 'woocommerce_api_create_product_tag', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_product_tag( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product tag. + * + * @since 2.5.0 + * @param int $id Product tag term ID + * @param array $data Posted data + * @return array|WP_Error Product tag if succeed, otherwise WP_Error + * will be returned + */ + public function edit_product_tag( $id, $data ) { + try { + if ( ! isset( $data['product_tag'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_tag', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_tag' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_tag']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_tag', __( 'You do not have permission to edit product tags', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_tag_data', $data, $this ); + $tag = $this->get_product_tag( $id ); + + if ( is_wp_error( $tag ) ) { + return $tag; + } + + $update = wp_update_term( $id, 'product_tag', $data ); + if ( is_wp_error( $update ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_tag', __( 'Could not edit the tag', 'woocommerce' ), 400 ); + } + + do_action( 'woocommerce_api_edit_product_tag', $id, $data ); + + return $this->get_product_tag( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product tag. + * + * @since 2.5.0 + * @param int $id Product tag term ID + * @return array|WP_Error Success message if succeed, otherwise WP_Error + * will be returned + */ + public function delete_product_tag( $id ) { + try { + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_tag', __( 'You do not have permission to delete product tag', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + $deleted = wp_delete_term( $id, 'product_tag' ); + if ( ! $deleted || is_wp_error( $deleted ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_tag', __( 'Could not delete the tag', 'woocommerce' ), 401 ); + } + + do_action( 'woocommerce_api_delete_product_tag', $id, $this ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_tag' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Helper method to get product post objects + * + * @since 2.1 + * @param array $args request arguments for filtering query + * @return WP_Query + */ + private function query_products( $args ) { + + // Set base query arguments + $query_args = array( + 'fields' => 'ids', + 'post_type' => 'product', + 'post_status' => 'publish', + 'meta_query' => array(), + ); + + // Taxonomy query to filter products by type, category, tag, shipping class, and + // attribute. + $tax_query = array(); + + // Map between taxonomy name and arg's key. + $taxonomies_arg_map = array( + 'product_type' => 'type', + 'product_cat' => 'category', + 'product_tag' => 'tag', + 'product_shipping_class' => 'shipping_class', + ); + + // Add attribute taxonomy names into the map. + foreach ( wc_get_attribute_taxonomy_names() as $attribute_name ) { + $taxonomies_arg_map[ $attribute_name ] = $attribute_name; + } + + // Set tax_query for each passed arg. + foreach ( $taxonomies_arg_map as $tax_name => $arg ) { + if ( ! empty( $args[ $arg ] ) ) { + $terms = explode( ',', $args[ $arg ] ); + + $tax_query[] = array( + 'taxonomy' => $tax_name, + 'field' => 'slug', + 'terms' => $terms, + ); + + unset( $args[ $arg ] ); + } + } + + if ( ! empty( $tax_query ) ) { + $query_args['tax_query'] = $tax_query; + } + + // Filter by specific sku + if ( ! empty( $args['sku'] ) ) { + if ( ! is_array( $query_args['meta_query'] ) ) { + $query_args['meta_query'] = array(); + } + + $query_args['meta_query'][] = array( + 'key' => '_sku', + 'value' => $args['sku'], + 'compare' => '=', + ); + + $query_args['post_type'] = array( 'product', 'product_variation' ); + } + + $query_args = $this->merge_query_args( $query_args, $args ); + + return new WP_Query( $query_args ); + } + + /** + * Get standard product data that applies to every product type + * + * @since 2.1 + * @param WC_Product|int $product + * + * @return array + */ + private function get_product_data( $product ) { + if ( is_numeric( $product ) ) { + $product = wc_get_product( $product ); + } + + if ( ! is_a( $product, 'WC_Product' ) ) { + return array(); + } + + return array( + 'title' => $product->get_name(), + 'id' => $product->get_id(), + 'created_at' => $this->server->format_datetime( $product->get_date_created(), false, true ), + 'updated_at' => $this->server->format_datetime( $product->get_date_modified(), false, true ), + 'type' => $product->get_type(), + 'status' => $product->get_status(), + 'downloadable' => $product->is_downloadable(), + 'virtual' => $product->is_virtual(), + 'permalink' => $product->get_permalink(), + 'sku' => $product->get_sku(), + 'price' => $product->get_price(), + 'regular_price' => $product->get_regular_price(), + 'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : null, + 'price_html' => $product->get_price_html(), + 'taxable' => $product->is_taxable(), + 'tax_status' => $product->get_tax_status(), + 'tax_class' => $product->get_tax_class(), + 'managing_stock' => $product->managing_stock(), + 'stock_quantity' => $product->get_stock_quantity(), + 'in_stock' => $product->is_in_stock(), + 'backorders_allowed' => $product->backorders_allowed(), + 'backordered' => $product->is_on_backorder(), + 'sold_individually' => $product->is_sold_individually(), + 'purchaseable' => $product->is_purchasable(), + 'featured' => $product->is_featured(), + 'visible' => $product->is_visible(), + 'catalog_visibility' => $product->get_catalog_visibility(), + 'on_sale' => $product->is_on_sale(), + 'product_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', + 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', + 'weight' => $product->get_weight() ? $product->get_weight() : null, + 'dimensions' => array( + 'length' => $product->get_length(), + 'width' => $product->get_width(), + 'height' => $product->get_height(), + 'unit' => get_option( 'woocommerce_dimension_unit' ), + ), + 'shipping_required' => $product->needs_shipping(), + 'shipping_taxable' => $product->is_shipping_taxable(), + 'shipping_class' => $product->get_shipping_class(), + 'shipping_class_id' => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null, + 'description' => wpautop( do_shortcode( $product->get_description() ) ), + 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_short_description() ), + 'reviews_allowed' => $product->get_reviews_allowed(), + 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), + 'rating_count' => $product->get_rating_count(), + 'related_ids' => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ), + 'upsell_ids' => array_map( 'absint', $product->get_upsell_ids() ), + 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sell_ids() ), + 'parent_id' => $product->get_parent_id(), + 'categories' => wc_get_object_terms( $product->get_id(), 'product_cat', 'name' ), + 'tags' => wc_get_object_terms( $product->get_id(), 'product_tag', 'name' ), + 'images' => $this->get_images( $product ), + 'featured_src' => wp_get_attachment_url( get_post_thumbnail_id( $product->get_id() ) ), + 'attributes' => $this->get_attributes( $product ), + 'downloads' => $this->get_downloads( $product ), + 'download_limit' => $product->get_download_limit(), + 'download_expiry' => $product->get_download_expiry(), + 'download_type' => 'standard', + 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ), + 'total_sales' => $product->get_total_sales(), + 'variations' => array(), + 'parent' => array(), + 'grouped_products' => array(), + 'menu_order' => $this->get_product_menu_order( $product ), + ); + } + + /** + * Get product menu order. + * + * @since 2.5.3 + * @param WC_Product $product + * @return int + */ + private function get_product_menu_order( $product ) { + $menu_order = $product->get_menu_order(); + + return apply_filters( 'woocommerce_api_product_menu_order', $menu_order, $product ); + } + + /** + * Get an individual variation's data. + * + * @since 2.1 + * @param WC_Product $product + * @return array + */ + private function get_variation_data( $product ) { + $variations = array(); + + foreach ( $product->get_children() as $child_id ) { + $variation = wc_get_product( $child_id ); + + if ( ! $variation || ! $variation->exists() ) { + continue; + } + + $variations[] = array( + 'id' => $variation->get_id(), + 'created_at' => $this->server->format_datetime( $variation->get_date_created(), false, true ), + 'updated_at' => $this->server->format_datetime( $variation->get_date_modified(), false, true ), + 'downloadable' => $variation->is_downloadable(), + 'virtual' => $variation->is_virtual(), + 'permalink' => $variation->get_permalink(), + 'sku' => $variation->get_sku(), + 'price' => $variation->get_price(), + 'regular_price' => $variation->get_regular_price(), + 'sale_price' => $variation->get_sale_price() ? $variation->get_sale_price() : null, + 'taxable' => $variation->is_taxable(), + 'tax_status' => $variation->get_tax_status(), + 'tax_class' => $variation->get_tax_class(), + 'managing_stock' => $variation->managing_stock(), + 'stock_quantity' => $variation->get_stock_quantity(), + 'in_stock' => $variation->is_in_stock(), + 'backorders_allowed' => $variation->backorders_allowed(), + 'backordered' => $variation->is_on_backorder(), + 'purchaseable' => $variation->is_purchasable(), + 'visible' => $variation->variation_is_visible(), + 'on_sale' => $variation->is_on_sale(), + 'weight' => $variation->get_weight() ? $variation->get_weight() : null, + 'dimensions' => array( + 'length' => $variation->get_length(), + 'width' => $variation->get_width(), + 'height' => $variation->get_height(), + 'unit' => get_option( 'woocommerce_dimension_unit' ), + ), + 'shipping_class' => $variation->get_shipping_class(), + 'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null, + 'image' => $this->get_images( $variation ), + 'attributes' => $this->get_attributes( $variation ), + 'downloads' => $this->get_downloads( $variation ), + 'download_limit' => (int) $product->get_download_limit(), + 'download_expiry' => (int) $product->get_download_expiry(), + ); + } + + return $variations; + } + + /** + * Get grouped products data + * + * @since 2.5.0 + * @param WC_Product $product + * + * @return array + */ + private function get_grouped_products_data( $product ) { + $products = array(); + + foreach ( $product->get_children() as $child_id ) { + $_product = wc_get_product( $child_id ); + + if ( ! $_product || ! $_product->exists() ) { + continue; + } + + $products[] = $this->get_product_data( $_product ); + + } + + return $products; + } + + /** + * Save default attributes. + * + * @since 3.0.0 + * + * @param WC_Product $product + * @param WP_REST_Request $request + * @return WC_Product + */ + protected function save_default_attributes( $product, $request ) { + // Update default attributes options setting. + if ( isset( $request['default_attribute'] ) ) { + $request['default_attributes'] = $request['default_attribute']; + } + + if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { + $attributes = $product->get_attributes(); + $default_attributes = array(); + + foreach ( $request['default_attributes'] as $default_attr_key => $default_attr ) { + if ( ! isset( $default_attr['name'] ) ) { + continue; + } + + $taxonomy = sanitize_title( $default_attr['name'] ); + + if ( isset( $default_attr['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] ); + } + + if ( isset( $attributes[ $taxonomy ] ) ) { + $_attribute = $attributes[ $taxonomy ]; + + if ( $_attribute['is_variation'] ) { + $value = ''; + + if ( isset( $default_attr['option'] ) ) { + if ( $_attribute['is_taxonomy'] ) { + // Don't use wc_clean as it destroys sanitized characters + $value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) ); + } else { + $value = wc_clean( trim( stripslashes( $default_attr['option'] ) ) ); + } + } + + if ( $value ) { + $default_attributes[ $taxonomy ] = $value; + } + } + } + } + + $product->set_default_attributes( $default_attributes ); + } + + return $product; + } + + /** + * Save product meta. + * + * @since 2.2 + * @param WC_Product $product + * @param array $data + * @return WC_Product + * @throws WC_API_Exception + */ + protected function save_product_meta( $product, $data ) { + global $wpdb; + + // Virtual. + if ( isset( $data['virtual'] ) ) { + $product->set_virtual( $data['virtual'] ); + } + + // Tax status. + if ( isset( $data['tax_status'] ) ) { + $product->set_tax_status( wc_clean( $data['tax_status'] ) ); + } + + // Tax Class. + if ( isset( $data['tax_class'] ) ) { + $product->set_tax_class( wc_clean( $data['tax_class'] ) ); + } + + // Catalog Visibility. + if ( isset( $data['catalog_visibility'] ) ) { + $product->set_catalog_visibility( wc_clean( $data['catalog_visibility'] ) ); + } + + // Purchase Note. + if ( isset( $data['purchase_note'] ) ) { + $product->set_purchase_note( wc_clean( $data['purchase_note'] ) ); + } + + // Featured Product. + if ( isset( $data['featured'] ) ) { + $product->set_featured( $data['featured'] ); + } + + // Shipping data. + $product = $this->save_product_shipping_data( $product, $data ); + + // SKU. + if ( isset( $data['sku'] ) ) { + $sku = $product->get_sku(); + $new_sku = wc_clean( $data['sku'] ); + + if ( '' == $new_sku ) { + $product->set_sku( '' ); + } elseif ( $new_sku !== $sku ) { + if ( ! empty( $new_sku ) ) { + $unique_sku = wc_product_has_unique_sku( $product->get_id(), $new_sku ); + if ( ! $unique_sku ) { + throw new WC_API_Exception( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 ); + } else { + $product->set_sku( $new_sku ); + } + } else { + $product->set_sku( '' ); + } + } + } + + // Attributes. + if ( isset( $data['attributes'] ) ) { + $attributes = array(); + + foreach ( $data['attributes'] as $attribute ) { + $is_taxonomy = 0; + $taxonomy = 0; + + if ( ! isset( $attribute['name'] ) ) { + continue; + } + + $attribute_slug = sanitize_title( $attribute['name'] ); + + if ( isset( $attribute['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); + $attribute_slug = sanitize_title( $attribute['slug'] ); + } + + if ( $taxonomy ) { + $is_taxonomy = 1; + } + + if ( $is_taxonomy ) { + + $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute['name'] ); + + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $attribute['options'] ) ) { + // Text based attributes - Posted values are term names. + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = array(); + } + + // Update post terms + if ( taxonomy_exists( $taxonomy ) ) { + wp_set_object_terms( $product->get_id(), $values, $taxonomy ); + } + + if ( ! empty( $values ) ) { + // Add attribute to array, but don't set values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $taxonomy ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } elseif ( isset( $attribute['options'] ) ) { + // Array based. + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + + // Text based, separate by pipe. + } else { + $values = array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ); + } + + // Custom attribute - Add attribute to array and set the values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute['name'] ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } + + uasort( $attributes, 'wc_product_attribute_uasort_comparison' ); + + $product->set_attributes( $attributes ); + } + + // Sales and prices. + if ( in_array( $product->get_type(), array( 'variable', 'grouped' ) ) ) { + + // Variable and grouped products have no prices. + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->set_price( '' ); + + } else { + + // Regular Price. + if ( isset( $data['regular_price'] ) ) { + $regular_price = ( '' === $data['regular_price'] ) ? '' : $data['regular_price']; + $product->set_regular_price( $regular_price ); + } + + // Sale Price. + if ( isset( $data['sale_price'] ) ) { + $sale_price = ( '' === $data['sale_price'] ) ? '' : $data['sale_price']; + $product->set_sale_price( $sale_price ); + } + + if ( isset( $data['sale_price_dates_from'] ) ) { + $date_from = $data['sale_price_dates_from']; + } else { + $date_from = $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : ''; + } + + if ( isset( $data['sale_price_dates_to'] ) ) { + $date_to = $data['sale_price_dates_to']; + } else { + $date_to = $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : ''; + } + + if ( $date_to && ! $date_from ) { + $date_from = strtotime( 'NOW', current_time( 'timestamp', true ) ); + } + + $product->set_date_on_sale_to( $date_to ); + $product->set_date_on_sale_from( $date_from ); + + if ( $product->is_on_sale( 'edit' ) ) { + $product->set_price( $product->get_sale_price( 'edit' ) ); + } else { + $product->set_price( $product->get_regular_price( 'edit' ) ); + } + } + + // Product parent ID for groups. + if ( isset( $data['parent_id'] ) ) { + $product->set_parent_id( absint( $data['parent_id'] ) ); + } + + // Sold Individually. + if ( isset( $data['sold_individually'] ) ) { + $product->set_sold_individually( true === $data['sold_individually'] ? 'yes' : '' ); + } + + // Stock status. + if ( isset( $data['in_stock'] ) ) { + $stock_status = ( true === $data['in_stock'] ) ? 'instock' : 'outofstock'; + } else { + $stock_status = $product->get_stock_status(); + + if ( '' === $stock_status ) { + $stock_status = 'instock'; + } + } + + // Stock Data. + if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $data['managing_stock'] ) ) { + $managing_stock = ( true === $data['managing_stock'] ) ? 'yes' : 'no'; + $product->set_manage_stock( $managing_stock ); + } else { + $managing_stock = $product->get_manage_stock() ? 'yes' : 'no'; + } + + // Backorders. + if ( isset( $data['backorders'] ) ) { + if ( 'notify' === $data['backorders'] ) { + $backorders = 'notify'; + } else { + $backorders = ( true === $data['backorders'] ) ? 'yes' : 'no'; + } + + $product->set_backorders( $backorders ); + } else { + $backorders = $product->get_backorders(); + } + + if ( $product->is_type( 'grouped' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } elseif ( $product->is_type( 'external' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( 'instock' ); + } elseif ( 'yes' == $managing_stock ) { + $product->set_backorders( $backorders ); + + // Stock status is always determined by children so sync later. + if ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Stock quantity. + if ( isset( $data['stock_quantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $data['stock_quantity'] ) ); + } elseif ( isset( $data['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); + $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); + } + } else { + // Don't manage stock. + $product->set_manage_stock( 'no' ); + $product->set_backorders( $backorders ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } + } elseif ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Upsells. + if ( isset( $data['upsell_ids'] ) ) { + $upsells = array(); + $ids = $data['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + + $product->set_upsell_ids( $upsells ); + } else { + $product->set_upsell_ids( array() ); + } + } + + // Cross sells. + if ( isset( $data['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $data['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + + $product->set_cross_sell_ids( $crosssells ); + } else { + $product->set_cross_sell_ids( array() ); + } + } + + // Product categories. + if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) { + $product->set_category_ids( $data['categories'] ); + } + + // Product tags. + if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) { + $product->set_tag_ids( $data['tags'] ); + } + + // Downloadable. + if ( isset( $data['downloadable'] ) ) { + $is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no'; + $product->set_downloadable( $is_downloadable ); + } else { + $is_downloadable = $product->get_downloadable() ? 'yes' : 'no'; + } + + // Downloadable options. + if ( 'yes' == $is_downloadable ) { + + // Downloadable files. + if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { + $product = $this->save_downloadable_files( $product, $data['downloads'] ); + } + + // Download limit. + if ( isset( $data['download_limit'] ) ) { + $product->set_download_limit( $data['download_limit'] ); + } + + // Download expiry. + if ( isset( $data['download_expiry'] ) ) { + $product->set_download_expiry( $data['download_expiry'] ); + } + } + + // Product url. + if ( $product->is_type( 'external' ) ) { + if ( isset( $data['product_url'] ) ) { + $product->set_product_url( $data['product_url'] ); + } + + if ( isset( $data['button_text'] ) ) { + $product->set_button_text( $data['button_text'] ); + } + } + + // Reviews allowed. + if ( isset( $data['reviews_allowed'] ) ) { + $product->set_reviews_allowed( $data['reviews_allowed'] ); + } + + // Save default attributes for variable products. + if ( $product->is_type( 'variable' ) ) { + $product = $this->save_default_attributes( $product, $data ); + } + + // Do action for product type + do_action( 'woocommerce_api_process_product_meta_' . $product->get_type(), $product->get_id(), $data ); + + return $product; + } + + /** + * Save variations. + * + * @since 2.2 + * + * @param WC_Product $product + * @param array $request + * + * @return bool + * @throws WC_API_Exception + */ + protected function save_variations( $product, $request ) { + global $wpdb; + + $id = $product->get_id(); + $variations = $request['variations']; + $attributes = $product->get_attributes(); + + foreach ( $variations as $menu_order => $data ) { + $variation_id = isset( $data['id'] ) ? absint( $data['id'] ) : 0; + $variation = new WC_Product_Variation( $variation_id ); + + // Create initial name and status. + if ( ! $variation->get_slug() ) { + /* translators: 1: variation id 2: product name */ + $variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) ); + $variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' ); + } + + // Parent ID. + $variation->set_parent_id( $product->get_id() ); + + // Menu order. + $variation->set_menu_order( $menu_order ); + + // Status. + if ( isset( $data['visible'] ) ) { + $variation->set_status( false === $data['visible'] ? 'private' : 'publish' ); + } + + // SKU. + if ( isset( $data['sku'] ) ) { + $variation->set_sku( wc_clean( $data['sku'] ) ); + } + + // Thumbnail. + if ( isset( $data['image'] ) && is_array( $data['image'] ) ) { + $image = current( $data['image'] ); + if ( is_array( $image ) ) { + $image['position'] = 0; + } + + $variation = $this->save_product_images( $variation, array( $image ) ); + } + + // Virtual variation. + if ( isset( $data['virtual'] ) ) { + $variation->set_virtual( $data['virtual'] ); + } + + // Downloadable variation. + if ( isset( $data['downloadable'] ) ) { + $is_downloadable = $data['downloadable']; + $variation->set_downloadable( $is_downloadable ); + } else { + $is_downloadable = $variation->get_downloadable(); + } + + // Downloads. + if ( $is_downloadable ) { + // Downloadable files. + if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { + $variation = $this->save_downloadable_files( $variation, $data['downloads'] ); + } + + // Download limit. + if ( isset( $data['download_limit'] ) ) { + $variation->set_download_limit( $data['download_limit'] ); + } + + // Download expiry. + if ( isset( $data['download_expiry'] ) ) { + $variation->set_download_expiry( $data['download_expiry'] ); + } + } + + // Shipping data. + $variation = $this->save_product_shipping_data( $variation, $data ); + + // Stock handling. + $manage_stock = (bool) $variation->get_manage_stock(); + if ( isset( $data['managing_stock'] ) ) { + $manage_stock = $data['managing_stock']; + } + $variation->set_manage_stock( $manage_stock ); + + $stock_status = $variation->get_stock_status(); + if ( isset( $data['in_stock'] ) ) { + $stock_status = true === $data['in_stock'] ? 'instock' : 'outofstock'; + } + $variation->set_stock_status( $stock_status ); + + $backorders = $variation->get_backorders(); + if ( isset( $data['backorders'] ) ) { + $backorders = $data['backorders']; + } + $variation->set_backorders( $backorders ); + + if ( $manage_stock ) { + if ( isset( $data['stock_quantity'] ) ) { + $variation->set_stock_quantity( $data['stock_quantity'] ); + } elseif ( isset( $data['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); + $variation->set_stock_quantity( $stock_quantity ); + } + } else { + $variation->set_backorders( 'no' ); + $variation->set_stock_quantity( '' ); + } + + // Regular Price. + if ( isset( $data['regular_price'] ) ) { + $variation->set_regular_price( $data['regular_price'] ); + } + + // Sale Price. + if ( isset( $data['sale_price'] ) ) { + $variation->set_sale_price( $data['sale_price'] ); + } + + if ( isset( $data['sale_price_dates_from'] ) ) { + $variation->set_date_on_sale_from( $data['sale_price_dates_from'] ); + } + + if ( isset( $data['sale_price_dates_to'] ) ) { + $variation->set_date_on_sale_to( $data['sale_price_dates_to'] ); + } + + // Tax class. + if ( isset( $data['tax_class'] ) ) { + $variation->set_tax_class( $data['tax_class'] ); + } + + // Description. + if ( isset( $data['description'] ) ) { + $variation->set_description( wp_kses_post( $data['description'] ) ); + } + + // Update taxonomies. + if ( isset( $data['attributes'] ) ) { + $_attributes = array(); + + foreach ( $data['attributes'] as $attribute_key => $attribute ) { + if ( ! isset( $attribute['name'] ) ) { + continue; + } + + $taxonomy = 0; + $_attribute = array(); + + if ( isset( $attribute['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); + } + + if ( ! $taxonomy ) { + $taxonomy = sanitize_title( $attribute['name'] ); + } + + if ( isset( $attributes[ $taxonomy ] ) ) { + $_attribute = $attributes[ $taxonomy ]; + } + + if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) { + $_attribute_key = sanitize_title( $_attribute['name'] ); + + if ( isset( $_attribute['is_taxonomy'] ) && $_attribute['is_taxonomy'] ) { + // Don't use wc_clean as it destroys sanitized characters. + $_attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : ''; + } else { + $_attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + } + + $_attributes[ $_attribute_key ] = $_attribute_value; + } + } + + $variation->set_attributes( $_attributes ); + } + + $variation->save(); + + do_action( 'woocommerce_api_save_product_variation', $variation_id, $menu_order, $variation ); + } + + return true; + } + + /** + * Save product shipping data + * + * @since 2.2 + * @param WC_Product $product + * @param array $data + * @return WC_Product + */ + private function save_product_shipping_data( $product, $data ) { + if ( isset( $data['weight'] ) ) { + $product->set_weight( '' === $data['weight'] ? '' : wc_format_decimal( $data['weight'] ) ); + } + + // Product dimensions + if ( isset( $data['dimensions'] ) ) { + // Height + if ( isset( $data['dimensions']['height'] ) ) { + $product->set_height( '' === $data['dimensions']['height'] ? '' : wc_format_decimal( $data['dimensions']['height'] ) ); + } + + // Width + if ( isset( $data['dimensions']['width'] ) ) { + $product->set_width( '' === $data['dimensions']['width'] ? '' : wc_format_decimal( $data['dimensions']['width'] ) ); + } + + // Length + if ( isset( $data['dimensions']['length'] ) ) { + $product->set_length( '' === $data['dimensions']['length'] ? '' : wc_format_decimal( $data['dimensions']['length'] ) ); + } + } + + // Virtual + if ( isset( $data['virtual'] ) ) { + $virtual = ( true === $data['virtual'] ) ? 'yes' : 'no'; + + if ( 'yes' == $virtual ) { + $product->set_weight( '' ); + $product->set_height( '' ); + $product->set_length( '' ); + $product->set_width( '' ); + } + } + + // Shipping class + if ( isset( $data['shipping_class'] ) ) { + $data_store = $product->get_data_store(); + $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); + $product->set_shipping_class_id( $shipping_class_id ); + } + + return $product; + } + + /** + * Save downloadable files + * + * @since 2.2 + * @param WC_Product $product + * @param array $downloads + * @param int $deprecated Deprecated since 3.0. + * @return WC_Product + */ + private function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { + if ( $deprecated ) { + wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() does not require a variation_id anymore.' ); + } + + $files = array(); + foreach ( $downloads as $key => $file ) { + if ( isset( $file['url'] ) ) { + $file['file'] = $file['url']; + } + + if ( empty( $file['file'] ) ) { + continue; + } + + $download = new WC_Product_Download(); + $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); + $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); + $files[] = $download; + } + $product->set_downloads( $files ); + + return $product; + } + + /** + * Get attribute taxonomy by slug. + * + * @since 2.2 + * @param string $slug + * @return string|null + */ + private function get_attribute_taxonomy_by_slug( $slug ) { + $taxonomy = null; + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + foreach ( $attribute_taxonomies as $key => $tax ) { + if ( $slug == $tax->attribute_name ) { + $taxonomy = 'pa_' . $tax->attribute_name; + + break; + } + } + + return $taxonomy; + } + + /** + * Get the images for a product or product variation + * + * @since 2.1 + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + private function get_images( $product ) { + $images = $attachment_ids = array(); + $product_image = $product->get_image_id(); + + // Add featured image. + if ( ! empty( $product_image ) ) { + $attachment_ids[] = $product_image; + } + + // Add gallery images. + $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); + + // Build image data. + foreach ( $attachment_ids as $position => $attachment_id ) { + + $attachment_post = get_post( $attachment_id ); + + if ( is_null( $attachment_post ) ) { + continue; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + + if ( ! is_array( $attachment ) ) { + continue; + } + + $images[] = array( + 'id' => (int) $attachment_id, + 'created_at' => $this->server->format_datetime( $attachment_post->post_date_gmt ), + 'updated_at' => $this->server->format_datetime( $attachment_post->post_modified_gmt ), + 'src' => current( $attachment ), + 'title' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + 'position' => (int) $position, + ); + } + + // Set a placeholder image if the product has no images set. + if ( empty( $images ) ) { + + $images[] = array( + 'id' => 0, + 'created_at' => $this->server->format_datetime( time() ), // Default to now. + 'updated_at' => $this->server->format_datetime( time() ), + 'src' => wc_placeholder_img_src(), + 'title' => __( 'Placeholder', 'woocommerce' ), + 'alt' => __( 'Placeholder', 'woocommerce' ), + 'position' => 0, + ); + } + + return $images; + } + + /** + * Save product images. + * + * @since 2.2 + * @param WC_Product $product + * @param array $images + * @throws WC_API_Exception + * @return WC_Product + */ + protected function save_product_images( $product, $images ) { + if ( is_array( $images ) ) { + $gallery = array(); + + foreach ( $images as $image ) { + if ( isset( $image['position'] ) && 0 == $image['position'] ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); + } + + $attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() ); + } + + $product->set_image_id( $attachment_id ); + } else { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); + } + + $attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() ); + } + + $gallery[] = $attachment_id; + } + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) && $attachment_id ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image title if present. + if ( ! empty( $image['title'] ) && $attachment_id ) { + wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['title'] ) ); + } + } + + if ( ! empty( $gallery ) ) { + $product->set_gallery_image_ids( $gallery ); + } + } else { + $product->set_image_id( '' ); + $product->set_gallery_image_ids( array() ); + } + + return $product; + } + + /** + * Upload image from URL + * + * @since 2.2 + * @param string $image_url + * @return int|WP_Error attachment id + */ + public function upload_product_image( $image_url ) { + return $this->upload_image_from_url( $image_url, 'product_image' ); + } + + /** + * Upload product category image from URL. + * + * @since 2.5.0 + * @param string $image_url + * @return int|WP_Error attachment id + */ + public function upload_product_category_image( $image_url ) { + return $this->upload_image_from_url( $image_url, 'product_category_image' ); + } + + /** + * Upload image from URL. + * + * @throws WC_API_Exception + * + * @since 2.5.0 + * @param string $image_url + * @param string $upload_for + * @return array + */ + protected function upload_image_from_url( $image_url, $upload_for = 'product_image' ) { + $upload = wc_rest_upload_image_from_url( $image_url ); + if ( is_wp_error( $upload ) ) { + throw new WC_API_Exception( 'woocommerce_api_' . $upload_for . '_upload_error', $upload->get_error_message(), 400 ); + } + + do_action( 'woocommerce_api_uploaded_image_from_url', $upload, $image_url, $upload_for ); + + return $upload; + } + + /** + * Sets product image as attachment and returns the attachment ID. + * + * @since 2.2 + * @param array $upload + * @param int $id + * @return int + */ + protected function set_product_image_as_attachment( $upload, $id ) { + return $this->set_uploaded_image_as_attachment( $upload, $id ); + } + + /** + * Sets uploaded category image as attachment and returns the attachment ID. + * + * @since 2.5.0 + * @param integer $upload Upload information from wp_upload_bits + * @return int Attachment ID + */ + protected function set_product_category_image_as_attachment( $upload ) { + return $this->set_uploaded_image_as_attachment( $upload ); + } + + /** + * Set uploaded image as attachment. + * + * @since 2.5.0 + * @param array $upload Upload information from wp_upload_bits + * @param int $id Post ID. Default to 0. + * @return int Attachment ID + */ + protected function set_uploaded_image_as_attachment( $upload, $id = 0 ) { + $info = wp_check_filetype( $upload['file'] ); + $title = ''; + $content = ''; + + if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) { + if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { + $title = wc_clean( $image_meta['title'] ); + } + if ( trim( $image_meta['caption'] ) ) { + $content = wc_clean( $image_meta['caption'] ); + } + } + + $attachment = array( + 'post_mime_type' => $info['type'], + 'guid' => $upload['url'], + 'post_parent' => $id, + 'post_title' => $title, + 'post_content' => $content, + ); + + $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id ); + if ( ! is_wp_error( $attachment_id ) ) { + wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); + } + + return $attachment_id; + } + + /** + * Get attribute options. + * + * @param int $product_id + * @param array $attribute + * @return array + */ + protected function get_attribute_options( $product_id, $attribute ) { + if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { + return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) ); + } elseif ( isset( $attribute['value'] ) ) { + return array_map( 'trim', explode( '|', $attribute['value'] ) ); + } + + return array(); + } + + /** + * Get the attributes for a product or product variation + * + * @since 2.1 + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + private function get_attributes( $product ) { + + $attributes = array(); + + if ( $product->is_type( 'variation' ) ) { + + // variation attributes + foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { + + // taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_` + $attributes[] = array( + 'name' => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ), $product ), + 'slug' => str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $attribute_name ) ), + 'option' => $attribute, + ); + } + } else { + + foreach ( $product->get_attributes() as $attribute ) { + $attributes[] = array( + 'name' => wc_attribute_label( $attribute['name'], $product ), + 'slug' => wc_attribute_taxonomy_slug( $attribute['name'] ), + 'position' => (int) $attribute['position'], + 'visible' => (bool) $attribute['is_visible'], + 'variation' => (bool) $attribute['is_variation'], + 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), + ); + } + } + + return $attributes; + } + + /** + * Get the downloads for a product or product variation + * + * @since 2.1 + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + private function get_downloads( $product ) { + + $downloads = array(); + + if ( $product->is_downloadable() ) { + + foreach ( $product->get_downloads() as $file_id => $file ) { + + $downloads[] = array( + 'id' => $file_id, // do not cast as int as this is a hash + 'name' => $file['name'], + 'file' => $file['file'], + ); + } + } + + return $downloads; + } + + /** + * Get a listing of product attributes + * + * @since 2.5.0 + * + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_attributes( $fields = null ) { + try { + // Permissions check. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); + } + + $product_attributes = array(); + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + foreach ( $attribute_taxonomies as $attribute ) { + $product_attributes[] = array( + 'id' => intval( $attribute->attribute_id ), + 'name' => $attribute->attribute_label, + 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), + 'type' => $attribute->attribute_type, + 'order_by' => $attribute->attribute_orderby, + 'has_archives' => (bool) $attribute->attribute_public, + ); + } + + return array( 'product_attributes' => apply_filters( 'woocommerce_api_product_attributes_response', $product_attributes, $attribute_taxonomies, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product attribute for the given ID + * + * @since 2.5.0 + * + * @param string $id product attribute term ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_attribute( $id, $fields = null ) { + global $wpdb; + + try { + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); + } + + $attribute = $wpdb->get_row( $wpdb->prepare( " + SELECT * + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", $id ) ); + + if ( is_wp_error( $attribute ) || is_null( $attribute ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $product_attribute = array( + 'id' => intval( $attribute->attribute_id ), + 'name' => $attribute->attribute_label, + 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), + 'type' => $attribute->attribute_type, + 'order_by' => $attribute->attribute_orderby, + 'has_archives' => (bool) $attribute->attribute_public, + ); + + return array( 'product_attribute' => apply_filters( 'woocommerce_api_product_attribute_response', $product_attribute, $id, $fields, $attribute, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Validate attribute data. + * + * @since 2.5.0 + * @param string $name + * @param string $slug + * @param string $type + * @param string $order_by + * @param bool $new_data + * @return bool + * @throws WC_API_Exception + */ + protected function validate_attribute_data( $name, $slug, $type, $order_by, $new_data = true ) { + if ( empty( $name ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); + } + + if ( strlen( $slug ) >= 28 ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), 400 ); + } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), 400 ); + } elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), 400 ); + } + + // Validate the attribute type + if ( ! in_array( wc_clean( $type ), array_keys( wc_get_attribute_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_type', sprintf( __( 'Invalid product attribute type - the product attribute type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_attribute_types() ) ) ), 400 ); + } + + // Validate the attribute order by + if ( ! in_array( wc_clean( $order_by ), array( 'menu_order', 'name', 'name_num', 'id' ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_order_by', sprintf( __( 'Invalid product attribute order_by type - the product attribute order_by type must be any of these: %s', 'woocommerce' ), implode( ', ', array( 'menu_order', 'name', 'name_num', 'id' ) ) ), 400 ); + } + + return true; + } + + /** + * Create a new product attribute. + * + * @since 2.5.0 + * + * @param array $data Posted data. + * + * @return array|WP_Error + */ + public function create_product_attribute( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_attribute'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); + } + + $data = $data['product_attribute']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_product_attribute_data', $data, $this ); + + if ( ! isset( $data['name'] ) ) { + $data['name'] = ''; + } + + // Set the attribute slug. + if ( ! isset( $data['slug'] ) ) { + $data['slug'] = wc_sanitize_taxonomy_name( stripslashes( $data['name'] ) ); + } else { + $data['slug'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ) ); + } + + // Set attribute type when not sent. + if ( ! isset( $data['type'] ) ) { + $data['type'] = 'select'; + } + + // Set order by when not sent. + if ( ! isset( $data['order_by'] ) ) { + $data['order_by'] = 'menu_order'; + } + + // Validate the attribute data. + $this->validate_attribute_data( $data['name'], $data['slug'], $data['type'], $data['order_by'], true ); + + $insert = $wpdb->insert( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( + 'attribute_label' => $data['name'], + 'attribute_name' => $data['slug'], + 'attribute_type' => $data['type'], + 'attribute_orderby' => $data['order_by'], + 'attribute_public' => isset( $data['has_archives'] ) && true === $data['has_archives'] ? 1 : 0, + ), + array( '%s', '%s', '%s', '%s', '%d' ) + ); + + // Checks for an error in the product creation. + if ( is_wp_error( $insert ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $insert->get_error_message(), 400 ); + } + + $id = $wpdb->insert_id; + + do_action( 'woocommerce_api_create_product_attribute', $id, $data ); + + // Clear transients. + wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); + delete_transient( 'wc_attribute_taxonomies' ); + WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); + + $this->server->send_status( 201 ); + + return $this->get_product_attribute( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product attribute. + * + * @since 2.5.0 + * + * @param int $id the attribute ID. + * @param array $data + * + * @return array|WP_Error + */ + public function edit_product_attribute( $id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_attribute'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_attribute']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_attribute_data', $data, $this ); + $attribute = $this->get_product_attribute( $id ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $attribute_name = isset( $data['name'] ) ? $data['name'] : $attribute['product_attribute']['name']; + $attribute_type = isset( $data['type'] ) ? $data['type'] : $attribute['product_attribute']['type']; + $attribute_order_by = isset( $data['order_by'] ) ? $data['order_by'] : $attribute['product_attribute']['order_by']; + + if ( isset( $data['slug'] ) ) { + $attribute_slug = wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ); + } else { + $attribute_slug = $attribute['product_attribute']['slug']; + } + $attribute_slug = preg_replace( '/^pa\_/', '', $attribute_slug ); + + if ( isset( $data['has_archives'] ) ) { + $attribute_public = true === $data['has_archives'] ? 1 : 0; + } else { + $attribute_public = $attribute['product_attribute']['has_archives']; + } + + // Validate the attribute data. + $this->validate_attribute_data( $attribute_name, $attribute_slug, $attribute_type, $attribute_order_by, false ); + + $update = $wpdb->update( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( + 'attribute_label' => $attribute_name, + 'attribute_name' => $attribute_slug, + 'attribute_type' => $attribute_type, + 'attribute_orderby' => $attribute_order_by, + 'attribute_public' => $attribute_public, + ), + array( 'attribute_id' => $id ), + array( '%s', '%s', '%s', '%s', '%d' ), + array( '%d' ) + ); + + // Checks for an error in the product creation. + if ( false === $update ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute', __( 'Could not edit the attribute', 'woocommerce' ), 400 ); + } + + do_action( 'woocommerce_api_edit_product_attribute', $id, $data ); + + // Clear transients. + wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); + delete_transient( 'wc_attribute_taxonomies' ); + WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); + + return $this->get_product_attribute( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product attribute. + * + * @since 2.5.0 + * + * @param int $id the product attribute ID. + * + * @return array|WP_Error + */ + public function delete_product_attribute( $id ) { + global $wpdb; + + try { + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute', __( 'You do not have permission to delete product attributes', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + + $attribute_name = $wpdb->get_var( $wpdb->prepare( " + SELECT attribute_name + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", $id ) ); + + if ( is_null( $attribute_name ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $deleted = $wpdb->delete( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( 'attribute_id' => $id ), + array( '%d' ) + ); + + if ( false === $deleted ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute', __( 'Could not delete the attribute', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name( $attribute_name ); + + if ( taxonomy_exists( $taxonomy ) ) { + $terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' ); + foreach ( $terms as $term ) { + wp_delete_term( $term->term_id, $taxonomy ); + } + } + + do_action( 'woocommerce_attribute_deleted', $id, $attribute_name, $taxonomy ); + do_action( 'woocommerce_api_delete_product_attribute', $id, $this ); + + // Clear transients. + wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); + delete_transient( 'wc_attribute_taxonomies' ); + WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a listing of product attribute terms. + * + * @since 2.5.0 + * + * @param int $attribute_id Attribute ID. + * @param string|null $fields Fields to limit response to. + * + * @return array|WP_Error + */ + public function get_product_attribute_terms( $attribute_id, $fields = null ) { + try { + // Permissions check. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attribute_terms', __( 'You do not have permission to read product attribute terms', 'woocommerce' ), 401 ); + } + + $attribute_id = absint( $attribute_id ); + $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( ! $taxonomy ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $terms = get_terms( $taxonomy, array( 'hide_empty' => false ) ); + $attribute_terms = array(); + + foreach ( $terms as $term ) { + $attribute_terms[] = array( + 'id' => $term->term_id, + 'slug' => $term->slug, + 'name' => $term->name, + 'count' => $term->count, + ); + } + + return array( 'product_attribute_terms' => apply_filters( 'woocommerce_api_product_attribute_terms_response', $attribute_terms, $terms, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product attribute term for the given ID. + * + * @since 2.5.0 + * + * @param int $attribute_id Attribute ID. + * @param string $id Product attribute term ID. + * @param string|null $fields Fields to limit response to. + * + * @return array|WP_Error + */ + public function get_product_attribute_term( $attribute_id, $id, $fields = null ) { + global $wpdb; + + try { + $id = absint( $id ); + $attribute_id = absint( $attribute_id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_term_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attribute_terms', __( 'You do not have permission to read product attribute terms', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( ! $taxonomy ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $term = get_term( $id, $taxonomy ); + + if ( is_wp_error( $term ) || is_null( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_term_id', __( 'A product attribute term with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $attribute_term = array( + 'id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'count' => $term->count, + ); + + return array( 'product_attribute_term' => apply_filters( 'woocommerce_api_product_attribute_response', $attribute_term, $id, $fields, $term, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new product attribute term. + * + * @since 2.5.0 + * + * @param int $attribute_id Attribute ID. + * @param array $data Posted data. + * + * @return array|WP_Error + */ + public function create_product_attribute_term( $attribute_id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_attribute_term'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute_term' ), 400 ); + } + + $data = $data['product_attribute_term']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( ! $taxonomy ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $data = apply_filters( 'woocommerce_api_create_product_attribute_term_data', $data, $this ); + + // Check if attribute term name is specified. + if ( ! isset( $data['name'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); + } + + $args = array(); + + // Set the attribute term slug. + if ( isset( $data['slug'] ) ) { + $args['slug'] = sanitize_title( wp_unslash( $data['slug'] ) ); + } + + $term = wp_insert_term( $data['name'], $taxonomy, $args ); + + // Checks for an error in the term creation. + if ( is_wp_error( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $term->get_error_message(), 400 ); + } + + $id = $term['term_id']; + + do_action( 'woocommerce_api_create_product_attribute_term', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_product_attribute_term( $attribute_id, $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product attribute term. + * + * @since 2.5.0 + * + * @param int $attribute_id Attribute ID. + * @param int $id the attribute ID. + * @param array $data + * + * @return array|WP_Error + */ + public function edit_product_attribute_term( $attribute_id, $id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_attribute_term'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute_term' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_attribute_term']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( ! $taxonomy ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_attribute_term_data', $data, $this ); + + $args = array(); + + // Update name. + if ( isset( $data['name'] ) ) { + $args['name'] = wc_clean( wp_unslash( $data['name'] ) ); + } + + // Update slug. + if ( isset( $data['slug'] ) ) { + $args['slug'] = sanitize_title( wp_unslash( $data['slug'] ) ); + } + + $term = wp_update_term( $id, $taxonomy, $args ); + + if ( is_wp_error( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute_term', $term->get_error_message(), 400 ); + } + + do_action( 'woocommerce_api_edit_product_attribute_term', $id, $data ); + + return $this->get_product_attribute_term( $attribute_id, $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product attribute term. + * + * @since 2.5.0 + * + * @param int $attribute_id Attribute ID. + * @param int $id the product attribute ID. + * + * @return array|WP_Error + */ + public function delete_product_attribute_term( $attribute_id, $id ) { + global $wpdb; + + try { + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute_term', __( 'You do not have permission to delete product attribute terms', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( ! $taxonomy ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $id = absint( $id ); + $term = wp_delete_term( $id, $taxonomy ); + + if ( ! $term ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute_term', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product_attribute_term' ), 500 ); + } elseif ( is_wp_error( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute_term', $term->get_error_message(), 400 ); + } + + do_action( 'woocommerce_api_delete_product_attribute_term', $id, $this ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Clear product + * + * @param int $product_id + */ + protected function clear_product( $product_id ) { + if ( ! is_numeric( $product_id ) || 0 >= $product_id ) { + return; + } + + // Delete product attachments + $attachments = get_children( array( + 'post_parent' => $product_id, + 'post_status' => 'any', + 'post_type' => 'attachment', + ) ); + + foreach ( (array) $attachments as $attachment ) { + wp_delete_attachment( $attachment->ID, true ); + } + + // Delete product + $product = wc_get_product( $product_id ); + $product->delete( true ); + } + + /** + * Bulk update or insert products + * Accepts an array with products in the formats supported by + * WC_API_Products->create_product() and WC_API_Products->edit_product() + * + * @since 2.4.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + + try { + if ( ! isset( $data['products'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_products_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'products' ), 400 ); + } + + $data = $data['products']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'products' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_products_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $products = array(); + + foreach ( $data as $_product ) { + $product_id = 0; + $product_sku = ''; + + // Try to get the product ID + if ( isset( $_product['id'] ) ) { + $product_id = intval( $_product['id'] ); + } + + if ( ! $product_id && isset( $_product['sku'] ) ) { + $product_sku = wc_clean( $_product['sku'] ); + $product_id = wc_get_product_id_by_sku( $product_sku ); + } + + if ( $product_id ) { + + // Product exists / edit product + $edit = $this->edit_product( $product_id, array( 'product' => $_product ) ); + + if ( is_wp_error( $edit ) ) { + $products[] = array( + 'id' => $product_id, + 'sku' => $product_sku, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $products[] = $edit['product']; + } + } else { + + // Product don't exists / create product + $new = $this->create_product( array( 'product' => $_product ) ); + + if ( is_wp_error( $new ) ) { + $products[] = array( + 'id' => $product_id, + 'sku' => $product_sku, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $products[] = $new['product']; + } + } + } + + return array( 'products' => apply_filters( 'woocommerce_api_products_bulk_response', $products, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a listing of product shipping classes. + * + * @since 2.5.0 + * @param string|null $fields Fields to limit response to + * @return array|WP_Error List of product shipping classes if succeed, + * otherwise WP_Error will be returned + */ + public function get_product_shipping_classes( $fields = null ) { + try { + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_shipping_classes', __( 'You do not have permission to read product shipping classes', 'woocommerce' ), 401 ); + } + + $product_shipping_classes = array(); + + $terms = get_terms( 'product_shipping_class', array( 'hide_empty' => false, 'fields' => 'ids' ) ); + + foreach ( $terms as $term_id ) { + $product_shipping_classes[] = current( $this->get_product_shipping_class( $term_id, $fields ) ); + } + + return array( 'product_shipping_classes' => apply_filters( 'woocommerce_api_product_shipping_classes_response', $product_shipping_classes, $terms, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product shipping class for the given ID. + * + * @since 2.5.0 + * @param string $id Product shipping class term ID + * @param string|null $fields Fields to limit response to + * @return array|WP_Error Product shipping class if succeed, otherwise + * WP_Error will be returned + */ + public function get_product_shipping_class( $id, $fields = null ) { + try { + $id = absint( $id ); + if ( ! $id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_id', __( 'Invalid product shipping class ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_shipping_classes', __( 'You do not have permission to read product shipping classes', 'woocommerce' ), 401 ); + } + + $term = get_term( $id, 'product_shipping_class' ); + + if ( is_wp_error( $term ) || is_null( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_id', __( 'A product shipping class with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $term_id = intval( $term->term_id ); + + $product_shipping_class = array( + 'id' => $term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'parent' => $term->parent, + 'description' => $term->description, + 'count' => intval( $term->count ), + ); + + return array( 'product_shipping_class' => apply_filters( 'woocommerce_api_product_shipping_class_response', $product_shipping_class, $id, $fields, $term, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new product shipping class. + * + * @since 2.5.0 + * @param array $data Posted data + * @return array|WP_Error Product shipping class if succeed, otherwise + * WP_Error will be returned + */ + public function create_product_shipping_class( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_shipping_class'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_shipping_class_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_shipping_class' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_shipping_class', __( 'You do not have permission to create product shipping classes', 'woocommerce' ), 401 ); + } + + $defaults = array( + 'name' => '', + 'slug' => '', + 'description' => '', + 'parent' => 0, + ); + + $data = wp_parse_args( $data['product_shipping_class'], $defaults ); + $data = apply_filters( 'woocommerce_api_create_product_shipping_class_data', $data, $this ); + + // Check parent. + $data['parent'] = absint( $data['parent'] ); + if ( $data['parent'] ) { + $parent = get_term_by( 'id', $data['parent'], 'product_shipping_class' ); + if ( ! $parent ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_parent', __( 'Product shipping class parent is invalid', 'woocommerce' ), 400 ); + } + } + + $insert = wp_insert_term( $data['name'], 'product_shipping_class', $data ); + if ( is_wp_error( $insert ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_shipping_class', $insert->get_error_message(), 400 ); + } + + $id = $insert['term_id']; + + do_action( 'woocommerce_api_create_product_shipping_class', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_product_shipping_class( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product shipping class. + * + * @since 2.5.0 + * @param int $id Product shipping class term ID + * @param array $data Posted data + * @return array|WP_Error Product shipping class if succeed, otherwise + * WP_Error will be returned + */ + public function edit_product_shipping_class( $id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_shipping_class'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_shipping_class', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_shipping_class' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_shipping_class']; + + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_shipping_class', __( 'You do not have permission to edit product shipping classes', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_shipping_class_data', $data, $this ); + $shipping_class = $this->get_product_shipping_class( $id ); + + if ( is_wp_error( $shipping_class ) ) { + return $shipping_class; + } + + $update = wp_update_term( $id, 'product_shipping_class', $data ); + if ( is_wp_error( $update ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_shipping_class', __( 'Could not edit the shipping class', 'woocommerce' ), 400 ); + } + + do_action( 'woocommerce_api_edit_product_shipping_class', $id, $data ); + + return $this->get_product_shipping_class( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product shipping class. + * + * @since 2.5.0 + * @param int $id Product shipping class term ID + * @return array|WP_Error Success message if succeed, otherwise WP_Error + * will be returned + */ + public function delete_product_shipping_class( $id ) { + global $wpdb; + + try { + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_shipping_class', __( 'You do not have permission to delete product shipping classes', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + $deleted = wp_delete_term( $id, 'product_shipping_class' ); + if ( ! $deleted || is_wp_error( $deleted ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_shipping_class', __( 'Could not delete the shipping class', 'woocommerce' ), 401 ); + } + + do_action( 'woocommerce_api_delete_product_shipping_class', $id, $this ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_shipping_class' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/legacy/api/v3/class-wc-api-reports.php b/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-reports.php similarity index 100% rename from includes/legacy/api/v3/class-wc-api-reports.php rename to plugins/woocommerce/includes/legacy/api/v3/class-wc-api-reports.php diff --git a/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-resource.php b/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-resource.php new file mode 100644 index 00000000000..bab067b99d9 --- /dev/null +++ b/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-resource.php @@ -0,0 +1,477 @@ +server = $server; + + // automatically register routes for sub-classes + add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) ); + + // maybe add meta to top-level resource responses + foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) { + add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 ); + } + + $response_names = array( + 'order', + 'coupon', + 'customer', + 'product', + 'report', + 'customer_orders', + 'customer_downloads', + 'order_note', + 'order_refund', + 'product_reviews', + 'product_category', + 'tax', + 'tax_class', + ); + + foreach ( $response_names as $name ) { + + /** + * Remove fields from responses when requests specify certain fields + * note these are hooked at a later priority so data added via + * filters (e.g. customer data to the order response) still has the + * fields filtered properly + */ + add_filter( "woocommerce_api_{$name}_response", array( $this, 'filter_response_fields' ), 20, 3 ); + } + } + + /** + * Validate the request by checking: + * + * 1) the ID is a valid integer + * 2) the ID returns a valid post object and matches the provided post type + * 3) the current user has the proper permissions to read/edit/delete the post + * + * @since 2.1 + * @param string|int $id the post ID + * @param string $type the post type, either `shop_order`, `shop_coupon`, or `product` + * @param string $context the context of the request, either `read`, `edit` or `delete` + * @return int|WP_Error valid post ID or WP_Error if any of the checks fails + */ + protected function validate_request( $id, $type, $context ) { + + if ( 'shop_order' === $type || 'shop_coupon' === $type || 'shop_webhook' === $type ) { + $resource_name = str_replace( 'shop_', '', $type ); + } else { + $resource_name = $type; + } + + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) ); + } + + // Only custom post types have per-post type/permission checks + if ( 'customer' === $type ) { + return $id; + } + + $post = get_post( $id ); + + // Orders request are a special case. + $is_invalid_orders_request = ( 'shop_order' === $type && ( ! $post || ! is_a ( $post, 'WP_Post' ) || 'shop_order' !== $post->post_type ) && ! wc_rest_check_post_permissions( 'shop_order', 'read' ) ); + + if ( ! $is_invalid_orders_request ) { + if ( null === $post ) { + return new WP_Error( "woocommerce_api_no_{$resource_name}_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), $resource_name, $id ), array( 'status' => 404 ) ); + } + + // For checking permissions, product variations are the same as the product post type + $post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type; + + // Validate post type + if ( $type !== $post_type ) { + return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) ); + } + } + + // Validate permissions + switch ( $context ) { + + case 'read': + if ( $is_invalid_orders_request || ! $this->is_readable( $post ) ) { + return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); + } + break; + + case 'edit': + if ( $is_invalid_orders_request || ! $this->is_editable( $post ) ) { + return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); + } + break; + + case 'delete': + if ( $is_invalid_orders_request || ! $this->is_deletable( $post ) ) { + return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); + } + break; + } + + return $id; + } + + /** + * Add common request arguments to argument list before WP_Query is run + * + * @since 2.1 + * @param array $base_args required arguments for the query (e.g. `post_type`, etc) + * @param array $request_args arguments provided in the request + * @return array + */ + protected function merge_query_args( $base_args, $request_args ) { + + $args = array(); + + // date + if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) { + + $args['date_query'] = array(); + + // resources created after specified date + if ( ! empty( $request_args['created_at_min'] ) ) { + $args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true ); + } + + // resources created before specified date + if ( ! empty( $request_args['created_at_max'] ) ) { + $args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true ); + } + + // resources updated after specified date + if ( ! empty( $request_args['updated_at_min'] ) ) { + $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true ); + } + + // resources updated before specified date + if ( ! empty( $request_args['updated_at_max'] ) ) { + $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true ); + } + } + + // search + if ( ! empty( $request_args['q'] ) ) { + $args['s'] = $request_args['q']; + } + + // resources per response + if ( ! empty( $request_args['limit'] ) ) { + $args['posts_per_page'] = $request_args['limit']; + } + + // resource offset + if ( ! empty( $request_args['offset'] ) ) { + $args['offset'] = $request_args['offset']; + } + + // order (ASC or DESC, ASC by default) + if ( ! empty( $request_args['order'] ) ) { + $args['order'] = $request_args['order']; + } + + // orderby + if ( ! empty( $request_args['orderby'] ) ) { + $args['orderby'] = $request_args['orderby']; + + // allow sorting by meta value + if ( ! empty( $request_args['orderby_meta_key'] ) ) { + $args['meta_key'] = $request_args['orderby_meta_key']; + } + } + + // allow post status change + if ( ! empty( $request_args['post_status'] ) ) { + $args['post_status'] = $request_args['post_status']; + unset( $request_args['post_status'] ); + } + + // filter by a list of post id + if ( ! empty( $request_args['in'] ) ) { + $args['post__in'] = explode( ',', $request_args['in'] ); + unset( $request_args['in'] ); + } + + // exclude by a list of post id + if ( ! empty( $request_args['not_in'] ) ) { + $args['post__not_in'] = explode( ',', $request_args['not_in'] ); + unset( $request_args['not_in'] ); + } + + // resource page + $args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1; + + $args = apply_filters( 'woocommerce_api_query_args', $args, $request_args ); + + return array_merge( $base_args, $args ); + } + + /** + * Add meta to resources when requested by the client. Meta is added as a top-level + * `_meta` attribute (e.g. `order_meta`) as a list of key/value pairs + * + * @since 2.1 + * @param array $data the resource data + * @param object $resource the resource object (e.g WC_Order) + * @return mixed + */ + public function maybe_add_meta( $data, $resource ) { + + if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) { + + // don't attempt to add meta more than once + if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) { + return $data; + } + + // define the top-level property name for the meta + switch ( get_class( $resource ) ) { + + case 'WC_Order': + $meta_name = 'order_meta'; + break; + + case 'WC_Coupon': + $meta_name = 'coupon_meta'; + break; + + case 'WP_User': + $meta_name = 'customer_meta'; + break; + + default: + $meta_name = 'product_meta'; + break; + } + + if ( is_a( $resource, 'WP_User' ) ) { + + // customer meta + $meta = (array) get_user_meta( $resource->ID ); + + } else { + + // coupon/order/product meta + $meta = (array) get_post_meta( $resource->get_id() ); + } + + foreach ( $meta as $meta_key => $meta_value ) { + + // don't add hidden meta by default + if ( ! is_protected_meta( $meta_key ) ) { + $data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] ); + } + } + } + + return $data; + } + + /** + * Restrict the fields included in the response if the request specified certain only certain fields should be returned + * + * @since 2.1 + * @param array $data the response data + * @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order + * @param array|string the requested list of fields to include in the response + * @return array response data + */ + public function filter_response_fields( $data, $resource, $fields ) { + + if ( ! is_array( $data ) || empty( $fields ) ) { + return $data; + } + + $fields = explode( ',', $fields ); + $sub_fields = array(); + + // get sub fields + foreach ( $fields as $field ) { + + if ( false !== strpos( $field, '.' ) ) { + + list( $name, $value ) = explode( '.', $field ); + + $sub_fields[ $name ] = $value; + } + } + + // iterate through top-level fields + foreach ( $data as $data_field => $data_value ) { + + // if a field has sub-fields and the top-level field has sub-fields to filter + if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) { + + // iterate through each sub-field + foreach ( $data_value as $sub_field => $sub_field_value ) { + + // remove non-matching sub-fields + if ( ! in_array( $sub_field, $sub_fields ) ) { + unset( $data[ $data_field ][ $sub_field ] ); + } + } + } else { + + // remove non-matching top-level fields + if ( ! in_array( $data_field, $fields ) ) { + unset( $data[ $data_field ] ); + } + } + } + + return $data; + } + + /** + * Delete a given resource + * + * @since 2.1 + * @param int $id the resource ID + * @param string $type the resource post type, or `customer` + * @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`) + * @return array|WP_Error + */ + protected function delete( $id, $type, $force = false ) { + + if ( 'shop_order' === $type || 'shop_coupon' === $type ) { + $resource_name = str_replace( 'shop_', '', $type ); + } else { + $resource_name = $type; + } + + if ( 'customer' === $type ) { + + $result = wp_delete_user( $id ); + + if ( $result ) { + return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) ); + } else { + return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) ); + } + } else { + + // delete order/coupon/product/webhook + $result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id ); + + if ( ! $result ) { + return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) ); + } + + if ( $force ) { + return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) ); + + } else { + + $this->server->send_status( '202' ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) ); + } + } + } + + + /** + * Checks if the given post is readable by the current user + * + * @since 2.1 + * @see WC_API_Resource::check_permission() + * @param WP_Post|int $post + * @return bool + */ + protected function is_readable( $post ) { + + return $this->check_permission( $post, 'read' ); + } + + /** + * Checks if the given post is editable by the current user + * + * @since 2.1 + * @see WC_API_Resource::check_permission() + * @param WP_Post|int $post + * @return bool + */ + protected function is_editable( $post ) { + + return $this->check_permission( $post, 'edit' ); + + } + + /** + * Checks if the given post is deletable by the current user + * + * @since 2.1 + * @see WC_API_Resource::check_permission() + * @param WP_Post|int $post + * @return bool + */ + protected function is_deletable( $post ) { + + return $this->check_permission( $post, 'delete' ); + } + + /** + * Checks the permissions for the current user given a post and context + * + * @since 2.1 + * @param WP_Post|int $post + * @param string $context the type of permission to check, either `read`, `write`, or `delete` + * @return bool true if the current user has the permissions to perform the context on the post + */ + private function check_permission( $post, $context ) { + $permission = false; + + if ( ! is_a( $post, 'WP_Post' ) ) { + $post = get_post( $post ); + } + + if ( is_null( $post ) ) { + return $permission; + } + + $post_type = get_post_type_object( $post->post_type ); + + if ( 'read' === $context ) { + $permission = 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID ); + } elseif ( 'edit' === $context ) { + $permission = current_user_can( $post_type->cap->edit_post, $post->ID ); + } elseif ( 'delete' === $context ) { + $permission = current_user_can( $post_type->cap->delete_post, $post->ID ); + } + + return apply_filters( 'woocommerce_api_check_permission', $permission, $context, $post, $post_type ); + } +} diff --git a/includes/legacy/api/v3/class-wc-api-server.php b/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-server.php similarity index 100% rename from includes/legacy/api/v3/class-wc-api-server.php rename to plugins/woocommerce/includes/legacy/api/v3/class-wc-api-server.php diff --git a/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-taxes.php b/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-taxes.php new file mode 100644 index 00000000000..d5a96ccf994 --- /dev/null +++ b/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-taxes.php @@ -0,0 +1,649 @@ + + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET/POST /taxes + $routes[ $this->base ] = array( + array( array( $this, 'get_taxes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_tax' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /taxes/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_taxes_count' ), WC_API_Server::READABLE ), + ); + + # GET/PUT/DELETE /taxes/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_tax' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_tax' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ), + array( array( $this, 'delete_tax' ), WC_API_SERVER::DELETABLE ), + ); + + # GET/POST /taxes/classes + $routes[ $this->base . '/classes' ] = array( + array( array( $this, 'get_tax_classes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_tax_class' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /taxes/classes/count + $routes[ $this->base . '/classes/count' ] = array( + array( array( $this, 'get_tax_classes_count' ), WC_API_Server::READABLE ), + ); + + # GET /taxes/classes/ + $routes[ $this->base . '/classes/(?P\w[\w\s\-]*)' ] = array( + array( array( $this, 'delete_tax_class' ), WC_API_SERVER::DELETABLE ), + ); + + # POST|PUT /taxes/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + return $routes; + } + + /** + * Get all taxes + * + * @since 2.5.0 + * + * @param string $fields + * @param array $filter + * @param string $class + * @param int $page + * + * @return array + */ + public function get_taxes( $fields = null, $filter = array(), $class = null, $page = 1 ) { + if ( ! empty( $class ) ) { + $filter['tax_rate_class'] = $class; + } + + $filter['page'] = $page; + + $query = $this->query_tax_rates( $filter ); + + $taxes = array(); + + foreach ( $query['results'] as $tax ) { + $taxes[] = current( $this->get_tax( $tax->tax_rate_id, $fields ) ); + } + + // Set pagination headers + $this->server->add_pagination_headers( $query['headers'] ); + + return array( 'taxes' => $taxes ); + } + + /** + * Get the tax for the given ID + * + * @since 2.5.0 + * + * @param int $id The tax ID + * @param string $fields fields to include in response + * + * @return array|WP_Error + */ + public function get_tax( $id, $fields = null ) { + global $wpdb; + + try { + $id = absint( $id ); + + // Permissions check + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax', __( 'You do not have permission to read tax rate', 'woocommerce' ), 401 ); + } + + // Get tax rate details + $tax = WC_Tax::_get_tax_rate( $id ); + + if ( is_wp_error( $tax ) || empty( $tax ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_tax_id', __( 'A tax rate with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $tax_data = array( + 'id' => (int) $tax['tax_rate_id'], + 'country' => $tax['tax_rate_country'], + 'state' => $tax['tax_rate_state'], + 'postcode' => '', + 'city' => '', + 'rate' => $tax['tax_rate'], + 'name' => $tax['tax_rate_name'], + 'priority' => (int) $tax['tax_rate_priority'], + 'compound' => (bool) $tax['tax_rate_compound'], + 'shipping' => (bool) $tax['tax_rate_shipping'], + 'order' => (int) $tax['tax_rate_order'], + 'class' => $tax['tax_rate_class'] ? $tax['tax_rate_class'] : 'standard', + ); + + // Get locales from a tax rate + $locales = $wpdb->get_results( $wpdb->prepare( " + SELECT location_code, location_type + FROM {$wpdb->prefix}woocommerce_tax_rate_locations + WHERE tax_rate_id = %d + ", $id ) ); + + if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) { + foreach ( $locales as $locale ) { + $tax_data[ $locale->location_type ] = $locale->location_code; + } + } + + return array( 'tax' => apply_filters( 'woocommerce_api_tax_response', $tax_data, $tax, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a tax + * + * @since 2.5.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function create_tax( $data ) { + try { + if ( ! isset( $data['tax'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_tax_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'tax' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_tax', __( 'You do not have permission to create tax rates', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_tax_data', $data['tax'], $this ); + + $tax_data = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '', + 'tax_rate_name' => '', + 'tax_rate_priority' => 1, + 'tax_rate_compound' => 0, + 'tax_rate_shipping' => 1, + 'tax_rate_order' => 0, + 'tax_rate_class' => '', + ); + + foreach ( $tax_data as $key => $value ) { + $new_key = str_replace( 'tax_rate_', '', $key ); + $new_key = 'tax_rate' === $new_key ? 'rate' : $new_key; + + if ( isset( $data[ $new_key ] ) ) { + if ( in_array( $new_key, array( 'compound', 'shipping' ) ) ) { + $tax_data[ $key ] = $data[ $new_key ] ? 1 : 0; + } else { + $tax_data[ $key ] = $data[ $new_key ]; + } + } + } + + // Create tax rate + $id = WC_Tax::_insert_tax_rate( $tax_data ); + + // Add locales + if ( ! empty( $data['postcode'] ) ) { + WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $data['postcode'] ) ); + } + + if ( ! empty( $data['city'] ) ) { + WC_Tax::_update_tax_rate_cities( $id, wc_clean( $data['city'] ) ); + } + + do_action( 'woocommerce_api_create_tax', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_tax( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a tax + * + * @since 2.5.0 + * + * @param int $id The tax ID + * @param array $data + * + * @return array|WP_Error + */ + public function edit_tax( $id, $data ) { + try { + if ( ! isset( $data['tax'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_tax_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'tax' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_tax', __( 'You do not have permission to edit tax rates', 'woocommerce' ), 401 ); + } + + $data = $data['tax']; + + // Get current tax rate data + $tax = $this->get_tax( $id ); + + if ( is_wp_error( $tax ) ) { + $error_data = $tax->get_error_data(); + throw new WC_API_Exception( $tax->get_error_code(), $tax->get_error_message(), $error_data['status'] ); + } + + $current_data = $tax['tax']; + $data = apply_filters( 'woocommerce_api_edit_tax_data', $data, $this ); + $tax_data = array(); + $default_fields = array( + 'tax_rate_country', + 'tax_rate_state', + 'tax_rate', + 'tax_rate_name', + 'tax_rate_priority', + 'tax_rate_compound', + 'tax_rate_shipping', + 'tax_rate_order', + 'tax_rate_class', + ); + + foreach ( $data as $key => $value ) { + $new_key = 'rate' === $key ? 'tax_rate' : 'tax_rate_' . $key; + + // Check if the key is valid + if ( ! in_array( $new_key, $default_fields ) ) { + continue; + } + + // Test new data against current data + if ( $value === $current_data[ $key ] ) { + continue; + } + + // Fix compound and shipping values + if ( in_array( $key, array( 'compound', 'shipping' ) ) ) { + $value = $value ? 1 : 0; + } + + $tax_data[ $new_key ] = $value; + } + + // Update tax rate + WC_Tax::_update_tax_rate( $id, $tax_data ); + + // Update locales + if ( ! empty( $data['postcode'] ) && $current_data['postcode'] != $data['postcode'] ) { + WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $data['postcode'] ) ); + } + + if ( ! empty( $data['city'] ) && $current_data['city'] != $data['city'] ) { + WC_Tax::_update_tax_rate_cities( $id, wc_clean( $data['city'] ) ); + } + + do_action( 'woocommerce_api_edit_tax_rate', $id, $data ); + + return $this->get_tax( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a tax + * + * @since 2.5.0 + * + * @param int $id The tax ID + * + * @return array|WP_Error + */ + public function delete_tax( $id ) { + global $wpdb; + + try { + // Check permissions + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_tax', __( 'You do not have permission to delete tax rates', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + + WC_Tax::_delete_tax_rate( $id ); + + if ( 0 === $wpdb->rows_affected ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_tax', __( 'Could not delete the tax rate', 'woocommerce' ), 401 ); + } + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'tax' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the total number of taxes + * + * @since 2.5.0 + * + * @param string $class + * @param array $filter + * + * @return array|WP_Error + */ + public function get_taxes_count( $class = null, $filter = array() ) { + try { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_taxes_count', __( 'You do not have permission to read the taxes count', 'woocommerce' ), 401 ); + } + + if ( ! empty( $class ) ) { + $filter['tax_rate_class'] = $class; + } + + $query = $this->query_tax_rates( $filter, true ); + + return array( 'count' => (int) $query['headers']->total ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Helper method to get tax rates objects + * + * @since 2.5.0 + * + * @param array $args + * @param bool $count_only + * + * @return array + */ + protected function query_tax_rates( $args, $count_only = false ) { + global $wpdb; + + $results = ''; + + // Set args + $args = $this->merge_query_args( $args, array() ); + + $query = " + SELECT tax_rate_id + FROM {$wpdb->prefix}woocommerce_tax_rates + WHERE 1 = 1 + "; + + // Filter by tax class + if ( ! empty( $args['tax_rate_class'] ) ) { + $tax_rate_class = 'standard' !== $args['tax_rate_class'] ? sanitize_title( $args['tax_rate_class'] ) : ''; + $query .= " AND tax_rate_class = '$tax_rate_class'"; + } + + // Order tax rates + $order_by = ' ORDER BY tax_rate_order'; + + // Pagination + $per_page = absint( isset( $args['posts_per_page'] ) ? $args['posts_per_page'] : get_option( 'posts_per_page' ) ); + $offset = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $per_page : 0; + $pagination = sprintf( ' LIMIT %d, %d', $offset, $per_page ); + + if ( ! $count_only ) { + $results = $wpdb->get_results( $query . $order_by . $pagination ); + } + + $wpdb->get_results( $query ); + $headers = new stdClass; + $headers->page = $args['paged']; + $headers->total = (int) $wpdb->num_rows; + $headers->is_single = $per_page > $headers->total; + $headers->total_pages = ceil( $headers->total / $per_page ); + + return array( + 'results' => $results, + 'headers' => $headers, + ); + } + + /** + * Bulk update or insert taxes + * Accepts an array with taxes in the formats supported by + * WC_API_Taxes->create_tax() and WC_API_Taxes->edit_tax() + * + * @since 2.5.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + try { + if ( ! isset( $data['taxes'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_taxes_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'taxes' ), 400 ); + } + + $data = $data['taxes']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'taxes' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_taxes_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $taxes = array(); + + foreach ( $data as $_tax ) { + $tax_id = 0; + + // Try to get the tax rate ID + if ( isset( $_tax['id'] ) ) { + $tax_id = intval( $_tax['id'] ); + } + + if ( $tax_id ) { + + // Tax rate exists / edit tax rate + $edit = $this->edit_tax( $tax_id, array( 'tax' => $_tax ) ); + + if ( is_wp_error( $edit ) ) { + $taxes[] = array( + 'id' => $tax_id, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $taxes[] = $edit['tax']; + } + } else { + + // Tax rate don't exists / create tax rate + $new = $this->create_tax( array( 'tax' => $_tax ) ); + + if ( is_wp_error( $new ) ) { + $taxes[] = array( + 'id' => $tax_id, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $taxes[] = $new['tax']; + } + } + } + + return array( 'taxes' => apply_filters( 'woocommerce_api_taxes_bulk_response', $taxes, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get all tax classes + * + * @since 2.5.0 + * + * @param string $fields + * + * @return array|WP_Error + */ + public function get_tax_classes( $fields = null ) { + try { + // Permissions check + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax_classes', __( 'You do not have permission to read tax classes', 'woocommerce' ), 401 ); + } + + $tax_classes = array(); + + // Add standard class + $tax_classes[] = array( + 'slug' => 'standard', + 'name' => __( 'Standard rate', 'woocommerce' ), + ); + + $classes = WC_Tax::get_tax_classes(); + + foreach ( $classes as $class ) { + $tax_classes[] = apply_filters( 'woocommerce_api_tax_class_response', array( + 'slug' => sanitize_title( $class ), + 'name' => $class, + ), $class, $fields, $this ); + } + + return array( 'tax_classes' => apply_filters( 'woocommerce_api_tax_classes_response', $tax_classes, $classes, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a tax class. + * + * @since 2.5.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function create_tax_class( $data ) { + try { + if ( ! isset( $data['tax_class'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_tax_class_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'tax_class' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_tax_class', __( 'You do not have permission to create tax classes', 'woocommerce' ), 401 ); + } + + $data = $data['tax_class']; + + if ( empty( $data['name'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_tax_class_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); + } + + $name = sanitize_text_field( $data['name'] ); + $tax_class = WC_Tax::create_tax_class( $name ); + + if ( is_wp_error( $tax_class ) ) { + return new WP_Error( 'woocommerce_api_' . $tax_class->get_error_code(), $tax_class->get_error_message(), 401 ); + } + + do_action( 'woocommerce_api_create_tax_class', $tax_class['slug'], $data ); + + $this->server->send_status( 201 ); + + return array( + 'tax_class' => $tax_class, + ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a tax class + * + * @since 2.5.0 + * + * @param int $slug The tax class slug + * + * @return array|WP_Error + */ + public function delete_tax_class( $slug ) { + global $wpdb; + + try { + // Check permissions + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_tax_class', __( 'You do not have permission to delete tax classes', 'woocommerce' ), 401 ); + } + + $slug = sanitize_title( $slug ); + $tax_class = WC_Tax::get_tax_class_by( 'slug', $slug ); + $deleted = WC_Tax::delete_tax_class_by( 'slug', $slug ); + + if ( is_wp_error( $deleted ) || ! $deleted ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_tax_class', __( 'Could not delete the tax class', 'woocommerce' ), 401 ); + } + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'tax_class' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the total number of tax classes + * + * @since 2.5.0 + * + * @return array|WP_Error + */ + public function get_tax_classes_count() { + try { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax_classes_count', __( 'You do not have permission to read the tax classes count', 'woocommerce' ), 401 ); + } + + $total = count( WC_Tax::get_tax_classes() ) + 1; // +1 for Standard Rate + + return array( 'count' => $total ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/legacy/api/v3/class-wc-api-webhooks.php b/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-webhooks.php similarity index 100% rename from includes/legacy/api/v3/class-wc-api-webhooks.php rename to plugins/woocommerce/includes/legacy/api/v3/class-wc-api-webhooks.php diff --git a/includes/legacy/api/v3/interface-wc-api-handler.php b/plugins/woocommerce/includes/legacy/api/v3/interface-wc-api-handler.php similarity index 100% rename from includes/legacy/api/v3/interface-wc-api-handler.php rename to plugins/woocommerce/includes/legacy/api/v3/interface-wc-api-handler.php diff --git a/plugins/woocommerce/includes/legacy/class-wc-legacy-api.php b/plugins/woocommerce/includes/legacy/class-wc-legacy-api.php new file mode 100644 index 00000000000..e0ae2afdd8e --- /dev/null +++ b/plugins/woocommerce/includes/legacy/class-wc-legacy-api.php @@ -0,0 +1,298 @@ +query_vars['wc-api-version'] = $_GET['wc-api-version']; + } + + if ( ! empty( $_GET['wc-api-route'] ) ) { + $wp->query_vars['wc-api-route'] = $_GET['wc-api-route']; + } + + // REST API request. + if ( ! empty( $wp->query_vars['wc-api-version'] ) && ! empty( $wp->query_vars['wc-api-route'] ) ) { + + wc_maybe_define_constant( 'WC_API_REQUEST', true ); + wc_maybe_define_constant( 'WC_API_REQUEST_VERSION', absint( $wp->query_vars['wc-api-version'] ) ); + + // Legacy v1 API request. + if ( 1 === WC_API_REQUEST_VERSION ) { + $this->handle_v1_rest_api_request(); + } elseif ( 2 === WC_API_REQUEST_VERSION ) { + $this->handle_v2_rest_api_request(); + } else { + $this->includes(); + + $this->server = new WC_API_Server( $wp->query_vars['wc-api-route'] ); + + // load API resource classes. + $this->register_resources( $this->server ); + + // Fire off the request. + $this->server->serve_request(); + } + + exit; + } + } + + /** + * Include required files for REST API request. + * + * @since 2.1 + * @deprecated 2.6.0 + */ + public function includes() { + + // API server / response handlers. + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-exception.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-server.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/interface-wc-api-handler.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-json-handler.php' ); + + // Authentication. + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-authentication.php' ); + $this->authentication = new WC_API_Authentication(); + + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-resource.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-coupons.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-customers.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-orders.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-products.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-reports.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-taxes.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-webhooks.php' ); + + // Allow plugins to load other response handlers or resource classes. + do_action( 'woocommerce_api_loaded' ); + } + + /** + * Register available API resources. + * + * @since 2.1 + * @deprecated 2.6.0 + * @param WC_API_Server $server the REST server. + */ + public function register_resources( $server ) { + + $api_classes = apply_filters( 'woocommerce_api_classes', + array( + 'WC_API_Coupons', + 'WC_API_Customers', + 'WC_API_Orders', + 'WC_API_Products', + 'WC_API_Reports', + 'WC_API_Taxes', + 'WC_API_Webhooks', + ) + ); + + foreach ( $api_classes as $api_class ) { + $this->$api_class = new $api_class( $server ); + } + } + + + /** + * Handle legacy v1 REST API requests. + * + * @since 2.2 + * @deprecated 2.6.0 + */ + private function handle_v1_rest_api_request() { + + // Include legacy required files for v1 REST API request. + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-server.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/interface-wc-api-handler.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-json-handler.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-xml-handler.php' ); + + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-authentication.php' ); + $this->authentication = new WC_API_Authentication(); + + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-resource.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-coupons.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-customers.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-orders.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-products.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-reports.php' ); + + // Allow plugins to load other response handlers or resource classes. + do_action( 'woocommerce_api_loaded' ); + + $this->server = new WC_API_Server( $GLOBALS['wp']->query_vars['wc-api-route'] ); + + // Register available resources for legacy v1 REST API request. + $api_classes = apply_filters( 'woocommerce_api_classes', + array( + 'WC_API_Customers', + 'WC_API_Orders', + 'WC_API_Products', + 'WC_API_Coupons', + 'WC_API_Reports', + ) + ); + + foreach ( $api_classes as $api_class ) { + $this->$api_class = new $api_class( $this->server ); + } + + // Fire off the request. + $this->server->serve_request(); + } + + /** + * Handle legacy v2 REST API requests. + * + * @since 2.4 + * @deprecated 2.6.0 + */ + private function handle_v2_rest_api_request() { + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-exception.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-server.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/interface-wc-api-handler.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-json-handler.php' ); + + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-authentication.php' ); + $this->authentication = new WC_API_Authentication(); + + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-resource.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-coupons.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-customers.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-orders.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-products.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-reports.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-webhooks.php' ); + + // allow plugins to load other response handlers or resource classes. + do_action( 'woocommerce_api_loaded' ); + + $this->server = new WC_API_Server( $GLOBALS['wp']->query_vars['wc-api-route'] ); + + // Register available resources for legacy v2 REST API request. + $api_classes = apply_filters( 'woocommerce_api_classes', + array( + 'WC_API_Customers', + 'WC_API_Orders', + 'WC_API_Products', + 'WC_API_Coupons', + 'WC_API_Reports', + 'WC_API_Webhooks', + ) + ); + + foreach ( $api_classes as $api_class ) { + $this->$api_class = new $api_class( $this->server ); + } + + // Fire off the request. + $this->server->serve_request(); + } + + /** + * Rest API Init. + * + * @deprecated 3.7.0 - REST API classes autoload. + */ + public function rest_api_init() {} + + /** + * Include REST API classes. + * + * @deprecated 3.7.0 - REST API classes autoload. + */ + public function rest_api_includes() { + $this->rest_api_init(); + } + /** + * Register REST API routes. + * + * @deprecated 3.7.0 + */ + public function register_rest_routes() { + wc_deprecated_function( 'WC_Legacy_API::register_rest_routes', '3.7.0', '' ); + $this->register_wp_admin_settings(); + } +} diff --git a/includes/legacy/class-wc-legacy-cart.php b/plugins/woocommerce/includes/legacy/class-wc-legacy-cart.php similarity index 100% rename from includes/legacy/class-wc-legacy-cart.php rename to plugins/woocommerce/includes/legacy/class-wc-legacy-cart.php diff --git a/includes/legacy/class-wc-legacy-coupon.php b/plugins/woocommerce/includes/legacy/class-wc-legacy-coupon.php similarity index 100% rename from includes/legacy/class-wc-legacy-coupon.php rename to plugins/woocommerce/includes/legacy/class-wc-legacy-coupon.php diff --git a/includes/legacy/class-wc-legacy-customer.php b/plugins/woocommerce/includes/legacy/class-wc-legacy-customer.php similarity index 100% rename from includes/legacy/class-wc-legacy-customer.php rename to plugins/woocommerce/includes/legacy/class-wc-legacy-customer.php diff --git a/includes/legacy/class-wc-legacy-shipping-zone.php b/plugins/woocommerce/includes/legacy/class-wc-legacy-shipping-zone.php similarity index 100% rename from includes/legacy/class-wc-legacy-shipping-zone.php rename to plugins/woocommerce/includes/legacy/class-wc-legacy-shipping-zone.php diff --git a/includes/legacy/class-wc-legacy-webhook.php b/plugins/woocommerce/includes/legacy/class-wc-legacy-webhook.php similarity index 100% rename from includes/legacy/class-wc-legacy-webhook.php rename to plugins/woocommerce/includes/legacy/class-wc-legacy-webhook.php diff --git a/includes/libraries/class-wc-eval-math.php b/plugins/woocommerce/includes/libraries/class-wc-eval-math.php similarity index 100% rename from includes/libraries/class-wc-eval-math.php rename to plugins/woocommerce/includes/libraries/class-wc-eval-math.php diff --git a/includes/libraries/wp-async-request.php b/plugins/woocommerce/includes/libraries/wp-async-request.php similarity index 100% rename from includes/libraries/wp-async-request.php rename to plugins/woocommerce/includes/libraries/wp-async-request.php diff --git a/includes/libraries/wp-background-process.php b/plugins/woocommerce/includes/libraries/wp-background-process.php similarity index 100% rename from includes/libraries/wp-background-process.php rename to plugins/woocommerce/includes/libraries/wp-background-process.php diff --git a/includes/log-handlers/class-wc-log-handler-db.php b/plugins/woocommerce/includes/log-handlers/class-wc-log-handler-db.php similarity index 100% rename from includes/log-handlers/class-wc-log-handler-db.php rename to plugins/woocommerce/includes/log-handlers/class-wc-log-handler-db.php diff --git a/includes/log-handlers/class-wc-log-handler-email.php b/plugins/woocommerce/includes/log-handlers/class-wc-log-handler-email.php similarity index 100% rename from includes/log-handlers/class-wc-log-handler-email.php rename to plugins/woocommerce/includes/log-handlers/class-wc-log-handler-email.php diff --git a/plugins/woocommerce/includes/log-handlers/class-wc-log-handler-file.php b/plugins/woocommerce/includes/log-handlers/class-wc-log-handler-file.php new file mode 100644 index 00000000000..49347b5a259 --- /dev/null +++ b/plugins/woocommerce/includes/log-handlers/class-wc-log-handler-file.php @@ -0,0 +1,446 @@ +log_size_limit = apply_filters( 'woocommerce_log_file_size_limit', $log_size_limit ); + + add_action( 'plugins_loaded', array( $this, 'write_cached_logs' ) ); + } + + /** + * Destructor. + * + * Cleans up open file handles. + */ + public function __destruct() { + foreach ( $this->handles as $handle ) { + if ( is_resource( $handle ) ) { + fclose( $handle ); // @codingStandardsIgnoreLine. + } + } + } + + /** + * Handle a log entry. + * + * @param int $timestamp Log timestamp. + * @param string $level emergency|alert|critical|error|warning|notice|info|debug. + * @param string $message Log message. + * @param array $context { + * Additional information for log handlers. + * + * @type string $source Optional. Determines log file to write to. Default 'log'. + * @type bool $_legacy Optional. Default false. True to use outdated log format + * originally used in deprecated WC_Logger::add calls. + * } + * + * @return bool False if value was not handled and true if value was handled. + */ + public function handle( $timestamp, $level, $message, $context ) { + + if ( isset( $context['source'] ) && $context['source'] ) { + $handle = $context['source']; + } else { + $handle = 'log'; + } + + $entry = self::format_entry( $timestamp, $level, $message, $context ); + + return $this->add( $entry, $handle ); + } + + /** + * Builds a log entry text from timestamp, level and message. + * + * @param int $timestamp Log timestamp. + * @param string $level emergency|alert|critical|error|warning|notice|info|debug. + * @param string $message Log message. + * @param array $context Additional information for log handlers. + * + * @return string Formatted log entry. + */ + protected static function format_entry( $timestamp, $level, $message, $context ) { + + if ( isset( $context['_legacy'] ) && true === $context['_legacy'] ) { + if ( isset( $context['source'] ) && $context['source'] ) { + $handle = $context['source']; + } else { + $handle = 'log'; + } + $message = apply_filters( 'woocommerce_logger_add_message', $message, $handle ); + $time = date_i18n( 'm-d-Y @ H:i:s' ); + $entry = "{$time} - {$message}"; + } else { + $entry = parent::format_entry( $timestamp, $level, $message, $context ); + } + + return $entry; + } + + /** + * Open log file for writing. + * + * @param string $handle Log handle. + * @param string $mode Optional. File mode. Default 'a'. + * @return bool Success. + */ + protected function open( $handle, $mode = 'a' ) { + if ( $this->is_open( $handle ) ) { + return true; + } + + $file = self::get_log_file_path( $handle ); + + if ( $file ) { + if ( ! file_exists( $file ) ) { + $temphandle = @fopen( $file, 'w+' ); // @codingStandardsIgnoreLine. + if ( is_resource( $temphandle ) ) { + @fclose( $temphandle ); // @codingStandardsIgnoreLine. + + if ( Constants::is_defined( 'FS_CHMOD_FILE' ) ) { + @chmod( $file, FS_CHMOD_FILE ); // @codingStandardsIgnoreLine. + } + } + } + + $resource = @fopen( $file, $mode ); // @codingStandardsIgnoreLine. + + if ( $resource ) { + $this->handles[ $handle ] = $resource; + return true; + } + } + + return false; + } + + /** + * Check if a handle is open. + * + * @param string $handle Log handle. + * @return bool True if $handle is open. + */ + protected function is_open( $handle ) { + return array_key_exists( $handle, $this->handles ) && is_resource( $this->handles[ $handle ] ); + } + + /** + * Close a handle. + * + * @param string $handle Log handle. + * @return bool success + */ + protected function close( $handle ) { + $result = false; + + if ( $this->is_open( $handle ) ) { + $result = fclose( $this->handles[ $handle ] ); // @codingStandardsIgnoreLine. + unset( $this->handles[ $handle ] ); + } + + return $result; + } + + /** + * Add a log entry to chosen file. + * + * @param string $entry Log entry text. + * @param string $handle Log entry handle. + * + * @return bool True if write was successful. + */ + protected function add( $entry, $handle ) { + $result = false; + + if ( $this->should_rotate( $handle ) ) { + $this->log_rotate( $handle ); + } + + if ( $this->open( $handle ) && is_resource( $this->handles[ $handle ] ) ) { + $result = fwrite( $this->handles[ $handle ], $entry . PHP_EOL ); // @codingStandardsIgnoreLine. + } else { + $this->cache_log( $entry, $handle ); + } + + return false !== $result; + } + + /** + * Clear entries from chosen file. + * + * @param string $handle Log handle. + * + * @return bool + */ + public function clear( $handle ) { + $result = false; + + // Close the file if it's already open. + $this->close( $handle ); + + /** + * $this->open( $handle, 'w' ) == Open the file for writing only. Place the file pointer at + * the beginning of the file, and truncate the file to zero length. + */ + if ( $this->open( $handle, 'w' ) && is_resource( $this->handles[ $handle ] ) ) { + $result = true; + } + + do_action( 'woocommerce_log_clear', $handle ); + + return $result; + } + + /** + * Remove/delete the chosen file. + * + * @param string $handle Log handle. + * + * @return bool + */ + public function remove( $handle ) { + $removed = false; + $logs = $this->get_log_files(); + $handle = sanitize_title( $handle ); + + if ( isset( $logs[ $handle ] ) && $logs[ $handle ] ) { + $file = realpath( trailingslashit( WC_LOG_DIR ) . $logs[ $handle ] ); + if ( 0 === stripos( $file, realpath( trailingslashit( WC_LOG_DIR ) ) ) && is_file( $file ) && is_writable( $file ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable + $this->close( $file ); // Close first to be certain no processes keep it alive after it is unlinked. + $removed = unlink( $file ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink + } + do_action( 'woocommerce_log_remove', $handle, $removed ); + } + return $removed; + } + + /** + * Check if log file should be rotated. + * + * Compares the size of the log file to determine whether it is over the size limit. + * + * @param string $handle Log handle. + * @return bool True if if should be rotated. + */ + protected function should_rotate( $handle ) { + $file = self::get_log_file_path( $handle ); + if ( $file ) { + if ( $this->is_open( $handle ) ) { + $file_stat = fstat( $this->handles[ $handle ] ); + return $file_stat['size'] > $this->log_size_limit; + } elseif ( file_exists( $file ) ) { + return filesize( $file ) > $this->log_size_limit; + } else { + return false; + } + } else { + return false; + } + } + + /** + * Rotate log files. + * + * Logs are rotated by prepending '.x' to the '.log' suffix. + * The current log plus 10 historical logs are maintained. + * For example: + * base.9.log -> [ REMOVED ] + * base.8.log -> base.9.log + * ... + * base.0.log -> base.1.log + * base.log -> base.0.log + * + * @param string $handle Log handle. + */ + protected function log_rotate( $handle ) { + for ( $i = 8; $i >= 0; $i-- ) { + $this->increment_log_infix( $handle, $i ); + } + $this->increment_log_infix( $handle ); + } + + /** + * Increment a log file suffix. + * + * @param string $handle Log handle. + * @param null|int $number Optional. Default null. Log suffix number to be incremented. + * @return bool True if increment was successful, otherwise false. + */ + protected function increment_log_infix( $handle, $number = null ) { + if ( null === $number ) { + $suffix = ''; + $next_suffix = '.0'; + } else { + $suffix = '.' . $number; + $next_suffix = '.' . ( $number + 1 ); + } + + $rename_from = self::get_log_file_path( "{$handle}{$suffix}" ); + $rename_to = self::get_log_file_path( "{$handle}{$next_suffix}" ); + + if ( $this->is_open( $rename_from ) ) { + $this->close( $rename_from ); + } + + if ( is_writable( $rename_from ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable + return rename( $rename_from, $rename_to ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_rename + } else { + return false; + } + + } + + /** + * Get a log file path. + * + * @param string $handle Log name. + * @return bool|string The log file path or false if path cannot be determined. + */ + public static function get_log_file_path( $handle ) { + if ( function_exists( 'wp_hash' ) ) { + return trailingslashit( WC_LOG_DIR ) . self::get_log_file_name( $handle ); + } else { + wc_doing_it_wrong( __METHOD__, __( 'This method should not be called before plugins_loaded.', 'woocommerce' ), '3.0' ); + return false; + } + } + + /** + * Get a log file name. + * + * File names consist of the handle, followed by the date, followed by a hash, .log. + * + * @since 3.3 + * @param string $handle Log name. + * @return bool|string The log file name or false if cannot be determined. + */ + public static function get_log_file_name( $handle ) { + if ( function_exists( 'wp_hash' ) ) { + $date_suffix = date( 'Y-m-d', time() ); + $hash_suffix = wp_hash( $handle ); + return sanitize_file_name( implode( '-', array( $handle, $date_suffix, $hash_suffix ) ) . '.log' ); + } else { + wc_doing_it_wrong( __METHOD__, __( 'This method should not be called before plugins_loaded.', 'woocommerce' ), '3.3' ); + return false; + } + } + + /** + * Cache log to write later. + * + * @param string $entry Log entry text. + * @param string $handle Log entry handle. + */ + protected function cache_log( $entry, $handle ) { + $this->cached_logs[] = array( + 'entry' => $entry, + 'handle' => $handle, + ); + } + + /** + * Write cached logs. + */ + public function write_cached_logs() { + foreach ( $this->cached_logs as $log ) { + $this->add( $log['entry'], $log['handle'] ); + } + } + + /** + * Delete all logs older than a defined timestamp. + * + * @since 3.4.0 + * @param integer $timestamp Timestamp to delete logs before. + */ + public static function delete_logs_before_timestamp( $timestamp = 0 ) { + if ( ! $timestamp ) { + return; + } + + $log_files = self::get_log_files(); + + foreach ( $log_files as $log_file ) { + $last_modified = filemtime( trailingslashit( WC_LOG_DIR ) . $log_file ); + + if ( $last_modified < $timestamp ) { + @unlink( trailingslashit( WC_LOG_DIR ) . $log_file ); // @codingStandardsIgnoreLine. + } + } + } + + /** + * Get all log files in the log directory. + * + * @since 3.4.0 + * @return array + */ + public static function get_log_files() { + $files = @scandir( WC_LOG_DIR ); // @codingStandardsIgnoreLine. + $result = array(); + + if ( ! empty( $files ) ) { + foreach ( $files as $key => $value ) { + if ( ! in_array( $value, array( '.', '..' ), true ) ) { + if ( ! is_dir( $value ) && strstr( $value, '.log' ) ) { + $result[ sanitize_title( $value ) ] = $value; + } + } + } + } + + return $result; + } +} diff --git a/includes/payment-tokens/class-wc-payment-token-cc.php b/plugins/woocommerce/includes/payment-tokens/class-wc-payment-token-cc.php similarity index 100% rename from includes/payment-tokens/class-wc-payment-token-cc.php rename to plugins/woocommerce/includes/payment-tokens/class-wc-payment-token-cc.php diff --git a/includes/payment-tokens/class-wc-payment-token-echeck.php b/plugins/woocommerce/includes/payment-tokens/class-wc-payment-token-echeck.php similarity index 100% rename from includes/payment-tokens/class-wc-payment-token-echeck.php rename to plugins/woocommerce/includes/payment-tokens/class-wc-payment-token-echeck.php diff --git a/plugins/woocommerce/includes/queue/class-wc-action-queue.php b/plugins/woocommerce/includes/queue/class-wc-action-queue.php new file mode 100644 index 00000000000..069e27053dc --- /dev/null +++ b/plugins/woocommerce/includes/queue/class-wc-action-queue.php @@ -0,0 +1,160 @@ +schedule_single( time(), $hook, $args, $group ); + } + + /** + * Schedule an action to run once at some time in the future + * + * @param int $timestamp When the job will run. + * @param string $hook The hook to trigger. + * @param array $args Arguments to pass when the hook triggers. + * @param string $group The group to assign this job to. + * @return string The action ID. + */ + public function schedule_single( $timestamp, $hook, $args = array(), $group = '' ) { + return as_schedule_single_action( $timestamp, $hook, $args, $group ); + } + + /** + * Schedule a recurring action + * + * @param int $timestamp When the first instance of the job will run. + * @param int $interval_in_seconds How long to wait between runs. + * @param string $hook The hook to trigger. + * @param array $args Arguments to pass when the hook triggers. + * @param string $group The group to assign this job to. + * @return string The action ID. + */ + public function schedule_recurring( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '' ) { + return as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args, $group ); + } + + /** + * Schedule an action that recurs on a cron-like schedule. + * + * @param int $timestamp The schedule will start on or after this time. + * @param string $cron_schedule A cron-link schedule string. + * @see http://en.wikipedia.org/wiki/Cron + * * * * * * * + * ┬ ┬ ┬ ┬ ┬ ┬ + * | | | | | | + * | | | | | + year [optional] + * | | | | +----- day of week (0 - 7) (Sunday=0 or 7) + * | | | +---------- month (1 - 12) + * | | +--------------- day of month (1 - 31) + * | +-------------------- hour (0 - 23) + * +------------------------- min (0 - 59) + * @param string $hook The hook to trigger. + * @param array $args Arguments to pass when the hook triggers. + * @param string $group The group to assign this job to. + * @return string The action ID + */ + public function schedule_cron( $timestamp, $cron_schedule, $hook, $args = array(), $group = '' ) { + return as_schedule_cron_action( $timestamp, $cron_schedule, $hook, $args, $group ); + } + + /** + * Dequeue the next scheduled instance of an action with a matching hook (and optionally matching args and group). + * + * Any recurring actions with a matching hook should also be cancelled, not just the next scheduled action. + * + * While technically only the next instance of a recurring or cron action is unscheduled by this method, that will also + * prevent all future instances of that recurring or cron action from being run. Recurring and cron actions are scheduled + * in a sequence instead of all being scheduled at once. Each successive occurrence of a recurring action is scheduled + * only after the former action is run. As the next instance is never run, because it's unscheduled by this function, + * then the following instance will never be scheduled (or exist), which is effectively the same as being unscheduled + * by this method also. + * + * @param string $hook The hook that the job will trigger. + * @param array $args Args that would have been passed to the job. + * @param string $group The group the job is assigned to (if any). + */ + public function cancel( $hook, $args = array(), $group = '' ) { + as_unschedule_action( $hook, $args, $group ); + } + + /** + * Dequeue all actions with a matching hook (and optionally matching args and group) so no matching actions are ever run. + * + * @param string $hook The hook that the job will trigger. + * @param array $args Args that would have been passed to the job. + * @param string $group The group the job is assigned to (if any). + */ + public function cancel_all( $hook, $args = array(), $group = '' ) { + as_unschedule_all_actions( $hook, $args, $group ); + } + + /** + * Get the date and time for the next scheduled occurrence of an action with a given hook + * (an optionally that matches certain args and group), if any. + * + * @param string $hook The hook that the job will trigger. + * @param array $args Filter to a hook with matching args that will be passed to the job when it runs. + * @param string $group Filter to only actions assigned to a specific group. + * @return WC_DateTime|null The date and time for the next occurrence, or null if there is no pending, scheduled action for the given hook. + */ + public function get_next( $hook, $args = null, $group = '' ) { + + $next_timestamp = as_next_scheduled_action( $hook, $args, $group ); + + if ( is_numeric( $next_timestamp ) ) { + return new WC_DateTime( "@{$next_timestamp}", new DateTimeZone( 'UTC' ) ); + } + + return null; + } + + /** + * Find scheduled actions + * + * @param array $args Possible arguments, with their default values: + * 'hook' => '' - the name of the action that will be triggered + * 'args' => null - the args array that will be passed with the action + * 'date' => null - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. + * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '=' + * 'modified' => null - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. + * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '=' + * 'group' => '' - the group the action belongs to + * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING + * 'claimed' => null - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID + * 'per_page' => 5 - Number of results to return + * 'offset' => 0 + * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', or 'date' + * 'order' => 'ASC'. + * + * @param string $return_format OBJECT, ARRAY_A, or ids. + * @return array + */ + public function search( $args = array(), $return_format = OBJECT ) { + return as_get_scheduled_actions( $args, $return_format ); + } +} diff --git a/includes/queue/class-wc-queue.php b/plugins/woocommerce/includes/queue/class-wc-queue.php similarity index 100% rename from includes/queue/class-wc-queue.php rename to plugins/woocommerce/includes/queue/class-wc-queue.php diff --git a/plugins/woocommerce/includes/react-admin/class-experimental-abtest.php b/plugins/woocommerce/includes/react-admin/class-experimental-abtest.php new file mode 100644 index 00000000000..7a399fe6064 --- /dev/null +++ b/plugins/woocommerce/includes/react-admin/class-experimental-abtest.php @@ -0,0 +1,203 @@ +anon_id = $anon_id; + $this->platform = $platform; + $this->consent = $consent; + $this->as_auth_wpcom_user = $as_auth_wpcom_user; + } + + /** + * Retrieve the test variation for a provided A/B test. + * + * @param string $test_name Name of the A/B test. + * @return mixed|null A/B test variation, or null on failure. + */ + public function get_variation( $test_name ) { + // Default to the control variation when users haven't consented to tracking. + if ( ! $this->consent ) { + return 'control'; + } + + $variation = $this->fetch_variation( $test_name ); + + // If there was an error retrieving a variation, conceal the error for the consumer. + if ( is_wp_error( $variation ) ) { + return 'control'; + } + + return $variation; + } + + + /** + * Perform the request for a experiment assignment of a provided A/B test from WP.com. + * + * @param array $args Arguments to pass to the request for A/B test. + * @return array|\WP_Error A/B test variation error on failure. + */ + public function request_assignment( $args ) { + // Request as authenticated wp user. + if ( $this->as_auth_wpcom_user && class_exists( Jetpack_Connection_Manager::class ) ) { + $jetpack_connection_manager = new Jetpack_Connection_Manager(); + if ( $jetpack_connection_manager->is_user_connected() ) { + $response = Jetpack_Connection_client::wpcom_json_api_request_as_user( + '/experiments/0.1.0/assignments/' . $this->platform, + '2', + $args + ); + } + } + + // Request as anonymous user. + if ( ! isset( $response ) ) { + $url = add_query_arg( + $args, + sprintf( + 'https://public-api.wordpress.com/wpcom/v2/experiments/0.1.0/assignments/%s', + $this->platform + ) + ); + $response = wp_remote_get( $url ); + } + + return $response; + } + + /** + * Fetch and cache the test variation for a provided A/B test from WP.com. + * + * ExPlat returns a null value when the assigned variation is control or + * an assignment has not been set. In these instances, this method returns + * a value of "control". + * + * @param string $test_name Name of the A/B test. + * @return array|\WP_Error A/B test variation, or error on failure. + */ + protected function fetch_variation( $test_name ) { + // Make sure test name exists. + if ( ! $test_name ) { + return new \WP_Error( 'test_name_not_provided', 'A/B test name has not been provided.' ); + } + + // Make sure test name is a valid one. + if ( ! preg_match( '/^[A-Za-z0-9_]+$/', $test_name ) ) { + return new \WP_Error( 'invalid_test_name', 'Invalid A/B test name.' ); + } + + // Return internal-cached test variations. + if ( isset( $this->tests[ $test_name ] ) ) { + return $this->tests[ $test_name ]; + } + + // Return external-cached test variations. + if ( ! empty( get_transient( 'abtest_variation_' . $test_name ) ) ) { + return get_transient( 'abtest_variation_' . $test_name ); + } + + // Make the request to the WP.com API. + $args = array( + 'experiment_name' => $test_name, + 'anon_id' => rawurlencode( $this->anon_id ), + 'woo_country_code' => rawurlencode( get_option( 'woocommerce_default_country', 'US:CA' ) ), + ); + $args = apply_filters( 'woocommerce_explat_request_args', $args ); + $response = $this->request_assignment( $args ); + + // Bail if there was an error or malformed response. + if ( is_wp_error( $response ) || ! is_array( $response ) || ! isset( $response['body'] ) ) { + return new \WP_Error( 'failed_to_fetch_data', 'Unable to fetch the requested data.' ); + } + + // Decode the results. + $results = json_decode( $response['body'], true ); + + // Bail if there were no resultsreturned. + if ( ! is_array( $results ) ) { + return new \WP_Error( 'unexpected_data_format', 'Data was not returned in the expected format.' ); + } + + // Store the variation in our internal cache. + $this->tests[ $test_name ] = $results['variations'][ $test_name ] ?? null; + + $variation = $results['variations'][ $test_name ] ?? 'control'; + // Store the variation in our external cache. + if ( ! empty( $results['ttl'] ) ) { + set_transient( 'abtest_variation_' . $test_name, $variation, $results['ttl'] ); + } + + return $variation; + } +} + diff --git a/plugins/woocommerce/includes/react-admin/connect-existing-pages.php b/plugins/woocommerce/includes/react-admin/connect-existing-pages.php new file mode 100644 index 00000000000..0949d050e9f --- /dev/null +++ b/plugins/woocommerce/includes/react-admin/connect-existing-pages.php @@ -0,0 +1,296 @@ + $report_data ) { + $report_tabs[ $report_id ] = $report_data['title']; + } + + return array( + 'wc-addons' => array( + 'title' => __( 'Extensions', 'woocommerce' ), + 'tabs' => array(), + ), + 'wc-reports' => array( + 'title' => __( 'Reports', 'woocommerce' ), + 'tabs' => $report_tabs, + ), + 'wc-settings' => array( + 'title' => __( 'Settings', 'woocommerce' ), + 'tabs' => apply_filters( 'woocommerce_settings_tabs_array', array() ), + ), + 'wc-status' => array( + 'title' => __( 'Status', 'woocommerce' ), + 'tabs' => apply_filters( + 'woocommerce_admin_status_tabs', + array( + 'status' => __( 'System status', 'woocommerce' ), + 'tools' => __( 'Tools', 'woocommerce' ), + 'logs' => __( 'Logs', 'woocommerce' ), + ) + ), + ), + ); +} + +/** + * Filter breadcrumbs for core pages that aren't explicitly connected. + * + * @param array $breadcrumbs Breadcrumb pieces. + * @return array Filtered breadcrumb pieces. + */ +function wc_admin_filter_core_page_breadcrumbs( $breadcrumbs ) { + $screen_id = PageController::get_instance()->get_current_screen_id(); + $pages_to_connect = wc_admin_get_core_pages_to_connect(); + $woocommerce_breadcrumb = array( + 'admin.php?page=wc-admin', + __( 'WooCommerce', 'woocommerce' ), + ); + + foreach ( $pages_to_connect as $page_id => $page_data ) { + if ( preg_match( "/^woocommerce_page_{$page_id}\-/", $screen_id ) ) { + if ( empty( $page_data['tabs'] ) ) { + $new_breadcrumbs = array( + $woocommerce_breadcrumb, + $page_data['title'], + ); + } else { + $new_breadcrumbs = array( + $woocommerce_breadcrumb, + array( + add_query_arg( 'page', $page_id, 'admin.php' ), + $page_data['title'], + ), + ); + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( isset( $_GET['tab'] ) ) { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $current_tab = wc_clean( wp_unslash( $_GET['tab'] ) ); + } else { + $current_tab = key( $page_data['tabs'] ); + } + + $new_breadcrumbs[] = $page_data['tabs'][ $current_tab ]; + } + + return $new_breadcrumbs; + } + } + + return $breadcrumbs; +} + +/** + * Render the WC-Admin header bar on all WooCommerce core pages. + * + * @param bool $is_connected Whether the current page is connected. + * @param bool $current_page The current page, if connected. + * @return bool Whether to connect the page. + */ +function wc_admin_connect_core_pages( $is_connected, $current_page ) { + if ( false === $is_connected && false === $current_page ) { + $screen_id = PageController::get_instance()->get_current_screen_id(); + $pages_to_connect = wc_admin_get_core_pages_to_connect(); + + foreach ( $pages_to_connect as $page_id => $page_data ) { + if ( preg_match( "/^woocommerce_page_{$page_id}\-/", $screen_id ) ) { + add_filter( 'woocommerce_navigation_get_breadcrumbs', 'wc_admin_filter_core_page_breadcrumbs' ); + + return true; + } + } + } + + return $is_connected; +} + +add_filter( 'woocommerce_navigation_is_connected_page', 'wc_admin_connect_core_pages', 10, 2 ); + +$posttype_list_base = 'edit.php'; + +// WooCommerce > Orders. +wc_admin_connect_page( + array( + 'id' => 'woocommerce-orders', + 'screen_id' => 'edit-shop_order', + 'title' => __( 'Orders', 'woocommerce' ), + 'path' => add_query_arg( 'post_type', 'shop_order', $posttype_list_base ), + ) +); + +// WooCommerce > Orders > Add New. +wc_admin_connect_page( + array( + 'id' => 'woocommerce-add-order', + 'parent' => 'woocommerce-orders', + 'screen_id' => 'shop_order-add', + 'title' => __( 'Add New', 'woocommerce' ), + ) +); + +// WooCommerce > Orders > Edit Order. +wc_admin_connect_page( + array( + 'id' => 'woocommerce-edit-order', + 'parent' => 'woocommerce-orders', + 'screen_id' => 'shop_order', + 'title' => __( 'Edit Order', 'woocommerce' ), + ) +); + +// WooCommerce > Coupons. +wc_admin_connect_page( + array( + 'id' => 'woocommerce-coupons', + 'parent' => Features::is_enabled( 'coupons' ) ? 'woocommerce-marketing' : null, + 'screen_id' => 'edit-shop_coupon', + 'title' => __( 'Coupons', 'woocommerce' ), + 'path' => add_query_arg( 'post_type', 'shop_coupon', $posttype_list_base ), + ) +); + +// WooCommerce > Coupons > Add New. +wc_admin_connect_page( + array( + 'id' => 'woocommerce-add-coupon', + 'parent' => 'woocommerce-coupons', + 'screen_id' => 'shop_coupon-add', + 'title' => __( 'Add New', 'woocommerce' ), + ) +); + +// WooCommerce > Coupons > Edit Coupon. +wc_admin_connect_page( + array( + 'id' => 'woocommerce-edit-coupon', + 'parent' => 'woocommerce-coupons', + 'screen_id' => 'shop_coupon', + 'title' => __( 'Edit Coupon', 'woocommerce' ), + ) +); + +// WooCommerce > Products. +wc_admin_connect_page( + array( + 'id' => 'woocommerce-products', + 'screen_id' => 'edit-product', + 'title' => __( 'Products', 'woocommerce' ), + 'path' => add_query_arg( 'post_type', 'product', $posttype_list_base ), + ) +); + +// WooCommerce > Products > Add New. +wc_admin_connect_page( + array( + 'id' => 'woocommerce-add-product', + 'parent' => 'woocommerce-products', + 'screen_id' => 'product-add', + 'title' => __( 'Add New', 'woocommerce' ), + ) +); + +// WooCommerce > Products > Edit Order. +wc_admin_connect_page( + array( + 'id' => 'woocommerce-edit-product', + 'parent' => 'woocommerce-products', + 'screen_id' => 'product', + 'title' => __( 'Edit Product', 'woocommerce' ), + ) +); + +// WooCommerce > Products > Import Products. +wc_admin_connect_page( + array( + 'id' => 'woocommerce-import-products', + 'parent' => 'woocommerce-products', + 'screen_id' => 'product_page_product_importer', + 'title' => __( 'Import Products', 'woocommerce' ), + ) +); + +// WooCommerce > Products > Export Products. +wc_admin_connect_page( + array( + 'id' => 'woocommerce-export-products', + 'parent' => 'woocommerce-products', + 'screen_id' => 'product_page_product_exporter', + 'title' => __( 'Export Products', 'woocommerce' ), + ) +); + +// WooCommerce > Products > Product categories. +wc_admin_connect_page( + array( + 'id' => 'woocommerce-product-categories', + 'parent' => 'woocommerce-products', + 'screen_id' => 'edit-product_cat', + 'title' => __( 'Product categories', 'woocommerce' ), + ) +); + +// WooCommerce > Products > Edit category. +wc_admin_connect_page( + array( + 'id' => 'woocommerce-product-edit-category', + 'parent' => 'woocommerce-products', + 'screen_id' => 'product_cat', + 'title' => __( 'Edit category', 'woocommerce' ), + ) +); + +// WooCommerce > Products > Product tags. +wc_admin_connect_page( + array( + 'id' => 'woocommerce-product-tags', + 'parent' => 'woocommerce-products', + 'screen_id' => 'edit-product_tag', + 'title' => __( 'Product tags', 'woocommerce' ), + ) +); + +// WooCommerce > Products > Edit tag. +wc_admin_connect_page( + array( + 'id' => 'woocommerce-product-edit-tag', + 'parent' => 'woocommerce-products', + 'screen_id' => 'product_tag', + 'title' => __( 'Edit tag', 'woocommerce' ), + ) +); + +// WooCommerce > Products > Attributes. +wc_admin_connect_page( + array( + 'id' => 'woocommerce-product-attributes', + 'parent' => 'woocommerce-products', + 'screen_id' => 'product_page_product_attributes', + 'title' => __( 'Attributes', 'woocommerce' ), + ) +); + +// WooCommerce > Products > Edit attribute. +wc_admin_connect_page( + array( + 'id' => 'woocommerce-product-edit-attribute', + 'parent' => 'woocommerce-products', + 'screen_id' => 'product_page_product_attribute-edit', + 'title' => __( 'Edit attribute', 'woocommerce' ), + ) +); diff --git a/plugins/woocommerce/includes/react-admin/core-functions.php b/plugins/woocommerce/includes/react-admin/core-functions.php new file mode 100644 index 00000000000..9c40a1a9bd3 --- /dev/null +++ b/plugins/woocommerce/includes/react-admin/core-functions.php @@ -0,0 +1,72 @@ + + + + + + + +
    + +
    + + + array( + 'href' => array(), + 'title' => array(), + ), + 'br' => array(), + 'em' => array(), + 'strong' => array(), + ) +); + +$base_color = get_option( 'woocommerce_email_base_color' ); +$base_text = wc_light_or_dark( $base_color, '#202020', '#ffffff' ); +$container_styles = 'margin-top: 25px;'; +$buttons_styles = " + font-style: normal; + font-weight: normal; + font-size: 13px; + line-height: 18px; + text-align: center; + color: {$base_text}; + margin-right: 15px; + text-decoration: none; + background: {$base_color}; + border: 1px solid {$base_color}; + border-radius: 3px; + padding: 6px 15px;"; +?> + +
    + +
    +label, $trigger_note_action_url . $an_action->id ) ); +} +echo "\n\n----------------------------------------\n\n"; + +echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) ); diff --git a/plugins/woocommerce/includes/react-admin/page-controller-functions.php b/plugins/woocommerce/includes/react-admin/page-controller-functions.php new file mode 100644 index 00000000000..92b5f3aea3c --- /dev/null +++ b/plugins/woocommerce/includes/react-admin/page-controller-functions.php @@ -0,0 +1,63 @@ +connect_page( $options ); +} + +/** + * Register JS-powered WooCommerce Admin Page. + * Passthrough to PageController::register_page(). + * + * @param array $options Options for PageController::register_page(). + */ +function wc_admin_register_page( $options ) { + $controller = PageController::get_instance(); + $controller->register_page( $options ); +} + +/** + * Is this page connected to WooCommerce Admin? + * Passthrough to PageController::is_connected_page(). + * + * @return boolean True if the page is connected to WooCommerce Admin. + */ +function wc_admin_is_connected_page() { + $controller = PageController::get_instance(); + return $controller->is_connected_page(); +} + +/** + * Is this a WooCommerce Admin Page? + * Passthrough to PageController::is_registered_page(). + * + * @return boolean True if the page is a WooCommerce Admin page. + */ +function wc_admin_is_registered_page() { + $controller = PageController::get_instance(); + return $controller->is_registered_page(); +} + +/** + * Get breadcrumbs for WooCommerce Admin Page navigation. + * Passthrough to PageController::get_breadcrumbs(). + * + * @return array Navigation pieces (breadcrumbs). + */ +function wc_admin_get_breadcrumbs() { + $controller = PageController::get_instance(); + return $controller->get_breadcrumbs(); +} diff --git a/plugins/woocommerce/includes/react-admin/wc-admin-update-functions.php b/plugins/woocommerce/includes/react-admin/wc-admin-update-functions.php new file mode 100644 index 00000000000..c9f2a689192 --- /dev/null +++ b/plugins/woocommerce/includes/react-admin/wc-admin-update-functions.php @@ -0,0 +1,278 @@ +get_row( "SHOW INDEX FROM {$wpdb->prefix}wc_order_stats WHERE key_name = 'status'" ); + + if ( property_exists( $index, 'Sub_part' ) ) { + // The index was created with the right length. Time to bail. + if ( $max_index_length === $index->Sub_part ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName + return; + } + + // We need to drop the index so it can be recreated. + $wpdb->query( "DROP INDEX `status` ON {$wpdb->prefix}wc_order_stats" ); + } + + // Recreate the status index with a max length. + $wpdb->query( $wpdb->prepare( "ALTER TABLE {$wpdb->prefix}wc_order_stats ADD INDEX status (status(%d))", $max_index_length ) ); +} + +/** + * Rename "gross_total" to "total_sales". + * See: https://github.com/woocommerce/woocommerce-admin/issues/3175 + */ +function wc_admin_update_0230_rename_gross_total() { + global $wpdb; + + // We first need to drop the new `total_sales` column, since dbDelta() will have created it. + $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_order_stats DROP COLUMN `total_sales`" ); + // Then we can rename the existing `gross_total` column. + $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_order_stats CHANGE COLUMN `gross_total` `total_sales` double DEFAULT 0 NOT NULL" ); +} + +/** + * Remove the note unsnoozing scheduled action. + */ +function wc_admin_update_0251_remove_unsnooze_action() { + as_unschedule_action( Notes::UNSNOOZE_HOOK, null, 'wc-admin-data' ); + as_unschedule_action( Notes::UNSNOOZE_HOOK, null, 'wc-admin-notes' ); +} + +/** + * Remove Facebook Extension note. + */ +function wc_admin_update_110_remove_facebook_note() { + Notes::delete_notes_with_name( 'wc-admin-facebook-extension' ); +} + +/** + * Remove Dismiss action from tracking opt-in admin note. + */ +function wc_admin_update_130_remove_dismiss_action_from_tracking_opt_in_note() { + global $wpdb; + + $wpdb->query( "DELETE actions FROM {$wpdb->prefix}wc_admin_note_actions actions INNER JOIN {$wpdb->prefix}wc_admin_notes notes USING (note_id) WHERE actions.name = 'tracking-dismiss' AND notes.name = 'wc-admin-usage-tracking-opt-in'" ); +} + +/** + + * Update DB Version. + */ +function wc_admin_update_130_db_version() { + Installer::update_db_version( '1.3.0' ); +} + +/** + * Update DB Version. + */ +function wc_admin_update_140_db_version() { + Installer::update_db_version( '1.4.0' ); +} + +/** + * Remove Facebook Experts note. + */ +function wc_admin_update_160_remove_facebook_note() { + Notes::delete_notes_with_name( 'wc-admin-facebook-marketing-expert' ); +} + +/** + * Set "two column" homescreen layout as default for existing stores. + */ +function wc_admin_update_170_homescreen_layout() { + add_option( 'woocommerce_default_homepage_layout', 'two_columns', '', 'no' ); +} + +/** + * Delete the preexisting export files. + */ +function wc_admin_update_270_delete_report_downloads() { + $upload_dir = wp_upload_dir(); + $base_dir = trailingslashit( $upload_dir['basedir'] ); + + $failed_files = array(); + $exports_status = get_option( ReportExporter::EXPORT_STATUS_OPTION, array() ); + $has_failure = false; + + if ( ! is_array( $exports_status ) ) { + // This is essentially the same path as files failing deletion. Handle as such. + return; + } + + // Delete all export files based on the status option values. + foreach ( $exports_status as $key => $progress ) { + list( $report_type, $export_id ) = explode( ':', $key ); + + if ( ! $export_id ) { + continue; + } + + $file = "{$base_dir}wc-{$report_type}-report-export-{$export_id}.csv"; + $header = $file . '.headers'; + + // phpcs:ignore + if ( @file_exists( $file ) && false === @unlink( $file ) ) { + array_push( $failed_files, $file ); + } + + // phpcs:ignore + if ( @file_exists( $header ) && false === @unlink( $header ) ) { + array_push( $failed_files, $header ); + } + } + + // If the status option was missing or corrupt, there will be files left over. + $potential_exports = glob( $base_dir . 'wc-*-report-export-*.csv' ); + $reports_pattern = '(revenue|products|variations|orders|categories|coupons|taxes|stock|customers|downloads)'; + + /** + * Look for files we can be reasonably sure were created by the report export. + * + * Export files we created will match the 'wc-*-report-export-*.csv' glob, with + * the first wildcard being one of the exportable report slugs, and the second + * being an integer with 11-14 digits (from microtime()'s output) that represents + * a time in the past. + */ + foreach ( $potential_exports as $potential_export ) { + $matches = array(); + // See if the filename matches an unfiltered export pattern. + if ( ! preg_match( "/wc-{$reports_pattern}-report-export-(?P\d{11,14})\.csv\$/", $potential_export, $matches ) ) { + $has_failure = true; + continue; + } + + // Validate the timestamp (anything in the past). + $timestamp = (int) substr( $matches['export_id'], 0, 10 ); + + if ( ! $timestamp || $timestamp > time() ) { + $has_failure = true; + continue; + } + + // phpcs:ignore + if ( false === @unlink( $potential_export ) ) { + array_push( $failed_files, $potential_export ); + } + } + + // Try deleting failed files once more. + foreach ( $failed_files as $failed_file ) { + // phpcs:ignore + if ( false === @unlink( $failed_file ) ) { + $has_failure = true; + } + } + + if ( $has_failure ) { + UnsecuredReportFiles::possibly_add_note(); + } +} + +/** + * Update the old task list options. + */ +function wc_admin_update_271_update_task_list_options() { + $hidden_lists = get_option( 'woocommerce_task_list_hidden_lists', array() ); + $setup_list_hidden = get_option( 'woocommerce_task_list_hidden', 'no' ); + $extended_list_hidden = get_option( 'woocommerce_extended_task_list_hidden', 'no' ); + if ( 'yes' === $setup_list_hidden ) { + $hidden_lists[] = 'setup'; + } + if ( 'yes' === $extended_list_hidden ) { + $hidden_lists[] = 'extended'; + } + + update_option( 'woocommerce_task_list_hidden_lists', array_unique( $hidden_lists ) ); + delete_option( 'woocommerce_task_list_hidden' ); + delete_option( 'woocommerce_extended_task_list_hidden' ); +} + +/** + * Update order stats `status`. + */ +function wc_admin_update_280_order_status() { + global $wpdb; + + $wpdb->query( + "UPDATE {$wpdb->prefix}wc_order_stats refunds + INNER JOIN {$wpdb->prefix}wc_order_stats orders + ON orders.order_id = refunds.parent_id + SET refunds.status = orders.status + WHERE refunds.parent_id != 0" + ); +} + +/** + * Update the old task list options. + */ +function wc_admin_update_290_update_apperance_task_option() { + $is_actioned = get_option( 'woocommerce_task_list_appearance_complete', false ); + + $task = TaskLists::get_task( 'appearance' ); + if ( $task && $is_actioned ) { + $task->mark_actioned(); + } + + delete_option( 'woocommerce_task_list_appearance_complete' ); +} + +/** + * Delete the old woocommerce_default_homepage_layout option. + */ +function wc_admin_update_290_delete_default_homepage_layout_option() { + delete_option( 'woocommerce_default_homepage_layout' ); +} + +/** + * Use woocommerce_admin_activity_panel_inbox_last_read from the user meta to set wc_admin_notes.is_read col. + */ +function wc_admin_update_300_update_is_read_from_last_read() { + global $wpdb; + $meta_key = 'woocommerce_admin_activity_panel_inbox_last_read'; + // phpcs:ignore + $users = get_users( "meta_key={$meta_key}&orderby={$meta_key}&fields=all_with_meta&number=1" ); + + if ( count( $users ) ) { + $last_read = current( $users )->{$meta_key}; + $date_in_utc = gmdate( 'Y-m-d H:i:s', intval( $last_read ) / 1000 ); + $wpdb->query( + $wpdb->prepare( + " + update {$wpdb->prefix}wc_admin_notes set is_read = 1 + where + date_created <= %s", + $date_in_utc + ) + ); + $wpdb->query( $wpdb->prepare( "delete from {$wpdb->usermeta} where meta_key=%s", $meta_key ) ); + } +} + +/** + * Delete "is_primary" column from the wc_admin_notes table. + */ +function wc_admin_update_340_remove_is_primary_from_note_action() { + global $wpdb; + $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_admin_note_actions DROP COLUMN `is_primary`" ); +} diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Telemetry/class-wc-rest-telemetry-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Telemetry/class-wc-rest-telemetry-controller.php new file mode 100644 index 00000000000..7a4893bcd36 --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Telemetry/class-wc-rest-telemetry-controller.php @@ -0,0 +1,142 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'record_usage_data' ), + 'permission_callback' => array( $this, 'telemetry_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to post telemetry data + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function telemetry_permissions_check( $request ) { + if ( ! is_user_logged_in() ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you post telemetry data.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Record WCTracker Data + * + * @param WP_REST_Request $request Full details about the request. + */ + public function record_usage_data( $request ) { + $new = $this->get_usage_data( $request ); + if ( ! $new || ! $new['platform'] ) { + return; + } + + $data = get_option( 'woocommerce_mobile_app_usage' ); + if ( ! $data ) { + $data = array(); + } + + $platform = $new['platform']; + if ( ! $data[ $platform ] || version_compare( $new['version'], $data[ $platform ]['version'], '>=' ) ) { + $data[ $platform ] = $new; + } + + update_option( 'woocommerce_mobile_app_usage', $data ); + } + + /** + * Get usage data from current request + * + * @param WP_REST_Request $request Full details about the request. + * @return Array + */ + public function get_usage_data( $request ) { + $platform = strtolower( $request->get_param( 'platform' ) ); + switch ( $platform ) { + case 'ios': + case 'android': + break; + default: + return; + } + + $version = $request->get_param( 'version' ); + if ( ! $version ) { + return; + } + + return array( + 'platform' => sanitize_text_field( $platform ), + 'version' => sanitize_text_field( $version ), + 'last_used' => gmdate( 'c' ), + ); + } + + /** + * Get any query params needed. + * + * @return array + */ + public function get_collection_params() { + return array( + 'platform' => array( + 'description' => __( 'Platform to track.', 'woocommerce' ), + 'required' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ), + 'version' => array( + 'description' => __( 'Platform version to track.', 'woocommerce' ), + 'required' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ), + ); + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-customer-downloads-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-customer-downloads-v1-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version1/class-wc-rest-customer-downloads-v1-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-customer-downloads-v1-controller.php diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php new file mode 100644 index 00000000000..e1e3d8ab0b3 --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php @@ -0,0 +1,924 @@ +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' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'email' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'New user email address.', 'woocommerce' ), + ), + 'username' => array( + 'required' => 'no' === get_option( 'woocommerce_registration_generate_username', 'yes' ), + 'description' => __( 'New user username.', 'woocommerce' ), + 'type' => 'string', + ), + 'password' => array( + 'required' => 'no' === get_option( 'woocommerce_registration_generate_password', 'no' ), + 'description' => __( 'New user password.', 'woocommerce' ), + 'type' => 'string', + ), + ) ), + ), + '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.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 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 ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + 'reassign' => array( + 'default' => 0, + 'type' => 'integer', + 'description' => __( 'ID to reassign posts to.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read customers. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_user_permissions( 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access create customers. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_user_permissions( 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $id = (int) $request['id']; + + if ( ! wc_rest_check_user_permissions( 'read', $id ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access update a customer. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function update_item_permissions_check( $request ) { + $id = (int) $request['id']; + + if ( ! wc_rest_check_user_permissions( 'edit', $id ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access delete a customer. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + $id = (int) $request['id']; + + if ( ! wc_rest_check_user_permissions( 'delete', $id ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access batch create, update and delete items. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function batch_items_permissions_check( $request ) { + if ( ! wc_rest_check_user_permissions( 'batch' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all customers. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $prepared_args = array(); + $prepared_args['exclude'] = $request['exclude']; + $prepared_args['include'] = $request['include']; + $prepared_args['order'] = $request['order']; + $prepared_args['number'] = $request['per_page']; + if ( ! empty( $request['offset'] ) ) { + $prepared_args['offset'] = $request['offset']; + } else { + $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; + } + $orderby_possibles = array( + 'id' => 'ID', + 'include' => 'include', + 'name' => 'display_name', + 'registered_date' => 'registered', + ); + $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; + $prepared_args['search'] = $request['search']; + + if ( ! empty( $prepared_args['search'] ) ) { + $prepared_args['search'] = '*' . $prepared_args['search'] . '*'; + } + + // Filter by email. + if ( ! empty( $request['email'] ) ) { + $prepared_args['search'] = $request['email']; + $prepared_args['search_columns'] = array( 'user_email' ); + } + + // Filter by role. + if ( 'all' !== $request['role'] ) { + $prepared_args['role'] = $request['role']; + } + + /** + * Filter arguments, before passing to WP_User_Query, when querying users via the REST API. + * + * @see https://developer.wordpress.org/reference/classes/wp_user_query/ + * + * @param array $prepared_args Array of arguments for WP_User_Query. + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( 'woocommerce_rest_customer_query', $prepared_args, $request ); + + $query = new WP_User_Query( $prepared_args ); + + $users = array(); + foreach ( $query->results as $user ) { + $data = $this->prepare_item_for_response( $user, $request ); + $users[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $users ); + + // Store pagination values for headers then unset for count query. + $per_page = (int) $prepared_args['number']; + $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + + $prepared_args['fields'] = 'ID'; + + $total_users = $query->get_total(); + if ( $total_users < 1 ) { + // Out-of-bounds, run the query again without LIMIT for total count. + unset( $prepared_args['number'] ); + unset( $prepared_args['offset'] ); + $count_query = new WP_User_Query( $prepared_args ); + $total_users = $count_query->get_total(); + } + $response->header( 'X-WP-Total', (int) $total_users ); + $max_pages = ceil( $total_users / $per_page ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Create a single customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + try { + if ( ! empty( $request['id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_customer_exists', __( 'Cannot create existing resource.', 'woocommerce' ), 400 ); + } + + // Sets the username. + $request['username'] = ! empty( $request['username'] ) ? $request['username'] : ''; + + // Sets the password. + $request['password'] = ! empty( $request['password'] ) ? $request['password'] : ''; + + // Create customer. + $customer = new WC_Customer; + $customer->set_username( $request['username'] ); + $customer->set_password( $request['password'] ); + $customer->set_email( $request['email'] ); + $this->update_customer_meta_fields( $customer, $request ); + $customer->save(); + + if ( ! $customer->get_id() ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_create', __( 'This resource cannot be created.', 'woocommerce' ), 400 ); + } + + $user_data = get_userdata( $customer->get_id() ); + $this->update_additional_fields_for_object( $user_data, $request ); + + /** + * Fires after a customer is created or updated via the REST API. + * + * @param WP_User $user_data Data used to create the customer. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating customer, false when updating customer. + */ + do_action( 'woocommerce_rest_insert_customer', $user_data, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $user_data, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer->get_id() ) ) ); + + return $response; + } catch ( Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a single customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $user_data = get_userdata( $id ); + + if ( empty( $id ) || empty( $user_data->ID ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $customer = $this->prepare_item_for_response( $user_data, $request ); + $response = rest_ensure_response( $customer ); + + return $response; + } + + /** + * Update a single user. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + try { + $id = (int) $request['id']; + $customer = new WC_Customer( $id ); + + if ( ! $customer->get_id() ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), 400 ); + } + + if ( ! empty( $request['email'] ) && email_exists( $request['email'] ) && $request['email'] !== $customer->get_email() ) { + throw new WC_REST_Exception( 'woocommerce_rest_customer_invalid_email', __( 'Email address is invalid.', 'woocommerce' ), 400 ); + } + + if ( ! empty( $request['username'] ) && $request['username'] !== $customer->get_username() ) { + throw new WC_REST_Exception( 'woocommerce_rest_customer_invalid_argument', __( "Username isn't editable.", 'woocommerce' ), 400 ); + } + + // Customer email. + if ( isset( $request['email'] ) ) { + $customer->set_email( sanitize_email( $request['email'] ) ); + } + + // Customer password. + if ( isset( $request['password'] ) ) { + $customer->set_password( $request['password'] ); + } + + $this->update_customer_meta_fields( $customer, $request ); + $customer->save(); + + $user_data = get_userdata( $customer->get_id() ); + $this->update_additional_fields_for_object( $user_data, $request ); + + if ( ! is_user_member_of_blog( $user_data->ID ) ) { + $user_data->add_role( 'customer' ); + } + + /** + * Fires after a customer is created or updated via the REST API. + * + * @param WP_User $customer Data used to create the customer. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating customer, false when updating customer. + */ + do_action( 'woocommerce_rest_insert_customer', $user_data, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $user_data, $request ); + $response = rest_ensure_response( $response ); + return $response; + } catch ( Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a single customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $reassign = isset( $request['reassign'] ) ? absint( $request['reassign'] ) : null; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Customers do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $user_data = get_userdata( $id ); + if ( ! $user_data ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + if ( ! empty( $reassign ) ) { + if ( $reassign === $id || ! get_userdata( $reassign ) ) { + return new WP_Error( 'woocommerce_rest_customer_invalid_reassign', __( 'Invalid resource id for reassignment.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $user_data, $request ); + + /** Include admin customer functions to get access to wp_delete_user() */ + require_once ABSPATH . 'wp-admin/includes/user.php'; + + $customer = new WC_Customer( $id ); + + if ( ! is_null( $reassign ) ) { + $result = $customer->delete_and_reassign( $reassign ); + } else { + $result = $customer->delete(); + } + + if ( ! $result ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a customer is deleted via the REST API. + * + * @param WP_User $user_data User data. + * @param WP_REST_Response $response The response returned from the API. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_customer', $user_data, $response, $request ); + + return $response; + } + + /** + * Prepare a single customer output for response. + * + * @param WP_User $user_data User object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $user_data, $request ) { + $customer = new WC_Customer( $user_data->ID ); + $_data = $customer->get_data(); + $last_order = wc_get_customer_last_order( $customer->get_id() ); + $format_date = array( 'date_created', 'date_modified' ); + + // Format date values. + foreach ( $format_date as $key ) { + $_data[ $key ] = $_data[ $key ] ? wc_rest_prepare_date_response( $_data[ $key ] ) : null; // v1 API used UTC. + } + + $data = array( + 'id' => $_data['id'], + 'date_created' => $_data['date_created'], + 'date_modified' => $_data['date_modified'], + 'email' => $_data['email'], + 'first_name' => $_data['first_name'], + 'last_name' => $_data['last_name'], + 'username' => $_data['username'], + 'last_order' => array( + 'id' => is_object( $last_order ) ? $last_order->get_id() : null, + 'date' => is_object( $last_order ) ? wc_rest_prepare_date_response( $last_order->get_date_created() ) : null, // v1 API used UTC. + ), + 'orders_count' => $customer->get_order_count(), + 'total_spent' => $customer->get_total_spent(), + 'avatar_url' => $customer->get_avatar_url(), + 'billing' => $_data['billing'], + 'shipping' => $_data['shipping'], + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $user_data ) ); + + /** + * Filter customer data returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_User $user_data User object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_customer', $response, $user_data, $request ); + } + + /** + * Update customer meta fields. + * + * @param WC_Customer $customer + * @param WP_REST_Request $request + */ + protected function update_customer_meta_fields( $customer, $request ) { + $schema = $this->get_item_schema(); + + // Customer first name. + if ( isset( $request['first_name'] ) ) { + $customer->set_first_name( wc_clean( $request['first_name'] ) ); + } + + // Customer last name. + if ( isset( $request['last_name'] ) ) { + $customer->set_last_name( wc_clean( $request['last_name'] ) ); + } + + // Customer billing address. + if ( isset( $request['billing'] ) ) { + foreach ( array_keys( $schema['properties']['billing']['properties'] ) as $field ) { + if ( isset( $request['billing'][ $field ] ) && is_callable( array( $customer, "set_billing_{$field}" ) ) ) { + $customer->{"set_billing_{$field}"}( $request['billing'][ $field ] ); + } + } + } + + // Customer shipping address. + if ( isset( $request['shipping'] ) ) { + foreach ( array_keys( $schema['properties']['shipping']['properties'] ) as $field ) { + if ( isset( $request['shipping'][ $field ] ) && is_callable( array( $customer, "set_shipping_{$field}" ) ) ) { + $customer->{"set_shipping_{$field}"}( $request['shipping'][ $field ] ); + } + } + } + } + + /** + * Prepare links for the request. + * + * @param WP_User $customer Customer object. + * @return array Links for the given customer. + */ + protected function prepare_links( $customer ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer->ID ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the Customer's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'customer', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( 'The date the customer was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'email' => array( + 'description' => __( 'The email address for the customer.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'first_name' => array( + 'description' => __( 'Customer first name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'last_name' => array( + 'description' => __( 'Customer last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'username' => array( + 'description' => __( 'Customer login name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_user', + ), + ), + 'password' => array( + 'description' => __( 'Customer password.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'last_order' => array( + 'description' => __( 'Last order data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => array( + 'id' => array( + 'description' => __( 'Last order ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date' => array( + 'description' => __( 'The date of the customer last order, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + 'orders_count' => array( + 'description' => __( 'Quantity of orders made by the customer.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_spent' => array( + 'description' => __( 'Total amount spent.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'avatar_url' => array( + 'description' => __( 'Avatar URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'billing' => array( + 'description' => __( 'List of billing address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Email address.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping' => array( + 'description' => __( 'List of shipping address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get role names. + * + * @return array + */ + protected function get_role_names() { + global $wp_roles; + + return array_keys( $wp_roles->role_names ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['context']['default'] = 'view'; + + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'default' => 'asc', + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'enum' => array( 'asc', 'desc' ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'default' => 'name', + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'enum' => array( + 'id', + 'include', + 'name', + 'registered_date', + ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['email'] = array( + 'description' => __( 'Limit result set to resources with a specific email.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['role'] = array( + 'description' => __( 'Limit result set to resources with a specific role.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'customer', + 'enum' => array_merge( array( 'all' ), $this->get_role_names() ), + 'validate_callback' => 'rest_validate_request_arg', + ); + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-order-notes-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-order-notes-v1-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version1/class-wc-rest-order-notes-v1-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-order-notes-v1-controller.php diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php new file mode 100644 index 00000000000..345935333d2 --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php @@ -0,0 +1,530 @@ +/refunds endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce\RestApi + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Order Refunds controller class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Orders_V1_Controller + */ +class WC_REST_Order_Refunds_V1_Controller extends WC_REST_Orders_V1_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'orders/(?P[\d]+)/refunds'; + + /** + * Post type. + * + * @var string + */ + protected $post_type = 'shop_order_refund'; + + /** + * Order refunds actions. + */ + public function __construct() { + add_filter( "woocommerce_rest_{$this->post_type}_trashable", '__return_false' ); + add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); + } + + /** + * Register the routes for order refunds. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'order_id' => array( + 'description' => __( 'The order ID.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + 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( + 'order_id' => array( + 'description' => __( 'The order ID.', 'woocommerce' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => true, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Prepare a single order refund output for response. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * + * @return WP_Error|WP_REST_Response + */ + public function prepare_item_for_response( $post, $request ) { + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order ) { + return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); + } + + $refund = wc_get_order( $post ); + + if ( ! $refund || $refund->get_parent_id() !== $order->get_id() ) { + return new WP_Error( 'woocommerce_rest_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 404 ); + } + + $dp = is_null( $request['dp'] ) ? wc_get_price_decimals() : absint( $request['dp'] ); + + $data = array( + 'id' => $refund->get_id(), + 'date_created' => wc_rest_prepare_date_response( $refund->get_date_created() ), + 'amount' => wc_format_decimal( $refund->get_amount(), $dp ), + 'reason' => $refund->get_reason(), + 'line_items' => array(), + ); + + // Add line items. + foreach ( $refund->get_items() as $item_id => $item ) { + $product = $item->get_product(); + $product_id = 0; + $variation_id = 0; + $product_sku = null; + + // Check if the product exists. + if ( is_object( $product ) ) { + $product_id = $item->get_product_id(); + $variation_id = $item->get_variation_id(); + $product_sku = $product->get_sku(); + } + + $item_meta = array(); + + $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; + + foreach ( $item->get_all_formatted_meta_data( $hideprefix ) as $meta_key => $formatted_meta ) { + $item_meta[] = array( + 'key' => $formatted_meta->key, + 'label' => $formatted_meta->display_key, + 'value' => wc_clean( $formatted_meta->display_value ), + ); + } + + $line_item = array( + 'id' => $item_id, + 'name' => $item['name'], + 'sku' => $product_sku, + 'product_id' => (int) $product_id, + 'variation_id' => (int) $variation_id, + 'quantity' => wc_stock_amount( $item['qty'] ), + 'tax_class' => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '', + 'price' => wc_format_decimal( $refund->get_item_total( $item, false, false ), $dp ), + 'subtotal' => wc_format_decimal( $refund->get_line_subtotal( $item, false, false ), $dp ), + 'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ), + 'total' => wc_format_decimal( $refund->get_line_total( $item, false, false ), $dp ), + 'total_tax' => wc_format_decimal( $item['line_tax'], $dp ), + 'taxes' => array(), + 'meta' => $item_meta, + ); + + $item_line_taxes = maybe_unserialize( $item['line_tax_data'] ); + if ( isset( $item_line_taxes['total'] ) ) { + $line_tax = array(); + + foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + 'subtotal' => '', + ); + } + + foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ]['subtotal'] = $tax; + } + + $line_item['taxes'] = array_values( $line_tax ); + } + + $data['line_items'][] = $line_item; + } + + $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 ); + + $response->add_links( $this->prepare_links( $refund, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Order_Refund $refund Comment object. + * @param WP_REST_Request $request Request object. + * @return array Links for the given order refund. + */ + protected function prepare_links( $refund, $request ) { + $order_id = $refund->get_parent_id(); + $base = str_replace( '(?P[\d]+)', $order_id, $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $refund->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order_id ) ), + ), + ); + + return $links; + } + + /** + * Query args. + * + * @param array $args Request args. + * @param WP_REST_Request $request Request object. + * @return array + */ + public function query_args( $args, $request ) { + $args['post_status'] = array_keys( wc_get_order_statuses() ); + $args['post_parent__in'] = array( absint( $request['order_id'] ) ); + + return $args; + } + + /** + * Create a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $order_data = get_post( (int) $request['order_id'] ); + + if ( empty( $order_data ) ) { + return new WP_Error( 'woocommerce_rest_invalid_order', __( 'Order is invalid', 'woocommerce' ), 400 ); + } + + if ( 0 > $request['amount'] ) { + return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 ); + } + + // Create the refund. + $refund = wc_create_refund( array( + 'order_id' => $order_data->ID, + 'amount' => $request['amount'], + 'reason' => empty( $request['reason'] ) ? null : $request['reason'], + 'refund_payment' => is_bool( $request['api_refund'] ) ? $request['api_refund'] : true, + 'restock_items' => true, + ) ); + + if ( is_wp_error( $refund ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 ); + } + + if ( ! $refund ) { + return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); + } + + $post = get_post( $refund->get_id() ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); + + return $response; + } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @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 for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the order refund was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'amount' => array( + 'description' => __( 'Refund amount.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'reason' => array( + 'description' => __( 'Reason for refund.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'line_items' => array( + 'description' => __( 'Line items data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Product SKU.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'variation_id' => array( + 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'quantity' => array( + 'description' => __( 'Quantity ordered.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tax_class' => array( + 'description' => __( 'Tax class of product.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'price' => array( + 'description' => __( 'Product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal_tax' => array( + 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'meta' => array( + 'description' => __( 'Line item meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Meta label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['dp'] = array( + 'default' => wc_get_price_decimals(), + 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php new file mode 100644 index 00000000000..f18b51eeb67 --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php @@ -0,0 +1,1679 @@ +post_type}_query", array( $this, 'query_args' ), 10, 2 ); + } + + /** + * Register the routes for orders. + */ + 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.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 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 ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Prepare a single order output for response. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $data + */ + public function prepare_item_for_response( $post, $request ) { + $order = wc_get_order( $post ); + $dp = is_null( $request['dp'] ) ? wc_get_price_decimals() : absint( $request['dp'] ); + + $data = array( + 'id' => $order->get_id(), + 'parent_id' => $order->get_parent_id(), + 'status' => $order->get_status(), + 'order_key' => $order->get_order_key(), + 'number' => $order->get_order_number(), + 'currency' => $order->get_currency(), + 'version' => $order->get_version(), + 'prices_include_tax' => $order->get_prices_include_tax(), + 'date_created' => wc_rest_prepare_date_response( $order->get_date_created() ), // v1 API used UTC. + 'date_modified' => wc_rest_prepare_date_response( $order->get_date_modified() ), // v1 API used UTC. + 'customer_id' => $order->get_customer_id(), + 'discount_total' => wc_format_decimal( $order->get_total_discount(), $dp ), + 'discount_tax' => wc_format_decimal( $order->get_discount_tax(), $dp ), + 'shipping_total' => wc_format_decimal( $order->get_shipping_total(), $dp ), + 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), + 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), + 'total' => wc_format_decimal( $order->get_total(), $dp ), + 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), + 'billing' => array(), + 'shipping' => array(), + 'payment_method' => $order->get_payment_method(), + 'payment_method_title' => $order->get_payment_method_title(), + 'transaction_id' => $order->get_transaction_id(), + 'customer_ip_address' => $order->get_customer_ip_address(), + 'customer_user_agent' => $order->get_customer_user_agent(), + 'created_via' => $order->get_created_via(), + 'customer_note' => $order->get_customer_note(), + 'date_completed' => wc_rest_prepare_date_response( $order->get_date_completed(), false ), // v1 API used local time. + 'date_paid' => wc_rest_prepare_date_response( $order->get_date_paid(), false ), // v1 API used local time. + 'cart_hash' => $order->get_cart_hash(), + 'line_items' => array(), + 'tax_lines' => array(), + 'shipping_lines' => array(), + 'fee_lines' => array(), + 'coupon_lines' => array(), + 'refunds' => array(), + ); + + // Add addresses. + $data['billing'] = $order->get_address( 'billing' ); + $data['shipping'] = $order->get_address( 'shipping' ); + + // Add line items. + foreach ( $order->get_items() as $item_id => $item ) { + $product = $item->get_product(); + $product_id = 0; + $variation_id = 0; + $product_sku = null; + + // Check if the product exists. + if ( is_object( $product ) ) { + $product_id = $item->get_product_id(); + $variation_id = $item->get_variation_id(); + $product_sku = $product->get_sku(); + } + + $item_meta = array(); + + $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; + + foreach ( $item->get_all_formatted_meta_data( $hideprefix ) as $meta_key => $formatted_meta ) { + $item_meta[] = array( + 'key' => $formatted_meta->key, + 'label' => $formatted_meta->display_key, + 'value' => wc_clean( $formatted_meta->display_value ), + ); + } + + $line_item = array( + 'id' => $item_id, + 'name' => $item['name'], + 'sku' => $product_sku, + 'product_id' => (int) $product_id, + 'variation_id' => (int) $variation_id, + 'quantity' => wc_stock_amount( $item['qty'] ), + 'tax_class' => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '', + 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), + 'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ), + 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), + 'total_tax' => wc_format_decimal( $item['line_tax'], $dp ), + 'taxes' => array(), + 'meta' => $item_meta, + ); + + $item_line_taxes = maybe_unserialize( $item['line_tax_data'] ); + if ( isset( $item_line_taxes['total'] ) ) { + $line_tax = array(); + + foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + 'subtotal' => '', + ); + } + + foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ]['subtotal'] = $tax; + } + + $line_item['taxes'] = array_values( $line_tax ); + } + + $data['line_items'][] = $line_item; + } + + // Add taxes. + foreach ( $order->get_items( 'tax' ) as $key => $tax ) { + $tax_line = array( + 'id' => $key, + 'rate_code' => $tax['name'], + 'rate_id' => $tax['rate_id'], + 'label' => isset( $tax['label'] ) ? $tax['label'] : $tax['name'], + 'compound' => (bool) $tax['compound'], + 'tax_total' => wc_format_decimal( $tax['tax_amount'], $dp ), + 'shipping_tax_total' => wc_format_decimal( $tax['shipping_tax_amount'], $dp ), + ); + + $data['tax_lines'][] = $tax_line; + } + + // Add shipping. + foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { + $shipping_line = array( + 'id' => $shipping_item_id, + 'method_title' => $shipping_item['name'], + 'method_id' => $shipping_item['method_id'], + 'total' => wc_format_decimal( $shipping_item['cost'], $dp ), + 'total_tax' => wc_format_decimal( '', $dp ), + 'taxes' => array(), + ); + + $shipping_taxes = $shipping_item->get_taxes(); + + if ( ! empty( $shipping_taxes['total'] ) ) { + $shipping_line['total_tax'] = wc_format_decimal( array_sum( $shipping_taxes['total'] ), $dp ); + + foreach ( $shipping_taxes['total'] as $tax_rate_id => $tax ) { + $shipping_line['taxes'][] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + ); + } + } + + $data['shipping_lines'][] = $shipping_line; + } + + // Add fees. + foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { + $fee_line = array( + 'id' => $fee_item_id, + 'name' => $fee_item['name'], + 'tax_class' => ! empty( $fee_item['tax_class'] ) ? $fee_item['tax_class'] : '', + 'tax_status' => 'taxable', + 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), + 'taxes' => array(), + ); + + $fee_line_taxes = maybe_unserialize( $fee_item['line_tax_data'] ); + if ( isset( $fee_line_taxes['total'] ) ) { + $fee_tax = array(); + + foreach ( $fee_line_taxes['total'] as $tax_rate_id => $tax ) { + $fee_tax[ $tax_rate_id ] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + 'subtotal' => '', + ); + } + + if ( isset( $fee_line_taxes['subtotal'] ) ) { + foreach ( $fee_line_taxes['subtotal'] as $tax_rate_id => $tax ) { + $fee_tax[ $tax_rate_id ]['subtotal'] = $tax; + } + } + + $fee_line['taxes'] = array_values( $fee_tax ); + } + + $data['fee_lines'][] = $fee_line; + } + + // Add coupons. + foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { + $coupon_line = array( + 'id' => $coupon_item_id, + 'code' => $coupon_item['name'], + 'discount' => wc_format_decimal( $coupon_item['discount_amount'], $dp ), + 'discount_tax' => wc_format_decimal( $coupon_item['discount_amount_tax'], $dp ), + ); + + $data['coupon_lines'][] = $coupon_line; + } + + // Add refunds. + foreach ( $order->get_refunds() as $refund ) { + $data['refunds'][] = array( + 'id' => $refund->get_id(), + 'refund' => $refund->get_reason() ? $refund->get_reason() : '', + 'total' => '-' . wc_format_decimal( $refund->get_amount(), $dp ), + ); + } + + $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 ); + + $response->add_links( $this->prepare_links( $order, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Order $order Order object. + * @param WP_REST_Request $request Request object. + * @return array Links for the given order. + */ + protected function prepare_links( $order, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $order->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + if ( 0 !== (int) $order->get_user_id() ) { + $links['customer'] = array( + 'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $order->get_user_id() ) ), + ); + } + if ( 0 !== (int) $order->get_parent_id() ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order->get_parent_id() ) ), + ); + } + return $links; + } + + /** + * Query args. + * + * @param array $args + * @param WP_REST_Request $request + * @return array + */ + public function query_args( $args, $request ) { + global $wpdb; + + // Set post_status. + if ( 'any' !== $request['status'] ) { + $args['post_status'] = 'wc-' . $request['status']; + } else { + $args['post_status'] = 'any'; + } + + if ( isset( $request['customer'] ) ) { + if ( ! empty( $args['meta_query'] ) ) { + $args['meta_query'] = array(); + } + + $args['meta_query'][] = array( + 'key' => '_customer_user', + 'value' => $request['customer'], + 'type' => 'NUMERIC', + ); + } + + // Search by product. + if ( ! empty( $request['product'] ) ) { + $order_ids = $wpdb->get_col( $wpdb->prepare( " + SELECT order_id + FROM {$wpdb->prefix}woocommerce_order_items + WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) + AND order_item_type = 'line_item' + ", $request['product'] ) ); + + // Force WP_Query return empty if don't found any order. + $order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 ); + + $args['post__in'] = $order_ids; + } + + // Search. + if ( ! empty( $args['s'] ) ) { + $order_ids = wc_order_search( $args['s'] ); + + if ( ! empty( $order_ids ) ) { + unset( $args['s'] ); + $args['post__in'] = array_merge( $order_ids, array( 0 ) ); + } + } + + return $args; + } + + /** + * Prepare a single order for create. + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|WC_Order $data Object. + */ + protected function prepare_item_for_database( $request ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $order = new WC_Order( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Handle all writable props + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'billing' : + case 'shipping' : + $this->update_address( $order, $value, $key ); + break; + case 'line_items' : + case 'shipping_lines' : + case 'fee_lines' : + case 'coupon_lines' : + if ( is_array( $value ) ) { + foreach ( $value as $item ) { + if ( is_array( $item ) ) { + if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) { + $order->remove_item( $item['id'] ); + } else { + $this->set_item( $order, $key, $item ); + } + } + } + } + break; + default : + if ( is_callable( array( $order, "set_{$key}" ) ) ) { + $order->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filter the data for the insert. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WC_Order $order The order object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $order, $request ); + } + + /** + * Create base WC Order object. + * @deprecated 3.0.0 + * @param array $data + * @return WC_Order + */ + protected function create_base_order( $data ) { + return wc_create_order( $data ); + } + + /** + * Only return writable props from schema. + * @param array $schema + * @return bool + */ + protected function filter_writable_props( $schema ) { + return empty( $schema['readonly'] ); + } + + /** + * Create order. + * + * @param WP_REST_Request $request Full details about the request. + * @return int|WP_Error + */ + protected function create_order( $request ) { + try { + // Make sure customer exists. + if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id',__( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + // Make sure customer is part of blog. + if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) { + add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' ); + } + + $order = $this->prepare_item_for_database( $request ); + $order->set_created_via( 'rest-api' ); + $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); + $order->calculate_totals(); + $order->save(); + + // Handle set paid. + if ( true === $request['set_paid'] ) { + $order->payment_complete( $request['transaction_id'] ); + } + + return $order->get_id(); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update order. + * + * @param WP_REST_Request $request Full details about the request. + * @return int|WP_Error + */ + protected function update_order( $request ) { + try { + $order = $this->prepare_item_for_database( $request ); + $order->save(); + + // Handle set paid. + if ( $order->needs_payment() && true === $request['set_paid'] ) { + $order->payment_complete( $request['transaction_id'] ); + } + + // If items have changed, recalculate order totals. + if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) { + $order->calculate_totals( true ); + } + + return $order->get_id(); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update address. + * + * @param WC_Order $order + * @param array $posted + * @param string $type + */ + protected function update_address( $order, $posted, $type = 'billing' ) { + foreach ( $posted as $key => $value ) { + if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { + $order->{"set_{$type}_{$key}"}( $value ); + } + } + } + + /** + * Gets the product ID from the SKU or posted ID. + * + * @throws WC_REST_Exception When SKU or ID is not valid. + * @param array $posted Request data. + * @param string $action 'create' to add line item or 'update' to update it. + * @return int + */ + protected function get_product_id( $posted, $action = 'create' ) { + if ( ! empty( $posted['sku'] ) ) { + $product_id = (int) wc_get_product_id_by_sku( $posted['sku'] ); + } elseif ( ! empty( $posted['product_id'] ) && empty( $posted['variation_id'] ) ) { + $product_id = (int) $posted['product_id']; + } elseif ( ! empty( $posted['variation_id'] ) ) { + $product_id = (int) $posted['variation_id']; + } elseif ( 'update' === $action ) { + $product_id = 0; + } else { + throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 ); + } + return $product_id; + } + + /** + * Maybe set an item prop if the value was posted. + * @param WC_Order_Item $item + * @param string $prop + * @param array $posted Request data. + */ + protected function maybe_set_item_prop( $item, $prop, $posted ) { + if ( isset( $posted[ $prop ] ) ) { + $item->{"set_$prop"}( $posted[ $prop ] ); + } + } + + /** + * Maybe set item props if the values were posted. + * @param WC_Order_Item $item + * @param string[] $props + * @param array $posted Request data. + */ + protected function maybe_set_item_props( $item, $props, $posted ) { + foreach ( $props as $prop ) { + $this->maybe_set_item_prop( $item, $prop, $posted ); + } + } + + /** + * Create or update a line item. + * + * @param array $posted Line item data. + * @param string $action 'create' to add line item or 'update' to update it. + * + * @return WC_Order_Item_Product + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_line_items( $posted, $action = 'create' ) { + $item = new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' ); + $product = wc_get_product( $this->get_product_id( $posted, $action ) ); + + if ( $product && $product !== $item->get_product() ) { + $item->set_product( $product ); + + if ( 'create' === $action ) { + $quantity = isset( $posted['quantity'] ) ? $posted['quantity'] : 1; + $total = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) ); + $item->set_total( $total ); + $item->set_subtotal( $total ); + } + } + + $this->maybe_set_item_props( $item, array( 'name', 'quantity', 'total', 'subtotal', 'tax_class' ), $posted ); + + return $item; + } + + /** + * Create or update an order shipping method. + * + * @param $posted $shipping Item data. + * @param string $action 'create' to add shipping or 'update' to update it. + * + * @return WC_Order_Item_Shipping + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_shipping_lines( $posted, $action ) { + $item = new WC_Order_Item_Shipping( ! empty( $posted['id'] ) ? $posted['id'] : '' ); + + if ( 'create' === $action ) { + if ( empty( $posted['method_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'method_id', 'method_title', 'total' ), $posted ); + + return $item; + } + + /** + * Create or update an order fee. + * + * @param array $posted Item data. + * @param string $action 'create' to add fee or 'update' to update it. + * + * @return WC_Order_Item_Fee + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_fee_lines( $posted, $action ) { + $item = new WC_Order_Item_Fee( ! empty( $posted['id'] ) ? $posted['id'] : '' ); + + if ( 'create' === $action ) { + if ( empty( $posted['name'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'name', 'tax_class', 'tax_status', 'total' ), $posted ); + + return $item; + } + + /** + * Create or update an order coupon. + * + * @param array $posted Item data. + * @param string $action 'create' to add coupon or 'update' to update it. + * + * @return WC_Order_Item_Coupon + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_coupon_lines( $posted, $action ) { + $item = new WC_Order_Item_Coupon( ! empty( $posted['id'] ) ? $posted['id'] : '' ); + + if ( 'create' === $action ) { + if ( empty( $posted['code'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'code', 'discount' ), $posted ); + + return $item; + } + + /** + * Wrapper method to create/update order items. + * When updating, the item ID provided is checked to ensure it is associated + * with the order. + * + * @param WC_Order $order order + * @param string $item_type + * @param array $posted item provided in the request body + * @throws WC_REST_Exception If item ID is not associated with order + */ + protected function set_item( $order, $item_type, $posted ) { + global $wpdb; + + if ( ! empty( $posted['id'] ) ) { + $action = 'update'; + } else { + $action = 'create'; + } + + $method = 'prepare_' . $item_type; + + // Verify provided line item ID is associated with order. + if ( 'update' === $action ) { + $result = $wpdb->get_row( + $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", + absint( $posted['id'] ), + absint( $order->get_id() ) + ) ); + if ( is_null( $result ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); + } + } + + // Prepare item data + $item = $this->$method( $posted, $action ); + + /** + * Action hook to adjust item before save. + * @since 3.0.0 + */ + do_action( 'woocommerce_rest_set_order_item', $item, $posted ); + + // Save or add to order + if ( 'create' === $action ) { + $order->add_item( $item ); + } else { + $item->save(); + } + } + + /** + * Helper method to check if the resource ID associated with the provided item is null. + * Items can be deleted by setting the resource ID to null. + * + * @param array $item Item provided in the request body. + * @return bool True if the item resource ID is null, false otherwise. + */ + protected function item_is_null( $item ) { + $keys = array( 'product_id', 'method_id', 'method_title', 'name', 'code' ); + + foreach ( $keys as $key ) { + if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { + return true; + } + } + + return false; + } + + /** + * Create a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $order_id = $this->create_order( $request ); + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $post = get_post( $order_id ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); + + return $response; + } + + /** + * Update a single order. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + try { + $post_id = (int) $request['id']; + + if ( empty( $post_id ) || get_post_type( $post_id ) !== $this->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $order_id = $this->update_order( $request ); + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $post = get_post( $order_id ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false ); + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + return rest_ensure_response( $response ); + + } catch ( Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get order statuses without prefixes. + * @return array + */ + protected function get_order_statuses() { + $order_statuses = array(); + + foreach ( array_keys( wc_get_order_statuses() ) as $status ) { + $order_statuses[] = str_replace( 'wc-', '', $status ); + } + + return $order_statuses; + } + + /** + * Check if a given request has access to read an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $object = wc_get_order( (int) $request['id'] ); + + if ( ( ! $object || 0 === $object->get_id() ) && ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return parent::get_item_permissions_check( $request ); + } + + /** + * Check if a given request has access to update an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + $object = wc_get_order( (int) $request['id'] ); + + if ( ( ! $object || 0 === $object->get_id() ) && ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return parent::update_item_permissions_check( $request ); + } + + /** + * Check if a given request has access to delete an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + $object = wc_get_order( (int) $request['id'] ); + + if ( ( ! $object || 0 === $object->get_id() ) && ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return parent::delete_item_permissions_check( $request ); + } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @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 for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'parent_id' => array( + 'description' => __( 'Parent order ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Order status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'pending', + 'enum' => $this->get_order_statuses(), + 'context' => array( 'view', 'edit' ), + ), + 'order_key' => array( + 'description' => __( 'Order key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'number' => array( + 'description' => __( 'Order number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'currency' => array( + 'description' => __( 'Currency the order was created with, in ISO format.', 'woocommerce' ), + 'type' => 'string', + 'default' => get_woocommerce_currency(), + 'enum' => array_keys( get_woocommerce_currencies() ), + 'context' => array( 'view', 'edit' ), + ), + 'version' => array( + 'description' => __( 'Version of WooCommerce which last updated the order.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'prices_include_tax' => array( + 'description' => __( 'True the prices included tax during checkout.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the order was created, as GMT.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the order was last modified, as GMT.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_id' => array( + 'description' => __( 'User ID who owns the order. 0 for guests.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 0, + 'context' => array( 'view', 'edit' ), + ), + 'discount_total' => array( + 'description' => __( 'Total discount amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'discount_tax' => array( + 'description' => __( 'Total discount tax amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_total' => array( + 'description' => __( 'Total shipping amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_tax' => array( + 'description' => __( 'Total shipping tax amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'cart_tax' => array( + 'description' => __( 'Sum of line item taxes only.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Grand total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_tax' => array( + 'description' => __( 'Sum of all taxes.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'billing' => array( + 'description' => __( 'Billing address.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Email address.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping' => array( + 'description' => __( 'Shipping address.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'payment_method' => array( + 'description' => __( 'Payment method ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'payment_method_title' => array( + 'description' => __( 'Payment method title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'set_paid' => array( + 'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'edit' ), + ), + 'transaction_id' => array( + 'description' => __( 'Unique transaction ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'customer_ip_address' => array( + 'description' => __( "Customer's IP address.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_user_agent' => array( + 'description' => __( 'User agent of the customer.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'created_via' => array( + 'description' => __( 'Shows where the order was created.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_note' => array( + 'description' => __( 'Note left by customer during checkout.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_completed' => array( + 'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_paid' => array( + 'description' => __( "The date the order was paid, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'cart_hash' => array( + 'description' => __( 'MD5 hash of cart items to ensure orders are not modified.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'line_items' => array( + 'description' => __( 'Line items data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Product SKU.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'variation_id' => array( + 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'quantity' => array( + 'description' => __( 'Quantity ordered.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of product.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'price' => array( + 'description' => __( 'Product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal_tax' => array( + 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'meta' => array( + 'description' => __( 'Line item meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Meta label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ), + ), + 'tax_lines' => array( + 'description' => __( 'Tax lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rate_code' => array( + 'description' => __( 'Tax rate code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rate_id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Tax rate label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'compound' => array( + 'description' => __( 'Show if is a compound tax rate.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tax_total' => array( + 'description' => __( 'Tax total (not including shipping taxes).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_tax_total' => array( + 'description' => __( 'Shipping tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'shipping_lines' => array( + 'description' => __( 'Shipping lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'method_title' => array( + 'description' => __( 'Shipping method name.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'method_id' => array( + 'description' => __( 'Shipping method ID.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ), + ), + 'fee_lines' => array( + 'description' => __( 'Fee lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Fee name.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of fee.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status of fee.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'enum' => array( 'taxable', 'none' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ), + ), + 'coupon_lines' => array( + 'description' => __( 'Coupons line data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'discount' => array( + 'description' => __( 'Discount total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'discount_tax' => array( + 'description' => __( 'Discount total tax.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'refunds' => array( + 'description' => __( 'List of refunds.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Refund ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'reason' => array( + 'description' => __( 'Refund reason.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Refund total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to orders assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'any' ), $this->get_order_statuses() ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['customer'] = array( + 'description' => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['product'] = array( + 'description' => __( 'Limit result set to orders assigned a specific product.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['dp'] = array( + 'default' => wc_get_price_decimals(), + 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-product-categories-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-categories-v1-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version1/class-wc-rest-product-categories-v1-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-categories-v1-controller.php diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php new file mode 100644 index 00000000000..210efaa79b7 --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php @@ -0,0 +1,581 @@ +/reviews. + * + * @author WooThemes + * @category API + * @package WooCommerce\RestApi + * @since 3.0.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Product Reviews Controller Class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Controller + */ +class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'products/(?P[\d]+)/reviews'; + + /** + * Register the routes for product reviews. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'product_id' => array( + 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the variation.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + 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' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'review' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Review content.', 'woocommerce' ), + ), + 'name' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Name of the reviewer.', 'woocommerce' ), + ), + 'email' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Email of the reviewer.', 'woocommerce' ), + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'product_id' => array( + 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 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 ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read webhook deliveries. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_product_reviews_permissions( 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! wc_rest_check_product_reviews_permissions( 'read', (int) $request['id'] ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to create a new product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_product_reviews_permissions( 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to update a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + if ( ! wc_rest_check_product_reviews_permissions( 'edit', (int) $request['id'] ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to delete a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function delete_item_permissions_check( $request ) { + if ( ! wc_rest_check_product_reviews_permissions( 'delete', (int) $request['id'] ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all reviews from a product. + * + * @param WP_REST_Request $request + * + * @return array|WP_Error + */ + public function get_items( $request ) { + $product_id = (int) $request['product_id']; + + if ( 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $reviews = get_approved_comments( $product_id ); + $data = array(); + foreach ( $reviews as $review_data ) { + $review = $this->prepare_item_for_response( $review_data, $request ); + $review = $this->prepare_response_for_collection( $review ); + $data[] = $review; + } + + return rest_ensure_response( $data ); + } + + /** + * Get a single product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $product_id = (int) $request['product_id']; + + if ( 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $review = get_comment( $id ); + + if ( empty( $id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $delivery = $this->prepare_item_for_response( $review, $request ); + $response = rest_ensure_response( $delivery ); + + return $response; + } + + + /** + * Create a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + $product_id = (int) $request['product_id']; + + if ( 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $prepared_review = $this->prepare_item_for_database( $request ); + + /** + * Filter a product review (comment) before it is inserted via the REST API. + * + * Allows modification of the comment right before it is inserted via `wp_insert_comment`. + * + * @param array $prepared_review The prepared comment data for `wp_insert_comment`. + * @param WP_REST_Request $request Request used to insert the comment. + */ + $prepared_review = apply_filters( 'rest_pre_insert_product_review', $prepared_review, $request ); + + $product_review_id = wp_insert_comment( $prepared_review ); + if ( ! $product_review_id ) { + return new WP_Error( 'rest_product_review_failed_create', __( 'Creating product review failed.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + update_comment_meta( $product_review_id, 'rating', ( ! empty( $request['rating'] ) ? $request['rating'] : '0' ) ); + + $product_review = get_comment( $product_review_id ); + $this->update_additional_fields_for_object( $product_review, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Comment $product_review Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_product_review", $product_review, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $product_review, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $base = str_replace( '(?P[\d]+)', $product_id, $this->rest_base ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $product_review_id ) ) ); + + return $response; + } + + /** + * Update a single product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $product_review_id = (int) $request['id']; + $product_id = (int) $request['product_id']; + + if ( 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $review = get_comment( $product_review_id ); + + if ( empty( $product_review_id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) { + return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $prepared_review = $this->prepare_item_for_database( $request ); + + $updated = wp_update_comment( $prepared_review ); + if ( 0 === $updated ) { + return new WP_Error( 'rest_product_review_failed_edit', __( 'Updating product review failed.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + if ( ! empty( $request['rating'] ) ) { + update_comment_meta( $product_review_id, 'rating', $request['rating'] ); + } + + $product_review = get_comment( $product_review_id ); + $this->update_additional_fields_for_object( $product_review, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Comment $comment Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_product_review", $product_review, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $product_review, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Delete a product review. + * + * @param WP_REST_Request $request Full details about the request + * + * @return bool|WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + $product_id = (int) $request['product_id']; + $product_review_id = (int) $request['id']; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + if ( 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $product_review = get_comment( $product_review_id ); + if ( empty( $product_review_id ) || empty( $product_review->comment_ID ) || empty( $product_review->comment_post_ID ) ) { + return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid product review ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + /** + * Filter whether a product review is trashable. + * + * Return false to disable trash support for the product review. + * + * @param boolean $supports_trash Whether the object supports trashing. + * @param WP_Post $product_review The object being considered for trashing support. + */ + $supports_trash = apply_filters( 'rest_product_review_trashable', ( EMPTY_TRASH_DAYS > 0 ), $product_review ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $product_review, $request ); + + if ( $force ) { + $result = wp_delete_comment( $product_review_id, true ); + } else { + if ( ! $supports_trash ) { + return new WP_Error( 'rest_trash_not_supported', __( 'The product review does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + if ( 'trash' === $product_review->comment_approved ) { + return new WP_Error( 'rest_already_trashed', __( 'The comment has already been trashed.', 'woocommerce' ), array( 'status' => 410 ) ); + } + + $result = wp_trash_comment( $product_review->comment_ID ); + } + + if ( ! $result ) { + return new WP_Error( 'rest_cannot_delete', __( 'The product review cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a product review is deleted via the REST API. + * + * @param object $product_review The deleted item. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'rest_delete_product_review', $product_review, $response, $request ); + + return $response; + } + + /** + * Prepare a single product review output for response. + * + * @param WP_Comment $review Product review object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $review, $request ) { + $data = array( + 'id' => (int) $review->comment_ID, + 'date_created' => wc_rest_prepare_date_response( $review->comment_date_gmt ), + 'review' => $review->comment_content, + 'rating' => (int) get_comment_meta( $review->comment_ID, 'rating', true ), + 'name' => $review->comment_author, + 'email' => $review->comment_author_email, + 'verified' => wc_review_is_from_verified_owner( $review->comment_ID ), + ); + + $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 ); + + $response->add_links( $this->prepare_links( $review, $request ) ); + + /** + * Filter product reviews object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment $review Product review object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request ); + } + + /** + * Prepare a single product review to be inserted into the database. + * + * @param WP_REST_Request $request Request object. + * @return array|WP_Error $prepared_review + */ + protected function prepare_item_for_database( $request ) { + $prepared_review = array( 'comment_approved' => 1, 'comment_type' => 'review' ); + + if ( isset( $request['id'] ) ) { + $prepared_review['comment_ID'] = (int) $request['id']; + } + + if ( isset( $request['review'] ) ) { + $prepared_review['comment_content'] = $request['review']; + } + + if ( isset( $request['product_id'] ) ) { + $prepared_review['comment_post_ID'] = (int) $request['product_id']; + } + + if ( isset( $request['name'] ) ) { + $prepared_review['comment_author'] = $request['name']; + } + + if ( isset( $request['email'] ) ) { + $prepared_review['comment_author_email'] = $request['email']; + } + + if ( isset( $request['date_created'] ) ) { + $prepared_review['comment_date'] = $request['date_created']; + } + + if ( isset( $request['date_created_gmt'] ) ) { + $prepared_review['comment_date_gmt'] = $request['date_created_gmt']; + } + + return apply_filters( 'rest_preprocess_product_review', $prepared_review, $request ); + } + + /** + * Prepare links for the request. + * + * @param WP_Comment $review Product review object. + * @param WP_REST_Request $request Request object. + * @return array Links for the given product review. + */ + protected function prepare_links( $review, $request ) { + $product_id = (int) $request['product_id']; + $base = str_replace( '(?P[\d]+)', $product_id, $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $review->comment_ID ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product_id ) ), + ), + ); + + return $links; + } + + /** + * Get the Product Review's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'product_review', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'review' => array( + 'description' => __( 'The content of the review.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'rating' => array( + 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Reviewer name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Reviewer email.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'verified' => array( + 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-product-shipping-classes-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-shipping-classes-v1-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version1/class-wc-rest-product-shipping-classes-v1-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-shipping-classes-v1-controller.php diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-product-tags-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-tags-v1-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version1/class-wc-rest-product-tags-v1-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-tags-v1-controller.php diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-report-sales-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-report-sales-v1-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version1/class-wc-rest-report-sales-v1-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-report-sales-v1-controller.php diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-report-top-sellers-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-report-top-sellers-v1-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version1/class-wc-rest-report-top-sellers-v1-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-report-top-sellers-v1-controller.php diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-reports-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-reports-v1-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version1/class-wc-rest-reports-v1-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-reports-v1-controller.php diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-tax-classes-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-tax-classes-v1-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version1/class-wc-rest-tax-classes-v1-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-tax-classes-v1-controller.php diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php new file mode 100644 index 00000000000..2af61aa7a99 --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php @@ -0,0 +1,768 @@ +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.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 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 ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/batch', + array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to read taxes. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access create taxes. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access update a tax. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function update_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access delete a tax. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access batch create, update and delete items. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function batch_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'batch' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all taxes. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + global $wpdb; + + $prepared_args = array(); + $prepared_args['order'] = $request['order']; + $prepared_args['number'] = $request['per_page']; + if ( ! empty( $request['offset'] ) ) { + $prepared_args['offset'] = $request['offset']; + } else { + $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; + } + $orderby_possibles = array( + 'id' => 'tax_rate_id', + 'order' => 'tax_rate_order', + 'priority' => 'tax_rate_priority', + ); + $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; + $prepared_args['class'] = $request['class']; + + /** + * Filter arguments, before passing to $wpdb->get_results(), when querying taxes via the REST API. + * + * @param array $prepared_args Array of arguments for $wpdb->get_results(). + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( 'woocommerce_rest_tax_query', $prepared_args, $request ); + + $orderby = sanitize_key( $prepared_args['orderby'] ) . ' ' . sanitize_key( $prepared_args['order'] ); + $query = " + SELECT * + FROM {$wpdb->prefix}woocommerce_tax_rates + %s + ORDER BY {$orderby} + LIMIT %%d, %%d + "; + + $wpdb_prepare_args = array( + $prepared_args['offset'], + $prepared_args['number'], + ); + + // Filter by tax class. + if ( empty( $prepared_args['class'] ) ) { + $query = sprintf( $query, '' ); + } else { + $class = 'standard' !== $prepared_args['class'] ? sanitize_title( $prepared_args['class'] ) : ''; + array_unshift( $wpdb_prepare_args, $class ); + $query = sprintf( $query, 'WHERE tax_rate_class = %s' ); + } + + // Query taxes. + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + $results = $wpdb->get_results( + $wpdb->prepare( + $query, + $wpdb_prepare_args + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared + + $taxes = array(); + foreach ( $results as $tax ) { + $data = $this->prepare_item_for_response( $tax, $request ); + $taxes[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $taxes ); + + $per_page = (int) $prepared_args['number']; + $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + + // Unset LIMIT args. + array_splice( $wpdb_prepare_args, -2 ); + + // Count query. + $query = str_replace( + array( + 'SELECT *', + 'LIMIT %d, %d', + ), + array( + 'SELECT COUNT(*)', + '', + ), + $query + ); + + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + $total_taxes = (int) $wpdb->get_var( empty( $wpdb_prepare_args ) ? $query : $wpdb->prepare( $query, $wpdb_prepare_args ) ); + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared + + // Calculate totals. + $response->header( 'X-WP-Total', $total_taxes ); + $max_pages = ceil( $total_taxes / $per_page ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Take tax data from the request and return the updated or newly created rate. + * + * @param WP_REST_Request $request Full details about the request. + * @param stdClass|null $current Existing tax object. + * @return object + */ + protected function create_or_update_tax( $request, $current = null ) { + $id = absint( isset( $request['id'] ) ? $request['id'] : 0 ); + $data = array(); + $fields = array( + 'tax_rate_country', + 'tax_rate_state', + 'tax_rate', + 'tax_rate_name', + 'tax_rate_priority', + 'tax_rate_compound', + 'tax_rate_shipping', + 'tax_rate_order', + 'tax_rate_class', + ); + + foreach ( $fields as $field ) { + // Keys via API differ from the stored names returned by _get_tax_rate. + $key = 'tax_rate' === $field ? 'rate' : str_replace( 'tax_rate_', '', $field ); + + // Remove data that was not posted. + if ( ! isset( $request[ $key ] ) ) { + continue; + } + + // Test new data against current data. + if ( $current && $current->$field === $request[ $key ] ) { + continue; + } + + // Add to data array. + switch ( $key ) { + case 'tax_rate_priority': + case 'tax_rate_compound': + case 'tax_rate_shipping': + case 'tax_rate_order': + $data[ $field ] = absint( $request[ $key ] ); + break; + case 'tax_rate_class': + $data[ $field ] = 'standard' !== $request['tax_rate_class'] ? $request['tax_rate_class'] : ''; + break; + default: + $data[ $field ] = wc_clean( $request[ $key ] ); + break; + } + } + + if ( ! $id ) { + $id = WC_Tax::_insert_tax_rate( $data ); + } elseif ( $data ) { + WC_Tax::_update_tax_rate( $id, $data ); + } + + // Add locales. + if ( ! empty( $request['postcode'] ) ) { + WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $request['postcode'] ) ); + } + if ( ! empty( $request['city'] ) ) { + WC_Tax::_update_tax_rate_cities( $id, wc_clean( $request['city'] ) ); + } + + return WC_Tax::_get_tax_rate( $id, OBJECT ); + } + + /** + * Create a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + return new WP_Error( 'woocommerce_rest_tax_exists', __( 'Cannot create existing resource.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $tax = $this->create_or_update_tax( $request ); + + $this->update_additional_fields_for_object( $tax, $request ); + + /** + * Fires after a tax is created or updated via the REST API. + * + * @param stdClass $tax Data used to create the tax. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating tax, false when updating tax. + */ + do_action( 'woocommerce_rest_insert_tax', $tax, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ) ); + + return $response; + } + + /** + * Get a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $tax_obj = WC_Tax::_get_tax_rate( $id, OBJECT ); + + if ( empty( $id ) || empty( $tax_obj ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $tax = $this->prepare_item_for_response( $tax_obj, $request ); + $response = rest_ensure_response( $tax ); + + return $response; + } + + /** + * Update a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $id = (int) $request['id']; + $tax_obj = WC_Tax::_get_tax_rate( $id, OBJECT ); + + if ( empty( $id ) || empty( $tax_obj ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $tax = $this->create_or_update_tax( $request, $tax_obj ); + + $this->update_additional_fields_for_object( $tax, $request ); + + /** + * Fires after a tax is created or updated via the REST API. + * + * @param stdClass $tax Data used to create the tax. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating tax, false when updating tax. + */ + do_action( 'woocommerce_rest_insert_tax', $tax, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax, $request ); + $response = rest_ensure_response( $response ); + + return $response; + } + + /** + * Delete a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + global $wpdb; + + $id = (int) $request['id']; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Taxes do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $tax = WC_Tax::_get_tax_rate( $id, OBJECT ); + + if ( empty( $id ) || empty( $tax ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax, $request ); + + WC_Tax::_delete_tax_rate( $id ); + + if ( 0 === $wpdb->rows_affected ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a tax is deleted via the REST API. + * + * @param stdClass $tax The tax data. + * @param WP_REST_Response $response The response returned from the API. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_tax', $tax, $response, $request ); + + return $response; + } + + /** + * Prepare a single tax output for response. + * + * @param stdClass $tax Tax object. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $tax, $request ) { + $id = (int) $tax->tax_rate_id; + $data = array( + 'id' => $id, + 'country' => $tax->tax_rate_country, + 'state' => $tax->tax_rate_state, + 'postcode' => '', + 'city' => '', + 'rate' => $tax->tax_rate, + 'name' => $tax->tax_rate_name, + 'priority' => (int) $tax->tax_rate_priority, + 'compound' => (bool) $tax->tax_rate_compound, + 'shipping' => (bool) $tax->tax_rate_shipping, + 'order' => (int) $tax->tax_rate_order, + 'class' => $tax->tax_rate_class ? $tax->tax_rate_class : 'standard', + ); + + $data = $this->add_tax_rate_locales( $data, $tax ); + + $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 ); + + $response->add_links( $this->prepare_links( $tax ) ); + + /** + * Filter tax object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $tax Tax object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_tax', $response, $tax, $request ); + } + + /** + * Prepare links for the request. + * + * @param stdClass $tax Tax object. + * @return array Links for the given tax. + */ + protected function prepare_links( $tax ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Add tax rate locales to the response array. + * + * @param array $data Response data. + * @param stdClass $tax Tax object. + * + * @return array + */ + protected function add_tax_rate_locales( $data, $tax ) { + global $wpdb; + + // Get locales from a tax rate. + $locales = $wpdb->get_results( + $wpdb->prepare( + " + SELECT location_code, location_type + FROM {$wpdb->prefix}woocommerce_tax_rate_locations + WHERE tax_rate_id = %d + ", + $tax->tax_rate_id + ) + ); + + if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) { + foreach ( $locales as $locale ) { + $data[ $locale->location_type ] = $locale->location_code; + } + } + + return $data; + } + + /** + * Get the Taxes schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'tax', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'country' => array( + 'description' => __( 'Country ISO 3166 code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'State code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postcode / ZIP.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'rate' => array( + 'description' => __( 'Tax rate.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Tax rate name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'priority' => array( + 'description' => __( 'Tax priority.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 1, + 'context' => array( 'view', 'edit' ), + ), + 'compound' => array( + 'description' => __( 'Whether or not this is a compound rate.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'shipping' => array( + 'description' => __( 'Whether or not this tax rate also gets applied to shipping.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'order' => array( + 'description' => __( 'Indicates the order that will appear in queries.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'standard', + 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = array(); + $params['context'] = $this->get_context_param(); + $params['context']['default'] = 'view'; + + $params['page'] = array( + 'description' => __( 'Current page of the collection.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 1, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + 'minimum' => 1, + ); + $params['per_page'] = array( + 'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'default' => 'asc', + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'enum' => array( 'asc', 'desc' ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'default' => 'order', + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'enum' => array( + 'id', + 'order', + 'priority', + ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['class'] = array( + 'description' => __( 'Sort by tax class.', 'woocommerce' ), + 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), + 'sanitize_callback' => 'sanitize_title', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-webhook-deliveries-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-webhook-deliveries-v1-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version1/class-wc-rest-webhook-deliveries-v1-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-webhook-deliveries-v1-controller.php diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php new file mode 100644 index 00000000000..b96d9875fd0 --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php @@ -0,0 +1,548 @@ +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' => array_merge( + $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'required' => true, + 'type' => 'string', + ), + ) + ), + ), + '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.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 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 ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Get object. + * + * @since 3.0.0 + * @param int $id Object ID. + * @return WC_Data + */ + protected function get_object( $id ) { + return new WC_Coupon( $id ); + } + + /** + * Get formatted item data. + * + * @since 3.0.0 + * @param WC_Data $object WC_Data instance. + * @return array + */ + protected function get_formatted_item_data( $object ) { + $data = $object->get_data(); + + $format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' ); + $format_date = array( 'date_created', 'date_modified', 'date_expires' ); + $format_null = array( 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items' ); + + // Format decimal values. + foreach ( $format_decimal as $key ) { + $data[ $key ] = wc_format_decimal( $data[ $key ], 2 ); + } + + // Format date values. + foreach ( $format_date as $key ) { + $datetime = $data[ $key ]; + $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); + $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); + } + + // Format null values. + foreach ( $format_null as $key ) { + $data[ $key ] = $data[ $key ] ? $data[ $key ] : null; + } + + return array( + 'id' => $object->get_id(), + 'code' => $data['code'], + 'amount' => $data['amount'], + 'status' => $data['status'], + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'date_modified' => $data['date_modified'], + 'date_modified_gmt' => $data['date_modified_gmt'], + 'discount_type' => $data['discount_type'], + 'description' => $data['description'], + 'date_expires' => $data['date_expires'], + 'date_expires_gmt' => $data['date_expires_gmt'], + 'usage_count' => $data['usage_count'], + 'individual_use' => $data['individual_use'], + 'product_ids' => $data['product_ids'], + 'excluded_product_ids' => $data['excluded_product_ids'], + 'usage_limit' => $data['usage_limit'], + 'usage_limit_per_user' => $data['usage_limit_per_user'], + 'limit_usage_to_x_items' => $data['limit_usage_to_x_items'], + 'free_shipping' => $data['free_shipping'], + 'product_categories' => $data['product_categories'], + 'excluded_product_categories' => $data['excluded_product_categories'], + 'exclude_sale_items' => $data['exclude_sale_items'], + 'minimum_amount' => $data['minimum_amount'], + 'maximum_amount' => $data['maximum_amount'], + 'email_restrictions' => $data['email_restrictions'], + 'used_by' => $data['used_by'], + 'meta_data' => $data['meta_data'], + ); + } + + /** + * Prepare a single coupon output for response. + * + * @since 3.0.0 + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { + $data = $this->get_formatted_item_data( $object ); + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $object, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = parent::prepare_objects_query( $request ); + + if ( ! empty( $request['code'] ) ) { + $id = wc_get_coupon_id_by_code( $request['code'] ); + $args['post__in'] = array( $id ); + } + + // Get only ids. + $args['fields'] = 'ids'; + + return $args; + } + + /** + * Only return writable props from schema. + * + * @param array $schema Schema. + * @return bool + */ + protected function filter_writable_props( $schema ) { + return empty( $schema['readonly'] ); + } + + /** + * Prepare a single coupon for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $coupon = new WC_Coupon( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Validate required POST fields. + if ( $creating && empty( $request['code'] ) ) { + return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) ); + } + + // Handle all writable props. + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'code': + $coupon_code = wc_format_coupon_code( $value ); + $id = $coupon->get_id() ? $coupon->get_id() : 0; + $id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id ); + + if ( $id_from_code ) { + return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $coupon->set_code( $coupon_code ); + break; + case 'meta_data': + if ( is_array( $value ) ) { + foreach ( $value as $meta ) { + $coupon->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + break; + case 'description': + $coupon->set_description( wp_filter_post_kses( $value ) ); + break; + default: + if ( is_callable( array( $coupon, "set_{$key}" ) ) ) { + $coupon->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $coupon Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $coupon, $request, $creating ); + } + + /** + * Get the Coupon's schema, conforming to JSON Schema. + * + * @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 for the object.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'amount' => array( + 'description' => __( 'The amount of discount. Should always be numeric, even if setting a percentage.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'The status of the coupon. Should always be draft, published, or pending review', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the coupon was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the coupon was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the coupon was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the coupon was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'discount_type' => array( + 'description' => __( 'Determines the type of discount that will be applied.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'fixed_cart', + 'enum' => array_keys( wc_get_coupon_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Coupon description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_expires' => array( + 'description' => __( "The date the coupon expires, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_expires_gmt' => array( + 'description' => __( 'The date the coupon expires, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'usage_count' => array( + 'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'individual_use' => array( + 'description' => __( 'If true, the coupon can only be used individually. Other applied coupons will be removed from the cart.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'product_ids' => array( + 'description' => __( 'List of product IDs the coupon can be used on.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'excluded_product_ids' => array( + 'description' => __( 'List of product IDs the coupon cannot be used on.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'usage_limit' => array( + 'description' => __( 'How many times the coupon can be used in total.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'usage_limit_per_user' => array( + 'description' => __( 'How many times the coupon can be used per customer.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'limit_usage_to_x_items' => array( + 'description' => __( 'Max number of items in the cart the coupon can be applied to.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'free_shipping' => array( + 'description' => __( 'If true and if the free shipping method requires a coupon, this coupon will enable free shipping.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'product_categories' => array( + 'description' => __( 'List of category IDs the coupon applies to.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'excluded_product_categories' => array( + 'description' => __( 'List of category IDs the coupon does not apply to.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'exclude_sale_items' => array( + 'description' => __( 'If true, this coupon will not be applied to items that have sale prices.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'minimum_amount' => array( + 'description' => __( 'Minimum order amount that needs to be in the cart before coupon applies.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'maximum_amount' => array( + 'description' => __( 'Maximum order amount allowed when using the coupon.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email_restrictions' => array( + 'description' => __( 'List of email addresses that can use this coupon.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'context' => array( 'view', 'edit' ), + ), + 'used_by' => array( + 'description' => __( 'List of user IDs (or guest email addresses) that have used the coupon.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['code'] = array( + 'description' => __( 'Limit result set to resources with a specific code.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-customer-downloads-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-customer-downloads-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-customer-downloads-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-customer-downloads-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-customers-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-customers-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-customers-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-customers-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-network-orders-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-network-orders-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-network-orders-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-network-orders-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-order-refunds-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-order-refunds-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-order-refunds-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-order-refunds-v2-controller.php diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php new file mode 100644 index 00000000000..8bda913c60a --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php @@ -0,0 +1,1911 @@ +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.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 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 ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/batch', + array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Get object. Return false if object is not of required type. + * + * @since 3.0.0 + * @param int $id Object ID. + * @return WC_Data|bool + */ + protected function get_object( $id ) { + $order = wc_get_order( $id ); + // In case id is a refund's id (or it's not an order at all), don't expose it via /orders/ path. + if ( ! $order || 'shop_order_refund' === $order->get_type() ) { + return false; + } + + return $order; + } + + /** + * Check if a given request has access to read an item. + * + * @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 ( ( ! $object || 0 === $object->get_id() ) && ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return parent::get_item_permissions_check( $request ); + } + + /** + * Check if a given request has access to update an item. + * + * @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 ( ( ! $object || 0 === $object->get_id() ) && ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return parent::update_item_permissions_check( $request ); + } + + /** + * Check if a given request has access to delete an item. + * + * @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 ( ( ! $object || 0 === $object->get_id() ) && ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return parent::delete_item_permissions_check( $request ); + } + + /** + * Expands an order item to get its data. + * + * @param WC_Order_item $item Order item data. + * @return array + */ + protected function get_order_item_data( $item ) { + $data = $item->get_data(); + $format_decimal = array( 'subtotal', 'subtotal_tax', 'total', 'total_tax', 'tax_total', 'shipping_tax_total' ); + + // Format decimal values. + foreach ( $format_decimal as $key ) { + if ( isset( $data[ $key ] ) ) { + $data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] ); + } + } + + // Add SKU and PRICE to products. + if ( is_callable( array( $item, 'get_product' ) ) ) { + $data['sku'] = $item->get_product() ? $item->get_product()->get_sku() : null; + $data['price'] = $item->get_quantity() ? $item->get_total() / $item->get_quantity() : 0; + } + + // Add parent_name if the product is a variation. + if ( is_callable( array( $item, 'get_product' ) ) ) { + $product = $item->get_product(); + + if ( is_callable( array( $product, 'get_parent_data' ) ) ) { + $data['parent_name'] = $product->get_title(); + } else { + $data['parent_name'] = null; + } + } + + // Format taxes. + if ( ! empty( $data['taxes']['total'] ) ) { + $taxes = array(); + + foreach ( $data['taxes']['total'] as $tax_rate_id => $tax ) { + $taxes[] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + 'subtotal' => isset( $data['taxes']['subtotal'][ $tax_rate_id ] ) ? $data['taxes']['subtotal'][ $tax_rate_id ] : '', + ); + } + $data['taxes'] = $taxes; + } elseif ( isset( $data['taxes'] ) ) { + $data['taxes'] = array(); + } + + // Remove names for coupons, taxes and shipping. + if ( isset( $data['code'] ) || isset( $data['rate_code'] ) || isset( $data['method_title'] ) ) { + unset( $data['name'] ); + } + + // Remove props we don't want to expose. + unset( $data['order_id'] ); + unset( $data['type'] ); + + // Expand meta_data to include user-friendly values. + $formatted_meta_data = $item->get_all_formatted_meta_data( null ); + + // Filter out product variations. + if ( isset( $product ) && 'true' === $this->request['order_item_display_meta'] ) { + $order_item_name = $data['name']; + $data['meta_data'] = array_filter( + $data['meta_data'], + function( $meta ) use ( $product, $order_item_name ) { + $display_value = wp_kses_post( rawurldecode( (string) $meta->value ) ); + + // Skip items with values already in the product details area of the product name. + if ( $product && $product->is_type( 'variation' ) && wc_is_attribute_in_product_name( $display_value, $order_item_name ) ) { + return false; + } + + return true; + } + ); + } + + $data['meta_data'] = array_map( + array( $this, 'merge_meta_item_with_formatted_meta_display_attributes' ), + $data['meta_data'], + array_fill( 0, count( $data['meta_data'] ), $formatted_meta_data ) + ); + + return $data; + } + + /** + * Merge the `$formatted_meta_data` `display_key` and `display_value` attribute values into the corresponding + * {@link WC_Meta_Data}. Returns the merged array. + * + * @param WC_Meta_Data $meta_item An object from {@link WC_Order_Item::get_meta_data()}. + * @param array $formatted_meta_data An object result from {@link WC_Order_Item::get_all_formatted_meta_data}. + * The keys are the IDs of {@link WC_Meta_Data}. + * + * @return array + */ + private function merge_meta_item_with_formatted_meta_display_attributes( $meta_item, $formatted_meta_data ) { + $result = array( + 'id' => $meta_item->id, + 'key' => $meta_item->key, + 'value' => $meta_item->value, + 'display_key' => $meta_item->key, // Default to original key, in case a formatted key is not available. + 'display_value' => $meta_item->value, // Default to original value, in case a formatted value is not available. + ); + + if ( array_key_exists( $meta_item->id, $formatted_meta_data ) ) { + $formatted_meta_item = $formatted_meta_data[ $meta_item->id ]; + + $result['display_key'] = wc_clean( $formatted_meta_item->display_key ); + $result['display_value'] = wc_clean( $formatted_meta_item->display_value ); + } + + return $result; + } + + /** + * Get formatted item data. + * + * @since 3.0.0 + * @param WC_Order $order WC_Data instance. + * + * @return array + */ + protected function get_formatted_item_data( $order ) { + $extra_fields = array( 'meta_data', 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines', 'refunds', 'payment_url' ); + $format_decimal = array( 'discount_total', 'discount_tax', 'shipping_total', 'shipping_tax', 'shipping_total', 'shipping_tax', 'cart_tax', 'total', 'total_tax' ); + $format_date = array( 'date_created', 'date_modified', 'date_completed', 'date_paid' ); + // These fields are dependent on other fields. + $dependent_fields = array( + 'date_created_gmt' => 'date_created', + 'date_modified_gmt' => 'date_modified', + 'date_completed_gmt' => 'date_completed', + 'date_paid_gmt' => 'date_paid', + ); + + $format_line_items = array( 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines' ); + + // Only fetch fields that we need. + $fields = $this->get_fields_for_response( $this->request ); + foreach ( $dependent_fields as $field_key => $dependency ) { + if ( in_array( $field_key, $fields ) && ! in_array( $dependency, $fields ) ) { + $fields[] = $dependency; + } + } + + $extra_fields = array_intersect( $extra_fields, $fields ); + $format_decimal = array_intersect( $format_decimal, $fields ); + $format_date = array_intersect( $format_date, $fields ); + + $format_line_items = array_intersect( $format_line_items, $fields ); + + $data = $order->get_base_data(); + + // Add extra data as necessary. + foreach ( $extra_fields as $field ) { + switch ( $field ) { + case 'meta_data': + $data['meta_data'] = $order->get_meta_data(); + break; + case 'line_items': + $data['line_items'] = $order->get_items( 'line_item' ); + break; + case 'tax_lines': + $data['tax_lines'] = $order->get_items( 'tax' ); + break; + case 'shipping_lines': + $data['shipping_lines'] = $order->get_items( 'shipping' ); + break; + case 'fee_lines': + $data['fee_lines'] = $order->get_items( 'fee' ); + break; + case 'coupon_lines': + $data['coupon_lines'] = $order->get_items( 'coupon' ); + break; + case 'refunds': + $data['refunds'] = array(); + foreach ( $order->get_refunds() as $refund ) { + $data['refunds'][] = array( + 'id' => $refund->get_id(), + 'reason' => $refund->get_reason() ? $refund->get_reason() : '', + 'total' => '-' . wc_format_decimal( $refund->get_amount(), $this->request['dp'] ), + ); + } + break; + case 'payment_url': + $data['payment_url'] = $order->get_checkout_payment_url(); + break; + } + } + + // Format decimal values. + foreach ( $format_decimal as $key ) { + $data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] ); + } + + // Format date values. + foreach ( $format_date as $key ) { + $datetime = $data[ $key ]; + $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); + $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); + } + + // Format the order status. + $data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status']; + + // Format line items. + foreach ( $format_line_items as $key ) { + $data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) ); + } + + $allowed_fields = array( + 'id', + 'parent_id', + 'number', + 'order_key', + 'created_via', + 'version', + 'status', + 'currency', + 'date_created', + 'date_created_gmt', + 'date_modified', + 'date_modified_gmt', + 'discount_total', + 'discount_tax', + 'shipping_total', + 'shipping_tax', + 'cart_tax', + 'total', + 'total_tax', + 'prices_include_tax', + 'customer_id', + 'customer_ip_address', + 'customer_user_agent', + 'customer_note', + 'billing', + 'shipping', + 'payment_method', + 'payment_method_title', + 'transaction_id', + 'date_paid', + 'date_paid_gmt', + 'date_completed', + 'date_completed_gmt', + 'cart_hash', + 'meta_data', + 'line_items', + 'tax_lines', + 'shipping_lines', + 'fee_lines', + 'coupon_lines', + 'refunds', + 'payment_url', + ); + + $data = array_intersect_key( $data, array_flip( $allowed_fields ) ); + + return $data; + } + + /** + * Prepare a single order output for response. + * + * @since 3.0.0 + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { + $this->request = $request; + $this->request['dp'] = is_null( $this->request['dp'] ) ? wc_get_price_decimals() : absint( $this->request['dp'] ); + $request['context'] = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->get_formatted_item_data( $object ); + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $request['context'] ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $object, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return array Links for the given post. + */ + protected function prepare_links( $object, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + if ( 0 !== (int) $object->get_customer_id() ) { + $links['customer'] = array( + 'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $object->get_customer_id() ) ), + ); + } + + if ( 0 !== (int) $object->get_parent_id() ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $object->get_parent_id() ) ), + ); + } + + return $links; + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + global $wpdb; + + $args = parent::prepare_objects_query( $request ); + + // Set post_status. + if ( in_array( $request['status'], $this->get_order_statuses(), true ) ) { + $args['post_status'] = 'wc-' . $request['status']; + } elseif ( 'any' === $request['status'] ) { + $args['post_status'] = 'any'; + } else { + $args['post_status'] = $request['status']; + } + + if ( isset( $request['customer'] ) ) { + if ( ! empty( $args['meta_query'] ) ) { + $args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + } + + $args['meta_query'][] = array( + 'key' => '_customer_user', + 'value' => $request['customer'], + 'type' => 'NUMERIC', + ); + } + + // Search by product. + if ( ! empty( $request['product'] ) ) { + $order_ids = $wpdb->get_col( + $wpdb->prepare( + "SELECT order_id + FROM {$wpdb->prefix}woocommerce_order_items + WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) + AND order_item_type = 'line_item'", + $request['product'] + ) + ); + + // Force WP_Query return empty if don't found any order. + $order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 ); + + $args['post__in'] = $order_ids; + } + + // Search. + if ( ! empty( $args['s'] ) ) { + $order_ids = wc_order_search( $args['s'] ); + + if ( ! empty( $order_ids ) ) { + unset( $args['s'] ); + $args['post__in'] = array_merge( $order_ids, array( 0 ) ); + } + } + + /** + * Filter the query arguments for a request. + * + * Enables adding extra arguments or setting defaults for an order collection request. + * + * @param array $args Key value array of query var to query value. + * @param WP_REST_Request $request The request used. + */ + $args = apply_filters( 'woocommerce_rest_orders_prepare_object_query', $args, $request ); + + return $args; + } + + /** + * Only return writable props from schema. + * + * @param array $schema Schema. + * @return bool + */ + protected function filter_writable_props( $schema ) { + return empty( $schema['readonly'] ); + } + + /** + * Prepare a single order for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $order = new WC_Order( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Handle all writable props. + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'status': + // Status change should be done later so transitions have new data. + break; + case 'billing': + case 'shipping': + $this->update_address( $order, $value, $key ); + break; + case 'line_items': + case 'shipping_lines': + case 'fee_lines': + case 'coupon_lines': + if ( is_array( $value ) ) { + foreach ( $value as $item ) { + if ( is_array( $item ) ) { + if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) { + $order->remove_item( $item['id'] ); + } else { + $this->set_item( $order, $key, $item ); + } + } + } + } + break; + case 'meta_data': + if ( is_array( $value ) ) { + foreach ( $value as $meta ) { + $order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + break; + default: + if ( is_callable( array( $order, "set_{$key}" ) ) ) { + $order->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $order Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $order, $request, $creating ); + } + + /** + * Save an object data. + * + * @since 3.0.0 + * @throws WC_REST_Exception But all errors are validated before returning any data. + * @param WP_REST_Request $request Full details about the request. + * @param bool $creating If is creating a new object. + * @return WC_Data|WP_Error + */ + protected function save_object( $request, $creating = false ) { + try { + $object = $this->prepare_object_for_database( $request, $creating ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + // Make sure gateways are loaded so hooks from gateways fire on save/create. + WC()->payment_gateways(); + + if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] ) { + // Make sure customer exists. + if ( false === get_user_by( 'id', $request['customer_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + // Make sure customer is part of blog. + if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) { + add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' ); + } + } + + if ( $creating ) { + $object->set_created_via( 'rest-api' ); + $object->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); + $object->calculate_totals(); + } else { + // If items have changed, recalculate order totals. + if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) { + $object->calculate_totals( true ); + } + } + + // Set status. + if ( ! empty( $request['status'] ) ) { + $object->set_status( $request['status'] ); + } + + $object->save(); + + // Actions for after the order is saved. + if ( true === $request['set_paid'] ) { + if ( $creating || $object->needs_payment() ) { + $object->payment_complete( $request['transaction_id'] ); + } + } + + return $this->get_object( $object->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update address. + * + * @param WC_Order $order Order data. + * @param array $posted Posted data. + * @param string $type Address type. + */ + protected function update_address( $order, $posted, $type = 'billing' ) { + foreach ( $posted as $key => $value ) { + if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { + $order->{"set_{$type}_{$key}"}( $value ); + } + } + } + + /** + * Gets the product ID from the SKU or posted ID. + * + * @throws WC_REST_Exception When SKU or ID is not valid. + * @param array $posted Request data. + * @param string $action 'create' to add line item or 'update' to update it. + * @return int + */ + protected function get_product_id( $posted, $action = 'create' ) { + if ( ! empty( $posted['sku'] ) ) { + $product_id = (int) wc_get_product_id_by_sku( $posted['sku'] ); + } elseif ( ! empty( $posted['product_id'] ) && empty( $posted['variation_id'] ) ) { + $product_id = (int) $posted['product_id']; + } elseif ( ! empty( $posted['variation_id'] ) ) { + $product_id = (int) $posted['variation_id']; + } elseif ( 'update' === $action ) { + $product_id = 0; + } else { + throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 ); + } + return $product_id; + } + + /** + * Maybe set an item prop if the value was posted. + * + * @param WC_Order_Item $item Order item. + * @param string $prop Order property. + * @param array $posted Request data. + */ + protected function maybe_set_item_prop( $item, $prop, $posted ) { + if ( isset( $posted[ $prop ] ) ) { + $item->{"set_$prop"}( $posted[ $prop ] ); + } + } + + /** + * Maybe set item props if the values were posted. + * + * @param WC_Order_Item $item Order item data. + * @param string[] $props Properties. + * @param array $posted Request data. + */ + protected function maybe_set_item_props( $item, $props, $posted ) { + foreach ( $props as $prop ) { + $this->maybe_set_item_prop( $item, $prop, $posted ); + } + } + + /** + * Maybe set item meta if posted. + * + * @param WC_Order_Item $item Order item data. + * @param array $posted Request data. + */ + protected function maybe_set_item_meta_data( $item, $posted ) { + if ( ! empty( $posted['meta_data'] ) && is_array( $posted['meta_data'] ) ) { + foreach ( $posted['meta_data'] as $meta ) { + if ( isset( $meta['key'] ) ) { + $value = isset( $meta['value'] ) ? $meta['value'] : null; + $item->update_meta_data( $meta['key'], $value, isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + } + } + + /** + * Create or update a line item. + * + * @param array $posted Line item data. + * @param string $action 'create' to add line item or 'update' to update it. + * @param object $item Passed when updating an item. Null during creation. + * @return WC_Order_Item_Product + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_line_items( $posted, $action = 'create', $item = null ) { + $item = is_null( $item ) ? new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item; + $product = wc_get_product( $this->get_product_id( $posted, $action ) ); + + if ( $product && $product !== $item->get_product() ) { + $item->set_product( $product ); + + if ( 'create' === $action ) { + $quantity = isset( $posted['quantity'] ) ? $posted['quantity'] : 1; + $total = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) ); + $item->set_total( $total ); + $item->set_subtotal( $total ); + } + } + + $this->maybe_set_item_props( $item, array( 'name', 'quantity', 'total', 'subtotal', 'tax_class' ), $posted ); + $this->maybe_set_item_meta_data( $item, $posted ); + + return $item; + } + + /** + * Create or update an order shipping method. + * + * @param array $posted $shipping Item data. + * @param string $action 'create' to add shipping or 'update' to update it. + * @param object $item Passed when updating an item. Null during creation. + * @return WC_Order_Item_Shipping + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_shipping_lines( $posted, $action = 'create', $item = null ) { + $item = is_null( $item ) ? new WC_Order_Item_Shipping( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item; + + if ( 'create' === $action ) { + if ( empty( $posted['method_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'method_id', 'method_title', 'total', 'instance_id' ), $posted ); + $this->maybe_set_item_meta_data( $item, $posted ); + + return $item; + } + + /** + * Create or update an order fee. + * + * @param array $posted Item data. + * @param string $action 'create' to add fee or 'update' to update it. + * @param object $item Passed when updating an item. Null during creation. + * @return WC_Order_Item_Fee + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_fee_lines( $posted, $action = 'create', $item = null ) { + $item = is_null( $item ) ? new WC_Order_Item_Fee( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item; + + if ( 'create' === $action ) { + if ( empty( $posted['name'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'name', 'tax_class', 'tax_status', 'total' ), $posted ); + $this->maybe_set_item_meta_data( $item, $posted ); + + return $item; + } + + /** + * Create or update an order coupon. + * + * @param array $posted Item data. + * @param string $action 'create' to add coupon or 'update' to update it. + * @param object $item Passed when updating an item. Null during creation. + * @return WC_Order_Item_Coupon + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_coupon_lines( $posted, $action = 'create', $item = null ) { + $item = is_null( $item ) ? new WC_Order_Item_Coupon( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item; + + if ( 'create' === $action ) { + if ( empty( $posted['code'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'code', 'discount' ), $posted ); + $this->maybe_set_item_meta_data( $item, $posted ); + + return $item; + } + + /** + * Wrapper method to create/update order items. + * When updating, the item ID provided is checked to ensure it is associated + * with the order. + * + * @param WC_Order $order order object. + * @param string $item_type The item type. + * @param array $posted item provided in the request body. + * @throws WC_REST_Exception If item ID is not associated with order. + */ + protected function set_item( $order, $item_type, $posted ) { + global $wpdb; + + if ( ! empty( $posted['id'] ) ) { + $action = 'update'; + } else { + $action = 'create'; + } + + $method = 'prepare_' . $item_type; + $item = null; + + // Verify provided line item ID is associated with order. + if ( 'update' === $action ) { + $item = $order->get_item( absint( $posted['id'] ), false ); + + if ( ! $item ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); + } + } + + // Prepare item data. + $item = $this->$method( $posted, $action, $item ); + + do_action( 'woocommerce_rest_set_order_item', $item, $posted ); + + // If creating the order, add the item to it. + if ( 'create' === $action ) { + $order->add_item( $item ); + } else { + $item->save(); + } + } + + /** + * Helper method to check if the resource ID associated with the provided item is null. + * Items can be deleted by setting the resource ID to null. + * + * @param array $item Item provided in the request body. + * @return bool True if the item resource ID is null, false otherwise. + */ + protected function item_is_null( $item ) { + $keys = array( 'product_id', 'method_id', 'method_title', 'name', 'code' ); + + foreach ( $keys as $key ) { + if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { + return true; + } + } + + return false; + } + + /** + * Get order statuses without prefixes. + * + * @return array + */ + protected function get_order_statuses() { + $order_statuses = array( 'auto-draft' ); + + foreach ( array_keys( wc_get_order_statuses() ) as $status ) { + $order_statuses[] = str_replace( 'wc-', '', $status ); + } + + return $order_statuses; + } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @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 for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'parent_id' => array( + 'description' => __( 'Parent order ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'number' => array( + 'description' => __( 'Order number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'order_key' => array( + 'description' => __( 'Order key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'created_via' => array( + 'description' => __( 'Shows where the order was created.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'version' => array( + 'description' => __( 'Version of WooCommerce which last updated the order.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'status' => array( + 'description' => __( 'Order status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'pending', + 'enum' => $this->get_order_statuses(), + 'context' => array( 'view', 'edit' ), + ), + 'currency' => array( + 'description' => __( 'Currency the order was created with, in ISO format.', 'woocommerce' ), + 'type' => 'string', + 'default' => get_woocommerce_currency(), + 'enum' => array_keys( get_woocommerce_currencies() ), + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the order was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the order was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the order was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the order was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'discount_total' => array( + 'description' => __( 'Total discount amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'discount_tax' => array( + 'description' => __( 'Total discount tax amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_total' => array( + 'description' => __( 'Total shipping amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_tax' => array( + 'description' => __( 'Total shipping tax amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'cart_tax' => array( + 'description' => __( 'Sum of line item taxes only.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Grand total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_tax' => array( + 'description' => __( 'Sum of all taxes.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'prices_include_tax' => array( + 'description' => __( 'True the prices included tax during checkout.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_id' => array( + 'description' => __( 'User ID who owns the order. 0 for guests.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 0, + 'context' => array( 'view', 'edit' ), + ), + 'customer_ip_address' => array( + 'description' => __( "Customer's IP address.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_user_agent' => array( + 'description' => __( 'User agent of the customer.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_note' => array( + 'description' => __( 'Note left by customer during checkout.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'billing' => array( + 'description' => __( 'Billing address.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Email address.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping' => array( + 'description' => __( 'Shipping address.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'payment_method' => array( + 'description' => __( 'Payment method ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'payment_method_title' => array( + 'description' => __( 'Payment method title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'transaction_id' => array( + 'description' => __( 'Unique transaction ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_paid' => array( + 'description' => __( "The date the order was paid, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_paid_gmt' => array( + 'description' => __( 'The date the order was paid, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_completed' => array( + 'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_completed_gmt' => array( + 'description' => __( 'The date the order was completed, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'cart_hash' => array( + 'description' => __( 'MD5 hash of cart items to ensure orders are not modified.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'line_items' => array( + 'description' => __( 'Line items data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'parent_name' => array( + 'description' => __( 'Parent product name if the product is a variation.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'variation_id' => array( + 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'quantity' => array( + 'description' => __( 'Quantity ordered.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of product.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal' => array( + 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal_tax' => array( + 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'display_key' => array( + 'description' => __( 'Meta key for UI display.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'display_value' => array( + 'description' => __( 'Meta value for UI display.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'sku' => array( + 'description' => __( 'Product SKU.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'price' => array( + 'description' => __( 'Product price.', 'woocommerce' ), + 'type' => 'number', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'tax_lines' => array( + 'description' => __( 'Tax lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rate_code' => array( + 'description' => __( 'Tax rate code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rate_id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Tax rate label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'compound' => array( + 'description' => __( 'Show if is a compound tax rate.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tax_total' => array( + 'description' => __( 'Tax total (not including shipping taxes).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_tax_total' => array( + 'description' => __( 'Shipping tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ), + ), + 'shipping_lines' => array( + 'description' => __( 'Shipping lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'method_title' => array( + 'description' => __( 'Shipping method name.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'method_id' => array( + 'description' => __( 'Shipping method ID.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'instance_id' => array( + 'description' => __( 'Shipping instance ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ), + ), + 'fee_lines' => array( + 'description' => __( 'Fee lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Fee name.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of fee.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status of fee.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'enum' => array( 'taxable', 'none' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ), + ), + 'coupon_lines' => array( + 'description' => __( 'Coupons line data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'discount' => array( + 'description' => __( 'Discount total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'discount_tax' => array( + 'description' => __( 'Discount total tax.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ), + ), + 'refunds' => array( + 'description' => __( 'List of refunds.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Refund ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'reason' => array( + 'description' => __( 'Refund reason.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Refund total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'payment_url' => array( + 'description' => __( 'Order payment URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'set_paid' => array( + 'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to orders assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'any', 'trash' ), $this->get_order_statuses() ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['customer'] = array( + 'description' => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['product'] = array( + 'description' => __( 'Limit result set to orders assigned a specific product.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['dp'] = array( + 'default' => wc_get_price_decimals(), + 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order_item_display_meta'] = array( + 'default' => false, + 'description' => __( 'Only show meta which is meant to be displayed for an order.', 'woocommerce' ), + 'type' => 'boolean', + 'sanitize_callback' => 'rest_sanitize_boolean', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-payment-gateways-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-payment-gateways-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-payment-gateways-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-payment-gateways-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-product-attribute-terms-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-attribute-terms-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-product-attribute-terms-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-attribute-terms-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-product-attributes-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-attributes-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-product-attributes-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-attributes-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-product-categories-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-categories-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-product-categories-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-categories-v2-controller.php diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php new file mode 100644 index 00000000000..9a8007322e6 --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php @@ -0,0 +1,199 @@ +/reviews. + * + * @package WooCommerce\RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Product Reviews Controller Class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Product_Reviews_V1_Controller + */ +class WC_REST_Product_Reviews_V2_Controller extends WC_REST_Product_Reviews_V1_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v2'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'products/(?P[\d]+)/reviews'; + + /** + * Register the routes for product reviews. + */ + public function register_routes() { + parent::register_routes(); + + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/batch', array( + 'args' => array( + 'product_id' => array( + 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Check if a given request has access to batch manage product reviews. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function batch_items_permissions_check( $request ) { + if ( ! wc_rest_check_product_reviews_permissions( 'batch' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Prepare a single product review output for response. + * + * @param WP_Comment $review Product review object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $review, $request ) { + $data = array( + 'id' => (int) $review->comment_ID, + 'date_created' => wc_rest_prepare_date_response( $review->comment_date ), + 'date_created_gmt' => wc_rest_prepare_date_response( $review->comment_date_gmt ), + 'review' => $review->comment_content, + 'rating' => (int) get_comment_meta( $review->comment_ID, 'rating', true ), + 'name' => $review->comment_author, + 'email' => $review->comment_author_email, + 'verified' => wc_review_is_from_verified_owner( $review->comment_ID ), + ); + + $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 ); + + $response->add_links( $this->prepare_links( $review, $request ) ); + + /** + * Filter product reviews object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment $review Product review object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request ); + } + + + /** + * Bulk create, update and delete items. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array Of WP_Error or WP_REST_Response. + */ + public function batch_items( $request ) { + $items = array_filter( $request->get_params() ); + $params = $request->get_url_params(); + $product_id = $params['product_id']; + $body_params = array(); + + foreach ( array( 'update', 'create', 'delete' ) as $batch_type ) { + if ( ! empty( $items[ $batch_type ] ) ) { + $injected_items = array(); + foreach ( $items[ $batch_type ] as $item ) { + $injected_items[] = is_array( $item ) ? array_merge( array( 'product_id' => $product_id ), $item ) : $item; + } + $body_params[ $batch_type ] = $injected_items; + } + } + + $request = new WP_REST_Request( $request->get_method() ); + $request->set_body_params( $body_params ); + + return parent::batch_items( $request ); + } + + /** + * Get the Product Review's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'product_review', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'review' => array( + 'description' => __( 'The content of the review.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the review was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'rating' => array( + 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Reviewer name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Reviewer email.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'verified' => array( + 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-product-shipping-classes-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-shipping-classes-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-product-shipping-classes-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-shipping-classes-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-product-tags-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-tags-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-product-tags-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-tags-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php new file mode 100644 index 00000000000..4927dc37f1b --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php @@ -0,0 +1,2385 @@ +post_type}_object", array( $this, 'clear_transients' ) ); + } + + /** + * Register the routes for products. + */ + 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.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( + array( + 'default' => 'view', + ) + ), + ), + ), + 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 ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + 'type' => 'boolean', + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/batch', + array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Get object. + * + * @param int $id Object ID. + * + * @since 3.0.0 + * @return WC_Data + */ + protected function get_object( $id ) { + return wc_get_product( $id ); + } + + /** + * Prepare a single product output for response. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * + * @since 3.0.0 + * @return WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $this->request = $request; + $data = $this->get_product_data( $object, $context, $request ); + + // Add variations to variable products. + if ( $object->is_type( 'variable' ) && $object->has_child() ) { + $data['variations'] = $object->get_children(); + } + + // Add grouped products data. + if ( $object->is_type( 'grouped' ) && $object->has_child() ) { + $data['grouped_products'] = $object->get_children(); + } + + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $object, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Prepare objects query. + * + * @param WP_REST_Request $request Full details about the request. + * + * @since 3.0.0 + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = parent::prepare_objects_query( $request ); + + // Set post_status. + $args['post_status'] = $request['status']; + + // Taxonomy query to filter products by type, category, + // tag, shipping class, and attribute. + $tax_query = array(); + + // Map between taxonomy name and arg's key. + $taxonomies = array( + 'product_cat' => 'category', + 'product_tag' => 'tag', + 'product_shipping_class' => 'shipping_class', + ); + + // Set tax_query for each passed arg. + foreach ( $taxonomies as $taxonomy => $key ) { + if ( ! empty( $request[ $key ] ) ) { + $tax_query[] = array( + 'taxonomy' => $taxonomy, + 'field' => 'term_id', + 'terms' => $request[ $key ], + ); + } + } + + // Filter product type by slug. + if ( ! empty( $request['type'] ) ) { + $tax_query[] = array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $request['type'], + ); + } + + // Filter by attribute and term. + if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { + if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { + $tax_query[] = array( + 'taxonomy' => $request['attribute'], + 'field' => 'term_id', + 'terms' => $request['attribute_term'], + ); + } + } + + if ( ! empty( $tax_query ) ) { + $args['tax_query'] = $tax_query; // WPCS: slow query ok. + } + + // Filter featured. + if ( is_bool( $request['featured'] ) ) { + $args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'name', + 'terms' => 'featured', + 'operator' => true === $request['featured'] ? 'IN' : 'NOT IN', + ); + } + + // Filter by sku. + if ( ! empty( $request['sku'] ) ) { + $skus = explode( ',', $request['sku'] ); + // Include the current string as a SKU too. + if ( 1 < count( $skus ) ) { + $skus[] = $request['sku']; + } + + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_sku', + 'value' => $skus, + 'compare' => 'IN', + ) + ); + } + + // Filter by tax class. + if ( ! empty( $request['tax_class'] ) ) { + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_tax_class', + 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', + ) + ); + } + + // Price filter. + if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); // WPCS: slow query ok. + } + + // Filter product in stock or out of stock. + if ( is_bool( $request['in_stock'] ) ) { + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_stock_status', + 'value' => true === $request['in_stock'] ? 'instock' : 'outofstock', + ) + ); + } + + // Filter by on sale products. + if ( is_bool( $request['on_sale'] ) ) { + $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; + $on_sale_ids = wc_get_product_ids_on_sale(); + + // Use 0 when there's no on sale products to avoid return all products. + $on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids; + + $args[ $on_sale_key ] += $on_sale_ids; + } + + // Force the post_type argument, since it's not a user input variable. + if ( ! empty( $request['sku'] ) ) { + $args['post_type'] = array( 'product', 'product_variation' ); + } else { + $args['post_type'] = $this->post_type; + } + + return $args; + } + + /** + * Get the downloads for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * + * @return array + */ + protected function get_downloads( $product ) { + $downloads = array(); + + if ( $product->is_downloadable() ) { + foreach ( $product->get_downloads() as $file_id => $file ) { + $downloads[] = array( + 'id' => $file_id, // MD5 hash. + 'name' => $file['name'], + 'file' => $file['file'], + ); + } + } + + return $downloads; + } + + /** + * Get taxonomy terms. + * + * @param WC_Product $product Product instance. + * @param string $taxonomy Taxonomy slug. + * + * @return array + */ + protected function get_taxonomy_terms( $product, $taxonomy = 'cat' ) { + $terms = array(); + + foreach ( wc_get_object_terms( $product->get_id(), 'product_' . $taxonomy ) as $term ) { + $terms[] = array( + 'id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + ); + } + + return $terms; + } + + /** + * Get the images for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * + * @return array + */ + protected function get_images( $product ) { + $images = array(); + $attachment_ids = array(); + + // Add featured image. + if ( $product->get_image_id() ) { + $attachment_ids[] = $product->get_image_id(); + } + + // Add gallery images. + $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); + + // Build image data. + foreach ( $attachment_ids as $position => $attachment_id ) { + $attachment_post = get_post( $attachment_id ); + if ( is_null( $attachment_post ) ) { + continue; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + if ( ! is_array( $attachment ) ) { + continue; + } + + $images[] = array( + 'id' => (int) $attachment_id, + 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ), + 'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ), + 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ), + 'src' => current( $attachment ), + 'name' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + 'position' => (int) $position, + ); + } + + // Set a placeholder image if the product has no images set. + if ( empty( $images ) ) { + $images[] = array( + 'id' => 0, + 'date_created' => wc_rest_prepare_date_response( current_time( 'mysql' ), false ), // Default to now. + 'date_created_gmt' => wc_rest_prepare_date_response( time() ), // Default to now. + 'date_modified' => wc_rest_prepare_date_response( current_time( 'mysql' ), false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( time() ), + 'src' => wc_placeholder_img_src(), + 'name' => __( 'Placeholder', 'woocommerce' ), + 'alt' => __( 'Placeholder', 'woocommerce' ), + 'position' => 0, + ); + } + + return $images; + } + + /** + * Get attribute taxonomy label. + * + * @param string $name Taxonomy name. + * + * @deprecated 3.0.0 + * @return string + */ + protected function get_attribute_taxonomy_label( $name ) { + $tax = get_taxonomy( $name ); + $labels = get_taxonomy_labels( $tax ); + + return $labels->singular_name; + } + + /** + * Get product attribute taxonomy name. + * + * @param string $slug Taxonomy name. + * @param WC_Product $product Product data. + * + * @since 3.0.0 + * @return string + */ + protected function get_attribute_taxonomy_name( $slug, $product ) { + // Format slug so it matches attributes of the product. + $slug = wc_attribute_taxonomy_slug( $slug ); + $attributes = $product->get_attributes(); + $attribute = false; + + // pa_ attributes. + if ( isset( $attributes[ wc_attribute_taxonomy_name( $slug ) ] ) ) { + $attribute = $attributes[ wc_attribute_taxonomy_name( $slug ) ]; + } elseif ( isset( $attributes[ $slug ] ) ) { + $attribute = $attributes[ $slug ]; + } + + if ( ! $attribute ) { + return $slug; + } + + // Taxonomy attribute name. + if ( $attribute->is_taxonomy() ) { + $taxonomy = $attribute->get_taxonomy_object(); + return $taxonomy->attribute_label; + } + + // Custom product attribute name. + return $attribute->get_name(); + } + + /** + * Get default attributes. + * + * @param WC_Product $product Product instance. + * + * @return array + */ + protected function get_default_attributes( $product ) { + $default = array(); + + if ( $product->is_type( 'variable' ) ) { + foreach ( array_filter( (array) $product->get_default_attributes(), 'strlen' ) as $key => $value ) { + if ( 0 === strpos( $key, 'pa_' ) ) { + $default[] = array( + 'id' => wc_attribute_taxonomy_id_by_name( $key ), + 'name' => $this->get_attribute_taxonomy_name( $key, $product ), + 'option' => $value, + ); + } else { + $default[] = array( + 'id' => 0, + 'name' => $this->get_attribute_taxonomy_name( $key, $product ), + 'option' => $value, + ); + } + } + } + + return $default; + } + + /** + * Get attribute options. + * + * @param int $product_id Product ID. + * @param array $attribute Attribute data. + * + * @return array + */ + protected function get_attribute_options( $product_id, $attribute ) { + if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { + return wc_get_product_terms( + $product_id, + $attribute['name'], + array( + 'fields' => 'names', + ) + ); + } elseif ( isset( $attribute['value'] ) ) { + return array_map( 'trim', explode( '|', $attribute['value'] ) ); + } + + return array(); + } + + /** + * Get the attributes for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * + * @return array + */ + protected function get_attributes( $product ) { + $attributes = array(); + + if ( $product->is_type( 'variation' ) ) { + $_product = wc_get_product( $product->get_parent_id() ); + foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { + $name = str_replace( 'attribute_', '', $attribute_name ); + + if ( empty( $attribute ) && '0' !== $attribute ) { + continue; + } + + // Taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`. + if ( 0 === strpos( $attribute_name, 'attribute_pa_' ) ) { + $option_term = get_term_by( 'slug', $attribute, $name ); + $attributes[] = array( + 'id' => wc_attribute_taxonomy_id_by_name( $name ), + 'name' => $this->get_attribute_taxonomy_name( $name, $_product ), + 'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute, + ); + } else { + $attributes[] = array( + 'id' => 0, + 'name' => $this->get_attribute_taxonomy_name( $name, $_product ), + 'option' => $attribute, + ); + } + } + } else { + foreach ( $product->get_attributes() as $attribute ) { + $attributes[] = array( + 'id' => $attribute['is_taxonomy'] ? wc_attribute_taxonomy_id_by_name( $attribute['name'] ) : 0, + 'name' => $this->get_attribute_taxonomy_name( $attribute['name'], $product ), + 'position' => (int) $attribute['position'], + 'visible' => (bool) $attribute['is_visible'], + 'variation' => (bool) $attribute['is_variation'], + 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), + ); + } + } + + return $attributes; + } + + /** + * Fetch price HTML. + * + * @param WC_Product $product Product object. + * @param string $context Context of request, can be `view` or `edit`. + * + * @return string + */ + protected function api_get_price_html( $product, $context ) { + return $product->get_price_html(); + } + + /** + * Fetch related IDs. + * + * @param WC_Product $product Product object. + * @param string $context Context of request, can be `view` or `edit`. + * + * @return array + */ + protected function api_get_related_ids( $product, $context ) { + return array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ); + } + + /** + * Fetch meta data. + * + * @param WC_Product $product Product object. + * @param string $context Context of request, can be `view` or `edit`. + * + * @return array + */ + protected function api_get_meta_data( $product, $context ) { + return $product->get_meta_data(); + } + + /** + * Get product data. + * + * @param WC_Product $product Product instance. + * @param string $context Request context. Options: 'view' and 'edit'. + * + * @return array + */ + protected function get_product_data( $product, $context = 'view' ) { + /* + * @param WP_REST_Request $request Current request object. For backward compatibility, we pass this argument silently. + * + * TODO: Refactor to fix this behavior when DI gets included to make it obvious and clean. + */ + $request = func_num_args() >= 3 ? func_get_arg( 2 ) : new WP_REST_Request( '', '', array( 'context' => $context ) ); + $fields = $this->get_fields_for_response( $request ); + + $base_data = array(); + foreach ( $fields as $field ) { + switch ( $field ) { + case 'id': + $base_data['id'] = $product->get_id(); + break; + case 'name': + $base_data['name'] = $product->get_name( $context ); + break; + case 'slug': + $base_data['slug'] = $product->get_slug( $context ); + break; + case 'permalink': + $base_data['permalink'] = $product->get_permalink(); + break; + case 'date_created': + $base_data['date_created'] = wc_rest_prepare_date_response( $product->get_date_created( $context ), false ); + break; + case 'date_created_gmt': + $base_data['date_created_gmt'] = wc_rest_prepare_date_response( $product->get_date_created( $context ) ); + break; + case 'date_modified': + $base_data['date_modified'] = wc_rest_prepare_date_response( $product->get_date_modified( $context ), false ); + break; + case 'date_modified_gmt': + $base_data['date_modified_gmt'] = wc_rest_prepare_date_response( $product->get_date_modified( $context ) ); + break; + case 'type': + $base_data['type'] = $product->get_type(); + break; + case 'status': + $base_data['status'] = $product->get_status( $context ); + break; + case 'featured': + $base_data['featured'] = $product->is_featured(); + break; + case 'catalog_visibility': + $base_data['catalog_visibility'] = $product->get_catalog_visibility( $context ); + break; + case 'description': + $base_data['description'] = 'view' === $context ? wpautop( do_shortcode( $product->get_description() ) ) : $product->get_description( $context ); + break; + case 'short_description': + $base_data['short_description'] = 'view' === $context ? apply_filters( 'woocommerce_short_description', $product->get_short_description() ) : $product->get_short_description( $context ); + break; + case 'sku': + $base_data['sku'] = $product->get_sku( $context ); + break; + case 'price': + $base_data['price'] = $product->get_price( $context ); + break; + case 'regular_price': + $base_data['regular_price'] = $product->get_regular_price( $context ); + break; + case 'sale_price': + $base_data['sale_price'] = $product->get_sale_price( $context ) ? $product->get_sale_price( $context ) : ''; + break; + case 'date_on_sale_from': + $base_data['date_on_sale_from'] = wc_rest_prepare_date_response( $product->get_date_on_sale_from( $context ), false ); + break; + case 'date_on_sale_from_gmt': + $base_data['date_on_sale_from_gmt'] = wc_rest_prepare_date_response( $product->get_date_on_sale_from( $context ) ); + break; + case 'date_on_sale_to': + $base_data['date_on_sale_to'] = wc_rest_prepare_date_response( $product->get_date_on_sale_to( $context ), false ); + break; + case 'date_on_sale_to_gmt': + $base_data['date_on_sale_to_gmt'] = wc_rest_prepare_date_response( $product->get_date_on_sale_to( $context ) ); + break; + case 'on_sale': + $base_data['on_sale'] = $product->is_on_sale( $context ); + break; + case 'purchasable': + $base_data['purchasable'] = $product->is_purchasable(); + break; + case 'total_sales': + $base_data['total_sales'] = $product->get_total_sales( $context ); + break; + case 'virtual': + $base_data['virtual'] = $product->is_virtual(); + break; + case 'downloadable': + $base_data['downloadable'] = $product->is_downloadable(); + break; + case 'downloads': + $base_data['downloads'] = $this->get_downloads( $product ); + break; + case 'download_limit': + $base_data['download_limit'] = $product->get_download_limit( $context ); + break; + case 'download_expiry': + $base_data['download_expiry'] = $product->get_download_expiry( $context ); + break; + case 'external_url': + $base_data['external_url'] = $product->is_type( 'external' ) ? $product->get_product_url( $context ) : ''; + break; + case 'button_text': + $base_data['button_text'] = $product->is_type( 'external' ) ? $product->get_button_text( $context ) : ''; + break; + case 'tax_status': + $base_data['tax_status'] = $product->get_tax_status( $context ); + break; + case 'tax_class': + $base_data['tax_class'] = $product->get_tax_class( $context ); + break; + case 'manage_stock': + $base_data['manage_stock'] = $product->managing_stock(); + break; + case 'stock_quantity': + $base_data['stock_quantity'] = $product->get_stock_quantity( $context ); + break; + case 'in_stock': + $base_data['in_stock'] = $product->is_in_stock(); + break; + case 'backorders': + $base_data['backorders'] = $product->get_backorders( $context ); + break; + case 'backorders_allowed': + $base_data['backorders_allowed'] = $product->backorders_allowed(); + break; + case 'backordered': + $base_data['backordered'] = $product->is_on_backorder(); + break; + case 'low_stock_amount': + $base_data['low_stock_amount'] = '' === $product->get_low_stock_amount() ? null : $product->get_low_stock_amount(); + break; + case 'sold_individually': + $base_data['sold_individually'] = $product->is_sold_individually(); + break; + case 'weight': + $base_data['weight'] = $product->get_weight( $context ); + break; + case 'dimensions': + $base_data['dimensions'] = array( + 'length' => $product->get_length( $context ), + 'width' => $product->get_width( $context ), + 'height' => $product->get_height( $context ), + ); + break; + case 'shipping_required': + $base_data['shipping_required'] = $product->needs_shipping(); + break; + case 'shipping_taxable': + $base_data['shipping_taxable'] = $product->is_shipping_taxable(); + break; + case 'shipping_class': + $base_data['shipping_class'] = $product->get_shipping_class(); + break; + case 'shipping_class_id': + $base_data['shipping_class_id'] = $product->get_shipping_class_id( $context ); + break; + case 'reviews_allowed': + $base_data['reviews_allowed'] = $product->get_reviews_allowed( $context ); + break; + case 'average_rating': + $base_data['average_rating'] = 'view' === $context ? wc_format_decimal( $product->get_average_rating(), 2 ) : $product->get_average_rating( $context ); + break; + case 'rating_count': + $base_data['rating_count'] = $product->get_rating_count(); + break; + case 'upsell_ids': + $base_data['upsell_ids'] = array_map( 'absint', $product->get_upsell_ids( $context ) ); + break; + case 'cross_sell_ids': + $base_data['cross_sell_ids'] = array_map( 'absint', $product->get_cross_sell_ids( $context ) ); + break; + case 'parent_id': + $base_data['parent_id'] = $product->get_parent_id( $context ); + break; + case 'purchase_note': + $base_data['purchase_note'] = 'view' === $context ? wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ) : $product->get_purchase_note( $context ); + break; + case 'categories': + $base_data['categories'] = $this->get_taxonomy_terms( $product ); + break; + case 'tags': + $base_data['tags'] = $this->get_taxonomy_terms( $product, 'tag' ); + break; + case 'images': + $base_data['images'] = $this->get_images( $product ); + break; + case 'attributes': + $base_data['attributes'] = $this->get_attributes( $product ); + break; + case 'default_attributes': + $base_data['default_attributes'] = $this->get_default_attributes( $product ); + break; + case 'variations': + $base_data['variations'] = array(); + break; + case 'grouped_products': + $base_data['grouped_products'] = array(); + break; + case 'menu_order': + $base_data['menu_order'] = $product->get_menu_order( $context ); + break; + } + } + + $data = array_merge( + $base_data, + $this->fetch_fields_using_getters( $product, $context, $fields ) + ); + + return $data; + } + + /** + * Prepare links for the request. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * + * @return array Links for the given post. + */ + protected function prepare_links( $object, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), // @codingStandardsIgnoreLine. + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), // @codingStandardsIgnoreLine. + ), + ); + + if ( $object->get_parent_id() ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $object->get_parent_id() ) ), // @codingStandardsIgnoreLine. + ); + } + + return $links; + } + + /** + * Prepare a single product for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + + // Type is the most important part here because we need to be using the correct class and methods. + if ( isset( $request['type'] ) ) { + $classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] ); + + if ( ! class_exists( $classname ) ) { + $classname = 'WC_Product_Simple'; + } + + $product = new $classname( $id ); + } elseif ( isset( $request['id'] ) ) { + $product = wc_get_product( $id ); + } else { + $product = new WC_Product_Simple(); + } + + if ( 'variation' === $product->get_type() ) { + return new WP_Error( + "woocommerce_rest_invalid_{$this->post_type}_id", + __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), + array( + 'status' => 404, + ) + ); + } + + // Post title. + if ( isset( $request['name'] ) ) { + $product->set_name( wp_filter_post_kses( $request['name'] ) ); + } + + // Post content. + if ( isset( $request['description'] ) ) { + $product->set_description( wp_filter_post_kses( $request['description'] ) ); + } + + // Post excerpt. + if ( isset( $request['short_description'] ) ) { + $product->set_short_description( wp_filter_post_kses( $request['short_description'] ) ); + } + + // Post status. + if ( isset( $request['status'] ) ) { + $product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); + } + + // Post slug. + if ( isset( $request['slug'] ) ) { + $product->set_slug( $request['slug'] ); + } + + // Menu order. + if ( isset( $request['menu_order'] ) ) { + $product->set_menu_order( $request['menu_order'] ); + } + + // Comment status. + if ( isset( $request['reviews_allowed'] ) ) { + $product->set_reviews_allowed( $request['reviews_allowed'] ); + } + + // Virtual. + if ( isset( $request['virtual'] ) ) { + $product->set_virtual( $request['virtual'] ); + } + + // Tax status. + if ( isset( $request['tax_status'] ) ) { + $product->set_tax_status( $request['tax_status'] ); + } + + // Tax Class. + if ( isset( $request['tax_class'] ) ) { + $product->set_tax_class( $request['tax_class'] ); + } + + // Catalog Visibility. + if ( isset( $request['catalog_visibility'] ) ) { + $product->set_catalog_visibility( $request['catalog_visibility'] ); + } + + // Purchase Note. + if ( isset( $request['purchase_note'] ) ) { + $product->set_purchase_note( wp_kses_post( wp_unslash( $request['purchase_note'] ) ) ); + } + + // Featured Product. + if ( isset( $request['featured'] ) ) { + $product->set_featured( $request['featured'] ); + } + + // Shipping data. + $product = $this->save_product_shipping_data( $product, $request ); + + // SKU. + if ( isset( $request['sku'] ) ) { + $product->set_sku( wc_clean( $request['sku'] ) ); + } + + // Attributes. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + + foreach ( $request['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = wc_clean( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( $attribute_id ) { + + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $attribute['options'] ) ) { + // Text based attributes - Posted values are term names. + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = array(); + } + + if ( ! empty( $values ) ) { + // Add attribute to array, but don't set values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } elseif ( isset( $attribute['options'] ) ) { + // Custom attribute - Add attribute to array and set the values. + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + } else { + $values = explode( WC_DELIMITER, $attribute['options'] ); + } + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } + $product->set_attributes( $attributes ); + } + + // Sales and prices. + if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) { + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->set_price( '' ); + } else { + // Regular Price. + if ( isset( $request['regular_price'] ) ) { + $product->set_regular_price( $request['regular_price'] ); + } + + // Sale Price. + if ( isset( $request['sale_price'] ) ) { + $product->set_sale_price( $request['sale_price'] ); + } + + if ( isset( $request['date_on_sale_from'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from'] ); + } + + if ( isset( $request['date_on_sale_from_gmt'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null ); + } + + if ( isset( $request['date_on_sale_to'] ) ) { + $product->set_date_on_sale_to( $request['date_on_sale_to'] ); + } + + if ( isset( $request['date_on_sale_to_gmt'] ) ) { + $product->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null ); + } + } + + // Product parent ID. + if ( isset( $request['parent_id'] ) ) { + $product->set_parent_id( $request['parent_id'] ); + } + + // Sold individually. + if ( isset( $request['sold_individually'] ) ) { + $product->set_sold_individually( $request['sold_individually'] ); + } + + // Stock status. + if ( isset( $request['in_stock'] ) ) { + $stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock'; + } else { + $stock_status = $product->get_stock_status(); + } + + // Stock data. + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $request['manage_stock'] ) ) { + $product->set_manage_stock( $request['manage_stock'] ); + } + + // Backorders. + if ( isset( $request['backorders'] ) ) { + $product->set_backorders( $request['backorders'] ); + } + + if ( $product->is_type( 'grouped' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } elseif ( $product->is_type( 'external' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( 'instock' ); + } elseif ( $product->get_manage_stock() ) { + // Stock status is always determined by children so sync later. + if ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Stock quantity. + if ( isset( $request['stock_quantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) ); + } elseif ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); + } + } else { + // Don't manage stock. + $product->set_manage_stock( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } + } elseif ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Upsells. + if ( isset( $request['upsell_ids'] ) ) { + $upsells = array(); + $ids = $request['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + } + + $product->set_upsell_ids( $upsells ); + } + + // Cross sells. + if ( isset( $request['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $request['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + } + + $product->set_cross_sell_ids( $crosssells ); + } + + // Product categories. + if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['categories'] ); + } + + // Product tags. + if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' ); + } + + // Downloadable. + if ( isset( $request['downloadable'] ) ) { + $product->set_downloadable( $request['downloadable'] ); + } + + // Downloadable options. + if ( $product->get_downloadable() ) { + + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $product = $this->save_downloadable_files( $product, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + $product->set_download_limit( $request['download_limit'] ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + $product->set_download_expiry( $request['download_expiry'] ); + } + } + + // Product url and button text for external products. + if ( $product->is_type( 'external' ) ) { + if ( isset( $request['external_url'] ) ) { + $product->set_product_url( $request['external_url'] ); + } + + if ( isset( $request['button_text'] ) ) { + $product->set_button_text( $request['button_text'] ); + } + } + + // Save default attributes for variable products. + if ( $product->is_type( 'variable' ) ) { + $product = $this->save_default_attributes( $product, $request ); + } + + // Set children for a grouped product. + if ( $product->is_type( 'grouped' ) && isset( $request['grouped_products'] ) ) { + $product->set_children( $request['grouped_products'] ); + } + + // Check for featured/gallery images, upload it and set it. + if ( isset( $request['images'] ) ) { + $product = $this->set_product_images( $product, $request['images'] ); + } + + // Allow set meta_data. + if ( is_array( $request['meta_data'] ) ) { + foreach ( $request['meta_data'] as $meta ) { + $product->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $product Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $product, $request, $creating ); + } + + /** + * Set product images. + * + * @param WC_Product $product Product instance. + * @param array $images Images data. + * + * @throws WC_REST_Exception REST API exceptions. + * @return WC_Product + */ + protected function set_product_images( $product, $images ) { + $images = is_array( $images ) ? array_filter( $images ) : array(); + + if ( ! empty( $images ) ) { + $gallery_positions = array(); + + foreach ( $images as $index => $image ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } else { + continue; + } + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); + } + + if ( ! wp_attachment_is_image( $attachment_id ) ) { + /* translators: %s: attachment id */ + throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); + } + + $gallery_positions[ $attachment_id ] = absint( isset( $image['position'] ) ? $image['position'] : $index ); + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image name if present. + if ( ! empty( $image['name'] ) ) { + wp_update_post( + array( + 'ID' => $attachment_id, + 'post_title' => $image['name'], + ) + ); + } + + // Set the image source if present, for future reference. + if ( ! empty( $image['src'] ) ) { + update_post_meta( $attachment_id, '_wc_attachment_source', esc_url_raw( $image['src'] ) ); + } + } + + // Sort images and get IDs in correct order. + asort( $gallery_positions ); + + // Get gallery in correct order. + $gallery = array_keys( $gallery_positions ); + + // Featured image is in position 0. + $image_id = array_shift( $gallery ); + + // Set images. + $product->set_image_id( $image_id ); + $product->set_gallery_image_ids( $gallery ); + } else { + $product->set_image_id( '' ); + $product->set_gallery_image_ids( array() ); + } + + return $product; + } + + /** + * Save product shipping data. + * + * @param WC_Product $product Product instance. + * @param array $data Shipping data. + * + * @return WC_Product + */ + protected function save_product_shipping_data( $product, $data ) { + // Virtual. + if ( isset( $data['virtual'] ) && true === $data['virtual'] ) { + $product->set_weight( '' ); + $product->set_height( '' ); + $product->set_length( '' ); + $product->set_width( '' ); + } else { + if ( isset( $data['weight'] ) ) { + $product->set_weight( $data['weight'] ); + } + + // Height. + if ( isset( $data['dimensions']['height'] ) ) { + $product->set_height( $data['dimensions']['height'] ); + } + + // Width. + if ( isset( $data['dimensions']['width'] ) ) { + $product->set_width( $data['dimensions']['width'] ); + } + + // Length. + if ( isset( $data['dimensions']['length'] ) ) { + $product->set_length( $data['dimensions']['length'] ); + } + } + + // Shipping class. + if ( isset( $data['shipping_class'] ) ) { + $data_store = $product->get_data_store(); + $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); + $product->set_shipping_class_id( $shipping_class_id ); + } + + return $product; + } + + /** + * Save downloadable files. + * + * @param WC_Product $product Product instance. + * @param array $downloads Downloads data. + * @param int $deprecated Deprecated since 3.0. + * + * @return WC_Product + */ + protected function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { + if ( $deprecated ) { + wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() not requires a variation_id anymore.' ); + } + + $files = array(); + foreach ( $downloads as $key => $file ) { + if ( empty( $file['file'] ) ) { + continue; + } + + $download = new WC_Product_Download(); + $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); + $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); + $files[] = $download; + } + $product->set_downloads( $files ); + + return $product; + } + + /** + * Save taxonomy terms. + * + * @param WC_Product $product Product instance. + * @param array $terms Terms data. + * @param string $taxonomy Taxonomy name. + * + * @return WC_Product + */ + protected function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) { + $term_ids = wp_list_pluck( $terms, 'id' ); + + if ( 'cat' === $taxonomy ) { + $product->set_category_ids( $term_ids ); + } elseif ( 'tag' === $taxonomy ) { + $product->set_tag_ids( $term_ids ); + } + + return $product; + } + + /** + * Save default attributes. + * + * @param WC_Product $product Product instance. + * @param WP_REST_Request $request Request data. + * + * @since 3.0.0 + * @return WC_Product + */ + protected function save_default_attributes( $product, $request ) { + if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { + + $attributes = $product->get_attributes(); + $default_attributes = array(); + + foreach ( $request['default_attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( isset( $attributes[ $attribute_name ] ) ) { + $_attribute = $attributes[ $attribute_name ]; + + if ( $_attribute['is_variation'] ) { + $value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + + if ( ! empty( $_attribute['is_taxonomy'] ) ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $value = $term->slug; + } else { + $value = sanitize_title( $value ); + } + } + + if ( $value ) { + $default_attributes[ $attribute_name ] = $value; + } + } + } + } + + $product->set_default_attributes( $default_attributes ); + } + + return $product; + } + + /** + * Clear caches here so in sync with any new variations/children. + * + * @param WC_Data $object Object data. + */ + public function clear_transients( $object ) { + wc_delete_product_transients( $object->get_id() ); + wp_cache_delete( 'product-' . $object->get_id(), 'products' ); + } + + /** + * Delete a single item. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $force = (bool) $request['force']; + $object = $this->get_object( (int) $request['id'] ); + $result = false; + + if ( ! $object || 0 === $object->get_id() ) { + return new WP_Error( + "woocommerce_rest_{$this->post_type}_invalid_id", + __( 'Invalid ID.', 'woocommerce' ), + array( + 'status' => 404, + ) + ); + } + + if ( 'variation' === $object->get_type() ) { + return new WP_Error( + "woocommerce_rest_invalid_{$this->post_type}_id", + __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), + array( + 'status' => 404, + ) + ); + } + + $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) ); + + /** + * Filter whether an object is trashable. + * + * Return false to disable trash support for the object. + * + * @param boolean $supports_trash Whether the object type support trashing. + * @param WC_Data $object The object being considered for trashing support. + */ + $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object ); + + if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { + return new WP_Error( + "woocommerce_rest_user_cannot_delete_{$this->post_type}", + /* translators: %s: post type */ + sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), + array( + 'status' => rest_authorization_required_code(), + ) + ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_object_for_response( $object, $request ); + + // If we're forcing, then delete permanently. + if ( $force ) { + if ( $object->is_type( 'variable' ) ) { + foreach ( $object->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->delete( true ); + } + } + } else { + // For other product types, if the product has children, remove the relationship. + foreach ( $object->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->set_parent_id( 0 ); + $child->save(); + } + } + } + + $object->delete( true ); + $result = 0 === $object->get_id(); + } else { + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + return new WP_Error( + 'woocommerce_rest_trash_not_supported', + /* translators: %s: post type */ + sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), + array( + 'status' => 501, + ) + ); + } + + // Otherwise, only trash if we haven't already. + if ( is_callable( array( $object, 'get_status' ) ) ) { + if ( 'trash' === $object->get_status() ) { + return new WP_Error( + 'woocommerce_rest_already_trashed', + /* translators: %s: post type */ + sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), + array( + 'status' => 410, + ) + ); + } + + $object->delete(); + $result = 'trash' === $object->get_status(); + } + } + + if ( ! $result ) { + return new WP_Error( + 'woocommerce_rest_cannot_delete', + /* translators: %s: post type */ + sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), + array( + 'status' => 500, + ) + ); + } + + // Delete parent product transients. + if ( 0 !== $object->get_parent_id() ) { + wc_delete_product_transients( $object->get_parent_id() ); + } + + /** + * Fires after a single object is deleted or trashed via the REST API. + * + * @param WC_Data $object The deleted or trashed object. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request ); + + return $response; + } + + /** + * Get the Product's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $weight_unit = get_option( 'woocommerce_weight_unit' ); + $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'slug' => array( + 'description' => __( 'Product slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'permalink' => array( + 'description' => __( 'Product URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the product was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the product was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'Product type.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'simple', + 'enum' => array_keys( wc_get_product_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Product status (post status).', 'woocommerce' ), + 'type' => 'string', + 'default' => 'publish', + 'enum' => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ), + 'context' => array( 'view', 'edit' ), + ), + 'featured' => array( + 'description' => __( 'Featured product.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'catalog_visibility' => array( + 'description' => __( 'Catalog visibility.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'visible', + 'enum' => array( 'visible', 'catalog', 'search', 'hidden' ), + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Product description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'short_description' => array( + 'description' => __( 'Product short description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sku' => array( + 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price' => array( + 'description' => __( 'Current product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Product regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Product sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from' => array( + 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from_gmt' => array( + 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to_gmt' => array( + 'description' => __( 'End date of sale price, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'price_html' => array( + 'description' => __( 'Price formatted in HTML.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'on_sale' => array( + 'description' => __( 'Shows if the product is on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'purchasable' => array( + 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_sales' => array( + 'description' => __( 'Amount of sales.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'virtual' => array( + 'description' => __( 'If the product is virtual.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloadable' => array( + 'description' => __( 'If the product is downloadable.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloads' => array( + 'description' => __( 'List of downloadable files.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'File ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'external_url' => array( + 'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'button_text' => array( + 'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'taxable', + 'enum' => array( 'taxable', 'shipping', 'none' ), + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'manage_stock' => array( + 'description' => __( 'Stock management at product level.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'stock_quantity' => array( + 'description' => __( 'Stock quantity.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'in_stock' => array( + 'description' => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'backorders' => array( + 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'no', + 'enum' => array( 'no', 'notify', 'yes' ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders_allowed' => array( + 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'backordered' => array( + 'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sold_individually' => array( + 'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'weight' => array( + /* translators: %s: weight unit */ + 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'dimensions' => array( + 'description' => __( 'Product dimensions.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping_required' => array( + 'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_taxable' => array( + 'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_class' => array( + 'description' => __( 'Shipping class slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'shipping_class_id' => array( + 'description' => __( 'Shipping class ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'reviews_allowed' => array( + 'description' => __( 'Allow reviews.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'average_rating' => array( + 'description' => __( 'Reviews average rating.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rating_count' => array( + 'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'related_ids' => array( + 'description' => __( 'List of related products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'upsell_ids' => array( + 'description' => __( 'List of up-sell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'cross_sell_ids' => array( + 'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'parent_id' => array( + 'description' => __( 'Product parent ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'purchase_note' => array( + 'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'categories' => array( + 'description' => __( 'List of categories.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Category ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Category name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Category slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'tags' => array( + 'description' => __( 'List of tags.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tag ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Tag name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Tag slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'images' => array( + 'description' => __( 'List of images.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'attributes' => array( + 'description' => __( 'List of attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Attribute position.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'visible' => array( + 'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'variation' => array( + 'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'options' => array( + 'description' => __( 'List of available term names of the attribute.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'string', + ), + ), + ), + ), + ), + 'default_attributes' => array( + 'description' => __( 'Defaults variation attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'option' => array( + 'description' => __( 'Selected attribute term name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'variations' => array( + 'description' => __( 'List of variations IDs.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'integer', + ), + 'readonly' => true, + ), + 'grouped_products' => array( + 'description' => __( 'List of grouped products ID.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['orderby']['enum'] = array_merge( $params['orderby']['enum'], array( 'menu_order' ) ); + + $params['slug'] = array( + 'description' => __( 'Limit result set to products with a specific slug.', 'woocommerce' ), + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to products assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'any', 'future', 'trash' ), array_keys( get_post_statuses() ) ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['type'] = array( + 'description' => __( 'Limit result set to products assigned a specific type.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_keys( wc_get_product_types() ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['sku'] = array( + 'description' => __( 'Limit result set to products with specific SKU(s). Use commas to separate.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['featured'] = array( + 'description' => __( 'Limit result set to featured products.', 'woocommerce' ), + 'type' => 'boolean', + 'sanitize_callback' => 'wc_string_to_bool', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['category'] = array( + 'description' => __( 'Limit result set to products assigned a specific category ID.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['tag'] = array( + 'description' => __( 'Limit result set to products assigned a specific tag ID.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['shipping_class'] = array( + 'description' => __( 'Limit result set to products assigned a specific shipping class ID.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['attribute'] = array( + 'description' => __( 'Limit result set to products with a specific attribute. Use the taxonomy name/attribute slug.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['attribute_term'] = array( + 'description' => __( 'Limit result set to products with a specific attribute term ID (required an assigned attribute).', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + + if ( wc_tax_enabled() ) { + $params['tax_class'] = array( + 'description' => __( 'Limit result set to products with a specific tax class.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + } + + $params['in_stock'] = array( + 'description' => __( 'Limit result set to products in stock or out of stock.', 'woocommerce' ), + 'type' => 'boolean', + 'sanitize_callback' => 'wc_string_to_bool', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['on_sale'] = array( + 'description' => __( 'Limit result set to products on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'sanitize_callback' => 'wc_string_to_bool', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['min_price'] = array( + 'description' => __( 'Limit result set to products based on a minimum price.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['max_price'] = array( + 'description' => __( 'Limit result set to products based on a maximum price.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-report-sales-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-report-sales-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-report-sales-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-report-sales-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-report-top-sellers-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-report-top-sellers-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-report-top-sellers-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-report-top-sellers-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-reports-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-reports-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-reports-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-reports-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-setting-options-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-setting-options-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-setting-options-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-setting-options-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-settings-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-settings-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-settings-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-settings-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-methods-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-methods-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-shipping-methods-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-methods-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-locations-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-locations-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-locations-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-locations-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-methods-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-methods-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-methods-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-methods-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zones-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zones-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zones-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zones-v2-controller.php diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php new file mode 100644 index 00000000000..4b8a1db0f4b --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php @@ -0,0 +1,645 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\w-]+)', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + ), + 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 ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to view system status tools. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check whether a given request has permission to view a specific system status tool. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check whether a given request has permission to execute a specific system status tool. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'system_status', 'edit' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * A list of available tools for use in the system status section. + * 'button' becomes 'action' in the API. + * + * @return array + */ + public function get_tools() { + $tools = array( + 'clear_transients' => array( + 'name' => __( 'WooCommerce transients', 'woocommerce' ), + 'button' => __( 'Clear transients', 'woocommerce' ), + 'desc' => __( 'This tool will clear the product/shop transients cache.', 'woocommerce' ), + ), + 'clear_expired_transients' => array( + 'name' => __( 'Expired transients', 'woocommerce' ), + 'button' => __( 'Clear transients', 'woocommerce' ), + 'desc' => __( 'This tool will clear ALL expired transients from WordPress.', 'woocommerce' ), + ), + 'delete_orphaned_variations' => array( + 'name' => __( 'Orphaned variations', 'woocommerce' ), + 'button' => __( 'Delete orphaned variations', 'woocommerce' ), + 'desc' => __( 'This tool will delete all variations which have no parent.', 'woocommerce' ), + ), + 'clear_expired_download_permissions' => array( + 'name' => __( 'Used-up download permissions', 'woocommerce' ), + 'button' => __( 'Clean up download permissions', 'woocommerce' ), + 'desc' => __( 'This tool will delete expired download permissions and permissions with 0 remaining downloads.', 'woocommerce' ), + ), + 'regenerate_product_lookup_tables' => array( + 'name' => __( 'Product lookup tables', 'woocommerce' ), + 'button' => __( 'Regenerate', 'woocommerce' ), + 'desc' => __( 'This tool will regenerate product lookup table data. This process may take a while.', 'woocommerce' ), + ), + 'recount_terms' => array( + 'name' => __( 'Term counts', 'woocommerce' ), + 'button' => __( 'Recount terms', 'woocommerce' ), + 'desc' => __( 'This tool will recount product terms - useful when changing your settings in a way which hides products from the catalog.', 'woocommerce' ), + ), + 'reset_roles' => array( + 'name' => __( 'Capabilities', 'woocommerce' ), + 'button' => __( 'Reset capabilities', 'woocommerce' ), + 'desc' => __( 'This tool will reset the admin, customer and shop_manager roles to default. Use this if your users cannot access all of the WooCommerce admin pages.', 'woocommerce' ), + ), + 'clear_sessions' => array( + 'name' => __( 'Clear customer sessions', 'woocommerce' ), + 'button' => __( 'Clear', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + __( 'This tool will delete all customer session data from the database, including current carts and saved carts in the database.', 'woocommerce' ) + ), + ), + 'clear_template_cache' => array( + 'name' => __( 'Clear template cache', 'woocommerce' ), + 'button' => __( 'Clear', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + __( 'This tool will empty the template cache.', 'woocommerce' ) + ), + ), + 'install_pages' => array( + 'name' => __( 'Create default WooCommerce pages', 'woocommerce' ), + 'button' => __( 'Create pages', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + __( 'This tool will install all the missing WooCommerce pages. Pages already defined and set up will not be replaced.', 'woocommerce' ) + ), + ), + 'delete_taxes' => array( + 'name' => __( 'Delete WooCommerce tax rates', 'woocommerce' ), + 'button' => __( 'Delete tax rates', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + __( 'This option will delete ALL of your tax rates, use with caution. This action cannot be reversed.', 'woocommerce' ) + ), + ), + 'regenerate_thumbnails' => array( + 'name' => __( 'Regenerate shop thumbnails', 'woocommerce' ), + 'button' => __( 'Regenerate', 'woocommerce' ), + 'desc' => __( 'This will regenerate all shop thumbnails to match your theme and/or image settings.', 'woocommerce' ), + ), + 'db_update_routine' => array( + 'name' => __( 'Update database', 'woocommerce' ), + 'button' => __( 'Update database', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + __( 'This tool will update your WooCommerce database to the latest version. Please ensure you make sufficient backups before proceeding.', 'woocommerce' ) + ), + ), + ); + if ( method_exists( 'WC_Install', 'verify_base_tables' ) ) { + $tools['verify_db_tables'] = array( + 'name' => __( 'Verify base database tables', 'woocommerce' ), + 'button' => __( 'Verify database', 'woocommerce' ), + 'desc' => sprintf( + __( 'Verify if all base database tables are present.', 'woocommerce' ) + ), + ); + } + + // Jetpack does the image resizing heavy lifting so you don't have to. + if ( ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'photon' ) ) || ! apply_filters( 'woocommerce_background_image_regeneration', true ) ) { + unset( $tools['regenerate_thumbnails'] ); + } + + if ( ! function_exists( 'wc_clear_template_cache' ) ) { + unset( $tools['clear_template_cache'] ); + } + + return apply_filters( 'woocommerce_debug_tools', $tools ); + } + + /** + * Get a list of system status tools. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $tools = array(); + foreach ( $this->get_tools() as $id => $tool ) { + $tools[] = $this->prepare_response_for_collection( + $this->prepare_item_for_response( + array( + 'id' => $id, + 'name' => $tool['name'], + 'action' => $tool['button'], + 'description' => $tool['desc'], + ), + $request + ) + ); + } + + $response = rest_ensure_response( $tools ); + return $response; + } + + /** + * Return a single tool. + * + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $tools = $this->get_tools(); + if ( empty( $tools[ $request['id'] ] ) ) { + return new WP_Error( 'woocommerce_rest_system_status_tool_invalid_id', __( 'Invalid tool ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + $tool = $tools[ $request['id'] ]; + return rest_ensure_response( + $this->prepare_item_for_response( + array( + 'id' => $request['id'], + 'name' => $tool['name'], + 'action' => $tool['button'], + 'description' => $tool['desc'], + ), + $request + ) + ); + } + + /** + * Update (execute) a tool. + * + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $tools = $this->get_tools(); + if ( empty( $tools[ $request['id'] ] ) ) { + return new WP_Error( 'woocommerce_rest_system_status_tool_invalid_id', __( 'Invalid tool ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $tool = $tools[ $request['id'] ]; + $tool = array( + 'id' => $request['id'], + 'name' => $tool['name'], + 'action' => $tool['button'], + 'description' => $tool['desc'], + ); + + $execute_return = $this->execute_tool( $request['id'] ); + $tool = array_merge( $tool, $execute_return ); + + /** + * Fires after a WooCommerce REST system status tool has been executed. + * + * @param array $tool Details about the tool that has been executed. + * @param WP_REST_Request $request The current WP_REST_Request object. + */ + do_action( 'woocommerce_rest_insert_system_status_tool', $tool, $request ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tool, $request ); + return rest_ensure_response( $response ); + } + + /** + * Prepare a tool item for serialization. + * + * @param array $item Object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $item, $request ) { + $context = empty( $request['context'] ) ? 'view' : $request['context']; + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item['id'] ) ); + + return $response; + } + + /** + * Get the system status tools schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'system_status_tool', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'A unique identifier for the tool.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'name' => array( + 'description' => __( 'Tool name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'action' => array( + 'description' => __( 'What running the tool will do.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'description' => array( + 'description' => __( 'Tool description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'success' => array( + 'description' => __( 'Did the tool run successfully?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'edit' ), + ), + 'message' => array( + 'description' => __( 'Tool return message.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Prepare links for the request. + * + * @param string $id ID. + * @return array + */ + protected function prepare_links( $id ) { + $base = '/' . $this->namespace . '/' . $this->rest_base; + $links = array( + 'item' => array( + 'href' => rest_url( trailingslashit( $base ) . $id ), + 'embeddable' => true, + ), + ); + + return $links; + } + + /** + * Get any query params needed. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } + + /** + * Actually executes a tool. + * + * @param string $tool Tool. + * @return array + */ + public function execute_tool( $tool ) { + global $wpdb; + $ran = true; + switch ( $tool ) { + case 'clear_transients': + wc_delete_product_transients(); + wc_delete_shop_order_transients(); + delete_transient( 'wc_count_comments' ); + delete_transient( 'as_comment_count' ); + + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + if ( $attribute_taxonomies ) { + foreach ( $attribute_taxonomies as $attribute ) { + delete_transient( 'wc_layered_nav_counts_pa_' . $attribute->attribute_name ); + } + } + + WC_Cache_Helper::get_transient_version( 'shipping', true ); + $message = __( 'Product transients cleared', 'woocommerce' ); + break; + + case 'clear_expired_transients': + /* translators: %d: amount of expired transients */ + $message = sprintf( __( '%d transients rows cleared', 'woocommerce' ), wc_delete_expired_transients() ); + break; + + case 'delete_orphaned_variations': + // Delete orphans. + $result = absint( + $wpdb->query( + "DELETE products + FROM {$wpdb->posts} products + LEFT JOIN {$wpdb->posts} wp ON wp.ID = products.post_parent + WHERE wp.ID IS NULL AND products.post_type = 'product_variation';" + ) + ); + /* translators: %d: amount of orphaned variations */ + $message = sprintf( __( '%d orphaned variations deleted', 'woocommerce' ), $result ); + break; + + case 'clear_expired_download_permissions': + // Delete expired download permissions and ones with 0 downloads remaining. + $result = absint( + $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE ( downloads_remaining != '' AND downloads_remaining = 0 ) OR ( access_expires IS NOT NULL AND access_expires < %s )", + gmdate( 'Y-m-d', current_time( 'timestamp' ) ) + ) + ) + ); + /* translators: %d: amount of permissions */ + $message = sprintf( __( '%d permissions deleted', 'woocommerce' ), $result ); + break; + + case 'regenerate_product_lookup_tables': + if ( ! wc_update_product_lookup_tables_is_running() ) { + wc_update_product_lookup_tables(); + } + $message = __( 'Lookup tables are regenerating', 'woocommerce' ); + break; + case 'reset_roles': + // Remove then re-add caps and roles. + WC_Install::remove_roles(); + WC_Install::create_roles(); + $message = __( 'Roles successfully reset', 'woocommerce' ); + break; + + case 'recount_terms': + wc_recount_all_terms(); + $message = __( 'Terms successfully recounted', 'woocommerce' ); + break; + + case 'clear_sessions': + $wpdb->query( "TRUNCATE {$wpdb->prefix}woocommerce_sessions" ); + $result = absint( $wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key='_woocommerce_persistent_cart_" . get_current_blog_id() . "';" ) ); // WPCS: unprepared SQL ok. + wp_cache_flush(); + /* translators: %d: amount of sessions */ + $message = sprintf( __( 'Deleted all active sessions, and %d saved carts.', 'woocommerce' ), absint( $result ) ); + break; + + case 'install_pages': + WC_Install::create_pages(); + $message = __( 'All missing WooCommerce pages successfully installed', 'woocommerce' ); + break; + + case 'delete_taxes': + $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_tax_rates;" ); + $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_tax_rate_locations;" ); + + if ( method_exists( 'WC_Cache_Helper', 'invalidate_cache_group' ) ) { + WC_Cache_Helper::invalidate_cache_group( 'taxes' ); + } else { + WC_Cache_Helper::incr_cache_prefix( 'taxes' ); + } + $message = __( 'Tax rates successfully deleted', 'woocommerce' ); + break; + + case 'regenerate_thumbnails': + WC_Regenerate_Images::queue_image_regeneration(); + $message = __( 'Thumbnail regeneration has been scheduled to run in the background.', 'woocommerce' ); + break; + + case 'db_update_routine': + $blog_id = get_current_blog_id(); + // Used to fire an action added in WP_Background_Process::_construct() that calls WP_Background_Process::handle_cron_healthcheck(). + // This method will make sure the database updates are executed even if cron is disabled. Nothing will happen if the updates are already running. + do_action( 'wp_' . $blog_id . '_wc_updater_cron' ); + $message = __( 'Database upgrade routine has been scheduled to run in the background.', 'woocommerce' ); + break; + + case 'clear_template_cache': + if ( function_exists( 'wc_clear_template_cache' ) ) { + wc_clear_template_cache(); + $message = __( 'Template cache cleared.', 'woocommerce' ); + } else { + $message = __( 'The active version of WooCommerce does not support template cache clearing.', 'woocommerce' ); + $ran = false; + } + break; + + case 'verify_db_tables': + if ( ! method_exists( 'WC_Install', 'verify_base_tables' ) ) { + $message = __( 'You need WooCommerce 4.2 or newer to run this tool.', 'woocommerce' ); + $ran = false; + break; + } + // Try to manually create table again. + $missing_tables = WC_Install::verify_base_tables( true, true ); + if ( 0 === count( $missing_tables ) ) { + $message = __( 'Database verified successfully.', 'woocommerce' ); + } else { + $message = __( 'Verifying database... One or more tables are still missing: ', 'woocommerce' ); + $message .= implode( ', ', $missing_tables ); + $ran = false; + } + break; + + default: + $tools = $this->get_tools(); + if ( isset( $tools[ $tool ]['callback'] ) ) { + $callback = $tools[ $tool ]['callback']; + try { + $return = call_user_func( $callback ); + } catch ( Exception $exception ) { + $return = $exception; + } + if ( is_a( $return, Exception::class ) ) { + $callback_string = $this->get_printable_callback_name( $callback, $tool ); + $ran = false; + /* translators: %1$s: callback string, %2$s: error message */ + $message = sprintf( __( 'There was an error calling %1$s: %2$s', 'woocommerce' ), $callback_string, $return->getMessage() ); + + $logger = wc_get_logger(); + $logger->error( + sprintf( + 'Error running debug tool %s: %s', + $tool, + $return->getMessage() + ), + array( + 'source' => 'run-debug-tool', + 'tool' => $tool, + 'callback' => $callback, + 'error' => $return, + ) + ); + } elseif ( is_string( $return ) ) { + $message = $return; + } elseif ( false === $return ) { + $callback_string = $this->get_printable_callback_name( $callback, $tool ); + $ran = false; + /* translators: %s: callback string */ + $message = sprintf( __( 'There was an error calling %s', 'woocommerce' ), $callback_string ); + } else { + $message = __( 'Tool ran.', 'woocommerce' ); + } + } else { + $ran = false; + $message = __( 'There was an error calling this tool. There is no callback present.', 'woocommerce' ); + } + break; + } + + return array( + 'success' => $ran, + 'message' => $message, + ); + } + + /** + * Get a printable name for a callback. + * + * @param mixed $callback The callback to get a name for. + * @param string $default The default name, to be returned when the callback is an inline function. + * @return string A printable name for the callback. + */ + private function get_printable_callback_name( $callback, $default ) { + if ( is_array( $callback ) ) { + return get_class( $callback[0] ) . '::' . $callback[1]; + } + if ( is_string( $callback ) ) { + return $callback; + } + + return $default; + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-tax-classes-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-tax-classes-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-tax-classes-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-tax-classes-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-taxes-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-taxes-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-taxes-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-taxes-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-webhook-deliveries-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-webhook-deliveries-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-webhook-deliveries-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-webhook-deliveries-v2-controller.php diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-webhooks-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-webhooks-v2-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version2/class-wc-rest-webhooks-v2-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-webhooks-v2-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-controller.php diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller.php new file mode 100644 index 00000000000..fcfbe9b28f0 --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller.php @@ -0,0 +1,50 @@ + 405 ) ); + } + + /** + * Check if a given request has access to read an item. + * + * @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 ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to update an item. + * + * @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 ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $object->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to delete an item. + * + * @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 ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get object permalink. + * + * @param object $object Object. + * @return string + */ + protected function get_permalink( $object ) { + return ''; + } + + /** + * Prepares the object for the REST response. + * + * @since 3.0.0 + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. + */ + protected function prepare_object_for_response( $object, $request ) { + // translators: %s: Class method name. + return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Prepares one object for create or update operation. + * + * @since 3.0.0 + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure. + */ + protected function prepare_object_for_database( $request, $creating = false ) { + // translators: %s: Class method name. + return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Get a single item. + * + * @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 ( ! $object || 0 === $object->get_id() ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $data = $this->prepare_object_for_response( $object, $request ); + $response = rest_ensure_response( $data ); + + if ( $this->public ) { + $response->link_header( 'alternate', $this->get_permalink( $object ), array( 'type' => 'text/html' ) ); + } + + return $response; + } + + /** + * Save an object data. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @param bool $creating If is creating a new object. + * @return WC_Data|WP_Error + */ + protected function save_object( $request, $creating = false ) { + try { + $object = $this->prepare_object_for_database( $request, $creating ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + $object->save(); + + return $this->get_object( $object->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $object = $this->save_object( $request, true ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + try { + $this->update_additional_fields_for_object( $object, $request ); + + /** + * Fires after a single object is created or updated via the REST API. + * + * @param WC_Data $object Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating object, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}_object", $object, $request, true ); + } catch ( WC_Data_Exception $e ) { + $object->delete(); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + $object->delete(); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_object_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, $object->get_id() ) ) ); + + return $response; + } + + /** + * Update a single post. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $object = $this->get_object( (int) $request['id'] ); + + if ( ! $object || 0 === $object->get_id() ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $object = $this->save_object( $request, false ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + try { + $this->update_additional_fields_for_object( $object, $request ); + + /** + * Fires after a single object is created or updated via the REST API. + * + * @param WC_Data $object Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating object, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}_object", $object, $request, false ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_object_for_response( $object, $request ); + return rest_ensure_response( $response ); + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = array(); + $args['offset'] = $request['offset']; + $args['order'] = $request['order']; + $args['orderby'] = $request['orderby']; + $args['paged'] = $request['page']; + $args['post__in'] = $request['include']; + $args['post__not_in'] = $request['exclude']; + $args['posts_per_page'] = $request['per_page']; + $args['name'] = $request['slug']; + $args['post_parent__in'] = $request['parent']; + $args['post_parent__not_in'] = $request['parent_exclude']; + $args['s'] = $request['search']; + $args['fields'] = $this->get_fields_for_response( $request ); + + if ( 'date' === $args['orderby'] ) { + $args['orderby'] = 'date ID'; + } + + $date_query = array(); + $use_gmt = $request['dates_are_gmt']; + + if ( isset( $request['before'] ) ) { + $date_query[] = array( + 'column' => $use_gmt ? 'post_date_gmt' : 'post_date', + 'before' => $request['before'], + ); + } + + if ( isset( $request['after'] ) ) { + $date_query[] = array( + 'column' => $use_gmt ? 'post_date_gmt' : 'post_date', + 'after' => $request['after'], + ); + } + + if ( isset( $request['modified_before'] ) ) { + $date_query[] = array( + 'column' => $use_gmt ? 'post_modified_gmt' : 'post_modified', + 'before' => $request['modified_before'], + ); + } + + if ( isset( $request['modified_after'] ) ) { + $date_query[] = array( + 'column' => $use_gmt ? 'post_modified_gmt' : 'post_modified', + 'after' => $request['modified_after'], + ); + } + + if ( ! empty( $date_query ) ) { + $date_query['relation'] = 'AND'; + $args['date_query'] = $date_query; + } + + // Force the post_type argument, since it's not a user input variable. + $args['post_type'] = $this->post_type; + + /** + * Filter the query arguments for a request. + * + * Enables adding extra arguments or setting defaults for a post + * collection request. + * + * @param array $args Key value array of query var to query value. + * @param WP_REST_Request $request The request used. + */ + $args = apply_filters( "woocommerce_rest_{$this->post_type}_object_query", $args, $request ); + + return $this->prepare_items_query( $args, $request ); + } + + /** + * Get objects. + * + * @since 3.0.0 + * @param array $query_args Query args. + * @return array + */ + protected function get_objects( $query_args ) { + $query = new WP_Query(); + $result = $query->query( $query_args ); + + $total_posts = $query->found_posts; + if ( $total_posts < 1 ) { + // Out-of-bounds, run the query again without LIMIT for total count. + unset( $query_args['paged'] ); + $count_query = new WP_Query(); + $count_query->query( $query_args ); + $total_posts = $count_query->found_posts; + } + + return array( + 'objects' => array_filter( array_map( array( $this, 'get_object' ), $result ) ), + 'total' => (int) $total_posts, + 'pages' => (int) ceil( $total_posts / (int) $query->query_vars['posts_per_page'] ), + ); + } + + /** + * Get a collection of posts. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $query_args = $this->prepare_objects_query( $request ); + if ( is_wp_error( current( $query_args ) ) ) { + return current( $query_args ); + } + $query_results = $this->get_objects( $query_args ); + + $objects = array(); + foreach ( $query_results['objects'] as $object ) { + if ( ! wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) { + continue; + } + + $data = $this->prepare_object_for_response( $object, $request ); + $objects[] = $this->prepare_response_for_collection( $data ); + } + + $page = (int) $query_args['paged']; + $max_pages = $query_results['pages']; + + $response = rest_ensure_response( $objects ); + $response->header( 'X-WP-Total', $query_results['total'] ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = $this->rest_base; + $attrib_prefix = '(?P<'; + if ( strpos( $base, $attrib_prefix ) !== false ) { + $attrib_names = array(); + preg_match( '/\(\?P<[^>]+>.*\)/', $base, $attrib_names, PREG_OFFSET_CAPTURE ); + foreach ( $attrib_names as $attrib_name_match ) { + $beginning_offset = strlen( $attrib_prefix ); + $attrib_name_end = strpos( $attrib_name_match[0], '>', $attrib_name_match[1] ); + $attrib_name = substr( $attrib_name_match[0], $beginning_offset, $attrib_name_end - $beginning_offset ); + if ( isset( $request[ $attrib_name ] ) ) { + $base = str_replace( "(?P<$attrib_name>[\d]+)", $request[ $attrib_name ], $base ); + } + } + } + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ) ); + + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Delete a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $force = (bool) $request['force']; + $object = $this->get_object( (int) $request['id'] ); + $result = false; + + if ( ! $object || 0 === $object->get_id() ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) ); + + /** + * Filter whether an object is trashable. + * + * Return false to disable trash support for the object. + * + * @param boolean $supports_trash Whether the object type support trashing. + * @param WC_Data $object The object being considered for trashing support. + */ + $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object ); + + if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_object_for_response( $object, $request ); + + // If we're forcing, then delete permanently. + if ( $force ) { + $object->delete( true ); + $result = 0 === $object->get_id(); + } else { + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) ); + } + + // Otherwise, only trash if we haven't already. + if ( is_callable( array( $object, 'get_status' ) ) ) { + if ( 'trash' === $object->get_status() ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); + } + + $object->delete(); + $result = 'trash' === $object->get_status(); + } + } + + if ( ! $result ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); + } + + /** + * Fires after a single object is deleted or trashed via the REST API. + * + * @param WC_Data $object The deleted or trashed object. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request ); + + return $response; + } + + /** + * Get fields for an object if getter is defined. + * + * @param object $object Object we are fetching response for. + * @param string $context Context of the request. Can be `view` or `edit`. + * @param array $fields List of fields to fetch. + * @return array Data fetched from getters. + */ + public function fetch_fields_using_getters( $object, $context, $fields ) { + $data = array(); + foreach ( $fields as $field ) { + if ( method_exists( $this, "api_get_$field" ) ) { + $data[ $field ] = $this->{"api_get_$field"}( $object, $context ); + } + } + return $data; + } + + /** + * Prepare links for the request. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return array Links for the given post. + */ + protected function prepare_links( $object, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = array(); + $params['context'] = $this->get_context_param(); + $params['context']['default'] = 'view'; + + $params['page'] = array( + 'description' => __( 'Current page of the collection.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 1, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + 'minimum' => 1, + ); + $params['per_page'] = array( + 'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['search'] = array( + 'description' => __( 'Limit results to those matching a string.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['after'] = array( + 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['before'] = array( + 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['modified_after'] = array( + 'description' => __( 'Limit response to resources modified after a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['modified_before'] = array( + 'description' => __( 'Limit response to resources modified before a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['dates_are_gmt'] = array( + 'description' => __( 'Whether to consider GMT post dates when limiting response by published or modified date.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( 'asc', 'desc' ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'date', + 'enum' => array( + 'date', + 'id', + 'include', + 'title', + 'slug', + 'modified', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + + if ( $this->hierarchical ) { + $params['parent'] = array( + 'description' => __( 'Limit result set to those of particular parent IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'sanitize_callback' => 'wp_parse_id_list', + 'default' => array(), + ); + $params['parent_exclude'] = array( + 'description' => __( 'Limit result set to all items except those of a particular parent ID.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'sanitize_callback' => 'wp_parse_id_list', + 'default' => array(), + ); + } + + /** + * Filter collection parameters for the posts controller. + * + * The dynamic part of the filter `$this->post_type` refers to the post + * type slug for the controller. + * + * This filter registers the collection parameter, but does not map the + * collection parameter to an internal WP_Query parameter. Use the + * `rest_{$this->post_type}_query` filter to set WP_Query parameters. + * + * @param array $query_params JSON Schema-formatted collection parameters. + * @param WP_Post_Type $post_type Post type object. + */ + return apply_filters( "rest_{$this->post_type}_collection_params", $params, $this->post_type ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-customer-downloads-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-customer-downloads-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-customer-downloads-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-customer-downloads-controller.php diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-customers-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-customers-controller.php new file mode 100644 index 00000000000..217aef00b6e --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-customers-controller.php @@ -0,0 +1,312 @@ +get_data(); + $format_date = array( 'date_created', 'date_modified' ); + + // Format date values. + foreach ( $format_date as $key ) { + // Date created is stored UTC, date modified is stored WP local time. + $datetime = 'date_created' === $key ? get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $data[ $key ]->getTimestamp() ) ) : $data[ $key ]; + $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); + $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); + } + + return array( + 'id' => $object->get_id(), + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'date_modified' => $data['date_modified'], + 'date_modified_gmt' => $data['date_modified_gmt'], + 'email' => $data['email'], + 'first_name' => $data['first_name'], + 'last_name' => $data['last_name'], + 'role' => $data['role'], + 'username' => $data['username'], + 'billing' => $data['billing'], + 'shipping' => $data['shipping'], + 'is_paying_customer' => $data['is_paying_customer'], + 'avatar_url' => $object->get_avatar_url(), + 'meta_data' => $data['meta_data'], + ); + } + + /** + * Get the Customer's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'customer', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the customer was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the customer was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the customer was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'email' => array( + 'description' => __( 'The email address for the customer.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'first_name' => array( + 'description' => __( 'Customer first name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'last_name' => array( + 'description' => __( 'Customer last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'role' => array( + 'description' => __( 'Customer role.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'username' => array( + 'description' => __( 'Customer login name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_user', + ), + ), + 'password' => array( + 'description' => __( 'Customer password.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'billing' => array( + 'description' => __( 'List of billing address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Email address.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping' => array( + 'description' => __( 'List of shipping address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'is_paying_customer' => array( + 'description' => __( 'Is the customer a paying customer?', 'woocommerce' ), + 'type' => 'bool', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'avatar_url' => array( + 'description' => __( 'Avatar URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-data-continents-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-data-continents-controller.php new file mode 100644 index 00000000000..357777dcf39 --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-data-continents-controller.php @@ -0,0 +1,362 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\w-]+)', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => array( + 'continent' => array( + 'description' => __( '2 character continent code.', 'woocommerce' ), + 'type' => 'string', + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Return the list of countries and states for a given continent. + * + * @since 3.5.0 + * @param string $continent_code Continent code. + * @param WP_REST_Request $request Request data. + * @return array|mixed Response data, ready for insertion into collection data. + */ + public function get_continent( $continent_code, $request ) { + $continents = WC()->countries->get_continents(); + $countries = WC()->countries->get_countries(); + $states = WC()->countries->get_states(); + $locale_info = include WC()->plugin_path() . '/i18n/locale-info.php'; + $data = array(); + + if ( ! array_key_exists( $continent_code, $continents ) ) { + return false; + } + + $continent_list = $continents[ $continent_code ]; + + $continent = array( + 'code' => $continent_code, + 'name' => $continent_list['name'], + ); + + $local_countries = array(); + foreach ( $continent_list['countries'] as $country_code ) { + if ( isset( $countries[ $country_code ] ) ) { + $country = array( + 'code' => $country_code, + 'name' => $countries[ $country_code ], + ); + + // If we have detailed locale information include that in the response. + if ( array_key_exists( $country_code, $locale_info ) ) { + // Defensive programming against unexpected changes in locale-info.php. + $country_data = wp_parse_args( + $locale_info[ $country_code ], + array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'decimal_sep' => '.', + 'dimension_unit' => 'in', + 'num_decimals' => 2, + 'thousand_sep' => ',', + 'weight_unit' => 'lbs', + ) + ); + + $country = array_merge( $country, $country_data ); + } + + $local_states = array(); + if ( isset( $states[ $country_code ] ) ) { + foreach ( $states[ $country_code ] as $state_code => $state_name ) { + $local_states[] = array( + 'code' => $state_code, + 'name' => $state_name, + ); + } + } + $country['states'] = $local_states; + + // Allow only desired keys (e.g. filter out tax rates). + $allowed = array( + 'code', + 'currency_code', + 'currency_pos', + 'decimal_sep', + 'dimension_unit', + 'name', + 'num_decimals', + 'states', + 'thousand_sep', + 'weight_unit', + ); + $country = array_intersect_key( $country, array_flip( $allowed ) ); + + $local_countries[] = $country; + } + } + + $continent['countries'] = $local_countries; + return $continent; + } + + /** + * Return the list of states for all continents. + * + * @since 3.5.0 + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $continents = WC()->countries->get_continents(); + $data = array(); + + foreach ( array_keys( $continents ) as $continent_code ) { + $continent = $this->get_continent( $continent_code, $request ); + $response = $this->prepare_item_for_response( $continent, $request ); + $data[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $data ); + } + + /** + * Return the list of locations for a given continent. + * + * @since 3.5.0 + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $data = $this->get_continent( strtoupper( $request['location'] ), $request ); + if ( empty( $data ) ) { + return new WP_Error( 'woocommerce_rest_data_invalid_location', __( 'There are no locations matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) ); + } + return $this->prepare_item_for_response( $data, $request ); + } + + /** + * Prepare the data object for response. + * + * @since 3.5.0 + * @param object $item Data object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $item, $request ) { + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, 'view' ); + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item ) ); + + /** + * Filter the location list returned from the API. + * + * Allows modification of the location data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param array $item The original list of continent(s), countries, and states. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_data_continent', $response, $item, $request ); + } + + /** + * Prepare links for the request. + * + * @param object $item Data object. + * @return array Links for the given continent. + */ + protected function prepare_links( $item ) { + $continent_code = strtolower( $item['code'] ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $continent_code ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + return $links; + } + + /** + * Get the location schema, conforming to JSON Schema. + * + * @since 3.5.0 + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'data_continents', + 'type' => 'object', + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( '2 character continent code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of continent.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'countries' => array( + 'type' => 'array', + 'description' => __( 'List of countries on this continent.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'currency_code' => array( + 'type' => 'string', + 'description' => __( 'Default ISO4127 alpha-3 currency code for the country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'currency_pos' => array( + 'type' => 'string', + 'description' => __( 'Currency symbol position for this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'decimal_sep' => array( + 'type' => 'string', + 'description' => __( 'Decimal separator for displayed prices for this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'dimension_unit' => array( + 'type' => 'string', + 'description' => __( 'The unit lengths are defined in for this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'num_decimals' => array( + 'type' => 'integer', + 'description' => __( 'Number of decimal points shown in displayed prices for this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'states' => array( + 'type' => 'array', + 'description' => __( 'List of states in this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( 'State code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of state.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + ), + 'thousand_sep' => array( + 'type' => 'string', + 'description' => __( 'Thousands separator for displayed prices in this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'weight_unit' => array( + 'type' => 'string', + 'description' => __( 'The unit weights are defined in for this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-data-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-data-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-data-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-data-controller.php diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-data-countries-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-data-countries-controller.php new file mode 100644 index 00000000000..b9783bc2d38 --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-data-countries-controller.php @@ -0,0 +1,244 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\w-]+)', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => array( + 'location' => array( + 'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ), + 'type' => 'string', + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Get a list of countries and states. + * + * @param string $country_code Country code. + * @param WP_REST_Request $request Request data. + * @return array|mixed Response data, ready for insertion into collection data. + */ + public function get_country( $country_code, $request ) { + $countries = WC()->countries->get_countries(); + $states = WC()->countries->get_states(); + $data = array(); + + if ( ! array_key_exists( $country_code, $countries ) ) { + return false; + } + + $country = array( + 'code' => $country_code, + 'name' => $countries[ $country_code ], + ); + + $local_states = array(); + if ( isset( $states[ $country_code ] ) ) { + foreach ( $states[ $country_code ] as $state_code => $state_name ) { + $local_states[] = array( + 'code' => $state_code, + 'name' => $state_name, + ); + } + } + $country['states'] = $local_states; + return $country; + } + + /** + * Return the list of states for all countries. + * + * @since 3.5.0 + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $countries = WC()->countries->get_countries(); + $data = array(); + + foreach ( array_keys( $countries ) as $country_code ) { + $country = $this->get_country( $country_code, $request ); + $response = $this->prepare_item_for_response( $country, $request ); + $data[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $data ); + } + + /** + * Return the list of states for a given country. + * + * @since 3.5.0 + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $data = $this->get_country( strtoupper( $request['location'] ), $request ); + if ( empty( $data ) ) { + return new WP_Error( 'woocommerce_rest_data_invalid_location', __( 'There are no locations matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) ); + } + return $this->prepare_item_for_response( $data, $request ); + } + + /** + * Prepare the data object for response. + * + * @since 3.5.0 + * @param object $item Data object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $item, $request ) { + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, 'view' ); + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item ) ); + + /** + * Filter the states list for a country returned from the API. + * + * Allows modification of the location data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param array $data The original country's states list. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_data_country', $response, $item, $request ); + } + + /** + * Prepare links for the request. + * + * @param object $item Data object. + * @return array Links for the given country. + */ + protected function prepare_links( $item ) { + $country_code = strtolower( $item['code'] ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $country_code ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + + /** + * Get the location schema, conforming to JSON Schema. + * + * @since 3.5.0 + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'data_countries', + 'type' => 'object', + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'states' => array( + 'type' => 'array', + 'description' => __( 'List of states in this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( 'State code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of state.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-data-currencies-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-data-currencies-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-data-currencies-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-data-currencies-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-network-orders-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-network-orders-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-network-orders-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-network-orders-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-order-notes-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-order-notes-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-order-notes-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-order-notes-controller.php diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller.php new file mode 100644 index 00000000000..bd021942688 --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller.php @@ -0,0 +1,122 @@ +/refunds endpoint. + * + * @package WooCommerce\RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +use Automattic\WooCommerce\Internal\RestApiUtil; + +/** + * REST API Order Refunds controller class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Order_Refunds_V2_Controller + */ +class WC_REST_Order_Refunds_Controller extends WC_REST_Order_Refunds_V2_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v3'; + + /** + * Prepares one object for create or update operation. + * + * @since 3.0.0 + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure. + */ + protected function prepare_object_for_database( $request, $creating = false ) { + RestApiUtil::adjust_create_refund_request_parameters( $request ); + + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order ) { + return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); + } + + if ( 0 > $request['amount'] ) { + return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 ); + } + + // Create the refund. + $refund = wc_create_refund( + array( + 'order_id' => $order->get_id(), + 'amount' => $request['amount'], + 'reason' => $request['reason'], + 'line_items' => $request['line_items'], + 'refund_payment' => $request['api_refund'], + 'restock_items' => $request['api_restock'], + ) + ); + + if ( is_wp_error( $refund ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 ); + } + + if ( ! $refund ) { + return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); + } + + if ( ! empty( $request['meta_data'] ) && is_array( $request['meta_data'] ) ) { + foreach ( $request['meta_data'] as $meta ) { + $refund->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + $refund->save_meta_data(); + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $coupon Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $refund, $request, $creating ); + } + + /** + * Get the refund schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = parent::get_item_schema(); + + $schema['properties']['line_items']['items']['properties']['refund_total'] = array( + 'description' => __( 'Amount that will be refunded for this line item (excluding taxes).', 'woocommerce' ), + 'type' => 'number', + 'context' => array( 'edit' ), + 'readonly' => true, + ); + + $schema['properties']['line_items']['items']['properties']['taxes']['items']['properties']['refund_total'] = array( + 'description' => __( 'Amount that will be refunded for this tax.', 'woocommerce' ), + 'type' => 'number', + 'context' => array( 'edit' ), + 'readonly' => true, + ); + + $schema['properties']['api_restock'] = array( + 'description' => __( 'When true, refunded items are restocked.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'edit' ), + 'default' => true, + ); + + return $schema; + } +} diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php new file mode 100644 index 00000000000..d61a52cdcb3 --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php @@ -0,0 +1,300 @@ +get_coupons() ); + $current_order_coupon_codes = array_map( + function( $coupon ) { + return $coupon->get_code(); + }, + $current_order_coupons + ); + + foreach ( $request['coupon_lines'] as $item ) { + if ( ! empty( $item['id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_coupon_item_id_readonly', __( 'Coupon item ID is readonly.', 'woocommerce' ), 400 ); + } + + if ( empty( $item['code'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + + $coupon_code = wc_format_coupon_code( wc_clean( $item['code'] ) ); + $coupon = new WC_Coupon( $coupon_code ); + + // Skip check if the coupon is already applied to the order, as this could wrongly throw an error for single-use coupons. + if ( ! in_array( $coupon_code, $current_order_coupon_codes, true ) ) { + $check_result = $discounts->is_coupon_valid( $coupon ); + if ( is_wp_error( $check_result ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_' . $check_result->get_error_code(), $check_result->get_error_message(), 400 ); + } + } + + $coupon_codes[] = $coupon_code; + } + + // Remove all coupons first to ensure calculation is correct. + foreach ( $order->get_items( 'coupon' ) as $existing_coupon ) { + $order->remove_coupon( $existing_coupon->get_code() ); + } + + // Apply the coupons. + foreach ( $coupon_codes as $new_coupon ) { + $results = $order->apply_coupon( $new_coupon ); + + if ( is_wp_error( $results ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_' . $results->get_error_code(), $results->get_error_message(), 400 ); + } + } + + return true; + } + + /** + * Prepare a single order for create or update. + * + * @throws WC_REST_Exception When fails to set any item. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $order = new WC_Order( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Handle all writable props. + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'coupon_lines': + case 'status': + // Change should be done later so transitions have new data. + break; + case 'billing': + case 'shipping': + $this->update_address( $order, $value, $key ); + break; + case 'line_items': + case 'shipping_lines': + case 'fee_lines': + if ( is_array( $value ) ) { + foreach ( $value as $item ) { + if ( is_array( $item ) ) { + if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) { + $order->remove_item( $item['id'] ); + } else { + $this->set_item( $order, $key, $item ); + } + } + } + } + break; + case 'meta_data': + if ( is_array( $value ) ) { + foreach ( $value as $meta ) { + $order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + break; + default: + if ( is_callable( array( $order, "set_{$key}" ) ) ) { + $order->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $order Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $order, $request, $creating ); + } + + /** + * Save an object data. + * + * @since 3.0.0 + * @throws WC_REST_Exception But all errors are validated before returning any data. + * @param WP_REST_Request $request Full details about the request. + * @param bool $creating If is creating a new object. + * @return WC_Data|WP_Error + */ + protected function save_object( $request, $creating = false ) { + try { + $object = $this->prepare_object_for_database( $request, $creating ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + // Make sure gateways are loaded so hooks from gateways fire on save/create. + WC()->payment_gateways(); + + if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] ) { + // Make sure customer exists. + if ( false === get_user_by( 'id', $request['customer_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + // Make sure customer is part of blog. + if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) { + add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' ); + } + } + + if ( $creating ) { + $object->set_created_via( 'rest-api' ); + $object->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); + $object->calculate_totals(); + } else { + // If items have changed, recalculate order totals. + if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) { + $object->calculate_totals( true ); + } + } + + // Set coupons. + $this->calculate_coupons( $request, $object ); + + // Set status. + if ( ! empty( $request['status'] ) ) { + $object->set_status( $request['status'] ); + } + + $object->save(); + + // Actions for after the order is saved. + if ( true === $request['set_paid'] ) { + if ( $creating || $object->needs_payment() ) { + $object->payment_complete( $request['transaction_id'] ); + } + } + + return $this->get_object( $object->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + // This is needed to get around an array to string notice in WC_REST_Orders_V2_Controller::prepare_objects_query. + $statuses = $request['status']; + unset( $request['status'] ); + $args = parent::prepare_objects_query( $request ); + + $args['post_status'] = array(); + foreach ( $statuses as $status ) { + if ( in_array( $status, $this->get_order_statuses(), true ) ) { + $args['post_status'][] = 'wc-' . $status; + } elseif ( 'any' === $status ) { + // Set status to "any" and short-circuit out. + $args['post_status'] = 'any'; + break; + } else { + $args['post_status'][] = $status; + } + } + + // Put the statuses back for further processing (next/prev links, etc). + $request['status'] = $statuses; + + return $args; + } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = parent::get_item_schema(); + + $schema['properties']['coupon_lines']['items']['properties']['discount']['readonly'] = true; + + return $schema; + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to orders which have specific statuses.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + 'enum' => array_merge( array( 'any', 'trash' ), $this->get_order_statuses() ), + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-payment-gateways-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-payment-gateways-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-payment-gateways-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-payment-gateways-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-posts-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-posts-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-posts-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-posts-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-product-attribute-terms-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-attribute-terms-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-product-attribute-terms-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-attribute-terms-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-product-attributes-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-attributes-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-product-attributes-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-attributes-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-product-categories-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-categories-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-product-categories-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-categories-controller.php diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php new file mode 100644 index 00000000000..000bcf8fb37 --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php @@ -0,0 +1,1173 @@ +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' => array_merge( + $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'product_id' => array( + 'required' => true, + 'description' => __( 'Unique identifier for the product.', 'woocommerce' ), + 'type' => 'integer', + ), + 'review' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Review content.', 'woocommerce' ), + ), + 'reviewer' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Name of the reviewer.', 'woocommerce' ), + ), + 'reviewer_email' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Email of the reviewer.', 'woocommerce' ), + ), + ) + ), + ), + '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.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 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 ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to read webhook deliveries. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_product_reviews_permissions( 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! wc_rest_check_product_reviews_permissions( 'read', (int) $request['id'] ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to create a new product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_product_reviews_permissions( 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to update a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + if ( ! wc_rest_check_product_reviews_permissions( 'edit', (int) $request['id'] ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to delete a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function delete_item_permissions_check( $request ) { + if ( ! wc_rest_check_product_reviews_permissions( 'delete', (int) $request['id'] ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access batch create, update and delete items. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean|WP_Error + */ + public function batch_items_permissions_check( $request ) { + if ( ! wc_rest_check_product_reviews_permissions( 'batch' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all reviews. + * + * @param WP_REST_Request $request Full details about the request. + * @return array|WP_Error + */ + public function get_items( $request ) { + // Retrieve the list of registered collection query parameters. + $registered = $this->get_collection_params(); + + /* + * 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( + 'reviewer' => 'author__in', + 'reviewer_email' => 'author_email', + 'reviewer_exclude' => 'author__not_in', + 'exclude' => 'comment__not_in', + 'include' => 'comment__in', + 'offset' => 'offset', + 'order' => 'order', + 'per_page' => 'number', + 'product' => 'post__in', + 'search' => 'search', + 'status' => 'status', + ); + + $prepared_args = array(); + + /* + * For each known parameter which is both registered and present in the request, + * set the parameter's value on the query $prepared_args. + */ + foreach ( $parameter_mappings as $api_param => $wp_param ) { + if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) { + $prepared_args[ $wp_param ] = $request[ $api_param ]; + } + } + + // Ensure certain parameter values default to empty strings. + foreach ( array( 'author_email', 'search' ) as $param ) { + if ( ! isset( $prepared_args[ $param ] ) ) { + $prepared_args[ $param ] = ''; + } + } + + if ( isset( $registered['orderby'] ) ) { + $prepared_args['orderby'] = $this->normalize_query_param( $request['orderby'] ); + } + + if ( isset( $prepared_args['status'] ) ) { + $prepared_args['status'] = 'approved' === $prepared_args['status'] ? 'approve' : $prepared_args['status']; + } + + $prepared_args['no_found_rows'] = false; + $prepared_args['date_query'] = array(); + + // Set before into date query. Date query must be specified as an array of an array. + if ( isset( $registered['before'], $request['before'] ) ) { + $prepared_args['date_query'][0]['before'] = $request['before']; + } + + // Set after into date query. Date query must be specified as an array of an array. + if ( isset( $registered['after'], $request['after'] ) ) { + $prepared_args['date_query'][0]['after'] = $request['after']; + } + + if ( isset( $registered['page'] ) && empty( $request['offset'] ) ) { + $prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 ); + } + + /** + * Filters arguments, before passing to WP_Comment_Query, when querying reviews via the REST API. + * + * @since 3.5.0 + * @link https://developer.wordpress.org/reference/classes/wp_comment_query/ + * @param array $prepared_args Array of arguments for WP_Comment_Query. + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( 'woocommerce_rest_product_review_query', $prepared_args, $request ); + + // Make sure that returns only reviews. + $prepared_args['type'] = 'review'; + + // Query reviews. + $query = new WP_Comment_Query(); + $query_result = $query->query( $prepared_args ); + $reviews = array(); + + foreach ( $query_result as $review ) { + if ( ! wc_rest_check_product_reviews_permissions( 'read', $review->comment_ID ) ) { + continue; + } + + $data = $this->prepare_item_for_response( $review, $request ); + $reviews[] = $this->prepare_response_for_collection( $data ); + } + + $total_reviews = (int) $query->found_comments; + $max_pages = (int) $query->max_num_pages; + + if ( $total_reviews < 1 ) { + // Out-of-bounds, run the query again without LIMIT for total count. + unset( $prepared_args['number'], $prepared_args['offset'] ); + + $query = new WP_Comment_Query(); + $prepared_args['count'] = true; + + $total_reviews = $query->query( $prepared_args ); + $max_pages = ceil( $total_reviews / $request['per_page'] ); + } + + $response = rest_ensure_response( $reviews ); + $response->header( 'X-WP-Total', $total_reviews ); + $response->header( 'X-WP-TotalPages', $max_pages ); + + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); + + if ( $request['page'] > 1 ) { + $prev_page = $request['page'] - 1; + + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + + if ( $max_pages > $request['page'] ) { + $next_page = $request['page'] + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Create a single review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + return new WP_Error( 'woocommerce_rest_review_exists', __( 'Cannot create existing product review.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $product_id = (int) $request['product_id']; + + if ( 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $prepared_review = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $prepared_review ) ) { + return $prepared_review; + } + + $prepared_review['comment_type'] = 'review'; + + /* + * Do not allow a comment to be created with missing or empty comment_content. See wp_handle_comment_submission(). + */ + if ( empty( $prepared_review['comment_content'] ) ) { + return new WP_Error( 'woocommerce_rest_review_content_invalid', __( 'Invalid review content.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Setting remaining values before wp_insert_comment so we can use wp_allow_comment(). + if ( ! isset( $prepared_review['comment_date_gmt'] ) ) { + $prepared_review['comment_date_gmt'] = current_time( 'mysql', true ); + } + + if ( ! empty( $_SERVER['REMOTE_ADDR'] ) && rest_is_ip_address( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ) { // WPCS: input var ok, sanitization ok. + $prepared_review['comment_author_IP'] = wc_clean( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); // WPCS: input var ok. + } else { + $prepared_review['comment_author_IP'] = '127.0.0.1'; + } + + if ( ! empty( $request['author_user_agent'] ) ) { + $prepared_review['comment_agent'] = $request['author_user_agent']; + } elseif ( $request->get_header( 'user_agent' ) ) { + $prepared_review['comment_agent'] = $request->get_header( 'user_agent' ); + } else { + $prepared_review['comment_agent'] = ''; + } + + $check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_review ); + if ( is_wp_error( $check_comment_lengths ) ) { + $error_code = str_replace( array( 'comment_author', 'comment_content' ), array( 'reviewer', 'review_content' ), $check_comment_lengths->get_error_code() ); + return new WP_Error( 'woocommerce_rest_' . $error_code, __( 'Product review field exceeds maximum length allowed.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $prepared_review['comment_parent'] = 0; + $prepared_review['comment_author_url'] = ''; + $prepared_review['comment_approved'] = wp_allow_comment( $prepared_review, true ); + + if ( is_wp_error( $prepared_review['comment_approved'] ) ) { + $error_code = $prepared_review['comment_approved']->get_error_code(); + $error_message = $prepared_review['comment_approved']->get_error_message(); + + if ( 'comment_duplicate' === $error_code ) { + return new WP_Error( 'woocommerce_rest_' . $error_code, $error_message, array( 'status' => 409 ) ); + } + + if ( 'comment_flood' === $error_code ) { + return new WP_Error( 'woocommerce_rest_' . $error_code, $error_message, array( 'status' => 400 ) ); + } + + return $prepared_review['comment_approved']; + } + + /** + * Filters a review before it is inserted via the REST API. + * + * Allows modification of the review right before it is inserted via wp_insert_comment(). + * Returning a WP_Error value from the filter will shortcircuit insertion and allow + * skipping further processing. + * + * @since 3.5.0 + * @param array|WP_Error $prepared_review The prepared review data for wp_insert_comment(). + * @param WP_REST_Request $request Request used to insert the review. + */ + $prepared_review = apply_filters( 'woocommerce_rest_pre_insert_product_review', $prepared_review, $request ); + if ( is_wp_error( $prepared_review ) ) { + return $prepared_review; + } + + $review_id = wp_insert_comment( wp_filter_comment( wp_slash( (array) $prepared_review ) ) ); + + if ( ! $review_id ) { + return new WP_Error( 'woocommerce_rest_review_failed_create', __( 'Creating product review failed.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + if ( isset( $request['status'] ) ) { + $this->handle_status_param( $request['status'], $review_id ); + } + + update_comment_meta( $review_id, 'rating', ! empty( $request['rating'] ) ? $request['rating'] : '0' ); + + $review = get_comment( $review_id ); + + /** + * Fires after a comment is created or updated via the REST API. + * + * @param WP_Comment $review Inserted or updated comment object. + * @param WP_REST_Request $request Request object. + * @param bool $creating True when creating a comment, false when updating. + */ + do_action( 'woocommerce_rest_insert_product_review', $review, $request, true ); + + $fields_update = $this->update_additional_fields_for_object( $review, $request ); + if ( is_wp_error( $fields_update ) ) { + return $fields_update; + } + + $context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view'; + $request->set_param( 'context', $context ); + + $response = $this->prepare_item_for_response( $review, $request ); + $response = rest_ensure_response( $response ); + + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $review_id ) ) ); + + return $response; + } + + /** + * Get a single product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $review = $this->get_review( $request['id'] ); + if ( is_wp_error( $review ) ) { + return $review; + } + + $data = $this->prepare_item_for_response( $review, $request ); + $response = rest_ensure_response( $data ); + + return $response; + } + + /** + * Updates a review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response Response object on success, or error object on failure. + */ + public function update_item( $request ) { + $review = $this->get_review( $request['id'] ); + if ( is_wp_error( $review ) ) { + return $review; + } + + $id = (int) $review->comment_ID; + + if ( isset( $request['type'] ) && 'review' !== get_comment_type( $id ) ) { + return new WP_Error( 'woocommerce_rest_review_invalid_type', __( 'Sorry, you are not allowed to change the comment type.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $prepared_args = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $prepared_args ) ) { + return $prepared_args; + } + + if ( ! empty( $prepared_args['comment_post_ID'] ) ) { + if ( 'product' !== get_post_type( (int) $prepared_args['comment_post_ID'] ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + } + + if ( empty( $prepared_args ) && isset( $request['status'] ) ) { + // Only the comment status is being changed. + $change = $this->handle_status_param( $request['status'], $id ); + + if ( ! $change ) { + return new WP_Error( 'woocommerce_rest_review_failed_edit', __( 'Updating review status failed.', 'woocommerce' ), array( 'status' => 500 ) ); + } + } elseif ( ! empty( $prepared_args ) ) { + if ( is_wp_error( $prepared_args ) ) { + return $prepared_args; + } + + if ( isset( $prepared_args['comment_content'] ) && empty( $prepared_args['comment_content'] ) ) { + return new WP_Error( 'woocommerce_rest_review_content_invalid', __( 'Invalid review content.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $prepared_args['comment_ID'] = $id; + + $check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_args ); + if ( is_wp_error( $check_comment_lengths ) ) { + $error_code = str_replace( array( 'comment_author', 'comment_content' ), array( 'reviewer', 'review_content' ), $check_comment_lengths->get_error_code() ); + return new WP_Error( 'woocommerce_rest_' . $error_code, __( 'Product review field exceeds maximum length allowed.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $updated = wp_update_comment( wp_slash( (array) $prepared_args ) ); + + if ( false === $updated ) { + return new WP_Error( 'woocommerce_rest_comment_failed_edit', __( 'Updating review failed.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + if ( isset( $request['status'] ) ) { + $this->handle_status_param( $request['status'], $id ); + } + } + + if ( ! empty( $request['rating'] ) ) { + update_comment_meta( $id, 'rating', $request['rating'] ); + } + + $review = get_comment( $id ); + + /** This action is documented in includes/api/class-wc-rest-product-reviews-controller.php */ + do_action( 'woocommerce_rest_insert_product_review', $review, $request, false ); + + $fields_update = $this->update_additional_fields_for_object( $review, $request ); + + if ( is_wp_error( $fields_update ) ) { + return $fields_update; + } + + $request->set_param( 'context', 'edit' ); + + $response = $this->prepare_item_for_response( $review, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Deletes a review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response Response object on success, or error object on failure. + */ + public function delete_item( $request ) { + $review = $this->get_review( $request['id'] ); + if ( is_wp_error( $review ) ) { + return $review; + } + + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + /** + * Filters whether a review can be trashed. + * + * Return false to disable trash support for the post. + * + * @since 3.5.0 + * @param bool $supports_trash Whether the post type support trashing. + * @param WP_Comment $review The review object being considered for trashing support. + */ + $supports_trash = apply_filters( 'woocommerce_rest_product_review_trashable', ( EMPTY_TRASH_DAYS > 0 ), $review ); + + $request->set_param( 'context', 'edit' ); + + if ( $force ) { + $previous = $this->prepare_item_for_response( $review, $request ); + $result = wp_delete_comment( $review->comment_ID, true ); + $response = new WP_REST_Response(); + $response->set_data( + array( + 'deleted' => true, + 'previous' => $previous->get_data(), + ) + ); + } else { + // If this type doesn't support trashing, error out. + if ( ! $supports_trash ) { + /* translators: %s: force=true */ + return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( "The object does not support trashing. Set '%s' to delete.", 'woocommerce' ), 'force=true' ), array( 'status' => 501 ) ); + } + + if ( 'trash' === $review->comment_approved ) { + return new WP_Error( 'woocommerce_rest_already_trashed', __( 'The object has already been trashed.', 'woocommerce' ), array( 'status' => 410 ) ); + } + + $result = wp_trash_comment( $review->comment_ID ); + $review = get_comment( $review->comment_ID ); + $response = $this->prepare_item_for_response( $review, $request ); + } + + if ( ! $result ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The object cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a review is deleted via the REST API. + * + * @param WP_Comment $review The deleted review data. + * @param WP_REST_Response $response The response returned from the API. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_review', $review, $response, $request ); + + return $response; + } + + /** + * Prepare a single product review output for response. + * + * @param WP_Comment $review Product review object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $review, $request ) { + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $fields = $this->get_fields_for_response( $request ); + $data = array(); + + if ( in_array( 'id', $fields, true ) ) { + $data['id'] = (int) $review->comment_ID; + } + if ( in_array( 'date_created', $fields, true ) ) { + $data['date_created'] = wc_rest_prepare_date_response( $review->comment_date ); + } + if ( in_array( 'date_created_gmt', $fields, true ) ) { + $data['date_created_gmt'] = wc_rest_prepare_date_response( $review->comment_date_gmt ); + } + if ( in_array( 'product_id', $fields, true ) ) { + $data['product_id'] = (int) $review->comment_post_ID; + } + if ( in_array( 'product_name', $fields, true ) ) { + $data['product_name'] = get_the_title( (int) $review->comment_post_ID ); + } + if ( in_array( 'product_permalink', $fields, true ) ) { + $data['product_permalink'] = get_permalink( (int) $review->comment_post_ID ); + } + if ( in_array( 'status', $fields, true ) ) { + $data['status'] = $this->prepare_status_response( (string) $review->comment_approved ); + } + if ( in_array( 'reviewer', $fields, true ) ) { + $data['reviewer'] = $review->comment_author; + } + if ( in_array( 'reviewer_email', $fields, true ) ) { + $data['reviewer_email'] = $review->comment_author_email; + } + if ( in_array( 'review', $fields, true ) ) { + $data['review'] = 'view' === $context ? wpautop( $review->comment_content ) : $review->comment_content; + } + if ( in_array( 'rating', $fields, true ) ) { + $data['rating'] = (int) get_comment_meta( $review->comment_ID, 'rating', true ); + } + if ( in_array( 'verified', $fields, true ) ) { + $data['verified'] = wc_review_is_from_verified_owner( $review->comment_ID ); + } + if ( in_array( 'reviewer_avatar_urls', $fields, true ) ) { + $data['reviewer_avatar_urls'] = rest_get_avatar_urls( $review->comment_author_email ); + } + + $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 ); + + $response->add_links( $this->prepare_links( $review ) ); + + /** + * Filter product reviews object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment $review Product review object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request ); + } + + /** + * Prepare a single product review to be inserted into the database. + * + * @param WP_REST_Request $request Request object. + * @return array|WP_Error $prepared_review + */ + protected function prepare_item_for_database( $request ) { + if ( isset( $request['id'] ) ) { + $prepared_review['comment_ID'] = (int) $request['id']; + } + + if ( isset( $request['review'] ) ) { + $prepared_review['comment_content'] = $request['review']; + } + + if ( isset( $request['product_id'] ) ) { + $prepared_review['comment_post_ID'] = (int) $request['product_id']; + } + + if ( isset( $request['reviewer'] ) ) { + $prepared_review['comment_author'] = $request['reviewer']; + } + + if ( isset( $request['reviewer_email'] ) ) { + $prepared_review['comment_author_email'] = $request['reviewer_email']; + } + + if ( ! empty( $request['date_created'] ) ) { + $date_data = rest_get_date_with_gmt( $request['date_created'] ); + + if ( ! empty( $date_data ) ) { + list( $prepared_review['comment_date'], $prepared_review['comment_date_gmt'] ) = $date_data; + } + } elseif ( ! empty( $request['date_created_gmt'] ) ) { + $date_data = rest_get_date_with_gmt( $request['date_created_gmt'], true ); + + if ( ! empty( $date_data ) ) { + list( $prepared_review['comment_date'], $prepared_review['comment_date_gmt'] ) = $date_data; + } + } + + /** + * Filters a review after it is prepared for the database. + * + * Allows modification of the review right after it is prepared for the database. + * + * @since 3.5.0 + * @param array $prepared_review The prepared review data for `wp_insert_comment`. + * @param WP_REST_Request $request The current request. + */ + return apply_filters( 'woocommerce_rest_preprocess_product_review', $prepared_review, $request ); + } + + /** + * Prepare links for the request. + * + * @param WP_Comment $review Product review object. + * @return array Links for the given product review. + */ + protected function prepare_links( $review ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $review->comment_ID ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + if ( 0 !== (int) $review->comment_post_ID ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $review->comment_post_ID ) ), + ); + } + + if ( 0 !== (int) $review->user_id ) { + $links['reviewer'] = array( + 'href' => rest_url( 'wp/v2/users/' . $review->user_id ), + 'embeddable' => true, + ); + } + + return $links; + } + + /** + * Get the Product Review's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'product_review', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the review was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Unique identifier for the product that the review belongs to.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'product_name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'product_permalink' => array( + 'description' => __( 'Product URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'status' => array( + 'description' => __( 'Status of the review.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'approved', + 'enum' => array( 'approved', 'hold', 'spam', 'unspam', 'trash', 'untrash' ), + 'context' => array( 'view', 'edit' ), + ), + 'reviewer' => array( + 'description' => __( 'Reviewer name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'reviewer_email' => array( + 'description' => __( 'Reviewer email.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'review' => array( + 'description' => __( 'The content of the review.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wp_filter_post_kses', + ), + ), + 'rating' => array( + 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'verified' => array( + 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + if ( get_option( 'show_avatars' ) ) { + $avatar_properties = array(); + $avatar_sizes = rest_get_avatar_sizes(); + + foreach ( $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.', 'woocommerce' ), $size ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'embed', 'view', 'edit' ), + ); + } + $schema['properties']['reviewer_avatar_urls'] = array( + 'description' => __( 'Avatar URLs for the object reviewer.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => $avatar_properties, + ); + } + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['context']['default'] = 'view'; + + $params['after'] = array( + 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + ); + $params['before'] = array( + 'description' => __( 'Limit response to reviews published before a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + ); + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + ); + $params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( + 'asc', + 'desc', + ), + ); + $params['orderby'] = array( + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'date_gmt', + 'enum' => array( + 'date', + 'date_gmt', + 'id', + 'include', + 'product', + ), + ); + $params['reviewer'] = array( + 'description' => __( 'Limit result set to reviews assigned to specific user IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + ); + $params['reviewer_exclude'] = array( + 'description' => __( 'Ensure result set excludes reviews assigned to specific user IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + ); + $params['reviewer_email'] = array( + 'default' => null, + 'description' => __( 'Limit result set to that from a specific author email.', 'woocommerce' ), + 'format' => 'email', + 'type' => 'string', + ); + $params['product'] = array( + 'default' => array(), + 'description' => __( 'Limit result set to reviews assigned to specific product IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + ); + $params['status'] = array( + 'default' => 'approved', + 'description' => __( 'Limit result set to reviews assigned a specific status.', 'woocommerce' ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'enum' => array( + 'all', + 'hold', + 'approved', + 'spam', + 'trash', + ), + ); + + /** + * Filter collection parameters for the reviews controller. + * + * This filter registers the collection parameter, but does not map the + * collection parameter to an internal WP_Comment_Query parameter. Use the + * `wc_rest_review_query` filter to set WP_Comment_Query parameters. + * + * @since 3.5.0 + * @param array $params JSON Schema-formatted collection parameters. + */ + return apply_filters( 'woocommerce_rest_product_review_collection_params', $params ); + } + + /** + * Get the reivew, if the ID is valid. + * + * @since 3.5.0 + * @param int $id Supplied ID. + * @return WP_Comment|WP_Error Comment object if ID is valid, WP_Error otherwise. + */ + protected function get_review( $id ) { + $id = (int) $id; + $error = new WP_Error( 'woocommerce_rest_review_invalid_id', __( 'Invalid review ID.', 'woocommerce' ), array( 'status' => 404 ) ); + + if ( 0 >= $id ) { + return $error; + } + + $review = get_comment( $id ); + if ( empty( $review ) ) { + return $error; + } + + if ( ! empty( $review->comment_post_ID ) ) { + $post = get_post( (int) $review->comment_post_ID ); + + if ( 'product' !== get_post_type( (int) $review->comment_post_ID ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + } + + return $review; + } + + /** + * Prepends internal property prefix to query parameters to match our response fields. + * + * @since 3.5.0 + * @param string $query_param Query parameter. + * @return string + */ + protected function normalize_query_param( $query_param ) { + $prefix = 'comment_'; + + switch ( $query_param ) { + case 'id': + $normalized = $prefix . 'ID'; + break; + case 'product': + $normalized = $prefix . 'post_ID'; + break; + case 'include': + $normalized = 'comment__in'; + break; + default: + $normalized = $prefix . $query_param; + break; + } + + return $normalized; + } + + /** + * Checks comment_approved to set comment status for single comment output. + * + * @since 3.5.0 + * @param string|int $comment_approved comment status. + * @return string Comment status. + */ + protected function prepare_status_response( $comment_approved ) { + switch ( $comment_approved ) { + case 'hold': + case '0': + $status = 'hold'; + break; + case 'approve': + case '1': + $status = 'approved'; + break; + case 'spam': + case 'trash': + default: + $status = $comment_approved; + break; + } + + return $status; + } + + /** + * Sets the comment_status of a given review object when creating or updating a review. + * + * @since 3.5.0 + * @param string|int $new_status New review status. + * @param int $id Review ID. + * @return bool Whether the status was changed. + */ + protected function handle_status_param( $new_status, $id ) { + $old_status = wp_get_comment_status( $id ); + + if ( $new_status === $old_status ) { + return false; + } + + switch ( $new_status ) { + case 'approved': + case 'approve': + case '1': + $changed = wp_set_comment_status( $id, 'approve' ); + break; + case 'hold': + case '0': + $changed = wp_set_comment_status( $id, 'hold' ); + break; + case 'spam': + $changed = wp_spam_comment( $id ); + break; + case 'unspam': + $changed = wp_unspam_comment( $id ); + break; + case 'trash': + $changed = wp_trash_comment( $id ); + break; + case 'untrash': + $changed = wp_untrash_comment( $id ); + break; + default: + $changed = false; + break; + } + + return $changed; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-product-shipping-classes-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-shipping-classes-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-product-shipping-classes-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-shipping-classes-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-product-tags-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-tags-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-product-tags-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-tags-controller.php diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php new file mode 100644 index 00000000000..9d2000b23eb --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php @@ -0,0 +1,880 @@ +/variations endpoints. + * + * @package WooCommerce\RestApi + * @since 3.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API variations controller class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Product_Variations_V2_Controller + */ +class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V2_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v3'; + + /** + * Prepare a single variation output for response. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { + $data = array( + 'id' => $object->get_id(), + 'date_created' => wc_rest_prepare_date_response( $object->get_date_created(), false ), + 'date_created_gmt' => wc_rest_prepare_date_response( $object->get_date_created() ), + 'date_modified' => wc_rest_prepare_date_response( $object->get_date_modified(), false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $object->get_date_modified() ), + 'description' => wc_format_content( $object->get_description() ), + 'permalink' => $object->get_permalink(), + 'sku' => $object->get_sku(), + 'price' => $object->get_price(), + 'regular_price' => $object->get_regular_price(), + 'sale_price' => $object->get_sale_price(), + 'date_on_sale_from' => wc_rest_prepare_date_response( $object->get_date_on_sale_from(), false ), + 'date_on_sale_from_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_from() ), + 'date_on_sale_to' => wc_rest_prepare_date_response( $object->get_date_on_sale_to(), false ), + 'date_on_sale_to_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_to() ), + 'on_sale' => $object->is_on_sale(), + 'status' => $object->get_status(), + 'purchasable' => $object->is_purchasable(), + 'virtual' => $object->is_virtual(), + 'downloadable' => $object->is_downloadable(), + 'downloads' => $this->get_downloads( $object ), + 'download_limit' => '' !== $object->get_download_limit() ? (int) $object->get_download_limit() : -1, + 'download_expiry' => '' !== $object->get_download_expiry() ? (int) $object->get_download_expiry() : -1, + 'tax_status' => $object->get_tax_status(), + 'tax_class' => $object->get_tax_class(), + 'manage_stock' => $object->managing_stock(), + 'stock_quantity' => $object->get_stock_quantity(), + 'stock_status' => $object->get_stock_status(), + 'backorders' => $object->get_backorders(), + 'backorders_allowed' => $object->backorders_allowed(), + 'backordered' => $object->is_on_backorder(), + 'low_stock_amount' => '' === $object->get_low_stock_amount() ? null : $object->get_low_stock_amount(), + 'weight' => $object->get_weight(), + 'dimensions' => array( + 'length' => $object->get_length(), + 'width' => $object->get_width(), + 'height' => $object->get_height(), + ), + 'shipping_class' => $object->get_shipping_class(), + 'shipping_class_id' => $object->get_shipping_class_id(), + 'image' => $this->get_image( $object ), + 'attributes' => $this->get_attributes( $object ), + 'menu_order' => $object->get_menu_order(), + 'meta_data' => $object->get_meta_data(), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $object, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Prepare a single variation for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + if ( isset( $request['id'] ) ) { + $variation = wc_get_product( absint( $request['id'] ) ); + } else { + $variation = new WC_Product_Variation(); + } + + $variation->set_parent_id( absint( $request['product_id'] ) ); + + // Status. + if ( isset( $request['status'] ) ) { + $variation->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); + } + + // SKU. + if ( isset( $request['sku'] ) ) { + $variation->set_sku( wc_clean( $request['sku'] ) ); + } + + // Thumbnail. + if ( isset( $request['image'] ) ) { + if ( is_array( $request['image'] ) ) { + $variation = $this->set_variation_image( $variation, $request['image'] ); + } else { + $variation->set_image_id( '' ); + } + } + + // Virtual variation. + if ( isset( $request['virtual'] ) ) { + $variation->set_virtual( $request['virtual'] ); + } + + // Downloadable variation. + if ( isset( $request['downloadable'] ) ) { + $variation->set_downloadable( $request['downloadable'] ); + } + + // Downloads. + if ( $variation->get_downloadable() ) { + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $variation = $this->save_downloadable_files( $variation, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + $variation->set_download_limit( $request['download_limit'] ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + $variation->set_download_expiry( $request['download_expiry'] ); + } + } + + // Shipping data. + $variation = $this->save_product_shipping_data( $variation, $request ); + + // Stock handling. + if ( isset( $request['manage_stock'] ) ) { + $variation->set_manage_stock( $request['manage_stock'] ); + } + + if ( isset( $request['stock_status'] ) ) { + $variation->set_stock_status( $request['stock_status'] ); + } + + if ( isset( $request['backorders'] ) ) { + $variation->set_backorders( $request['backorders'] ); + } + + if ( $variation->get_manage_stock() ) { + if ( isset( $request['stock_quantity'] ) ) { + $variation->set_stock_quantity( $request['stock_quantity'] ); + } elseif ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + $variation->set_stock_quantity( $stock_quantity ); + } + // isset() returns false for value null, thus we need to check whether the value has been sent by the request. + if ( array_key_exists( 'low_stock_amount', $request->get_params() ) ) { + if ( null === $request['low_stock_amount'] ) { + $variation->set_low_stock_amount( '' ); + } else { + $variation->set_low_stock_amount( wc_stock_amount( $request['low_stock_amount'] ) ); + } + } + } else { + $variation->set_backorders( 'no' ); + $variation->set_stock_quantity( '' ); + $variation->set_low_stock_amount( '' ); + } + + // Regular Price. + if ( isset( $request['regular_price'] ) ) { + $variation->set_regular_price( $request['regular_price'] ); + } + + // Sale Price. + if ( isset( $request['sale_price'] ) ) { + $variation->set_sale_price( $request['sale_price'] ); + } + + if ( isset( $request['date_on_sale_from'] ) ) { + $variation->set_date_on_sale_from( $request['date_on_sale_from'] ); + } + + if ( isset( $request['date_on_sale_from_gmt'] ) ) { + $variation->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null ); + } + + if ( isset( $request['date_on_sale_to'] ) ) { + $variation->set_date_on_sale_to( $request['date_on_sale_to'] ); + } + + if ( isset( $request['date_on_sale_to_gmt'] ) ) { + $variation->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null ); + } + + // Tax class. + if ( isset( $request['tax_class'] ) ) { + $variation->set_tax_class( $request['tax_class'] ); + } + + // Description. + if ( isset( $request['description'] ) ) { + $variation->set_description( wp_kses_post( $request['description'] ) ); + } + + // Update taxonomies. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + $parent = wc_get_product( $variation->get_parent_id() ); + + if ( ! $parent ) { + return new WP_Error( + // Translators: %d parent ID. + "woocommerce_rest_{$this->post_type}_invalid_parent", + __( 'Cannot set attributes due to invalid parent product.', 'woocommerce' ), + array( 'status' => 404 ) + ); + } + + $parent_attributes = $parent->get_attributes(); + + foreach ( $request['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { + continue; + } + + $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); + $attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + + if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $attribute_value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $attribute_value = $term->slug; + } else { + $attribute_value = sanitize_title( $attribute_value ); + } + } + + $attributes[ $attribute_key ] = $attribute_value; + } + + $variation->set_attributes( $attributes ); + } + + // Menu order. + if ( $request['menu_order'] ) { + $variation->set_menu_order( $request['menu_order'] ); + } + + // Meta data. + if ( is_array( $request['meta_data'] ) ) { + foreach ( $request['meta_data'] as $meta ) { + $variation->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $variation Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $variation, $request, $creating ); + } + + /** + * Get the image for a product variation. + * + * @param WC_Product_Variation $variation Variation data. + * @return array + */ + protected function get_image( $variation ) { + if ( ! $variation->get_image_id() ) { + return; + } + + $attachment_id = $variation->get_image_id(); + $attachment_post = get_post( $attachment_id ); + if ( is_null( $attachment_post ) ) { + return; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + if ( ! is_array( $attachment ) ) { + return; + } + + if ( ! isset( $image ) ) { + return array( + 'id' => (int) $attachment_id, + 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ), + 'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ), + 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ), + 'src' => current( $attachment ), + 'name' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + ); + } + } + + /** + * Set variation image. + * + * @throws WC_REST_Exception REST API exceptions. + * @param WC_Product_Variation $variation Variation instance. + * @param array $image Image data. + * @return WC_Product_Variation + */ + protected function set_variation_image( $variation, $image ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id ) { + if ( isset( $image['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $variation->get_id(), array( $image ) ) ) { + throw new WC_REST_Exception( 'woocommerce_variation_image_upload_error', $upload->get_error_message(), 400 ); + } + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $variation->get_id() ); + } else { + $variation->set_image_id( '' ); + return $variation; + } + } + + if ( ! wp_attachment_is_image( $attachment_id ) ) { + /* translators: %s: attachment ID */ + throw new WC_REST_Exception( 'woocommerce_variation_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); + } + + $variation->set_image_id( $attachment_id ); + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image name if present. + if ( ! empty( $image['name'] ) ) { + wp_update_post( + array( + 'ID' => $attachment_id, + 'post_title' => $image['name'], + ) + ); + } + + return $variation; + } + + /** + * Get the Variation's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $weight_unit = get_option( 'woocommerce_weight_unit' ); + $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'Variation description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'permalink' => array( + 'description' => __( 'Variation URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price' => array( + 'description' => __( 'Current variation price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Variation regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Variation sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from' => array( + 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from_gmt' => array( + 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to_gmt' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'on_sale' => array( + 'description' => __( 'Shows if the variation is on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'status' => array( + 'description' => __( 'Variation status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'publish', + 'enum' => array_keys( get_post_statuses() ), + 'context' => array( 'view', 'edit' ), + ), + 'purchasable' => array( + 'description' => __( 'Shows if the variation can be bought.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'virtual' => array( + 'description' => __( 'If the variation is virtual.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloadable' => array( + 'description' => __( 'If the variation is downloadable.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloads' => array( + 'description' => __( 'List of downloadable files.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'File ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'taxable', + 'enum' => array( 'taxable', 'shipping', 'none' ), + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'manage_stock' => array( + 'description' => __( 'Stock management at variation level.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'stock_quantity' => array( + 'description' => __( 'Stock quantity.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'stock_status' => array( + 'description' => __( 'Controls the stock status of the product.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'instock', + 'enum' => array_keys( wc_get_product_stock_status_options() ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders' => array( + 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'no', + 'enum' => array( 'no', 'notify', 'yes' ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders_allowed' => array( + 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'backordered' => array( + 'description' => __( 'Shows if the variation is on backordered.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'low_stock_amount' => array( + 'description' => __( 'Low Stock amount for the variation.', 'woocommerce' ), + 'type' => array( 'integer', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + 'weight' => array( + /* translators: %s: weight unit */ + 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'dimensions' => array( + 'description' => __( 'Variation dimensions.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping_class' => array( + 'description' => __( 'Shipping class slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'shipping_class_id' => array( + 'description' => __( 'Shipping class ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'image' => array( + 'description' => __( 'Variation image data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'attributes' => array( + 'description' => __( 'List of attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'option' => array( + 'description' => __( 'Selected attribute term name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = WC_REST_CRUD_Controller::prepare_objects_query( $request ); + + // Set post_status. + $args['post_status'] = $request['status']; + + // Filter by sku. + if ( ! empty( $request['sku'] ) ) { + $skus = explode( ',', $request['sku'] ); + // Include the current string as a SKU too. + if ( 1 < count( $skus ) ) { + $skus[] = $request['sku']; + } + + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_sku', + 'value' => $skus, + 'compare' => 'IN', + ) + ); + } + + // Filter by tax class. + if ( ! empty( $request['tax_class'] ) ) { + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_tax_class', + 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', + ) + ); + } + + // Price filter. + if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); // WPCS: slow query ok. + } + + // Filter product based on stock_status. + if ( ! empty( $request['stock_status'] ) ) { + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_stock_status', + 'value' => $request['stock_status'], + ) + ); + } + + // Filter by on sale products. + if ( is_bool( $request['on_sale'] ) ) { + $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; + $on_sale_ids = wc_get_product_ids_on_sale(); + + // Use 0 when there's no on sale products to avoid return all products. + $on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids; + + $args[ $on_sale_key ] += $on_sale_ids; + } + + // Force the post_type argument, since it's not a user input variable. + if ( ! empty( $request['sku'] ) ) { + $args['post_type'] = array( 'product', 'product_variation' ); + } else { + $args['post_type'] = $this->post_type; + } + + $args['post_parent'] = $request['product_id']; + + return $args; + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + unset( + $params['in_stock'], + $params['type'], + $params['featured'], + $params['category'], + $params['tag'], + $params['shipping_class'], + $params['attribute'], + $params['attribute_term'] + ); + + $params['stock_status'] = array( + 'description' => __( 'Limit result set to products with specified stock status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_keys( wc_get_product_stock_status_options() ), + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php new file mode 100644 index 00000000000..c2c03690b87 --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php @@ -0,0 +1,1396 @@ +get_image_id() ) { + $attachment_ids[] = $product->get_image_id(); + } + + // Add gallery images. + $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); + + // Build image data. + foreach ( $attachment_ids as $attachment_id ) { + $attachment_post = get_post( $attachment_id ); + if ( is_null( $attachment_post ) ) { + continue; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + if ( ! is_array( $attachment ) ) { + continue; + } + + $images[] = array( + 'id' => (int) $attachment_id, + 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ), + 'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ), + 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ), + 'src' => current( $attachment ), + 'name' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + ); + } + + return $images; + } + + /** + * Make extra product orderby features supported by WooCommerce available to the WC API. + * This includes 'price', 'popularity', and 'rating'. + * + * @param WP_REST_Request $request Request data. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = WC_REST_CRUD_Controller::prepare_objects_query( $request ); + + // Set post_status. + $args['post_status'] = $request['status']; + + // Taxonomy query to filter products by type, category, + // tag, shipping class, and attribute. + $tax_query = array(); + + // Map between taxonomy name and arg's key. + $taxonomies = array( + 'product_cat' => 'category', + 'product_tag' => 'tag', + 'product_shipping_class' => 'shipping_class', + ); + + // Set tax_query for each passed arg. + foreach ( $taxonomies as $taxonomy => $key ) { + if ( ! empty( $request[ $key ] ) ) { + $tax_query[] = array( + 'taxonomy' => $taxonomy, + 'field' => 'term_id', + 'terms' => $request[ $key ], + ); + } + } + + // Filter product type by slug. + if ( ! empty( $request['type'] ) ) { + $tax_query[] = array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $request['type'], + ); + } + + // Filter by attribute and term. + if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { + if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { + $tax_query[] = array( + 'taxonomy' => $request['attribute'], + 'field' => 'term_id', + 'terms' => $request['attribute_term'], + ); + } + } + + // Build tax_query if taxonomies are set. + if ( ! empty( $tax_query ) ) { + if ( ! empty( $args['tax_query'] ) ) { + $args['tax_query'] = array_merge( $tax_query, $args['tax_query'] ); // WPCS: slow query ok. + } else { + $args['tax_query'] = $tax_query; // WPCS: slow query ok. + } + } + + // Filter featured. + if ( is_bool( $request['featured'] ) ) { + $args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'name', + 'terms' => 'featured', + 'operator' => true === $request['featured'] ? 'IN' : 'NOT IN', + ); + } + + // Filter by sku. + if ( ! empty( $request['sku'] ) ) { + $skus = explode( ',', $request['sku'] ); + // Include the current string as a SKU too. + if ( 1 < count( $skus ) ) { + $skus[] = $request['sku']; + } + + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_sku', + 'value' => $skus, + 'compare' => 'IN', + ) + ); + } + + // Filter by tax class. + if ( ! empty( $request['tax_class'] ) ) { + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_tax_class', + 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', + ) + ); + } + + // Price filter. + if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); // WPCS: slow query ok. + } + + // Filter product by stock_status. + if ( ! empty( $request['stock_status'] ) ) { + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_stock_status', + 'value' => $request['stock_status'], + ) + ); + } + + // Filter by on sale products. + if ( is_bool( $request['on_sale'] ) ) { + $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; + $on_sale_ids = wc_get_product_ids_on_sale(); + + // Use 0 when there's no on sale products to avoid return all products. + $on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids; + + $args[ $on_sale_key ] += $on_sale_ids; + } + + // Force the post_type argument, since it's not a user input variable. + if ( ! empty( $request['sku'] ) ) { + $args['post_type'] = array( 'product', 'product_variation' ); + } else { + $args['post_type'] = $this->post_type; + } + + $ordering_args = WC()->query->get_catalog_ordering_args( $args['orderby'], $args['order'] ); + $args['orderby'] = $ordering_args['orderby']; + $args['order'] = $ordering_args['order']; + if ( $ordering_args['meta_key'] ) { + $args['meta_key'] = $ordering_args['meta_key']; // WPCS: slow query ok. + } + + return $args; + } + + /** + * Set product images. + * + * @throws WC_REST_Exception REST API exceptions. + * @param WC_Product $product Product instance. + * @param array $images Images data. + * @return WC_Product + */ + protected function set_product_images( $product, $images ) { + $images = is_array( $images ) ? array_filter( $images ) : array(); + + if ( ! empty( $images ) ) { + $gallery = array(); + + foreach ( $images as $index => $image ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } else { + continue; + } + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); + } + + if ( ! wp_attachment_is_image( $attachment_id ) ) { + /* translators: %s: image ID */ + throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); + } + + $featured_image = $product->get_image_id(); + + if ( 0 === $index ) { + $product->set_image_id( $attachment_id ); + } else { + $gallery[] = $attachment_id; + } + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image name if present. + if ( ! empty( $image['name'] ) ) { + wp_update_post( + array( + 'ID' => $attachment_id, + 'post_title' => $image['name'], + ) + ); + } + } + + $product->set_gallery_image_ids( $gallery ); + } else { + $product->set_image_id( '' ); + $product->set_gallery_image_ids( array() ); + } + + return $product; + } + + /** + * Prepare a single product for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + + // Type is the most important part here because we need to be using the correct class and methods. + if ( isset( $request['type'] ) ) { + $classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] ); + + if ( ! class_exists( $classname ) ) { + $classname = 'WC_Product_Simple'; + } + + $product = new $classname( $id ); + } elseif ( isset( $request['id'] ) ) { + $product = wc_get_product( $id ); + } else { + $product = new WC_Product_Simple(); + } + + if ( 'variation' === $product->get_type() ) { + return new WP_Error( + "woocommerce_rest_invalid_{$this->post_type}_id", + __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), + array( + 'status' => 404, + ) + ); + } + + // Post title. + if ( isset( $request['name'] ) ) { + $product->set_name( wp_filter_post_kses( $request['name'] ) ); + } + + // Post content. + if ( isset( $request['description'] ) ) { + $product->set_description( wp_filter_post_kses( $request['description'] ) ); + } + + // Post excerpt. + if ( isset( $request['short_description'] ) ) { + $product->set_short_description( wp_filter_post_kses( $request['short_description'] ) ); + } + + // Post status. + if ( isset( $request['status'] ) ) { + $product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); + } + + // Post slug. + if ( isset( $request['slug'] ) ) { + $product->set_slug( $request['slug'] ); + } + + // Menu order. + if ( isset( $request['menu_order'] ) ) { + $product->set_menu_order( $request['menu_order'] ); + } + + // Comment status. + if ( isset( $request['reviews_allowed'] ) ) { + $product->set_reviews_allowed( $request['reviews_allowed'] ); + } + + // Virtual. + if ( isset( $request['virtual'] ) ) { + $product->set_virtual( $request['virtual'] ); + } + + // Tax status. + if ( isset( $request['tax_status'] ) ) { + $product->set_tax_status( $request['tax_status'] ); + } + + // Tax Class. + if ( isset( $request['tax_class'] ) ) { + $product->set_tax_class( $request['tax_class'] ); + } + + // Catalog Visibility. + if ( isset( $request['catalog_visibility'] ) ) { + $product->set_catalog_visibility( $request['catalog_visibility'] ); + } + + // Purchase Note. + if ( isset( $request['purchase_note'] ) ) { + $product->set_purchase_note( wp_kses_post( wp_unslash( $request['purchase_note'] ) ) ); + } + + // Featured Product. + if ( isset( $request['featured'] ) ) { + $product->set_featured( $request['featured'] ); + } + + // Shipping data. + $product = $this->save_product_shipping_data( $product, $request ); + + // SKU. + if ( isset( $request['sku'] ) ) { + $product->set_sku( wc_clean( $request['sku'] ) ); + } + + // Attributes. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + + foreach ( $request['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = wc_clean( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( $attribute_id ) { + + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $attribute['options'] ) ) { + // Text based attributes - Posted values are term names. + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = array(); + } + + if ( ! empty( $values ) ) { + // Add attribute to array, but don't set values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } elseif ( isset( $attribute['options'] ) ) { + // Custom attribute - Add attribute to array and set the values. + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + } else { + $values = explode( WC_DELIMITER, $attribute['options'] ); + } + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } + $product->set_attributes( $attributes ); + } + + // Sales and prices. + if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) { + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->set_price( '' ); + } else { + // Regular Price. + if ( isset( $request['regular_price'] ) ) { + $product->set_regular_price( $request['regular_price'] ); + } + + // Sale Price. + if ( isset( $request['sale_price'] ) ) { + $product->set_sale_price( $request['sale_price'] ); + } + + if ( isset( $request['date_on_sale_from'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from'] ); + } + + if ( isset( $request['date_on_sale_from_gmt'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null ); + } + + if ( isset( $request['date_on_sale_to'] ) ) { + $product->set_date_on_sale_to( $request['date_on_sale_to'] ); + } + + if ( isset( $request['date_on_sale_to_gmt'] ) ) { + $product->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null ); + } + } + + // Product parent ID. + if ( isset( $request['parent_id'] ) ) { + $product->set_parent_id( $request['parent_id'] ); + } + + // Sold individually. + if ( isset( $request['sold_individually'] ) ) { + $product->set_sold_individually( $request['sold_individually'] ); + } + + // Stock status; stock_status has priority over in_stock. + if ( isset( $request['stock_status'] ) ) { + $stock_status = $request['stock_status']; + } else { + $stock_status = $product->get_stock_status(); + } + + // Stock data. + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $request['manage_stock'] ) ) { + $product->set_manage_stock( $request['manage_stock'] ); + } + + // Backorders. + if ( isset( $request['backorders'] ) ) { + $product->set_backorders( $request['backorders'] ); + } + + if ( $product->is_type( 'grouped' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } elseif ( $product->is_type( 'external' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( 'instock' ); + } elseif ( $product->get_manage_stock() ) { + // Stock status is always determined by children so sync later. + if ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Stock quantity. + if ( isset( $request['stock_quantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) ); + } elseif ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); + } + + // Low stock amount. + // isset() returns false for value null, thus we need to check whether the value has been sent by the request. + if ( array_key_exists( 'low_stock_amount', $request->get_params() ) ) { + if ( null === $request['low_stock_amount'] ) { + $product->set_low_stock_amount( '' ); + } else { + $product->set_low_stock_amount( wc_stock_amount( $request['low_stock_amount'] ) ); + } + } + } else { + // Don't manage stock. + $product->set_manage_stock( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + $product->set_low_stock_amount( '' ); + } + } elseif ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Upsells. + if ( isset( $request['upsell_ids'] ) ) { + $upsells = array(); + $ids = $request['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + } + + $product->set_upsell_ids( $upsells ); + } + + // Cross sells. + if ( isset( $request['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $request['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + } + + $product->set_cross_sell_ids( $crosssells ); + } + + // Product categories. + if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['categories'] ); + } + + // Product tags. + if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { + $new_tags = array(); + + foreach ( $request['tags'] as $tag ) { + if ( ! isset( $tag['name'] ) ) { + $new_tags[] = $tag; + continue; + } + + if ( ! term_exists( $tag['name'], 'product_tag' ) ) { + // Create the tag if it doesn't exist. + $term = wp_insert_term( $tag['name'], 'product_tag' ); + + if ( ! is_wp_error( $term ) ) { + $new_tags[] = array( + 'id' => $term['term_id'], + ); + + continue; + } + } else { + // Tag exists, assume user wants to set the product with this tag. + $new_tags[] = array( + 'id' => get_term_by( 'name', $tag['name'], 'product_tag' )->term_id, + ); + } + } + + $product = $this->save_taxonomy_terms( $product, $new_tags, 'tag' ); + } + + // Downloadable. + if ( isset( $request['downloadable'] ) ) { + $product->set_downloadable( $request['downloadable'] ); + } + + // Downloadable options. + if ( $product->get_downloadable() ) { + + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $product = $this->save_downloadable_files( $product, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + $product->set_download_limit( $request['download_limit'] ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + $product->set_download_expiry( $request['download_expiry'] ); + } + } + + // Product url and button text for external products. + if ( $product->is_type( 'external' ) ) { + if ( isset( $request['external_url'] ) ) { + $product->set_product_url( $request['external_url'] ); + } + + if ( isset( $request['button_text'] ) ) { + $product->set_button_text( $request['button_text'] ); + } + } + + // Save default attributes for variable products. + if ( $product->is_type( 'variable' ) ) { + $product = $this->save_default_attributes( $product, $request ); + } + + // Set children for a grouped product. + if ( $product->is_type( 'grouped' ) && isset( $request['grouped_products'] ) ) { + $product->set_children( $request['grouped_products'] ); + } + + // Check for featured/gallery images, upload it and set it. + if ( isset( $request['images'] ) ) { + $product = $this->set_product_images( $product, $request['images'] ); + } + + // Allow set meta_data. + if ( is_array( $request['meta_data'] ) ) { + foreach ( $request['meta_data'] as $meta ) { + $product->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + + if ( ! empty( $request['date_created'] ) ) { + $date = rest_parse_date( $request['date_created'] ); + + if ( $date ) { + $product->set_date_created( $date ); + } + } + + if ( ! empty( $request['date_created_gmt'] ) ) { + $date = rest_parse_date( $request['date_created_gmt'], true ); + + if ( $date ) { + $product->set_date_created( $date ); + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $product Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $product, $request, $creating ); + } + + /** + * Get the Product's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $weight_unit = get_option( 'woocommerce_weight_unit' ); + $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'slug' => array( + 'description' => __( 'Product slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'permalink' => array( + 'description' => __( 'Product URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the product was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_modified' => array( + 'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the product was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'Product type.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'simple', + 'enum' => array_keys( wc_get_product_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Product status (post status).', 'woocommerce' ), + 'type' => 'string', + 'default' => 'publish', + 'enum' => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ), + 'context' => array( 'view', 'edit' ), + ), + 'featured' => array( + 'description' => __( 'Featured product.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'catalog_visibility' => array( + 'description' => __( 'Catalog visibility.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'visible', + 'enum' => array( 'visible', 'catalog', 'search', 'hidden' ), + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Product description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'short_description' => array( + 'description' => __( 'Product short description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sku' => array( + 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price' => array( + 'description' => __( 'Current product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Product regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Product sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from' => array( + 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from_gmt' => array( + 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to_gmt' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'price_html' => array( + 'description' => __( 'Price formatted in HTML.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'on_sale' => array( + 'description' => __( 'Shows if the product is on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'purchasable' => array( + 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_sales' => array( + 'description' => __( 'Amount of sales.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'virtual' => array( + 'description' => __( 'If the product is virtual.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloadable' => array( + 'description' => __( 'If the product is downloadable.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloads' => array( + 'description' => __( 'List of downloadable files.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'File ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'external_url' => array( + 'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'button_text' => array( + 'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'taxable', + 'enum' => array( 'taxable', 'shipping', 'none' ), + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'manage_stock' => array( + 'description' => __( 'Stock management at product level.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'stock_quantity' => array( + 'description' => __( 'Stock quantity.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'stock_status' => array( + 'description' => __( 'Controls the stock status of the product.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'instock', + 'enum' => array_keys( wc_get_product_stock_status_options() ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders' => array( + 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'no', + 'enum' => array( 'no', 'notify', 'yes' ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders_allowed' => array( + 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'backordered' => array( + 'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'low_stock_amount' => array( + 'description' => __( 'Low Stock amount for the product.', 'woocommerce' ), + 'type' => array( 'integer', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + 'sold_individually' => array( + 'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'weight' => array( + /* translators: %s: weight unit */ + 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'dimensions' => array( + 'description' => __( 'Product dimensions.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping_required' => array( + 'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_taxable' => array( + 'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_class' => array( + 'description' => __( 'Shipping class slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'shipping_class_id' => array( + 'description' => __( 'Shipping class ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'reviews_allowed' => array( + 'description' => __( 'Allow reviews.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'average_rating' => array( + 'description' => __( 'Reviews average rating.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rating_count' => array( + 'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'related_ids' => array( + 'description' => __( 'List of related products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'upsell_ids' => array( + 'description' => __( 'List of up-sell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'cross_sell_ids' => array( + 'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'parent_id' => array( + 'description' => __( 'Product parent ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'purchase_note' => array( + 'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'categories' => array( + 'description' => __( 'List of categories.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Category ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Category name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Category slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'tags' => array( + 'description' => __( 'List of tags.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tag ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Tag name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Tag slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'images' => array( + 'description' => __( 'List of images.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'has_options' => array( + 'description' => __( 'Shows if the product needs to be configured before it can be bought.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'attributes' => array( + 'description' => __( 'List of attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Attribute position.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'visible' => array( + 'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'variation' => array( + 'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'options' => array( + 'description' => __( 'List of available term names of the attribute.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'default_attributes' => array( + 'description' => __( 'Defaults variation attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'option' => array( + 'description' => __( 'Selected attribute term name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'variations' => array( + 'description' => __( 'List of variations IDs.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'integer', + ), + 'readonly' => true, + ), + 'grouped_products' => array( + 'description' => __( 'List of grouped products ID.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Add new options for 'orderby' to the collection params. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + $params['orderby']['enum'] = array_merge( $params['orderby']['enum'], array( 'price', 'popularity', 'rating' ) ); + + unset( $params['in_stock'] ); + $params['stock_status'] = array( + 'description' => __( 'Limit result set to products with specified stock status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_keys( wc_get_product_stock_status_options() ), + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } + + /** + * Get product data. + * + * @param WC_Product $product Product instance. + * @param string $context Request context. Options: 'view' and 'edit'. + * + * @return array + */ + protected function get_product_data( $product, $context = 'view' ) { + $data = parent::get_product_data( ...func_get_args() ); + // Add stock_status if needed. + if ( isset( $this->request ) ) { + $fields = $this->get_fields_for_response( $this->request ); + if ( in_array( 'stock_status', $fields ) ) { + $data['stock_status'] = $product->get_stock_status( $context ); + } + if ( in_array( 'has_options', $fields ) ) { + $data['has_options'] = $product->has_options( $context ); + } + } + return $data; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-report-coupons-totals-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-report-coupons-totals-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-report-coupons-totals-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-report-coupons-totals-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-report-customers-totals-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-report-customers-totals-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-report-customers-totals-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-report-customers-totals-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-report-orders-totals-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-report-orders-totals-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-report-orders-totals-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-report-orders-totals-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-report-products-totals-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-report-products-totals-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-report-products-totals-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-report-products-totals-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-report-reviews-totals-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-report-reviews-totals-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-report-reviews-totals-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-report-reviews-totals-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-report-sales-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-report-sales-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-report-sales-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-report-sales-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-report-top-sellers-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-report-top-sellers-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-report-top-sellers-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-report-top-sellers-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-reports-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-reports-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-reports-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-reports-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-setting-options-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-setting-options-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-setting-options-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-setting-options-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-settings-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-settings-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-settings-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-settings-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-methods-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-methods-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-shipping-methods-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-methods-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-locations-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-locations-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-locations-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-locations-controller.php diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php new file mode 100644 index 00000000000..f03234e0633 --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php @@ -0,0 +1,43 @@ +/methods endpoint. + * + * @package WooCommerce\RestApi + * @since 3.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Shipping Zone Methods class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Shipping_Zone_Methods_V2_Controller + */ +class WC_REST_Shipping_Zone_Methods_Controller extends WC_REST_Shipping_Zone_Methods_V2_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v3'; + + /** + * Get the settings schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + // Get parent schema to append additional supported settings types for shipping zone method. + $schema = parent::get_item_schema(); + + // Append additional settings supported types (class, order). + $schema['properties']['settings']['properties']['type']['enum'][] = 'class'; + $schema['properties']['settings']['properties']['type']['enum'][] = 'order'; + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller-base.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller-base.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller-base.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller-base.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-system-status-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-system-status-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-system-status-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-system-status-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-system-status-tools-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-system-status-tools-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-system-status-tools-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-system-status-tools-controller.php diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-tax-classes-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-tax-classes-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-tax-classes-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-tax-classes-controller.php diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php new file mode 100644 index 00000000000..6fef3703eb6 --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php @@ -0,0 +1,141 @@ +get_results( + $wpdb->prepare( + " + SELECT location_code, location_type + FROM {$wpdb->prefix}woocommerce_tax_rate_locations + WHERE tax_rate_id = %d + ", + $tax->tax_rate_id + ) + ); + + if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) { + foreach ( $locales as $locale ) { + if ( 'postcode' === $locale->location_type ) { + $data['postcodes'][] = $locale->location_code; + } elseif ( 'city' === $locale->location_type ) { + $data['cities'][] = $locale->location_code; + } + } + } + + return $data; + } + + /** + * Get the taxes schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = parent::get_item_schema(); + + $schema['properties']['postcodes'] = array( + 'description' => __( 'List of postcodes / ZIPs. Introduced in WooCommerce 5.3.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'context' => array( 'view', 'edit' ), + ); + + $schema['properties']['cities'] = array( + 'description' => __( 'List of city names. Introduced in WooCommerce 5.3.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'context' => array( 'view', 'edit' ), + ); + + $schema['properties']['postcode']['description'] = + __( "Postcode/ZIP, it doesn't support multiple values. Deprecated as of WooCommerce 5.3, 'postcodes' should be used instead.", 'woocommerce' ); + + $schema['properties']['city']['description'] = + __( "City name, it doesn't support multiple values. Deprecated as of WooCommerce 5.3, 'cities' should be used instead.", 'woocommerce' ); + + return $schema; + } + + /** + * Create a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response The response, or an error. + */ + public function create_item( $request ) { + $this->adjust_cities_and_postcodes( $request ); + + return parent::create_item( $request ); + } + + /** + * Update a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response The response, or an error. + */ + public function update_item( $request ) { + $this->adjust_cities_and_postcodes( $request ); + + return parent::update_item( $request ); + } + + /** + * Convert array "cities" and "postcodes" parameters + * into semicolon-separated strings "city" and "postcode". + * + * @param WP_REST_Request $request The request to adjust. + */ + private function adjust_cities_and_postcodes( &$request ) { + if ( isset( $request['cities'] ) ) { + $request['city'] = join( ';', $request['cities'] ); + } + if ( isset( $request['postcodes'] ) ) { + $request['postcode'] = join( ';', $request['postcodes'] ); + } + } +} diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-terms-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-terms-controller.php new file mode 100644 index 00000000000..40423decb89 --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-terms-controller.php @@ -0,0 +1,811 @@ +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' => array_merge( + $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + array( + 'name' => array( + 'type' => 'string', + 'description' => __( 'Name for the resource.', 'woocommerce' ), + 'required' => true, + ), + ) + ), + ), + '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.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 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 ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/batch', + array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Check if a given request has access to read the terms. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + $permissions = $this->check_permissions( $request, 'read' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; + } + + if ( ! $permissions ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to create a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + $permissions = $this->check_permissions( $request, 'create' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; + } + + if ( ! $permissions ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $permissions = $this->check_permissions( $request, 'read' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; + } + + if ( ! $permissions ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to update a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + $permissions = $this->check_permissions( $request, 'edit' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; + } + + if ( ! $permissions ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to delete a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function delete_item_permissions_check( $request ) { + $permissions = $this->check_permissions( $request, 'delete' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; + } + + if ( ! $permissions ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access batch create, update and delete items. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean|WP_Error + */ + public function batch_items_permissions_check( $request ) { + $permissions = $this->check_permissions( $request, 'batch' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; + } + + if ( ! $permissions ) { + return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check permissions. + * + * @param WP_REST_Request $request Full details about the request. + * @param string $context Request context. + * @return bool|WP_Error + */ + protected function check_permissions( $request, $context = 'read' ) { + // Get taxonomy. + $taxonomy = $this->get_taxonomy( $request ); + if ( ! $taxonomy || ! taxonomy_exists( $taxonomy ) ) { + return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Taxonomy does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + // Check permissions for a single term. + $id = intval( $request['id'] ); + if ( $id ) { + $term = get_term( $id, $taxonomy ); + + if ( is_wp_error( $term ) || ! $term || $term->taxonomy !== $taxonomy ) { + return new WP_Error( 'woocommerce_rest_term_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + return wc_rest_check_product_term_permissions( $taxonomy, $context, $term->term_id ); + } + + return wc_rest_check_product_term_permissions( $taxonomy, $context ); + } + + /** + * Get terms associated with a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function get_items( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + $prepared_args = array( + 'exclude' => $request['exclude'], + 'include' => $request['include'], + 'order' => $request['order'], + 'orderby' => $request['orderby'], + 'product' => $request['product'], + 'hide_empty' => $request['hide_empty'], + 'number' => $request['per_page'], + 'search' => $request['search'], + 'slug' => $request['slug'], + ); + + if ( ! empty( $request['offset'] ) ) { + $prepared_args['offset'] = $request['offset']; + } else { + $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; + } + + $taxonomy_obj = get_taxonomy( $taxonomy ); + + if ( $taxonomy_obj->hierarchical && isset( $request['parent'] ) ) { + if ( 0 === $request['parent'] ) { + // Only query top-level terms. + $prepared_args['parent'] = 0; + } else { + if ( $request['parent'] ) { + $prepared_args['parent'] = $request['parent']; + } + } + } + + /** + * Filter the query arguments, before passing them to `get_terms()`. + * + * Enables adding extra arguments or setting defaults for a terms + * collection request. + * + * @see https://developer.wordpress.org/reference/functions/get_terms/ + * + * @param array $prepared_args Array of arguments to be + * passed to get_terms. + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( "woocommerce_rest_{$taxonomy}_query", $prepared_args, $request ); + + if ( ! empty( $prepared_args['product'] ) ) { + $query_result = $this->get_terms_for_product( $prepared_args, $request ); + $total_terms = $this->total_terms; + } else { + $query_result = get_terms( $taxonomy, $prepared_args ); + + $count_args = $prepared_args; + unset( $count_args['number'] ); + unset( $count_args['offset'] ); + $total_terms = wp_count_terms( $taxonomy, $count_args ); + + // Ensure we don't return results when offset is out of bounds. + // See https://core.trac.wordpress.org/ticket/35935. + if ( $prepared_args['offset'] && $prepared_args['offset'] >= $total_terms ) { + $query_result = array(); + } + + // wp_count_terms can return a falsy value when the term has no children. + if ( ! $total_terms ) { + $total_terms = 0; + } + } + $response = array(); + foreach ( $query_result as $term ) { + $data = $this->prepare_item_for_response( $term, $request ); + $response[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $response ); + + // Store pagination values for headers then unset for count query. + $per_page = (int) $prepared_args['number']; + $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + + $response->header( 'X-WP-Total', (int) $total_terms ); + $max_pages = ceil( $total_terms / $per_page ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = str_replace( '(?P[\d]+)', $request['attribute_id'], $this->rest_base ); + $base = add_query_arg( $request->get_query_params(), rest_url( '/' . $this->namespace . '/' . $base ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Create a single term for a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function create_item( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + $name = $request['name']; + $args = array(); + $schema = $this->get_item_schema(); + + if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) { + $args['description'] = $request['description']; + } + if ( isset( $request['slug'] ) ) { + $args['slug'] = $request['slug']; + } + if ( isset( $request['parent'] ) ) { + if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { + return new WP_Error( 'woocommerce_rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.', 'woocommerce' ), array( 'status' => 400 ) ); + } + $args['parent'] = $request['parent']; + } + + $term = wp_insert_term( $name, $taxonomy, $args ); + if ( is_wp_error( $term ) ) { + $error_data = array( 'status' => 400 ); + + // If we're going to inform the client that the term exists, + // give them the identifier they can actually use. + $term_id = $term->get_error_data( 'term_exists' ); + if ( $term_id ) { + $error_data['resource_id'] = $term_id; + } + + return new WP_Error( $term->get_error_code(), $term->get_error_message(), $error_data ); + } + + $term = get_term( $term['term_id'], $taxonomy ); + + $this->update_additional_fields_for_object( $term, $request ); + + // Add term data. + $meta_fields = $this->update_term_meta_fields( $term, $request ); + if ( is_wp_error( $meta_fields ) ) { + wp_delete_term( $term->term_id, $taxonomy ); + + return $meta_fields; + } + + /** + * Fires after a single term is created or updated via the REST API. + * + * @param WP_Term $term Inserted Term object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating term, false when updating. + */ + do_action( "woocommerce_rest_insert_{$taxonomy}", $term, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $term, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + + $base = '/' . $this->namespace . '/' . $this->rest_base; + if ( ! empty( $request['attribute_id'] ) ) { + $base = str_replace( '(?P[\d]+)', (int) $request['attribute_id'], $base ); + } + + $response->header( 'Location', rest_url( $base . '/' . $term->term_id ) ); + + return $response; + } + + /** + * Get a single term from a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function get_item( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + $term = get_term( (int) $request['id'], $taxonomy ); + + if ( is_wp_error( $term ) ) { + return $term; + } + + $response = $this->prepare_item_for_response( $term, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Update a single term from a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function update_item( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + $term = get_term( (int) $request['id'], $taxonomy ); + $schema = $this->get_item_schema(); + $prepared_args = array(); + + if ( isset( $request['name'] ) ) { + $prepared_args['name'] = $request['name']; + } + if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) { + $prepared_args['description'] = $request['description']; + } + if ( isset( $request['slug'] ) ) { + $prepared_args['slug'] = $request['slug']; + } + if ( isset( $request['parent'] ) ) { + if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { + return new WP_Error( 'woocommerce_rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.', 'woocommerce' ), array( 'status' => 400 ) ); + } + $prepared_args['parent'] = $request['parent']; + } + + // Only update the term if we haz something to update. + if ( ! empty( $prepared_args ) ) { + $update = wp_update_term( $term->term_id, $term->taxonomy, $prepared_args ); + if ( is_wp_error( $update ) ) { + return $update; + } + } + + $term = get_term( (int) $request['id'], $taxonomy ); + + $this->update_additional_fields_for_object( $term, $request ); + + // Update term data. + $meta_fields = $this->update_term_meta_fields( $term, $request ); + if ( is_wp_error( $meta_fields ) ) { + return $meta_fields; + } + + /** + * Fires after a single term is created or updated via the REST API. + * + * @param WP_Term $term Inserted Term object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating term, false when updating. + */ + do_action( "woocommerce_rest_insert_{$taxonomy}", $term, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $term, $request ); + return rest_ensure_response( $response ); + } + + /** + * Delete a single term from a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $term = get_term( (int) $request['id'], $taxonomy ); + // Get default category id. + $default_category_id = absint( get_option( 'default_product_cat', 0 ) ); + + // Prevent deleting the default product category. + if ( $default_category_id === (int) $request['id'] ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Default product category cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $term, $request ); + + $retval = wp_delete_term( $term->term_id, $term->taxonomy ); + if ( ! $retval ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + // Schedule action to assign default category. + wc_get_container()->get( AssignDefaultCategory::class )->schedule_action(); + + /** + * Fires after a single term is deleted via the REST API. + * + * @param WP_Term $term The deleted term. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$taxonomy}", $term, $response, $request ); + + return $response; + } + + /** + * Prepare links for the request. + * + * @param object $term Term object. + * @param WP_REST_Request $request Full details about the request. + * @return array Links for the given term. + */ + protected function prepare_links( $term, $request ) { + $base = '/' . $this->namespace . '/' . $this->rest_base; + + if ( ! empty( $request['attribute_id'] ) ) { + $base = str_replace( '(?P[\d]+)', (int) $request['attribute_id'], $base ); + } + + $links = array( + 'self' => array( + 'href' => rest_url( trailingslashit( $base ) . $term->term_id ), + ), + 'collection' => array( + 'href' => rest_url( $base ), + ), + ); + + if ( $term->parent ) { + $parent_term = get_term( (int) $term->parent, $term->taxonomy ); + if ( $parent_term ) { + $links['up'] = array( + 'href' => rest_url( trailingslashit( $base ) . $parent_term->term_id ), + ); + } + } + + return $links; + } + + /** + * Update term meta fields. + * + * @param WP_Term $term Term object. + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + protected function update_term_meta_fields( $term, $request ) { + return true; + } + + /** + * Get the terms attached to a product. + * + * This is an alternative to `get_terms()` that uses `get_the_terms()` + * instead, which hits the object cache. There are a few things not + * supported, notably `include`, `exclude`. In `self::get_items()` these + * are instead treated as a full query. + * + * @param array $prepared_args Arguments for `get_terms()`. + * @param WP_REST_Request $request Full details about the request. + * @return array List of term objects. (Total count in `$this->total_terms`). + */ + protected function get_terms_for_product( $prepared_args, $request ) { + $taxonomy = $this->get_taxonomy( $request ); + + $query_result = get_the_terms( $prepared_args['product'], $taxonomy ); + if ( empty( $query_result ) ) { + $this->total_terms = 0; + return array(); + } + + // get_items() verifies that we don't have `include` set, and default. + // ordering is by `name`. + if ( ! in_array( $prepared_args['orderby'], array( 'name', 'none', 'include' ), true ) ) { + switch ( $prepared_args['orderby'] ) { + case 'id': + $this->sort_column = 'term_id'; + break; + case 'slug': + case 'term_group': + case 'description': + case 'count': + $this->sort_column = $prepared_args['orderby']; + break; + } + usort( $query_result, array( $this, 'compare_terms' ) ); + } + if ( strtolower( $prepared_args['order'] ) !== 'asc' ) { + $query_result = array_reverse( $query_result ); + } + + // Pagination. + $this->total_terms = count( $query_result ); + $query_result = array_slice( $query_result, $prepared_args['offset'], $prepared_args['number'] ); + + return $query_result; + } + + /** + * Comparison function for sorting terms by a column. + * + * Uses `$this->sort_column` to determine field to sort by. + * + * @param stdClass $left Term object. + * @param stdClass $right Term object. + * @return int <0 if left is higher "priority" than right, 0 if equal, >0 if right is higher "priority" than left. + */ + protected function compare_terms( $left, $right ) { + $col = $this->sort_column; + $left_val = $left->$col; + $right_val = $right->$col; + + if ( is_int( $left_val ) && is_int( $right_val ) ) { + return $left_val - $right_val; + } + + return strcmp( $left_val, $right_val ); + } + + /** + * Get the query params for collections + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['context']['default'] = 'view'; + + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items. Applies to hierarchical taxonomies only.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_key', + 'default' => 'asc', + 'enum' => array( + 'asc', + 'desc', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'description' => __( 'Sort collection by resource attribute.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_key', + 'default' => 'name', + 'enum' => array( + 'id', + 'include', + 'name', + 'slug', + 'term_group', + 'description', + 'count', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['hide_empty'] = array( + 'description' => __( 'Whether to hide resources not assigned to any products.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['parent'] = array( + 'description' => __( 'Limit result set to resources assigned to a specific parent. Applies to hierarchical taxonomies only.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['product'] = array( + 'description' => __( 'Limit result set to resources assigned to a specific product.', 'woocommerce' ), + 'type' => 'integer', + 'default' => null, + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['slug'] = array( + 'description' => __( 'Limit result set to resources with a specific slug.', 'woocommerce' ), + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } + + /** + * Get taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return int|WP_Error + */ + protected function get_taxonomy( $request ) { + $attribute_id = $request['attribute_id']; + + if ( empty( $attribute_id ) ) { + return $this->taxonomy; + } + + if ( isset( $this->taxonomies_by_id[ $attribute_id ] ) ) { + return $this->taxonomies_by_id[ $attribute_id ]; + } + + $taxonomy = WC()->call_function( 'wc_attribute_taxonomy_name_by_id', (int) $request['attribute_id'] ); + if ( ! empty( $taxonomy ) ) { + $this->taxonomy = $taxonomy; + $this->taxonomies_by_id[ $attribute_id ] = $taxonomy; + } + + return $taxonomy; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller.php similarity index 100% rename from includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller.php rename to plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller.php diff --git a/plugins/woocommerce/includes/rest-api/Package.php b/plugins/woocommerce/includes/rest-api/Package.php new file mode 100644 index 00000000000..0bb8b7ca4e2 --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Package.php @@ -0,0 +1,60 @@ +init() + */ + public static function init() { + wc_deprecated_function( 'Automattic\WooCommerce\RestApi\Server::instance()->init()', '4.5.0' ); + \Automattic\WooCommerce\RestApi\Server::instance()->init(); + } + + /** + * Return the version of the package. + * + * @deprecated since 4.5.0. This tracks WooCommerce version now. + * @return string + */ + public static function get_version() { + wc_deprecated_function( 'WC()->version', '4.5.0' ); + return WC()->version; + } + + /** + * Return the path to the package. + * + * @deprecated since 4.5.0. Directly call Automattic\WooCommerce\RestApi\Server::get_path() + * @return string + */ + public static function get_path() { + wc_deprecated_function( 'Automattic\WooCommerce\RestApi\Server::get_path()', '4.5.0' ); + return \Automattic\WooCommerce\RestApi\Server::get_path(); + } +} diff --git a/plugins/woocommerce/includes/rest-api/Server.php b/plugins/woocommerce/includes/rest-api/Server.php new file mode 100644 index 00000000000..b8d863f3253 --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Server.php @@ -0,0 +1,202 @@ +get_rest_namespaces() as $namespace => $controllers ) { + foreach ( $controllers as $controller_name => $controller_class ) { + $this->controllers[ $namespace ][ $controller_name ] = new $controller_class(); + $this->controllers[ $namespace ][ $controller_name ]->register_routes(); + } + } + } + + /** + * Get API namespaces - new namespaces should be registered here. + * + * @return array List of Namespaces and Main controller classes. + */ + protected function get_rest_namespaces() { + return apply_filters( + 'woocommerce_rest_api_get_rest_namespaces', + array( + 'wc/v1' => $this->get_v1_controllers(), + 'wc/v2' => $this->get_v2_controllers(), + 'wc/v3' => $this->get_v3_controllers(), + 'wc-telemetry' => $this->get_telemetry_controllers(), + ) + ); + } + + /** + * List of controllers in the wc/v1 namespace. + * + * @return array + */ + protected function get_v1_controllers() { + return array( + 'coupons' => 'WC_REST_Coupons_V1_Controller', + 'customer-downloads' => 'WC_REST_Customer_Downloads_V1_Controller', + 'customers' => 'WC_REST_Customers_V1_Controller', + 'order-notes' => 'WC_REST_Order_Notes_V1_Controller', + 'order-refunds' => 'WC_REST_Order_Refunds_V1_Controller', + 'orders' => 'WC_REST_Orders_V1_Controller', + 'product-attribute-terms' => 'WC_REST_Product_Attribute_Terms_V1_Controller', + 'product-attributes' => 'WC_REST_Product_Attributes_V1_Controller', + 'product-categories' => 'WC_REST_Product_Categories_V1_Controller', + 'product-reviews' => 'WC_REST_Product_Reviews_V1_Controller', + 'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_V1_Controller', + 'product-tags' => 'WC_REST_Product_Tags_V1_Controller', + 'products' => 'WC_REST_Products_V1_Controller', + 'reports-sales' => 'WC_REST_Report_Sales_V1_Controller', + 'reports-top-sellers' => 'WC_REST_Report_Top_Sellers_V1_Controller', + 'reports' => 'WC_REST_Reports_V1_Controller', + 'tax-classes' => 'WC_REST_Tax_Classes_V1_Controller', + 'taxes' => 'WC_REST_Taxes_V1_Controller', + 'webhooks' => 'WC_REST_Webhooks_V1_Controller', + 'webhook-deliveries' => 'WC_REST_Webhook_Deliveries_V1_Controller', + ); + } + + /** + * List of controllers in the wc/v2 namespace. + * + * @return array + */ + protected function get_v2_controllers() { + return array( + 'coupons' => 'WC_REST_Coupons_V2_Controller', + 'customer-downloads' => 'WC_REST_Customer_Downloads_V2_Controller', + 'customers' => 'WC_REST_Customers_V2_Controller', + 'network-orders' => 'WC_REST_Network_Orders_V2_Controller', + 'order-notes' => 'WC_REST_Order_Notes_V2_Controller', + 'order-refunds' => 'WC_REST_Order_Refunds_V2_Controller', + 'orders' => 'WC_REST_Orders_V2_Controller', + 'product-attribute-terms' => 'WC_REST_Product_Attribute_Terms_V2_Controller', + 'product-attributes' => 'WC_REST_Product_Attributes_V2_Controller', + 'product-categories' => 'WC_REST_Product_Categories_V2_Controller', + 'product-reviews' => 'WC_REST_Product_Reviews_V2_Controller', + 'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_V2_Controller', + 'product-tags' => 'WC_REST_Product_Tags_V2_Controller', + 'products' => 'WC_REST_Products_V2_Controller', + 'product-variations' => 'WC_REST_Product_Variations_V2_Controller', + 'reports-sales' => 'WC_REST_Report_Sales_V2_Controller', + 'reports-top-sellers' => 'WC_REST_Report_Top_Sellers_V2_Controller', + 'reports' => 'WC_REST_Reports_V2_Controller', + 'settings' => 'WC_REST_Settings_V2_Controller', + 'settings-options' => 'WC_REST_Setting_Options_V2_Controller', + 'shipping-zones' => 'WC_REST_Shipping_Zones_V2_Controller', + 'shipping-zone-locations' => 'WC_REST_Shipping_Zone_Locations_V2_Controller', + 'shipping-zone-methods' => 'WC_REST_Shipping_Zone_Methods_V2_Controller', + 'tax-classes' => 'WC_REST_Tax_Classes_V2_Controller', + 'taxes' => 'WC_REST_Taxes_V2_Controller', + 'webhooks' => 'WC_REST_Webhooks_V2_Controller', + 'webhook-deliveries' => 'WC_REST_Webhook_Deliveries_V2_Controller', + 'system-status' => 'WC_REST_System_Status_V2_Controller', + 'system-status-tools' => 'WC_REST_System_Status_Tools_V2_Controller', + 'shipping-methods' => 'WC_REST_Shipping_Methods_V2_Controller', + 'payment-gateways' => 'WC_REST_Payment_Gateways_V2_Controller', + ); + } + + /** + * List of controllers in the wc/v3 namespace. + * + * @return array + */ + protected function get_v3_controllers() { + return array( + 'coupons' => 'WC_REST_Coupons_Controller', + 'customer-downloads' => 'WC_REST_Customer_Downloads_Controller', + 'customers' => 'WC_REST_Customers_Controller', + 'network-orders' => 'WC_REST_Network_Orders_Controller', + 'order-notes' => 'WC_REST_Order_Notes_Controller', + 'order-refunds' => 'WC_REST_Order_Refunds_Controller', + 'orders' => 'WC_REST_Orders_Controller', + 'product-attribute-terms' => 'WC_REST_Product_Attribute_Terms_Controller', + 'product-attributes' => 'WC_REST_Product_Attributes_Controller', + 'product-categories' => 'WC_REST_Product_Categories_Controller', + 'product-reviews' => 'WC_REST_Product_Reviews_Controller', + 'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_Controller', + 'product-tags' => 'WC_REST_Product_Tags_Controller', + 'products' => 'WC_REST_Products_Controller', + 'product-variations' => 'WC_REST_Product_Variations_Controller', + 'reports-sales' => 'WC_REST_Report_Sales_Controller', + 'reports-top-sellers' => 'WC_REST_Report_Top_Sellers_Controller', + 'reports-orders-totals' => 'WC_REST_Report_Orders_Totals_Controller', + 'reports-products-totals' => 'WC_REST_Report_Products_Totals_Controller', + 'reports-customers-totals' => 'WC_REST_Report_Customers_Totals_Controller', + 'reports-coupons-totals' => 'WC_REST_Report_Coupons_Totals_Controller', + 'reports-reviews-totals' => 'WC_REST_Report_Reviews_Totals_Controller', + 'reports' => 'WC_REST_Reports_Controller', + 'settings' => 'WC_REST_Settings_Controller', + 'settings-options' => 'WC_REST_Setting_Options_Controller', + 'shipping-zones' => 'WC_REST_Shipping_Zones_Controller', + 'shipping-zone-locations' => 'WC_REST_Shipping_Zone_Locations_Controller', + 'shipping-zone-methods' => 'WC_REST_Shipping_Zone_Methods_Controller', + 'tax-classes' => 'WC_REST_Tax_Classes_Controller', + 'taxes' => 'WC_REST_Taxes_Controller', + 'webhooks' => 'WC_REST_Webhooks_Controller', + 'system-status' => 'WC_REST_System_Status_Controller', + 'system-status-tools' => 'WC_REST_System_Status_Tools_Controller', + 'shipping-methods' => 'WC_REST_Shipping_Methods_Controller', + 'payment-gateways' => 'WC_REST_Payment_Gateways_Controller', + 'data' => 'WC_REST_Data_Controller', + 'data-continents' => 'WC_REST_Data_Continents_Controller', + 'data-countries' => 'WC_REST_Data_Countries_Controller', + 'data-currencies' => 'WC_REST_Data_Currencies_Controller', + ); + } + + /** + * List of controllers in the telemetry namespace. + * + * @return array + */ + protected function get_telemetry_controllers() { + return array( + 'tracker' => 'WC_REST_Telemetry_Controller', + ); + } + + /** + * Return the path to the package. + * + * @return string + */ + public static function get_path() { + return dirname( __DIR__ ); + } +} diff --git a/includes/rest-api/Utilities/ImageAttachment.php b/plugins/woocommerce/includes/rest-api/Utilities/ImageAttachment.php similarity index 100% rename from includes/rest-api/Utilities/ImageAttachment.php rename to plugins/woocommerce/includes/rest-api/Utilities/ImageAttachment.php diff --git a/includes/rest-api/Utilities/SingletonTrait.php b/plugins/woocommerce/includes/rest-api/Utilities/SingletonTrait.php similarity index 100% rename from includes/rest-api/Utilities/SingletonTrait.php rename to plugins/woocommerce/includes/rest-api/Utilities/SingletonTrait.php diff --git a/includes/shipping/flat-rate/class-wc-shipping-flat-rate.php b/plugins/woocommerce/includes/shipping/flat-rate/class-wc-shipping-flat-rate.php similarity index 100% rename from includes/shipping/flat-rate/class-wc-shipping-flat-rate.php rename to plugins/woocommerce/includes/shipping/flat-rate/class-wc-shipping-flat-rate.php diff --git a/includes/shipping/flat-rate/includes/settings-flat-rate.php b/plugins/woocommerce/includes/shipping/flat-rate/includes/settings-flat-rate.php similarity index 100% rename from includes/shipping/flat-rate/includes/settings-flat-rate.php rename to plugins/woocommerce/includes/shipping/flat-rate/includes/settings-flat-rate.php diff --git a/plugins/woocommerce/includes/shipping/free-shipping/class-wc-shipping-free-shipping.php b/plugins/woocommerce/includes/shipping/free-shipping/class-wc-shipping-free-shipping.php new file mode 100644 index 00000000000..00d813f6d24 --- /dev/null +++ b/plugins/woocommerce/includes/shipping/free-shipping/class-wc-shipping-free-shipping.php @@ -0,0 +1,245 @@ +id = 'free_shipping'; + $this->instance_id = absint( $instance_id ); + $this->method_title = __( 'Free shipping', 'woocommerce' ); + $this->method_description = __( 'Free shipping is a special method which can be triggered with coupons and minimum spends.', 'woocommerce' ); + $this->supports = array( + 'shipping-zones', + 'instance-settings', + 'instance-settings-modal', + ); + + $this->init(); + } + + /** + * Initialize free shipping. + */ + public function init() { + // Load the settings. + $this->init_form_fields(); + $this->init_settings(); + + // Define user set variables. + $this->title = $this->get_option( 'title' ); + $this->min_amount = $this->get_option( 'min_amount', 0 ); + $this->requires = $this->get_option( 'requires' ); + $this->ignore_discounts = $this->get_option( 'ignore_discounts' ); + + // Actions. + add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); + add_action( 'admin_footer', array( 'WC_Shipping_Free_Shipping', 'enqueue_admin_js' ), 10 ); // Priority needs to be higher than wc_print_js (25). + } + + /** + * Init form fields. + */ + public function init_form_fields() { + $this->instance_form_fields = array( + 'title' => array( + 'title' => __( 'Title', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), + 'default' => $this->method_title, + 'desc_tip' => true, + ), + 'requires' => array( + 'title' => __( 'Free shipping requires...', 'woocommerce' ), + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'default' => '', + 'options' => array( + '' => __( 'N/A', 'woocommerce' ), + 'coupon' => __( 'A valid free shipping coupon', 'woocommerce' ), + 'min_amount' => __( 'A minimum order amount', 'woocommerce' ), + 'either' => __( 'A minimum order amount OR a coupon', 'woocommerce' ), + 'both' => __( 'A minimum order amount AND a coupon', 'woocommerce' ), + ), + ), + 'min_amount' => array( + 'title' => __( 'Minimum order amount', 'woocommerce' ), + 'type' => 'price', + 'placeholder' => wc_format_localized_price( 0 ), + 'description' => __( 'Users will need to spend this amount to get free shipping (if enabled above).', 'woocommerce' ), + 'default' => '0', + 'desc_tip' => true, + ), + 'ignore_discounts' => array( + 'title' => __( 'Coupons discounts', 'woocommerce' ), + 'label' => __( 'Apply minimum order rule before coupon discount', 'woocommerce' ), + 'type' => 'checkbox', + 'description' => __( 'If checked, free shipping would be available based on pre-discount order amount.', 'woocommerce' ), + 'default' => 'no', + 'desc_tip' => true, + ), + ); + } + + /** + * Get setting form fields for instances of this shipping method within zones. + * + * @return array + */ + public function get_instance_form_fields() { + return parent::get_instance_form_fields(); + } + + /** + * See if free shipping is available based on the package and cart. + * + * @param array $package Shipping package. + * @return bool + */ + public function is_available( $package ) { + $has_coupon = false; + $has_met_min_amount = false; + + if ( in_array( $this->requires, array( 'coupon', 'either', 'both' ), true ) ) { + $coupons = WC()->cart->get_coupons(); + + if ( $coupons ) { + foreach ( $coupons as $code => $coupon ) { + if ( $coupon->is_valid() && $coupon->get_free_shipping() ) { + $has_coupon = true; + break; + } + } + } + } + + if ( in_array( $this->requires, array( 'min_amount', 'either', 'both' ), true ) ) { + $total = WC()->cart->get_displayed_subtotal(); + + if ( WC()->cart->display_prices_including_tax() ) { + $total = $total - WC()->cart->get_discount_tax(); + } + + if ( 'no' === $this->ignore_discounts ) { + $total = $total - WC()->cart->get_discount_total(); + } + + $total = NumberUtil::round( $total, wc_get_price_decimals() ); + + if ( $total >= $this->min_amount ) { + $has_met_min_amount = true; + } + } + + switch ( $this->requires ) { + case 'min_amount': + $is_available = $has_met_min_amount; + break; + case 'coupon': + $is_available = $has_coupon; + break; + case 'both': + $is_available = $has_met_min_amount && $has_coupon; + break; + case 'either': + $is_available = $has_met_min_amount || $has_coupon; + break; + default: + $is_available = true; + break; + } + + return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', $is_available, $package, $this ); + } + + /** + * Called to calculate shipping rates for this method. Rates can be added using the add_rate() method. + * + * @uses WC_Shipping_Method::add_rate() + * + * @param array $package Shipping package. + */ + public function calculate_shipping( $package = array() ) { + $this->add_rate( + array( + 'label' => $this->title, + 'cost' => 0, + 'taxes' => false, + 'package' => $package, + ) + ); + } + + /** + * Enqueue JS to handle free shipping options. + * + * Static so that's enqueued only once. + */ + public static function enqueue_admin_js() { + wc_enqueue_js( + "jQuery( function( $ ) { + function wcFreeShippingShowHideMinAmountField( el ) { + var form = $( el ).closest( 'form' ); + var minAmountField = $( '#woocommerce_free_shipping_min_amount', form ).closest( 'tr' ); + var ignoreDiscountField = $( '#woocommerce_free_shipping_ignore_discounts', form ).closest( 'tr' ); + if ( 'coupon' === $( el ).val() || '' === $( el ).val() ) { + minAmountField.hide(); + ignoreDiscountField.hide(); + } else { + minAmountField.show(); + ignoreDiscountField.show(); + } + } + + $( document.body ).on( 'change', '#woocommerce_free_shipping_requires', function() { + wcFreeShippingShowHideMinAmountField( this ); + }); + + // Change while load. + $( '#woocommerce_free_shipping_requires' ).trigger( 'change' ); + $( document.body ).on( 'wc_backbone_modal_loaded', function( evt, target ) { + if ( 'wc-modal-shipping-method-settings' === target ) { + wcFreeShippingShowHideMinAmountField( $( '#wc-backbone-modal-dialog #woocommerce_free_shipping_requires', evt.currentTarget ) ); + } + } ); + });" + ); + } +} diff --git a/plugins/woocommerce/includes/shipping/legacy-flat-rate/class-wc-shipping-legacy-flat-rate.php b/plugins/woocommerce/includes/shipping/legacy-flat-rate/class-wc-shipping-legacy-flat-rate.php new file mode 100644 index 00000000000..c21bc7e4438 --- /dev/null +++ b/plugins/woocommerce/includes/shipping/legacy-flat-rate/class-wc-shipping-legacy-flat-rate.php @@ -0,0 +1,411 @@ +id = 'legacy_flat_rate'; + $this->method_title = __( 'Flat rate (legacy)', 'woocommerce' ); + /* translators: %s: Admin shipping settings URL */ + $this->method_description = '' . sprintf( __( 'This method is deprecated in 2.6.0 and will be removed in future versions - we recommend disabling it and instead setting up a new rate within your Shipping zones.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ) . ''; + $this->init(); + + add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); + add_action( 'woocommerce_flat_rate_shipping_add_rate', array( $this, 'calculate_extra_shipping' ), 10, 2 ); + } + + /** + * Process and redirect if disabled. + */ + public function process_admin_options() { + parent::process_admin_options(); + + if ( 'no' === $this->settings['enabled'] ) { + wp_redirect( admin_url( 'admin.php?page=wc-settings&tab=shipping§ion=options' ) ); + exit; + } + } + + /** + * Return the name of the option in the WP DB. + * + * @since 2.6.0 + * @return string + */ + public function get_option_key() { + return $this->plugin_id . 'flat_rate_settings'; + } + + /** + * Init function. + */ + public function init() { + // Load the settings. + $this->init_form_fields(); + $this->init_settings(); + + // Define user set variables. + $this->title = $this->get_option( 'title' ); + $this->availability = $this->get_option( 'availability' ); + $this->countries = $this->get_option( 'countries' ); + $this->tax_status = $this->get_option( 'tax_status' ); + $this->cost = $this->get_option( 'cost' ); + $this->type = $this->get_option( 'type', 'class' ); + $this->options = $this->get_option( 'options', false ); // @deprecated 2.4.0 + } + + /** + * Initialise Settings Form Fields. + */ + public function init_form_fields() { + $this->form_fields = include __DIR__ . '/includes/settings-flat-rate.php'; + } + + /** + * Evaluate a cost from a sum/string. + * + * @param string $sum Sum to evaluate. + * @param array $args Arguments. + * @return string + */ + protected function evaluate_cost( $sum, $args = array() ) { + include_once WC()->plugin_path() . '/includes/libraries/class-wc-eval-math.php'; + + $locale = localeconv(); + $decimals = array( wc_get_price_decimal_separator(), $locale['decimal_point'], $locale['mon_decimal_point'] ); + + $this->fee_cost = $args['cost']; + + // Expand shortcodes. + add_shortcode( 'fee', array( $this, 'fee' ) ); + + $sum = do_shortcode( + str_replace( + array( + '[qty]', + '[cost]', + ), + array( + $args['qty'], + $args['cost'], + ), + $sum + ) + ); + + remove_shortcode( 'fee', array( $this, 'fee' ) ); + + // Remove whitespace from string. + $sum = preg_replace( '/\s+/', '', $sum ); + + // Remove locale from string. + $sum = str_replace( $decimals, '.', $sum ); + + // Trim invalid start/end characters. + $sum = rtrim( ltrim( $sum, "\t\n\r\0\x0B+*/" ), "\t\n\r\0\x0B+-*/" ); + + // Do the math. + return $sum ? WC_Eval_Math::evaluate( $sum ) : 0; + } + + /** + * Work out fee (shortcode). + * + * @param array $atts Shortcode attributes. + * @return string + */ + public function fee( $atts ) { + $atts = shortcode_atts( + array( + 'percent' => '', + 'min_fee' => '', + ), + $atts, + 'fee' + ); + + $calculated_fee = 0; + + if ( $atts['percent'] ) { + $calculated_fee = $this->fee_cost * ( floatval( $atts['percent'] ) / 100 ); + } + + if ( $atts['min_fee'] && $calculated_fee < $atts['min_fee'] ) { + $calculated_fee = $atts['min_fee']; + } + + return $calculated_fee; + } + + /** + * Calculate shipping. + * + * @param array $package (default: array()). + */ + public function calculate_shipping( $package = array() ) { + $rate = array( + 'id' => $this->id, + 'label' => $this->title, + 'cost' => 0, + 'package' => $package, + ); + + // Calculate the costs. + $has_costs = false; // True when a cost is set. False if all costs are blank strings. + $cost = $this->get_option( 'cost' ); + + if ( '' !== $cost ) { + $has_costs = true; + $rate['cost'] = $this->evaluate_cost( + $cost, + array( + 'qty' => $this->get_package_item_qty( $package ), + 'cost' => $package['contents_cost'], + ) + ); + } + + // Add shipping class costs. + $found_shipping_classes = $this->find_shipping_classes( $package ); + $highest_class_cost = 0; + + foreach ( $found_shipping_classes as $shipping_class => $products ) { + // Also handles BW compatibility when slugs were used instead of ids. + $shipping_class_term = get_term_by( 'slug', $shipping_class, 'product_shipping_class' ); + $class_cost_string = $shipping_class_term && $shipping_class_term->term_id ? $this->get_option( 'class_cost_' . $shipping_class_term->term_id, $this->get_option( 'class_cost_' . $shipping_class, '' ) ) : $this->get_option( 'no_class_cost', '' ); + + if ( '' === $class_cost_string ) { + continue; + } + + $has_costs = true; + $class_cost = $this->evaluate_cost( + $class_cost_string, + array( + 'qty' => array_sum( wp_list_pluck( $products, 'quantity' ) ), + 'cost' => array_sum( wp_list_pluck( $products, 'line_total' ) ), + ) + ); + + if ( 'class' === $this->type ) { + $rate['cost'] += $class_cost; + } else { + $highest_class_cost = $class_cost > $highest_class_cost ? $class_cost : $highest_class_cost; + } + } + + if ( 'order' === $this->type && $highest_class_cost ) { + $rate['cost'] += $highest_class_cost; + } + + $rate['package'] = $package; + + // Add the rate. + if ( $has_costs ) { + $this->add_rate( $rate ); + } + + /** + * Developers can add additional flat rates based on this one via this action since @version 2.4. + * + * Previously there were (overly complex) options to add additional rates however this was not user. + * friendly and goes against what Flat Rate Shipping was originally intended for. + * + * This example shows how you can add an extra rate based on this flat rate via custom function: + * + * add_action( 'woocommerce_flat_rate_shipping_add_rate', 'add_another_custom_flat_rate', 10, 2 ); + * + * function add_another_custom_flat_rate( $method, $rate ) { + * $new_rate = $rate; + * $new_rate['id'] .= ':' . 'custom_rate_name'; // Append a custom ID. + * $new_rate['label'] = 'Rushed Shipping'; // Rename to 'Rushed Shipping'. + * $new_rate['cost'] += 2; // Add $2 to the cost. + * + * // Add it to WC. + * $method->add_rate( $new_rate ); + * }. + */ + do_action( 'woocommerce_flat_rate_shipping_add_rate', $this, $rate ); + } + + /** + * Get items in package. + * + * @param array $package Package information. + * @return int + */ + public function get_package_item_qty( $package ) { + $total_quantity = 0; + foreach ( $package['contents'] as $item_id => $values ) { + if ( $values['quantity'] > 0 && $values['data']->needs_shipping() ) { + $total_quantity += $values['quantity']; + } + } + return $total_quantity; + } + + /** + * Finds and returns shipping classes and the products with said class. + * + * @param mixed $package Package information. + * @return array + */ + public function find_shipping_classes( $package ) { + $found_shipping_classes = array(); + + foreach ( $package['contents'] as $item_id => $values ) { + if ( $values['data']->needs_shipping() ) { + $found_class = $values['data']->get_shipping_class(); + + if ( ! isset( $found_shipping_classes[ $found_class ] ) ) { + $found_shipping_classes[ $found_class ] = array(); + } + + $found_shipping_classes[ $found_class ][ $item_id ] = $values; + } + } + + return $found_shipping_classes; + } + + /** + * Adds extra calculated flat rates. + * + * @deprecated 2.4.0 + * + * Additional rates defined like this: + * Option Name | Additional Cost [+- Percents%] | Per Cost Type (order, class, or item). + * + * @param null $method Deprecated. + * @param array $rate Rate information. + */ + public function calculate_extra_shipping( $method, $rate ) { + if ( $this->options ) { + $options = array_filter( (array) explode( "\n", $this->options ) ); + + foreach ( $options as $option ) { + $this_option = array_map( 'trim', explode( WC_DELIMITER, $option ) ); + if ( count( $this_option ) !== 3 ) { + continue; + } + $extra_rate = $rate; + $extra_rate['id'] = $this->id . ':' . urldecode( sanitize_title( $this_option[0] ) ); + $extra_rate['label'] = $this_option[0]; + $extra_cost = $this->get_extra_cost( $this_option[1], $this_option[2], $rate['package'] ); + if ( is_array( $extra_rate['cost'] ) ) { + $extra_rate['cost']['order'] = $extra_rate['cost']['order'] + $extra_cost; + } else { + $extra_rate['cost'] += $extra_cost; + } + + $this->add_rate( $extra_rate ); + } + } + } + + /** + * Calculate the percentage adjustment for each shipping rate. + * + * @deprecated 2.4.0 + * @param float $cost Cost. + * @param float $percent_adjustment Percent adjustment. + * @param string $percent_operator Percent operator. + * @param float $base_price Base price. + * @return float + */ + public function calc_percentage_adjustment( $cost, $percent_adjustment, $percent_operator, $base_price ) { + if ( '+' === $percent_operator ) { + $cost += $percent_adjustment * $base_price; + } else { + $cost -= $percent_adjustment * $base_price; + } + return $cost; + } + + /** + * Get extra cost. + * + * @deprecated 2.4.0 + * @param string $cost_string Cost string. + * @param string $type Type. + * @param array $package Package information. + * @return float + */ + public function get_extra_cost( $cost_string, $type, $package ) { + $cost = $cost_string; + $cost_percent = false; + // @codingStandardsIgnoreStart + $pattern = + '/' . // Start regex. + '(\d+\.?\d*)' . // Capture digits, optionally capture a `.` and more digits. + '\s*' . // Match whitespace. + '(\+|-)' . // Capture the operand. + '\s*' . // Match whitespace. + '(\d+\.?\d*)' . // Capture digits, optionally capture a `.` and more digits. + '\%/'; // Match the percent sign & end regex. + // @codingStandardsIgnoreEnd + if ( preg_match( $pattern, $cost_string, $this_cost_matches ) ) { + $cost_operator = $this_cost_matches[2]; + $cost_percent = $this_cost_matches[3] / 100; + $cost = $this_cost_matches[1]; + } + switch ( $type ) { + case 'class': + $cost = $cost * count( $this->find_shipping_classes( $package ) ); + break; + case 'item': + $cost = $cost * $this->get_package_item_qty( $package ); + break; + } + if ( $cost_percent ) { + switch ( $type ) { + case 'class': + $shipping_classes = $this->find_shipping_classes( $package ); + foreach ( $shipping_classes as $shipping_class => $items ) { + foreach ( $items as $item_id => $values ) { + $cost = $this->calc_percentage_adjustment( $cost, $cost_percent, $cost_operator, $values['line_total'] ); + } + } + break; + case 'item': + foreach ( $package['contents'] as $item_id => $values ) { + if ( $values['data']->needs_shipping() ) { + $cost = $this->calc_percentage_adjustment( $cost, $cost_percent, $cost_operator, $values['line_total'] ); + } + } + break; + case 'order': + $cost = $this->calc_percentage_adjustment( $cost, $cost_percent, $cost_operator, $package['contents_cost'] ); + break; + } + } + return $cost; + } +} diff --git a/includes/shipping/legacy-flat-rate/includes/settings-flat-rate.php b/plugins/woocommerce/includes/shipping/legacy-flat-rate/includes/settings-flat-rate.php similarity index 100% rename from includes/shipping/legacy-flat-rate/includes/settings-flat-rate.php rename to plugins/woocommerce/includes/shipping/legacy-flat-rate/includes/settings-flat-rate.php diff --git a/includes/shipping/legacy-free-shipping/class-wc-shipping-legacy-free-shipping.php b/plugins/woocommerce/includes/shipping/legacy-free-shipping/class-wc-shipping-legacy-free-shipping.php similarity index 100% rename from includes/shipping/legacy-free-shipping/class-wc-shipping-legacy-free-shipping.php rename to plugins/woocommerce/includes/shipping/legacy-free-shipping/class-wc-shipping-legacy-free-shipping.php diff --git a/includes/shipping/legacy-international-delivery/class-wc-shipping-legacy-international-delivery.php b/plugins/woocommerce/includes/shipping/legacy-international-delivery/class-wc-shipping-legacy-international-delivery.php similarity index 100% rename from includes/shipping/legacy-international-delivery/class-wc-shipping-legacy-international-delivery.php rename to plugins/woocommerce/includes/shipping/legacy-international-delivery/class-wc-shipping-legacy-international-delivery.php diff --git a/includes/shipping/legacy-local-delivery/class-wc-shipping-legacy-local-delivery.php b/plugins/woocommerce/includes/shipping/legacy-local-delivery/class-wc-shipping-legacy-local-delivery.php similarity index 100% rename from includes/shipping/legacy-local-delivery/class-wc-shipping-legacy-local-delivery.php rename to plugins/woocommerce/includes/shipping/legacy-local-delivery/class-wc-shipping-legacy-local-delivery.php diff --git a/includes/shipping/legacy-local-pickup/class-wc-shipping-legacy-local-pickup.php b/plugins/woocommerce/includes/shipping/legacy-local-pickup/class-wc-shipping-legacy-local-pickup.php similarity index 100% rename from includes/shipping/legacy-local-pickup/class-wc-shipping-legacy-local-pickup.php rename to plugins/woocommerce/includes/shipping/legacy-local-pickup/class-wc-shipping-legacy-local-pickup.php diff --git a/includes/shipping/local-pickup/class-wc-shipping-local-pickup.php b/plugins/woocommerce/includes/shipping/local-pickup/class-wc-shipping-local-pickup.php similarity index 100% rename from includes/shipping/local-pickup/class-wc-shipping-local-pickup.php rename to plugins/woocommerce/includes/shipping/local-pickup/class-wc-shipping-local-pickup.php diff --git a/includes/shortcodes/class-wc-shortcode-cart.php b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-cart.php similarity index 100% rename from includes/shortcodes/class-wc-shortcode-cart.php rename to plugins/woocommerce/includes/shortcodes/class-wc-shortcode-cart.php diff --git a/includes/shortcodes/class-wc-shortcode-checkout.php b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-checkout.php similarity index 100% rename from includes/shortcodes/class-wc-shortcode-checkout.php rename to plugins/woocommerce/includes/shortcodes/class-wc-shortcode-checkout.php diff --git a/includes/shortcodes/class-wc-shortcode-my-account.php b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php similarity index 100% rename from includes/shortcodes/class-wc-shortcode-my-account.php rename to plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php diff --git a/includes/shortcodes/class-wc-shortcode-order-tracking.php b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-order-tracking.php similarity index 100% rename from includes/shortcodes/class-wc-shortcode-order-tracking.php rename to plugins/woocommerce/includes/shortcodes/class-wc-shortcode-order-tracking.php diff --git a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-products.php b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-products.php new file mode 100644 index 00000000000..25ac8223eda --- /dev/null +++ b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-products.php @@ -0,0 +1,703 @@ +type = $type; + $this->attributes = $this->parse_attributes( $attributes ); + $this->query_args = $this->parse_query_args(); + } + + /** + * Get shortcode attributes. + * + * @since 3.2.0 + * @return array + */ + public function get_attributes() { + return $this->attributes; + } + + /** + * Get query args. + * + * @since 3.2.0 + * @return array + */ + public function get_query_args() { + return $this->query_args; + } + + /** + * Get shortcode type. + * + * @since 3.2.0 + * @return string + */ + public function get_type() { + return $this->type; + } + + /** + * Get shortcode content. + * + * @since 3.2.0 + * @return string + */ + public function get_content() { + return $this->product_loop(); + } + + /** + * Parse attributes. + * + * @since 3.2.0 + * @param array $attributes Shortcode attributes. + * @return array + */ + protected function parse_attributes( $attributes ) { + $attributes = $this->parse_legacy_attributes( $attributes ); + + $attributes = shortcode_atts( + array( + 'limit' => '-1', // Results limit. + 'columns' => '', // Number of columns. + 'rows' => '', // Number of rows. If defined, limit will be ignored. + 'orderby' => '', // menu_order, title, date, rand, price, popularity, rating, or id. + 'order' => '', // ASC or DESC. + 'ids' => '', // Comma separated IDs. + 'skus' => '', // Comma separated SKUs. + 'category' => '', // Comma separated category slugs or ids. + 'cat_operator' => 'IN', // Operator to compare categories. Possible values are 'IN', 'NOT IN', 'AND'. + 'attribute' => '', // Single attribute slug. + 'terms' => '', // Comma separated term slugs or ids. + 'terms_operator' => 'IN', // Operator to compare terms. Possible values are 'IN', 'NOT IN', 'AND'. + 'tag' => '', // Comma separated tag slugs. + 'tag_operator' => 'IN', // Operator to compare tags. Possible values are 'IN', 'NOT IN', 'AND'. + 'visibility' => 'visible', // Product visibility setting. Possible values are 'visible', 'catalog', 'search', 'hidden'. + 'class' => '', // HTML class. + 'page' => 1, // Page for pagination. + 'paginate' => false, // Should results be paginated. + 'cache' => true, // Should shortcode output be cached. + ), + $attributes, + $this->type + ); + + if ( ! absint( $attributes['columns'] ) ) { + $attributes['columns'] = wc_get_default_products_per_row(); + } + + return $attributes; + } + + /** + * Parse legacy attributes. + * + * @since 3.2.0 + * @param array $attributes Attributes. + * @return array + */ + protected function parse_legacy_attributes( $attributes ) { + $mapping = array( + 'per_page' => 'limit', + 'operator' => 'cat_operator', + 'filter' => 'terms', + ); + + foreach ( $mapping as $old => $new ) { + if ( isset( $attributes[ $old ] ) ) { + $attributes[ $new ] = $attributes[ $old ]; + unset( $attributes[ $old ] ); + } + } + + return $attributes; + } + + /** + * Parse query args. + * + * @since 3.2.0 + * @return array + */ + protected function parse_query_args() { + $query_args = array( + 'post_type' => 'product', + 'post_status' => 'publish', + 'ignore_sticky_posts' => true, + 'no_found_rows' => false === wc_string_to_bool( $this->attributes['paginate'] ), + 'orderby' => empty( $_GET['orderby'] ) ? $this->attributes['orderby'] : wc_clean( wp_unslash( $_GET['orderby'] ) ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended + ); + + $orderby_value = explode( '-', $query_args['orderby'] ); + $orderby = esc_attr( $orderby_value[0] ); + $order = ! empty( $orderby_value[1] ) ? $orderby_value[1] : strtoupper( $this->attributes['order'] ); + $query_args['orderby'] = $orderby; + $query_args['order'] = $order; + + if ( wc_string_to_bool( $this->attributes['paginate'] ) ) { + $this->attributes['page'] = absint( empty( $_GET['product-page'] ) ? 1 : $_GET['product-page'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + } + + if ( ! empty( $this->attributes['rows'] ) ) { + $this->attributes['limit'] = $this->attributes['columns'] * $this->attributes['rows']; + } + + $ordering_args = WC()->query->get_catalog_ordering_args( $query_args['orderby'], $query_args['order'] ); + $query_args['orderby'] = $ordering_args['orderby']; + $query_args['order'] = $ordering_args['order']; + if ( $ordering_args['meta_key'] ) { + $query_args['meta_key'] = $ordering_args['meta_key']; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + } + $query_args['posts_per_page'] = intval( $this->attributes['limit'] ); + if ( 1 < $this->attributes['page'] ) { + $query_args['paged'] = absint( $this->attributes['page'] ); + } + $query_args['meta_query'] = WC()->query->get_meta_query(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + $query_args['tax_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + + // Visibility. + $this->set_visibility_query_args( $query_args ); + + // SKUs. + $this->set_skus_query_args( $query_args ); + + // IDs. + $this->set_ids_query_args( $query_args ); + + // Set specific types query args. + if ( method_exists( $this, "set_{$this->type}_query_args" ) ) { + $this->{"set_{$this->type}_query_args"}( $query_args ); + } + + // Attributes. + $this->set_attributes_query_args( $query_args ); + + // Categories. + $this->set_categories_query_args( $query_args ); + + // Tags. + $this->set_tags_query_args( $query_args ); + + $query_args = apply_filters( 'woocommerce_shortcode_products_query', $query_args, $this->attributes, $this->type ); + + // Always query only IDs. + $query_args['fields'] = 'ids'; + + return $query_args; + } + + /** + * Set skus query args. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_skus_query_args( &$query_args ) { + if ( ! empty( $this->attributes['skus'] ) ) { + $skus = array_map( 'trim', explode( ',', $this->attributes['skus'] ) ); + $query_args['meta_query'][] = array( + 'key' => '_sku', + 'value' => 1 === count( $skus ) ? $skus[0] : $skus, + 'compare' => 1 === count( $skus ) ? '=' : 'IN', + ); + } + } + + /** + * Set ids query args. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_ids_query_args( &$query_args ) { + if ( ! empty( $this->attributes['ids'] ) ) { + $ids = array_map( 'trim', explode( ',', $this->attributes['ids'] ) ); + + if ( 1 === count( $ids ) ) { + $query_args['p'] = $ids[0]; + } else { + $query_args['post__in'] = $ids; + } + } + } + + /** + * Set attributes query args. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_attributes_query_args( &$query_args ) { + if ( ! empty( $this->attributes['attribute'] ) || ! empty( $this->attributes['terms'] ) ) { + $taxonomy = strstr( $this->attributes['attribute'], 'pa_' ) ? sanitize_title( $this->attributes['attribute'] ) : 'pa_' . sanitize_title( $this->attributes['attribute'] ); + $terms = $this->attributes['terms'] ? array_map( 'sanitize_title', explode( ',', $this->attributes['terms'] ) ) : array(); + $field = 'slug'; + + if ( $terms && is_numeric( $terms[0] ) ) { + $field = 'term_id'; + $terms = array_map( 'absint', $terms ); + // Check numeric slugs. + foreach ( $terms as $term ) { + $the_term = get_term_by( 'slug', $term, $taxonomy ); + if ( false !== $the_term ) { + $terms[] = $the_term->term_id; + } + } + } + + // If no terms were specified get all products that are in the attribute taxonomy. + if ( ! $terms ) { + $terms = get_terms( + array( + 'taxonomy' => $taxonomy, + 'fields' => 'ids', + ) + ); + $field = 'term_id'; + } + + // We always need to search based on the slug as well, this is to accommodate numeric slugs. + $query_args['tax_query'][] = array( + 'taxonomy' => $taxonomy, + 'terms' => $terms, + 'field' => $field, + 'operator' => $this->attributes['terms_operator'], + ); + } + } + + /** + * Set categories query args. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_categories_query_args( &$query_args ) { + if ( ! empty( $this->attributes['category'] ) ) { + $categories = array_map( 'sanitize_title', explode( ',', $this->attributes['category'] ) ); + $field = 'slug'; + + if ( is_numeric( $categories[0] ) ) { + $field = 'term_id'; + $categories = array_map( 'absint', $categories ); + // Check numeric slugs. + foreach ( $categories as $cat ) { + $the_cat = get_term_by( 'slug', $cat, 'product_cat' ); + if ( false !== $the_cat ) { + $categories[] = $the_cat->term_id; + } + } + } + + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_cat', + 'terms' => $categories, + 'field' => $field, + 'operator' => $this->attributes['cat_operator'], + + /* + * When cat_operator is AND, the children categories should be excluded, + * as only products belonging to all the children categories would be selected. + */ + 'include_children' => 'AND' === $this->attributes['cat_operator'] ? false : true, + ); + } + } + + /** + * Set tags query args. + * + * @since 3.3.0 + * @param array $query_args Query args. + */ + protected function set_tags_query_args( &$query_args ) { + if ( ! empty( $this->attributes['tag'] ) ) { + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_tag', + 'terms' => array_map( 'sanitize_title', explode( ',', $this->attributes['tag'] ) ), + 'field' => 'slug', + 'operator' => $this->attributes['tag_operator'], + ); + } + } + + /** + * Set sale products query args. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_sale_products_query_args( &$query_args ) { + $query_args['post__in'] = array_merge( array( 0 ), wc_get_product_ids_on_sale() ); + } + + /** + * Set best selling products query args. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_best_selling_products_query_args( &$query_args ) { + $query_args['meta_key'] = 'total_sales'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + $query_args['order'] = 'DESC'; + $query_args['orderby'] = 'meta_value_num'; + } + + /** + * Set top rated products query args. + * + * @since 3.6.5 + * @param array $query_args Query args. + */ + protected function set_top_rated_products_query_args( &$query_args ) { + $query_args['meta_key'] = '_wc_average_rating'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + $query_args['order'] = 'DESC'; + $query_args['orderby'] = 'meta_value_num'; + } + + /** + * Set visibility as hidden. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_visibility_hidden_query_args( &$query_args ) { + $this->custom_visibility = true; + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'terms' => array( 'exclude-from-catalog', 'exclude-from-search' ), + 'field' => 'name', + 'operator' => 'AND', + 'include_children' => false, + ); + } + + /** + * Set visibility as catalog. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_visibility_catalog_query_args( &$query_args ) { + $this->custom_visibility = true; + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'terms' => 'exclude-from-search', + 'field' => 'name', + 'operator' => 'IN', + 'include_children' => false, + ); + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'terms' => 'exclude-from-catalog', + 'field' => 'name', + 'operator' => 'NOT IN', + 'include_children' => false, + ); + } + + /** + * Set visibility as search. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_visibility_search_query_args( &$query_args ) { + $this->custom_visibility = true; + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'terms' => 'exclude-from-catalog', + 'field' => 'name', + 'operator' => 'IN', + 'include_children' => false, + ); + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'terms' => 'exclude-from-search', + 'field' => 'name', + 'operator' => 'NOT IN', + 'include_children' => false, + ); + } + + /** + * Set visibility as featured. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_visibility_featured_query_args( &$query_args ) { + $query_args['tax_query'] = array_merge( $query_args['tax_query'], WC()->query->get_tax_query() ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'terms' => 'featured', + 'field' => 'name', + 'operator' => 'IN', + 'include_children' => false, + ); + } + + /** + * Set visibility query args. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_visibility_query_args( &$query_args ) { + if ( method_exists( $this, 'set_visibility_' . $this->attributes['visibility'] . '_query_args' ) ) { + $this->{'set_visibility_' . $this->attributes['visibility'] . '_query_args'}( $query_args ); + } else { + $query_args['tax_query'] = array_merge( $query_args['tax_query'], WC()->query->get_tax_query() ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + } + } + + /** + * Set product as visible when querying for hidden products. + * + * @since 3.2.0 + * @param bool $visibility Product visibility. + * @return bool + */ + public function set_product_as_visible( $visibility ) { + return $this->custom_visibility ? true : $visibility; + } + + /** + * Get wrapper classes. + * + * @since 3.2.0 + * @param int $columns Number of columns. + * @return array + */ + protected function get_wrapper_classes( $columns ) { + $classes = array( 'woocommerce' ); + + if ( 'product' !== $this->type ) { + $classes[] = 'columns-' . $columns; + } + + $classes[] = $this->attributes['class']; + + return $classes; + } + + /** + * Generate and return the transient name for this shortcode based on the query args. + * + * @since 3.3.0 + * @return string + */ + protected function get_transient_name() { + $transient_name = 'wc_product_loop_' . md5( wp_json_encode( $this->query_args ) . $this->type ); + + if ( 'rand' === $this->query_args['orderby'] ) { + // When using rand, we'll cache a number of random queries and pull those to avoid querying rand on each page load. + $rand_index = wp_rand( 0, max( 1, absint( apply_filters( 'woocommerce_product_query_max_rand_cache_count', 5 ) ) ) ); + $transient_name .= $rand_index; + } + + return $transient_name; + } + + /** + * Run the query and return an array of data, including queried ids and pagination information. + * + * @since 3.3.0 + * @return object Object with the following props; ids, per_page, found_posts, max_num_pages, current_page + */ + protected function get_query_results() { + $transient_name = $this->get_transient_name(); + $transient_version = WC_Cache_Helper::get_transient_version( 'product_query' ); + $cache = wc_string_to_bool( $this->attributes['cache'] ) === true; + $transient_value = $cache ? get_transient( $transient_name ) : false; + + if ( isset( $transient_value['value'], $transient_value['version'] ) && $transient_value['version'] === $transient_version ) { + $results = $transient_value['value']; + } else { + $query = new WP_Query( $this->query_args ); + + $paginated = ! $query->get( 'no_found_rows' ); + + $results = (object) array( + 'ids' => wp_parse_id_list( $query->posts ), + 'total' => $paginated ? (int) $query->found_posts : count( $query->posts ), + 'total_pages' => $paginated ? (int) $query->max_num_pages : 1, + 'per_page' => (int) $query->get( 'posts_per_page' ), + 'current_page' => $paginated ? (int) max( 1, $query->get( 'paged', 1 ) ) : 1, + ); + + if ( $cache ) { + $transient_value = array( + 'version' => $transient_version, + 'value' => $results, + ); + set_transient( $transient_name, $transient_value, DAY_IN_SECONDS * 30 ); + } + } + + // Remove ordering query arguments which may have been added by get_catalog_ordering_args. + WC()->query->remove_ordering_args(); + + /** + * Filter shortcode products query results. + * + * @since 4.0.0 + * @param stdClass $results Query results. + * @param WC_Shortcode_Products $this WC_Shortcode_Products instance. + */ + return apply_filters( 'woocommerce_shortcode_products_query_results', $results, $this ); + } + + /** + * Loop over found products. + * + * @since 3.2.0 + * @return string + */ + protected function product_loop() { + $columns = absint( $this->attributes['columns'] ); + $classes = $this->get_wrapper_classes( $columns ); + $products = $this->get_query_results(); + + ob_start(); + + if ( $products && $products->ids ) { + // Prime caches to reduce future queries. + if ( is_callable( '_prime_post_caches' ) ) { + _prime_post_caches( $products->ids ); + } + + // Setup the loop. + wc_setup_loop( + array( + 'columns' => $columns, + 'name' => $this->type, + 'is_shortcode' => true, + 'is_search' => false, + 'is_paginated' => wc_string_to_bool( $this->attributes['paginate'] ), + 'total' => $products->total, + 'total_pages' => $products->total_pages, + 'per_page' => $products->per_page, + 'current_page' => $products->current_page, + ) + ); + + $original_post = $GLOBALS['post']; + + do_action( "woocommerce_shortcode_before_{$this->type}_loop", $this->attributes ); + + // Fire standard shop loop hooks when paginating results so we can show result counts and so on. + if ( wc_string_to_bool( $this->attributes['paginate'] ) ) { + do_action( 'woocommerce_before_shop_loop' ); + } + + woocommerce_product_loop_start(); + + if ( wc_get_loop_prop( 'total' ) ) { + foreach ( $products->ids as $product_id ) { + $GLOBALS['post'] = get_post( $product_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + setup_postdata( $GLOBALS['post'] ); + + // Set custom product visibility when quering hidden products. + add_action( 'woocommerce_product_is_visible', array( $this, 'set_product_as_visible' ) ); + + // Render product template. + wc_get_template_part( 'content', 'product' ); + + // Restore product visibility. + remove_action( 'woocommerce_product_is_visible', array( $this, 'set_product_as_visible' ) ); + } + } + + $GLOBALS['post'] = $original_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + woocommerce_product_loop_end(); + + // Fire standard shop loop hooks when paginating results so we can show result counts and so on. + if ( wc_string_to_bool( $this->attributes['paginate'] ) ) { + do_action( 'woocommerce_after_shop_loop' ); + } + + do_action( "woocommerce_shortcode_after_{$this->type}_loop", $this->attributes ); + + wp_reset_postdata(); + wc_reset_loop(); + } else { + do_action( "woocommerce_shortcode_{$this->type}_loop_no_results", $this->attributes ); + } + + return '
    ' . ob_get_clean() . '
    '; + } + + /** + * Order by rating. + * + * @since 3.2.0 + * @param array $args Query args. + * @return array + */ + public static function order_by_rating_post_clauses( $args ) { + global $wpdb; + + $args['where'] .= " AND $wpdb->commentmeta.meta_key = 'rating' "; + $args['join'] .= "LEFT JOIN $wpdb->comments ON($wpdb->posts.ID = $wpdb->comments.comment_post_ID) LEFT JOIN $wpdb->commentmeta ON($wpdb->comments.comment_ID = $wpdb->commentmeta.comment_id)"; + $args['orderby'] = "$wpdb->commentmeta.meta_value DESC"; + $args['groupby'] = "$wpdb->posts.ID"; + + return $args; + } +} diff --git a/includes/theme-support/class-wc-twenty-eleven.php b/plugins/woocommerce/includes/theme-support/class-wc-twenty-eleven.php similarity index 100% rename from includes/theme-support/class-wc-twenty-eleven.php rename to plugins/woocommerce/includes/theme-support/class-wc-twenty-eleven.php diff --git a/includes/theme-support/class-wc-twenty-fifteen.php b/plugins/woocommerce/includes/theme-support/class-wc-twenty-fifteen.php similarity index 100% rename from includes/theme-support/class-wc-twenty-fifteen.php rename to plugins/woocommerce/includes/theme-support/class-wc-twenty-fifteen.php diff --git a/includes/theme-support/class-wc-twenty-fourteen.php b/plugins/woocommerce/includes/theme-support/class-wc-twenty-fourteen.php similarity index 100% rename from includes/theme-support/class-wc-twenty-fourteen.php rename to plugins/woocommerce/includes/theme-support/class-wc-twenty-fourteen.php diff --git a/plugins/woocommerce/includes/theme-support/class-wc-twenty-nineteen.php b/plugins/woocommerce/includes/theme-support/class-wc-twenty-nineteen.php new file mode 100644 index 00000000000..9d1f44cc64c --- /dev/null +++ b/plugins/woocommerce/includes/theme-support/class-wc-twenty-nineteen.php @@ -0,0 +1,132 @@ + 300, + 'single_image_width' => 450, + ) + ); + + // Tweak Twenty Nineteen features. + add_action( 'wp', array( __CLASS__, 'tweak_theme_features' ) ); + + // Color scheme CSS. + add_filter( 'twentynineteen_custom_colors_css', array( __CLASS__, 'custom_colors_css' ), 10, 3 ); + } + + /** + * Open the Twenty Nineteen wrapper. + */ + public static function output_content_wrapper() { + echo '
    '; + echo '
    '; + } + + /** + * Close the Twenty Nineteen wrapper. + */ + public static function output_content_wrapper_end() { + echo '
    '; + echo '
    '; + } + + /** + * Enqueue CSS for this theme. + * + * @param array $styles Array of registered styles. + * @return array + */ + public static function enqueue_styles( $styles ) { + unset( $styles['woocommerce-general'] ); + + $styles['woocommerce-general'] = array( + 'src' => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-nineteen.css', + 'deps' => '', + 'version' => Constants::get_constant( 'WC_VERSION' ), + 'media' => 'all', + 'has_rtl' => true, + ); + + return apply_filters( 'woocommerce_twenty_nineteen_styles', $styles ); + } + + /** + * Tweak Twenty Nineteen features. + */ + public static function tweak_theme_features() { + if ( is_woocommerce() ) { + add_filter( 'twentynineteen_can_show_post_thumbnail', '__return_false' ); + } + } + + /** + * Filters Twenty Nineteen custom colors CSS. + * + * @param string $css Base theme colors CSS. + * @param int $primary_color The user's selected color hue. + * @param string $saturation Filtered theme color saturation level. + */ + public static function custom_colors_css( $css, $primary_color, $saturation ) { + if ( function_exists( 'register_block_type' ) && is_admin() ) { + return $css; + } + + $lightness = absint( apply_filters( 'twentynineteen_custom_colors_lightness', 33 ) ); + $lightness = $lightness . '%'; + + $css .= ' + .onsale, + .woocommerce-info, + .woocommerce-store-notice { + background-color: hsl( ' . $primary_color . ', ' . $saturation . ', ' . $lightness . ' ); + } + + .woocommerce-tabs ul li.active a { + color: hsl( ' . $primary_color . ', ' . $saturation . ', ' . $lightness . ' ); + box-shadow: 0 2px 0 hsl( ' . $primary_color . ', ' . $saturation . ', ' . $lightness . ' ); + } + '; + + return $css; + } +} + +WC_Twenty_Nineteen::init(); diff --git a/includes/theme-support/class-wc-twenty-seventeen.php b/plugins/woocommerce/includes/theme-support/class-wc-twenty-seventeen.php similarity index 100% rename from includes/theme-support/class-wc-twenty-seventeen.php rename to plugins/woocommerce/includes/theme-support/class-wc-twenty-seventeen.php diff --git a/includes/theme-support/class-wc-twenty-sixteen.php b/plugins/woocommerce/includes/theme-support/class-wc-twenty-sixteen.php similarity index 100% rename from includes/theme-support/class-wc-twenty-sixteen.php rename to plugins/woocommerce/includes/theme-support/class-wc-twenty-sixteen.php diff --git a/includes/theme-support/class-wc-twenty-ten.php b/plugins/woocommerce/includes/theme-support/class-wc-twenty-ten.php similarity index 100% rename from includes/theme-support/class-wc-twenty-ten.php rename to plugins/woocommerce/includes/theme-support/class-wc-twenty-ten.php diff --git a/includes/theme-support/class-wc-twenty-thirteen.php b/plugins/woocommerce/includes/theme-support/class-wc-twenty-thirteen.php similarity index 100% rename from includes/theme-support/class-wc-twenty-thirteen.php rename to plugins/woocommerce/includes/theme-support/class-wc-twenty-thirteen.php diff --git a/includes/theme-support/class-wc-twenty-twelve.php b/plugins/woocommerce/includes/theme-support/class-wc-twenty-twelve.php similarity index 100% rename from includes/theme-support/class-wc-twenty-twelve.php rename to plugins/woocommerce/includes/theme-support/class-wc-twenty-twelve.php diff --git a/plugins/woocommerce/includes/theme-support/class-wc-twenty-twenty-one.php b/plugins/woocommerce/includes/theme-support/class-wc-twenty-twenty-one.php new file mode 100644 index 00000000000..6b568ae0e2d --- /dev/null +++ b/plugins/woocommerce/includes/theme-support/class-wc-twenty-twenty-one.php @@ -0,0 +1,86 @@ + 450, + 'single_image_width' => 600, + ) + ); + + } + + /** + * Enqueue CSS for this theme. + * + * @param array $styles Array of registered styles. + * @return array + */ + public static function enqueue_styles( $styles ) { + unset( $styles['woocommerce-general'] ); + + $styles['woocommerce-general'] = array( + 'src' => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-twenty-one.css', + 'deps' => '', + 'version' => Constants::get_constant( 'WC_VERSION' ), + 'media' => 'all', + 'has_rtl' => true, + ); + + return apply_filters( 'woocommerce_twenty_twenty_one_styles', $styles ); + } + + /** + * Enqueue the wp-admin CSS overrides for this theme. + */ + public static function enqueue_admin_styles() { + wp_enqueue_style( + 'woocommerce-twenty-twenty-one-admin', + str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-twenty-one-admin.css', + '', + Constants::get_constant( 'WC_VERSION' ), + 'all' + ); + } + + +} + +WC_Twenty_Twenty_One::init(); diff --git a/plugins/woocommerce/includes/theme-support/class-wc-twenty-twenty-two.php b/plugins/woocommerce/includes/theme-support/class-wc-twenty-twenty-two.php new file mode 100644 index 00000000000..1a4b2c9824a --- /dev/null +++ b/plugins/woocommerce/includes/theme-support/class-wc-twenty-twenty-two.php @@ -0,0 +1,85 @@ + 450, + 'single_image_width' => 600, + ) + ); + + } + + /** + * Enqueue CSS for this theme. + * + * @param array $styles Array of registered styles. + * @return array + */ + public static function enqueue_styles( $styles ) { + unset( $styles['woocommerce-general'] ); + + $styles['woocommerce-general'] = array( + 'src' => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-twenty-two.css', + 'deps' => '', + 'version' => Constants::get_constant( 'WC_VERSION' ), + 'media' => 'all', + 'has_rtl' => true, + ); + + return apply_filters( 'woocommerce_twenty_twenty_two_styles', $styles ); + } + + /** + * Wrap checkout order review with a `col2-set` div. + */ + public static function before_order_review() { + echo '
    '; + } + + /** + * Close the div wrapper. + */ + public static function after_order_review() { + echo '
    '; + } +} + +WC_Twenty_Twenty_Two::init(); diff --git a/includes/theme-support/class-wc-twenty-twenty.php b/plugins/woocommerce/includes/theme-support/class-wc-twenty-twenty.php similarity index 100% rename from includes/theme-support/class-wc-twenty-twenty.php rename to plugins/woocommerce/includes/theme-support/class-wc-twenty-twenty.php diff --git a/plugins/woocommerce/includes/tracks/class-wc-site-tracking.php b/plugins/woocommerce/includes/tracks/class-wc-site-tracking.php new file mode 100644 index 00000000000..d2bb5297688 --- /dev/null +++ b/plugins/woocommerce/includes/tracks/class-wc-site-tracking.php @@ -0,0 +1,188 @@ + + + + registered['woo-tracks'] ) ) { + return; + } + + $woo_tracks_script = $wp_scripts->registered['woo-tracks']->src; + + ?> + + cap_key ) { + return false; + } + $user_id = $user->ID; + $anon_id = get_user_meta( $user_id, '_woocommerce_tracks_anon_id', true ); + + // If an id is still not found, create one and save it. + if ( ! $anon_id ) { + $anon_id = self::get_anon_id(); + update_user_meta( $user_id, '_woocommerce_tracks_anon_id', $anon_id ); + } + + // Don't set cookie on API requests. + if ( ! Constants::is_true( 'REST_REQUEST' ) && ! Constants::is_true( 'XMLRPC_REQUEST' ) ) { + wc_setcookie( 'tk_ai', $anon_id ); + } + } + + /** + * Record a Tracks event + * + * @param array $event Array of event properties. + * @return bool|WP_Error True on success, WP_Error on failure. + */ + public static function record_event( $event ) { + if ( ! $event instanceof WC_Tracks_Event ) { + $event = new WC_Tracks_Event( $event ); + } + + if ( is_wp_error( $event ) ) { + return $event; + } + + $pixel = $event->build_pixel_url( $event ); + + if ( ! $pixel ) { + return new WP_Error( 'invalid_pixel', 'cannot generate tracks pixel for given input', 400 ); + } + + return self::record_pixel( $pixel ); + } + + /** + * Synchronously request the pixel. + * + * @param string $pixel pixel url and query string. + * @return bool Always returns true. + */ + public static function record_pixel( $pixel ) { + // Add the Request Timestamp and URL terminator just before the HTTP request. + $pixel .= '&_rt=' . self::build_timestamp() . '&_=_'; + + wp_safe_remote_get( + $pixel, + array( + 'blocking' => false, + 'redirection' => 2, + 'httpversion' => '1.1', + 'timeout' => 1, + ) + ); + + return true; + } + + /** + * Create a timestamp representing milliseconds since 1970-01-01 + * + * @return string A string representing a timestamp. + */ + public static function build_timestamp() { + $ts = NumberUtil::round( microtime( true ) * 1000 ); + + return number_format( $ts, 0, '', '' ); + } + + /** + * Get a user's identity to send to Tracks. If Jetpack exists, default to its implementation. + * + * @param int $user_id User id. + * @return array Identity properties. + */ + public static function get_identity( $user_id ) { + $jetpack_lib = '/tracks/client.php'; + + if ( class_exists( 'Jetpack' ) && Constants::is_defined( 'JETPACK__VERSION' ) ) { + if ( version_compare( Constants::get_constant( 'JETPACK__VERSION' ), '7.5', '<' ) ) { + if ( file_exists( jetpack_require_lib_dir() . $jetpack_lib ) ) { + include_once jetpack_require_lib_dir() . $jetpack_lib; + if ( function_exists( 'jetpack_tracks_get_identity' ) ) { + return jetpack_tracks_get_identity( $user_id ); + } + } + } else { + $tracking = new Automattic\Jetpack\Tracking(); + return $tracking->tracks_get_identity( $user_id ); + } + } + + // Start with a previously set cookie. + $anon_id = isset( $_COOKIE['tk_ai'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ) : false; + + // If there is no cookie, apply a saved id. + if ( ! $anon_id ) { + $anon_id = get_user_meta( $user_id, '_woocommerce_tracks_anon_id', true ); + } + + // If an id is still not found, create one and save it. + if ( ! $anon_id ) { + $anon_id = self::get_anon_id(); + + update_user_meta( $user_id, '_woocommerce_tracks_anon_id', $anon_id ); + } + + return array( + '_ut' => 'anon', + '_ui' => $anon_id, + ); + } + + /** + * Grabs the user's anon id from cookies, or generates and sets a new one + * + * @return string An anon id for the user + */ + public static function get_anon_id() { + static $anon_id = null; + + if ( ! isset( $anon_id ) ) { + + // Did the browser send us a cookie? + if ( isset( $_COOKIE['tk_ai'] ) ) { + $anon_id = sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ); + } else { + + $binary = ''; + + // Generate a new anonId and try to save it in the browser's cookies. + // Note that base64-encoding an 18 character string generates a 24-character anon id. + for ( $i = 0; $i < 18; ++$i ) { + $binary .= chr( wp_rand( 0, 255 ) ); + } + + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + $anon_id = 'woo:' . base64_encode( $binary ); + } + } + + return $anon_id; + } +} + +WC_Tracks_Client::init(); diff --git a/includes/tracks/class-wc-tracks-event.php b/plugins/woocommerce/includes/tracks/class-wc-tracks-event.php similarity index 100% rename from includes/tracks/class-wc-tracks-event.php rename to plugins/woocommerce/includes/tracks/class-wc-tracks-event.php diff --git a/includes/tracks/class-wc-tracks-footer-pixel.php b/plugins/woocommerce/includes/tracks/class-wc-tracks-footer-pixel.php similarity index 100% rename from includes/tracks/class-wc-tracks-footer-pixel.php rename to plugins/woocommerce/includes/tracks/class-wc-tracks-footer-pixel.php diff --git a/plugins/woocommerce/includes/tracks/class-wc-tracks.php b/plugins/woocommerce/includes/tracks/class-wc-tracks.php new file mode 100644 index 00000000000..f91121420d2 --- /dev/null +++ b/plugins/woocommerce/includes/tracks/class-wc-tracks.php @@ -0,0 +1,117 @@ + home_url(), + 'blog_lang' => get_user_locale( $user_id ), + 'blog_id' => class_exists( 'Jetpack_Options' ) ? Jetpack_Options::get_option( 'id' ) : null, + 'products_count' => self::get_products_count(), + 'wc_version' => WC()->version, + ); + set_transient( 'wc_tracks_blog_details', $blog_details, DAY_IN_SECONDS ); + } + return $blog_details; + } + + /** + * Gather details from the request to the server. + * + * @return array Server details. + */ + public static function get_server_details() { + $data = array(); + + $data['_via_ua'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; + $data['_via_ip'] = isset( $_SERVER['REMOTE_ADDR'] ) ? wc_clean( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; + $data['_lg'] = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : ''; + $data['_dr'] = isset( $_SERVER['HTTP_REFERER'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : ''; + + $uri = isset( $_SERVER['REQUEST_URI'] ) ? wc_clean( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; + $host = isset( $_SERVER['HTTP_HOST'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; + $data['_dl'] = isset( $_SERVER['REQUEST_SCHEME'] ) ? wc_clean( wp_unslash( $_SERVER['REQUEST_SCHEME'] ) ) . '://' . $host . $uri : ''; + + return $data; + } + + /** + * Record an event in Tracks - this is the preferred way to record events from PHP. + * Note: the event request won't be made if $properties has a member called `error`. + * + * @param string $event_name The name of the event. + * @param array $properties Custom properties to send with the event. + * @return bool|WP_Error True for success or WP_Error if the event pixel could not be fired. + */ + public static function record_event( $event_name, $properties = array() ) { + /** + * Don't track users who don't have tracking enabled. + */ + if ( ! WC_Site_Tracking::is_tracking_enabled() ) { + return false; + } + + $user = wp_get_current_user(); + + // We don't want to track user events during unit tests/CI runs. + if ( $user instanceof WP_User && 'wptests_capabilities' === $user->cap_key ) { + return false; + } + $prefixed_event_name = self::PREFIX . $event_name; + + $data = array( + '_en' => $prefixed_event_name, + '_ts' => WC_Tracks_Client::build_timestamp(), + ); + + $server_details = self::get_server_details(); + $identity = WC_Tracks_Client::get_identity( $user->ID ); + $blog_details = self::get_blog_details( $user->ID ); + + // Allow event props to be filtered to enable adding site-wide props. + $filtered_properties = apply_filters( 'woocommerce_tracks_event_properties', $properties, $prefixed_event_name ); + + // Delete _ui and _ut protected properties. + unset( $filtered_properties['_ui'] ); + unset( $filtered_properties['_ut'] ); + + $event_obj = new WC_Tracks_Event( array_merge( $data, $server_details, $identity, $blog_details, $filtered_properties ) ); + + if ( is_wp_error( $event_obj->error ) ) { + return $event_obj->error; + } + + return $event_obj->record(); + } +} diff --git a/includes/tracks/events/class-wc-admin-setup-wizard-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-admin-setup-wizard-tracking.php similarity index 100% rename from includes/tracks/events/class-wc-admin-setup-wizard-tracking.php rename to plugins/woocommerce/includes/tracks/events/class-wc-admin-setup-wizard-tracking.php diff --git a/includes/tracks/events/class-wc-coupon-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-coupon-tracking.php similarity index 100% rename from includes/tracks/events/class-wc-coupon-tracking.php rename to plugins/woocommerce/includes/tracks/events/class-wc-coupon-tracking.php diff --git a/includes/tracks/events/class-wc-coupons-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-coupons-tracking.php similarity index 100% rename from includes/tracks/events/class-wc-coupons-tracking.php rename to plugins/woocommerce/includes/tracks/events/class-wc-coupons-tracking.php diff --git a/plugins/woocommerce/includes/tracks/events/class-wc-extensions-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-extensions-tracking.php new file mode 100644 index 00000000000..cbd16c61744 --- /dev/null +++ b/plugins/woocommerce/includes/tracks/events/class-wc-extensions-tracking.php @@ -0,0 +1,125 @@ + empty( $_REQUEST['section'] ) ? '_featured' : wc_clean( wp_unslash( $_REQUEST['section'] ) ), + ); + + $event = 'extensions_view'; + if ( 'helper' === $properties['section'] ) { + $event = 'subscriptions_view'; + } + + if ( ! empty( $_REQUEST['search'] ) ) { + $event = 'extensions_view_search'; + $properties['search_term'] = wc_clean( wp_unslash( $_REQUEST['search'] ) ); + } + // phpcs:enable + + WC_Tracks::record_event( $event, $properties ); + } + + /** + * Send a Tracks event when the Extensions page gets a bad response or no response + * from the WCCOM extensions API. + * + * @param string $error + */ + public function track_extensions_page_connection_error( string $error = '' ) { + // phpcs:disable WordPress.Security.NonceVerification.Recommended + $properties = array( + 'section' => empty( $_REQUEST['section'] ) ? '_featured' : wc_clean( wp_unslash( $_REQUEST['section'] ) ), + ); + + if ( ! empty( $_REQUEST['search'] ) ) { + $properties['search_term'] = wc_clean( wp_unslash( $_REQUEST['search'] ) ); + } + // phpcs:enable + + if ( ! empty( $error ) ) { + $properties['error_data'] = $error; + } + WC_Tracks::record_event( 'extensions_view_connection_error', $properties ); + } + + /** + * Send a Tracks even when a Helper connection process is initiated. + */ + public function track_helper_connection_start() { + WC_Tracks::record_event( 'extensions_subscriptions_connect' ); + } + + /** + * Send a Tracks even when a Helper connection process is cancelled. + */ + public function track_helper_connection_cancelled() { + WC_Tracks::record_event( 'extensions_subscriptions_cancelled' ); + } + + /** + * Send a Tracks even when a Helper connection process completed successfully. + */ + public function track_helper_connection_complete() { + WC_Tracks::record_event( 'extensions_subscriptions_connected' ); + } + + /** + * Send a Tracks even when a Helper has been disconnected. + */ + public function track_helper_disconnected() { + WC_Tracks::record_event( 'extensions_subscriptions_disconnect' ); + } + + /** + * Send a Tracks even when Helper subscriptions are refreshed. + */ + public function track_helper_subscriptions_refresh() { + WC_Tracks::record_event( 'extensions_subscriptions_update' ); + } + + /** + * Send a Tracks event when addon is installed via the Extensions page. + * + * @param string $addon_id Addon slug. + * @param string $section Extensions tab. + */ + public function track_addon_install( $addon_id, $section ) { + $properties = array( + 'context' => 'extensions', + 'section' => $section, + ); + + if ( 'woocommerce-payments' === $addon_id ) { + WC_Tracks::record_event( 'woocommerce_payments_install', $properties ); + } + } +} diff --git a/includes/tracks/events/class-wc-importer-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-importer-tracking.php similarity index 100% rename from includes/tracks/events/class-wc-importer-tracking.php rename to plugins/woocommerce/includes/tracks/events/class-wc-importer-tracking.php diff --git a/includes/tracks/events/class-wc-order-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-order-tracking.php similarity index 100% rename from includes/tracks/events/class-wc-order-tracking.php rename to plugins/woocommerce/includes/tracks/events/class-wc-order-tracking.php diff --git a/includes/tracks/events/class-wc-orders-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-orders-tracking.php similarity index 100% rename from includes/tracks/events/class-wc-orders-tracking.php rename to plugins/woocommerce/includes/tracks/events/class-wc-orders-tracking.php diff --git a/plugins/woocommerce/includes/tracks/events/class-wc-products-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-products-tracking.php new file mode 100644 index 00000000000..e8c472c14bd --- /dev/null +++ b/plugins/woocommerce/includes/tracks/events/class-wc-products-tracking.php @@ -0,0 +1,219 @@ +post_type ) { + return; + } + + $properties = array( + 'product_id' => $product_id, + ); + + WC_Tracks::record_event( 'product_edit', $properties ); + } + + /** + * Track the Update button being clicked on the client side. + * This is needed because `track_product_updated` (using the `edit_post` + * hook) is called in response to a number of other triggers. + * + * @param WP_Post $post The post, not used. + */ + public function track_product_updated_client_side( $post ) { + wc_enqueue_js( + " + if ( $( 'h1.wp-heading-inline' ).text().trim() === '" . __( 'Edit product', 'woocommerce' ) . "') { + var initialStockValue = $( '#_stock' ).val(); + var hasRecordedEvent = false; + + $( '#publish' ).on( 'click', function() { + if ( hasRecordedEvent ) { + return; + } + + var currentStockValue = $( '#_stock' ).val(); + var properties = { + product_type: $( '#product-type' ).val(), + is_virtual: $( '#_virtual' ).is( ':checked' ) ? 'Y' : 'N', + is_downloadable: $( '#_downloadable' ).is( ':checked' ) ? 'Y' : 'N', + manage_stock: $( '#_manage_stock' ).is( ':checked' ) ? 'Y' : 'N', + stock_quantity_update: ( initialStockValue != currentStockValue ) ? 'Y' : 'N', + }; + + window.wcTracks.recordEvent( 'product_update', properties ); + hasRecordedEvent = true; + } ); + } + " + ); + } + + /** + * Send a Tracks event when a product is published. + * + * @param int $post_id Post ID. + * @param WP_Post $post Post object. + * @param bool $update Whether this is an existing post being updated. + * @param null|WP_Post $post_before Null for new posts, the WP_Post object prior + * to the update for updated posts. + */ + public function track_product_published( $post_id, $post, $update, $post_before ) { + if ( + 'product' !== $post->post_type || + 'publish' !== $post->post_status || + ( $post_before && 'publish' === $post_before->post_status ) + ) { + return; + } + + $product = wc_get_product( $post_id ); + + $properties = array( + 'product_id' => $post_id, + 'product_type' => $product->get_type(), + 'is_downloadable' => $product->is_downloadable() ? 'yes' : 'no', + 'is_virtual' => $product->is_virtual() ? 'yes' : 'no', + 'manage_stock' => $product->get_manage_stock() ? 'yes' : 'no', + ); + + WC_Tracks::record_event( 'product_add_publish', $properties ); + } + + /** + * Send a Tracks event when a product category is created. + * + * @param int $category_id Category ID. + */ + public function track_product_category_created( $category_id ) { + // phpcs:disable WordPress.Security.NonceVerification.Missing + // Only track category creation from the edit product screen or the + // category management screen (which both occur via AJAX). + if ( + ! Constants::is_defined( 'DOING_AJAX' ) || + empty( $_POST['action'] ) || + ( + // Product Categories screen. + 'add-tag' !== $_POST['action'] && + // Edit Product screen. + 'add-product_cat' !== $_POST['action'] + ) + ) { + return; + } + + $category = get_term( $category_id, 'product_cat' ); + $properties = array( + 'category_id' => $category_id, + 'parent_id' => $category->parent, + 'page' => ( 'add-tag' === $_POST['action'] ) ? 'categories' : 'product', + ); + // phpcs:enable + + WC_Tracks::record_event( 'product_category_add', $properties ); + } +} diff --git a/plugins/woocommerce/includes/tracks/events/class-wc-settings-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-settings-tracking.php new file mode 100644 index 00000000000..eb9e5840732 --- /dev/null +++ b/plugins/woocommerce/includes/tracks/events/class-wc-settings-tracking.php @@ -0,0 +1,121 @@ +allowed_options[] = $option['id']; + + // Delay attaching this action since it could get fired a lot. + if ( false === has_action( 'update_option', array( $this, 'track_setting_change' ) ) ) { + add_action( 'update_option', array( $this, 'track_setting_change' ), 10, 3 ); + } + } + + /** + * Add WooCommerce option to a list of updated options. + * + * @param string $option_name Option being updated. + * @param mixed $old_value Old value of option. + * @param mixed $new_value New value of option. + */ + public function track_setting_change( $option_name, $old_value, $new_value ) { + // Make sure this is a WooCommerce option. + if ( ! in_array( $option_name, $this->allowed_options, true ) ) { + return; + } + + // Check to make sure the new value is truly different. + // `woocommerce_price_num_decimals` tends to trigger this + // because form values aren't coerced (e.g. '2' vs. 2). + if ( + is_scalar( $old_value ) && + is_scalar( $new_value ) && + (string) $old_value === (string) $new_value + ) { + return; + } + + $this->updated_options[] = $option_name; + } + + /** + * Send a Tracks event for WooCommerce options that changed values. + */ + public function send_settings_change_event() { + global $current_tab, $current_section; + + if ( empty( $this->updated_options ) ) { + return; + } + + $properties = array( + 'settings' => implode( ',', $this->updated_options ), + ); + + if ( isset( $current_tab ) ) { + $properties['tab'] = $current_tab; + } + if ( isset( $current_section ) ) { + $properties['section'] = $current_section; + } + + WC_Tracks::record_event( 'settings_change', $properties ); + } + + /** + * Send a Tracks event for WooCommerce settings page views. + */ + public function track_settings_page_view() { + global $current_tab, $current_section; + + $properties = array( + 'tab' => $current_tab, + 'section' => empty( $current_section ) ? null : $current_section, + ); + + WC_Tracks::record_event( 'settings_view', $properties ); + } +} diff --git a/includes/tracks/events/class-wc-status-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-status-tracking.php similarity index 100% rename from includes/tracks/events/class-wc-status-tracking.php rename to plugins/woocommerce/includes/tracks/events/class-wc-status-tracking.php diff --git a/plugins/woocommerce/includes/tracks/events/class-wc-theme-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-theme-tracking.php new file mode 100644 index 00000000000..51e23a7fceb --- /dev/null +++ b/plugins/woocommerce/includes/tracks/events/class-wc-theme-tracking.php @@ -0,0 +1,56 @@ +track_initial_theme(); + add_action( 'switch_theme', array( $this, 'track_activated_theme' ) ); + } + + /** + * Tracks the sites current theme the first time this code is run, and will only be run once. + */ + public function track_initial_theme() { + $has_been_initially_tracked = get_option( 'wc_has_tracked_default_theme' ); + + if ( $has_been_initially_tracked ) { + return; + } + + $this->track_activated_theme(); + add_option( 'wc_has_tracked_default_theme', 1 ); + } + + /** + * Send a Tracks event when a theme is activated so that we can track active block themes. + */ + public function track_activated_theme() { + $is_block_theme = false; + $theme_object = wp_get_theme(); + + if ( function_exists( 'wc_current_theme_is_fse_theme' ) ) { + $is_block_theme = wc_current_theme_is_fse_theme(); + } + + $properties = array( + 'block_theme' => $is_block_theme, + 'theme_name' => $theme_object->get( 'Name' ), + 'theme_version' => $theme_object->get( 'Version' ), + ); + + WC_Tracks::record_event( 'activated_theme', $properties ); + } +} diff --git a/includes/traits/trait-wc-item-totals.php b/plugins/woocommerce/includes/traits/trait-wc-item-totals.php similarity index 100% rename from includes/traits/trait-wc-item-totals.php rename to plugins/woocommerce/includes/traits/trait-wc-item-totals.php diff --git a/includes/walkers/class-product-cat-dropdown-walker.php b/plugins/woocommerce/includes/walkers/class-product-cat-dropdown-walker.php similarity index 100% rename from includes/walkers/class-product-cat-dropdown-walker.php rename to plugins/woocommerce/includes/walkers/class-product-cat-dropdown-walker.php diff --git a/includes/walkers/class-product-cat-list-walker.php b/plugins/woocommerce/includes/walkers/class-product-cat-list-walker.php similarity index 100% rename from includes/walkers/class-product-cat-list-walker.php rename to plugins/woocommerce/includes/walkers/class-product-cat-list-walker.php diff --git a/includes/walkers/class-wc-product-cat-dropdown-walker.php b/plugins/woocommerce/includes/walkers/class-wc-product-cat-dropdown-walker.php similarity index 100% rename from includes/walkers/class-wc-product-cat-dropdown-walker.php rename to plugins/woocommerce/includes/walkers/class-wc-product-cat-dropdown-walker.php diff --git a/includes/walkers/class-wc-product-cat-list-walker.php b/plugins/woocommerce/includes/walkers/class-wc-product-cat-list-walker.php similarity index 100% rename from includes/walkers/class-wc-product-cat-list-walker.php rename to plugins/woocommerce/includes/walkers/class-wc-product-cat-list-walker.php diff --git a/includes/wc-account-functions.php b/plugins/woocommerce/includes/wc-account-functions.php similarity index 100% rename from includes/wc-account-functions.php rename to plugins/woocommerce/includes/wc-account-functions.php diff --git a/plugins/woocommerce/includes/wc-attribute-functions.php b/plugins/woocommerce/includes/wc-attribute-functions.php new file mode 100644 index 00000000000..2f42435f91a --- /dev/null +++ b/plugins/woocommerce/includes/wc-attribute-functions.php @@ -0,0 +1,734 @@ +get_results( "SELECT * FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_name != '' ORDER BY attribute_name ASC;" ); + + set_transient( 'wc_attribute_taxonomies', $raw_attribute_taxonomies ); + } + + /** + * Filter attribute taxonomies. + * + * @param array $attribute_taxonomies Results of the DB query. Each taxonomy is an object. + */ + $raw_attribute_taxonomies = (array) array_filter( apply_filters( 'woocommerce_attribute_taxonomies', $raw_attribute_taxonomies ) ); + + // Index by ID for easier lookups. + $attribute_taxonomies = array(); + + foreach ( $raw_attribute_taxonomies as $result ) { + $attribute_taxonomies[ 'id:' . $result->attribute_id ] = $result; + } + + wp_cache_set( $cache_key, $attribute_taxonomies, 'woocommerce-attributes' ); + + return $attribute_taxonomies; +} + +/** + * Get (cached) attribute taxonomy ID and name pairs. + * + * @since 3.6.0 + * @return array + */ +function wc_get_attribute_taxonomy_ids() { + $prefix = WC_Cache_Helper::get_cache_prefix( 'woocommerce-attributes' ); + $cache_key = $prefix . 'ids'; + $cache_value = wp_cache_get( $cache_key, 'woocommerce-attributes' ); + + if ( false !== $cache_value ) { + return $cache_value; + } + + $taxonomy_ids = array_map( 'absint', wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_id', 'attribute_name' ) ); + + wp_cache_set( $cache_key, $taxonomy_ids, 'woocommerce-attributes' ); + + return $taxonomy_ids; +} + +/** + * Get (cached) attribute taxonomy label and name pairs. + * + * @since 3.6.0 + * @return array + */ +function wc_get_attribute_taxonomy_labels() { + $prefix = WC_Cache_Helper::get_cache_prefix( 'woocommerce-attributes' ); + $cache_key = $prefix . 'labels'; + $cache_value = wp_cache_get( $cache_key, 'woocommerce-attributes' ); + + if ( false !== $cache_value ) { + return $cache_value; + } + + $taxonomy_labels = wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_label', 'attribute_name' ); + + wp_cache_set( $cache_key, $taxonomy_labels, 'woocommerce-attributes' ); + + return $taxonomy_labels; +} + +/** + * Get a product attribute name. + * + * @param string $attribute_name Attribute name. + * @return string + */ +function wc_attribute_taxonomy_name( $attribute_name ) { + return $attribute_name ? 'pa_' . wc_sanitize_taxonomy_name( $attribute_name ) : ''; +} + +/** + * Get the attribute name used when storing values in post meta. + * + * @since 2.6.0 + * @param string $attribute_name Attribute name. + * @return string + */ +function wc_variation_attribute_name( $attribute_name ) { + return 'attribute_' . sanitize_title( $attribute_name ); +} + +/** + * Get a product attribute name by ID. + * + * @since 2.4.0 + * @param int $attribute_id Attribute ID. + * @return string Return an empty string if attribute doesn't exist. + */ +function wc_attribute_taxonomy_name_by_id( $attribute_id ) { + $taxonomy_ids = wc_get_attribute_taxonomy_ids(); + $attribute_name = (string) array_search( $attribute_id, $taxonomy_ids, true ); + return wc_attribute_taxonomy_name( $attribute_name ); +} + +/** + * Get a product attribute ID by name. + * + * @since 2.6.0 + * @param string $name Attribute name. + * @return int + */ +function wc_attribute_taxonomy_id_by_name( $name ) { + $name = wc_attribute_taxonomy_slug( $name ); + $taxonomy_ids = wc_get_attribute_taxonomy_ids(); + + return isset( $taxonomy_ids[ $name ] ) ? $taxonomy_ids[ $name ] : 0; +} + +/** + * Get a product attributes label. + * + * @param string $name Attribute name. + * @param WC_Product $product Product data. + * @return string + */ +function wc_attribute_label( $name, $product = '' ) { + if ( taxonomy_is_product_attribute( $name ) ) { + $slug = wc_attribute_taxonomy_slug( $name ); + $all_labels = wc_get_attribute_taxonomy_labels(); + $label = isset( $all_labels[ $slug ] ) ? $all_labels[ $slug ] : $slug; + } elseif ( $product ) { + if ( $product->is_type( 'variation' ) ) { + $product = wc_get_product( $product->get_parent_id() ); + } + $attributes = array(); + + if ( false !== $product ) { + $attributes = $product->get_attributes(); + } + + // Attempt to get label from product, as entered by the user. + if ( $attributes && isset( $attributes[ sanitize_title( $name ) ] ) ) { + $label = $attributes[ sanitize_title( $name ) ]->get_name(); + } else { + $label = $name; + } + } else { + $label = $name; + } + + return apply_filters( 'woocommerce_attribute_label', $label, $name, $product ); +} + +/** + * Get a product attributes orderby setting. + * + * @param string $name Attribute name. + * @return string + */ +function wc_attribute_orderby( $name ) { + $name = wc_attribute_taxonomy_slug( $name ); + $id = wc_attribute_taxonomy_id_by_name( $name ); + $taxonomies = wc_get_attribute_taxonomies(); + + return apply_filters( 'woocommerce_attribute_orderby', isset( $taxonomies[ 'id:' . $id ] ) ? $taxonomies[ 'id:' . $id ]->attribute_orderby : 'menu_order', $name ); +} + +/** + * Get an array of product attribute taxonomies. + * + * @return array + */ +function wc_get_attribute_taxonomy_names() { + $taxonomy_names = array(); + $attribute_taxonomies = wc_get_attribute_taxonomies(); + if ( ! empty( $attribute_taxonomies ) ) { + foreach ( $attribute_taxonomies as $tax ) { + $taxonomy_names[] = wc_attribute_taxonomy_name( $tax->attribute_name ); + } + } + return $taxonomy_names; +} + +/** + * Get attribute types. + * + * @since 2.4.0 + * @return array + */ +function wc_get_attribute_types() { + return (array) apply_filters( + 'product_attributes_type_selector', + array( + 'select' => __( 'Select', 'woocommerce' ), + ) + ); +} + +/** + * Check if there are custom attribute types. + * + * @since 3.3.2 + * @return bool True if there are custom types, otherwise false. + */ +function wc_has_custom_attribute_types() { + $types = wc_get_attribute_types(); + + return 1 < count( $types ) || ! array_key_exists( 'select', $types ); +} + +/** + * Get attribute type label. + * + * @since 3.0.0 + * @param string $type Attribute type slug. + * @return string + */ +function wc_get_attribute_type_label( $type ) { + $types = wc_get_attribute_types(); + + return isset( $types[ $type ] ) ? $types[ $type ] : __( 'Select', 'woocommerce' ); +} + +/** + * Check if attribute name is reserved. + * https://codex.wordpress.org/Function_Reference/register_taxonomy#Reserved_Terms. + * + * @since 2.4.0 + * @param string $attribute_name Attribute name. + * @return bool + */ +function wc_check_if_attribute_name_is_reserved( $attribute_name ) { + // Forbidden attribute names. + $reserved_terms = array( + 'attachment', + 'attachment_id', + 'author', + 'author_name', + 'calendar', + 'cat', + 'category', + 'category__and', + 'category__in', + 'category__not_in', + 'category_name', + 'comments_per_page', + 'comments_popup', + 'cpage', + 'day', + 'debug', + 'error', + 'exact', + 'feed', + 'hour', + 'link_category', + 'm', + 'minute', + 'monthnum', + 'more', + 'name', + 'nav_menu', + 'nopaging', + 'offset', + 'order', + 'orderby', + 'p', + 'page', + 'page_id', + 'paged', + 'pagename', + 'pb', + 'perm', + 'post', + 'post__in', + 'post__not_in', + 'post_format', + 'post_mime_type', + 'post_status', + 'post_tag', + 'post_type', + 'posts', + 'posts_per_archive_page', + 'posts_per_page', + 'preview', + 'robots', + 's', + 'search', + 'second', + 'sentence', + 'showposts', + 'static', + 'subpost', + 'subpost_id', + 'tag', + 'tag__and', + 'tag__in', + 'tag__not_in', + 'tag_id', + 'tag_slug__and', + 'tag_slug__in', + 'taxonomy', + 'tb', + 'term', + 'type', + 'w', + 'withcomments', + 'withoutcomments', + 'year', + ); + + return in_array( $attribute_name, $reserved_terms, true ); +} + +/** + * Callback for array filter to get visible only. + * + * @since 3.0.0 + * @param WC_Product_Attribute $attribute Attribute data. + * @return bool + */ +function wc_attributes_array_filter_visible( $attribute ) { + return $attribute && is_a( $attribute, 'WC_Product_Attribute' ) && $attribute->get_visible() && ( ! $attribute->is_taxonomy() || taxonomy_exists( $attribute->get_name() ) ); +} + +/** + * Callback for array filter to get variation attributes only. + * + * @since 3.0.0 + * @param WC_Product_Attribute $attribute Attribute data. + * @return bool + */ +function wc_attributes_array_filter_variation( $attribute ) { + return $attribute && is_a( $attribute, 'WC_Product_Attribute' ) && $attribute->get_variation(); +} + +/** + * Check if an attribute is included in the attributes area of a variation name. + * + * @since 3.0.2 + * @param string $attribute Attribute value to check for. + * @param string $name Product name to check in. + * @return bool + */ +function wc_is_attribute_in_product_name( $attribute, $name ) { + $is_in_name = stristr( $name, ' ' . $attribute . ',' ) || 0 === stripos( strrev( $name ), strrev( ' ' . $attribute ) ); + return apply_filters( 'woocommerce_is_attribute_in_product_name', $is_in_name, $attribute, $name ); +} + +/** + * Callback for array filter to get default attributes. Will allow for '0' string values, but regard all other + * class PHP FALSE equivalents normally. + * + * @since 3.1.0 + * @param mixed $attribute Attribute being considered for exclusion from parent array. + * @return bool + */ +function wc_array_filter_default_attributes( $attribute ) { + return is_scalar( $attribute ) && ( ! empty( $attribute ) || '0' === $attribute ); +} + +/** + * Get attribute data by ID. + * + * @since 3.2.0 + * @param int $id Attribute ID. + * @return stdClass|null + */ +function wc_get_attribute( $id ) { + $attributes = wc_get_attribute_taxonomies(); + + if ( ! isset( $attributes[ 'id:' . $id ] ) ) { + return null; + } + + $data = $attributes[ 'id:' . $id ]; + $attribute = new stdClass(); + $attribute->id = (int) $data->attribute_id; + $attribute->name = $data->attribute_label; + $attribute->slug = wc_attribute_taxonomy_name( $data->attribute_name ); + $attribute->type = $data->attribute_type; + $attribute->order_by = $data->attribute_orderby; + $attribute->has_archives = (bool) $data->attribute_public; + return $attribute; +} + +/** + * Create attribute. + * + * @since 3.2.0 + * @param array $args Attribute arguments { + * Array of attribute parameters. + * + * @type int $id Unique identifier, used to update an attribute. + * @type string $name Attribute name. Always required. + * @type string $slug Attribute alphanumeric identifier. + * @type string $type Type of attribute. + * Core by default accepts: 'select' and 'text'. + * Default to 'select'. + * @type string $order_by Sort order. + * Accepts: 'menu_order', 'name', 'name_num' and 'id'. + * Default to 'menu_order'. + * @type bool $has_archives Enable or disable attribute archives. False by default. + * } + * @return int|WP_Error + */ +function wc_create_attribute( $args ) { + global $wpdb; + + $args = wp_unslash( $args ); + $id = ! empty( $args['id'] ) ? intval( $args['id'] ) : 0; + $format = array( '%s', '%s', '%s', '%s', '%d' ); + + // Name is required. + if ( empty( $args['name'] ) ) { + return new WP_Error( 'missing_attribute_name', __( 'Please, provide an attribute name.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Set the attribute slug. + if ( empty( $args['slug'] ) ) { + $slug = wc_sanitize_taxonomy_name( $args['name'] ); + } else { + $slug = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( $args['slug'] ) ); + } + + // Validate slug. + if ( strlen( $slug ) >= 28 ) { + /* translators: %s: attribute slug */ + return new WP_Error( 'invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { + /* translators: %s: attribute slug */ + return new WP_Error( 'invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } elseif ( ( 0 === $id && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) || ( isset( $args['old_slug'] ) && $args['old_slug'] !== $slug && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) ) { + /* translators: %s: attribute slug */ + return new WP_Error( 'invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } + + // Validate type. + if ( empty( $args['type'] ) || ! array_key_exists( $args['type'], wc_get_attribute_types() ) ) { + $args['type'] = 'select'; + } + + // Validate order by. + if ( empty( $args['order_by'] ) || ! in_array( $args['order_by'], array( 'menu_order', 'name', 'name_num', 'id' ), true ) ) { + $args['order_by'] = 'menu_order'; + } + + $data = array( + 'attribute_label' => $args['name'], + 'attribute_name' => $slug, + 'attribute_type' => $args['type'], + 'attribute_orderby' => $args['order_by'], + 'attribute_public' => isset( $args['has_archives'] ) ? (int) $args['has_archives'] : 0, + ); + + // Create or update. + if ( 0 === $id ) { + $results = $wpdb->insert( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + $data, + $format + ); + + if ( is_wp_error( $results ) ) { + return new WP_Error( 'cannot_create_attribute', $results->get_error_message(), array( 'status' => 400 ) ); + } + + $id = $wpdb->insert_id; + + /** + * Attribute added. + * + * @param int $id Added attribute ID. + * @param array $data Attribute data. + */ + do_action( 'woocommerce_attribute_added', $id, $data ); + } else { + $results = $wpdb->update( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + $data, + array( 'attribute_id' => $id ), + $format, + array( '%d' ) + ); + + if ( false === $results ) { + return new WP_Error( 'cannot_update_attribute', __( 'Could not update the attribute.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Set old slug to check for database changes. + $old_slug = ! empty( $args['old_slug'] ) ? wc_sanitize_taxonomy_name( $args['old_slug'] ) : $slug; + + /** + * Attribute updated. + * + * @param int $id Added attribute ID. + * @param array $data Attribute data. + * @param string $old_slug Attribute old name. + */ + do_action( 'woocommerce_attribute_updated', $id, $data, $old_slug ); + + if ( $old_slug !== $slug ) { + // Update taxonomies in the wp term taxonomy table. + $wpdb->update( + $wpdb->term_taxonomy, + array( 'taxonomy' => wc_attribute_taxonomy_name( $data['attribute_name'] ) ), + array( 'taxonomy' => 'pa_' . $old_slug ) + ); + + // Update taxonomy ordering term meta. + $wpdb->update( + $wpdb->termmeta, + array( 'meta_key' => 'order_pa_' . sanitize_title( $data['attribute_name'] ) ), // WPCS: slow query ok. + array( 'meta_key' => 'order_pa_' . sanitize_title( $old_slug ) ) // WPCS: slow query ok. + ); + + // Update product attributes which use this taxonomy. + $old_taxonomy_name = 'pa_' . $old_slug; + $new_taxonomy_name = 'pa_' . $data['attribute_name']; + $old_attribute_key = sanitize_title( $old_taxonomy_name ); // @see WC_Product::set_attributes(). + $new_attribute_key = sanitize_title( $new_taxonomy_name ); // @see WC_Product::set_attributes(). + $metadatas = $wpdb->get_results( + $wpdb->prepare( + "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_product_attributes' AND meta_value LIKE %s", + '%' . $wpdb->esc_like( $old_taxonomy_name ) . '%' + ), + ARRAY_A + ); + foreach ( $metadatas as $metadata ) { + $product_id = $metadata['post_id']; + $unserialized_data = maybe_unserialize( $metadata['meta_value'] ); + + if ( ! $unserialized_data || ! is_array( $unserialized_data ) || ! isset( $unserialized_data[ $old_attribute_key ] ) ) { + continue; + } + + $unserialized_data[ $new_attribute_key ] = $unserialized_data[ $old_attribute_key ]; + unset( $unserialized_data[ $old_attribute_key ] ); + $unserialized_data[ $new_attribute_key ]['name'] = $new_taxonomy_name; + update_post_meta( $product_id, '_product_attributes', wp_slash( $unserialized_data ) ); + } + + // Update variations which use this taxonomy. + $wpdb->update( + $wpdb->postmeta, + array( 'meta_key' => 'attribute_pa_' . sanitize_title( $data['attribute_name'] ) ), // WPCS: slow query ok. + array( 'meta_key' => 'attribute_pa_' . sanitize_title( $old_slug ) ) // WPCS: slow query ok. + ); + } + } + + // Clear cache and flush rewrite rules. + wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); + delete_transient( 'wc_attribute_taxonomies' ); + WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); + + return $id; +} + +/** + * Update an attribute. + * + * For available args see wc_create_attribute(). + * + * @since 3.2.0 + * @param int $id Attribute ID. + * @param array $args Attribute arguments. + * @return int|WP_Error + */ +function wc_update_attribute( $id, $args ) { + global $wpdb; + + $attribute = wc_get_attribute( $id ); + + $args['id'] = $attribute ? $attribute->id : 0; + + if ( $args['id'] && empty( $args['name'] ) ) { + $args['name'] = $attribute->name; + } + + $args['old_slug'] = $wpdb->get_var( + $wpdb->prepare( + " + SELECT attribute_name + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", + $args['id'] + ) + ); + + return wc_create_attribute( $args ); +} + +/** + * Delete attribute by ID. + * + * @since 3.2.0 + * @param int $id Attribute ID. + * @return bool + */ +function wc_delete_attribute( $id ) { + global $wpdb; + + $name = $wpdb->get_var( + $wpdb->prepare( + " + SELECT attribute_name + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", + $id + ) + ); + + $taxonomy = wc_attribute_taxonomy_name( $name ); + + /** + * Before deleting an attribute. + * + * @param int $id Attribute ID. + * @param string $name Attribute name. + * @param string $taxonomy Attribute taxonomy name. + */ + do_action( 'woocommerce_before_attribute_delete', $id, $name, $taxonomy ); + + if ( $name && $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_id = %d", $id ) ) ) { + if ( taxonomy_exists( $taxonomy ) ) { + $terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' ); + foreach ( $terms as $term ) { + wp_delete_term( $term->term_id, $taxonomy ); + } + } + + /** + * After deleting an attribute. + * + * @param int $id Attribute ID. + * @param string $name Attribute name. + * @param string $taxonomy Attribute taxonomy name. + */ + do_action( 'woocommerce_attribute_deleted', $id, $name, $taxonomy ); + wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); + delete_transient( 'wc_attribute_taxonomies' ); + WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); + + return true; + } + + return false; +} + +/** + * Get an unprefixed product attribute name. + * + * @since 3.6.0 + * + * @param string $attribute_name Attribute name. + * @return string + */ +function wc_attribute_taxonomy_slug( $attribute_name ) { + $prefix = WC_Cache_Helper::get_cache_prefix( 'woocommerce-attributes' ); + $cache_key = $prefix . 'slug-' . $attribute_name; + $cache_value = wp_cache_get( $cache_key, 'woocommerce-attributes' ); + + if ( false !== $cache_value ) { + return $cache_value; + } + + $attribute_name = wc_sanitize_taxonomy_name( $attribute_name ); + $attribute_slug = 0 === strpos( $attribute_name, 'pa_' ) ? substr( $attribute_name, 3 ) : $attribute_name; + wp_cache_set( $cache_key, $attribute_slug, 'woocommerce-attributes' ); + + return $attribute_slug; +} diff --git a/plugins/woocommerce/includes/wc-cart-functions.php b/plugins/woocommerce/includes/wc-cart-functions.php new file mode 100644 index 00000000000..1cab675f281 --- /dev/null +++ b/plugins/woocommerce/includes/wc-cart-functions.php @@ -0,0 +1,505 @@ +cart ) || '' === WC()->cart ) { + WC()->cart = new WC_Cart(); + } + WC()->cart->empty_cart( false ); +} + +/** + * Load the persistent cart. + * + * @param string $user_login User login. + * @param WP_User $user User data. + * @deprecated 2.3 + */ +function wc_load_persistent_cart( $user_login, $user ) { + if ( ! $user || ! apply_filters( 'woocommerce_persistent_cart_enabled', true ) ) { + return; + } + + $saved_cart = get_user_meta( $user->ID, '_woocommerce_persistent_cart_' . get_current_blog_id(), true ); + + if ( ! $saved_cart ) { + return; + } + + $cart = WC()->session->cart; + + if ( empty( $cart ) || ! is_array( $cart ) || 0 === count( $cart ) ) { + WC()->session->cart = $saved_cart['cart']; + } +} + +/** + * Retrieves unvalidated referer from '_wp_http_referer' or HTTP referer. + * + * Do not use for redirects, use {@see wp_get_referer()} instead. + * + * @since 2.6.1 + * @return string|false Referer URL on success, false on failure. + */ +function wc_get_raw_referer() { + if ( function_exists( 'wp_get_raw_referer' ) ) { + return wp_get_raw_referer(); + } + + if ( ! empty( $_REQUEST['_wp_http_referer'] ) ) { // WPCS: input var ok, CSRF ok. + return wp_unslash( $_REQUEST['_wp_http_referer'] ); // WPCS: input var ok, CSRF ok, sanitization ok. + } elseif ( ! empty( $_SERVER['HTTP_REFERER'] ) ) { // WPCS: input var ok, CSRF ok. + return wp_unslash( $_SERVER['HTTP_REFERER'] ); // WPCS: input var ok, CSRF ok, sanitization ok. + } + + return false; +} + +/** + * Add to cart messages. + * + * @param int|array $products Product ID list or single product ID. + * @param bool $show_qty Should quantities be shown? Added in 2.6.0. + * @param bool $return Return message rather than add it. + * + * @return mixed + */ +function wc_add_to_cart_message( $products, $show_qty = false, $return = false ) { + $titles = array(); + $count = 0; + + if ( ! is_array( $products ) ) { + $products = array( $products => 1 ); + $show_qty = false; + } + + if ( ! $show_qty ) { + $products = array_fill_keys( array_keys( $products ), 1 ); + } + + foreach ( $products as $product_id => $qty ) { + /* translators: %s: product name */ + $titles[] = apply_filters( 'woocommerce_add_to_cart_qty_html', ( $qty > 1 ? absint( $qty ) . ' × ' : '' ), $product_id ) . apply_filters( 'woocommerce_add_to_cart_item_name_in_quotes', sprintf( _x( '“%s”', 'Item name in quotes', 'woocommerce' ), strip_tags( get_the_title( $product_id ) ) ), $product_id ); + $count += $qty; + } + + $titles = array_filter( $titles ); + /* translators: %s: product name */ + $added_text = sprintf( _n( '%s has been added to your cart.', '%s have been added to your cart.', $count, 'woocommerce' ), wc_format_list_of_items( $titles ) ); + + // Output success messages. + if ( 'yes' === get_option( 'woocommerce_cart_redirect_after_add' ) ) { + $return_to = apply_filters( 'woocommerce_continue_shopping_redirect', wc_get_raw_referer() ? wp_validate_redirect( wc_get_raw_referer(), false ) : wc_get_page_permalink( 'shop' ) ); + $message = sprintf( '%s %s', esc_url( $return_to ), esc_html__( 'Continue shopping', 'woocommerce' ), esc_html( $added_text ) ); + } else { + $message = sprintf( '%s %s', esc_url( wc_get_cart_url() ), esc_html__( 'View cart', 'woocommerce' ), esc_html( $added_text ) ); + } + + if ( has_filter( 'wc_add_to_cart_message' ) ) { + wc_deprecated_function( 'The wc_add_to_cart_message filter', '3.0', 'wc_add_to_cart_message_html' ); + $message = apply_filters( 'wc_add_to_cart_message', $message, $product_id ); + } + + $message = apply_filters( 'wc_add_to_cart_message_html', $message, $products, $show_qty ); + + if ( $return ) { + return $message; + } else { + wc_add_notice( $message, apply_filters( 'woocommerce_add_to_cart_notice_type', 'success' ) ); + } +} + +/** + * Comma separate a list of item names, and replace final comma with 'and'. + * + * @param array $items Cart items. + * @return string + */ +function wc_format_list_of_items( $items ) { + $item_string = ''; + + foreach ( $items as $key => $item ) { + $item_string .= $item; + + if ( count( $items ) === $key + 2 ) { + $item_string .= ' ' . __( 'and', 'woocommerce' ) . ' '; + } elseif ( count( $items ) !== $key + 1 ) { + $item_string .= ', '; + } + } + + return $item_string; +} + +/** + * Clear cart after payment. + */ +function wc_clear_cart_after_payment() { + global $wp; + + if ( ! empty( $wp->query_vars['order-received'] ) ) { + + $order_id = absint( $wp->query_vars['order-received'] ); + $order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // WPCS: input var ok, CSRF ok. + + if ( $order_id > 0 ) { + $order = wc_get_order( $order_id ); + + if ( $order && hash_equals( $order->get_order_key(), $order_key ) ) { + WC()->cart->empty_cart(); + } + } + } + + if ( WC()->session->order_awaiting_payment > 0 ) { + $order = wc_get_order( WC()->session->order_awaiting_payment ); + + if ( $order && $order->get_id() > 0 ) { + // If the order has not failed, or is not pending, the order must have gone through. + if ( ! $order->has_status( array( 'failed', 'pending', 'cancelled' ) ) ) { + WC()->cart->empty_cart(); + } + } + } +} +add_action( 'template_redirect', 'wc_clear_cart_after_payment', 20 ); + +/** + * Get the subtotal. + */ +function wc_cart_totals_subtotal_html() { + echo WC()->cart->get_cart_subtotal(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +} + +/** + * Get shipping methods. + */ +function wc_cart_totals_shipping_html() { + $packages = WC()->shipping()->get_packages(); + $first = true; + + foreach ( $packages as $i => $package ) { + $chosen_method = isset( WC()->session->chosen_shipping_methods[ $i ] ) ? WC()->session->chosen_shipping_methods[ $i ] : ''; + $product_names = array(); + + if ( count( $packages ) > 1 ) { + foreach ( $package['contents'] as $item_id => $values ) { + $product_names[ $item_id ] = $values['data']->get_name() . ' ×' . $values['quantity']; + } + $product_names = apply_filters( 'woocommerce_shipping_package_details_array', $product_names, $package ); + } + + wc_get_template( + 'cart/cart-shipping.php', + array( + 'package' => $package, + 'available_methods' => $package['rates'], + 'show_package_details' => count( $packages ) > 1, + 'show_shipping_calculator' => is_cart() && apply_filters( 'woocommerce_shipping_show_shipping_calculator', $first, $i, $package ), + 'package_details' => implode( ', ', $product_names ), + /* translators: %d: shipping package number */ + 'package_name' => apply_filters( 'woocommerce_shipping_package_name', ( ( $i + 1 ) > 1 ) ? sprintf( _x( 'Shipping %d', 'shipping packages', 'woocommerce' ), ( $i + 1 ) ) : _x( 'Shipping', 'shipping packages', 'woocommerce' ), $i, $package ), + 'index' => $i, + 'chosen_method' => $chosen_method, + 'formatted_destination' => WC()->countries->get_formatted_address( $package['destination'], ', ' ), + 'has_calculated_shipping' => WC()->customer->has_calculated_shipping(), + ) + ); + + $first = false; + } +} + +/** + * Get taxes total. + */ +function wc_cart_totals_taxes_total_html() { + echo apply_filters( 'woocommerce_cart_totals_taxes_total_html', wc_price( WC()->cart->get_taxes_total() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +} + +/** + * Get a coupon label. + * + * @param string|WC_Coupon $coupon Coupon data or code. + * @param bool $echo Echo or return. + * + * @return string + */ +function wc_cart_totals_coupon_label( $coupon, $echo = true ) { + if ( is_string( $coupon ) ) { + $coupon = new WC_Coupon( $coupon ); + } + + /* translators: %s: coupon code */ + $label = apply_filters( 'woocommerce_cart_totals_coupon_label', sprintf( esc_html__( 'Coupon: %s', 'woocommerce' ), $coupon->get_code() ), $coupon ); + + if ( $echo ) { + echo $label; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } else { + return $label; + } +} + +/** + * Get coupon display HTML. + * + * @param string|WC_Coupon $coupon Coupon data or code. + */ +function wc_cart_totals_coupon_html( $coupon ) { + if ( is_string( $coupon ) ) { + $coupon = new WC_Coupon( $coupon ); + } + + $discount_amount_html = ''; + + $amount = WC()->cart->get_coupon_discount_amount( $coupon->get_code(), WC()->cart->display_cart_ex_tax ); + $discount_amount_html = '-' . wc_price( $amount ); + + if ( $coupon->get_free_shipping() && empty( $amount ) ) { + $discount_amount_html = __( 'Free shipping coupon', 'woocommerce' ); + } + + $discount_amount_html = apply_filters( 'woocommerce_coupon_discount_amount_html', $discount_amount_html, $coupon ); + $coupon_html = $discount_amount_html . ' ' . __( '[Remove]', 'woocommerce' ) . ''; + + echo wp_kses( apply_filters( 'woocommerce_cart_totals_coupon_html', $coupon_html, $coupon, $discount_amount_html ), array_replace_recursive( wp_kses_allowed_html( 'post' ), array( 'a' => array( 'data-coupon' => true ) ) ) ); // phpcs:ignore PHPCompatibility.PHP.NewFunctions.array_replace_recursiveFound +} + +/** + * Get order total html including inc tax if needed. + */ +function wc_cart_totals_order_total_html() { + $value = '' . WC()->cart->get_total() . ' '; + + // If prices are tax inclusive, show taxes here. + if ( wc_tax_enabled() && WC()->cart->display_prices_including_tax() ) { + $tax_string_array = array(); + $cart_tax_totals = WC()->cart->get_tax_totals(); + + if ( get_option( 'woocommerce_tax_total_display' ) === 'itemized' ) { + foreach ( $cart_tax_totals as $code => $tax ) { + $tax_string_array[] = sprintf( '%s %s', $tax->formatted_amount, $tax->label ); + } + } elseif ( ! empty( $cart_tax_totals ) ) { + $tax_string_array[] = sprintf( '%s %s', wc_price( WC()->cart->get_taxes_total( true, true ) ), WC()->countries->tax_or_vat() ); + } + + if ( ! empty( $tax_string_array ) ) { + $taxable_address = WC()->customer->get_taxable_address(); + if ( WC()->customer->is_customer_outside_base() && ! WC()->customer->has_calculated_shipping() ) { + $country = WC()->countries->estimated_for_prefix( $taxable_address[0] ) . WC()->countries->countries[ $taxable_address[0] ]; + /* translators: 1: tax amount 2: country name */ + $tax_text = wp_kses_post( sprintf( __( '(includes %1$s estimated for %2$s)', 'woocommerce' ), implode( ', ', $tax_string_array ), $country ) ); + } else { + /* translators: %s: tax amount */ + $tax_text = wp_kses_post( sprintf( __( '(includes %s)', 'woocommerce' ), implode( ', ', $tax_string_array ) ) ); + } + + $value .= '' . $tax_text . ''; + } + } + + echo apply_filters( 'woocommerce_cart_totals_order_total_html', $value ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +} + +/** + * Get the fee value. + * + * @param object $fee Fee data. + */ +function wc_cart_totals_fee_html( $fee ) { + $cart_totals_fee_html = WC()->cart->display_prices_including_tax() ? wc_price( $fee->total + $fee->tax ) : wc_price( $fee->total ); + + echo apply_filters( 'woocommerce_cart_totals_fee_html', $cart_totals_fee_html, $fee ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +} + +/** + * Get a shipping methods full label including price. + * + * @param WC_Shipping_Rate $method Shipping method rate data. + * @return string + */ +function wc_cart_totals_shipping_method_label( $method ) { + $label = $method->get_label(); + $has_cost = 0 < $method->cost; + $hide_cost = ! $has_cost && in_array( $method->get_method_id(), array( 'free_shipping', 'local_pickup' ), true ); + + if ( $has_cost && ! $hide_cost ) { + if ( WC()->cart->display_prices_including_tax() ) { + $label .= ': ' . wc_price( $method->cost + $method->get_shipping_tax() ); + if ( $method->get_shipping_tax() > 0 && ! wc_prices_include_tax() ) { + $label .= ' ' . WC()->countries->inc_tax_or_vat() . ''; + } + } else { + $label .= ': ' . wc_price( $method->cost ); + if ( $method->get_shipping_tax() > 0 && wc_prices_include_tax() ) { + $label .= ' ' . WC()->countries->ex_tax_or_vat() . ''; + } + } + } + + return apply_filters( 'woocommerce_cart_shipping_method_full_label', $label, $method ); +} + +/** + * Round discount. + * + * @param double $value Amount to round. + * @param int $precision DP to round. + * @return float + */ +function wc_cart_round_discount( $value, $precision ) { + return wc_round_discount( $value, $precision ); +} + +/** + * Gets chosen shipping method IDs from chosen_shipping_methods session, without instance IDs. + * + * @since 2.6.2 + * @return string[] + */ +function wc_get_chosen_shipping_method_ids() { + $method_ids = array(); + $chosen_methods = WC()->session->get( 'chosen_shipping_methods', array() ); + foreach ( $chosen_methods as $chosen_method ) { + $chosen_method = explode( ':', $chosen_method ); + $method_ids[] = current( $chosen_method ); + } + return $method_ids; +} + +/** + * Get chosen method for package from session. + * + * @since 3.2.0 + * @param int $key Key of package. + * @param array $package Package data array. + * @return string|bool + */ +function wc_get_chosen_shipping_method_for_package( $key, $package ) { + $chosen_methods = WC()->session->get( 'chosen_shipping_methods' ); + $chosen_method = isset( $chosen_methods[ $key ] ) ? $chosen_methods[ $key ] : false; + $changed = wc_shipping_methods_have_changed( $key, $package ); + + // This is deprecated but here for BW compat. TODO: Remove in 4.0.0. + $method_counts = WC()->session->get( 'shipping_method_counts' ); + + if ( ! empty( $method_counts[ $key ] ) ) { + $method_count = absint( $method_counts[ $key ] ); + } else { + $method_count = 0; + } + + // If not set, not available, or available methods have changed, set to the DEFAULT option. + if ( ! $chosen_method || $changed || ! isset( $package['rates'][ $chosen_method ] ) || count( $package['rates'] ) !== $method_count ) { + $chosen_method = wc_get_default_shipping_method_for_package( $key, $package, $chosen_method ); + $chosen_methods[ $key ] = $chosen_method; + $method_counts[ $key ] = count( $package['rates'] ); + + WC()->session->set( 'chosen_shipping_methods', $chosen_methods ); + WC()->session->set( 'shipping_method_counts', $method_counts ); + + do_action( 'woocommerce_shipping_method_chosen', $chosen_method ); + } + return $chosen_method; +} + +/** + * Choose the default method for a package. + * + * @since 3.2.0 + * @param int $key Key of package. + * @param array $package Package data array. + * @param string $chosen_method Chosen method id. + * @return string + */ +function wc_get_default_shipping_method_for_package( $key, $package, $chosen_method ) { + $rate_keys = array_keys( $package['rates'] ); + $default = current( $rate_keys ); + $coupons = WC()->cart->get_coupons(); + foreach ( $coupons as $coupon ) { + if ( $coupon->get_free_shipping() ) { + foreach ( $rate_keys as $rate_key ) { + if ( 0 === stripos( $rate_key, 'free_shipping' ) ) { + $default = $rate_key; + break; + } + } + break; + } + } + return apply_filters( 'woocommerce_shipping_chosen_method', $default, $package['rates'], $chosen_method ); +} + +/** + * See if the methods have changed since the last request. + * + * @since 3.2.0 + * @param int $key Key of package. + * @param array $package Package data array. + * @return bool + */ +function wc_shipping_methods_have_changed( $key, $package ) { + // Lookup previous methods from session. + $previous_shipping_methods = WC()->session->get( 'previous_shipping_methods' ); + // Get new and old rates. + $new_rates = array_keys( $package['rates'] ); + $prev_rates = isset( $previous_shipping_methods[ $key ] ) ? $previous_shipping_methods[ $key ] : false; + // Update session. + $previous_shipping_methods[ $key ] = $new_rates; + WC()->session->set( 'previous_shipping_methods', $previous_shipping_methods ); + return $new_rates !== $prev_rates; +} + +/** + * Gets a hash of important product data that when changed should cause cart items to be invalidated. + * + * The woocommerce_cart_item_data_to_validate filter can be used to add custom properties. + * + * @param WC_Product $product Product object. + * @return string + */ +function wc_get_cart_item_data_hash( $product ) { + return md5( + wp_json_encode( + apply_filters( + 'woocommerce_cart_item_data_to_validate', + array( + 'type' => $product->get_type(), + 'attributes' => 'variation' === $product->get_type() ? $product->get_variation_attributes() : '', + ), + $product + ) + ) + ); +} diff --git a/plugins/woocommerce/includes/wc-conditional-functions.php b/plugins/woocommerce/includes/wc-conditional-functions.php new file mode 100644 index 00000000000..780f310095e --- /dev/null +++ b/plugins/woocommerce/includes/wc-conditional-functions.php @@ -0,0 +1,526 @@ +query_vars['order-pay'] ); + } +} + +if ( ! function_exists( 'is_wc_endpoint_url' ) ) { + + /** + * Is_wc_endpoint_url - Check if an endpoint is showing. + * + * @param string|false $endpoint Whether endpoint. + * @return bool + */ + function is_wc_endpoint_url( $endpoint = false ) { + global $wp; + + $wc_endpoints = WC()->query->get_query_vars(); + + if ( false !== $endpoint ) { + if ( ! isset( $wc_endpoints[ $endpoint ] ) ) { + return false; + } else { + $endpoint_var = $wc_endpoints[ $endpoint ]; + } + + return isset( $wp->query_vars[ $endpoint_var ] ); + } else { + foreach ( $wc_endpoints as $key => $value ) { + if ( isset( $wp->query_vars[ $key ] ) ) { + return true; + } + } + + return false; + } + } +} + +if ( ! function_exists( 'is_account_page' ) ) { + + /** + * Is_account_page - Returns true when viewing an account page. + * + * @return bool + */ + function is_account_page() { + $page_id = wc_get_page_id( 'myaccount' ); + + return ( $page_id && is_page( $page_id ) ) || wc_post_content_has_shortcode( 'woocommerce_my_account' ) || apply_filters( 'woocommerce_is_account_page', false ); + } +} + +if ( ! function_exists( 'is_view_order_page' ) ) { + + /** + * Is_view_order_page - Returns true when on the view order page. + * + * @return bool + */ + function is_view_order_page() { + global $wp; + + $page_id = wc_get_page_id( 'myaccount' ); + + return ( $page_id && is_page( $page_id ) && isset( $wp->query_vars['view-order'] ) ); + } +} + +if ( ! function_exists( 'is_edit_account_page' ) ) { + + /** + * Check for edit account page. + * Returns true when viewing the edit account page. + * + * @since 2.5.1 + * @return bool + */ + function is_edit_account_page() { + global $wp; + + $page_id = wc_get_page_id( 'myaccount' ); + + return ( $page_id && is_page( $page_id ) && isset( $wp->query_vars['edit-account'] ) ); + } +} + +if ( ! function_exists( 'is_order_received_page' ) ) { + + /** + * Is_order_received_page - Returns true when viewing the order received page. + * + * @return bool + */ + function is_order_received_page() { + global $wp; + + $page_id = wc_get_page_id( 'checkout' ); + + return apply_filters( 'woocommerce_is_order_received_page', ( $page_id && is_page( $page_id ) && isset( $wp->query_vars['order-received'] ) ) ); + } +} + +if ( ! function_exists( 'is_add_payment_method_page' ) ) { + + /** + * Is_add_payment_method_page - Returns true when viewing the add payment method page. + * + * @return bool + */ + function is_add_payment_method_page() { + global $wp; + + $page_id = wc_get_page_id( 'myaccount' ); + + return ( $page_id && is_page( $page_id ) && ( isset( $wp->query_vars['payment-methods'] ) || isset( $wp->query_vars['add-payment-method'] ) ) ); + } +} + +if ( ! function_exists( 'is_lost_password_page' ) ) { + + /** + * Is_lost_password_page - Returns true when viewing the lost password page. + * + * @return bool + */ + function is_lost_password_page() { + global $wp; + + $page_id = wc_get_page_id( 'myaccount' ); + + return ( $page_id && is_page( $page_id ) && isset( $wp->query_vars['lost-password'] ) ); + } +} + +if ( ! function_exists( 'is_ajax' ) ) { + + /** + * Is_ajax - Returns true when the page is loaded via ajax. + * + * @see wp_doing_ajax() for an equivalent function provided by WordPress since 4.7.0 + * @return bool + */ + function is_ajax() { + return function_exists( 'wp_doing_ajax' ) ? wp_doing_ajax() : Constants::is_defined( 'DOING_AJAX' ); + } +} + +if ( ! function_exists( 'is_store_notice_showing' ) ) { + + /** + * Is_store_notice_showing - Returns true when store notice is active. + * + * @return bool + */ + function is_store_notice_showing() { + return 'no' !== get_option( 'woocommerce_demo_store', 'no' ); + } +} + +if ( ! function_exists( 'is_filtered' ) ) { + + /** + * Is_filtered - Returns true when filtering products using layered nav or price sliders. + * + * @return bool + */ + function is_filtered() { + return apply_filters( 'woocommerce_is_filtered', ( count( WC_Query::get_layered_nav_chosen_attributes() ) > 0 || isset( $_GET['max_price'] ) || isset( $_GET['min_price'] ) || isset( $_GET['rating_filter'] ) ) ); // WPCS: CSRF ok. + } +} + +if ( ! function_exists( 'taxonomy_is_product_attribute' ) ) { + + /** + * Returns true when the passed taxonomy name is a product attribute. + * + * @uses $wc_product_attributes global which stores taxonomy names upon registration + * @param string $name of the attribute. + * @return bool + */ + function taxonomy_is_product_attribute( $name ) { + global $wc_product_attributes; + + return taxonomy_exists( $name ) && array_key_exists( $name, (array) $wc_product_attributes ); + } +} + +if ( ! function_exists( 'meta_is_product_attribute' ) ) { + + /** + * Returns true when the passed meta name is a product attribute. + * + * @param string $name of the attribute. + * @param string $value of the attribute. + * @param int $product_id to check for attribute. + * @return bool + */ + function meta_is_product_attribute( $name, $value, $product_id ) { + $product = wc_get_product( $product_id ); + + if ( $product && method_exists( $product, 'get_variation_attributes' ) ) { + $variation_attributes = $product->get_variation_attributes(); + $attributes = $product->get_attributes(); + return ( in_array( $name, array_keys( $attributes ), true ) && in_array( $value, $variation_attributes[ $attributes[ $name ]['name'] ], true ) ); + } else { + return false; + } + } +} + +if ( ! function_exists( 'wc_tax_enabled' ) ) { + + /** + * Are store-wide taxes enabled? + * + * @return bool + */ + function wc_tax_enabled() { + return apply_filters( 'wc_tax_enabled', get_option( 'woocommerce_calc_taxes' ) === 'yes' ); + } +} + +if ( ! function_exists( 'wc_shipping_enabled' ) ) { + + /** + * Is shipping enabled? + * + * @return bool + */ + function wc_shipping_enabled() { + return apply_filters( 'wc_shipping_enabled', get_option( 'woocommerce_ship_to_countries' ) !== 'disabled' ); + } +} + +if ( ! function_exists( 'wc_prices_include_tax' ) ) { + + /** + * Are prices inclusive of tax? + * + * @return bool + */ + function wc_prices_include_tax() { + return wc_tax_enabled() && apply_filters( 'woocommerce_prices_include_tax', get_option( 'woocommerce_prices_include_tax' ) === 'yes' ); + } +} + +/** + * Simple check for validating a URL, it must start with http:// or https://. + * and pass FILTER_VALIDATE_URL validation. + * + * @param string $url to check. + * @return bool + */ +function wc_is_valid_url( $url ) { + + // Must start with http:// or https://. + if ( 0 !== strpos( $url, 'http://' ) && 0 !== strpos( $url, 'https://' ) ) { + return false; + } + + // Must pass validation. + if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) { + return false; + } + + return true; +} + +/** + * Check if the home URL is https. If it is, we don't need to do things such as 'force ssl'. + * + * @since 2.4.13 + * @return bool + */ +function wc_site_is_https() { + return false !== strstr( get_option( 'home' ), 'https:' ); +} + +/** + * Check if the checkout is configured for https. Look at options, WP HTTPS plugin, or the permalink itself. + * + * @since 2.5.0 + * @return bool + */ +function wc_checkout_is_https() { + return wc_site_is_https() || 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) || class_exists( 'WordPressHTTPS' ) || strstr( wc_get_page_permalink( 'checkout' ), 'https:' ); +} + +/** + * Checks whether the content passed contains a specific short code. + * + * @param string $tag Shortcode tag to check. + * @return bool + */ +function wc_post_content_has_shortcode( $tag = '' ) { + global $post; + + return is_singular() && is_a( $post, 'WP_Post' ) && has_shortcode( $post->post_content, $tag ); +} + +/** + * Check if reviews are enabled. + * + * @since 3.6.0 + * @return bool + */ +function wc_reviews_enabled() { + return 'yes' === get_option( 'woocommerce_enable_reviews' ); +} + +/** + * Check if reviews ratings are enabled. + * + * @since 3.6.0 + * @return bool + */ +function wc_review_ratings_enabled() { + return wc_reviews_enabled() && 'yes' === get_option( 'woocommerce_enable_review_rating' ); +} + +/** + * Check if review ratings are required. + * + * @since 3.6.0 + * @return bool + */ +function wc_review_ratings_required() { + return 'yes' === get_option( 'woocommerce_review_rating_required' ); +} + +/** + * Check if a CSV file is valid. + * + * @since 3.6.5 + * @param string $file File name. + * @param bool $check_path If should check for the path. + * @return bool + */ +function wc_is_file_valid_csv( $file, $check_path = true ) { + /** + * Filter check for CSV file path. + * + * @since 3.6.4 + * @param bool $check_import_file_path If requires file path check. Defaults to true. + * @param string $file Path of the file to be checked. + */ + $check_import_file_path = apply_filters( 'woocommerce_csv_importer_check_import_file_path', true, $file ); + + if ( $check_path && $check_import_file_path && false !== stripos( $file, '://' ) ) { + return false; + } + + /** + * Filter CSV valid file types. + * + * @since 3.6.5 + * @param array $valid_filetypes List of valid file types. + */ + $valid_filetypes = apply_filters( + 'woocommerce_csv_import_valid_filetypes', + array( + 'csv' => 'text/csv', + 'txt' => 'text/plain', + ) + ); + + $filetype = wp_check_filetype( $file, $valid_filetypes ); + + if ( in_array( $filetype['type'], $valid_filetypes, true ) ) { + return true; + } + + return false; +} + +/** + * Check if the current theme is a block theme. + * + * @since x.x.x + * @return bool + */ +function wc_current_theme_is_fse_theme() { + if ( function_exists( 'wp_is_block_theme' ) ) { + return (bool) wp_is_block_theme(); + } + if ( function_exists( 'gutenberg_is_fse_theme' ) ) { + return (bool) gutenberg_is_fse_theme(); + } + + return false; +} + +/** + * Check if the current theme has WooCommerce support or is a FSE theme. + * + * @since x.x.x + * @return bool + */ +function wc_current_theme_supports_woocommerce_or_fse() { + return (bool) current_theme_supports( 'woocommerce' ) || wc_current_theme_is_fse_theme(); +} + diff --git a/plugins/woocommerce/includes/wc-core-functions.php b/plugins/woocommerce/includes/wc-core-functions.php new file mode 100644 index 00000000000..177dbf2eff3 --- /dev/null +++ b/plugins/woocommerce/includes/wc-core-functions.php @@ -0,0 +1,2544 @@ + null, + 'customer_id' => null, + 'customer_note' => null, + 'parent' => null, + 'created_via' => null, + 'cart_hash' => null, + 'order_id' => 0, + ); + + try { + $args = wp_parse_args( $args, $default_args ); + $order = new WC_Order( $args['order_id'] ); + + // Update props that were set (not null). + if ( ! is_null( $args['parent'] ) ) { + $order->set_parent_id( absint( $args['parent'] ) ); + } + + if ( ! is_null( $args['status'] ) ) { + $order->set_status( $args['status'] ); + } + + if ( ! is_null( $args['customer_note'] ) ) { + $order->set_customer_note( $args['customer_note'] ); + } + + if ( ! is_null( $args['customer_id'] ) ) { + $order->set_customer_id( is_numeric( $args['customer_id'] ) ? absint( $args['customer_id'] ) : 0 ); + } + + if ( ! is_null( $args['created_via'] ) ) { + $order->set_created_via( sanitize_text_field( $args['created_via'] ) ); + } + + if ( ! is_null( $args['cart_hash'] ) ) { + $order->set_cart_hash( sanitize_text_field( $args['cart_hash'] ) ); + } + + // Set these fields when creating a new order but not when updating an existing order. + if ( ! $args['order_id'] ) { + $order->set_currency( get_woocommerce_currency() ); + $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); + $order->set_customer_ip_address( WC_Geolocation::get_ip_address() ); + $order->set_customer_user_agent( wc_get_user_agent() ); + } + + // Update other order props set automatically. + $order->save(); + } catch ( Exception $e ) { + return new WP_Error( 'error', $e->getMessage() ); + } + + return $order; +} + +/** + * Update an order. Uses wc_create_order. + * + * @param array $args Order arguments. + * @return WC_Order|WP_Error + */ +function wc_update_order( $args ) { + if ( empty( $args['order_id'] ) ) { + return new WP_Error( __( 'Invalid order ID.', 'woocommerce' ) ); + } + return wc_create_order( $args ); +} + +/** + * Given a path, this will convert any of the subpaths into their corresponding tokens. + * + * @since 4.3.0 + * @param string $path The absolute path to tokenize. + * @param array $path_tokens An array keyed with the token, containing paths that should be replaced. + * @return string The tokenized path. + */ +function wc_tokenize_path( $path, $path_tokens ) { + // Order most to least specific so that the token can encompass as much of the path as possible. + uasort( + $path_tokens, + function ( $a, $b ) { + $a = strlen( $a ); + $b = strlen( $b ); + + if ( $a > $b ) { + return -1; + } + + if ( $b > $a ) { + return 1; + } + + return 0; + } + ); + + foreach ( $path_tokens as $token => $token_path ) { + if ( 0 !== strpos( $path, $token_path ) ) { + continue; + } + + $path = str_replace( $token_path, '{{' . $token . '}}', $path ); + } + + return $path; +} + +/** + * Given a tokenized path, this will expand the tokens to their full path. + * + * @since 4.3.0 + * @param string $path The absolute path to expand. + * @param array $path_tokens An array keyed with the token, containing paths that should be expanded. + * @return string The absolute path. + */ +function wc_untokenize_path( $path, $path_tokens ) { + foreach ( $path_tokens as $token => $token_path ) { + $path = str_replace( '{{' . $token . '}}', $token_path, $path ); + } + + return $path; +} + +/** + * Fetches an array containing all of the configurable path constants to be used in tokenization. + * + * @return array The key is the define and the path is the constant. + */ +function wc_get_path_define_tokens() { + $defines = array( + 'ABSPATH', + 'WP_CONTENT_DIR', + 'WP_PLUGIN_DIR', + 'WPMU_PLUGIN_DIR', + 'PLUGINDIR', + 'WP_THEME_DIR', + ); + + $path_tokens = array(); + foreach ( $defines as $define ) { + if ( defined( $define ) ) { + $path_tokens[ $define ] = constant( $define ); + } + } + + return apply_filters( 'woocommerce_get_path_define_tokens', $path_tokens ); +} + +/** + * Get template part (for templates like the shop-loop). + * + * WC_TEMPLATE_DEBUG_MODE will prevent overrides in themes from taking priority. + * + * @param mixed $slug Template slug. + * @param string $name Template name (default: ''). + */ +function wc_get_template_part( $slug, $name = '' ) { + $cache_key = sanitize_key( implode( '-', array( 'template-part', $slug, $name, Constants::get_constant( 'WC_VERSION' ) ) ) ); + $template = (string) wp_cache_get( $cache_key, 'woocommerce' ); + + if ( ! $template ) { + if ( $name ) { + $template = WC_TEMPLATE_DEBUG_MODE ? '' : locate_template( + array( + "{$slug}-{$name}.php", + WC()->template_path() . "{$slug}-{$name}.php", + ) + ); + + if ( ! $template ) { + $fallback = WC()->plugin_path() . "/templates/{$slug}-{$name}.php"; + $template = file_exists( $fallback ) ? $fallback : ''; + } + } + + if ( ! $template ) { + // If template file doesn't exist, look in yourtheme/slug.php and yourtheme/woocommerce/slug.php. + $template = WC_TEMPLATE_DEBUG_MODE ? '' : locate_template( + array( + "{$slug}.php", + WC()->template_path() . "{$slug}.php", + ) + ); + } + + // Don't cache the absolute path so that it can be shared between web servers with different paths. + $cache_path = wc_tokenize_path( $template, wc_get_path_define_tokens() ); + + wc_set_template_cache( $cache_key, $cache_path ); + } else { + // Make sure that the absolute path to the template is resolved. + $template = wc_untokenize_path( $template, wc_get_path_define_tokens() ); + } + + // Allow 3rd party plugins to filter template file from their plugin. + $template = apply_filters( 'wc_get_template_part', $template, $slug, $name ); + + if ( $template ) { + load_template( $template, false ); + } +} + +/** + * Get other templates (e.g. product attributes) passing attributes and including the file. + * + * @param string $template_name Template name. + * @param array $args Arguments. (default: array). + * @param string $template_path Template path. (default: ''). + * @param string $default_path Default path. (default: ''). + */ +function wc_get_template( $template_name, $args = array(), $template_path = '', $default_path = '' ) { + $cache_key = sanitize_key( implode( '-', array( 'template', $template_name, $template_path, $default_path, Constants::get_constant( 'WC_VERSION' ) ) ) ); + $template = (string) wp_cache_get( $cache_key, 'woocommerce' ); + + if ( ! $template ) { + $template = wc_locate_template( $template_name, $template_path, $default_path ); + + // Don't cache the absolute path so that it can be shared between web servers with different paths. + $cache_path = wc_tokenize_path( $template, wc_get_path_define_tokens() ); + + wc_set_template_cache( $cache_key, $cache_path ); + } else { + // Make sure that the absolute path to the template is resolved. + $template = wc_untokenize_path( $template, wc_get_path_define_tokens() ); + } + + // Allow 3rd party plugin filter template file from their plugin. + $filter_template = apply_filters( 'wc_get_template', $template, $template_name, $args, $template_path, $default_path ); + + if ( $filter_template !== $template ) { + if ( ! file_exists( $filter_template ) ) { + /* translators: %s template */ + wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%s does not exist.', 'woocommerce' ), '' . $filter_template . '' ), '2.1' ); + return; + } + $template = $filter_template; + } + + $action_args = array( + 'template_name' => $template_name, + 'template_path' => $template_path, + 'located' => $template, + 'args' => $args, + ); + + if ( ! empty( $args ) && is_array( $args ) ) { + if ( isset( $args['action_args'] ) ) { + wc_doing_it_wrong( + __FUNCTION__, + __( 'action_args should not be overwritten when calling wc_get_template.', 'woocommerce' ), + '3.6.0' + ); + unset( $args['action_args'] ); + } + extract( $args ); // @codingStandardsIgnoreLine + } + + do_action( 'woocommerce_before_template_part', $action_args['template_name'], $action_args['template_path'], $action_args['located'], $action_args['args'] ); + + include $action_args['located']; + + do_action( 'woocommerce_after_template_part', $action_args['template_name'], $action_args['template_path'], $action_args['located'], $action_args['args'] ); +} + +/** + * Like wc_get_template, but returns the HTML instead of outputting. + * + * @see wc_get_template + * @since 2.5.0 + * @param string $template_name Template name. + * @param array $args Arguments. (default: array). + * @param string $template_path Template path. (default: ''). + * @param string $default_path Default path. (default: ''). + * + * @return string + */ +function wc_get_template_html( $template_name, $args = array(), $template_path = '', $default_path = '' ) { + ob_start(); + wc_get_template( $template_name, $args, $template_path, $default_path ); + return ob_get_clean(); +} +/** + * Locate a template and return the path for inclusion. + * + * This is the load order: + * + * yourtheme/$template_path/$template_name + * yourtheme/$template_name + * $default_path/$template_name + * + * @param string $template_name Template name. + * @param string $template_path Template path. (default: ''). + * @param string $default_path Default path. (default: ''). + * @return string + */ +function wc_locate_template( $template_name, $template_path = '', $default_path = '' ) { + if ( ! $template_path ) { + $template_path = WC()->template_path(); + } + + if ( ! $default_path ) { + $default_path = WC()->plugin_path() . '/templates/'; + } + + // Look within passed path within the theme - this is priority. + if ( false !== strpos( $template_name, 'product_cat' ) || false !== strpos( $template_name, 'product_tag' ) ) { + $cs_template = str_replace( '_', '-', $template_name ); + $template = locate_template( + array( + trailingslashit( $template_path ) . $cs_template, + $cs_template, + ) + ); + } + + if ( empty( $template ) ) { + $template = locate_template( + array( + trailingslashit( $template_path ) . $template_name, + $template_name, + ) + ); + } + + // Get default template/. + if ( ! $template || WC_TEMPLATE_DEBUG_MODE ) { + if ( empty( $cs_template ) ) { + $template = $default_path . $template_name; + } else { + $template = $default_path . $cs_template; + } + } + + // Return what we found. + return apply_filters( 'woocommerce_locate_template', $template, $template_name, $template_path ); +} + +/** + * Add a template to the template cache. + * + * @since 4.3.0 + * @param string $cache_key Object cache key. + * @param string $template Located template. + */ +function wc_set_template_cache( $cache_key, $template ) { + wp_cache_set( $cache_key, $template, 'woocommerce' ); + + $cached_templates = wp_cache_get( 'cached_templates', 'woocommerce' ); + if ( is_array( $cached_templates ) ) { + $cached_templates[] = $cache_key; + } else { + $cached_templates = array( $cache_key ); + } + + wp_cache_set( 'cached_templates', $cached_templates, 'woocommerce' ); +} + +/** + * Clear the template cache. + * + * @since 4.3.0 + */ +function wc_clear_template_cache() { + $cached_templates = wp_cache_get( 'cached_templates', 'woocommerce' ); + if ( is_array( $cached_templates ) ) { + foreach ( $cached_templates as $cache_key ) { + wp_cache_delete( $cache_key, 'woocommerce' ); + } + + wp_cache_delete( 'cached_templates', 'woocommerce' ); + } +} + +/** + * Get Base Currency Code. + * + * @return string + */ +function get_woocommerce_currency() { + return apply_filters( 'woocommerce_currency', get_option( 'woocommerce_currency' ) ); +} + +/** + * Get full list of currency codes. + * + * Currency symbols and names should follow the Unicode CLDR recommendation (http://cldr.unicode.org/translation/currency-names) + * + * @return array + */ +function get_woocommerce_currencies() { + static $currencies; + + if ( ! isset( $currencies ) ) { + $currencies = array_unique( + apply_filters( + 'woocommerce_currencies', + array( + 'AED' => __( 'United Arab Emirates dirham', 'woocommerce' ), + 'AFN' => __( 'Afghan afghani', 'woocommerce' ), + 'ALL' => __( 'Albanian lek', 'woocommerce' ), + 'AMD' => __( 'Armenian dram', 'woocommerce' ), + 'ANG' => __( 'Netherlands Antillean guilder', 'woocommerce' ), + 'AOA' => __( 'Angolan kwanza', 'woocommerce' ), + 'ARS' => __( 'Argentine peso', 'woocommerce' ), + 'AUD' => __( 'Australian dollar', 'woocommerce' ), + 'AWG' => __( 'Aruban florin', 'woocommerce' ), + 'AZN' => __( 'Azerbaijani manat', 'woocommerce' ), + 'BAM' => __( 'Bosnia and Herzegovina convertible mark', 'woocommerce' ), + 'BBD' => __( 'Barbadian dollar', 'woocommerce' ), + 'BDT' => __( 'Bangladeshi taka', 'woocommerce' ), + 'BGN' => __( 'Bulgarian lev', 'woocommerce' ), + 'BHD' => __( 'Bahraini dinar', 'woocommerce' ), + 'BIF' => __( 'Burundian franc', 'woocommerce' ), + 'BMD' => __( 'Bermudian dollar', 'woocommerce' ), + 'BND' => __( 'Brunei dollar', 'woocommerce' ), + 'BOB' => __( 'Bolivian boliviano', 'woocommerce' ), + 'BRL' => __( 'Brazilian real', 'woocommerce' ), + 'BSD' => __( 'Bahamian dollar', 'woocommerce' ), + 'BTC' => __( 'Bitcoin', 'woocommerce' ), + 'BTN' => __( 'Bhutanese ngultrum', 'woocommerce' ), + 'BWP' => __( 'Botswana pula', 'woocommerce' ), + 'BYR' => __( 'Belarusian ruble (old)', 'woocommerce' ), + 'BYN' => __( 'Belarusian ruble', 'woocommerce' ), + 'BZD' => __( 'Belize dollar', 'woocommerce' ), + 'CAD' => __( 'Canadian dollar', 'woocommerce' ), + 'CDF' => __( 'Congolese franc', 'woocommerce' ), + 'CHF' => __( 'Swiss franc', 'woocommerce' ), + 'CLP' => __( 'Chilean peso', 'woocommerce' ), + 'CNY' => __( 'Chinese yuan', 'woocommerce' ), + 'COP' => __( 'Colombian peso', 'woocommerce' ), + 'CRC' => __( 'Costa Rican colón', 'woocommerce' ), + 'CUC' => __( 'Cuban convertible peso', 'woocommerce' ), + 'CUP' => __( 'Cuban peso', 'woocommerce' ), + 'CVE' => __( 'Cape Verdean escudo', 'woocommerce' ), + 'CZK' => __( 'Czech koruna', 'woocommerce' ), + 'DJF' => __( 'Djiboutian franc', 'woocommerce' ), + 'DKK' => __( 'Danish krone', 'woocommerce' ), + 'DOP' => __( 'Dominican peso', 'woocommerce' ), + 'DZD' => __( 'Algerian dinar', 'woocommerce' ), + 'EGP' => __( 'Egyptian pound', 'woocommerce' ), + 'ERN' => __( 'Eritrean nakfa', 'woocommerce' ), + 'ETB' => __( 'Ethiopian birr', 'woocommerce' ), + 'EUR' => __( 'Euro', 'woocommerce' ), + 'FJD' => __( 'Fijian dollar', 'woocommerce' ), + 'FKP' => __( 'Falkland Islands pound', 'woocommerce' ), + 'GBP' => __( 'Pound sterling', 'woocommerce' ), + 'GEL' => __( 'Georgian lari', 'woocommerce' ), + 'GGP' => __( 'Guernsey pound', 'woocommerce' ), + 'GHS' => __( 'Ghana cedi', 'woocommerce' ), + 'GIP' => __( 'Gibraltar pound', 'woocommerce' ), + 'GMD' => __( 'Gambian dalasi', 'woocommerce' ), + 'GNF' => __( 'Guinean franc', 'woocommerce' ), + 'GTQ' => __( 'Guatemalan quetzal', 'woocommerce' ), + 'GYD' => __( 'Guyanese dollar', 'woocommerce' ), + 'HKD' => __( 'Hong Kong dollar', 'woocommerce' ), + 'HNL' => __( 'Honduran lempira', 'woocommerce' ), + 'HRK' => __( 'Croatian kuna', 'woocommerce' ), + 'HTG' => __( 'Haitian gourde', 'woocommerce' ), + 'HUF' => __( 'Hungarian forint', 'woocommerce' ), + 'IDR' => __( 'Indonesian rupiah', 'woocommerce' ), + 'ILS' => __( 'Israeli new shekel', 'woocommerce' ), + 'IMP' => __( 'Manx pound', 'woocommerce' ), + 'INR' => __( 'Indian rupee', 'woocommerce' ), + 'IQD' => __( 'Iraqi dinar', 'woocommerce' ), + 'IRR' => __( 'Iranian rial', 'woocommerce' ), + 'IRT' => __( 'Iranian toman', 'woocommerce' ), + 'ISK' => __( 'Icelandic króna', 'woocommerce' ), + 'JEP' => __( 'Jersey pound', 'woocommerce' ), + 'JMD' => __( 'Jamaican dollar', 'woocommerce' ), + 'JOD' => __( 'Jordanian dinar', 'woocommerce' ), + 'JPY' => __( 'Japanese yen', 'woocommerce' ), + 'KES' => __( 'Kenyan shilling', 'woocommerce' ), + 'KGS' => __( 'Kyrgyzstani som', 'woocommerce' ), + 'KHR' => __( 'Cambodian riel', 'woocommerce' ), + 'KMF' => __( 'Comorian franc', 'woocommerce' ), + 'KPW' => __( 'North Korean won', 'woocommerce' ), + 'KRW' => __( 'South Korean won', 'woocommerce' ), + 'KWD' => __( 'Kuwaiti dinar', 'woocommerce' ), + 'KYD' => __( 'Cayman Islands dollar', 'woocommerce' ), + 'KZT' => __( 'Kazakhstani tenge', 'woocommerce' ), + 'LAK' => __( 'Lao kip', 'woocommerce' ), + 'LBP' => __( 'Lebanese pound', 'woocommerce' ), + 'LKR' => __( 'Sri Lankan rupee', 'woocommerce' ), + 'LRD' => __( 'Liberian dollar', 'woocommerce' ), + 'LSL' => __( 'Lesotho loti', 'woocommerce' ), + 'LYD' => __( 'Libyan dinar', 'woocommerce' ), + 'MAD' => __( 'Moroccan dirham', 'woocommerce' ), + 'MDL' => __( 'Moldovan leu', 'woocommerce' ), + 'MGA' => __( 'Malagasy ariary', 'woocommerce' ), + 'MKD' => __( 'Macedonian denar', 'woocommerce' ), + 'MMK' => __( 'Burmese kyat', 'woocommerce' ), + 'MNT' => __( 'Mongolian tögrög', 'woocommerce' ), + 'MOP' => __( 'Macanese pataca', 'woocommerce' ), + 'MRU' => __( 'Mauritanian ouguiya', 'woocommerce' ), + 'MUR' => __( 'Mauritian rupee', 'woocommerce' ), + 'MVR' => __( 'Maldivian rufiyaa', 'woocommerce' ), + 'MWK' => __( 'Malawian kwacha', 'woocommerce' ), + 'MXN' => __( 'Mexican peso', 'woocommerce' ), + 'MYR' => __( 'Malaysian ringgit', 'woocommerce' ), + 'MZN' => __( 'Mozambican metical', 'woocommerce' ), + 'NAD' => __( 'Namibian dollar', 'woocommerce' ), + 'NGN' => __( 'Nigerian naira', 'woocommerce' ), + 'NIO' => __( 'Nicaraguan córdoba', 'woocommerce' ), + 'NOK' => __( 'Norwegian krone', 'woocommerce' ), + 'NPR' => __( 'Nepalese rupee', 'woocommerce' ), + 'NZD' => __( 'New Zealand dollar', 'woocommerce' ), + 'OMR' => __( 'Omani rial', 'woocommerce' ), + 'PAB' => __( 'Panamanian balboa', 'woocommerce' ), + 'PEN' => __( 'Sol', 'woocommerce' ), + 'PGK' => __( 'Papua New Guinean kina', 'woocommerce' ), + 'PHP' => __( 'Philippine peso', 'woocommerce' ), + 'PKR' => __( 'Pakistani rupee', 'woocommerce' ), + 'PLN' => __( 'Polish złoty', 'woocommerce' ), + 'PRB' => __( 'Transnistrian ruble', 'woocommerce' ), + 'PYG' => __( 'Paraguayan guaraní', 'woocommerce' ), + 'QAR' => __( 'Qatari riyal', 'woocommerce' ), + 'RON' => __( 'Romanian leu', 'woocommerce' ), + 'RSD' => __( 'Serbian dinar', 'woocommerce' ), + 'RUB' => __( 'Russian ruble', 'woocommerce' ), + 'RWF' => __( 'Rwandan franc', 'woocommerce' ), + 'SAR' => __( 'Saudi riyal', 'woocommerce' ), + 'SBD' => __( 'Solomon Islands dollar', 'woocommerce' ), + 'SCR' => __( 'Seychellois rupee', 'woocommerce' ), + 'SDG' => __( 'Sudanese pound', 'woocommerce' ), + 'SEK' => __( 'Swedish krona', 'woocommerce' ), + 'SGD' => __( 'Singapore dollar', 'woocommerce' ), + 'SHP' => __( 'Saint Helena pound', 'woocommerce' ), + 'SLL' => __( 'Sierra Leonean leone', 'woocommerce' ), + 'SOS' => __( 'Somali shilling', 'woocommerce' ), + 'SRD' => __( 'Surinamese dollar', 'woocommerce' ), + 'SSP' => __( 'South Sudanese pound', 'woocommerce' ), + 'STN' => __( 'São Tomé and Príncipe dobra', 'woocommerce' ), + 'SYP' => __( 'Syrian pound', 'woocommerce' ), + 'SZL' => __( 'Swazi lilangeni', 'woocommerce' ), + 'THB' => __( 'Thai baht', 'woocommerce' ), + 'TJS' => __( 'Tajikistani somoni', 'woocommerce' ), + 'TMT' => __( 'Turkmenistan manat', 'woocommerce' ), + 'TND' => __( 'Tunisian dinar', 'woocommerce' ), + 'TOP' => __( 'Tongan paʻanga', 'woocommerce' ), + 'TRY' => __( 'Turkish lira', 'woocommerce' ), + 'TTD' => __( 'Trinidad and Tobago dollar', 'woocommerce' ), + 'TWD' => __( 'New Taiwan dollar', 'woocommerce' ), + 'TZS' => __( 'Tanzanian shilling', 'woocommerce' ), + 'UAH' => __( 'Ukrainian hryvnia', 'woocommerce' ), + 'UGX' => __( 'Ugandan shilling', 'woocommerce' ), + 'USD' => __( 'United States (US) dollar', 'woocommerce' ), + 'UYU' => __( 'Uruguayan peso', 'woocommerce' ), + 'UZS' => __( 'Uzbekistani som', 'woocommerce' ), + 'VEF' => __( 'Venezuelan bolívar', 'woocommerce' ), + 'VES' => __( 'Bolívar soberano', 'woocommerce' ), + 'VND' => __( 'Vietnamese đồng', 'woocommerce' ), + 'VUV' => __( 'Vanuatu vatu', 'woocommerce' ), + 'WST' => __( 'Samoan tālā', 'woocommerce' ), + 'XAF' => __( 'Central African CFA franc', 'woocommerce' ), + 'XCD' => __( 'East Caribbean dollar', 'woocommerce' ), + 'XOF' => __( 'West African CFA franc', 'woocommerce' ), + 'XPF' => __( 'CFP franc', 'woocommerce' ), + 'YER' => __( 'Yemeni rial', 'woocommerce' ), + 'ZAR' => __( 'South African rand', 'woocommerce' ), + 'ZMW' => __( 'Zambian kwacha', 'woocommerce' ), + ) + ) + ); + } + + return $currencies; +} + +/** + * Get all available Currency symbols. + * + * Currency symbols and names should follow the Unicode CLDR recommendation (http://cldr.unicode.org/translation/currency-names) + * + * @since 4.1.0 + * @return array + */ +function get_woocommerce_currency_symbols() { + + $symbols = apply_filters( + 'woocommerce_currency_symbols', + array( + 'AED' => 'د.إ', + 'AFN' => '؋', + 'ALL' => 'L', + 'AMD' => 'AMD', + 'ANG' => 'ƒ', + 'AOA' => 'Kz', + 'ARS' => '$', + 'AUD' => '$', + 'AWG' => 'Afl.', + 'AZN' => 'AZN', + 'BAM' => 'KM', + 'BBD' => '$', + 'BDT' => '৳ ', + 'BGN' => 'лв.', + 'BHD' => '.د.ب', + 'BIF' => 'Fr', + 'BMD' => '$', + 'BND' => '$', + 'BOB' => 'Bs.', + 'BRL' => 'R$', + 'BSD' => '$', + 'BTC' => '฿', + 'BTN' => 'Nu.', + 'BWP' => 'P', + 'BYR' => 'Br', + 'BYN' => 'Br', + 'BZD' => '$', + 'CAD' => '$', + 'CDF' => 'Fr', + 'CHF' => 'CHF', + 'CLP' => '$', + 'CNY' => '¥', + 'COP' => '$', + 'CRC' => '₡', + 'CUC' => '$', + 'CUP' => '$', + 'CVE' => '$', + 'CZK' => 'Kč', + 'DJF' => 'Fr', + 'DKK' => 'DKK', + 'DOP' => 'RD$', + 'DZD' => 'د.ج', + 'EGP' => 'EGP', + 'ERN' => 'Nfk', + 'ETB' => 'Br', + 'EUR' => '€', + 'FJD' => '$', + 'FKP' => '£', + 'GBP' => '£', + 'GEL' => '₾', + 'GGP' => '£', + 'GHS' => '₵', + 'GIP' => '£', + 'GMD' => 'D', + 'GNF' => 'Fr', + 'GTQ' => 'Q', + 'GYD' => '$', + 'HKD' => '$', + 'HNL' => 'L', + 'HRK' => 'kn', + 'HTG' => 'G', + 'HUF' => 'Ft', + 'IDR' => 'Rp', + 'ILS' => '₪', + 'IMP' => '£', + 'INR' => '₹', + 'IQD' => 'د.ع', + 'IRR' => '﷼', + 'IRT' => 'تومان', + 'ISK' => 'kr.', + 'JEP' => '£', + 'JMD' => '$', + 'JOD' => 'د.ا', + 'JPY' => '¥', + 'KES' => 'KSh', + 'KGS' => 'сом', + 'KHR' => '៛', + 'KMF' => 'Fr', + 'KPW' => '₩', + 'KRW' => '₩', + 'KWD' => 'د.ك', + 'KYD' => '$', + 'KZT' => '₸', + 'LAK' => '₭', + 'LBP' => 'ل.ل', + 'LKR' => 'රු', + 'LRD' => '$', + 'LSL' => 'L', + 'LYD' => 'ل.د', + 'MAD' => 'د.م.', + 'MDL' => 'MDL', + 'MGA' => 'Ar', + 'MKD' => 'ден', + 'MMK' => 'Ks', + 'MNT' => '₮', + 'MOP' => 'P', + 'MRU' => 'UM', + 'MUR' => '₨', + 'MVR' => '.ރ', + 'MWK' => 'MK', + 'MXN' => '$', + 'MYR' => 'RM', + 'MZN' => 'MT', + 'NAD' => 'N$', + 'NGN' => '₦', + 'NIO' => 'C$', + 'NOK' => 'kr', + 'NPR' => '₨', + 'NZD' => '$', + 'OMR' => 'ر.ع.', + 'PAB' => 'B/.', + 'PEN' => 'S/', + 'PGK' => 'K', + 'PHP' => '₱', + 'PKR' => '₨', + 'PLN' => 'zł', + 'PRB' => 'р.', + 'PYG' => '₲', + 'QAR' => 'ر.ق', + 'RMB' => '¥', + 'RON' => 'lei', + 'RSD' => 'рсд', + 'RUB' => '₽', + 'RWF' => 'Fr', + 'SAR' => 'ر.س', + 'SBD' => '$', + 'SCR' => '₨', + 'SDG' => 'ج.س.', + 'SEK' => 'kr', + 'SGD' => '$', + 'SHP' => '£', + 'SLL' => 'Le', + 'SOS' => 'Sh', + 'SRD' => '$', + 'SSP' => '£', + 'STN' => 'Db', + 'SYP' => 'ل.س', + 'SZL' => 'E', + 'THB' => '฿', + 'TJS' => 'ЅМ', + 'TMT' => 'm', + 'TND' => 'د.ت', + 'TOP' => 'T$', + 'TRY' => '₺', + 'TTD' => '$', + 'TWD' => 'NT$', + 'TZS' => 'Sh', + 'UAH' => '₴', + 'UGX' => 'UGX', + 'USD' => '$', + 'UYU' => '$', + 'UZS' => 'UZS', + 'VEF' => 'Bs F', + 'VES' => 'Bs.S', + 'VND' => '₫', + 'VUV' => 'Vt', + 'WST' => 'T', + 'XAF' => 'CFA', + 'XCD' => '$', + 'XOF' => 'CFA', + 'XPF' => 'Fr', + 'YER' => '﷼', + 'ZAR' => 'R', + 'ZMW' => 'ZK', + ) + ); + + return $symbols; +} + +/** + * Get Currency symbol. + * + * Currency symbols and names should follow the Unicode CLDR recommendation (http://cldr.unicode.org/translation/currency-names) + * + * @param string $currency Currency. (default: ''). + * @return string + */ +function get_woocommerce_currency_symbol( $currency = '' ) { + if ( ! $currency ) { + $currency = get_woocommerce_currency(); + } + + $symbols = get_woocommerce_currency_symbols(); + + $currency_symbol = isset( $symbols[ $currency ] ) ? $symbols[ $currency ] : ''; + + return apply_filters( 'woocommerce_currency_symbol', $currency_symbol, $currency ); +} + +/** + * Send HTML emails from WooCommerce. + * + * @param mixed $to Receiver. + * @param mixed $subject Subject. + * @param mixed $message Message. + * @param string $headers Headers. (default: "Content-Type: text/html\r\n"). + * @param string $attachments Attachments. (default: ""). + * @return bool + */ +function wc_mail( $to, $subject, $message, $headers = "Content-Type: text/html\r\n", $attachments = '' ) { + $mailer = WC()->mailer(); + + return $mailer->send( $to, $subject, $message, $headers, $attachments ); +} + +/** + * Return "theme support" values from the current theme, if set. + * + * @since 3.3.0 + * @param string $prop Name of prop (or key::subkey for arrays of props) if you want a specific value. Leave blank to get all props as an array. + * @param mixed $default Optional value to return if the theme does not declare support for a prop. + * @return mixed Value of prop(s). + */ +function wc_get_theme_support( $prop = '', $default = null ) { + $theme_support = get_theme_support( 'woocommerce' ); + $theme_support = is_array( $theme_support ) ? $theme_support[0] : false; + + if ( ! $theme_support ) { + return $default; + } + + if ( $prop ) { + $prop_stack = explode( '::', $prop ); + $prop_key = array_shift( $prop_stack ); + + if ( isset( $theme_support[ $prop_key ] ) ) { + $value = $theme_support[ $prop_key ]; + + if ( count( $prop_stack ) ) { + foreach ( $prop_stack as $prop_key ) { + if ( is_array( $value ) && isset( $value[ $prop_key ] ) ) { + $value = $value[ $prop_key ]; + } else { + $value = $default; + break; + } + } + } + } else { + $value = $default; + } + + return $value; + } + + return $theme_support; +} + +/** + * Get an image size by name or defined dimensions. + * + * The returned variable is filtered by woocommerce_get_image_size_{image_size} filter to + * allow 3rd party customisation. + * + * Sizes defined by the theme take priority over settings. Settings are hidden when a theme + * defines sizes. + * + * @param array|string $image_size Name of the image size to get, or an array of dimensions. + * @return array Array of dimensions including width, height, and cropping mode. Cropping mode is 0 for no crop, and 1 for hard crop. + */ +function wc_get_image_size( $image_size ) { + $cache_key = 'size-' . ( is_array( $image_size ) ? implode( '-', $image_size ) : $image_size ); + $size = wp_cache_get( $cache_key, 'woocommerce' ); + + if ( $size ) { + return $size; + } + + $size = array( + 'width' => 600, + 'height' => 600, + 'crop' => 1, + ); + + if ( is_array( $image_size ) ) { + $size = array( + 'width' => isset( $image_size[0] ) ? absint( $image_size[0] ) : 600, + 'height' => isset( $image_size[1] ) ? absint( $image_size[1] ) : 600, + 'crop' => isset( $image_size[2] ) ? absint( $image_size[2] ) : 1, + ); + $image_size = $size['width'] . '_' . $size['height']; + } else { + $image_size = str_replace( 'woocommerce_', '', $image_size ); + + // Legacy size mapping. + if ( 'shop_single' === $image_size ) { + $image_size = 'single'; + } elseif ( 'shop_catalog' === $image_size ) { + $image_size = 'thumbnail'; + } elseif ( 'shop_thumbnail' === $image_size ) { + $image_size = 'gallery_thumbnail'; + } + + if ( 'single' === $image_size ) { + $size['width'] = absint( wc_get_theme_support( 'single_image_width', get_option( 'woocommerce_single_image_width', 600 ) ) ); + $size['height'] = ''; + $size['crop'] = 0; + + } elseif ( 'gallery_thumbnail' === $image_size ) { + $size['width'] = absint( wc_get_theme_support( 'gallery_thumbnail_image_width', 100 ) ); + $size['height'] = $size['width']; + $size['crop'] = 1; + + } elseif ( 'thumbnail' === $image_size ) { + $size['width'] = absint( wc_get_theme_support( 'thumbnail_image_width', get_option( 'woocommerce_thumbnail_image_width', 300 ) ) ); + $cropping = get_option( 'woocommerce_thumbnail_cropping', '1:1' ); + + if ( 'uncropped' === $cropping ) { + $size['height'] = ''; + $size['crop'] = 0; + } elseif ( 'custom' === $cropping ) { + $width = max( 1, (float) get_option( 'woocommerce_thumbnail_cropping_custom_width', '4' ) ); + $height = max( 1, (float) get_option( 'woocommerce_thumbnail_cropping_custom_height', '3' ) ); + $size['height'] = absint( NumberUtil::round( ( $size['width'] / $width ) * $height ) ); + $size['crop'] = 1; + } else { + $cropping_split = explode( ':', $cropping ); + $width = max( 1, (float) current( $cropping_split ) ); + $height = max( 1, (float) end( $cropping_split ) ); + $size['height'] = absint( NumberUtil::round( ( $size['width'] / $width ) * $height ) ); + $size['crop'] = 1; + } + } + } + + $size = apply_filters( 'woocommerce_get_image_size_' . $image_size, $size ); + + wp_cache_set( $cache_key, $size, 'woocommerce' ); + + return $size; +} + +/** + * Queue some JavaScript code to be output in the footer. + * + * @param string $code Code. + */ +function wc_enqueue_js( $code ) { + global $wc_queued_js; + + if ( empty( $wc_queued_js ) ) { + $wc_queued_js = ''; + } + + $wc_queued_js .= "\n" . $code . "\n"; +} + +/** + * Output any queued javascript code in the footer. + */ +function wc_print_js() { + global $wc_queued_js; + + if ( ! empty( $wc_queued_js ) ) { + // Sanitize. + $wc_queued_js = wp_check_invalid_utf8( $wc_queued_js ); + $wc_queued_js = preg_replace( '/&#(x)?0*(?(1)27|39);?/i', "'", $wc_queued_js ); + $wc_queued_js = str_replace( "\r", '', $wc_queued_js ); + + $js = "\n\n"; + + /** + * Queued jsfilter. + * + * @since 2.6.0 + * @param string $js JavaScript code. + */ + echo apply_filters( 'woocommerce_queued_js', $js ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + + unset( $wc_queued_js ); + } +} + +/** + * Set a cookie - wrapper for setcookie using WP constants. + * + * @param string $name Name of the cookie being set. + * @param string $value Value of the cookie. + * @param integer $expire Expiry of the cookie. + * @param bool $secure Whether the cookie should be served only over https. + * @param bool $httponly Whether the cookie is only accessible over HTTP, not scripting languages like JavaScript. @since 3.6.0. + */ +function wc_setcookie( $name, $value, $expire = 0, $secure = false, $httponly = false ) { + if ( ! apply_filters( 'woocommerce_set_cookie_enabled', true, $name ,$value, $expire, $secure ) ) { + return; + } + + if ( ! headers_sent() ) { + setcookie( $name, $value, $expire, COOKIEPATH ? COOKIEPATH : '/', COOKIE_DOMAIN, $secure, apply_filters( 'woocommerce_cookie_httponly', $httponly, $name, $value, $expire, $secure ) ); + } elseif ( Constants::is_true( 'WP_DEBUG' ) ) { + headers_sent( $file, $line ); + trigger_error( "{$name} cookie cannot be set - headers already sent by {$file} on line {$line}", E_USER_NOTICE ); // @codingStandardsIgnoreLine + } +} + +/** + * Get the URL to the WooCommerce REST API. + * + * @since 2.1 + * @param string $path an endpoint to include in the URL. + * @return string the URL. + */ +function get_woocommerce_api_url( $path ) { + if ( Constants::is_defined( 'WC_API_REQUEST_VERSION' ) ) { + $version = Constants::get_constant( 'WC_API_REQUEST_VERSION' ); + } else { + $version = substr( WC_API::VERSION, 0, 1 ); + } + + $url = get_home_url( null, "wc-api/v{$version}/", is_ssl() ? 'https' : 'http' ); + + if ( ! empty( $path ) && is_string( $path ) ) { + $url .= ltrim( $path, '/' ); + } + + return $url; +} + +/** + * Get a log file path. + * + * @since 2.2 + * + * @param string $handle name. + * @return string the log file path. + */ +function wc_get_log_file_path( $handle ) { + return WC_Log_Handler_File::get_log_file_path( $handle ); +} + +/** + * Get a log file name. + * + * @since 3.3 + * + * @param string $handle Name. + * @return string The log file name. + */ +function wc_get_log_file_name( $handle ) { + return WC_Log_Handler_File::get_log_file_name( $handle ); +} + +/** + * Recursively get page children. + * + * @param int $page_id Page ID. + * @return int[] + */ +function wc_get_page_children( $page_id ) { + $page_ids = get_posts( + array( + 'post_parent' => $page_id, + 'post_type' => 'page', + 'numberposts' => -1, // @codingStandardsIgnoreLine + 'post_status' => 'any', + 'fields' => 'ids', + ) + ); + + if ( ! empty( $page_ids ) ) { + foreach ( $page_ids as $page_id ) { + $page_ids = array_merge( $page_ids, wc_get_page_children( $page_id ) ); + } + } + + return $page_ids; +} + +/** + * Flushes rewrite rules when the shop page (or it's children) gets saved. + */ +function flush_rewrite_rules_on_shop_page_save() { + $screen = get_current_screen(); + $screen_id = $screen ? $screen->id : ''; + + // Check if this is the edit page. + if ( 'page' !== $screen_id ) { + return; + } + + // Check if page is edited. + if ( empty( $_GET['post'] ) || empty( $_GET['action'] ) || ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return; + } + + $post_id = intval( $_GET['post'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $shop_page_id = wc_get_page_id( 'shop' ); + + if ( $shop_page_id === $post_id || in_array( $post_id, wc_get_page_children( $shop_page_id ), true ) ) { + do_action( 'woocommerce_flush_rewrite_rules' ); + } +} +add_action( 'admin_footer', 'flush_rewrite_rules_on_shop_page_save' ); + +/** + * Various rewrite rule fixes. + * + * @since 2.2 + * @param array $rules Rules. + * @return array + */ +function wc_fix_rewrite_rules( $rules ) { + global $wp_rewrite; + + $permalinks = wc_get_permalink_structure(); + + // Fix the rewrite rules when the product permalink have %product_cat% flag. + if ( preg_match( '`/(.+)(/%product_cat%)`', $permalinks['product_rewrite_slug'], $matches ) ) { + foreach ( $rules as $rule => $rewrite ) { + if ( preg_match( '`^' . preg_quote( $matches[1], '`' ) . '/\(`', $rule ) && preg_match( '/^(index\.php\?product_cat)(?!(.*product))/', $rewrite ) ) { + unset( $rules[ $rule ] ); + } + } + } + + // If the shop page is used as the base, we need to handle shop page subpages to avoid 404s. + if ( ! $permalinks['use_verbose_page_rules'] ) { + return $rules; + } + + $shop_page_id = wc_get_page_id( 'shop' ); + if ( $shop_page_id ) { + $page_rewrite_rules = array(); + $subpages = wc_get_page_children( $shop_page_id ); + + // Subpage rules. + foreach ( $subpages as $subpage ) { + $uri = get_page_uri( $subpage ); + $page_rewrite_rules[ $uri . '/?$' ] = 'index.php?pagename=' . $uri; + $wp_generated_rewrite_rules = $wp_rewrite->generate_rewrite_rules( $uri, EP_PAGES, true, true, false, false ); + foreach ( $wp_generated_rewrite_rules as $key => $value ) { + $wp_generated_rewrite_rules[ $key ] = $value . '&pagename=' . $uri; + } + $page_rewrite_rules = array_merge( $page_rewrite_rules, $wp_generated_rewrite_rules ); + } + + // Merge with rules. + $rules = array_merge( $page_rewrite_rules, $rules ); + } + + return $rules; +} +add_filter( 'rewrite_rules_array', 'wc_fix_rewrite_rules' ); + +/** + * Prevent product attachment links from breaking when using complex rewrite structures. + * + * @param string $link Link. + * @param int $post_id Post ID. + * @return string + */ +function wc_fix_product_attachment_link( $link, $post_id ) { + $parent_type = get_post_type( wp_get_post_parent_id( $post_id ) ); + if ( 'product' === $parent_type || 'product_variation' === $parent_type ) { + $link = home_url( '/?attachment_id=' . $post_id ); + } + return $link; +} +add_filter( 'attachment_link', 'wc_fix_product_attachment_link', 10, 2 ); + +/** + * Protect downloads from ms-files.php in multisite. + * + * @param string $rewrite rewrite rules. + * @return string + */ +function wc_ms_protect_download_rewite_rules( $rewrite ) { + if ( ! is_multisite() || 'redirect' === get_option( 'woocommerce_file_download_method' ) ) { + return $rewrite; + } + + $rule = "\n# WooCommerce Rules - Protect Files from ms-files.php\n\n"; + $rule .= "\n"; + $rule .= "RewriteEngine On\n"; + $rule .= "RewriteCond %{QUERY_STRING} file=woocommerce_uploads/ [NC]\n"; + $rule .= "RewriteRule /ms-files.php$ - [F]\n"; + $rule .= "\n\n"; + + return $rule . $rewrite; +} +add_filter( 'mod_rewrite_rules', 'wc_ms_protect_download_rewite_rules' ); + +/** + * Formats a string in the format COUNTRY:STATE into an array. + * + * @since 2.3.0 + * @param string $country_string Country string. + * @return array + */ +function wc_format_country_state_string( $country_string ) { + if ( strstr( $country_string, ':' ) ) { + list( $country, $state ) = explode( ':', $country_string ); + } else { + $country = $country_string; + $state = ''; + } + return array( + 'country' => $country, + 'state' => $state, + ); +} + +/** + * Get the store's base location. + * + * @since 2.3.0 + * @return array + */ +function wc_get_base_location() { + $default = apply_filters( 'woocommerce_get_base_location', get_option( 'woocommerce_default_country', 'US:CA' ) ); + + return wc_format_country_state_string( $default ); +} + +/** + * Get the customer's default location. + * + * Filtered, and set to base location or left blank. If cache-busting, + * this should only be used when 'location' is set in the querystring. + * + * @since 2.3.0 + * @return array + */ +function wc_get_customer_default_location() { + $set_default_location_to = get_option( 'woocommerce_default_customer_address', 'base' ); + $default_location = '' === $set_default_location_to ? '' : get_option( 'woocommerce_default_country', 'US:CA' ); + $location = wc_format_country_state_string( apply_filters( 'woocommerce_customer_default_location', $default_location ) ); + + // Geolocation takes priority if used and if geolocation is possible. + if ( 'geolocation' === $set_default_location_to || 'geolocation_ajax' === $set_default_location_to ) { + $ua = wc_get_user_agent(); + + // Exclude common bots from geolocation by user agent. + if ( ! stristr( $ua, 'bot' ) && ! stristr( $ua, 'spider' ) && ! stristr( $ua, 'crawl' ) ) { + $geolocation = WC_Geolocation::geolocate_ip( '', true, false ); + + if ( ! empty( $geolocation['country'] ) ) { + $location = $geolocation; + } + } + } + + // Once we have a location, ensure it's valid, otherwise fallback to a valid location. + $allowed_country_codes = WC()->countries->get_allowed_countries(); + + if ( ! empty( $location['country'] ) && ! array_key_exists( $location['country'], $allowed_country_codes ) ) { + $location['country'] = current( array_keys( $allowed_country_codes ) ); + $location['state'] = ''; + } + + return apply_filters( 'woocommerce_customer_default_location_array', $location ); +} + +/** + * Get user agent string. + * + * @since 3.0.0 + * @return string + */ +function wc_get_user_agent() { + return isset( $_SERVER['HTTP_USER_AGENT'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; // @codingStandardsIgnoreLine +} + +/** + * Generate a rand hash. + * + * @since 2.4.0 + * @return string + */ +function wc_rand_hash() { + if ( ! function_exists( 'openssl_random_pseudo_bytes' ) ) { + return sha1( wp_rand() ); + } + + return bin2hex( openssl_random_pseudo_bytes( 20 ) ); // @codingStandardsIgnoreLine +} + +/** + * WC API - Hash. + * + * @since 2.4.0 + * @param string $data Message to be hashed. + * @return string + */ +function wc_api_hash( $data ) { + return hash_hmac( 'sha256', $data, 'wc-api' ); +} + +/** + * Find all possible combinations of values from the input array and return in a logical order. + * + * @since 2.5.0 + * @param array $input Input. + * @return array + */ +function wc_array_cartesian( $input ) { + $input = array_filter( $input ); + $results = array(); + $indexes = array(); + $index = 0; + + // Generate indexes from keys and values so we have a logical sort order. + foreach ( $input as $key => $values ) { + foreach ( $values as $value ) { + $indexes[ $key ][ $value ] = $index++; + } + } + + // Loop over the 2D array of indexes and generate all combinations. + foreach ( $indexes as $key => $values ) { + // When result is empty, fill with the values of the first looped array. + if ( empty( $results ) ) { + foreach ( $values as $value ) { + $results[] = array( $key => $value ); + } + } else { + // Second and subsequent input sub-array merging. + foreach ( $results as $result_key => $result ) { + foreach ( $values as $value ) { + // If the key is not set, we can set it. + if ( ! isset( $results[ $result_key ][ $key ] ) ) { + $results[ $result_key ][ $key ] = $value; + } else { + // If the key is set, we can add a new combination to the results array. + $new_combination = $results[ $result_key ]; + $new_combination[ $key ] = $value; + $results[] = $new_combination; + } + } + } + } + } + + // Sort the indexes. + arsort( $results ); + + // Convert indexes back to values. + foreach ( $results as $result_key => $result ) { + $converted_values = array(); + + // Sort the values. + arsort( $results[ $result_key ] ); + + // Convert the values. + foreach ( $results[ $result_key ] as $key => $value ) { + $converted_values[ $key ] = array_search( $value, $indexes[ $key ], true ); + } + + $results[ $result_key ] = $converted_values; + } + + return $results; +} + +/** + * Run a MySQL transaction query, if supported. + * + * @since 2.5.0 + * @param string $type Types: start (default), commit, rollback. + * @param bool $force use of transactions. + */ +function wc_transaction_query( $type = 'start', $force = false ) { + global $wpdb; + + $wpdb->hide_errors(); + + wc_maybe_define_constant( 'WC_USE_TRANSACTIONS', true ); + + if ( Constants::is_true( 'WC_USE_TRANSACTIONS' ) || $force ) { + switch ( $type ) { + case 'commit': + $wpdb->query( 'COMMIT' ); + break; + case 'rollback': + $wpdb->query( 'ROLLBACK' ); + break; + default: + $wpdb->query( 'START TRANSACTION' ); + break; + } + } +} + +/** + * Gets the url to the cart page. + * + * @since 2.5.0 + * + * @return string Url to cart page + */ +function wc_get_cart_url() { + return apply_filters( 'woocommerce_get_cart_url', wc_get_page_permalink( 'cart' ) ); +} + +/** + * Gets the url to the checkout page. + * + * @since 2.5.0 + * + * @return string Url to checkout page + */ +function wc_get_checkout_url() { + $checkout_url = wc_get_page_permalink( 'checkout' ); + if ( $checkout_url ) { + // Force SSL if needed. + if ( is_ssl() || 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ) { + $checkout_url = str_replace( 'http:', 'https:', $checkout_url ); + } + } + + return apply_filters( 'woocommerce_get_checkout_url', $checkout_url ); +} + +/** + * Register a shipping method. + * + * @since 1.5.7 + * @param string|object $shipping_method class name (string) or a class object. + */ +function woocommerce_register_shipping_method( $shipping_method ) { + WC()->shipping()->register_shipping_method( $shipping_method ); +} + +if ( ! function_exists( 'wc_get_shipping_zone' ) ) { + /** + * Get the shipping zone matching a given package from the cart. + * + * @since 2.6.0 + * @uses WC_Shipping_Zones::get_zone_matching_package + * @param array $package Shipping package. + * @return WC_Shipping_Zone + */ + function wc_get_shipping_zone( $package ) { + return WC_Shipping_Zones::get_zone_matching_package( $package ); + } +} + +/** + * Get a nice name for credit card providers. + * + * @since 2.6.0 + * @param string $type Provider Slug/Type. + * @return string + */ +function wc_get_credit_card_type_label( $type ) { + // Normalize. + $type = strtolower( $type ); + $type = str_replace( '-', ' ', $type ); + $type = str_replace( '_', ' ', $type ); + + $labels = apply_filters( + 'woocommerce_credit_card_type_labels', + array( + 'mastercard' => __( 'MasterCard', 'woocommerce' ), + 'visa' => __( 'Visa', 'woocommerce' ), + 'discover' => __( 'Discover', 'woocommerce' ), + 'american express' => __( 'American Express', 'woocommerce' ), + 'diners' => __( 'Diners', 'woocommerce' ), + 'jcb' => __( 'JCB', 'woocommerce' ), + ) + ); + + return apply_filters( 'woocommerce_get_credit_card_type_label', ( array_key_exists( $type, $labels ) ? $labels[ $type ] : ucfirst( $type ) ) ); +} + +/** + * Outputs a "back" link so admin screens can easily jump back a page. + * + * @param string $label Title of the page to return to. + * @param string $url URL of the page to return to. + */ +function wc_back_link( $label, $url ) { + echo ''; +} + +/** + * Display a WooCommerce help tip. + * + * @since 2.5.0 + * + * @param string $tip Help tip text. + * @param bool $allow_html Allow sanitized HTML if true or escape. + * @return string + */ +function wc_help_tip( $tip, $allow_html = false ) { + if ( $allow_html ) { + $tip = wc_sanitize_tooltip( $tip ); + } else { + $tip = esc_attr( $tip ); + } + + return ''; +} + +/** + * Return a list of potential postcodes for wildcard searching. + * + * @since 2.6.0 + * @param string $postcode Postcode. + * @param string $country Country to format postcode for matching. + * @return string[] + */ +function wc_get_wildcard_postcodes( $postcode, $country = '' ) { + $formatted_postcode = wc_format_postcode( $postcode, $country ); + $length = function_exists( 'mb_strlen' ) ? mb_strlen( $formatted_postcode ) : strlen( $formatted_postcode ); + $postcodes = array( + $postcode, + $formatted_postcode, + $formatted_postcode . '*', + ); + + for ( $i = 0; $i < $length; $i ++ ) { + $postcodes[] = ( function_exists( 'mb_substr' ) ? mb_substr( $formatted_postcode, 0, ( $i + 1 ) * -1 ) : substr( $formatted_postcode, 0, ( $i + 1 ) * -1 ) ) . '*'; + } + + return $postcodes; +} + +/** + * Used by shipping zones and taxes to compare a given $postcode to stored + * postcodes to find matches for numerical ranges, and wildcards. + * + * @since 2.6.0 + * @param string $postcode Postcode you want to match against stored postcodes. + * @param array $objects Array of postcode objects from Database. + * @param string $object_id_key DB column name for the ID. + * @param string $object_compare_key DB column name for the value. + * @param string $country Country from which this postcode belongs. Allows for formatting. + * @return array Array of matching object ID and matching values. + */ +function wc_postcode_location_matcher( $postcode, $objects, $object_id_key, $object_compare_key, $country = '' ) { + $postcode = wc_normalize_postcode( $postcode ); + $wildcard_postcodes = array_map( 'wc_clean', wc_get_wildcard_postcodes( $postcode, $country ) ); + $matches = array(); + + foreach ( $objects as $object ) { + $object_id = $object->$object_id_key; + $compare_against = $object->$object_compare_key; + + // Handle postcodes containing ranges. + if ( strstr( $compare_against, '...' ) ) { + $range = array_map( 'trim', explode( '...', $compare_against ) ); + + if ( 2 !== count( $range ) ) { + continue; + } + + list( $min, $max ) = $range; + + // If the postcode is non-numeric, make it numeric. + if ( ! is_numeric( $min ) || ! is_numeric( $max ) ) { + $compare = wc_make_numeric_postcode( $postcode ); + $min = str_pad( wc_make_numeric_postcode( $min ), strlen( $compare ), '0' ); + $max = str_pad( wc_make_numeric_postcode( $max ), strlen( $compare ), '0' ); + } else { + $compare = $postcode; + } + + if ( $compare >= $min && $compare <= $max ) { + $matches[ $object_id ] = isset( $matches[ $object_id ] ) ? $matches[ $object_id ] : array(); + $matches[ $object_id ][] = $compare_against; + } + } elseif ( in_array( $compare_against, $wildcard_postcodes, true ) ) { + // Wildcard and standard comparison. + $matches[ $object_id ] = isset( $matches[ $object_id ] ) ? $matches[ $object_id ] : array(); + $matches[ $object_id ][] = $compare_against; + } + } + + return $matches; +} + +/** + * Gets number of shipping methods currently enabled. Used to identify if + * shipping is configured. + * + * @since 2.6.0 + * @param bool $include_legacy Count legacy shipping methods too. + * @param bool $enabled_only Whether non-legacy shipping methods should be + * restricted to enabled ones. It doesn't affect + * legacy shipping methods. @since 4.3.0. + * @return int + */ +function wc_get_shipping_method_count( $include_legacy = false, $enabled_only = false ) { + global $wpdb; + + $transient_name = $include_legacy ? 'wc_shipping_method_count_legacy' : 'wc_shipping_method_count'; + $transient_version = WC_Cache_Helper::get_transient_version( 'shipping' ); + $transient_value = get_transient( $transient_name ); + + if ( isset( $transient_value['value'], $transient_value['version'] ) && $transient_value['version'] === $transient_version ) { + return absint( $transient_value['value'] ); + } + + $where_clause = $enabled_only ? 'WHERE is_enabled=1' : ''; + $method_count = absint( $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods ${where_clause}" ) ); + + if ( $include_legacy ) { + // Count activated methods that don't support shipping zones. + $methods = WC()->shipping()->get_shipping_methods(); + + foreach ( $methods as $method ) { + if ( isset( $method->enabled ) && 'yes' === $method->enabled && ! $method->supports( 'shipping-zones' ) ) { + $method_count++; + } + } + } + + $transient_value = array( + 'version' => $transient_version, + 'value' => $method_count, + ); + + set_transient( $transient_name, $transient_value, DAY_IN_SECONDS * 30 ); + + return $method_count; +} + +/** + * Wrapper for set_time_limit to see if it is enabled. + * + * @since 2.6.0 + * @param int $limit Time limit. + */ +function wc_set_time_limit( $limit = 0 ) { + if ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) { // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.safe_modeDeprecatedRemoved + @set_time_limit( $limit ); // @codingStandardsIgnoreLine + } +} + +/** + * Wrapper for nocache_headers which also disables page caching. + * + * @since 3.2.4 + */ +function wc_nocache_headers() { + WC_Cache_Helper::set_nocache_constants(); + nocache_headers(); +} + +/** + * Used to sort products attributes with uasort. + * + * @since 2.6.0 + * @param array $a First attribute to compare. + * @param array $b Second attribute to compare. + * @return int + */ +function wc_product_attribute_uasort_comparison( $a, $b ) { + $a_position = is_null( $a ) ? null : $a['position']; + $b_position = is_null( $b ) ? null : $b['position']; + return wc_uasort_comparison( $a_position, $b_position ); +} + +/** + * Used to sort shipping zone methods with uasort. + * + * @since 3.0.0 + * @param array $a First shipping zone method to compare. + * @param array $b Second shipping zone method to compare. + * @return int + */ +function wc_shipping_zone_method_order_uasort_comparison( $a, $b ) { + return wc_uasort_comparison( $a->method_order, $b->method_order ); +} + +/** + * User to sort checkout fields based on priority with uasort. + * + * @since 3.5.1 + * @param array $a First field to compare. + * @param array $b Second field to compare. + * @return int + */ +function wc_checkout_fields_uasort_comparison( $a, $b ) { + /* + * We are not guaranteed to get a priority + * setting. So don't compare if they don't + * exist. + */ + if ( ! isset( $a['priority'], $b['priority'] ) ) { + return 0; + } + + return wc_uasort_comparison( $a['priority'], $b['priority'] ); +} + +/** + * User to sort two values with ausort. + * + * @since 3.5.1 + * @param int $a First value to compare. + * @param int $b Second value to compare. + * @return int + */ +function wc_uasort_comparison( $a, $b ) { + if ( $a === $b ) { + return 0; + } + return ( $a < $b ) ? -1 : 1; +} + +/** + * Sort values based on ascii, usefull for special chars in strings. + * + * @param string $a First value. + * @param string $b Second value. + * @return int + */ +function wc_ascii_uasort_comparison( $a, $b ) { + $a = remove_accents( $a ); + $b = remove_accents( $b ); + return strcmp( $a, $b ); +} + +/** + * Sort array according to current locale rules and maintaining index association. + * By default tries to use Collator from PHP Internationalization Functions if available. + * If PHP Collator class doesn't exists it fallback to removing accepts from a array + * and by sorting with `uasort( $data, 'strcmp' )` giving support for ASCII values. + * + * @since 4.6.0 + * @param array $data List of values to sort. + * @param string $locale Locale. + * @return array + */ +function wc_asort_by_locale( &$data, $locale = '' ) { + // Use Collator if PHP Internationalization Functions (php-intl) is available. + if ( class_exists( 'Collator' ) ) { + try { + $locale = $locale ? $locale : get_locale(); + $collator = new Collator( $locale ); + $collator->asort( $data, Collator::SORT_STRING ); + return $data; + } catch ( IntlException $e ) { + /* + * Just skip if some error got caused. + * It may be caused in installations that doesn't include ICU TZData. + */ + if ( Constants::is_true( 'WP_DEBUG' ) ) { + error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + sprintf( + 'An unexpected error occurred while trying to use PHP Intl Collator class, it may be caused by an incorrect installation of PHP Intl and ICU, and could be fixed by reinstallaing PHP Intl, see more details about PHP Intl installation: %1$s. Error message: %2$s', + 'https://www.php.net/manual/en/intl.installation.php', + $e->getMessage() + ) + ); + } + } + } + + $raw_data = $data; + + array_walk( + $data, + function ( &$value ) { + $value = remove_accents( html_entity_decode( $value ) ); + } + ); + + uasort( $data, 'strcmp' ); + + foreach ( $data as $key => $val ) { + $data[ $key ] = $raw_data[ $key ]; + } + + return $data; +} + +/** + * Get rounding mode for internal tax calculations. + * + * @since 3.2.4 + * @return int + */ +function wc_get_tax_rounding_mode() { + $constant = WC_TAX_ROUNDING_MODE; + + if ( 'auto' === $constant ) { + return 'yes' === get_option( 'woocommerce_prices_include_tax', 'no' ) ? PHP_ROUND_HALF_DOWN : PHP_ROUND_HALF_UP; + } + + return intval( $constant ); +} + +/** + * Get rounding precision for internal WC calculations. + * Will increase the precision of wc_get_price_decimals by 2 decimals, unless WC_ROUNDING_PRECISION is set to a higher number. + * + * @since 2.6.3 + * @return int + */ +function wc_get_rounding_precision() { + $precision = wc_get_price_decimals() + 2; + if ( absint( WC_ROUNDING_PRECISION ) > $precision ) { + $precision = absint( WC_ROUNDING_PRECISION ); + } + return $precision; +} + +/** + * Add precision to a number and return a number. + * + * @since 3.2.0 + * @param float $value Number to add precision to. + * @param bool $round If should round after adding precision. + * @return int|float + */ +function wc_add_number_precision( $value, $round = true ) { + $cent_precision = pow( 10, wc_get_price_decimals() ); + $value = $value * $cent_precision; + return $round ? NumberUtil::round( $value, wc_get_rounding_precision() - wc_get_price_decimals() ) : $value; +} + +/** + * Remove precision from a number and return a float. + * + * @since 3.2.0 + * @param float $value Number to add precision to. + * @return float + */ +function wc_remove_number_precision( $value ) { + $cent_precision = pow( 10, wc_get_price_decimals() ); + return $value / $cent_precision; +} + +/** + * Add precision to an array of number and return an array of int. + * + * @since 3.2.0 + * @param array $value Number to add precision to. + * @param bool $round Should we round after adding precision?. + * @return int|array + */ +function wc_add_number_precision_deep( $value, $round = true ) { + if ( ! is_array( $value ) ) { + return wc_add_number_precision( $value, $round ); + } + + foreach ( $value as $key => $sub_value ) { + $value[ $key ] = wc_add_number_precision_deep( $sub_value, $round ); + } + + return $value; +} + +/** + * Remove precision from an array of number and return an array of int. + * + * @since 3.2.0 + * @param array $value Number to add precision to. + * @return int|array + */ +function wc_remove_number_precision_deep( $value ) { + if ( ! is_array( $value ) ) { + return wc_remove_number_precision( $value ); + } + + foreach ( $value as $key => $sub_value ) { + $value[ $key ] = wc_remove_number_precision_deep( $sub_value ); + } + + return $value; +} + +/** + * Get a shared logger instance. + * + * Use the woocommerce_logging_class filter to change the logging class. You may provide one of the following: + * - a class name which will be instantiated as `new $class` with no arguments + * - an instance which will be used directly as the logger + * In either case, the class or instance *must* implement WC_Logger_Interface. + * + * @see WC_Logger_Interface + * + * @return WC_Logger + */ +function wc_get_logger() { + static $logger = null; + + $class = apply_filters( 'woocommerce_logging_class', 'WC_Logger' ); + + if ( null !== $logger && is_string( $class ) && is_a( $logger, $class ) ) { + return $logger; + } + + $implements = class_implements( $class ); + + if ( is_array( $implements ) && in_array( 'WC_Logger_Interface', $implements, true ) ) { + $logger = is_object( $class ) ? $class : new $class(); + } else { + wc_doing_it_wrong( + __FUNCTION__, + sprintf( + /* translators: 1: class name 2: woocommerce_logging_class 3: WC_Logger_Interface */ + __( 'The class %1$s provided by %2$s filter must implement %3$s.', 'woocommerce' ), + '' . esc_html( is_object( $class ) ? get_class( $class ) : $class ) . '', + 'woocommerce_logging_class', + 'WC_Logger_Interface' + ), + '3.0' + ); + + $logger = is_a( $logger, 'WC_Logger' ) ? $logger : new WC_Logger(); + } + + return $logger; +} + +/** + * Trigger logging cleanup using the logging class. + * + * @since 3.4.0 + */ +function wc_cleanup_logs() { + $logger = wc_get_logger(); + + if ( is_callable( array( $logger, 'clear_expired_logs' ) ) ) { + $logger->clear_expired_logs(); + } +} +add_action( 'woocommerce_cleanup_logs', 'wc_cleanup_logs' ); + +/** + * Prints human-readable information about a variable. + * + * Some server environments block some debugging functions. This function provides a safe way to + * turn an expression into a printable, readable form without calling blocked functions. + * + * @since 3.0 + * + * @param mixed $expression The expression to be printed. + * @param bool $return Optional. Default false. Set to true to return the human-readable string. + * @return string|bool False if expression could not be printed. True if the expression was printed. + * If $return is true, a string representation will be returned. + */ +function wc_print_r( $expression, $return = false ) { + $alternatives = array( + array( + 'func' => 'print_r', + 'args' => array( $expression, true ), + ), + array( + 'func' => 'var_export', + 'args' => array( $expression, true ), + ), + array( + 'func' => 'json_encode', + 'args' => array( $expression ), + ), + array( + 'func' => 'serialize', + 'args' => array( $expression ), + ), + ); + + $alternatives = apply_filters( 'woocommerce_print_r_alternatives', $alternatives, $expression ); + + foreach ( $alternatives as $alternative ) { + if ( function_exists( $alternative['func'] ) ) { + $res = $alternative['func']( ...$alternative['args'] ); + if ( $return ) { + return $res; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + echo $res; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + return true; + } + } + + return false; +} + +/** + * Registers the default log handler. + * + * @since 3.0 + * @param array $handlers Handlers. + * @return array + */ +function wc_register_default_log_handler( $handlers ) { + $handler_class = Constants::get_constant( 'WC_LOG_HANDLER' ); + if ( ! class_exists( $handler_class ) ) { + $handler_class = WC_Log_Handler_File::class; + } + + array_push( $handlers, new $handler_class() ); + + return $handlers; +} +add_filter( 'woocommerce_register_log_handlers', 'wc_register_default_log_handler' ); + +/** + * Based on wp_list_pluck, this calls a method instead of returning a property. + * + * @since 3.0.0 + * @param array $list List of objects or arrays. + * @param int|string $callback_or_field Callback method from the object to place instead of the entire object. + * @param int|string $index_key Optional. Field from the object to use as keys for the new array. + * Default null. + * @return array Array of values. + */ +function wc_list_pluck( $list, $callback_or_field, $index_key = null ) { + // Use wp_list_pluck if this isn't a callback. + $first_el = current( $list ); + if ( ! is_object( $first_el ) || ! is_callable( array( $first_el, $callback_or_field ) ) ) { + return wp_list_pluck( $list, $callback_or_field, $index_key ); + } + if ( ! $index_key ) { + /* + * This is simple. Could at some point wrap array_column() + * if we knew we had an array of arrays. + */ + foreach ( $list as $key => $value ) { + $list[ $key ] = $value->{$callback_or_field}(); + } + return $list; + } + + /* + * When index_key is not set for a particular item, push the value + * to the end of the stack. This is how array_column() behaves. + */ + $newlist = array(); + foreach ( $list as $value ) { + // Get index. @since 3.2.0 this supports a callback. + if ( is_callable( array( $value, $index_key ) ) ) { + $newlist[ $value->{$index_key}() ] = $value->{$callback_or_field}(); + } elseif ( isset( $value->$index_key ) ) { + $newlist[ $value->$index_key ] = $value->{$callback_or_field}(); + } else { + $newlist[] = $value->{$callback_or_field}(); + } + } + return $newlist; +} + +/** + * Get permalink settings for things like products and taxonomies. + * + * As of 3.3.0, the permalink settings are stored to the option instead of + * being blank and inheritting from the locale. This speeds up page loading + * times by negating the need to switch locales on each page load. + * + * This is more inline with WP core behavior which does not localize slugs. + * + * @since 3.0.0 + * @return array + */ +function wc_get_permalink_structure() { + $saved_permalinks = (array) get_option( 'woocommerce_permalinks', array() ); + $permalinks = wp_parse_args( + array_filter( $saved_permalinks ), + array( + 'product_base' => _x( 'product', 'slug', 'woocommerce' ), + 'category_base' => _x( 'product-category', 'slug', 'woocommerce' ), + 'tag_base' => _x( 'product-tag', 'slug', 'woocommerce' ), + 'attribute_base' => '', + 'use_verbose_page_rules' => false, + ) + ); + + if ( $saved_permalinks !== $permalinks ) { + update_option( 'woocommerce_permalinks', $permalinks ); + } + + $permalinks['product_rewrite_slug'] = untrailingslashit( $permalinks['product_base'] ); + $permalinks['category_rewrite_slug'] = untrailingslashit( $permalinks['category_base'] ); + $permalinks['tag_rewrite_slug'] = untrailingslashit( $permalinks['tag_base'] ); + $permalinks['attribute_rewrite_slug'] = untrailingslashit( $permalinks['attribute_base'] ); + + return $permalinks; +} + +/** + * Switch WooCommerce to site language. + * + * @since 3.1.0 + */ +function wc_switch_to_site_locale() { + if ( function_exists( 'switch_to_locale' ) ) { + switch_to_locale( get_locale() ); + + // Filter on plugin_locale so load_plugin_textdomain loads the correct locale. + add_filter( 'plugin_locale', 'get_locale' ); + + // Init WC locale. + WC()->load_plugin_textdomain(); + } +} + +/** + * Switch WooCommerce language to original. + * + * @since 3.1.0 + */ +function wc_restore_locale() { + if ( function_exists( 'restore_previous_locale' ) ) { + restore_previous_locale(); + + // Remove filter. + remove_filter( 'plugin_locale', 'get_locale' ); + + // Init WC locale. + WC()->load_plugin_textdomain(); + } +} + +/** + * Convert plaintext phone number to clickable phone number. + * + * Remove formatting and allow "+". + * Example and specs: https://developer.mozilla.org/en/docs/Web/HTML/Element/a#Creating_a_phone_link + * + * @since 3.1.0 + * + * @param string $phone Content to convert phone number. + * @return string Content with converted phone number. + */ +function wc_make_phone_clickable( $phone ) { + $number = trim( preg_replace( '/[^\d|\+]/', '', $phone ) ); + + return $number ? '' . esc_html( $phone ) . '' : ''; +} + +/** + * Get an item of post data if set, otherwise return a default value. + * + * @since 3.0.9 + * @param string $key Meta key. + * @param string $default Default value. + * @return mixed Value sanitized by wc_clean. + */ +function wc_get_post_data_by_key( $key, $default = '' ) { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing + return wc_clean( wp_unslash( wc_get_var( $_POST[ $key ], $default ) ) ); +} + +/** + * Get data if set, otherwise return a default value or null. Prevents notices when data is not set. + * + * @since 3.2.0 + * @param mixed $var Variable. + * @param string $default Default value. + * @return mixed + */ +function wc_get_var( &$var, $default = null ) { + return isset( $var ) ? $var : $default; +} + +/** + * Read in WooCommerce headers when reading plugin headers. + * + * @since 3.2.0 + * @param array $headers Headers. + * @return array + */ +function wc_enable_wc_plugin_headers( $headers ) { + if ( ! class_exists( 'WC_Plugin_Updates' ) ) { + include_once dirname( __FILE__ ) . '/admin/plugin-updates/class-wc-plugin-updates.php'; + } + + // WC requires at least - allows developers to define which version of WooCommerce the plugin requires to run. + $headers[] = WC_Plugin_Updates::VERSION_REQUIRED_HEADER; + + // WC tested up to - allows developers to define which version of WooCommerce they have tested up to. + $headers[] = WC_Plugin_Updates::VERSION_TESTED_HEADER; + + // Woo - This is used in WooCommerce extensions and is picked up by the helper. + $headers[] = 'Woo'; + + return $headers; +} +add_filter( 'extra_theme_headers', 'wc_enable_wc_plugin_headers' ); +add_filter( 'extra_plugin_headers', 'wc_enable_wc_plugin_headers' ); + +/** + * Prevent auto-updating the WooCommerce plugin on major releases if there are untested extensions active. + * + * @since 3.2.0 + * @param bool $should_update If should update. + * @param object $plugin Plugin data. + * @return bool + */ +function wc_prevent_dangerous_auto_updates( $should_update, $plugin ) { + if ( ! isset( $plugin->plugin, $plugin->new_version ) ) { + return $should_update; + } + + if ( 'woocommerce/woocommerce.php' !== $plugin->plugin ) { + return $should_update; + } + + if ( ! class_exists( 'WC_Plugin_Updates' ) ) { + include_once dirname( __FILE__ ) . '/admin/plugin-updates/class-wc-plugin-updates.php'; + } + + $new_version = wc_clean( $plugin->new_version ); + $plugin_updates = new WC_Plugin_Updates(); + $version_type = Constants::get_constant( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' ); + if ( ! is_string( $version_type ) ) { + $version_type = 'none'; + } + $untested_plugins = $plugin_updates->get_untested_plugins( $new_version, $version_type ); + if ( ! empty( $untested_plugins ) ) { + return false; + } + + return $should_update; +} +add_filter( 'auto_update_plugin', 'wc_prevent_dangerous_auto_updates', 99, 2 ); + +/** + * Delete expired transients. + * + * Deletes all expired transients. The multi-table delete syntax is used. + * to delete the transient record from table a, and the corresponding. + * transient_timeout record from table b. + * + * Based on code inside core's upgrade_network() function. + * + * @since 3.2.0 + * @return int Number of transients that were cleared. + */ +function wc_delete_expired_transients() { + global $wpdb; + + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + $sql = "DELETE a, b FROM $wpdb->options a, $wpdb->options b + WHERE a.option_name LIKE %s + AND a.option_name NOT LIKE %s + AND b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) ) + AND b.option_value < %d"; + $rows = $wpdb->query( $wpdb->prepare( $sql, $wpdb->esc_like( '_transient_' ) . '%', $wpdb->esc_like( '_transient_timeout_' ) . '%', time() ) ); + + $sql = "DELETE a, b FROM $wpdb->options a, $wpdb->options b + WHERE a.option_name LIKE %s + AND a.option_name NOT LIKE %s + AND b.option_name = CONCAT( '_site_transient_timeout_', SUBSTRING( a.option_name, 17 ) ) + AND b.option_value < %d"; + $rows2 = $wpdb->query( $wpdb->prepare( $sql, $wpdb->esc_like( '_site_transient_' ) . '%', $wpdb->esc_like( '_site_transient_timeout_' ) . '%', time() ) ); + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared + + return absint( $rows + $rows2 ); +} +add_action( 'woocommerce_installed', 'wc_delete_expired_transients' ); + +/** + * Make a URL relative, if possible. + * + * @since 3.2.0 + * @param string $url URL to make relative. + * @return string + */ +function wc_get_relative_url( $url ) { + return wc_is_external_resource( $url ) ? $url : str_replace( array( 'http://', 'https://' ), '//', $url ); +} + +/** + * See if a resource is remote. + * + * @since 3.2.0 + * @param string $url URL to check. + * @return bool + */ +function wc_is_external_resource( $url ) { + $wp_base = str_replace( array( 'http://', 'https://' ), '//', get_home_url( null, '/', 'http' ) ); + + return strstr( $url, '://' ) && ! strstr( $url, $wp_base ); +} + +/** + * See if theme/s is activate or not. + * + * @since 3.3.0 + * @param string|array $theme Theme name or array of theme names to check. + * @return boolean + */ +function wc_is_active_theme( $theme ) { + return is_array( $theme ) ? in_array( get_template(), $theme, true ) : get_template() === $theme; +} + +/** + * Is the site using a default WP theme? + * + * @return boolean + */ +function wc_is_wp_default_theme_active() { + return wc_is_active_theme( + array( + 'twentytwentytwo', + 'twentytwentyone', + 'twentytwenty', + 'twentynineteen', + 'twentyseventeen', + 'twentysixteen', + 'twentyfifteen', + 'twentyfourteen', + 'twentythirteen', + 'twentyeleven', + 'twentytwelve', + 'twentyten', + ) + ); +} + +/** + * Cleans up session data - cron callback. + * + * @since 3.3.0 + */ +function wc_cleanup_session_data() { + $session_class = apply_filters( 'woocommerce_session_handler', 'WC_Session_Handler' ); + $session = new $session_class(); + + if ( is_callable( array( $session, 'cleanup_sessions' ) ) ) { + $session->cleanup_sessions(); + } +} +add_action( 'woocommerce_cleanup_sessions', 'wc_cleanup_session_data' ); + +/** + * Convert a decimal (e.g. 3.5) to a fraction (e.g. 7/2). + * From: https://www.designedbyaturtle.co.uk/2015/converting-a-decimal-to-a-fraction-in-php/ + * + * @param float $decimal the decimal number. + * @return array|bool a 1/2 would be [1, 2] array (this can be imploded with '/' to form a string). + */ +function wc_decimal_to_fraction( $decimal ) { + if ( 0 > $decimal || ! is_numeric( $decimal ) ) { + // Negative digits need to be passed in as positive numbers and prefixed as negative once the response is imploded. + return false; + } + + if ( 0 === $decimal ) { + return array( 0, 1 ); + } + + $tolerance = 1.e-4; + $numerator = 1; + $h2 = 0; + $denominator = 0; + $k2 = 1; + $b = 1 / $decimal; + + do { + $b = 1 / $b; + $a = floor( $b ); + $aux = $numerator; + $numerator = $a * $numerator + $h2; + $h2 = $aux; + $aux = $denominator; + $denominator = $a * $denominator + $k2; + $k2 = $aux; + $b = $b - $a; + } while ( abs( $decimal - $numerator / $denominator ) > $decimal * $tolerance ); + + return array( $numerator, $denominator ); +} + +/** + * Round discount. + * + * @param double $value Amount to round. + * @param int $precision DP to round. + * @return float + */ +function wc_round_discount( $value, $precision ) { + if ( version_compare( PHP_VERSION, '5.3.0', '>=' ) ) { + return NumberUtil::round( $value, $precision, WC_DISCOUNT_ROUNDING_MODE ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.round_modeFound + } + + if ( PHP_ROUND_HALF_DOWN === WC_DISCOUNT_ROUNDING_MODE ) { + return wc_legacy_round_half_down( $value, $precision ); + } + + return NumberUtil::round( $value, $precision ); +} + +/** + * Return the html selected attribute if stringified $value is found in array of stringified $options + * or if stringified $value is the same as scalar stringified $options. + * + * @param string|int $value Value to find within options. + * @param string|int|array $options Options to go through when looking for value. + * @return string + */ +function wc_selected( $value, $options ) { + if ( is_array( $options ) ) { + $options = array_map( 'strval', $options ); + return selected( in_array( (string) $value, $options, true ), true, false ); + } + + return selected( $value, $options, false ); +} + +/** + * Retrieves the MySQL server version. Based on $wpdb. + * + * @since 3.4.1 + * @return array Vesion information. + */ +function wc_get_server_database_version() { + global $wpdb; + + if ( empty( $wpdb->is_mysql ) ) { + return array( + 'string' => '', + 'number' => '', + ); + } + + // phpcs:disable WordPress.DB.RestrictedFunctions, PHPCompatibility.Extensions.RemovedExtensions.mysql_DeprecatedRemoved + if ( $wpdb->use_mysqli ) { + $server_info = mysqli_get_server_info( $wpdb->dbh ); + } else { + $server_info = mysql_get_server_info( $wpdb->dbh ); + } + // phpcs:enable WordPress.DB.RestrictedFunctions, PHPCompatibility.Extensions.RemovedExtensions.mysql_DeprecatedRemoved + + return array( + 'string' => $server_info, + 'number' => preg_replace( '/([^\d.]+).*/', '', $server_info ), + ); +} + +/** + * Initialize and load the cart functionality. + * + * @since 3.6.4 + * @return void + */ +function wc_load_cart() { + if ( ! did_action( 'before_woocommerce_init' ) || doing_action( 'before_woocommerce_init' ) ) { + /* translators: 1: wc_load_cart 2: woocommerce_init */ + wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%1$s should not be called before the %2$s action.', 'woocommerce' ), 'wc_load_cart', 'woocommerce_init' ), '3.7' ); + return; + } + + // Ensure dependencies are loaded in all contexts. + include_once WC_ABSPATH . 'includes/wc-cart-functions.php'; + include_once WC_ABSPATH . 'includes/wc-notice-functions.php'; + + WC()->initialize_session(); + WC()->initialize_cart(); +} + +/** + * Test whether the context of execution comes from async action scheduler. + * + * @since 4.0.0 + * @return bool + */ +function wc_is_running_from_async_action_scheduler() { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return isset( $_REQUEST['action'] ) && 'as_async_request_queue_runner' === $_REQUEST['action']; +} + +/** + * Polyfill for wp_cache_get_multiple for WP versions before 5.5. + * + * @param array $keys Array of keys to get from group. + * @param string $group Optional. Where the cache contents are grouped. Default empty. + * @param bool $force Optional. Whether to force an update of the local cache from the persistent + * cache. Default false. + * @return array|bool Array of values. + */ +function wc_cache_get_multiple( $keys, $group = '', $force = false ) { + if ( function_exists( 'wp_cache_get_multiple' ) ) { + return wp_cache_get_multiple( $keys, $group, $force ); + } + $values = array(); + foreach ( $keys as $key ) { + $values[ $key ] = wp_cache_get( $key, $group, $force ); + } + return $values; +} diff --git a/includes/wc-coupon-functions.php b/plugins/woocommerce/includes/wc-coupon-functions.php similarity index 100% rename from includes/wc-coupon-functions.php rename to plugins/woocommerce/includes/wc-coupon-functions.php diff --git a/plugins/woocommerce/includes/wc-deprecated-functions.php b/plugins/woocommerce/includes/wc-deprecated-functions.php new file mode 100644 index 00000000000..71c00b38513 --- /dev/null +++ b/plugins/woocommerce/includes/wc-deprecated-functions.php @@ -0,0 +1,1125 @@ +is_rest_api_request() ) { + do_action( 'deprecated_function_run', $function, $replacement, $version ); + $log_string = "The {$function} function is deprecated since version {$version}."; + $log_string .= $replacement ? " Replace with {$replacement}." : ''; + error_log( $log_string ); + } else { + _deprecated_function( $function, $version, $replacement ); + } + // @codingStandardsIgnoreEnd +} + +/** + * Wrapper for deprecated hook so we can apply some extra logic. + * + * @since 3.3.0 + * @param string $hook The hook that was used. + * @param string $version The version of WordPress that deprecated the hook. + * @param string $replacement The hook that should have been used. + * @param string $message A message regarding the change. + */ +function wc_deprecated_hook( $hook, $version, $replacement = null, $message = null ) { + // @codingStandardsIgnoreStart + if ( wp_doing_ajax() || WC()->is_rest_api_request() ) { + do_action( 'deprecated_hook_run', $hook, $replacement, $version, $message ); + + $message = empty( $message ) ? '' : ' ' . $message; + $log_string = "{$hook} is deprecated since version {$version}"; + $log_string .= $replacement ? "! Use {$replacement} instead." : ' with no alternative available.'; + + error_log( $log_string . $message ); + } else { + _deprecated_hook( $hook, $version, $replacement, $message ); + } + // @codingStandardsIgnoreEnd +} + +/** + * When catching an exception, this allows us to log it if unexpected. + * + * @since 3.3.0 + * @param Exception $exception_object The exception object. + * @param string $function The function which threw exception. + * @param array $args The args passed to the function. + */ +function wc_caught_exception( $exception_object, $function = '', $args = array() ) { + // @codingStandardsIgnoreStart + $message = $exception_object->getMessage(); + $message .= '. Args: ' . print_r( $args, true ) . '.'; + + do_action( 'woocommerce_caught_exception', $exception_object, $function, $args ); + error_log( "Exception caught in {$function}. {$message}." ); + // @codingStandardsIgnoreEnd +} + +/** + * Wrapper for _doing_it_wrong(). + * + * @since 3.0.0 + * @param string $function Function used. + * @param string $message Message to log. + * @param string $version Version the message was added in. + */ +function wc_doing_it_wrong( $function, $message, $version ) { + // @codingStandardsIgnoreStart + $message .= ' Backtrace: ' . wp_debug_backtrace_summary(); + + if ( wp_doing_ajax() || WC()->is_rest_api_request() ) { + do_action( 'doing_it_wrong_run', $function, $message, $version ); + error_log( "{$function} was called incorrectly. {$message}. This message was added in version {$version}." ); + } else { + _doing_it_wrong( $function, $message, $version ); + } + // @codingStandardsIgnoreEnd +} + +/** + * Wrapper for deprecated arguments so we can apply some extra logic. + * + * @since 3.0.0 + * @param string $argument + * @param string $version + * @param string $replacement + */ +function wc_deprecated_argument( $argument, $version, $message = null ) { + if ( wp_doing_ajax() || WC()->is_rest_api_request() ) { + do_action( 'deprecated_argument_run', $argument, $message, $version ); + error_log( "The {$argument} argument is deprecated since version {$version}. {$message}" ); + } else { + _deprecated_argument( $argument, $version, $message ); + } +} + +/** + * @deprecated 2.1 + */ +function woocommerce_show_messages() { + wc_deprecated_function( 'woocommerce_show_messages', '2.1', 'wc_print_notices' ); + wc_print_notices(); +} + +/** + * @deprecated 2.1 + */ +function woocommerce_weekend_area_js() { + wc_deprecated_function( 'woocommerce_weekend_area_js', '2.1' ); +} + +/** + * @deprecated 2.1 + */ +function woocommerce_tooltip_js() { + wc_deprecated_function( 'woocommerce_tooltip_js', '2.1' ); +} + +/** + * @deprecated 2.1 + */ +function woocommerce_datepicker_js() { + wc_deprecated_function( 'woocommerce_datepicker_js', '2.1' ); +} + +/** + * @deprecated 2.1 + */ +function woocommerce_admin_scripts() { + wc_deprecated_function( 'woocommerce_admin_scripts', '2.1' ); +} + +/** + * @deprecated 2.1 + */ +function woocommerce_create_page( $slug, $option = '', $page_title = '', $page_content = '', $post_parent = 0 ) { + wc_deprecated_function( 'woocommerce_create_page', '2.1', 'wc_create_page' ); + return wc_create_page( $slug, $option, $page_title, $page_content, $post_parent ); +} + +/** + * @deprecated 2.1 + */ +function woocommerce_readfile_chunked( $file, $retbytes = true ) { + wc_deprecated_function( 'woocommerce_readfile_chunked', '2.1', 'WC_Download_Handler::readfile_chunked()' ); + return WC_Download_Handler::readfile_chunked( $file ); +} + +/** + * Formal total costs - format to the number of decimal places for the base currency. + * + * @access public + * @param mixed $number + * @deprecated 2.1 + * @return string + */ +function woocommerce_format_total( $number ) { + wc_deprecated_function( __FUNCTION__, '2.1', 'wc_format_decimal()' ); + return wc_format_decimal( $number, wc_get_price_decimals(), false ); +} + +/** + * Get product name with extra details such as SKU price and attributes. Used within admin. + * + * @access public + * @param WC_Product $product + * @deprecated 2.1 + * @return string + */ +function woocommerce_get_formatted_product_name( $product ) { + wc_deprecated_function( __FUNCTION__, '2.1', 'WC_Product::get_formatted_name()' ); + return $product->get_formatted_name(); +} + +/** + * Handle IPN requests for the legacy paypal gateway by calling gateways manually if needed. + * + * @access public + */ +function woocommerce_legacy_paypal_ipn() { + if ( ! empty( $_GET['paypalListener'] ) && 'paypal_standard_IPN' === $_GET['paypalListener'] ) { + WC()->payment_gateways(); + do_action( 'woocommerce_api_wc_gateway_paypal' ); + } +} +add_action( 'init', 'woocommerce_legacy_paypal_ipn' ); + +/** + * @deprecated 3.0 + */ +function get_product( $the_product = false, $args = array() ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_product' ); + return wc_get_product( $the_product, $args ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_protected_product_add_to_cart( $passed, $product_id ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_protected_product_add_to_cart' ); + return wc_protected_product_add_to_cart( $passed, $product_id ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_empty_cart() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_empty_cart' ); + wc_empty_cart(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_load_persistent_cart( $user_login, $user = 0 ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_load_persistent_cart' ); + return wc_load_persistent_cart( $user_login, $user ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_add_to_cart_message( $product_id ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_add_to_cart_message' ); + wc_add_to_cart_message( $product_id ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_clear_cart_after_payment() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_clear_cart_after_payment' ); + wc_clear_cart_after_payment(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_cart_totals_subtotal_html() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_subtotal_html' ); + wc_cart_totals_subtotal_html(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_cart_totals_shipping_html() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_shipping_html' ); + wc_cart_totals_shipping_html(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_cart_totals_coupon_html( $coupon ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_coupon_html' ); + wc_cart_totals_coupon_html( $coupon ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_cart_totals_order_total_html() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_order_total_html' ); + wc_cart_totals_order_total_html(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_cart_totals_fee_html( $fee ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_fee_html' ); + wc_cart_totals_fee_html( $fee ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_cart_totals_shipping_method_label( $method ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_shipping_method_label' ); + return wc_cart_totals_shipping_method_label( $method ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_template_part( $slug, $name = '' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_template_part' ); + wc_get_template_part( $slug, $name ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_template( $template_name, $args = array(), $template_path = '', $default_path = '' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_template' ); + wc_get_template( $template_name, $args, $template_path, $default_path ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_locate_template( $template_name, $template_path = '', $default_path = '' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_locate_template' ); + return wc_locate_template( $template_name, $template_path, $default_path ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_mail( $to, $subject, $message, $headers = "Content-Type: text/html\r\n", $attachments = "" ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_mail' ); + wc_mail( $to, $subject, $message, $headers, $attachments ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_disable_admin_bar( $show_admin_bar ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_disable_admin_bar' ); + return wc_disable_admin_bar( $show_admin_bar ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_create_new_customer( $email, $username = '', $password = '' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_create_new_customer' ); + return wc_create_new_customer( $email, $username, $password ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_set_customer_auth_cookie( $customer_id ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_set_customer_auth_cookie' ); + wc_set_customer_auth_cookie( $customer_id ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_update_new_customer_past_orders( $customer_id ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_update_new_customer_past_orders' ); + return wc_update_new_customer_past_orders( $customer_id ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_paying_customer( $order_id ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_paying_customer' ); + wc_paying_customer( $order_id ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_customer_bought_product( $customer_email, $user_id, $product_id ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_customer_bought_product' ); + return wc_customer_bought_product( $customer_email, $user_id, $product_id ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_customer_has_capability( $allcaps, $caps, $args ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_customer_has_capability' ); + return wc_customer_has_capability( $allcaps, $caps, $args ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_sanitize_taxonomy_name( $taxonomy ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_sanitize_taxonomy_name' ); + return wc_sanitize_taxonomy_name( $taxonomy ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_filename_from_url( $file_url ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_filename_from_url' ); + return wc_get_filename_from_url( $file_url ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_dimension( $dim, $to_unit ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_dimension' ); + return wc_get_dimension( $dim, $to_unit ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_weight( $weight, $to_unit ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_weight' ); + return wc_get_weight( $weight, $to_unit ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_trim_zeros( $price ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_trim_zeros' ); + return wc_trim_zeros( $price ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_round_tax_total( $tax ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_round_tax_total' ); + return wc_round_tax_total( $tax ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_format_decimal( $number, $dp = false, $trim_zeros = false ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_format_decimal' ); + return wc_format_decimal( $number, $dp, $trim_zeros ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_clean( $var ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_clean' ); + return wc_clean( $var ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_array_overlay( $a1, $a2 ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_array_overlay' ); + return wc_array_overlay( $a1, $a2 ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_price( $price, $args = array() ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_price' ); + return wc_price( $price, $args ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_let_to_num( $size ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_let_to_num' ); + return wc_let_to_num( $size ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_date_format() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_date_format' ); + return wc_date_format(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_time_format() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_time_format' ); + return wc_time_format(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_timezone_string() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_timezone_string' ); + return wc_timezone_string(); +} + +if ( ! function_exists( 'woocommerce_rgb_from_hex' ) ) { + /** + * @deprecated 3.0 + */ + function woocommerce_rgb_from_hex( $color ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_rgb_from_hex' ); + return wc_rgb_from_hex( $color ); + } +} + +if ( ! function_exists( 'woocommerce_hex_darker' ) ) { + /** + * @deprecated 3.0 + */ + function woocommerce_hex_darker( $color, $factor = 30 ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_hex_darker' ); + return wc_hex_darker( $color, $factor ); + } +} + +if ( ! function_exists( 'woocommerce_hex_lighter' ) ) { + /** + * @deprecated 3.0 + */ + function woocommerce_hex_lighter( $color, $factor = 30 ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_hex_lighter' ); + return wc_hex_lighter( $color, $factor ); + } +} + +if ( ! function_exists( 'woocommerce_light_or_dark' ) ) { + /** + * @deprecated 3.0 + */ + function woocommerce_light_or_dark( $color, $dark = '#000000', $light = '#FFFFFF' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_light_or_dark' ); + return wc_light_or_dark( $color, $dark, $light ); + } +} + +if ( ! function_exists( 'woocommerce_format_hex' ) ) { + /** + * @deprecated 3.0 + */ + function woocommerce_format_hex( $hex ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_format_hex' ); + return wc_format_hex( $hex ); + } +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_order_id_by_order_key( $order_key ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_order_id_by_order_key' ); + return wc_get_order_id_by_order_key( $order_key ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_downloadable_file_permission( $download_id, $product_id, $order ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_downloadable_file_permission' ); + return wc_downloadable_file_permission( $download_id, $product_id, $order ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_downloadable_product_permissions( $order_id ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_downloadable_product_permissions' ); + wc_downloadable_product_permissions( $order_id ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_add_order_item( $order_id, $item ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_add_order_item' ); + return wc_add_order_item( $order_id, $item ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_delete_order_item( $item_id ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_delete_order_item' ); + return wc_delete_order_item( $item_id ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_update_order_item_meta( $item_id, $meta_key, $meta_value, $prev_value = '' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_update_order_item_meta' ); + return wc_update_order_item_meta( $item_id, $meta_key, $meta_value, $prev_value ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_add_order_item_meta( $item_id, $meta_key, $meta_value, $unique = false ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_add_order_item_meta' ); + return wc_add_order_item_meta( $item_id, $meta_key, $meta_value, $unique ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_delete_order_item_meta( $item_id, $meta_key, $meta_value = '', $delete_all = false ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_delete_order_item_meta' ); + return wc_delete_order_item_meta( $item_id, $meta_key, $meta_value, $delete_all ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_order_item_meta( $item_id, $key, $single = true ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_order_item_meta' ); + return wc_get_order_item_meta( $item_id, $key, $single ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_cancel_unpaid_orders() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cancel_unpaid_orders' ); + wc_cancel_unpaid_orders(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_processing_order_count() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_processing_order_count' ); + return wc_processing_order_count(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_page_id( $page ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_page_id' ); + return wc_get_page_id( $page ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_endpoint_url( $endpoint, $value = '', $permalink = '' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_endpoint_url' ); + return wc_get_endpoint_url( $endpoint, $value, $permalink ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_lostpassword_url( $url ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_lostpassword_url' ); + return wc_lostpassword_url( $url ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_customer_edit_account_url() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_customer_edit_account_url' ); + return wc_customer_edit_account_url(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_nav_menu_items( $items, $args ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_nav_menu_items' ); + return wc_nav_menu_items( $items ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_nav_menu_item_classes( $menu_items, $args ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_nav_menu_item_classes' ); + return wc_nav_menu_item_classes( $menu_items ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_list_pages( $pages ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_list_pages' ); + return wc_list_pages( $pages ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_product_dropdown_categories( $args = array(), $deprecated_hierarchical = 1, $deprecated_show_uncategorized = 1, $deprecated_orderby = '' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_product_dropdown_categories' ); + return wc_product_dropdown_categories( $args, $deprecated_hierarchical, $deprecated_show_uncategorized, $deprecated_orderby ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_walk_category_dropdown_tree( $a1 = '', $a2 = '', $a3 = '' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_walk_category_dropdown_tree' ); + return wc_walk_category_dropdown_tree( $a1, $a2, $a3 ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_taxonomy_metadata_wpdbfix() { + wc_deprecated_function( __FUNCTION__, '3.0' ); +} + +/** + * @deprecated 3.0 + */ +function wc_taxonomy_metadata_wpdbfix() { + wc_deprecated_function( __FUNCTION__, '3.0' ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_order_terms( $the_term, $next_id, $taxonomy, $index = 0, $terms = null ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_reorder_terms' ); + return wc_reorder_terms( $the_term, $next_id, $taxonomy, $index, $terms ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_set_term_order( $term_id, $index, $taxonomy, $recursive = false ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_set_term_order' ); + return wc_set_term_order( $term_id, $index, $taxonomy, $recursive ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_terms_clauses( $clauses, $taxonomies, $args ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_terms_clauses' ); + return wc_terms_clauses( $clauses, $taxonomies, $args ); +} + +/** + * @deprecated 3.0 + */ +function _woocommerce_term_recount( $terms, $taxonomy, $callback, $terms_are_term_taxonomy_ids ) { + wc_deprecated_function( __FUNCTION__, '3.0', '_wc_term_recount' ); + return _wc_term_recount( $terms, $taxonomy, $callback, $terms_are_term_taxonomy_ids ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_recount_after_stock_change( $product_id ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_recount_after_stock_change' ); + return wc_recount_after_stock_change( $product_id ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_change_term_counts( $terms, $taxonomies, $args ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_change_term_counts' ); + return wc_change_term_counts( $terms, $taxonomies ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_product_ids_on_sale() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_product_ids_on_sale' ); + return wc_get_product_ids_on_sale(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_featured_product_ids() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_featured_product_ids' ); + return wc_get_featured_product_ids(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_product_terms( $object_id, $taxonomy, $fields = 'all' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_product_terms' ); + return wc_get_product_terms( $object_id, $taxonomy, array( 'fields' => $fields ) ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_product_post_type_link( $permalink, $post ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_product_post_type_link' ); + return wc_product_post_type_link( $permalink, $post ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_placeholder_img_src() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_placeholder_img_src' ); + return wc_placeholder_img_src(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_placeholder_img( $size = 'woocommerce_thumbnail' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_placeholder_img' ); + return wc_placeholder_img( $size ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_formatted_variation( $variation = '', $flat = false ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_formatted_variation' ); + return wc_get_formatted_variation( $variation, $flat ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_scheduled_sales() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_scheduled_sales' ); + return wc_scheduled_sales(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_attachment_image_attributes( $attr ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_attachment_image_attributes' ); + return wc_get_attachment_image_attributes( $attr ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_prepare_attachment_for_js( $response ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_prepare_attachment_for_js' ); + return wc_prepare_attachment_for_js( $response ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_track_product_view() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_track_product_view' ); + return wc_track_product_view(); +} + +/** + * @deprecated 2.3 has no replacement + */ +function woocommerce_compile_less_styles() { + wc_deprecated_function( 'woocommerce_compile_less_styles', '2.3' ); +} + +/** + * woocommerce_calc_shipping was an option used to determine if shipping was enabled prior to version 2.6.0. This has since been replaced with wc_shipping_enabled() function and + * the woocommerce_ship_to_countries setting. + * @deprecated 2.6.0 + * @return string + */ +function woocommerce_calc_shipping_backwards_compatibility( $value ) { + if ( Constants::is_defined( 'WC_UPDATING' ) ) { + return $value; + } + return 'disabled' === get_option( 'woocommerce_ship_to_countries' ) ? 'no' : 'yes'; +} +add_filter( 'pre_option_woocommerce_calc_shipping', 'woocommerce_calc_shipping_backwards_compatibility' ); + +/** + * @deprecated 3.0.0 + * @see WC_Structured_Data class + * + * @return string + */ +function woocommerce_get_product_schema() { + wc_deprecated_function( 'woocommerce_get_product_schema', '3.0' ); + + global $product; + + $schema = "Product"; + + // Downloadable product schema handling + if ( $product->is_downloadable() ) { + switch ( $product->download_type ) { + case 'application' : + $schema = "SoftwareApplication"; + break; + case 'music' : + $schema = "MusicAlbum"; + break; + default : + $schema = "Product"; + break; + } + } + + return 'http://schema.org/' . $schema; +} + +/** + * Save product price. + * + * This is a private function (internal use ONLY) used until a data manipulation api is built. + * + * @deprecated 3.0.0 + * @param int $product_id + * @param float $regular_price + * @param float $sale_price + * @param string $date_from + * @param string $date_to + */ +function _wc_save_product_price( $product_id, $regular_price, $sale_price = '', $date_from = '', $date_to = '' ) { + wc_doing_it_wrong( '_wc_save_product_price()', 'This function is not for developer use and is deprecated.', '3.0' ); + + $product_id = absint( $product_id ); + $regular_price = wc_format_decimal( $regular_price ); + $sale_price = '' === $sale_price ? '' : wc_format_decimal( $sale_price ); + $date_from = wc_clean( $date_from ); + $date_to = wc_clean( $date_to ); + + update_post_meta( $product_id, '_regular_price', $regular_price ); + update_post_meta( $product_id, '_sale_price', $sale_price ); + + // Save Dates + update_post_meta( $product_id, '_sale_price_dates_from', $date_from ? strtotime( $date_from ) : '' ); + update_post_meta( $product_id, '_sale_price_dates_to', $date_to ? strtotime( $date_to ) : '' ); + + if ( $date_to && ! $date_from ) { + $date_from = strtotime( 'NOW', current_time( 'timestamp' ) ); + update_post_meta( $product_id, '_sale_price_dates_from', $date_from ); + } + + // Update price if on sale + if ( '' !== $sale_price && '' === $date_to && '' === $date_from ) { + update_post_meta( $product_id, '_price', $sale_price ); + } else { + update_post_meta( $product_id, '_price', $regular_price ); + } + + if ( '' !== $sale_price && $date_from && strtotime( $date_from ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { + update_post_meta( $product_id, '_price', $sale_price ); + } + + if ( $date_to && strtotime( $date_to ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { + update_post_meta( $product_id, '_price', $regular_price ); + update_post_meta( $product_id, '_sale_price_dates_from', '' ); + update_post_meta( $product_id, '_sale_price_dates_to', '' ); + } +} + +/** + * Return customer avatar URL. + * + * @deprecated 3.1.0 + * @since 2.6.0 + * @param string $email the customer's email. + * @return string the URL to the customer's avatar. + */ +function wc_get_customer_avatar_url( $email ) { + // Deprecated in favor of WordPress get_avatar_url() function. + wc_deprecated_function( 'wc_get_customer_avatar_url()', '3.1', 'get_avatar_url()' ); + + return get_avatar_url( $email ); +} + +/** + * WooCommerce Core Supported Themes. + * + * @deprecated 3.3.0 + * @since 2.2 + * @return string[] + */ +function wc_get_core_supported_themes() { + wc_deprecated_function( 'wc_get_core_supported_themes()', '3.3' ); + return array( 'twentyseventeen', 'twentysixteen', 'twentyfifteen', 'twentyfourteen', 'twentythirteen', 'twentyeleven', 'twentytwelve', 'twentyten' ); +} + +/** + * Get min/max price meta query args. + * + * @deprecated 3.6.0 + * @since 3.0.0 + * @param array $args Min price and max price arguments. + * @return array + */ +function wc_get_min_max_price_meta_query( $args ) { + wc_deprecated_function( 'wc_get_min_max_price_meta_query()', '3.6' ); + + $current_min_price = isset( $args['min_price'] ) ? floatval( $args['min_price'] ) : 0; + $current_max_price = isset( $args['max_price'] ) ? floatval( $args['max_price'] ) : PHP_INT_MAX; + + return apply_filters( + 'woocommerce_get_min_max_price_meta_query', + array( + 'key' => '_price', + 'value' => array( $current_min_price, $current_max_price ), + 'compare' => 'BETWEEN', + 'type' => 'DECIMAL(10,' . wc_get_price_decimals() . ')', + ), + $args + ); +} + +/** + * When a term is split, ensure meta data maintained. + * + * @deprecated 3.6.0 + * @param int $old_term_id Old term ID. + * @param int $new_term_id New term ID. + * @param string $term_taxonomy_id Term taxonomy ID. + * @param string $taxonomy Taxonomy. + */ +function wc_taxonomy_metadata_update_content_for_split_terms( $old_term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) { + wc_deprecated_function( 'wc_taxonomy_metadata_update_content_for_split_terms', '3.6' ); +} + +/** + * WooCommerce Term Meta API. + * + * WC tables for storing term meta are deprecated from WordPress 4.4 since 4.4 has its own table. + * This function serves as a wrapper, using the new table if present, or falling back to the WC table. + * + * @deprecated 3.6.0 + * @param int $term_id Term ID. + * @param string $meta_key Meta key. + * @param mixed $meta_value Meta value. + * @param string $prev_value Previous value. (default: ''). + * @return bool + */ +function update_woocommerce_term_meta( $term_id, $meta_key, $meta_value, $prev_value = '' ) { + wc_deprecated_function( 'update_woocommerce_term_meta', '3.6', 'update_term_meta' ); + return function_exists( 'update_term_meta' ) ? update_term_meta( $term_id, $meta_key, $meta_value, $prev_value ) : update_metadata( 'woocommerce_term', $term_id, $meta_key, $meta_value, $prev_value ); +} + +/** + * WooCommerce Term Meta API. + * + * WC tables for storing term meta are deprecated from WordPress 4.4 since 4.4 has its own table. + * This function serves as a wrapper, using the new table if present, or falling back to the WC table. + * + * @deprecated 3.6.0 + * @param int $term_id Term ID. + * @param string $meta_key Meta key. + * @param mixed $meta_value Meta value. + * @param bool $unique Make meta key unique. (default: false). + * @return bool + */ +function add_woocommerce_term_meta( $term_id, $meta_key, $meta_value, $unique = false ) { + wc_deprecated_function( 'add_woocommerce_term_meta', '3.6', 'add_term_meta' ); + return function_exists( 'add_term_meta' ) ? add_term_meta( $term_id, $meta_key, $meta_value, $unique ) : add_metadata( 'woocommerce_term', $term_id, $meta_key, $meta_value, $unique ); +} + +/** + * WooCommerce Term Meta API + * + * WC tables for storing term meta are deprecated from WordPress 4.4 since 4.4 has its own table. + * This function serves as a wrapper, using the new table if present, or falling back to the WC table. + * + * @deprecated 3.6.0 + * @param int $term_id Term ID. + * @param string $meta_key Meta key. + * @param string $meta_value Meta value (default: ''). + * @param bool $deprecated Deprecated param (default: false). + * @return bool + */ +function delete_woocommerce_term_meta( $term_id, $meta_key, $meta_value = '', $deprecated = false ) { + wc_deprecated_function( 'delete_woocommerce_term_meta', '3.6', 'delete_term_meta' ); + return function_exists( 'delete_term_meta' ) ? delete_term_meta( $term_id, $meta_key, $meta_value ) : delete_metadata( 'woocommerce_term', $term_id, $meta_key, $meta_value ); +} + +/** + * WooCommerce Term Meta API + * + * WC tables for storing term meta are deprecated from WordPress 4.4 since 4.4 has its own table. + * This function serves as a wrapper, using the new table if present, or falling back to the WC table. + * + * @deprecated 3.6.0 + * @param int $term_id Term ID. + * @param string $key Meta key. + * @param bool $single Whether to return a single value. (default: true). + * @return mixed + */ +function get_woocommerce_term_meta( $term_id, $key, $single = true ) { + wc_deprecated_function( 'get_woocommerce_term_meta', '3.6', 'get_term_meta' ); + return function_exists( 'get_term_meta' ) ? get_term_meta( $term_id, $key, $single ) : get_metadata( 'woocommerce_term', $term_id, $key, $single ); +} diff --git a/plugins/woocommerce/includes/wc-formatting-functions.php b/plugins/woocommerce/includes/wc-formatting-functions.php new file mode 100644 index 00000000000..2bb63a68b5d --- /dev/null +++ b/plugins/woocommerce/includes/wc-formatting-functions.php @@ -0,0 +1,1516 @@ +strip_invalid_text_for_column( $wpdb->options, 'option_value', $value ); + + if ( is_wp_error( $value ) ) { + $value = ''; + } + + $value = esc_url_raw( trim( $value ) ); + $value = str_replace( 'http://', '', $value ); + return untrailingslashit( $value ); +} + +/** + * Gets the filename part of a download URL. + * + * @param string $file_url File URL. + * @return string + */ +function wc_get_filename_from_url( $file_url ) { + $parts = wp_parse_url( $file_url ); + if ( isset( $parts['path'] ) ) { + return basename( $parts['path'] ); + } +} + +/** + * Normalise dimensions, unify to cm then convert to wanted unit value. + * + * Usage: + * wc_get_dimension( 55, 'in' ); + * wc_get_dimension( 55, 'in', 'm' ); + * + * @param int|float $dimension Dimension. + * @param string $to_unit Unit to convert to. + * Options: 'in', 'm', 'cm', 'm'. + * @param string $from_unit Unit to convert from. + * Defaults to ''. + * Options: 'in', 'm', 'cm', 'm'. + * @return float + */ +function wc_get_dimension( $dimension, $to_unit, $from_unit = '' ) { + $to_unit = strtolower( $to_unit ); + + if ( empty( $from_unit ) ) { + $from_unit = strtolower( get_option( 'woocommerce_dimension_unit' ) ); + } + + // Unify all units to cm first. + if ( $from_unit !== $to_unit ) { + switch ( $from_unit ) { + case 'in': + $dimension *= 2.54; + break; + case 'm': + $dimension *= 100; + break; + case 'mm': + $dimension *= 0.1; + break; + case 'yd': + $dimension *= 91.44; + break; + } + + // Output desired unit. + switch ( $to_unit ) { + case 'in': + $dimension *= 0.3937; + break; + case 'm': + $dimension *= 0.01; + break; + case 'mm': + $dimension *= 10; + break; + case 'yd': + $dimension *= 0.010936133; + break; + } + } + + return ( $dimension < 0 ) ? 0 : $dimension; +} + +/** + * Normalise weights, unify to kg then convert to wanted unit value. + * + * Usage: + * wc_get_weight(55, 'kg'); + * wc_get_weight(55, 'kg', 'lbs'); + * + * @param int|float $weight Weight. + * @param string $to_unit Unit to convert to. + * Options: 'g', 'kg', 'lbs', 'oz'. + * @param string $from_unit Unit to convert from. + * Defaults to ''. + * Options: 'g', 'kg', 'lbs', 'oz'. + * @return float + */ +function wc_get_weight( $weight, $to_unit, $from_unit = '' ) { + $weight = (float) $weight; + $to_unit = strtolower( $to_unit ); + + if ( empty( $from_unit ) ) { + $from_unit = strtolower( get_option( 'woocommerce_weight_unit' ) ); + } + + // Unify all units to kg first. + if ( $from_unit !== $to_unit ) { + switch ( $from_unit ) { + case 'g': + $weight *= 0.001; + break; + case 'lbs': + $weight *= 0.453592; + break; + case 'oz': + $weight *= 0.0283495; + break; + } + + // Output desired unit. + switch ( $to_unit ) { + case 'g': + $weight *= 1000; + break; + case 'lbs': + $weight *= 2.20462; + break; + case 'oz': + $weight *= 35.274; + break; + } + } + + return ( $weight < 0 ) ? 0 : $weight; +} + +/** + * Trim trailing zeros off prices. + * + * @param string|float|int $price Price. + * @return string + */ +function wc_trim_zeros( $price ) { + return preg_replace( '/' . preg_quote( wc_get_price_decimal_separator(), '/' ) . '0++$/', '', $price ); +} + +/** + * Round a tax amount. + * + * @param double $value Amount to round. + * @param int $precision DP to round. Defaults to wc_get_price_decimals. + * @return float + */ +function wc_round_tax_total( $value, $precision = null ) { + $precision = is_null( $precision ) ? wc_get_price_decimals() : intval( $precision ); + + if ( version_compare( PHP_VERSION, '5.3.0', '>=' ) ) { + $rounded_tax = NumberUtil::round( $value, $precision, wc_get_tax_rounding_mode() ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.round_modeFound + } elseif ( 2 === wc_get_tax_rounding_mode() ) { + $rounded_tax = wc_legacy_round_half_down( $value, $precision ); + } else { + $rounded_tax = NumberUtil::round( $value, $precision ); + } + + return apply_filters( 'wc_round_tax_total', $rounded_tax, $value, $precision, WC_TAX_ROUNDING_MODE ); +} + +/** + * Round half down in PHP 5.2. + * + * @since 3.2.6 + * @param float $value Value to round. + * @param int $precision Precision to round down to. + * @return float + */ +function wc_legacy_round_half_down( $value, $precision ) { + $value = wc_float_to_string( $value ); + + if ( false !== strstr( $value, '.' ) ) { + $value = explode( '.', $value ); + + if ( strlen( $value[1] ) > $precision && substr( $value[1], -1 ) === '5' ) { + $value[1] = substr( $value[1], 0, -1 ) . '4'; + } + + $value = implode( '.', $value ); + } + + return NumberUtil::round( floatval( $value ), $precision ); +} + +/** + * Make a refund total negative. + * + * @param float $amount Refunded amount. + * + * @return float + */ +function wc_format_refund_total( $amount ) { + return $amount * -1; +} + +/** + * Format decimal numbers ready for DB storage. + * + * Sanitize, optionally remove decimals, and optionally round + trim off zeros. + * + * This function does not remove thousands - this should be done before passing a value to the function. + * + * @param float|string $number Expects either a float or a string with a decimal separator only (no thousands). + * @param mixed $dp number Number of decimal points to use, blank to use woocommerce_price_num_decimals, or false to avoid all rounding. + * @param bool $trim_zeros From end of string. + * @return string + */ +function wc_format_decimal( $number, $dp = false, $trim_zeros = false ) { + $locale = localeconv(); + $decimals = array( wc_get_price_decimal_separator(), $locale['decimal_point'], $locale['mon_decimal_point'] ); + + // Remove locale from string. + if ( ! is_float( $number ) ) { + $number = str_replace( $decimals, '.', $number ); + + // Convert multiple dots to just one. + $number = preg_replace( '/\.(?![^.]+$)|[^0-9.-]/', '', wc_clean( $number ) ); + } + + if ( false !== $dp ) { + $dp = intval( '' === $dp ? wc_get_price_decimals() : $dp ); + $number = number_format( floatval( $number ), $dp, '.', '' ); + } elseif ( is_float( $number ) ) { + // DP is false - don't use number format, just return a string using whatever is given. Remove scientific notation using sprintf. + $number = str_replace( $decimals, '.', sprintf( '%.' . wc_get_rounding_precision() . 'f', $number ) ); + // We already had a float, so trailing zeros are not needed. + $trim_zeros = true; + } + + if ( $trim_zeros && strstr( $number, '.' ) ) { + $number = rtrim( rtrim( $number, '0' ), '.' ); + } + + return $number; +} + +/** + * Convert a float to a string without locale formatting which PHP adds when changing floats to strings. + * + * @param float $float Float value to format. + * @return string + */ +function wc_float_to_string( $float ) { + if ( ! is_float( $float ) ) { + return $float; + } + + $locale = localeconv(); + $string = strval( $float ); + $string = str_replace( $locale['decimal_point'], '.', $string ); + + return $string; +} + +/** + * Format a price with WC Currency Locale settings. + * + * @param string $value Price to localize. + * @return string + */ +function wc_format_localized_price( $value ) { + return apply_filters( 'woocommerce_format_localized_price', str_replace( '.', wc_get_price_decimal_separator(), strval( $value ) ), $value ); +} + +/** + * Format a decimal with PHP Locale settings. + * + * @param string $value Decimal to localize. + * @return string + */ +function wc_format_localized_decimal( $value ) { + $locale = localeconv(); + return apply_filters( 'woocommerce_format_localized_decimal', str_replace( '.', $locale['decimal_point'], strval( $value ) ), $value ); +} + +/** + * Format a coupon code. + * + * @since 3.0.0 + * @param string $value Coupon code to format. + * @return string + */ +function wc_format_coupon_code( $value ) { + return apply_filters( 'woocommerce_coupon_code', $value ); +} + +/** + * Sanitize a coupon code. + * + * Uses sanitize_post_field since coupon codes are stored as + * post_titles - the sanitization and escaping must match. + * + * @since 3.6.0 + * @param string $value Coupon code to format. + * @return string + */ +function wc_sanitize_coupon_code( $value ) { + return wp_filter_kses( sanitize_post_field( 'post_title', $value, 0, 'db' ) ); +} + +/** + * Clean variables using sanitize_text_field. Arrays are cleaned recursively. + * Non-scalar values are ignored. + * + * @param string|array $var Data to sanitize. + * @return string|array + */ +function wc_clean( $var ) { + if ( is_array( $var ) ) { + return array_map( 'wc_clean', $var ); + } else { + return is_scalar( $var ) ? sanitize_text_field( $var ) : $var; + } +} + +/** + * Function wp_check_invalid_utf8 with recursive array support. + * + * @param string|array $var Data to sanitize. + * @return string|array + */ +function wc_check_invalid_utf8( $var ) { + if ( is_array( $var ) ) { + return array_map( 'wc_check_invalid_utf8', $var ); + } else { + return wp_check_invalid_utf8( $var ); + } +} + +/** + * Run wc_clean over posted textarea but maintain line breaks. + * + * @since 3.0.0 + * @param string $var Data to sanitize. + * @return string + */ +function wc_sanitize_textarea( $var ) { + return implode( "\n", array_map( 'wc_clean', explode( "\n", $var ) ) ); +} + +/** + * Sanitize a string destined to be a tooltip. + * + * @since 2.3.10 Tooltips are encoded with htmlspecialchars to prevent XSS. Should not be used in conjunction with esc_attr() + * @param string $var Data to sanitize. + * @return string + */ +function wc_sanitize_tooltip( $var ) { + return htmlspecialchars( + wp_kses( + html_entity_decode( $var ), + array( + 'br' => array(), + 'em' => array(), + 'strong' => array(), + 'small' => array(), + 'span' => array(), + 'ul' => array(), + 'li' => array(), + 'ol' => array(), + 'p' => array(), + ) + ) + ); +} + +/** + * Merge two arrays. + * + * @param array $a1 First array to merge. + * @param array $a2 Second array to merge. + * @return array + */ +function wc_array_overlay( $a1, $a2 ) { + foreach ( $a1 as $k => $v ) { + if ( ! array_key_exists( $k, $a2 ) ) { + continue; + } + if ( is_array( $v ) && is_array( $a2[ $k ] ) ) { + $a1[ $k ] = wc_array_overlay( $v, $a2[ $k ] ); + } else { + $a1[ $k ] = $a2[ $k ]; + } + } + return $a1; +} + +/** + * Formats a stock amount by running it through a filter. + * + * @param int|float $amount Stock amount. + * @return int|float + */ +function wc_stock_amount( $amount ) { + return apply_filters( 'woocommerce_stock_amount', $amount ); +} + +/** + * Get the price format depending on the currency position. + * + * @return string + */ +function get_woocommerce_price_format() { + $currency_pos = get_option( 'woocommerce_currency_pos' ); + $format = '%1$s%2$s'; + + switch ( $currency_pos ) { + case 'left': + $format = '%1$s%2$s'; + break; + case 'right': + $format = '%2$s%1$s'; + break; + case 'left_space': + $format = '%1$s %2$s'; + break; + case 'right_space': + $format = '%2$s %1$s'; + break; + } + + return apply_filters( 'woocommerce_price_format', $format, $currency_pos ); +} + +/** + * Return the thousand separator for prices. + * + * @since 2.3 + * @return string + */ +function wc_get_price_thousand_separator() { + return stripslashes( apply_filters( 'wc_get_price_thousand_separator', get_option( 'woocommerce_price_thousand_sep' ) ) ); +} + +/** + * Return the decimal separator for prices. + * + * @since 2.3 + * @return string + */ +function wc_get_price_decimal_separator() { + $separator = apply_filters( 'wc_get_price_decimal_separator', get_option( 'woocommerce_price_decimal_sep' ) ); + return $separator ? stripslashes( $separator ) : '.'; +} + +/** + * Return the number of decimals after the decimal point. + * + * @since 2.3 + * @return int + */ +function wc_get_price_decimals() { + return absint( apply_filters( 'wc_get_price_decimals', get_option( 'woocommerce_price_num_decimals', 2 ) ) ); +} + +/** + * Format the price with a currency symbol. + * + * @param float $price Raw price. + * @param array $args Arguments to format a price { + * Array of arguments. + * Defaults to empty array. + * + * @type bool $ex_tax_label Adds exclude tax label. + * Defaults to false. + * @type string $currency Currency code. + * Defaults to empty string (Use the result from get_woocommerce_currency()). + * @type string $decimal_separator Decimal separator. + * Defaults the result of wc_get_price_decimal_separator(). + * @type string $thousand_separator Thousand separator. + * Defaults the result of wc_get_price_thousand_separator(). + * @type string $decimals Number of decimals. + * Defaults the result of wc_get_price_decimals(). + * @type string $price_format Price format depending on the currency position. + * Defaults the result of get_woocommerce_price_format(). + * } + * @return string + */ +function wc_price( $price, $args = array() ) { + $args = apply_filters( + 'wc_price_args', + wp_parse_args( + $args, + array( + 'ex_tax_label' => false, + 'currency' => '', + 'decimal_separator' => wc_get_price_decimal_separator(), + 'thousand_separator' => wc_get_price_thousand_separator(), + 'decimals' => wc_get_price_decimals(), + 'price_format' => get_woocommerce_price_format(), + ) + ) + ); + + $original_price = $price; + + // Convert to float to avoid issues on PHP 8. + $price = (float) $price; + + $unformatted_price = $price; + $negative = $price < 0; + + /** + * Filter raw price. + * + * @param float $raw_price Raw price. + * @param float|string $original_price Original price as float, or empty string. Since 5.0.0. + */ + $price = apply_filters( 'raw_woocommerce_price', $negative ? $price * -1 : $price, $original_price ); + + /** + * Filter formatted price. + * + * @param float $formatted_price Formatted price. + * @param float $price Unformatted price. + * @param int $decimals Number of decimals. + * @param string $decimal_separator Decimal separator. + * @param string $thousand_separator Thousand separator. + * @param float|string $original_price Original price as float, or empty string. Since 5.0.0. + */ + $price = apply_filters( 'formatted_woocommerce_price', number_format( $price, $args['decimals'], $args['decimal_separator'], $args['thousand_separator'] ), $price, $args['decimals'], $args['decimal_separator'], $args['thousand_separator'], $original_price ); + + if ( apply_filters( 'woocommerce_price_trim_zeros', false ) && $args['decimals'] > 0 ) { + $price = wc_trim_zeros( $price ); + } + + $formatted_price = ( $negative ? '-' : '' ) . sprintf( $args['price_format'], '' . get_woocommerce_currency_symbol( $args['currency'] ) . '', $price ); + $return = '' . $formatted_price . ''; + + if ( $args['ex_tax_label'] && wc_tax_enabled() ) { + $return .= ' ' . WC()->countries->ex_tax_or_vat() . ''; + } + + /** + * Filters the string of price markup. + * + * @param string $return Price HTML markup. + * @param string $price Formatted price. + * @param array $args Pass on the args. + * @param float $unformatted_price Price as float to allow plugins custom formatting. Since 3.2.0. + * @param float|string $original_price Original price as float, or empty string. Since 5.0.0. + */ + return apply_filters( 'wc_price', $return, $price, $args, $unformatted_price, $original_price ); +} + +/** + * Notation to numbers. + * + * This function transforms the php.ini notation for numbers (like '2M') to an integer. + * + * @param string $size Size value. + * @return int + */ +function wc_let_to_num( $size ) { + $l = substr( $size, -1 ); + $ret = (int) substr( $size, 0, -1 ); + switch ( strtoupper( $l ) ) { + case 'P': + $ret *= 1024; + // No break. + case 'T': + $ret *= 1024; + // No break. + case 'G': + $ret *= 1024; + // No break. + case 'M': + $ret *= 1024; + // No break. + case 'K': + $ret *= 1024; + // No break. + } + return $ret; +} + +/** + * WooCommerce Date Format - Allows to change date format for everything WooCommerce. + * + * @return string + */ +function wc_date_format() { + $date_format = get_option( 'date_format' ); + if ( empty( $date_format ) ) { + // Return default date format if the option is empty. + $date_format = 'F j, Y'; + } + return apply_filters( 'woocommerce_date_format', $date_format ); +} + +/** + * WooCommerce Time Format - Allows to change time format for everything WooCommerce. + * + * @return string + */ +function wc_time_format() { + $time_format = get_option( 'time_format' ); + if ( empty( $time_format ) ) { + // Return default time format if the option is empty. + $time_format = 'g:i a'; + } + return apply_filters( 'woocommerce_time_format', $time_format ); +} + +/** + * Convert mysql datetime to PHP timestamp, forcing UTC. Wrapper for strtotime. + * + * Based on wcs_strtotime_dark_knight() from WC Subscriptions by Prospress. + * + * @since 3.0.0 + * @param string $time_string Time string. + * @param int|null $from_timestamp Timestamp to convert from. + * @return int + */ +function wc_string_to_timestamp( $time_string, $from_timestamp = null ) { + $original_timezone = date_default_timezone_get(); + + // @codingStandardsIgnoreStart + date_default_timezone_set( 'UTC' ); + + if ( null === $from_timestamp ) { + $next_timestamp = strtotime( $time_string ); + } else { + $next_timestamp = strtotime( $time_string, $from_timestamp ); + } + + date_default_timezone_set( $original_timezone ); + // @codingStandardsIgnoreEnd + + return $next_timestamp; +} + +/** + * Convert a date string to a WC_DateTime. + * + * @since 3.1.0 + * @param string $time_string Time string. + * @return WC_DateTime + */ +function wc_string_to_datetime( $time_string ) { + // Strings are defined in local WP timezone. Convert to UTC. + if ( 1 === preg_match( '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(Z|((-|\+)\d{2}:\d{2}))$/', $time_string, $date_bits ) ) { + $offset = ! empty( $date_bits[7] ) ? iso8601_timezone_to_offset( $date_bits[7] ) : wc_timezone_offset(); + $timestamp = gmmktime( $date_bits[4], $date_bits[5], $date_bits[6], $date_bits[2], $date_bits[3], $date_bits[1] ) - $offset; + } else { + $timestamp = wc_string_to_timestamp( get_gmt_from_date( gmdate( 'Y-m-d H:i:s', wc_string_to_timestamp( $time_string ) ) ) ); + } + $datetime = new WC_DateTime( "@{$timestamp}", new DateTimeZone( 'UTC' ) ); + + // Set local timezone or offset. + if ( get_option( 'timezone_string' ) ) { + $datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) ); + } else { + $datetime->set_utc_offset( wc_timezone_offset() ); + } + + return $datetime; +} + +/** + * WooCommerce Timezone - helper to retrieve the timezone string for a site until. + * a WP core method exists (see https://core.trac.wordpress.org/ticket/24730). + * + * Adapted from https://secure.php.net/manual/en/function.timezone-name-from-abbr.php#89155. + * + * @since 2.1 + * @return string PHP timezone string for the site + */ +function wc_timezone_string() { + // Added in WordPress 5.3 Ref https://developer.wordpress.org/reference/functions/wp_timezone_string/. + if ( function_exists( 'wp_timezone_string' ) ) { + return wp_timezone_string(); + } + + // If site timezone string exists, return it. + $timezone = get_option( 'timezone_string' ); + if ( $timezone ) { + return $timezone; + } + + // Get UTC offset, if it isn't set then return UTC. + $utc_offset = floatval( get_option( 'gmt_offset', 0 ) ); + if ( ! is_numeric( $utc_offset ) || 0.0 === $utc_offset ) { + return 'UTC'; + } + + // Adjust UTC offset from hours to seconds. + $utc_offset = (int) ( $utc_offset * 3600 ); + + // Attempt to guess the timezone string from the UTC offset. + $timezone = timezone_name_from_abbr( '', $utc_offset ); + if ( $timezone ) { + return $timezone; + } + + // Last try, guess timezone string manually. + foreach ( timezone_abbreviations_list() as $abbr ) { + foreach ( $abbr as $city ) { + // WordPress restrict the use of date(), since it's affected by timezone settings, but in this case is just what we need to guess the correct timezone. + if ( (bool) date( 'I' ) === (bool) $city['dst'] && $city['timezone_id'] && intval( $city['offset'] ) === $utc_offset ) { // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + return $city['timezone_id']; + } + } + } + + // Fallback to UTC. + return 'UTC'; +} + +/** + * Get timezone offset in seconds. + * + * @since 3.0.0 + * @return float + */ +function wc_timezone_offset() { + $timezone = get_option( 'timezone_string' ); + + if ( $timezone ) { + $timezone_object = new DateTimeZone( $timezone ); + return $timezone_object->getOffset( new DateTime( 'now' ) ); + } else { + return floatval( get_option( 'gmt_offset', 0 ) ) * HOUR_IN_SECONDS; + } +} + +/** + * Callback which can flatten post meta (gets the first value if it's an array). + * + * @since 3.0.0 + * @param array $value Value to flatten. + * @return mixed + */ +function wc_flatten_meta_callback( $value ) { + return is_array( $value ) ? current( $value ) : $value; +} + +if ( ! function_exists( 'wc_rgb_from_hex' ) ) { + + /** + * Convert RGB to HEX. + * + * @param mixed $color Color. + * + * @return array + */ + function wc_rgb_from_hex( $color ) { + $color = str_replace( '#', '', $color ); + // Convert shorthand colors to full format, e.g. "FFF" -> "FFFFFF". + $color = preg_replace( '~^(.)(.)(.)$~', '$1$1$2$2$3$3', $color ); + + $rgb = array(); + $rgb['R'] = hexdec( $color[0] . $color[1] ); + $rgb['G'] = hexdec( $color[2] . $color[3] ); + $rgb['B'] = hexdec( $color[4] . $color[5] ); + + return $rgb; + } +} + +if ( ! function_exists( 'wc_hex_darker' ) ) { + + /** + * Make HEX color darker. + * + * @param mixed $color Color. + * @param int $factor Darker factor. + * Defaults to 30. + * @return string + */ + function wc_hex_darker( $color, $factor = 30 ) { + $base = wc_rgb_from_hex( $color ); + $color = '#'; + + foreach ( $base as $k => $v ) { + $amount = $v / 100; + $amount = NumberUtil::round( $amount * $factor ); + $new_decimal = $v - $amount; + + $new_hex_component = dechex( $new_decimal ); + if ( strlen( $new_hex_component ) < 2 ) { + $new_hex_component = '0' . $new_hex_component; + } + $color .= $new_hex_component; + } + + return $color; + } +} + +if ( ! function_exists( 'wc_hex_lighter' ) ) { + + /** + * Make HEX color lighter. + * + * @param mixed $color Color. + * @param int $factor Lighter factor. + * Defaults to 30. + * @return string + */ + function wc_hex_lighter( $color, $factor = 30 ) { + $base = wc_rgb_from_hex( $color ); + $color = '#'; + + foreach ( $base as $k => $v ) { + $amount = 255 - $v; + $amount = $amount / 100; + $amount = NumberUtil::round( $amount * $factor ); + $new_decimal = $v + $amount; + + $new_hex_component = dechex( $new_decimal ); + if ( strlen( $new_hex_component ) < 2 ) { + $new_hex_component = '0' . $new_hex_component; + } + $color .= $new_hex_component; + } + + return $color; + } +} + +if ( ! function_exists( 'wc_hex_is_light' ) ) { + + /** + * Determine whether a hex color is light. + * + * @param mixed $color Color. + * @return bool True if a light color. + */ + function wc_hex_is_light( $color ) { + $hex = str_replace( '#', '', $color ); + + $c_r = hexdec( substr( $hex, 0, 2 ) ); + $c_g = hexdec( substr( $hex, 2, 2 ) ); + $c_b = hexdec( substr( $hex, 4, 2 ) ); + + $brightness = ( ( $c_r * 299 ) + ( $c_g * 587 ) + ( $c_b * 114 ) ) / 1000; + + return $brightness > 155; + } +} + +if ( ! function_exists( 'wc_light_or_dark' ) ) { + + /** + * Detect if we should use a light or dark color on a background color. + * + * @param mixed $color Color. + * @param string $dark Darkest reference. + * Defaults to '#000000'. + * @param string $light Lightest reference. + * Defaults to '#FFFFFF'. + * @return string + */ + function wc_light_or_dark( $color, $dark = '#000000', $light = '#FFFFFF' ) { + return wc_hex_is_light( $color ) ? $dark : $light; + } +} + +if ( ! function_exists( 'wc_format_hex' ) ) { + + /** + * Format string as hex. + * + * @param string $hex HEX color. + * @return string|null + */ + function wc_format_hex( $hex ) { + $hex = trim( str_replace( '#', '', $hex ) ); + + if ( strlen( $hex ) === 3 ) { + $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; + } + + return $hex ? '#' . $hex : null; + } +} + +/** + * Format the postcode according to the country and length of the postcode. + * + * @param string $postcode Unformatted postcode. + * @param string $country Base country. + * @return string + */ +function wc_format_postcode( $postcode, $country ) { + $postcode = wc_normalize_postcode( $postcode ); + + switch ( $country ) { + case 'CA': + case 'GB': + $postcode = substr_replace( $postcode, ' ', -3, 0 ); + break; + case 'IE': + $postcode = substr_replace( $postcode, ' ', 3, 0 ); + break; + case 'BR': + case 'PL': + $postcode = substr_replace( $postcode, '-', -3, 0 ); + break; + case 'JP': + $postcode = substr_replace( $postcode, '-', 3, 0 ); + break; + case 'PT': + $postcode = substr_replace( $postcode, '-', 4, 0 ); + break; + case 'PR': + case 'US': + $postcode = rtrim( substr_replace( $postcode, '-', 5, 0 ), '-' ); + break; + case 'NL': + $postcode = substr_replace( $postcode, ' ', 4, 0 ); + break; + } + + return apply_filters( 'woocommerce_format_postcode', trim( $postcode ), $country ); +} + +/** + * Normalize postcodes. + * + * Remove spaces and convert characters to uppercase. + * + * @since 2.6.0 + * @param string $postcode Postcode. + * @return string + */ +function wc_normalize_postcode( $postcode ) { + return preg_replace( '/[\s\-]/', '', trim( wc_strtoupper( $postcode ) ) ); +} + +/** + * Format phone numbers. + * + * @param string $phone Phone number. + * @return string + */ +function wc_format_phone_number( $phone ) { + if ( ! WC_Validation::is_phone( $phone ) ) { + return ''; + } + return preg_replace( '/[^0-9\+\-\(\)\s]/', '-', preg_replace( '/[\x00-\x1F\x7F-\xFF]/', '', $phone ) ); +} + +/** + * Sanitize phone number. + * Allows only numbers and "+" (plus sign). + * + * @since 3.6.0 + * @param string $phone Phone number. + * @return string + */ +function wc_sanitize_phone_number( $phone ) { + return preg_replace( '/[^\d+]/', '', $phone ); +} + +/** + * Wrapper for mb_strtoupper which see's if supported first. + * + * @since 3.1.0 + * @param string $string String to format. + * @return string + */ +function wc_strtoupper( $string ) { + return function_exists( 'mb_strtoupper' ) ? mb_strtoupper( $string ) : strtoupper( $string ); +} + +/** + * Make a string lowercase. + * Try to use mb_strtolower() when available. + * + * @since 2.3 + * @param string $string String to format. + * @return string + */ +function wc_strtolower( $string ) { + return function_exists( 'mb_strtolower' ) ? mb_strtolower( $string ) : strtolower( $string ); +} + +/** + * Trim a string and append a suffix. + * + * @param string $string String to trim. + * @param integer $chars Amount of characters. + * Defaults to 200. + * @param string $suffix Suffix. + * Defaults to '...'. + * @return string + */ +function wc_trim_string( $string, $chars = 200, $suffix = '...' ) { + if ( strlen( $string ) > $chars ) { + if ( function_exists( 'mb_substr' ) ) { + $string = mb_substr( $string, 0, ( $chars - mb_strlen( $suffix ) ) ) . $suffix; + } else { + $string = substr( $string, 0, ( $chars - strlen( $suffix ) ) ) . $suffix; + } + } + return $string; +} + +/** + * Format content to display shortcodes. + * + * @since 2.3.0 + * @param string $raw_string Raw string. + * @return string + */ +function wc_format_content( $raw_string ) { + return apply_filters( 'woocommerce_format_content', apply_filters( 'woocommerce_short_description', $raw_string ), $raw_string ); +} + +/** + * Format product short description. + * Adds support for Jetpack Markdown. + * + * @codeCoverageIgnore + * @since 2.4.0 + * @param string $content Product short description. + * @return string + */ +function wc_format_product_short_description( $content ) { + // Add support for Jetpack Markdown. + if ( class_exists( 'WPCom_Markdown' ) ) { + $markdown = WPCom_Markdown::get_instance(); + + return wpautop( + $markdown->transform( + $content, + array( + 'unslash' => false, + ) + ) + ); + } + + return $content; +} + +/** + * Formats curency symbols when saved in settings. + * + * @codeCoverageIgnore + * @param string $value Option value. + * @param array $option Option name. + * @param string $raw_value Raw value. + * @return string + */ +function wc_format_option_price_separators( $value, $option, $raw_value ) { + return wp_kses_post( $raw_value ); +} +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_decimal_sep', 'wc_format_option_price_separators', 10, 3 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_thousand_sep', 'wc_format_option_price_separators', 10, 3 ); + +/** + * Formats decimals when saved in settings. + * + * @codeCoverageIgnore + * @param string $value Option value. + * @param array $option Option name. + * @param string $raw_value Raw value. + * @return string + */ +function wc_format_option_price_num_decimals( $value, $option, $raw_value ) { + return is_null( $raw_value ) ? 2 : absint( $raw_value ); +} +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_num_decimals', 'wc_format_option_price_num_decimals', 10, 3 ); + +/** + * Formats hold stock option and sets cron event up. + * + * @codeCoverageIgnore + * @param string $value Option value. + * @param array $option Option name. + * @param string $raw_value Raw value. + * @return string + */ +function wc_format_option_hold_stock_minutes( $value, $option, $raw_value ) { + $value = ! empty( $raw_value ) ? absint( $raw_value ) : ''; // Allow > 0 or set to ''. + + wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' ); + + if ( '' !== $value ) { + $cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $value ) ); + wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' ); + } + + return $value; +} +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_hold_stock_minutes', 'wc_format_option_hold_stock_minutes', 10, 3 ); + +/** + * Sanitize terms from an attribute text based. + * + * @since 2.4.5 + * @param string $term Term value. + * @return string + */ +function wc_sanitize_term_text_based( $term ) { + return trim( wp_strip_all_tags( wp_unslash( $term ) ) ); +} + +if ( ! function_exists( 'wc_make_numeric_postcode' ) ) { + /** + * Make numeric postcode. + * + * Converts letters to numbers so we can do a simple range check on postcodes. + * E.g. PE30 becomes 16050300 (P = 16, E = 05, 3 = 03, 0 = 00) + * + * @since 2.6.0 + * @param string $postcode Regular postcode. + * @return string + */ + function wc_make_numeric_postcode( $postcode ) { + $postcode = str_replace( array( ' ', '-' ), '', $postcode ); + $postcode_length = strlen( $postcode ); + $letters_to_numbers = array_merge( array( 0 ), range( 'A', 'Z' ) ); + $letters_to_numbers = array_flip( $letters_to_numbers ); + $numeric_postcode = ''; + + for ( $i = 0; $i < $postcode_length; $i ++ ) { + if ( is_numeric( $postcode[ $i ] ) ) { + $numeric_postcode .= str_pad( $postcode[ $i ], 2, '0', STR_PAD_LEFT ); + } elseif ( isset( $letters_to_numbers[ $postcode[ $i ] ] ) ) { + $numeric_postcode .= str_pad( $letters_to_numbers[ $postcode[ $i ] ], 2, '0', STR_PAD_LEFT ); + } else { + $numeric_postcode .= '00'; + } + } + + return $numeric_postcode; + } +} + +/** + * Format the stock amount ready for display based on settings. + * + * @since 3.0.0 + * @param WC_Product $product Product object for which the stock you need to format. + * @return string + */ +function wc_format_stock_for_display( $product ) { + $display = __( 'In stock', 'woocommerce' ); + $stock_amount = $product->get_stock_quantity(); + + switch ( get_option( 'woocommerce_stock_format' ) ) { + case 'low_amount': + if ( $stock_amount <= wc_get_low_stock_amount( $product ) ) { + /* translators: %s: stock amount */ + $display = sprintf( __( 'Only %s left in stock', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_amount, $product ) ); + } + break; + case '': + /* translators: %s: stock amount */ + $display = sprintf( __( '%s in stock', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_amount, $product ) ); + break; + } + + if ( $product->backorders_allowed() && $product->backorders_require_notification() ) { + $display .= ' ' . __( '(can be backordered)', 'woocommerce' ); + } + + return $display; +} + +/** + * Format the stock quantity ready for display. + * + * @since 3.0.0 + * @param int $stock_quantity Stock quantity. + * @param WC_Product $product Product instance so that we can pass through the filters. + * @return string + */ +function wc_format_stock_quantity_for_display( $stock_quantity, $product ) { + return apply_filters( 'woocommerce_format_stock_quantity', $stock_quantity, $product ); +} + +/** + * Format a sale price for display. + * + * @since 3.0.0 + * @param string $regular_price Regular price. + * @param string $sale_price Sale price. + * @return string + */ +function wc_format_sale_price( $regular_price, $sale_price ) { + $price = ' ' . ( is_numeric( $sale_price ) ? wc_price( $sale_price ) : $sale_price ) . ''; + return apply_filters( 'woocommerce_format_sale_price', $price, $regular_price, $sale_price ); +} + +/** + * Format a price range for display. + * + * @param string $from Price from. + * @param string $to Price to. + * @return string + */ +function wc_format_price_range( $from, $to ) { + /* translators: 1: price from 2: price to */ + $price = sprintf( _x( '%1$s – %2$s', 'Price range: from-to', 'woocommerce' ), is_numeric( $from ) ? wc_price( $from ) : $from, is_numeric( $to ) ? wc_price( $to ) : $to ); + return apply_filters( 'woocommerce_format_price_range', $price, $from, $to ); +} + +/** + * Format a weight for display. + * + * @since 3.0.0 + * @param float $weight Weight. + * @return string + */ +function wc_format_weight( $weight ) { + $weight_string = wc_format_localized_decimal( $weight ); + + if ( ! empty( $weight_string ) ) { + $weight_string .= ' ' . get_option( 'woocommerce_weight_unit' ); + } else { + $weight_string = __( 'N/A', 'woocommerce' ); + } + + return apply_filters( 'woocommerce_format_weight', $weight_string, $weight ); +} + +/** + * Format dimensions for display. + * + * @since 3.0.0 + * @param array $dimensions Array of dimensions. + * @return string + */ +function wc_format_dimensions( $dimensions ) { + $dimension_string = implode( ' × ', array_filter( array_map( 'wc_format_localized_decimal', $dimensions ) ) ); + + if ( ! empty( $dimension_string ) ) { + $dimension_string .= ' ' . get_option( 'woocommerce_dimension_unit' ); + } else { + $dimension_string = __( 'N/A', 'woocommerce' ); + } + + return apply_filters( 'woocommerce_format_dimensions', $dimension_string, $dimensions ); +} + +/** + * Format a date for output. + * + * @since 3.0.0 + * @param WC_DateTime $date Instance of WC_DateTime. + * @param string $format Data format. + * Defaults to the wc_date_format function if not set. + * @return string + */ +function wc_format_datetime( $date, $format = '' ) { + if ( ! $format ) { + $format = wc_date_format(); + } + if ( ! is_a( $date, 'WC_DateTime' ) ) { + return ''; + } + return $date->date_i18n( $format ); +} + +/** + * Process oEmbeds. + * + * @since 3.1.0 + * @param string $content Content. + * @return string + */ +function wc_do_oembeds( $content ) { + global $wp_embed; + + $content = $wp_embed->autoembed( $content ); + + return $content; +} + +/** + * Get part of a string before :. + * + * Used for example in shipping methods ids where they take the format + * method_id:instance_id + * + * @since 3.2.0 + * @param string $string String to extract. + * @return string + */ +function wc_get_string_before_colon( $string ) { + return trim( current( explode( ':', (string) $string ) ) ); +} + +/** + * Array merge and sum function. + * + * Source: https://gist.github.com/Nickology/f700e319cbafab5eaedc + * + * @since 3.2.0 + * @return array + */ +function wc_array_merge_recursive_numeric() { + $arrays = func_get_args(); + + // If there's only one array, it's already merged. + if ( 1 === count( $arrays ) ) { + return $arrays[0]; + } + + // Remove any items in $arrays that are NOT arrays. + foreach ( $arrays as $key => $array ) { + if ( ! is_array( $array ) ) { + unset( $arrays[ $key ] ); + } + } + + // We start by setting the first array as our final array. + // We will merge all other arrays with this one. + $final = array_shift( $arrays ); + + foreach ( $arrays as $b ) { + foreach ( $final as $key => $value ) { + // If $key does not exist in $b, then it is unique and can be safely merged. + if ( ! isset( $b[ $key ] ) ) { + $final[ $key ] = $value; + } else { + // If $key is present in $b, then we need to merge and sum numeric values in both. + if ( is_numeric( $value ) && is_numeric( $b[ $key ] ) ) { + // If both values for these keys are numeric, we sum them. + $final[ $key ] = $value + $b[ $key ]; + } elseif ( is_array( $value ) && is_array( $b[ $key ] ) ) { + // If both values are arrays, we recursively call ourself. + $final[ $key ] = wc_array_merge_recursive_numeric( $value, $b[ $key ] ); + } else { + // If both keys exist but differ in type, then we cannot merge them. + // In this scenario, we will $b's value for $key is used. + $final[ $key ] = $b[ $key ]; + } + } + } + + // Finally, we need to merge any keys that exist only in $b. + foreach ( $b as $key => $value ) { + if ( ! isset( $final[ $key ] ) ) { + $final[ $key ] = $value; + } + } + } + + return $final; +} + +/** + * Implode and escape HTML attributes for output. + * + * @since 3.3.0 + * @param array $raw_attributes Attribute name value pairs. + * @return string + */ +function wc_implode_html_attributes( $raw_attributes ) { + $attributes = array(); + foreach ( $raw_attributes as $name => $value ) { + $attributes[] = esc_attr( $name ) . '="' . esc_attr( $value ) . '"'; + } + return implode( ' ', $attributes ); +} + +/** + * Escape JSON for use on HTML or attribute text nodes. + * + * @since 3.5.5 + * @param string $json JSON to escape. + * @param bool $html True if escaping for HTML text node, false for attributes. Determines how quotes are handled. + * @return string Escaped JSON. + */ +function wc_esc_json( $json, $html = false ) { + return _wp_specialchars( + $json, + $html ? ENT_NOQUOTES : ENT_QUOTES, // Escape quotes in attribute nodes only. + 'UTF-8', // json_encode() outputs UTF-8 (really just ASCII), not the blog's charset. + true // Double escape entities: `&` -> `&amp;`. + ); +} + +/** + * Parse a relative date option from the settings API into a standard format. + * + * @since 3.4.0 + * @param mixed $raw_value Value stored in DB. + * @return array Nicely formatted array with number and unit values. + */ +function wc_parse_relative_date_option( $raw_value ) { + $periods = array( + 'days' => __( 'Day(s)', 'woocommerce' ), + 'weeks' => __( 'Week(s)', 'woocommerce' ), + 'months' => __( 'Month(s)', 'woocommerce' ), + 'years' => __( 'Year(s)', 'woocommerce' ), + ); + + $value = wp_parse_args( + (array) $raw_value, + array( + 'number' => '', + 'unit' => 'days', + ) + ); + + $value['number'] = ! empty( $value['number'] ) ? absint( $value['number'] ) : ''; + + if ( ! in_array( $value['unit'], array_keys( $periods ), true ) ) { + $value['unit'] = 'days'; + } + + return $value; +} + +/** + * Format the endpoint slug, strip out anything not allowed in a url. + * + * @since 3.5.0 + * @param string $raw_value The raw value. + * @return string + */ +function wc_sanitize_endpoint_slug( $raw_value ) { + return sanitize_title( $raw_value ); +} +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_checkout_pay_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_checkout_order_received_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_add_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_delete_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_set_default_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_orders_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_view_order_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_downloads_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_edit_account_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_edit_address_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_payment_methods_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_lost_password_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_logout_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); diff --git a/includes/wc-notice-functions.php b/plugins/woocommerce/includes/wc-notice-functions.php similarity index 100% rename from includes/wc-notice-functions.php rename to plugins/woocommerce/includes/wc-notice-functions.php diff --git a/plugins/woocommerce/includes/wc-order-functions.php b/plugins/woocommerce/includes/wc-order-functions.php new file mode 100644 index 00000000000..00c62e5ef16 --- /dev/null +++ b/plugins/woocommerce/includes/wc-order-functions.php @@ -0,0 +1,1112 @@ + 'limit', + 'post_type' => 'type', + 'post_status' => 'status', + 'post_parent' => 'parent', + 'author' => 'customer', + 'email' => 'billing_email', + 'posts_per_page' => 'limit', + 'paged' => 'page', + ); + + foreach ( $map_legacy as $from => $to ) { + if ( isset( $args[ $from ] ) ) { + $args[ $to ] = $args[ $from ]; + } + } + + // Map legacy date args to modern date args. + $date_before = false; + $date_after = false; + + if ( ! empty( $args['date_before'] ) ) { + $datetime = wc_string_to_datetime( $args['date_before'] ); + $date_before = strpos( $args['date_before'], ':' ) ? $datetime->getOffsetTimestamp() : $datetime->date( 'Y-m-d' ); + } + if ( ! empty( $args['date_after'] ) ) { + $datetime = wc_string_to_datetime( $args['date_after'] ); + $date_after = strpos( $args['date_after'], ':' ) ? $datetime->getOffsetTimestamp() : $datetime->date( 'Y-m-d' ); + } + + if ( $date_before && $date_after ) { + $args['date_created'] = $date_after . '...' . $date_before; + } elseif ( $date_before ) { + $args['date_created'] = '<' . $date_before; + } elseif ( $date_after ) { + $args['date_created'] = '>' . $date_after; + } + + $query = new WC_Order_Query( $args ); + return $query->get_orders(); +} + +/** + * Main function for returning orders, uses the WC_Order_Factory class. + * + * @since 2.2 + * + * @param mixed $the_order Post object or post ID of the order. + * + * @return bool|WC_Order|WC_Order_Refund + */ +function wc_get_order( $the_order = false ) { + if ( ! did_action( 'woocommerce_after_register_post_type' ) ) { + wc_doing_it_wrong( __FUNCTION__, 'wc_get_order should not be called before post types are registered (woocommerce_after_register_post_type action)', '2.5' ); + return false; + } + return WC()->order_factory->get_order( $the_order ); +} + +/** + * Get all order statuses. + * + * @since 2.2 + * @used-by WC_Order::set_status + * @return array + */ +function wc_get_order_statuses() { + $order_statuses = array( + 'wc-pending' => _x( 'Pending payment', 'Order status', 'woocommerce' ), + 'wc-processing' => _x( 'Processing', 'Order status', 'woocommerce' ), + 'wc-on-hold' => _x( 'On hold', 'Order status', 'woocommerce' ), + 'wc-completed' => _x( 'Completed', 'Order status', 'woocommerce' ), + 'wc-cancelled' => _x( 'Cancelled', 'Order status', 'woocommerce' ), + 'wc-refunded' => _x( 'Refunded', 'Order status', 'woocommerce' ), + 'wc-failed' => _x( 'Failed', 'Order status', 'woocommerce' ), + ); + return apply_filters( 'wc_order_statuses', $order_statuses ); +} + +/** + * See if a string is an order status. + * + * @param string $maybe_status Status, including any wc- prefix. + * @return bool + */ +function wc_is_order_status( $maybe_status ) { + $order_statuses = wc_get_order_statuses(); + return isset( $order_statuses[ $maybe_status ] ); +} + +/** + * Get list of statuses which are consider 'paid'. + * + * @since 3.0.0 + * @return array + */ +function wc_get_is_paid_statuses() { + return apply_filters( 'woocommerce_order_is_paid_statuses', array( 'processing', 'completed' ) ); +} + +/** + * Get list of statuses which are consider 'pending payment'. + * + * @since 3.6.0 + * @return array + */ +function wc_get_is_pending_statuses() { + return apply_filters( 'woocommerce_order_is_pending_statuses', array( 'pending' ) ); +} + +/** + * Get the nice name for an order status. + * + * @since 2.2 + * @param string $status Status. + * @return string + */ +function wc_get_order_status_name( $status ) { + $statuses = wc_get_order_statuses(); + $status = 'wc-' === substr( $status, 0, 3 ) ? substr( $status, 3 ) : $status; + $status = isset( $statuses[ 'wc-' . $status ] ) ? $statuses[ 'wc-' . $status ] : $status; + return $status; +} + +/** + * Generate an order key with prefix. + * + * @since 3.5.4 + * @param string $key Order key without a prefix. By default generates a 13 digit secret. + * @return string The order key. + */ +function wc_generate_order_key( $key = '' ) { + if ( '' === $key ) { + $key = wp_generate_password( 13, false ); + } + + return 'wc_' . apply_filters( 'woocommerce_generate_order_key', 'order_' . $key ); +} + +/** + * Finds an Order ID based on an order key. + * + * @param string $order_key An order key has generated by. + * @return int The ID of an order, or 0 if the order could not be found. + */ +function wc_get_order_id_by_order_key( $order_key ) { + $data_store = WC_Data_Store::load( 'order' ); + return $data_store->get_order_id_by_order_key( $order_key ); +} + +/** + * Get all registered order types. + * + * @since 2.2 + * @param string $for Optionally define what you are getting order types for so + * only relevant types are returned. + * e.g. for 'order-meta-boxes', 'order-count'. + * @return array + */ +function wc_get_order_types( $for = '' ) { + global $wc_order_types; + + if ( ! is_array( $wc_order_types ) ) { + $wc_order_types = array(); + } + + $order_types = array(); + + switch ( $for ) { + case 'order-count': + foreach ( $wc_order_types as $type => $args ) { + if ( ! $args['exclude_from_order_count'] ) { + $order_types[] = $type; + } + } + break; + case 'order-meta-boxes': + foreach ( $wc_order_types as $type => $args ) { + if ( $args['add_order_meta_boxes'] ) { + $order_types[] = $type; + } + } + break; + case 'view-orders': + foreach ( $wc_order_types as $type => $args ) { + if ( ! $args['exclude_from_order_views'] ) { + $order_types[] = $type; + } + } + break; + case 'reports': + foreach ( $wc_order_types as $type => $args ) { + if ( ! $args['exclude_from_order_reports'] ) { + $order_types[] = $type; + } + } + break; + case 'sales-reports': + foreach ( $wc_order_types as $type => $args ) { + if ( ! $args['exclude_from_order_sales_reports'] ) { + $order_types[] = $type; + } + } + break; + case 'order-webhooks': + foreach ( $wc_order_types as $type => $args ) { + if ( ! $args['exclude_from_order_webhooks'] ) { + $order_types[] = $type; + } + } + break; + default: + $order_types = array_keys( $wc_order_types ); + break; + } + + return apply_filters( 'wc_order_types', $order_types, $for ); +} + +/** + * Get an order type by post type name. + * + * @param string $type Post type name. + * @return bool|array Details about the order type. + */ +function wc_get_order_type( $type ) { + global $wc_order_types; + + if ( isset( $wc_order_types[ $type ] ) ) { + return $wc_order_types[ $type ]; + } + + return false; +} + +/** + * Register order type. Do not use before init. + * + * Wrapper for register post type, as well as a method of telling WC which. + * post types are types of orders, and having them treated as such. + * + * $args are passed to register_post_type, but there are a few specific to this function: + * - exclude_from_orders_screen (bool) Whether or not this order type also get shown in the main. + * orders screen. + * - add_order_meta_boxes (bool) Whether or not the order type gets shop_order meta boxes. + * - exclude_from_order_count (bool) Whether or not this order type is excluded from counts. + * - exclude_from_order_views (bool) Whether or not this order type is visible by customers when. + * viewing orders e.g. on the my account page. + * - exclude_from_order_reports (bool) Whether or not to exclude this type from core reports. + * - exclude_from_order_sales_reports (bool) Whether or not to exclude this type from core sales reports. + * + * @since 2.2 + * @see register_post_type for $args used in that function + * @param string $type Post type. (max. 20 characters, can not contain capital letters or spaces). + * @param array $args An array of arguments. + * @return bool Success or failure + */ +function wc_register_order_type( $type, $args = array() ) { + if ( post_type_exists( $type ) ) { + return false; + } + + global $wc_order_types; + + if ( ! is_array( $wc_order_types ) ) { + $wc_order_types = array(); + } + + // Register as a post type. + if ( is_wp_error( register_post_type( $type, $args ) ) ) { + return false; + } + + // Register for WC usage. + $order_type_args = array( + 'exclude_from_orders_screen' => false, + 'add_order_meta_boxes' => true, + 'exclude_from_order_count' => false, + 'exclude_from_order_views' => false, + 'exclude_from_order_webhooks' => false, + 'exclude_from_order_reports' => false, + 'exclude_from_order_sales_reports' => false, + 'class_name' => 'WC_Order', + ); + + $args = array_intersect_key( $args, $order_type_args ); + $args = wp_parse_args( $args, $order_type_args ); + $wc_order_types[ $type ] = $args; + + return true; +} + +/** + * Return the count of processing orders. + * + * @return int + */ +function wc_processing_order_count() { + return wc_orders_count( 'processing' ); +} + +/** + * Return the orders count of a specific order status. + * + * @param string $status Status. + * @return int + */ +function wc_orders_count( $status ) { + $count = 0; + $status = 'wc-' . $status; + $order_statuses = array_keys( wc_get_order_statuses() ); + + if ( ! in_array( $status, $order_statuses, true ) ) { + return 0; + } + + $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . $status; + $cached_count = wp_cache_get( $cache_key, 'counts' ); + + if ( false !== $cached_count ) { + return $cached_count; + } + + foreach ( wc_get_order_types( 'order-count' ) as $type ) { + $data_store = WC_Data_Store::load( 'shop_order' === $type ? 'order' : $type ); + if ( $data_store ) { + $count += $data_store->get_order_count( $status ); + } + } + + wp_cache_set( $cache_key, $count, 'counts' ); + + return $count; +} + +/** + * Grant downloadable product access to the file identified by $download_id. + * + * @param string $download_id File identifier. + * @param int|WC_Product $product Product instance or ID. + * @param WC_Order $order Order data. + * @param int $qty Quantity purchased. + * @param WC_Order_Item $item Item of the order. + * @return int|bool insert id or false on failure. + */ +function wc_downloadable_file_permission( $download_id, $product, $order, $qty = 1, $item = null ) { + if ( is_numeric( $product ) ) { + $product = wc_get_product( $product ); + } + $download = new WC_Customer_Download(); + $download->set_download_id( $download_id ); + $download->set_product_id( $product->get_id() ); + $download->set_user_id( $order->get_customer_id() ); + $download->set_order_id( $order->get_id() ); + $download->set_user_email( $order->get_billing_email() ); + $download->set_order_key( $order->get_order_key() ); + $download->set_downloads_remaining( 0 > $product->get_download_limit() ? '' : $product->get_download_limit() * $qty ); + $download->set_access_granted( time() ); + $download->set_download_count( 0 ); + + $expiry = $product->get_download_expiry(); + + if ( $expiry > 0 ) { + $from_date = $order->get_date_completed() ? $order->get_date_completed()->format( 'Y-m-d' ) : current_time( 'mysql', true ); + $download->set_access_expires( strtotime( $from_date . ' + ' . $expiry . ' DAY' ) ); + } + + $download = apply_filters( 'woocommerce_downloadable_file_permission', $download, $product, $order, $qty, $item ); + + return $download->save(); +} + +/** + * Order Status completed - give downloadable product access to customer. + * + * @param int $order_id Order ID. + * @param bool $force Force downloadable permissions. + */ +function wc_downloadable_product_permissions( $order_id, $force = false ) { + $order = wc_get_order( $order_id ); + + if ( ! $order || ( $order->get_data_store()->get_download_permissions_granted( $order ) && ! $force ) ) { + return; + } + + if ( $order->has_status( 'processing' ) && 'no' === get_option( 'woocommerce_downloads_grant_access_after_payment' ) ) { + return; + } + + if ( count( $order->get_items() ) > 0 ) { + foreach ( $order->get_items() as $item ) { + $product = $item->get_product(); + + if ( $product && $product->exists() && $product->is_downloadable() ) { + $downloads = $product->get_downloads(); + + foreach ( array_keys( $downloads ) as $download_id ) { + wc_downloadable_file_permission( $download_id, $product, $order, $item->get_quantity(), $item ); + } + } + } + } + + $order->get_data_store()->set_download_permissions_granted( $order, true ); + do_action( 'woocommerce_grant_product_download_permissions', $order_id ); +} +add_action( 'woocommerce_order_status_completed', 'wc_downloadable_product_permissions' ); +add_action( 'woocommerce_order_status_processing', 'wc_downloadable_product_permissions' ); + +/** + * Clear all transients cache for order data. + * + * @param int|WC_Order $order Order instance or ID. + */ +function wc_delete_shop_order_transients( $order = 0 ) { + if ( is_numeric( $order ) ) { + $order = wc_get_order( $order ); + } + $reports = WC_Admin_Reports::get_reports(); + $transients_to_clear = array( + 'wc_admin_report', + ); + + foreach ( $reports as $report_group ) { + foreach ( $report_group['reports'] as $report_key => $report ) { + $transients_to_clear[] = 'wc_report_' . $report_key; + } + } + + foreach ( $transients_to_clear as $transient ) { + delete_transient( $transient ); + } + + // Clear customer's order related caches. + if ( is_a( $order, 'WC_Order' ) ) { + $order_id = $order->get_id(); + delete_user_meta( $order->get_customer_id(), '_money_spent' ); + delete_user_meta( $order->get_customer_id(), '_order_count' ); + delete_user_meta( $order->get_customer_id(), '_last_order' ); + } else { + $order_id = 0; + } + + // Increments the transient version to invalidate cache. + WC_Cache_Helper::get_transient_version( 'orders', true ); + + // Do the same for regular cache. + WC_Cache_Helper::invalidate_cache_group( 'orders' ); + + do_action( 'woocommerce_delete_shop_order_transients', $order_id ); +} + +/** + * See if we only ship to billing addresses. + * + * @return bool + */ +function wc_ship_to_billing_address_only() { + return 'billing_only' === get_option( 'woocommerce_ship_to_destination' ); +} + +/** + * Create a new order refund programmatically. + * + * Returns a new refund object on success which can then be used to add additional data. + * + * @since 2.2 + * @throws Exception Throws exceptions when fail to create, but returns WP_Error instead. + * @param array $args New refund arguments. + * @return WC_Order_Refund|WP_Error + */ +function wc_create_refund( $args = array() ) { + $default_args = array( + 'amount' => 0, + 'reason' => null, + 'order_id' => 0, + 'refund_id' => 0, + 'line_items' => array(), + 'refund_payment' => false, + 'restock_items' => false, + ); + + try { + $args = wp_parse_args( $args, $default_args ); + $order = wc_get_order( $args['order_id'] ); + + if ( ! $order ) { + throw new Exception( __( 'Invalid order ID.', 'woocommerce' ) ); + } + + $remaining_refund_amount = $order->get_remaining_refund_amount(); + $remaining_refund_items = $order->get_remaining_refund_items(); + $refund_item_count = 0; + $refund = new WC_Order_Refund( $args['refund_id'] ); + + if ( 0 > $args['amount'] || $args['amount'] > $remaining_refund_amount ) { + throw new Exception( __( 'Invalid refund amount.', 'woocommerce' ) ); + } + + $refund->set_currency( $order->get_currency() ); + $refund->set_amount( $args['amount'] ); + $refund->set_parent_id( absint( $args['order_id'] ) ); + $refund->set_refunded_by( get_current_user_id() ? get_current_user_id() : 1 ); + $refund->set_prices_include_tax( $order->get_prices_include_tax() ); + + if ( ! is_null( $args['reason'] ) ) { + $refund->set_reason( $args['reason'] ); + } + + // Negative line items. + if ( count( $args['line_items'] ) > 0 ) { + $items = $order->get_items( array( 'line_item', 'fee', 'shipping' ) ); + + foreach ( $items as $item_id => $item ) { + if ( ! isset( $args['line_items'][ $item_id ] ) ) { + continue; + } + + $qty = isset( $args['line_items'][ $item_id ]['qty'] ) ? $args['line_items'][ $item_id ]['qty'] : 0; + $refund_total = $args['line_items'][ $item_id ]['refund_total']; + $refund_tax = isset( $args['line_items'][ $item_id ]['refund_tax'] ) ? array_filter( (array) $args['line_items'][ $item_id ]['refund_tax'] ) : array(); + + if ( empty( $qty ) && empty( $refund_total ) && empty( $args['line_items'][ $item_id ]['refund_tax'] ) ) { + continue; + } + + $class = get_class( $item ); + $refunded_item = new $class( $item ); + $refunded_item->set_id( 0 ); + $refunded_item->add_meta_data( '_refunded_item_id', $item_id, true ); + $refunded_item->set_total( wc_format_refund_total( $refund_total ) ); + $refunded_item->set_taxes( + array( + 'total' => array_map( 'wc_format_refund_total', $refund_tax ), + 'subtotal' => array_map( 'wc_format_refund_total', $refund_tax ), + ) + ); + + if ( is_callable( array( $refunded_item, 'set_subtotal' ) ) ) { + $refunded_item->set_subtotal( wc_format_refund_total( $refund_total ) ); + } + + if ( is_callable( array( $refunded_item, 'set_quantity' ) ) ) { + $refunded_item->set_quantity( $qty * -1 ); + } + + $refund->add_item( $refunded_item ); + $refund_item_count += $qty; + } + } + + $refund->update_taxes(); + $refund->calculate_totals( false ); + $refund->set_total( $args['amount'] * -1 ); + + // this should remain after update_taxes(), as this will save the order, and write the current date to the db + // so we must wait until the order is persisted to set the date. + if ( isset( $args['date_created'] ) ) { + $refund->set_date_created( $args['date_created'] ); + } + + /** + * Action hook to adjust refund before save. + * + * @since 3.0.0 + */ + do_action( 'woocommerce_create_refund', $refund, $args ); + + if ( $refund->save() ) { + if ( $args['refund_payment'] ) { + $result = wc_refund_payment( $order, $refund->get_amount(), $refund->get_reason() ); + + if ( is_wp_error( $result ) ) { + $refund->delete(); + return $result; + } + + $refund->set_refunded_payment( true ); + $refund->save(); + } + + if ( $args['restock_items'] ) { + wc_restock_refunded_items( $order, $args['line_items'] ); + } + + // Trigger notification emails. + if ( ( $remaining_refund_amount - $args['amount'] ) > 0 || ( $order->has_free_item() && ( $remaining_refund_items - $refund_item_count ) > 0 ) ) { + do_action( 'woocommerce_order_partially_refunded', $order->get_id(), $refund->get_id() ); + } else { + do_action( 'woocommerce_order_fully_refunded', $order->get_id(), $refund->get_id() ); + + $parent_status = apply_filters( 'woocommerce_order_fully_refunded_status', 'refunded', $order->get_id(), $refund->get_id() ); + + if ( $parent_status ) { + $order->update_status( $parent_status ); + } + } + } + + do_action( 'woocommerce_refund_created', $refund->get_id(), $args ); + do_action( 'woocommerce_order_refunded', $order->get_id(), $refund->get_id() ); + + } catch ( Exception $e ) { + if ( isset( $refund ) && is_a( $refund, 'WC_Order_Refund' ) ) { + wp_delete_post( $refund->get_id(), true ); + } + return new WP_Error( 'error', $e->getMessage() ); + } + + return $refund; +} + +/** + * Try to refund the payment for an order via the gateway. + * + * @since 3.0.0 + * @throws Exception Throws exceptions when fail to refund, but returns WP_Error instead. + * @param WC_Order $order Order instance. + * @param string $amount Amount to refund. + * @param string $reason Refund reason. + * @return bool|WP_Error + */ +function wc_refund_payment( $order, $amount, $reason = '' ) { + try { + if ( ! is_a( $order, 'WC_Order' ) ) { + throw new Exception( __( 'Invalid order.', 'woocommerce' ) ); + } + + $gateway_controller = WC_Payment_Gateways::instance(); + $all_gateways = $gateway_controller->payment_gateways(); + $payment_method = $order->get_payment_method(); + $gateway = isset( $all_gateways[ $payment_method ] ) ? $all_gateways[ $payment_method ] : false; + + if ( ! $gateway ) { + throw new Exception( __( 'The payment gateway for this order does not exist.', 'woocommerce' ) ); + } + + if ( ! $gateway->supports( 'refunds' ) ) { + throw new Exception( __( 'The payment gateway for this order does not support automatic refunds.', 'woocommerce' ) ); + } + + $result = $gateway->process_refund( $order->get_id(), $amount, $reason ); + + if ( ! $result ) { + throw new Exception( __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ) ); + } + + if ( is_wp_error( $result ) ) { + throw new Exception( $result->get_error_message() ); + } + + return true; + + } catch ( Exception $e ) { + return new WP_Error( 'error', $e->getMessage() ); + } +} + +/** + * Restock items during refund. + * + * @since 3.0.0 + * @param WC_Order $order Order instance. + * @param array $refunded_line_items Refunded items list. + */ +function wc_restock_refunded_items( $order, $refunded_line_items ) { + if ( ! apply_filters( 'woocommerce_can_restock_refunded_items', true, $order, $refunded_line_items ) ) { + return; + } + + $line_items = $order->get_items(); + + foreach ( $line_items as $item_id => $item ) { + if ( ! isset( $refunded_line_items[ $item_id ], $refunded_line_items[ $item_id ]['qty'] ) ) { + continue; + } + $product = $item->get_product(); + $item_stock_reduced = $item->get_meta( '_reduced_stock', true ); + $restock_refunded_items = (int) $item->get_meta( '_restock_refunded_items', true ); + $qty_to_refund = $refunded_line_items[ $item_id ]['qty']; + + if ( ! $item_stock_reduced || ! $qty_to_refund || ! $product || ! $product->managing_stock() ) { + continue; + } + + $old_stock = $product->get_stock_quantity(); + $new_stock = wc_update_product_stock( $product, $qty_to_refund, 'increase' ); + + // Update _reduced_stock meta to track changes. + $item_stock_reduced = $item_stock_reduced - $qty_to_refund; + + // Keeps track of total running tally of reduced stock. + $item->update_meta_data( '_reduced_stock', $item_stock_reduced ); + + // Keeps track of only refunded items that needs restock. + $item->update_meta_data( '_restock_refunded_items', $qty_to_refund + $restock_refunded_items ); + + /* translators: 1: product ID 2: old stock level 3: new stock level */ + $restock_note = sprintf( __( 'Item #%1$s stock increased from %2$s to %3$s.', 'woocommerce' ), $product->get_id(), $old_stock, $new_stock ); + + /** + * Allow the restock note to be modified. + * + * @since 6.4.0 + * + * @param string $restock_note The original note. + * @param int $old_stock The old stock. + * @param bool|int|null $new_stock The new stock. + * @param WC_Order $order The order the refund was done for. + * @param bool|WC_Product $product The product the refund was done for. + */ + $restock_note = apply_filters( 'woocommerce_refund_restock_note', $restock_note, $old_stock, $new_stock, $order, $product ); + + $order->add_order_note( $restock_note ); + + $item->save(); + + do_action( 'woocommerce_restock_refunded_item', $product->get_id(), $old_stock, $new_stock, $order, $product ); + } +} + +/** + * Get tax class by tax id. + * + * @since 2.2 + * @param int $tax_id Tax ID. + * @return string + */ +function wc_get_tax_class_by_tax_id( $tax_id ) { + global $wpdb; + return $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_class FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d", $tax_id ) ); +} + +/** + * Get payment gateway class by order data. + * + * @since 2.2 + * @param int|WC_Order $order Order instance. + * @return WC_Payment_Gateway|bool + */ +function wc_get_payment_gateway_by_order( $order ) { + if ( WC()->payment_gateways() ) { + $payment_gateways = WC()->payment_gateways()->payment_gateways(); + } else { + $payment_gateways = array(); + } + + if ( ! is_object( $order ) ) { + $order_id = absint( $order ); + $order = wc_get_order( $order_id ); + } + + return is_a( $order, 'WC_Order' ) && isset( $payment_gateways[ $order->get_payment_method() ] ) ? $payment_gateways[ $order->get_payment_method() ] : false; +} + +/** + * When refunding an order, create a refund line item if the partial refunds do not match order total. + * + * This is manual; no gateway refund will be performed. + * + * @since 2.4 + * @param int $order_id Order ID. + */ +function wc_order_fully_refunded( $order_id ) { + $order = wc_get_order( $order_id ); + $max_refund = wc_format_decimal( $order->get_total() - $order->get_total_refunded() ); + + if ( ! $max_refund ) { + return; + } + + // Create the refund object. + wc_switch_to_site_locale(); + wc_create_refund( + array( + 'amount' => $max_refund, + 'reason' => __( 'Order fully refunded.', 'woocommerce' ), + 'order_id' => $order_id, + 'line_items' => array(), + ) + ); + wc_restore_locale(); + + $order->add_order_note( __( 'Order status set to refunded. To return funds to the customer you will need to issue a refund through your payment gateway.', 'woocommerce' ) ); +} +add_action( 'woocommerce_order_status_refunded', 'wc_order_fully_refunded' ); + +/** + * Search orders. + * + * @since 2.6.0 + * @param string $term Term to search. + * @return array List of orders ID. + */ +function wc_order_search( $term ) { + $data_store = WC_Data_Store::load( 'order' ); + return $data_store->search_orders( str_replace( 'Order #', '', wc_clean( $term ) ) ); +} + +/** + * Update total sales amount for each product within a paid order. + * + * @since 3.0.0 + * @param int $order_id Order ID. + */ +function wc_update_total_sales_counts( $order_id ) { + $order = wc_get_order( $order_id ); + + if ( ! $order || $order->get_data_store()->get_recorded_sales( $order ) ) { + return; + } + + if ( count( $order->get_items() ) > 0 ) { + foreach ( $order->get_items() as $item ) { + $product_id = $item->get_product_id(); + + if ( $product_id ) { + $data_store = WC_Data_Store::load( 'product' ); + $data_store->update_product_sales( $product_id, absint( $item->get_quantity() ), 'increase' ); + } + } + } + + $order->get_data_store()->set_recorded_sales( $order, true ); + + /** + * Called when sales for an order are recorded + * + * @param int $order_id order id + */ + do_action( 'woocommerce_recorded_sales', $order_id ); +} +add_action( 'woocommerce_order_status_completed', 'wc_update_total_sales_counts' ); +add_action( 'woocommerce_order_status_processing', 'wc_update_total_sales_counts' ); +add_action( 'woocommerce_order_status_on-hold', 'wc_update_total_sales_counts' ); + +/** + * Update used coupon amount for each coupon within an order. + * + * @since 3.0.0 + * @param int $order_id Order ID. + */ +function wc_update_coupon_usage_counts( $order_id ) { + $order = wc_get_order( $order_id ); + + if ( ! $order ) { + return; + } + + $has_recorded = $order->get_data_store()->get_recorded_coupon_usage_counts( $order ); + + if ( $order->has_status( 'cancelled' ) && $has_recorded ) { + $action = 'reduce'; + $order->get_data_store()->set_recorded_coupon_usage_counts( $order, false ); + } elseif ( ! $order->has_status( 'cancelled' ) && ! $has_recorded ) { + $action = 'increase'; + $order->get_data_store()->set_recorded_coupon_usage_counts( $order, true ); + } elseif ( $order->has_status( 'cancelled' ) ) { + $order->get_data_store()->release_held_coupons( $order, true ); + return; + } else { + return; + } + + if ( count( $order->get_coupon_codes() ) > 0 ) { + foreach ( $order->get_coupon_codes() as $code ) { + if ( ! $code ) { + continue; + } + + $coupon = new WC_Coupon( $code ); + $used_by = $order->get_user_id(); + + if ( ! $used_by ) { + $used_by = $order->get_billing_email(); + } + + switch ( $action ) { + case 'reduce': + $coupon->decrease_usage_count( $used_by ); + break; + case 'increase': + $coupon->increase_usage_count( $used_by, $order ); + break; + } + } + $order->get_data_store()->release_held_coupons( $order, true ); + } +} +add_action( 'woocommerce_order_status_pending', 'wc_update_coupon_usage_counts' ); +add_action( 'woocommerce_order_status_completed', 'wc_update_coupon_usage_counts' ); +add_action( 'woocommerce_order_status_processing', 'wc_update_coupon_usage_counts' ); +add_action( 'woocommerce_order_status_on-hold', 'wc_update_coupon_usage_counts' ); +add_action( 'woocommerce_order_status_cancelled', 'wc_update_coupon_usage_counts' ); + +/** + * Cancel all unpaid orders after held duration to prevent stock lock for those products. + */ +function wc_cancel_unpaid_orders() { + $held_duration = get_option( 'woocommerce_hold_stock_minutes' ); + + // Re-schedule the event before cancelling orders + // this way in case of a DB timeout or (plugin) crash the event is always scheduled for retry. + wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' ); + $cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $held_duration ) ); + wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' ); + + if ( $held_duration < 1 || 'yes' !== get_option( 'woocommerce_manage_stock' ) ) { + return; + } + + $data_store = WC_Data_Store::load( 'order' ); + $unpaid_orders = $data_store->get_unpaid_orders( strtotime( '-' . absint( $held_duration ) . ' MINUTES', current_time( 'timestamp' ) ) ); + + if ( $unpaid_orders ) { + foreach ( $unpaid_orders as $unpaid_order ) { + $order = wc_get_order( $unpaid_order ); + + if ( apply_filters( 'woocommerce_cancel_unpaid_order', 'checkout' === $order->get_created_via(), $order ) ) { + $order->update_status( 'cancelled', __( 'Unpaid order cancelled - time limit reached.', 'woocommerce' ) ); + } + } + } +} +add_action( 'woocommerce_cancel_unpaid_orders', 'wc_cancel_unpaid_orders' ); + +/** + * Sanitize order id removing unwanted characters. + * + * E.g Users can sometimes try to track an order id using # with no success. + * This function will fix this. + * + * @since 3.1.0 + * @param int $order_id Order ID. + */ +function wc_sanitize_order_id( $order_id ) { + return (int) filter_var( $order_id, FILTER_SANITIZE_NUMBER_INT ); +} +add_filter( 'woocommerce_shortcode_order_tracking_order_id', 'wc_sanitize_order_id' ); + +/** + * Get an order note. + * + * @since 3.2.0 + * @param int|WP_Comment $data Note ID (or WP_Comment instance for internal use only). + * @return stdClass|null Object with order note details or null when does not exists. + */ +function wc_get_order_note( $data ) { + if ( is_numeric( $data ) ) { + $data = get_comment( $data ); + } + + if ( ! is_a( $data, 'WP_Comment' ) ) { + return null; + } + + return (object) apply_filters( + 'woocommerce_get_order_note', + array( + 'id' => (int) $data->comment_ID, + 'date_created' => wc_string_to_datetime( $data->comment_date ), + 'content' => $data->comment_content, + 'customer_note' => (bool) get_comment_meta( $data->comment_ID, 'is_customer_note', true ), + 'added_by' => __( 'WooCommerce', 'woocommerce' ) === $data->comment_author ? 'system' : $data->comment_author, + ), + $data + ); +} + +/** + * Get order notes. + * + * @since 3.2.0 + * @param array $args Query arguments { + * Array of query parameters. + * + * @type string $limit Maximum number of notes to retrieve. + * Default empty (no limit). + * @type int $order_id Limit results to those affiliated with a given order ID. + * Default 0. + * @type array $order__in Array of order IDs to include affiliated notes for. + * Default empty. + * @type array $order__not_in Array of order IDs to exclude affiliated notes for. + * Default empty. + * @type string $orderby Define how should sort notes. + * Accepts 'date_created', 'date_created_gmt' or 'id'. + * Default: 'id'. + * @type string $order How to order retrieved notes. + * Accepts 'ASC' or 'DESC'. + * Default: 'DESC'. + * @type string $type Define what type of note should retrieve. + * Accepts 'customer', 'internal' or empty for both. + * Default empty. + * } + * @return stdClass[] Array of stdClass objects with order notes details. + */ +function wc_get_order_notes( $args ) { + $key_mapping = array( + 'limit' => 'number', + 'order_id' => 'post_id', + 'order__in' => 'post__in', + 'order__not_in' => 'post__not_in', + ); + + foreach ( $key_mapping as $query_key => $db_key ) { + if ( isset( $args[ $query_key ] ) ) { + $args[ $db_key ] = $args[ $query_key ]; + unset( $args[ $query_key ] ); + } + } + + // Define orderby. + $orderby_mapping = array( + 'date_created' => 'comment_date', + 'date_created_gmt' => 'comment_date_gmt', + 'id' => 'comment_ID', + ); + + $args['orderby'] = ! empty( $args['orderby'] ) && in_array( $args['orderby'], array( 'date_created', 'date_created_gmt', 'id' ), true ) ? $orderby_mapping[ $args['orderby'] ] : 'comment_ID'; + + // Set WooCommerce order type. + if ( isset( $args['type'] ) && 'customer' === $args['type'] ) { + $args['meta_query'] = array( // WPCS: slow query ok. + array( + 'key' => 'is_customer_note', + 'value' => 1, + 'compare' => '=', + ), + ); + } elseif ( isset( $args['type'] ) && 'internal' === $args['type'] ) { + $args['meta_query'] = array( // WPCS: slow query ok. + array( + 'key' => 'is_customer_note', + 'compare' => 'NOT EXISTS', + ), + ); + } + + // Set correct comment type. + $args['type'] = 'order_note'; + + // Always approved. + $args['status'] = 'approve'; + + // Does not support 'count' or 'fields'. + unset( $args['count'], $args['fields'] ); + + remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $notes = get_comments( $args ); + + add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + return array_filter( array_map( 'wc_get_order_note', $notes ) ); +} + +/** + * Create an order note. + * + * @since 3.2.0 + * @param int $order_id Order ID. + * @param string $note Note to add. + * @param bool $is_customer_note If is a costumer note. + * @param bool $added_by_user If note is create by an user. + * @return int|WP_Error Integer when created or WP_Error when found an error. + */ +function wc_create_order_note( $order_id, $note, $is_customer_note = false, $added_by_user = false ) { + $order = wc_get_order( $order_id ); + + if ( ! $order ) { + return new WP_Error( 'invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + return $order->add_order_note( $note, (int) $is_customer_note, $added_by_user ); +} + +/** + * Delete an order note. + * + * @since 3.2.0 + * @param int $note_id Order note. + * @return bool True on success, false on failure. + */ +function wc_delete_order_note( $note_id ) { + return wp_delete_comment( $note_id, true ); +} diff --git a/includes/wc-order-item-functions.php b/plugins/woocommerce/includes/wc-order-item-functions.php similarity index 100% rename from includes/wc-order-item-functions.php rename to plugins/woocommerce/includes/wc-order-item-functions.php diff --git a/includes/wc-page-functions.php b/plugins/woocommerce/includes/wc-page-functions.php similarity index 100% rename from includes/wc-page-functions.php rename to plugins/woocommerce/includes/wc-page-functions.php diff --git a/plugins/woocommerce/includes/wc-product-functions.php b/plugins/woocommerce/includes/wc-product-functions.php new file mode 100644 index 00000000000..7a4811d9816 --- /dev/null +++ b/plugins/woocommerce/includes/wc-product-functions.php @@ -0,0 +1,1645 @@ + 'limit', + 'post_status' => 'status', + 'post_parent' => 'parent', + 'posts_per_page' => 'limit', + 'paged' => 'page', + ); + + foreach ( $map_legacy as $from => $to ) { + if ( isset( $args[ $from ] ) ) { + $args[ $to ] = $args[ $from ]; + } + } + + $query = new WC_Product_Query( $args ); + return $query->get_products(); +} + +/** + * Main function for returning products, uses the WC_Product_Factory class. + * + * This function should only be called after 'init' action is finished, as there might be taxonomies that are getting + * registered during the init action. + * + * @since 2.2.0 + * + * @param mixed $the_product Post object or post ID of the product. + * @param array $deprecated Previously used to pass arguments to the factory, e.g. to force a type. + * @return WC_Product|null|false + */ +function wc_get_product( $the_product = false, $deprecated = array() ) { + if ( ! did_action( 'woocommerce_init' ) || ! did_action( 'woocommerce_after_register_taxonomy' ) || ! did_action( 'woocommerce_after_register_post_type' ) ) { + /* translators: 1: wc_get_product 2: woocommerce_init 3: woocommerce_after_register_taxonomy 4: woocommerce_after_register_post_type */ + wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%1$s should not be called before the %2$s, %3$s and %4$s actions have finished.', 'woocommerce' ), 'wc_get_product', 'woocommerce_init', 'woocommerce_after_register_taxonomy', 'woocommerce_after_register_post_type' ), '3.9' ); + return false; + } + if ( ! empty( $deprecated ) ) { + wc_deprecated_argument( 'args', '3.0', 'Passing args to wc_get_product is deprecated. If you need to force a type, construct the product class directly.' ); + } + return WC()->product_factory->get_product( $the_product, $deprecated ); +} + +/** + * Get a product object. + * + * @see WC_Product_Factory::get_product_classname + * @since 3.9.0 + * @param string $product_type Product type. If used an invalid type a WC_Product_Simple instance will be returned. + * @param int $product_id Product ID. + * @return WC_Product + */ +function wc_get_product_object( $product_type, $product_id = 0 ) { + $classname = WC_Product_Factory::get_product_classname( $product_id, $product_type ); + + return new $classname( $product_id ); +} + +/** + * Returns whether or not SKUS are enabled. + * + * @return bool + */ +function wc_product_sku_enabled() { + return apply_filters( 'wc_product_sku_enabled', true ); +} + +/** + * Returns whether or not product weights are enabled. + * + * @return bool + */ +function wc_product_weight_enabled() { + return apply_filters( 'wc_product_weight_enabled', true ); +} + +/** + * Returns whether or not product dimensions (HxWxD) are enabled. + * + * @return bool + */ +function wc_product_dimensions_enabled() { + return apply_filters( 'wc_product_dimensions_enabled', true ); +} + +/** + * Clear transient cache for product data. + * + * @param int $post_id (default: 0) The product ID. + */ +function wc_delete_product_transients( $post_id = 0 ) { + // Transient data to clear with a fixed name which may be stale after product updates. + $transients_to_clear = array( + 'wc_products_onsale', + 'wc_featured_products', + 'wc_outofstock_count', + 'wc_low_stock_count', + ); + + foreach ( $transients_to_clear as $transient ) { + delete_transient( $transient ); + } + + if ( $post_id > 0 ) { + // Transient names that include an ID - since they are dynamic they cannot be cleaned in bulk without the ID. + $post_transient_names = array( + 'wc_product_children_', + 'wc_var_prices_', + 'wc_related_', + 'wc_child_has_weight_', + 'wc_child_has_dimensions_', + ); + + foreach ( $post_transient_names as $transient ) { + delete_transient( $transient . $post_id ); + } + } + + // Increments the transient version to invalidate cache. + WC_Cache_Helper::get_transient_version( 'product', true ); + + do_action( 'woocommerce_delete_product_transients', $post_id ); +} + +/** + * Function that returns an array containing the IDs of the products that are on sale. + * + * @since 2.0 + * @return array + */ +function wc_get_product_ids_on_sale() { + // Load from cache. + $product_ids_on_sale = get_transient( 'wc_products_onsale' ); + + // Valid cache found. + if ( false !== $product_ids_on_sale ) { + return $product_ids_on_sale; + } + + $data_store = WC_Data_Store::load( 'product' ); + $on_sale_products = $data_store->get_on_sale_products(); + $product_ids_on_sale = wp_parse_id_list( array_merge( wp_list_pluck( $on_sale_products, 'id' ), array_diff( wp_list_pluck( $on_sale_products, 'parent_id' ), array( 0 ) ) ) ); + + set_transient( 'wc_products_onsale', $product_ids_on_sale, DAY_IN_SECONDS * 30 ); + + return $product_ids_on_sale; +} + +/** + * Function that returns an array containing the IDs of the featured products. + * + * @since 2.1 + * @return array + */ +function wc_get_featured_product_ids() { + // Load from cache. + $featured_product_ids = get_transient( 'wc_featured_products' ); + + // Valid cache found. + if ( false !== $featured_product_ids ) { + return $featured_product_ids; + } + + $data_store = WC_Data_Store::load( 'product' ); + $featured = $data_store->get_featured_product_ids(); + $product_ids = array_keys( $featured ); + $parent_ids = array_values( array_filter( $featured ) ); + $featured_product_ids = array_unique( array_merge( $product_ids, $parent_ids ) ); + + set_transient( 'wc_featured_products', $featured_product_ids, DAY_IN_SECONDS * 30 ); + + return $featured_product_ids; +} + +/** + * Filter to allow product_cat in the permalinks for products. + * + * @param string $permalink The existing permalink URL. + * @param WP_Post $post WP_Post object. + * @return string + */ +function wc_product_post_type_link( $permalink, $post ) { + // Abort if post is not a product. + if ( 'product' !== $post->post_type ) { + return $permalink; + } + + // Abort early if the placeholder rewrite tag isn't in the generated URL. + if ( false === strpos( $permalink, '%' ) ) { + return $permalink; + } + + // Get the custom taxonomy terms in use by this post. + $terms = get_the_terms( $post->ID, 'product_cat' ); + + if ( ! empty( $terms ) ) { + $terms = wp_list_sort( + $terms, + array( + 'parent' => 'DESC', + 'term_id' => 'ASC', + ) + ); + $category_object = apply_filters( 'wc_product_post_type_link_product_cat', $terms[0], $terms, $post ); + $product_cat = $category_object->slug; + + if ( $category_object->parent ) { + $ancestors = get_ancestors( $category_object->term_id, 'product_cat' ); + foreach ( $ancestors as $ancestor ) { + $ancestor_object = get_term( $ancestor, 'product_cat' ); + if ( apply_filters( 'woocommerce_product_post_type_link_parent_category_only', false ) ) { + $product_cat = $ancestor_object->slug; + } else { + $product_cat = $ancestor_object->slug . '/' . $product_cat; + } + } + } + } else { + // If no terms are assigned to this post, use a string instead (can't leave the placeholder there). + $product_cat = _x( 'uncategorized', 'slug', 'woocommerce' ); + } + + $find = array( + '%year%', + '%monthnum%', + '%day%', + '%hour%', + '%minute%', + '%second%', + '%post_id%', + '%category%', + '%product_cat%', + ); + + $replace = array( + date_i18n( 'Y', strtotime( $post->post_date ) ), + date_i18n( 'm', strtotime( $post->post_date ) ), + date_i18n( 'd', strtotime( $post->post_date ) ), + date_i18n( 'H', strtotime( $post->post_date ) ), + date_i18n( 'i', strtotime( $post->post_date ) ), + date_i18n( 's', strtotime( $post->post_date ) ), + $post->ID, + $product_cat, + $product_cat, + ); + + $permalink = str_replace( $find, $replace, $permalink ); + + return $permalink; +} +add_filter( 'post_type_link', 'wc_product_post_type_link', 10, 2 ); + +/** + * Get the placeholder image URL either from media, or use the fallback image. + * + * @param string $size Thumbnail size to use. + * @return string + */ +function wc_placeholder_img_src( $size = 'woocommerce_thumbnail' ) { + $src = WC()->plugin_url() . '/assets/images/placeholder.png'; + $placeholder_image = get_option( 'woocommerce_placeholder_image', 0 ); + + if ( ! empty( $placeholder_image ) ) { + if ( is_numeric( $placeholder_image ) ) { + $image = wp_get_attachment_image_src( $placeholder_image, $size ); + + if ( ! empty( $image[0] ) ) { + $src = $image[0]; + } + } else { + $src = $placeholder_image; + } + } + + return apply_filters( 'woocommerce_placeholder_img_src', $src ); +} + +/** + * Get the placeholder image. + * + * Uses wp_get_attachment_image if using an attachment ID @since 3.6.0 to handle responsiveness. + * + * @param string $size Image size. + * @param string|array $attr Optional. Attributes for the image markup. Default empty. + * @return string + */ +function wc_placeholder_img( $size = 'woocommerce_thumbnail', $attr = '' ) { + $dimensions = wc_get_image_size( $size ); + $placeholder_image = get_option( 'woocommerce_placeholder_image', 0 ); + + $default_attr = array( + 'class' => 'woocommerce-placeholder wp-post-image', + 'alt' => __( 'Placeholder', 'woocommerce' ), + ); + + $attr = wp_parse_args( $attr, $default_attr ); + + if ( wp_attachment_is_image( $placeholder_image ) ) { + $image_html = wp_get_attachment_image( + $placeholder_image, + $size, + false, + $attr + ); + } else { + $image = wc_placeholder_img_src( $size ); + $hwstring = image_hwstring( $dimensions['width'], $dimensions['height'] ); + $attributes = array(); + + foreach ( $attr as $name => $value ) { + $attribute[] = esc_attr( $name ) . '="' . esc_attr( $value ) . '"'; + } + + $image_html = ''; + } + + return apply_filters( 'woocommerce_placeholder_img', $image_html, $size, $dimensions ); +} + +/** + * Variation Formatting. + * + * Gets a formatted version of variation data or item meta. + * + * @param array|WC_Product_Variation $variation Variation object. + * @param bool $flat Should this be a flat list or HTML list? (default: false). + * @param bool $include_names include attribute names/labels in the list. + * @param bool $skip_attributes_in_name Do not list attributes already part of the variation name. + * @return string + */ +function wc_get_formatted_variation( $variation, $flat = false, $include_names = true, $skip_attributes_in_name = false ) { + $return = ''; + + if ( is_a( $variation, 'WC_Product_Variation' ) ) { + $variation_attributes = $variation->get_attributes(); + $product = $variation; + $variation_name = $variation->get_name(); + } else { + $product = false; + $variation_name = ''; + // Remove attribute_ prefix from names. + $variation_attributes = array(); + if ( is_array( $variation ) ) { + foreach ( $variation as $key => $value ) { + $variation_attributes[ str_replace( 'attribute_', '', $key ) ] = $value; + } + } + } + + $list_type = $include_names ? 'dl' : 'ul'; + + if ( is_array( $variation_attributes ) ) { + + if ( ! $flat ) { + $return = '<' . $list_type . ' class="variation">'; + } + + $variation_list = array(); + + foreach ( $variation_attributes as $name => $value ) { + // If this is a term slug, get the term's nice name. + if ( taxonomy_exists( $name ) ) { + $term = get_term_by( 'slug', $value, $name ); + if ( ! is_wp_error( $term ) && ! empty( $term->name ) ) { + $value = $term->name; + } + } + + // Do not list attributes already part of the variation name. + if ( '' === $value || ( $skip_attributes_in_name && wc_is_attribute_in_product_name( $value, $variation_name ) ) ) { + continue; + } + + if ( $include_names ) { + if ( $flat ) { + $variation_list[] = wc_attribute_label( $name, $product ) . ': ' . rawurldecode( $value ); + } else { + $variation_list[] = '
    ' . wc_attribute_label( $name, $product ) . ':
    ' . rawurldecode( $value ) . '
    '; + } + } else { + if ( $flat ) { + $variation_list[] = rawurldecode( $value ); + } else { + $variation_list[] = '
  • ' . rawurldecode( $value ) . '
  • '; + } + } + } + + if ( $flat ) { + $return .= implode( ', ', $variation_list ); + } else { + $return .= implode( '', $variation_list ); + } + + if ( ! $flat ) { + $return .= ''; + } + } + return $return; +} + +/** + * Function which handles the start and end of scheduled sales via cron. + */ +function wc_scheduled_sales() { + $data_store = WC_Data_Store::load( 'product' ); + + // Sales which are due to start. + $product_ids = $data_store->get_starting_sales(); + if ( $product_ids ) { + do_action( 'wc_before_products_starting_sales', $product_ids ); + foreach ( $product_ids as $product_id ) { + $product = wc_get_product( $product_id ); + + if ( $product ) { + $sale_price = $product->get_sale_price(); + + if ( $sale_price ) { + $product->set_price( $sale_price ); + $product->set_date_on_sale_from( '' ); + } else { + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + } + + $product->save(); + } + } + do_action( 'wc_after_products_starting_sales', $product_ids ); + + WC_Cache_Helper::get_transient_version( 'product', true ); + delete_transient( 'wc_products_onsale' ); + } + + // Sales which are due to end. + $product_ids = $data_store->get_ending_sales(); + if ( $product_ids ) { + do_action( 'wc_before_products_ending_sales', $product_ids ); + foreach ( $product_ids as $product_id ) { + $product = wc_get_product( $product_id ); + + if ( $product ) { + $regular_price = $product->get_regular_price(); + $product->set_price( $regular_price ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->save(); + } + } + do_action( 'wc_after_products_ending_sales', $product_ids ); + + WC_Cache_Helper::get_transient_version( 'product', true ); + delete_transient( 'wc_products_onsale' ); + } +} +add_action( 'woocommerce_scheduled_sales', 'wc_scheduled_sales' ); + +/** + * Get attachment image attributes. + * + * @param array $attr Image attributes. + * @return array + */ +function wc_get_attachment_image_attributes( $attr ) { + /* + * If the user can manage woocommerce, allow them to + * see the image content. + */ + if ( current_user_can( 'manage_woocommerce' ) ) { + return $attr; + } + + /* + * If the user does not have the right capabilities, + * filter out the image source and replace with placeholder + * image. + */ + if ( isset( $attr['src'] ) && strstr( $attr['src'], 'woocommerce_uploads/' ) ) { + $attr['src'] = wc_placeholder_img_src(); + + if ( isset( $attr['srcset'] ) ) { + $attr['srcset'] = ''; + } + } + return $attr; +} +add_filter( 'wp_get_attachment_image_attributes', 'wc_get_attachment_image_attributes' ); + + +/** + * Prepare attachment for JavaScript. + * + * @param array $response JS version of a attachment post object. + * @return array + */ +function wc_prepare_attachment_for_js( $response ) { + /* + * If the user can manage woocommerce, allow them to + * see the image content. + */ + if ( current_user_can( 'manage_woocommerce' ) ) { + return $response; + } + + /* + * If the user does not have the right capabilities, + * filter out the image source and replace with placeholder + * image. + */ + if ( isset( $response['url'] ) && strstr( $response['url'], 'woocommerce_uploads/' ) ) { + $response['full']['url'] = wc_placeholder_img_src(); + if ( isset( $response['sizes'] ) ) { + foreach ( $response['sizes'] as $size => $value ) { + $response['sizes'][ $size ]['url'] = wc_placeholder_img_src(); + } + } + } + + return $response; +} +add_filter( 'wp_prepare_attachment_for_js', 'wc_prepare_attachment_for_js' ); + +/** + * Track product views. + */ +function wc_track_product_view() { + if ( ! is_singular( 'product' ) || ! is_active_widget( false, false, 'woocommerce_recently_viewed_products', true ) ) { + return; + } + + global $post; + + if ( empty( $_COOKIE['woocommerce_recently_viewed'] ) ) { // @codingStandardsIgnoreLine. + $viewed_products = array(); + } else { + $viewed_products = wp_parse_id_list( (array) explode( '|', wp_unslash( $_COOKIE['woocommerce_recently_viewed'] ) ) ); // @codingStandardsIgnoreLine. + } + + // Unset if already in viewed products list. + $keys = array_flip( $viewed_products ); + + if ( isset( $keys[ $post->ID ] ) ) { + unset( $viewed_products[ $keys[ $post->ID ] ] ); + } + + $viewed_products[] = $post->ID; + + if ( count( $viewed_products ) > 15 ) { + array_shift( $viewed_products ); + } + + // Store for session only. + wc_setcookie( 'woocommerce_recently_viewed', implode( '|', $viewed_products ) ); +} + +add_action( 'template_redirect', 'wc_track_product_view', 20 ); + +/** + * Get product types. + * + * @since 2.2 + * @return array + */ +function wc_get_product_types() { + return (array) apply_filters( + 'product_type_selector', + array( + 'simple' => __( 'Simple product', 'woocommerce' ), + 'grouped' => __( 'Grouped product', 'woocommerce' ), + 'external' => __( 'External/Affiliate product', 'woocommerce' ), + 'variable' => __( 'Variable product', 'woocommerce' ), + ) + ); +} + +/** + * Check if product sku is unique. + * + * @since 2.2 + * @param int $product_id Product ID. + * @param string $sku Product SKU. + * @return bool + */ +function wc_product_has_unique_sku( $product_id, $sku ) { + $data_store = WC_Data_Store::load( 'product' ); + $sku_found = $data_store->is_existing_sku( $product_id, $sku ); + + if ( apply_filters( 'wc_product_has_unique_sku', $sku_found, $product_id, $sku ) ) { + return false; + } + + return true; +} + +/** + * Force a unique SKU. + * + * @since 3.0.0 + * @param integer $product_id Product ID. + */ +function wc_product_force_unique_sku( $product_id ) { + $product = wc_get_product( $product_id ); + $current_sku = $product ? $product->get_sku( 'edit' ) : ''; + + if ( $current_sku ) { + try { + $new_sku = wc_product_generate_unique_sku( $product_id, $current_sku ); + + if ( $current_sku !== $new_sku ) { + $product->set_sku( $new_sku ); + $product->save(); + } + } catch ( Exception $e ) {} // @codingStandardsIgnoreLine. + } +} + +/** + * Recursively appends a suffix until a unique SKU is found. + * + * @since 3.0.0 + * @param integer $product_id Product ID. + * @param string $sku Product SKU. + * @param integer $index An optional index that can be added to the product SKU. + * @return string + */ +function wc_product_generate_unique_sku( $product_id, $sku, $index = 0 ) { + $generated_sku = 0 < $index ? $sku . '-' . $index : $sku; + + if ( ! wc_product_has_unique_sku( $product_id, $generated_sku ) ) { + $generated_sku = wc_product_generate_unique_sku( $product_id, $sku, ( $index + 1 ) ); + } + + return $generated_sku; +} + +/** + * Get product ID by SKU. + * + * @since 2.3.0 + * @param string $sku Product SKU. + * @return int + */ +function wc_get_product_id_by_sku( $sku ) { + $data_store = WC_Data_Store::load( 'product' ); + return $data_store->get_product_id_by_sku( $sku ); +} + +/** + * Get attributes/data for an individual variation from the database and maintain it's integrity. + * + * @since 2.4.0 + * @param int $variation_id Variation ID. + * @return array + */ +function wc_get_product_variation_attributes( $variation_id ) { + // Build variation data from meta. + $all_meta = get_post_meta( $variation_id ); + $parent_id = wp_get_post_parent_id( $variation_id ); + $parent_attributes = array_filter( (array) get_post_meta( $parent_id, '_product_attributes', true ) ); + $found_parent_attributes = array(); + $variation_attributes = array(); + + // Compare to parent variable product attributes and ensure they match. + foreach ( $parent_attributes as $attribute_name => $options ) { + if ( ! empty( $options['is_variation'] ) ) { + $attribute = 'attribute_' . sanitize_title( $attribute_name ); + $found_parent_attributes[] = $attribute; + if ( ! array_key_exists( $attribute, $variation_attributes ) ) { + $variation_attributes[ $attribute ] = ''; // Add it - 'any' will be asumed. + } + } + } + + // Get the variation attributes from meta. + foreach ( $all_meta as $name => $value ) { + // Only look at valid attribute meta, and also compare variation level attributes and remove any which do not exist at parent level. + if ( 0 !== strpos( $name, 'attribute_' ) || ! in_array( $name, $found_parent_attributes, true ) ) { + unset( $variation_attributes[ $name ] ); + continue; + } + /** + * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute. + * Attempt to get full version of the text attribute from the parent. + */ + if ( sanitize_title( $value[0] ) === $value[0] && version_compare( get_post_meta( $parent_id, '_product_version', true ), '2.4.0', '<' ) ) { + foreach ( $parent_attributes as $attribute ) { + if ( 'attribute_' . sanitize_title( $attribute['name'] ) !== $name ) { + continue; + } + $text_attributes = wc_get_text_attributes( $attribute['value'] ); + + foreach ( $text_attributes as $text_attribute ) { + if ( sanitize_title( $text_attribute ) === $value[0] ) { + $value[0] = $text_attribute; + break; + } + } + } + } + + $variation_attributes[ $name ] = $value[0]; + } + + return $variation_attributes; +} + +/** + * Get all product cats for a product by ID, including hierarchy + * + * @since 2.5.0 + * @param int $product_id Product ID. + * @return array + */ +function wc_get_product_cat_ids( $product_id ) { + $product_cats = wc_get_product_term_ids( $product_id, 'product_cat' ); + + foreach ( $product_cats as $product_cat ) { + $product_cats = array_merge( $product_cats, get_ancestors( $product_cat, 'product_cat' ) ); + } + + return $product_cats; +} + +/** + * Gets data about an attachment, such as alt text and captions. + * + * @since 2.6.0 + * + * @param int|null $attachment_id Attachment ID. + * @param WC_Product|bool $product WC_Product object. + * + * @return array + */ +function wc_get_product_attachment_props( $attachment_id = null, $product = false ) { + $props = array( + 'title' => '', + 'caption' => '', + 'url' => '', + 'alt' => '', + 'src' => '', + 'srcset' => false, + 'sizes' => false, + ); + $attachment = get_post( $attachment_id ); + + if ( $attachment && 'attachment' === $attachment->post_type ) { + $props['title'] = wp_strip_all_tags( $attachment->post_title ); + $props['caption'] = wp_strip_all_tags( $attachment->post_excerpt ); + $props['url'] = wp_get_attachment_url( $attachment_id ); + + // Alt text. + $alt_text = array( wp_strip_all_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ), $props['caption'], wp_strip_all_tags( $attachment->post_title ) ); + + if ( $product && $product instanceof WC_Product ) { + $alt_text[] = wp_strip_all_tags( get_the_title( $product->get_id() ) ); + } + + $alt_text = array_filter( $alt_text ); + $props['alt'] = isset( $alt_text[0] ) ? $alt_text[0] : ''; + + // Large version. + $full_size = apply_filters( 'woocommerce_gallery_full_size', apply_filters( 'woocommerce_product_thumbnails_large_size', 'full' ) ); + $src = wp_get_attachment_image_src( $attachment_id, $full_size ); + $props['full_src'] = $src[0]; + $props['full_src_w'] = $src[1]; + $props['full_src_h'] = $src[2]; + + // Gallery thumbnail. + $gallery_thumbnail = wc_get_image_size( 'gallery_thumbnail' ); + $gallery_thumbnail_size = apply_filters( 'woocommerce_gallery_thumbnail_size', array( $gallery_thumbnail['width'], $gallery_thumbnail['height'] ) ); + $src = wp_get_attachment_image_src( $attachment_id, $gallery_thumbnail_size ); + $props['gallery_thumbnail_src'] = $src[0]; + $props['gallery_thumbnail_src_w'] = $src[1]; + $props['gallery_thumbnail_src_h'] = $src[2]; + + // Thumbnail version. + $thumbnail_size = apply_filters( 'woocommerce_thumbnail_size', 'woocommerce_thumbnail' ); + $src = wp_get_attachment_image_src( $attachment_id, $thumbnail_size ); + $props['thumb_src'] = $src[0]; + $props['thumb_src_w'] = $src[1]; + $props['thumb_src_h'] = $src[2]; + + // Image source. + $image_size = apply_filters( 'woocommerce_gallery_image_size', 'woocommerce_single' ); + $src = wp_get_attachment_image_src( $attachment_id, $image_size ); + $props['src'] = $src[0]; + $props['src_w'] = $src[1]; + $props['src_h'] = $src[2]; + $props['srcset'] = function_exists( 'wp_get_attachment_image_srcset' ) ? wp_get_attachment_image_srcset( $attachment_id, $image_size ) : false; + $props['sizes'] = function_exists( 'wp_get_attachment_image_sizes' ) ? wp_get_attachment_image_sizes( $attachment_id, $image_size ) : false; + } + return $props; +} + +/** + * Get product visibility options. + * + * @since 3.0.0 + * @return array + */ +function wc_get_product_visibility_options() { + return apply_filters( + 'woocommerce_product_visibility_options', + array( + 'visible' => __( 'Shop and search results', 'woocommerce' ), + 'catalog' => __( 'Shop only', 'woocommerce' ), + 'search' => __( 'Search results only', 'woocommerce' ), + 'hidden' => __( 'Hidden', 'woocommerce' ), + ) + ); +} + +/** + * Get product tax class options. + * + * @since 3.0.0 + * @return array + */ +function wc_get_product_tax_class_options() { + $tax_classes = WC_Tax::get_tax_classes(); + $tax_class_options = array(); + $tax_class_options[''] = __( 'Standard', 'woocommerce' ); + + if ( ! empty( $tax_classes ) ) { + foreach ( $tax_classes as $class ) { + $tax_class_options[ sanitize_title( $class ) ] = $class; + } + } + return $tax_class_options; +} + +/** + * Get stock status options. + * + * @since 3.0.0 + * @return array + */ +function wc_get_product_stock_status_options() { + return apply_filters( + 'woocommerce_product_stock_status_options', + array( + 'instock' => __( 'In stock', 'woocommerce' ), + 'outofstock' => __( 'Out of stock', 'woocommerce' ), + 'onbackorder' => __( 'On backorder', 'woocommerce' ), + ) + ); +} + +/** + * Get backorder options. + * + * @since 3.0.0 + * @return array + */ +function wc_get_product_backorder_options() { + return array( + 'no' => __( 'Do not allow', 'woocommerce' ), + 'notify' => __( 'Allow, but notify customer', 'woocommerce' ), + 'yes' => __( 'Allow', 'woocommerce' ), + ); +} + +/** + * Get related products based on product category and tags. + * + * @since 3.0.0 + * @param int $product_id Product ID. + * @param int $limit Limit of results. + * @param array $exclude_ids Exclude IDs from the results. + * @return array + */ +function wc_get_related_products( $product_id, $limit = 5, $exclude_ids = array() ) { + + $product_id = absint( $product_id ); + $limit = $limit >= -1 ? $limit : 5; + $exclude_ids = array_merge( array( 0, $product_id ), $exclude_ids ); + $transient_name = 'wc_related_' . $product_id; + $query_args = http_build_query( + array( + 'limit' => $limit, + 'exclude_ids' => $exclude_ids, + ) + ); + + $transient = get_transient( $transient_name ); + $related_posts = $transient && isset( $transient[ $query_args ] ) ? $transient[ $query_args ] : false; + + // We want to query related posts if they are not cached, or we don't have enough. + if ( false === $related_posts || count( $related_posts ) < $limit ) { + + $cats_array = apply_filters( 'woocommerce_product_related_posts_relate_by_category', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_cat_terms', wc_get_product_term_ids( $product_id, 'product_cat' ), $product_id ) : array(); + $tags_array = apply_filters( 'woocommerce_product_related_posts_relate_by_tag', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_tag_terms', wc_get_product_term_ids( $product_id, 'product_tag' ), $product_id ) : array(); + + // Don't bother if none are set, unless woocommerce_product_related_posts_force_display is set to true in which case all products are related. + if ( empty( $cats_array ) && empty( $tags_array ) && ! apply_filters( 'woocommerce_product_related_posts_force_display', false, $product_id ) ) { + $related_posts = array(); + } else { + $data_store = WC_Data_Store::load( 'product' ); + $related_posts = $data_store->get_related_products( $cats_array, $tags_array, $exclude_ids, $limit + 10, $product_id ); + } + + if ( $transient ) { + $transient[ $query_args ] = $related_posts; + } else { + $transient = array( $query_args => $related_posts ); + } + + set_transient( $transient_name, $transient, DAY_IN_SECONDS ); + } + + $related_posts = apply_filters( + 'woocommerce_related_products', + $related_posts, + $product_id, + array( + 'limit' => $limit, + 'excluded_ids' => $exclude_ids, + ) + ); + + if ( apply_filters( 'woocommerce_product_related_posts_shuffle', true ) ) { + shuffle( $related_posts ); + } + + return array_slice( $related_posts, 0, $limit ); +} + +/** + * Retrieves product term ids for a taxonomy. + * + * @since 3.0.0 + * @param int $product_id Product ID. + * @param string $taxonomy Taxonomy slug. + * @return array + */ +function wc_get_product_term_ids( $product_id, $taxonomy ) { + $terms = get_the_terms( $product_id, $taxonomy ); + return ( empty( $terms ) || is_wp_error( $terms ) ) ? array() : wp_list_pluck( $terms, 'term_id' ); +} + +/** + * For a given product, and optionally price/qty, work out the price with tax included, based on store settings. + * + * @since 3.0.0 + * @param WC_Product $product WC_Product object. + * @param array $args Optional arguments to pass product quantity and price. + * @return float|string Price with tax included, or an empty string if price calculation failed. + */ +function wc_get_price_including_tax( $product, $args = array() ) { + $args = wp_parse_args( + $args, + array( + 'qty' => '', + 'price' => '', + ) + ); + + $price = '' !== $args['price'] ? max( 0.0, (float) $args['price'] ) : $product->get_price(); + $qty = '' !== $args['qty'] ? max( 0.0, (float) $args['qty'] ) : 1; + + if ( '' === $price ) { + return ''; + } elseif ( empty( $qty ) ) { + return 0.0; + } + + $line_price = $price * $qty; + $return_price = $line_price; + + if ( $product->is_taxable() ) { + if ( ! wc_prices_include_tax() ) { + $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); + $taxes = WC_Tax::calc_tax( $line_price, $tax_rates, false ); + + if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { + $taxes_total = array_sum( $taxes ); + } else { + $taxes_total = array_sum( array_map( 'wc_round_tax_total', $taxes ) ); + } + + $return_price = NumberUtil::round( $line_price + $taxes_total, wc_get_price_decimals() ); + } else { + $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); + $base_tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) ); + + /** + * If the customer is excempt from VAT, remove the taxes here. + * Either remove the base or the user taxes depending on woocommerce_adjust_non_base_location_prices setting. + */ + if ( ! empty( WC()->customer ) && WC()->customer->get_is_vat_exempt() ) { // @codingStandardsIgnoreLine. + $remove_taxes = apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ? WC_Tax::calc_tax( $line_price, $base_tax_rates, true ) : WC_Tax::calc_tax( $line_price, $tax_rates, true ); + + if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { + $remove_taxes_total = array_sum( $remove_taxes ); + } else { + $remove_taxes_total = array_sum( array_map( 'wc_round_tax_total', $remove_taxes ) ); + } + + $return_price = NumberUtil::round( $line_price - $remove_taxes_total, wc_get_price_decimals() ); + + /** + * The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing with out of base locations. + * e.g. If a product costs 10 including tax, all users will pay 10 regardless of location and taxes. + * This feature is experimental @since 2.4.7 and may change in the future. Use at your risk. + */ + } elseif ( $tax_rates !== $base_tax_rates && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) { + $base_taxes = WC_Tax::calc_tax( $line_price, $base_tax_rates, true ); + $modded_taxes = WC_Tax::calc_tax( $line_price - array_sum( $base_taxes ), $tax_rates, false ); + + if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { + $base_taxes_total = array_sum( $base_taxes ); + $modded_taxes_total = array_sum( $modded_taxes ); + } else { + $base_taxes_total = array_sum( array_map( 'wc_round_tax_total', $base_taxes ) ); + $modded_taxes_total = array_sum( array_map( 'wc_round_tax_total', $modded_taxes ) ); + } + + $return_price = NumberUtil::round( $line_price - $base_taxes_total + $modded_taxes_total, wc_get_price_decimals() ); + } + } + } + return apply_filters( 'woocommerce_get_price_including_tax', $return_price, $qty, $product ); +} + +/** + * For a given product, and optionally price/qty, work out the price with tax excluded, based on store settings. + * + * @since 3.0.0 + * @param WC_Product $product WC_Product object. + * @param array $args Optional arguments to pass product quantity and price. + * @return float|string Price with tax excluded, or an empty string if price calculation failed. + */ +function wc_get_price_excluding_tax( $product, $args = array() ) { + $args = wp_parse_args( + $args, + array( + 'qty' => '', + 'price' => '', + ) + ); + + $price = '' !== $args['price'] ? max( 0.0, (float) $args['price'] ) : $product->get_price(); + $qty = '' !== $args['qty'] ? max( 0.0, (float) $args['qty'] ) : 1; + + if ( '' === $price ) { + return ''; + } elseif ( empty( $qty ) ) { + return 0.0; + } + + $line_price = $price * $qty; + + if ( $product->is_taxable() && wc_prices_include_tax() ) { + $order = ArrayUtil::get_value_or_default( $args, 'order' ); + $customer_id = $order ? $order->get_customer_id() : 0; + if ( apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) { + $tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) ); + } else { + $customer = $customer_id ? wc_get_container()->get( LegacyProxy::class )->get_instance_of( WC_Customer::class, $customer_id ) : null; + $tax_rates = WC_Tax::get_rates( $product->get_tax_class(), $customer ); + } + $remove_taxes = WC_Tax::calc_tax( $line_price, $tax_rates, true ); + $return_price = $line_price - array_sum( $remove_taxes ); // Unrounded since we're dealing with tax inclusive prices. Matches logic in cart-totals class. @see adjust_non_base_location_price. + } else { + $return_price = $line_price; + } + + return apply_filters( 'woocommerce_get_price_excluding_tax', $return_price, $qty, $product ); +} + +/** + * Returns the price including or excluding tax, based on the 'woocommerce_tax_display_shop' setting. + * + * @since 3.0.0 + * @param WC_Product $product WC_Product object. + * @param array $args Optional arguments to pass product quantity and price. + * @return float + */ +function wc_get_price_to_display( $product, $args = array() ) { + $args = wp_parse_args( + $args, + array( + 'qty' => 1, + 'price' => $product->get_price(), + ) + ); + + $price = $args['price']; + $qty = $args['qty']; + + return 'incl' === get_option( 'woocommerce_tax_display_shop' ) ? + wc_get_price_including_tax( + $product, + array( + 'qty' => $qty, + 'price' => $price, + ) + ) : + wc_get_price_excluding_tax( + $product, + array( + 'qty' => $qty, + 'price' => $price, + ) + ); +} + +/** + * Returns the product categories in a list. + * + * @param int $product_id Product ID. + * @param string $sep (default: ', '). + * @param string $before (default: ''). + * @param string $after (default: ''). + * @return string + */ +function wc_get_product_category_list( $product_id, $sep = ', ', $before = '', $after = '' ) { + return get_the_term_list( $product_id, 'product_cat', $before, $sep, $after ); +} + +/** + * Returns the product tags in a list. + * + * @param int $product_id Product ID. + * @param string $sep (default: ', '). + * @param string $before (default: ''). + * @param string $after (default: ''). + * @return string + */ +function wc_get_product_tag_list( $product_id, $sep = ', ', $before = '', $after = '' ) { + return get_the_term_list( $product_id, 'product_tag', $before, $sep, $after ); +} + +/** + * Callback for array filter to get visible only. + * + * @since 3.0.0 + * @param WC_Product $product WC_Product object. + * @return bool + */ +function wc_products_array_filter_visible( $product ) { + return $product && is_a( $product, 'WC_Product' ) && $product->is_visible(); +} + +/** + * Callback for array filter to get visible grouped products only. + * + * @since 3.1.0 + * @param WC_Product $product WC_Product object. + * @return bool + */ +function wc_products_array_filter_visible_grouped( $product ) { + return $product && is_a( $product, 'WC_Product' ) && ( 'publish' === $product->get_status() || current_user_can( 'edit_product', $product->get_id() ) ); +} + +/** + * Callback for array filter to get products the user can edit only. + * + * @since 3.0.0 + * @param WC_Product $product WC_Product object. + * @return bool + */ +function wc_products_array_filter_editable( $product ) { + return $product && is_a( $product, 'WC_Product' ) && current_user_can( 'edit_product', $product->get_id() ); +} + +/** + * Callback for array filter to get products the user can view only. + * + * @since 3.4.0 + * @param WC_Product $product WC_Product object. + * @return bool + */ +function wc_products_array_filter_readable( $product ) { + return $product && is_a( $product, 'WC_Product' ) && current_user_can( 'read_product', $product->get_id() ); +} + +/** + * Sort an array of products by a value. + * + * @since 3.0.0 + * + * @param array $products List of products to be ordered. + * @param string $orderby Optional order criteria. + * @param string $order Ascending or descending order. + * + * @return array + */ +function wc_products_array_orderby( $products, $orderby = 'date', $order = 'desc' ) { + $orderby = strtolower( $orderby ); + $order = strtolower( $order ); + switch ( $orderby ) { + case 'title': + case 'id': + case 'date': + case 'modified': + case 'menu_order': + case 'price': + usort( $products, 'wc_products_array_orderby_' . $orderby ); + break; + case 'none': + break; + default: + shuffle( $products ); + break; + } + if ( 'desc' === $order ) { + $products = array_reverse( $products ); + } + return $products; +} + +/** + * Sort by title. + * + * @since 3.0.0 + * @param WC_Product $a First WC_Product object. + * @param WC_Product $b Second WC_Product object. + * @return int + */ +function wc_products_array_orderby_title( $a, $b ) { + return strcasecmp( $a->get_name(), $b->get_name() ); +} + +/** + * Sort by id. + * + * @since 3.0.0 + * @param WC_Product $a First WC_Product object. + * @param WC_Product $b Second WC_Product object. + * @return int + */ +function wc_products_array_orderby_id( $a, $b ) { + if ( $a->get_id() === $b->get_id() ) { + return 0; + } + return ( $a->get_id() < $b->get_id() ) ? -1 : 1; +} + +/** + * Sort by date. + * + * @since 3.0.0 + * @param WC_Product $a First WC_Product object. + * @param WC_Product $b Second WC_Product object. + * @return int + */ +function wc_products_array_orderby_date( $a, $b ) { + if ( $a->get_date_created() === $b->get_date_created() ) { + return 0; + } + return ( $a->get_date_created() < $b->get_date_created() ) ? -1 : 1; +} + +/** + * Sort by modified. + * + * @since 3.0.0 + * @param WC_Product $a First WC_Product object. + * @param WC_Product $b Second WC_Product object. + * @return int + */ +function wc_products_array_orderby_modified( $a, $b ) { + if ( $a->get_date_modified() === $b->get_date_modified() ) { + return 0; + } + return ( $a->get_date_modified() < $b->get_date_modified() ) ? -1 : 1; +} + +/** + * Sort by menu order. + * + * @since 3.0.0 + * @param WC_Product $a First WC_Product object. + * @param WC_Product $b Second WC_Product object. + * @return int + */ +function wc_products_array_orderby_menu_order( $a, $b ) { + if ( $a->get_menu_order() === $b->get_menu_order() ) { + return 0; + } + return ( $a->get_menu_order() < $b->get_menu_order() ) ? -1 : 1; +} + +/** + * Sort by price low to high. + * + * @since 3.0.0 + * @param WC_Product $a First WC_Product object. + * @param WC_Product $b Second WC_Product object. + * @return int + */ +function wc_products_array_orderby_price( $a, $b ) { + if ( $a->get_price() === $b->get_price() ) { + return 0; + } + return ( $a->get_price() < $b->get_price() ) ? -1 : 1; +} + +/** + * Queue a product for syncing at the end of the request. + * + * @param int $product_id Product ID. + */ +function wc_deferred_product_sync( $product_id ) { + global $wc_deferred_product_sync; + + if ( empty( $wc_deferred_product_sync ) ) { + $wc_deferred_product_sync = array(); + } + + $wc_deferred_product_sync[] = $product_id; +} + +/** + * See if the lookup table is being generated already. + * + * @since 3.6.0 + * @return bool + */ +function wc_update_product_lookup_tables_is_running() { + $table_updates_pending = WC()->queue()->search( + array( + 'status' => 'pending', + 'group' => 'wc_update_product_lookup_tables', + 'per_page' => 1, + ) + ); + + return (bool) count( $table_updates_pending ); +} + +/** + * Populate lookup table data for products. + * + * @since 3.6.0 + */ +function wc_update_product_lookup_tables() { + global $wpdb; + + $is_cli = Constants::is_true( 'WP_CLI' ); + + if ( ! $is_cli ) { + WC_Admin_Notices::add_notice( 'regenerating_lookup_table' ); + } + + // Note that the table is not yet generated. + update_option( 'woocommerce_product_lookup_table_is_generating', true ); + + // Make a row per product in lookup table. + $wpdb->query( + " + INSERT IGNORE INTO {$wpdb->wc_product_meta_lookup} (`product_id`) + SELECT + posts.ID + FROM {$wpdb->posts} posts + WHERE + posts.post_type IN ('product', 'product_variation') + " + ); + + // List of column names in the lookup table we need to populate. + $columns = array( + 'min_max_price', + 'stock_quantity', + 'sku', + 'stock_status', + 'average_rating', + 'total_sales', + 'downloadable', + 'virtual', + 'onsale', + 'tax_class', + 'tax_status', // When last column is updated, woocommerce_product_lookup_table_is_generating is updated. + ); + + foreach ( $columns as $index => $column ) { + if ( $is_cli ) { + wc_update_product_lookup_tables_column( $column ); + } else { + WC()->queue()->schedule_single( + time() + $index, + 'wc_update_product_lookup_tables_column', + array( + 'column' => $column, + ), + 'wc_update_product_lookup_tables' + ); + } + } + + // Rating counts are serialised so they have to be unserialised before populating the lookup table. + if ( $is_cli ) { + $rating_count_rows = $wpdb->get_results( + " + SELECT post_id, meta_value FROM {$wpdb->postmeta} + WHERE meta_key = '_wc_rating_count' + AND meta_value != '' + AND meta_value != 'a:0:{}' + ", + ARRAY_A + ); + wc_update_product_lookup_tables_rating_count( $rating_count_rows ); + } else { + WC()->queue()->schedule_single( + time() + 10, + 'wc_update_product_lookup_tables_rating_count_batch', + array( + 'offset' => 0, + 'limit' => 50, + ), + 'wc_update_product_lookup_tables' + ); + } +} + +/** + * Populate lookup table column data. + * + * @since 3.6.0 + * @param string $column Column name to set. + */ +function wc_update_product_lookup_tables_column( $column ) { + if ( empty( $column ) ) { + return; + } + global $wpdb; + switch ( $column ) { + case 'min_max_price': + $wpdb->query( + " + UPDATE + {$wpdb->wc_product_meta_lookup} lookup_table + INNER JOIN ( + SELECT lookup_table.product_id, MIN( meta_value+0 ) as min_price, MAX( meta_value+0 ) as max_price + FROM {$wpdb->wc_product_meta_lookup} lookup_table + LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_price' + WHERE + meta1.meta_value <> '' + GROUP BY lookup_table.product_id + ) as source on source.product_id = lookup_table.product_id + SET + lookup_table.min_price = source.min_price, + lookup_table.max_price = source.max_price + " + ); + break; + case 'stock_quantity': + $wpdb->query( + " + UPDATE + {$wpdb->wc_product_meta_lookup} lookup_table + LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_manage_stock' + LEFT JOIN {$wpdb->postmeta} meta2 ON lookup_table.product_id = meta2.post_id AND meta2.meta_key = '_stock' + SET + lookup_table.stock_quantity = meta2.meta_value + WHERE + meta1.meta_value = 'yes' + " + ); + break; + case 'sku': + case 'stock_status': + case 'average_rating': + case 'total_sales': + case 'tax_class': + case 'tax_status': + if ( 'total_sales' === $column ) { + $meta_key = 'total_sales'; + } elseif ( 'average_rating' === $column ) { + $meta_key = '_wc_average_rating'; + } else { + $meta_key = '_' . $column; + } + $column = esc_sql( $column ); + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $wpdb->query( + $wpdb->prepare( + " + UPDATE + {$wpdb->wc_product_meta_lookup} lookup_table + LEFT JOIN {$wpdb->postmeta} meta ON lookup_table.product_id = meta.post_id AND meta.meta_key = %s + SET + lookup_table.`{$column}` = meta.meta_value + ", + $meta_key + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + break; + case 'downloadable': + case 'virtual': + $column = esc_sql( $column ); + $meta_key = '_' . $column; + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $wpdb->query( + $wpdb->prepare( + " + UPDATE + {$wpdb->wc_product_meta_lookup} lookup_table + LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = %s + SET + lookup_table.`{$column}` = IF ( meta1.meta_value = 'yes', 1, 0 ) + ", + $meta_key + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + break; + case 'onsale': + $column = esc_sql( $column ); + $decimals = absint( wc_get_price_decimals() ); + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $wpdb->query( + $wpdb->prepare( + " + UPDATE + {$wpdb->wc_product_meta_lookup} lookup_table + LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_price' + LEFT JOIN {$wpdb->postmeta} meta2 ON lookup_table.product_id = meta2.post_id AND meta2.meta_key = '_sale_price' + SET + lookup_table.`{$column}` = IF ( + CAST( meta1.meta_value AS DECIMAL ) >= 0 + AND CAST( meta2.meta_value AS CHAR ) != '' + AND CAST( meta1.meta_value AS DECIMAL( 10, %d ) ) = CAST( meta2.meta_value AS DECIMAL( 10, %d ) ) + , 1, 0 ) + ", + $decimals, + $decimals + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + break; + } + + // Final column - mark complete. + if ( 'tax_status' === $column ) { + delete_option( 'woocommerce_product_lookup_table_is_generating' ); + } +} +add_action( 'wc_update_product_lookup_tables_column', 'wc_update_product_lookup_tables_column' ); + +/** + * Populate rating count lookup table data for products. + * + * @since 3.6.0 + * @param array $rows Rows of rating counts to update in lookup table. + */ +function wc_update_product_lookup_tables_rating_count( $rows ) { + if ( ! $rows || ! is_array( $rows ) ) { + return; + } + global $wpdb; + + foreach ( $rows as $row ) { + $count = array_sum( (array) maybe_unserialize( $row['meta_value'] ) ); + $wpdb->update( + $wpdb->wc_product_meta_lookup, + array( + 'rating_count' => absint( $count ), + ), + array( + 'product_id' => absint( $row['post_id'] ), + ) + ); + } +} + +/** + * Populate a batch of rating count lookup table data for products. + * + * @since 3.6.2 + * @param array $offset Offset to query. + * @param array $limit Limit to query. + */ +function wc_update_product_lookup_tables_rating_count_batch( $offset = 0, $limit = 0 ) { + global $wpdb; + + if ( ! $limit ) { + return; + } + + $rating_count_rows = $wpdb->get_results( + $wpdb->prepare( + " + SELECT post_id, meta_value FROM {$wpdb->postmeta} + WHERE meta_key = '_wc_rating_count' + AND meta_value != '' + AND meta_value != 'a:0:{}' + ORDER BY post_id ASC + LIMIT %d, %d + ", + $offset, + $limit + ), + ARRAY_A + ); + + if ( $rating_count_rows ) { + wc_update_product_lookup_tables_rating_count( $rating_count_rows ); + WC()->queue()->schedule_single( + time() + 1, + 'wc_update_product_lookup_tables_rating_count_batch', + array( + 'offset' => $offset + $limit, + 'limit' => $limit, + ), + 'wc_update_product_lookup_tables' + ); + } +} +add_action( 'wc_update_product_lookup_tables_rating_count_batch', 'wc_update_product_lookup_tables_rating_count_batch', 10, 2 ); diff --git a/plugins/woocommerce/includes/wc-rest-functions.php b/plugins/woocommerce/includes/wc-rest-functions.php new file mode 100644 index 00000000000..3337c071b40 --- /dev/null +++ b/plugins/woocommerce/includes/wc-rest-functions.php @@ -0,0 +1,364 @@ +setTimezone( new DateTimeZone( wc_timezone_string() ) ); + } elseif ( is_string( $date ) ) { + $date = new WC_DateTime( $date, new DateTimeZone( 'UTC' ) ); + $date->setTimezone( new DateTimeZone( wc_timezone_string() ) ); + } + + if ( ! is_a( $date, 'WC_DateTime' ) ) { + return null; + } + + // Get timestamp before changing timezone to UTC. + return gmdate( 'Y-m-d\TH:i:s', $utc ? $date->getTimestamp() : $date->getOffsetTimestamp() ); +} + +/** + * Returns image mime types users are allowed to upload via the API. + * + * @since 2.6.4 + * @return array + */ +function wc_rest_allowed_image_mime_types() { + return apply_filters( + 'woocommerce_rest_allowed_image_mime_types', + array( + 'jpg|jpeg|jpe' => 'image/jpeg', + 'gif' => 'image/gif', + 'png' => 'image/png', + 'bmp' => 'image/bmp', + 'tiff|tif' => 'image/tiff', + 'ico' => 'image/x-icon', + ) + ); +} + +/** + * Upload image from URL. + * + * @since 2.6.0 + * @param string $image_url Image URL. + * @return array|WP_Error Attachment data or error message. + */ +function wc_rest_upload_image_from_url( $image_url ) { + $parsed_url = wp_parse_url( $image_url ); + + // Check parsed URL. + if ( ! $parsed_url || ! is_array( $parsed_url ) ) { + /* translators: %s: image URL */ + return new WP_Error( 'woocommerce_rest_invalid_image_url', sprintf( __( 'Invalid URL %s.', 'woocommerce' ), $image_url ), array( 'status' => 400 ) ); + } + + // Ensure url is valid. + $image_url = esc_url_raw( $image_url ); + + // download_url function is part of wp-admin. + if ( ! function_exists( 'download_url' ) ) { + include_once ABSPATH . 'wp-admin/includes/file.php'; + } + + $file_array = array(); + $file_array['name'] = basename( current( explode( '?', $image_url ) ) ); + + // Download file to temp location. + $file_array['tmp_name'] = download_url( $image_url ); + + // If error storing temporarily, return the error. + if ( is_wp_error( $file_array['tmp_name'] ) ) { + return new WP_Error( + 'woocommerce_rest_invalid_remote_image_url', + /* translators: %s: image URL */ + sprintf( __( 'Error getting remote image %s.', 'woocommerce' ), $image_url ) . ' ' + /* translators: %s: error message */ + . sprintf( __( 'Error: %s', 'woocommerce' ), $file_array['tmp_name']->get_error_message() ), + array( 'status' => 400 ) + ); + } + + // Do the validation and storage stuff. + $file = wp_handle_sideload( + $file_array, + array( + 'test_form' => false, + 'mimes' => wc_rest_allowed_image_mime_types(), + ), + current_time( 'Y/m' ) + ); + + if ( isset( $file['error'] ) ) { + @unlink( $file_array['tmp_name'] ); // @codingStandardsIgnoreLine. + + /* translators: %s: error message */ + return new WP_Error( 'woocommerce_rest_invalid_image', sprintf( __( 'Invalid image: %s', 'woocommerce' ), $file['error'] ), array( 'status' => 400 ) ); + } + + do_action( 'woocommerce_rest_api_uploaded_image_from_url', $file, $image_url ); + + return $file; +} + +/** + * Set uploaded image as attachment. + * + * @since 2.6.0 + * @param array $upload Upload information from wp_upload_bits. + * @param int $id Post ID. Default to 0. + * @return int Attachment ID + */ +function wc_rest_set_uploaded_image_as_attachment( $upload, $id = 0 ) { + $info = wp_check_filetype( $upload['file'] ); + $title = ''; + $content = ''; + + if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) { + include_once ABSPATH . 'wp-admin/includes/image.php'; + } + + $image_meta = wp_read_image_metadata( $upload['file'] ); + if ( $image_meta ) { + if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { + $title = wc_clean( $image_meta['title'] ); + } + if ( trim( $image_meta['caption'] ) ) { + $content = wc_clean( $image_meta['caption'] ); + } + } + + $attachment = array( + 'post_mime_type' => $info['type'], + 'guid' => $upload['url'], + 'post_parent' => $id, + 'post_title' => $title ? $title : basename( $upload['file'] ), + 'post_content' => $content, + ); + + $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id ); + if ( ! is_wp_error( $attachment_id ) ) { + wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); + } + + return $attachment_id; +} + +/** + * Validate reports request arguments. + * + * @since 2.6.0 + * @param mixed $value Value to validate. + * @param WP_REST_Request $request Request instance. + * @param string $param Param to validate. + * @return WP_Error|boolean + */ +function wc_rest_validate_reports_request_arg( $value, $request, $param ) { + + $attributes = $request->get_attributes(); + if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) { + return true; + } + $args = $attributes['args'][ $param ]; + + if ( 'string' === $args['type'] && ! is_string( $value ) ) { + /* translators: 1: param 2: type */ + return new WP_Error( 'woocommerce_rest_invalid_param', sprintf( __( '%1$s is not of type %2$s', 'woocommerce' ), $param, 'string' ) ); + } + + if ( 'date' === $args['format'] ) { + $regex = '#^\d{4}-\d{2}-\d{2}$#'; + + if ( ! preg_match( $regex, $value, $matches ) ) { + return new WP_Error( 'woocommerce_rest_invalid_date', __( 'The date you provided is invalid.', 'woocommerce' ) ); + } + } + + return true; +} + +/** + * Encodes a value according to RFC 3986. + * Supports multidimensional arrays. + * + * @since 2.6.0 + * @param string|array $value The value to encode. + * @return string|array Encoded values. + */ +function wc_rest_urlencode_rfc3986( $value ) { + if ( is_array( $value ) ) { + return array_map( 'wc_rest_urlencode_rfc3986', $value ); + } + + return str_replace( array( '+', '%7E' ), array( ' ', '~' ), rawurlencode( $value ) ); +} + +/** + * Check permissions of posts on REST API. + * + * @since 2.6.0 + * @param string $post_type Post type. + * @param string $context Request context. + * @param int $object_id Post ID. + * @return bool + */ +function wc_rest_check_post_permissions( $post_type, $context = 'read', $object_id = 0 ) { + $contexts = array( + 'read' => 'read_private_posts', + 'create' => 'publish_posts', + 'edit' => 'edit_post', + 'delete' => 'delete_post', + 'batch' => 'edit_others_posts', + ); + + if ( 'revision' === $post_type ) { + $permission = false; + } else { + $cap = $contexts[ $context ]; + $post_type_object = get_post_type_object( $post_type ); + $permission = current_user_can( $post_type_object->cap->$cap, $object_id ); + } + + return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, $post_type ); +} + +/** + * Check permissions of users on REST API. + * + * @since 2.6.0 + * @param string $context Request context. + * @param int $object_id Post ID. + * @return bool + */ +function wc_rest_check_user_permissions( $context = 'read', $object_id = 0 ) { + $contexts = array( + 'read' => 'list_users', + 'create' => 'promote_users', // Check if current user can create users, shop managers are not allowed to create users. + 'edit' => 'edit_users', + 'delete' => 'delete_users', + 'batch' => 'promote_users', + ); + + // Check to allow shop_managers to manage only customers. + if ( in_array( $context, array( 'edit', 'delete' ), true ) && wc_current_user_has_role( 'shop_manager' ) ) { + $permission = false; + $user_data = get_userdata( $object_id ); + $shop_manager_editable_roles = apply_filters( 'woocommerce_shop_manager_editable_roles', array( 'customer' ) ); + + if ( isset( $user_data->roles ) ) { + $can_manage_users = array_intersect( $user_data->roles, array_unique( $shop_manager_editable_roles ) ); + + // Check if Shop Manager can edit customer or with the is same shop manager. + if ( 0 < count( $can_manage_users ) || intval( $object_id ) === intval( get_current_user_id() ) ) { + $permission = current_user_can( $contexts[ $context ], $object_id ); + } + } + } else { + $permission = current_user_can( $contexts[ $context ], $object_id ); + } + + return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, 'user' ); +} + +/** + * Check permissions of product terms on REST API. + * + * @since 2.6.0 + * @param string $taxonomy Taxonomy. + * @param string $context Request context. + * @param int $object_id Post ID. + * @return bool + */ +function wc_rest_check_product_term_permissions( $taxonomy, $context = 'read', $object_id = 0 ) { + $contexts = array( + 'read' => 'manage_terms', + 'create' => 'edit_terms', + 'edit' => 'edit_terms', + 'delete' => 'delete_terms', + 'batch' => 'edit_terms', + ); + + $cap = $contexts[ $context ]; + $taxonomy_object = get_taxonomy( $taxonomy ); + $permission = current_user_can( $taxonomy_object->cap->$cap, $object_id ); + + return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, $taxonomy ); +} + +/** + * Check manager permissions on REST API. + * + * @since 2.6.0 + * @param string $object Object. + * @param string $context Request context. + * @return bool + */ +function wc_rest_check_manager_permissions( $object, $context = 'read' ) { + $objects = array( + 'reports' => 'view_woocommerce_reports', + 'settings' => 'manage_woocommerce', + 'system_status' => 'manage_woocommerce', + 'attributes' => 'manage_product_terms', + 'shipping_methods' => 'manage_woocommerce', + 'payment_gateways' => 'manage_woocommerce', + 'webhooks' => 'manage_woocommerce', + ); + + $permission = current_user_can( $objects[ $object ] ); + + return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, 0, $object ); +} + +/** + * Check product reviews permissions on REST API. + * + * @since 3.5.0 + * @param string $context Request context. + * @param string $object_id Object ID. + * @return bool + */ +function wc_rest_check_product_reviews_permissions( $context = 'read', $object_id = 0 ) { + $permission = false; + $contexts = array( + 'read' => 'moderate_comments', + 'create' => 'edit_products', + 'edit' => 'edit_products', + 'delete' => 'edit_products', + 'batch' => 'edit_products', + ); + + if ( $object_id > 0 ) { + $object = get_comment( $object_id ); + + if ( ! is_a( $object, 'WP_Comment' ) || get_comment_type( $object ) !== 'review' ) { + return false; + } + } + + if ( isset( $contexts[ $context ] ) ) { + $permission = current_user_can( $contexts[ $context ], $object_id ); + } + + return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, 'product_review' ); +} diff --git a/plugins/woocommerce/includes/wc-stock-functions.php b/plugins/woocommerce/includes/wc-stock-functions.php new file mode 100644 index 00000000000..4a836dd07df --- /dev/null +++ b/plugins/woocommerce/includes/wc-stock-functions.php @@ -0,0 +1,415 @@ +managing_stock() ) { + // Some products (variations) can have their stock managed by their parent. Get the correct object to be updated here. + $product_id_with_stock = $product->get_stock_managed_by_id(); + $product_with_stock = $product_id_with_stock !== $product->get_id() ? wc_get_product( $product_id_with_stock ) : $product; + $data_store = WC_Data_Store::load( 'product' ); + + // Fire actions to let 3rd parties know the stock is about to be changed. + if ( $product_with_stock->is_type( 'variation' ) ) { + do_action( 'woocommerce_variation_before_set_stock', $product_with_stock ); + } else { + do_action( 'woocommerce_product_before_set_stock', $product_with_stock ); + } + + // Update the database. + $new_stock = $data_store->update_product_stock( $product_id_with_stock, $stock_quantity, $operation ); + + // Update the product object. + $data_store->read_stock_quantity( $product_with_stock, $new_stock ); + + // If this is not being called during an update routine, save the product so stock status etc is in sync, and caches are cleared. + if ( ! $updating ) { + $product_with_stock->save(); + } + + // Fire actions to let 3rd parties know the stock changed. + if ( $product_with_stock->is_type( 'variation' ) ) { + do_action( 'woocommerce_variation_set_stock', $product_with_stock ); + } else { + do_action( 'woocommerce_product_set_stock', $product_with_stock ); + } + + return $product_with_stock->get_stock_quantity(); + } + return $product->get_stock_quantity(); +} + +/** + * Update a product's stock status. + * + * @param int $product_id Product ID. + * @param string $status Status. + */ +function wc_update_product_stock_status( $product_id, $status ) { + $product = wc_get_product( $product_id ); + + if ( $product ) { + $product->set_stock_status( $status ); + $product->save(); + } +} + +/** + * When a payment is complete, we can reduce stock levels for items within an order. + * + * @since 3.0.0 + * @param int $order_id Order ID. + */ +function wc_maybe_reduce_stock_levels( $order_id ) { + $order = wc_get_order( $order_id ); + + if ( ! $order ) { + return; + } + + $stock_reduced = $order->get_data_store()->get_stock_reduced( $order_id ); + $trigger_reduce = apply_filters( 'woocommerce_payment_complete_reduce_order_stock', ! $stock_reduced, $order_id ); + + // Only continue if we're reducing stock. + if ( ! $trigger_reduce ) { + return; + } + + wc_reduce_stock_levels( $order ); + + // Ensure stock is marked as "reduced" in case payment complete or other stock actions are called. + $order->get_data_store()->set_stock_reduced( $order_id, true ); +} +add_action( 'woocommerce_payment_complete', 'wc_maybe_reduce_stock_levels' ); +add_action( 'woocommerce_order_status_completed', 'wc_maybe_reduce_stock_levels' ); +add_action( 'woocommerce_order_status_processing', 'wc_maybe_reduce_stock_levels' ); +add_action( 'woocommerce_order_status_on-hold', 'wc_maybe_reduce_stock_levels' ); + +/** + * When a payment is cancelled, restore stock. + * + * @since 3.0.0 + * @param int $order_id Order ID. + */ +function wc_maybe_increase_stock_levels( $order_id ) { + $order = wc_get_order( $order_id ); + + if ( ! $order ) { + return; + } + + $stock_reduced = $order->get_data_store()->get_stock_reduced( $order_id ); + $trigger_increase = (bool) $stock_reduced; + + // Only continue if we're increasing stock. + if ( ! $trigger_increase ) { + return; + } + + wc_increase_stock_levels( $order ); + + // Ensure stock is not marked as "reduced" anymore. + $order->get_data_store()->set_stock_reduced( $order_id, false ); +} +add_action( 'woocommerce_order_status_cancelled', 'wc_maybe_increase_stock_levels' ); +add_action( 'woocommerce_order_status_pending', 'wc_maybe_increase_stock_levels' ); + +/** + * Reduce stock levels for items within an order, if stock has not already been reduced for the items. + * + * @since 3.0.0 + * @param int|WC_Order $order_id Order ID or order instance. + */ +function wc_reduce_stock_levels( $order_id ) { + if ( is_a( $order_id, 'WC_Order' ) ) { + $order = $order_id; + $order_id = $order->get_id(); + } else { + $order = wc_get_order( $order_id ); + } + // We need an order, and a store with stock management to continue. + if ( ! $order || 'yes' !== get_option( 'woocommerce_manage_stock' ) || ! apply_filters( 'woocommerce_can_reduce_order_stock', true, $order ) ) { + return; + } + + $changes = array(); + + // Loop over all items. + foreach ( $order->get_items() as $item ) { + if ( ! $item->is_type( 'line_item' ) ) { + continue; + } + + // Only reduce stock once for each item. + $product = $item->get_product(); + $item_stock_reduced = $item->get_meta( '_reduced_stock', true ); + + if ( $item_stock_reduced || ! $product || ! $product->managing_stock() ) { + continue; + } + + /** + * Filter order item quantity. + * + * @param int|float $quantity Quantity. + * @param WC_Order $order Order data. + * @param WC_Order_Item_Product $item Order item data. + */ + $qty = apply_filters( 'woocommerce_order_item_quantity', $item->get_quantity(), $order, $item ); + $item_name = $product->get_formatted_name(); + $new_stock = wc_update_product_stock( $product, $qty, 'decrease' ); + + if ( is_wp_error( $new_stock ) ) { + /* translators: %s item name. */ + $order->add_order_note( sprintf( __( 'Unable to reduce stock for item %s.', 'woocommerce' ), $item_name ) ); + continue; + } + + $item->add_meta_data( '_reduced_stock', $qty, true ); + $item->save(); + + $changes[] = array( + 'product' => $product, + 'from' => $new_stock + $qty, + 'to' => $new_stock, + ); + } + + wc_trigger_stock_change_notifications( $order, $changes ); + + do_action( 'woocommerce_reduce_order_stock', $order ); +} + +/** + * After stock change events, triggers emails and adds order notes. + * + * @since 3.5.0 + * @param WC_Order $order order object. + * @param array $changes Array of changes. + */ +function wc_trigger_stock_change_notifications( $order, $changes ) { + if ( empty( $changes ) ) { + return; + } + + $order_notes = array(); + $no_stock_amount = absint( get_option( 'woocommerce_notify_no_stock_amount', 0 ) ); + + foreach ( $changes as $change ) { + $order_notes[] = $change['product']->get_formatted_name() . ' ' . $change['from'] . '→' . $change['to']; + $low_stock_amount = absint( wc_get_low_stock_amount( wc_get_product( $change['product']->get_id() ) ) ); + if ( $change['to'] <= $no_stock_amount ) { + do_action( 'woocommerce_no_stock', wc_get_product( $change['product']->get_id() ) ); + } elseif ( $change['to'] <= $low_stock_amount ) { + do_action( 'woocommerce_low_stock', wc_get_product( $change['product']->get_id() ) ); + } + + if ( $change['to'] < 0 ) { + do_action( + 'woocommerce_product_on_backorder', + array( + 'product' => wc_get_product( $change['product']->get_id() ), + 'order_id' => $order->get_id(), + 'quantity' => abs( $change['from'] - $change['to'] ), + ) + ); + } + } + + $order->add_order_note( __( 'Stock levels reduced:', 'woocommerce' ) . ' ' . implode( ', ', $order_notes ) ); +} + +/** + * Increase stock levels for items within an order. + * + * @since 3.0.0 + * @param int|WC_Order $order_id Order ID or order instance. + */ +function wc_increase_stock_levels( $order_id ) { + if ( is_a( $order_id, 'WC_Order' ) ) { + $order = $order_id; + $order_id = $order->get_id(); + } else { + $order = wc_get_order( $order_id ); + } + + // We need an order, and a store with stock management to continue. + if ( ! $order || 'yes' !== get_option( 'woocommerce_manage_stock' ) || ! apply_filters( 'woocommerce_can_restore_order_stock', true, $order ) ) { + return; + } + + $changes = array(); + + // Loop over all items. + foreach ( $order->get_items() as $item ) { + if ( ! $item->is_type( 'line_item' ) ) { + continue; + } + + // Only increase stock once for each item. + $product = $item->get_product(); + $item_stock_reduced = $item->get_meta( '_reduced_stock', true ); + + if ( ! $item_stock_reduced || ! $product || ! $product->managing_stock() ) { + continue; + } + + $item_name = $product->get_formatted_name(); + $new_stock = wc_update_product_stock( $product, $item_stock_reduced, 'increase' ); + + if ( is_wp_error( $new_stock ) ) { + /* translators: %s item name. */ + $order->add_order_note( sprintf( __( 'Unable to restore stock for item %s.', 'woocommerce' ), $item_name ) ); + continue; + } + + $item->delete_meta_data( '_reduced_stock' ); + $item->save(); + + $changes[] = $item_name . ' ' . ( $new_stock - $item_stock_reduced ) . '→' . $new_stock; + } + + if ( $changes ) { + $order->add_order_note( __( 'Stock levels increased:', 'woocommerce' ) . ' ' . implode( ', ', $changes ) ); + } + + do_action( 'woocommerce_restore_order_stock', $order ); +} + +/** + * See how much stock is being held in pending orders. + * + * @since 3.5.0 + * @param WC_Product $product Product to check. + * @param integer $exclude_order_id Order ID to exclude. + * @return int + */ +function wc_get_held_stock_quantity( WC_Product $product, $exclude_order_id = 0 ) { + /** + * Filter: woocommerce_hold_stock_for_checkout + * Allows enable/disable hold stock functionality on checkout. + * + * @since 4.3.0 + * @param bool $enabled Default to true if managing stock globally. + */ + if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) { + return 0; + } + + return ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->get_reserved_stock( $product, $exclude_order_id ); +} + +/** + * Hold stock for an order. + * + * @throws ReserveStockException If reserve stock fails. + * + * @since 4.1.0 + * @param \WC_Order|int $order Order ID or instance. + */ +function wc_reserve_stock_for_order( $order ) { + /** + * Filter: woocommerce_hold_stock_for_checkout + * Allows enable/disable hold stock functionality on checkout. + * + * @since @since 4.1.0 + * @param bool $enabled Default to true if managing stock globally. + */ + if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) { + return; + } + + $order = $order instanceof WC_Order ? $order : wc_get_order( $order ); + + if ( $order ) { + ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->reserve_stock_for_order( $order ); + } +} +add_action( 'woocommerce_checkout_order_created', 'wc_reserve_stock_for_order' ); + +/** + * Release held stock for an order. + * + * @since 4.3.0 + * @param \WC_Order|int $order Order ID or instance. + */ +function wc_release_stock_for_order( $order ) { + /** + * Filter: woocommerce_hold_stock_for_checkout + * Allows enable/disable hold stock functionality on checkout. + * + * @since 4.3.0 + * @param bool $enabled Default to true if managing stock globally. + */ + if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) { + return; + } + + $order = $order instanceof WC_Order ? $order : wc_get_order( $order ); + + if ( $order ) { + ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->release_stock_for_order( $order ); + } +} +add_action( 'woocommerce_checkout_order_exception', 'wc_release_stock_for_order' ); +add_action( 'woocommerce_payment_complete', 'wc_release_stock_for_order', 11 ); +add_action( 'woocommerce_order_status_cancelled', 'wc_release_stock_for_order', 11 ); +add_action( 'woocommerce_order_status_completed', 'wc_release_stock_for_order', 11 ); +add_action( 'woocommerce_order_status_processing', 'wc_release_stock_for_order', 11 ); +add_action( 'woocommerce_order_status_on-hold', 'wc_release_stock_for_order', 11 ); + +/** + * Return low stock amount to determine if notification needs to be sent + * + * Since 5.2.0, this function no longer redirects from variation to its parent product. + * Low stock amount can now be attached to the variation itself and if it isn't, only + * then we check the parent product, and if it's not there, then we take the default + * from the store-wide setting. + * + * @param WC_Product $product Product to get data from. + * @since 3.5.0 + * @return int + */ +function wc_get_low_stock_amount( WC_Product $product ) { + $low_stock_amount = $product->get_low_stock_amount(); + + if ( '' === $low_stock_amount && $product->is_type( 'variation' ) ) { + $product = wc_get_product( $product->get_parent_id() ); + $low_stock_amount = $product->get_low_stock_amount(); + } + + if ( '' === $low_stock_amount ) { + $low_stock_amount = get_option( 'woocommerce_notify_low_stock_amount', 2 ); + } + + return (int) $low_stock_amount; +} diff --git a/plugins/woocommerce/includes/wc-template-functions.php b/plugins/woocommerce/includes/wc-template-functions.php new file mode 100644 index 00000000000..9a5c4308607 --- /dev/null +++ b/plugins/woocommerce/includes/wc-template-functions.php @@ -0,0 +1,3847 @@ +cart->is_empty() && empty( $wp->query_vars['order-pay'] ) && ! isset( $wp->query_vars['order-received'] ) && ! is_customize_preview() && apply_filters( 'woocommerce_checkout_redirect_empty_cart', true ) ) { + wc_add_notice( __( 'Checkout is not available whilst your cart is empty.', 'woocommerce' ), 'notice' ); + wp_safe_redirect( wc_get_cart_url() ); + exit; + + } + + // Logout. + if ( isset( $wp->query_vars['customer-logout'] ) && ! empty( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'customer-logout' ) ) { + wp_safe_redirect( str_replace( '&', '&', wp_logout_url( apply_filters( 'woocommerce_logout_default_redirect_url', wc_get_page_permalink( 'myaccount' ) ) ) ) ); + exit; + } + + // Redirect to the correct logout endpoint. + if ( isset( $wp->query_vars['customer-logout'] ) && 'true' === $wp->query_vars['customer-logout'] ) { + wp_safe_redirect( esc_url_raw( wc_get_account_endpoint_url( 'customer-logout' ) ) ); + exit; + } + + // Trigger 404 if trying to access an endpoint on wrong page. + if ( is_wc_endpoint_url() && ! is_account_page() && ! is_checkout() && apply_filters( 'woocommerce_account_endpoint_page_not_found', true ) ) { + $wp_query->set_404(); + status_header( 404 ); + include get_query_template( '404' ); + exit; + } + + // Redirect to the product page if we have a single product. + if ( is_search() && is_post_type_archive( 'product' ) && apply_filters( 'woocommerce_redirect_single_search_result', true ) && 1 === absint( $wp_query->found_posts ) ) { + $product = wc_get_product( $wp_query->post ); + + if ( $product && $product->is_visible() ) { + wp_safe_redirect( get_permalink( $product->get_id() ), 302 ); + exit; + } + } + + // Ensure gateways and shipping methods are loaded early. + if ( is_add_payment_method_page() || is_checkout() ) { + // Buffer the checkout page. + ob_start(); + + // Ensure gateways and shipping methods are loaded early. + WC()->payment_gateways(); + WC()->shipping(); + } +} +add_action( 'template_redirect', 'wc_template_redirect' ); + +/** + * When loading sensitive checkout or account pages, send a HTTP header to limit rendering of pages to same origin iframes for security reasons. + * + * Can be disabled with: remove_action( 'template_redirect', 'wc_send_frame_options_header' ); + * + * @since 2.3.10 + */ +function wc_send_frame_options_header() { + + if ( ( is_checkout() || is_account_page() ) && ! is_customize_preview() ) { + send_frame_options_header(); + } +} +add_action( 'template_redirect', 'wc_send_frame_options_header' ); + +/** + * No index our endpoints. + * Prevent indexing pages like order-received. + * + * @since 2.5.3 + */ +function wc_prevent_endpoint_indexing() { + // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.PHP.NoSilencedErrors.Discouraged + if ( is_wc_endpoint_url() || isset( $_GET['download_file'] ) ) { + @header( 'X-Robots-Tag: noindex' ); + } + // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.PHP.NoSilencedErrors.Discouraged +} +add_action( 'template_redirect', 'wc_prevent_endpoint_indexing' ); + +/** + * Remove adjacent_posts_rel_link_wp_head - pointless for products. + * + * @since 3.0.0 + */ +function wc_prevent_adjacent_posts_rel_link_wp_head() { + if ( is_singular( 'product' ) ) { + remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head', 10, 0 ); + } +} +add_action( 'template_redirect', 'wc_prevent_adjacent_posts_rel_link_wp_head' ); + +/** + * Show the gallery if JS is disabled. + * + * @since 3.0.6 + */ +function wc_gallery_noscript() { + ?> + + post_type ) || ! in_array( $post->post_type, array( 'product', 'product_variation' ), true ) ) { + return; + } + + $GLOBALS['product'] = wc_get_product( $post ); + + return $GLOBALS['product']; +} +add_action( 'the_post', 'wc_setup_product_data' ); + +/** + * Sets up the woocommerce_loop global from the passed args or from the main query. + * + * @since 3.3.0 + * @param array $args Args to pass into the global. + */ +function wc_setup_loop( $args = array() ) { + $default_args = array( + 'loop' => 0, + 'columns' => wc_get_default_products_per_row(), + 'name' => '', + 'is_shortcode' => false, + 'is_paginated' => true, + 'is_search' => false, + 'is_filtered' => false, + 'total' => 0, + 'total_pages' => 0, + 'per_page' => 0, + 'current_page' => 1, + ); + + // If this is a main WC query, use global args as defaults. + if ( $GLOBALS['wp_query']->get( 'wc_query' ) ) { + $default_args = array_merge( + $default_args, + array( + 'is_search' => $GLOBALS['wp_query']->is_search(), + 'is_filtered' => is_filtered(), + 'total' => $GLOBALS['wp_query']->found_posts, + 'total_pages' => $GLOBALS['wp_query']->max_num_pages, + 'per_page' => $GLOBALS['wp_query']->get( 'posts_per_page' ), + 'current_page' => max( 1, $GLOBALS['wp_query']->get( 'paged', 1 ) ), + ) + ); + } + + // Merge any existing values. + if ( isset( $GLOBALS['woocommerce_loop'] ) ) { + $default_args = array_merge( $default_args, $GLOBALS['woocommerce_loop'] ); + } + + $GLOBALS['woocommerce_loop'] = wp_parse_args( $args, $default_args ); +} +add_action( 'woocommerce_before_shop_loop', 'wc_setup_loop' ); + +/** + * Resets the woocommerce_loop global. + * + * @since 3.3.0 + */ +function wc_reset_loop() { + unset( $GLOBALS['woocommerce_loop'] ); +} +add_action( 'woocommerce_after_shop_loop', 'woocommerce_reset_loop', 999 ); + +/** + * Gets a property from the woocommerce_loop global. + * + * @since 3.3.0 + * @param string $prop Prop to get. + * @param string $default Default if the prop does not exist. + * @return mixed + */ +function wc_get_loop_prop( $prop, $default = '' ) { + wc_setup_loop(); // Ensure shop loop is setup. + + return isset( $GLOBALS['woocommerce_loop'], $GLOBALS['woocommerce_loop'][ $prop ] ) ? $GLOBALS['woocommerce_loop'][ $prop ] : $default; +} + +/** + * Sets a property in the woocommerce_loop global. + * + * @since 3.3.0 + * @param string $prop Prop to set. + * @param string $value Value to set. + */ +function wc_set_loop_prop( $prop, $value = '' ) { + if ( ! isset( $GLOBALS['woocommerce_loop'] ) ) { + wc_setup_loop(); + } + $GLOBALS['woocommerce_loop'][ $prop ] = $value; +} + +/** + * Set the current visbility for a product in the woocommerce_loop global. + * + * @since 4.4.0 + * @param int $product_id Product it to cache visbiility for. + * @param bool $value The poduct visibility value to cache. + */ +function wc_set_loop_product_visibility( $product_id, $value ) { + wc_set_loop_prop( "product_visibility_$product_id", $value ); +} + +/** + * Gets the cached current visibility for a product from the woocommerce_loop global. + * + * @since 4.4.0 + * @param int $product_id Product id to get the cached visibility for. + * + * @return bool|null The cached product visibility, or null if on visibility has been cached for that product. + */ +function wc_get_loop_product_visibility( $product_id ) { + return wc_get_loop_prop( "product_visibility_$product_id", null ); +} + +/** + * Should the WooCommerce loop be displayed? + * + * This will return true if we have posts (products) or if we have subcats to display. + * + * @since 3.4.0 + * @return bool + */ +function woocommerce_product_loop() { + return have_posts() || 'products' !== woocommerce_get_loop_display_mode(); +} + +/** + * Output generator tag to aid debugging. + * + * @param string $gen Generator. + * @param string $type Type. + * @return string + */ +function wc_generator_tag( $gen, $type ) { + $version = Constants::get_constant( 'WC_VERSION' ); + + switch ( $type ) { + case 'html': + $gen .= "\n" . ''; + break; + case 'xhtml': + $gen .= "\n" . ''; + break; + } + return $gen; +} + +/** + * Add body classes for WC pages. + * + * @param array $classes Body Classes. + * @return array + */ +function wc_body_class( $classes ) { + $classes = (array) $classes; + + if ( is_shop() ) { + + $classes[] = 'woocommerce-shop'; + + } + + if ( is_woocommerce() ) { + + $classes[] = 'woocommerce'; + $classes[] = 'woocommerce-page'; + + } elseif ( is_checkout() ) { + + $classes[] = 'woocommerce-checkout'; + $classes[] = 'woocommerce-page'; + + } elseif ( is_cart() ) { + + $classes[] = 'woocommerce-cart'; + $classes[] = 'woocommerce-page'; + + } elseif ( is_account_page() ) { + + $classes[] = 'woocommerce-account'; + $classes[] = 'woocommerce-page'; + + } + + if ( is_store_notice_showing() ) { + $classes[] = 'woocommerce-demo-store'; + } + + foreach ( WC()->query->get_query_vars() as $key => $value ) { + if ( is_wc_endpoint_url( $key ) ) { + $classes[] = 'woocommerce-' . sanitize_html_class( $key ); + } + } + + $classes[] = 'woocommerce-no-js'; + + add_action( 'wp_footer', 'wc_no_js' ); + + return array_unique( $classes ); +} + +/** + * NO JS handling. + * + * @since 3.4.0 + */ +function wc_no_js() { + ?> + + $max_columns ) { + $columns = $max_columns; + update_option( 'woocommerce_catalog_columns', $columns ); + } + + if ( has_filter( 'loop_shop_columns' ) ) { // Legacy filter handling. + $columns = apply_filters( 'loop_shop_columns', $columns ); + } + + $columns = absint( $columns ); + + return max( 1, $columns ); +} + +/** + * Get the default rows setting - this is how many product rows will be shown in loops. + * + * @since 3.3.0 + * @return int + */ +function wc_get_default_product_rows_per_page() { + $rows = absint( get_option( 'woocommerce_catalog_rows', 4 ) ); + $product_grid = wc_get_theme_support( 'product_grid' ); + $min_rows = isset( $product_grid['min_rows'] ) ? absint( $product_grid['min_rows'] ) : 0; + $max_rows = isset( $product_grid['max_rows'] ) ? absint( $product_grid['max_rows'] ) : 0; + + if ( $min_rows && $rows < $min_rows ) { + $rows = $min_rows; + update_option( 'woocommerce_catalog_rows', $rows ); + } elseif ( $max_rows && $rows > $max_rows ) { + $rows = $max_rows; + update_option( 'woocommerce_catalog_rows', $rows ); + } + + return $rows; +} + +/** + * Reset the product grid settings when a new theme is activated. + * + * @since 3.3.0 + */ +function wc_reset_product_grid_settings() { + $product_grid = wc_get_theme_support( 'product_grid' ); + + if ( ! empty( $product_grid['default_rows'] ) ) { + update_option( 'woocommerce_catalog_rows', absint( $product_grid['default_rows'] ) ); + } + + if ( ! empty( $product_grid['default_columns'] ) ) { + update_option( 'woocommerce_catalog_columns', absint( $product_grid['default_columns'] ) ); + } + + wp_cache_flush(); // Flush any caches which could impact settings or templates. +} +add_action( 'after_switch_theme', 'wc_reset_product_grid_settings' ); + +/** + * Get classname for woocommerce loops. + * + * @since 2.6.0 + * @return string + */ +function wc_get_loop_class() { + $loop_index = wc_get_loop_prop( 'loop', 0 ); + $columns = absint( max( 1, wc_get_loop_prop( 'columns', wc_get_default_products_per_row() ) ) ); + + $loop_index ++; + wc_set_loop_prop( 'loop', $loop_index ); + + if ( 0 === ( $loop_index - 1 ) % $columns || 1 === $columns ) { + return 'first'; + } + + if ( 0 === $loop_index % $columns ) { + return 'last'; + } + + return ''; +} + + +/** + * Get the classes for the product cat div. + * + * @since 2.4.0 + * + * @param string|array $class One or more classes to add to the class list. + * @param object $category object Optional. + * + * @return array + */ +function wc_get_product_cat_class( $class = '', $category = null ) { + $classes = is_array( $class ) ? $class : array_map( 'trim', explode( ' ', $class ) ); + $classes[] = 'product-category'; + $classes[] = 'product'; + $classes[] = wc_get_loop_class(); + $classes = apply_filters( 'product_cat_class', $classes, $class, $category ); + + return array_unique( array_filter( $classes ) ); +} + +/** + * Adds extra post classes for products via the WordPress post_class hook, if used. + * + * Note: For performance reasons we instead recommend using wc_product_class/wc_get_product_class instead. + * + * @since 2.1.0 + * @param array $classes Current classes. + * @param string|array $class Additional class. + * @param int $post_id Post ID. + * @return array + */ +function wc_product_post_class( $classes, $class = '', $post_id = 0 ) { + if ( ! $post_id || ! in_array( get_post_type( $post_id ), array( 'product', 'product_variation' ), true ) ) { + return $classes; + } + + $product = wc_get_product( $post_id ); + + if ( ! $product ) { + return $classes; + } + + $classes[] = 'product'; + $classes[] = wc_get_loop_class(); + $classes[] = $product->get_stock_status(); + + if ( $product->is_on_sale() ) { + $classes[] = 'sale'; + } + if ( $product->is_featured() ) { + $classes[] = 'featured'; + } + if ( $product->is_downloadable() ) { + $classes[] = 'downloadable'; + } + if ( $product->is_virtual() ) { + $classes[] = 'virtual'; + } + if ( $product->is_sold_individually() ) { + $classes[] = 'sold-individually'; + } + if ( $product->is_taxable() ) { + $classes[] = 'taxable'; + } + if ( $product->is_shipping_taxable() ) { + $classes[] = 'shipping-taxable'; + } + if ( $product->is_purchasable() ) { + $classes[] = 'purchasable'; + } + if ( $product->get_type() ) { + $classes[] = 'product-type-' . $product->get_type(); + } + if ( $product->is_type( 'variable' ) && $product->get_default_attributes() ) { + $classes[] = 'has-default-attributes'; + } + + $key = array_search( 'hentry', $classes, true ); + if ( false !== $key ) { + unset( $classes[ $key ] ); + } + + return $classes; +} + +/** + * Get product taxonomy HTML classes. + * + * @since 3.4.0 + * @param array $term_ids Array of terms IDs or objects. + * @param string $taxonomy Taxonomy. + * @return array + */ +function wc_get_product_taxonomy_class( $term_ids, $taxonomy ) { + $classes = array(); + + foreach ( $term_ids as $term_id ) { + $term = get_term( $term_id, $taxonomy ); + + if ( empty( $term->slug ) ) { + continue; + } + + $term_class = sanitize_html_class( $term->slug, $term->term_id ); + if ( is_numeric( $term_class ) || ! trim( $term_class, '-' ) ) { + $term_class = $term->term_id; + } + + // 'post_tag' uses the 'tag' prefix for backward compatibility. + if ( 'post_tag' === $taxonomy ) { + $classes[] = 'tag-' . $term_class; + } else { + $classes[] = sanitize_html_class( $taxonomy . '-' . $term_class, $taxonomy . '-' . $term->term_id ); + } + } + + return $classes; +} + +/** + * Retrieves the classes for the post div as an array. + * + * This method was modified from WordPress's get_post_class() to allow the removal of taxonomies + * (for performance reasons). Previously wc_product_post_class was hooked into post_class. @since 3.6.0 + * + * @since 3.4.0 + * @param string|array $class One or more classes to add to the class list. + * @param int|WP_Post|WC_Product $product Product ID or product object. + * @return array + */ +function wc_get_product_class( $class = '', $product = null ) { + if ( is_null( $product ) && ! empty( $GLOBALS['product'] ) ) { + // Product was null so pull from global. + $product = $GLOBALS['product']; + } + + if ( $product && ! is_a( $product, 'WC_Product' ) ) { + // Make sure we have a valid product, or set to false. + $product = wc_get_product( $product ); + } + + if ( $class ) { + if ( ! is_array( $class ) ) { + $class = preg_split( '#\s+#', $class ); + } + } else { + $class = array(); + } + + $post_classes = array_map( 'esc_attr', $class ); + + if ( ! $product ) { + return $post_classes; + } + + // Run through the post_class hook so 3rd parties using this previously can still append classes. + // Note, to change classes you will need to use the newer woocommerce_post_class filter. + // @internal This removes the wc_product_post_class filter so classes are not duplicated. + $filtered = has_filter( 'post_class', 'wc_product_post_class' ); + + if ( $filtered ) { + remove_filter( 'post_class', 'wc_product_post_class', 20 ); + } + + $post_classes = apply_filters( 'post_class', $post_classes, $class, $product->get_id() ); + + if ( $filtered ) { + add_filter( 'post_class', 'wc_product_post_class', 20, 3 ); + } + + $classes = array_merge( + $post_classes, + array( + 'product', + 'type-product', + 'post-' . $product->get_id(), + 'status-' . $product->get_status(), + wc_get_loop_class(), + $product->get_stock_status(), + ), + wc_get_product_taxonomy_class( $product->get_category_ids(), 'product_cat' ), + wc_get_product_taxonomy_class( $product->get_tag_ids(), 'product_tag' ) + ); + + if ( $product->get_image_id() ) { + $classes[] = 'has-post-thumbnail'; + } + if ( $product->get_post_password() ) { + $classes[] = post_password_required( $product->get_id() ) ? 'post-password-required' : 'post-password-protected'; + } + if ( $product->is_on_sale() ) { + $classes[] = 'sale'; + } + if ( $product->is_featured() ) { + $classes[] = 'featured'; + } + if ( $product->is_downloadable() ) { + $classes[] = 'downloadable'; + } + if ( $product->is_virtual() ) { + $classes[] = 'virtual'; + } + if ( $product->is_sold_individually() ) { + $classes[] = 'sold-individually'; + } + if ( $product->is_taxable() ) { + $classes[] = 'taxable'; + } + if ( $product->is_shipping_taxable() ) { + $classes[] = 'shipping-taxable'; + } + if ( $product->is_purchasable() ) { + $classes[] = 'purchasable'; + } + if ( $product->get_type() ) { + $classes[] = 'product-type-' . $product->get_type(); + } + if ( $product->is_type( 'variable' ) && $product->get_default_attributes() ) { + $classes[] = 'has-default-attributes'; + } + + // Include attributes and any extra taxonomies only if enabled via the hook - this is a performance issue. + if ( apply_filters( 'woocommerce_get_product_class_include_taxonomies', false ) ) { + $taxonomies = get_taxonomies( array( 'public' => true ) ); + $type = 'variation' === $product->get_type() ? 'product_variation' : 'product'; + foreach ( (array) $taxonomies as $taxonomy ) { + if ( is_object_in_taxonomy( $type, $taxonomy ) && ! in_array( $taxonomy, array( 'product_cat', 'product_tag' ), true ) ) { + $classes = array_merge( $classes, wc_get_product_taxonomy_class( (array) get_the_terms( $product->get_id(), $taxonomy ), $taxonomy ) ); + } + } + } + + /** + * WooCommerce Post Class filter. + * + * @since 3.6.2 + * @param array $classes Array of CSS classes. + * @param WC_Product $product Product object. + */ + $classes = apply_filters( 'woocommerce_post_class', $classes, $product ); + + return array_map( 'esc_attr', array_unique( array_filter( $classes ) ) ); +} + +/** + * Display the classes for the product div. + * + * @since 3.4.0 + * @param string|array $class One or more classes to add to the class list. + * @param int|WP_Post|WC_Product $product_id Product ID or product object. + */ +function wc_product_class( $class = '', $product_id = null ) { + echo 'class="' . esc_attr( implode( ' ', wc_get_product_class( $class, $product_id ) ) ) . '"'; +} + +/** + * Outputs hidden form inputs for each query string variable. + * + * @since 3.0.0 + * @param string|array $values Name value pairs, or a URL to parse. + * @param array $exclude Keys to exclude. + * @param string $current_key Current key we are outputting. + * @param bool $return Whether to return. + * @return string + */ +function wc_query_string_form_fields( $values = null, $exclude = array(), $current_key = '', $return = false ) { + if ( is_null( $values ) ) { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $values = $_GET; + } elseif ( is_string( $values ) ) { + $url_parts = wp_parse_url( $values ); + $values = array(); + + if ( ! empty( $url_parts['query'] ) ) { + // This is to preserve full-stops, pluses and spaces in the query string when ran through parse_str. + $replace_chars = array( + '.' => '{dot}', + '+' => '{plus}', + ); + + $query_string = str_replace( array_keys( $replace_chars ), array_values( $replace_chars ), $url_parts['query'] ); + + // Parse the string. + parse_str( $query_string, $parsed_query_string ); + + // Convert the full-stops, pluses and spaces back and add to values array. + foreach ( $parsed_query_string as $key => $value ) { + $new_key = str_replace( array_values( $replace_chars ), array_keys( $replace_chars ), $key ); + $new_value = str_replace( array_values( $replace_chars ), array_keys( $replace_chars ), $value ); + $values[ $new_key ] = $new_value; + } + } + } + $html = ''; + + foreach ( $values as $key => $value ) { + if ( in_array( $key, $exclude, true ) ) { + continue; + } + if ( $current_key ) { + $key = $current_key . '[' . $key . ']'; + } + if ( is_array( $value ) ) { + $html .= wc_query_string_form_fields( $value, $exclude, $key, true ); + } else { + $html .= ''; + } + } + + if ( $return ) { + return $html; + } + + echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +} + +/** + * Get the terms and conditions page ID. + * + * @since 3.4.0 + * @return int + */ +function wc_terms_and_conditions_page_id() { + $page_id = wc_get_page_id( 'terms' ); + return apply_filters( 'woocommerce_terms_and_conditions_page_id', 0 < $page_id ? absint( $page_id ) : 0 ); +} + +/** + * Get the privacy policy page ID. + * + * @since 3.4.0 + * @return int + */ +function wc_privacy_policy_page_id() { + $page_id = get_option( 'wp_page_for_privacy_policy', 0 ); + return apply_filters( 'woocommerce_privacy_policy_page_id', 0 < $page_id ? absint( $page_id ) : 0 ); +} + +/** + * See if the checkbox is enabled or not based on the existance of the terms page and checkbox text. + * + * @since 3.4.0 + * @return bool + */ +function wc_terms_and_conditions_checkbox_enabled() { + $page_id = wc_terms_and_conditions_page_id(); + $page = $page_id ? get_post( $page_id ) : false; + return $page && wc_get_terms_and_conditions_checkbox_text(); +} + +/** + * Get the terms and conditions checkbox text, if set. + * + * @since 3.4.0 + * @return string + */ +function wc_get_terms_and_conditions_checkbox_text() { + /* translators: %s terms and conditions page name and link */ + return trim( apply_filters( 'woocommerce_get_terms_and_conditions_checkbox_text', get_option( 'woocommerce_checkout_terms_and_conditions_checkbox_text', sprintf( __( 'I have read and agree to the website %s', 'woocommerce' ), '[terms]' ) ) ) ); +} + +/** + * Get the privacy policy text, if set. + * + * @since 3.4.0 + * @param string $type Type of policy to load. Valid values include registration and checkout. + * @return string + */ +function wc_get_privacy_policy_text( $type = '' ) { + $text = ''; + + switch ( $type ) { + case 'checkout': + /* translators: %s privacy policy page name and link */ + $text = get_option( 'woocommerce_checkout_privacy_policy_text', sprintf( __( 'Your personal data will be used to process your order, support your experience throughout this website, and for other purposes described in our %s.', 'woocommerce' ), '[privacy_policy]' ) ); + break; + case 'registration': + /* translators: %s privacy policy page name and link */ + $text = get_option( 'woocommerce_registration_privacy_policy_text', sprintf( __( 'Your personal data will be used to support your experience throughout this website, to manage access to your account, and for other purposes described in our %s.', 'woocommerce' ), '[privacy_policy]' ) ); + break; + } + + return trim( apply_filters( 'woocommerce_get_privacy_policy_text', $text, $type ) ); +} + +/** + * Output t&c checkbox text. + * + * @since 3.4.0 + */ +function wc_terms_and_conditions_checkbox_text() { + $text = wc_get_terms_and_conditions_checkbox_text(); + + if ( ! $text ) { + return; + } + + echo wp_kses_post( wc_replace_policy_page_link_placeholders( $text ) ); +} + +/** + * Output t&c page's content (if set). The page can be set from checkout settings. + * + * @since 3.4.0 + */ +function wc_terms_and_conditions_page_content() { + $terms_page_id = wc_terms_and_conditions_page_id(); + + if ( ! $terms_page_id ) { + return; + } + + $page = get_post( $terms_page_id ); + + if ( $page && 'publish' === $page->post_status && $page->post_content && ! has_shortcode( $page->post_content, 'woocommerce_checkout' ) ) { + echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } +} + +/** + * Render privacy policy text on the checkout. + * + * @since 3.4.0 + */ +function wc_checkout_privacy_policy_text() { + echo '
    '; + wc_privacy_policy_text( 'checkout' ); + echo '
    '; +} + +/** + * Render privacy policy text on the register forms. + * + * @since 3.4.0 + */ +function wc_registration_privacy_policy_text() { + echo '
    '; + wc_privacy_policy_text( 'registration' ); + echo '
    '; +} + +/** + * Output privacy policy text. This is custom text which can be added via the customizer/privacy settings section. + * + * Loads the relevant policy for the current page unless a specific policy text is required. + * + * @since 3.4.0 + * @param string $type Type of policy to load. Valid values include registration and checkout. + */ +function wc_privacy_policy_text( $type = 'checkout' ) { + if ( ! wc_privacy_policy_page_id() ) { + return; + } + echo wp_kses_post( wpautop( wc_replace_policy_page_link_placeholders( wc_get_privacy_policy_text( $type ) ) ) ); +} + +/** + * Replaces placeholders with links to WooCommerce policy pages. + * + * @since 3.4.0 + * @param string $text Text to find/replace within. + * @return string + */ +function wc_replace_policy_page_link_placeholders( $text ) { + $privacy_page_id = wc_privacy_policy_page_id(); + $terms_page_id = wc_terms_and_conditions_page_id(); + $privacy_link = $privacy_page_id ? '' . __( 'privacy policy', 'woocommerce' ) . '' : __( 'privacy policy', 'woocommerce' ); + $terms_link = $terms_page_id ? '' . __( 'terms and conditions', 'woocommerce' ) . '' : __( 'terms and conditions', 'woocommerce' ); + + $find_replace = array( + '[terms]' => $terms_link, + '[privacy_policy]' => $privacy_link, + ); + + return str_replace( array_keys( $find_replace ), array_values( $find_replace ), $text ); +} + +/** + * Template pages + */ + +if ( ! function_exists( 'woocommerce_content' ) ) { + + /** + * Output WooCommerce content. + * + * This function is only used in the optional 'woocommerce.php' template. + * which people can add to their themes to add basic woocommerce support. + * without hooks or modifying core templates. + */ + function woocommerce_content() { + + if ( is_singular( 'product' ) ) { + + while ( have_posts() ) : + the_post(); + wc_get_template_part( 'content', 'single-product' ); + endwhile; + + } else { + ?> + + + +

    + + + + + + + + + + + + + + + + + + + + + + + ' . wp_kses_post( $notice ) . ' ' . esc_html__( 'Dismiss', 'woocommerce' ) . '

    ', $notice ); + } +} + +/** + * Loop + */ + +if ( ! function_exists( 'woocommerce_page_title' ) ) { + + /** + * Page Title function. + * + * @param bool $echo Should echo title. + * @return string + */ + function woocommerce_page_title( $echo = true ) { + + if ( is_search() ) { + /* translators: %s: search query */ + $page_title = sprintf( __( 'Search results: “%s”', 'woocommerce' ), get_search_query() ); + + if ( get_query_var( 'paged' ) ) { + /* translators: %s: page number */ + $page_title .= sprintf( __( ' – Page %s', 'woocommerce' ), get_query_var( 'paged' ) ); + } + } elseif ( is_tax() ) { + + $page_title = single_term_title( '', false ); + + } else { + + $shop_page_id = wc_get_page_id( 'shop' ); + $page_title = get_the_title( $shop_page_id ); + + } + + $page_title = apply_filters( 'woocommerce_page_title', $page_title ); + + if ( $echo ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $page_title; + } else { + return $page_title; + } + } +} + +if ( ! function_exists( 'woocommerce_product_loop_start' ) ) { + + /** + * Output the start of a product loop. By default this is a UL. + * + * @param bool $echo Should echo?. + * @return string + */ + function woocommerce_product_loop_start( $echo = true ) { + ob_start(); + + wc_set_loop_prop( 'loop', 0 ); + + wc_get_template( 'loop/loop-start.php' ); + + $loop_start = apply_filters( 'woocommerce_product_loop_start', ob_get_clean() ); + + if ( $echo ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $loop_start; + } else { + return $loop_start; + } + } +} + +if ( ! function_exists( 'woocommerce_product_loop_end' ) ) { + + /** + * Output the end of a product loop. By default this is a UL. + * + * @param bool $echo Should echo?. + * @return string + */ + function woocommerce_product_loop_end( $echo = true ) { + ob_start(); + + wc_get_template( 'loop/loop-end.php' ); + + $loop_end = apply_filters( 'woocommerce_product_loop_end', ob_get_clean() ); + + if ( $echo ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $loop_end; + } else { + return $loop_end; + } + } +} +if ( ! function_exists( 'woocommerce_template_loop_product_title' ) ) { + + /** + * Show the product title in the product loop. By default this is an H2. + */ + function woocommerce_template_loop_product_title() { + echo '

    ' . get_the_title() . '

    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } +} +if ( ! function_exists( 'woocommerce_template_loop_category_title' ) ) { + + /** + * Show the subcategory title in the product loop. + * + * @param object $category Category object. + */ + function woocommerce_template_loop_category_title( $category ) { + ?> +

    + name ); + + if ( $category->count > 0 ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo apply_filters( 'woocommerce_subcategory_count_html', ' (' . esc_html( $category->count ) . ')', $category ); + } + ?> +

    + '; + } +} + +if ( ! function_exists( 'woocommerce_template_loop_product_link_close' ) ) { + /** + * Insert the closing anchor tag for products in the loop. + */ + function woocommerce_template_loop_product_link_close() { + echo ''; + } +} + +if ( ! function_exists( 'woocommerce_template_loop_category_link_open' ) ) { + /** + * Insert the opening anchor tag for categories in the loop. + * + * @param int|object|string $category Category ID, Object or String. + */ + function woocommerce_template_loop_category_link_open( $category ) { + echo ''; + } +} + +if ( ! function_exists( 'woocommerce_template_loop_category_link_close' ) ) { + /** + * Insert the closing anchor tag for categories in the loop. + */ + function woocommerce_template_loop_category_link_close() { + echo ''; + } +} + +if ( ! function_exists( 'woocommerce_taxonomy_archive_description' ) ) { + + /** + * Show an archive description on taxonomy archives. + */ + function woocommerce_taxonomy_archive_description() { + if ( is_product_taxonomy() && 0 === absint( get_query_var( 'paged' ) ) ) { + $term = get_queried_object(); + + if ( $term && ! empty( $term->description ) ) { + echo '
    ' . wc_format_content( wp_kses_post( $term->description ) ) . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } + } +} +if ( ! function_exists( 'woocommerce_product_archive_description' ) ) { + + /** + * Show a shop page description on product archives. + */ + function woocommerce_product_archive_description() { + // Don't display the description on search results page. + if ( is_search() ) { + return; + } + + if ( is_post_type_archive( 'product' ) && in_array( absint( get_query_var( 'paged' ) ), array( 0, 1 ), true ) ) { + $shop_page = get_post( wc_get_page_id( 'shop' ) ); + if ( $shop_page ) { + + $allowed_html = wp_kses_allowed_html( 'post' ); + + // This is needed for the search product block to work. + $allowed_html = array_merge( + $allowed_html, + array( + 'form' => array( + 'action' => true, + 'accept' => true, + 'accept-charset' => true, + 'enctype' => true, + 'method' => true, + 'name' => true, + 'target' => true, + ), + + 'input' => array( + 'type' => true, + 'id' => true, + 'class' => true, + 'placeholder' => true, + 'name' => true, + 'value' => true, + ), + + 'button' => array( + 'type' => true, + 'class' => true, + 'label' => true, + ), + + 'svg' => array( + 'hidden' => true, + 'role' => true, + 'focusable' => true, + 'xmlns' => true, + 'width' => true, + 'height' => true, + 'viewbox' => true, + ), + 'path' => array( + 'd' => true, + ), + ) + ); + + $description = wc_format_content( wp_kses( $shop_page->post_content, $allowed_html ) ); + if ( $description ) { + echo '
    ' . $description . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } + } + } +} + +if ( ! function_exists( 'woocommerce_template_loop_add_to_cart' ) ) { + + /** + * Get the add to cart template for the loop. + * + * @param array $args Arguments. + */ + function woocommerce_template_loop_add_to_cart( $args = array() ) { + global $product; + + if ( $product ) { + $defaults = array( + 'quantity' => 1, + 'class' => implode( + ' ', + array_filter( + array( + 'button', + 'product_type_' . $product->get_type(), + $product->is_purchasable() && $product->is_in_stock() ? 'add_to_cart_button' : '', + $product->supports( 'ajax_add_to_cart' ) && $product->is_purchasable() && $product->is_in_stock() ? 'ajax_add_to_cart' : '', + ) + ) + ), + 'attributes' => array( + 'data-product_id' => $product->get_id(), + 'data-product_sku' => $product->get_sku(), + 'aria-label' => $product->add_to_cart_description(), + 'rel' => 'nofollow', + ), + ); + + $args = apply_filters( 'woocommerce_loop_add_to_cart_args', wp_parse_args( $args, $defaults ), $product ); + + if ( isset( $args['attributes']['aria-label'] ) ) { + $args['attributes']['aria-label'] = wp_strip_all_tags( $args['attributes']['aria-label'] ); + } + + wc_get_template( 'loop/add-to-cart.php', $args ); + } + } +} + +if ( ! function_exists( 'woocommerce_template_loop_product_thumbnail' ) ) { + + /** + * Get the product thumbnail for the loop. + */ + function woocommerce_template_loop_product_thumbnail() { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo woocommerce_get_product_thumbnail(); + } +} +if ( ! function_exists( 'woocommerce_template_loop_price' ) ) { + + /** + * Get the product price for the loop. + */ + function woocommerce_template_loop_price() { + wc_get_template( 'loop/price.php' ); + } +} +if ( ! function_exists( 'woocommerce_template_loop_rating' ) ) { + + /** + * Display the average rating in the loop. + */ + function woocommerce_template_loop_rating() { + wc_get_template( 'loop/rating.php' ); + } +} +if ( ! function_exists( 'woocommerce_show_product_loop_sale_flash' ) ) { + + /** + * Get the sale flash for the loop. + */ + function woocommerce_show_product_loop_sale_flash() { + wc_get_template( 'loop/sale-flash.php' ); + } +} + +if ( ! function_exists( 'woocommerce_get_product_thumbnail' ) ) { + + /** + * Get the product thumbnail, or the placeholder if not set. + * + * @param string $size (default: 'woocommerce_thumbnail'). + * @param array $attr Image attributes. + * @param bool $placeholder True to return $placeholder if no image is found, or false to return an empty string. + * @return string + */ + function woocommerce_get_product_thumbnail( $size = 'woocommerce_thumbnail', $attr = array(), $placeholder = true ) { + global $product; + + if ( ! is_array( $attr ) ) { + $attr = array(); + } + + if ( ! is_bool( $placeholder ) ) { + $placeholder = true; + } + + $image_size = apply_filters( 'single_product_archive_thumbnail_size', $size ); + + return $product ? $product->get_image( $image_size, $attr, $placeholder ) : ''; + } +} + +if ( ! function_exists( 'woocommerce_result_count' ) ) { + + /** + * Output the result count text (Showing x - x of x results). + */ + function woocommerce_result_count() { + if ( ! wc_get_loop_prop( 'is_paginated' ) || ! woocommerce_products_will_display() ) { + return; + } + $args = array( + 'total' => wc_get_loop_prop( 'total' ), + 'per_page' => wc_get_loop_prop( 'per_page' ), + 'current' => wc_get_loop_prop( 'current_page' ), + ); + + wc_get_template( 'loop/result-count.php', $args ); + } +} + +if ( ! function_exists( 'woocommerce_catalog_ordering' ) ) { + + /** + * Output the product sorting options. + */ + function woocommerce_catalog_ordering() { + if ( ! wc_get_loop_prop( 'is_paginated' ) || ! woocommerce_products_will_display() ) { + return; + } + $show_default_orderby = 'menu_order' === apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby', 'menu_order' ) ); + $catalog_orderby_options = apply_filters( + 'woocommerce_catalog_orderby', + array( + 'menu_order' => __( 'Default sorting', 'woocommerce' ), + 'popularity' => __( 'Sort by popularity', 'woocommerce' ), + 'rating' => __( 'Sort by average rating', 'woocommerce' ), + 'date' => __( 'Sort by latest', 'woocommerce' ), + 'price' => __( 'Sort by price: low to high', 'woocommerce' ), + 'price-desc' => __( 'Sort by price: high to low', 'woocommerce' ), + ) + ); + + $default_orderby = wc_get_loop_prop( 'is_search' ) ? 'relevance' : apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby', '' ) ); + // phpcs:disable WordPress.Security.NonceVerification.Recommended + $orderby = isset( $_GET['orderby'] ) ? wc_clean( wp_unslash( $_GET['orderby'] ) ) : $default_orderby; + // phpcs:enable WordPress.Security.NonceVerification.Recommended + + if ( wc_get_loop_prop( 'is_search' ) ) { + $catalog_orderby_options = array_merge( array( 'relevance' => __( 'Relevance', 'woocommerce' ) ), $catalog_orderby_options ); + + unset( $catalog_orderby_options['menu_order'] ); + } + + if ( ! $show_default_orderby ) { + unset( $catalog_orderby_options['menu_order'] ); + } + + if ( ! wc_review_ratings_enabled() ) { + unset( $catalog_orderby_options['rating'] ); + } + + if ( ! array_key_exists( $orderby, $catalog_orderby_options ) ) { + $orderby = current( array_keys( $catalog_orderby_options ) ); + } + + wc_get_template( + 'loop/orderby.php', + array( + 'catalog_orderby_options' => $catalog_orderby_options, + 'orderby' => $orderby, + 'show_default_orderby' => $show_default_orderby, + ) + ); + } +} + +if ( ! function_exists( 'woocommerce_pagination' ) ) { + + /** + * Output the pagination. + */ + function woocommerce_pagination() { + if ( ! wc_get_loop_prop( 'is_paginated' ) || ! woocommerce_products_will_display() ) { + return; + } + + $args = array( + 'total' => wc_get_loop_prop( 'total_pages' ), + 'current' => wc_get_loop_prop( 'current_page' ), + 'base' => esc_url_raw( add_query_arg( 'product-page', '%#%', false ) ), + 'format' => '?product-page=%#%', + ); + + if ( ! wc_get_loop_prop( 'is_shortcode' ) ) { + $args['format'] = ''; + $args['base'] = esc_url_raw( str_replace( 999999999, '%#%', remove_query_arg( 'add-to-cart', get_pagenum_link( 999999999, false ) ) ) ); + } + + wc_get_template( 'loop/pagination.php', $args ); + } +} + +/** + * Single Product + */ + +if ( ! function_exists( 'woocommerce_show_product_images' ) ) { + + /** + * Output the product image before the single product summary. + */ + function woocommerce_show_product_images() { + wc_get_template( 'single-product/product-image.php' ); + } +} +if ( ! function_exists( 'woocommerce_show_product_thumbnails' ) ) { + + /** + * Output the product thumbnails. + */ + function woocommerce_show_product_thumbnails() { + wc_get_template( 'single-product/product-thumbnails.php' ); + } +} + +/** + * Get HTML for a gallery image. + * + * Hooks: woocommerce_gallery_thumbnail_size, woocommerce_gallery_image_size and woocommerce_gallery_full_size accept name based image sizes, or an array of width/height values. + * + * @since 3.3.2 + * @param int $attachment_id Attachment ID. + * @param bool $main_image Is this the main image or a thumbnail?. + * @return string + */ +function wc_get_gallery_image_html( $attachment_id, $main_image = false ) { + $flexslider = (bool) apply_filters( 'woocommerce_single_product_flexslider_enabled', get_theme_support( 'wc-product-gallery-slider' ) ); + $gallery_thumbnail = wc_get_image_size( 'gallery_thumbnail' ); + $thumbnail_size = apply_filters( 'woocommerce_gallery_thumbnail_size', array( $gallery_thumbnail['width'], $gallery_thumbnail['height'] ) ); + $image_size = apply_filters( 'woocommerce_gallery_image_size', $flexslider || $main_image ? 'woocommerce_single' : $thumbnail_size ); + $full_size = apply_filters( 'woocommerce_gallery_full_size', apply_filters( 'woocommerce_product_thumbnails_large_size', 'full' ) ); + $thumbnail_src = wp_get_attachment_image_src( $attachment_id, $thumbnail_size ); + $full_src = wp_get_attachment_image_src( $attachment_id, $full_size ); + $alt_text = trim( wp_strip_all_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) ); + $image = wp_get_attachment_image( + $attachment_id, + $image_size, + false, + apply_filters( + 'woocommerce_gallery_image_html_attachment_image_params', + array( + 'title' => _wp_specialchars( get_post_field( 'post_title', $attachment_id ), ENT_QUOTES, 'UTF-8', true ), + 'data-caption' => _wp_specialchars( get_post_field( 'post_excerpt', $attachment_id ), ENT_QUOTES, 'UTF-8', true ), + 'data-src' => esc_url( $full_src[0] ), + 'data-large_image' => esc_url( $full_src[0] ), + 'data-large_image_width' => esc_attr( $full_src[1] ), + 'data-large_image_height' => esc_attr( $full_src[2] ), + 'class' => esc_attr( $main_image ? 'wp-post-image' : '' ), + ), + $attachment_id, + $image_size, + $main_image + ) + ); + + return ''; +} + +if ( ! function_exists( 'woocommerce_output_product_data_tabs' ) ) { + + /** + * Output the product tabs. + */ + function woocommerce_output_product_data_tabs() { + wc_get_template( 'single-product/tabs/tabs.php' ); + } +} +if ( ! function_exists( 'woocommerce_template_single_title' ) ) { + + /** + * Output the product title. + */ + function woocommerce_template_single_title() { + wc_get_template( 'single-product/title.php' ); + } +} +if ( ! function_exists( 'woocommerce_template_single_rating' ) ) { + + /** + * Output the product rating. + */ + function woocommerce_template_single_rating() { + if ( post_type_supports( 'product', 'comments' ) ) { + wc_get_template( 'single-product/rating.php' ); + } + } +} +if ( ! function_exists( 'woocommerce_template_single_price' ) ) { + + /** + * Output the product price. + */ + function woocommerce_template_single_price() { + wc_get_template( 'single-product/price.php' ); + } +} +if ( ! function_exists( 'woocommerce_template_single_excerpt' ) ) { + + /** + * Output the product short description (excerpt). + */ + function woocommerce_template_single_excerpt() { + wc_get_template( 'single-product/short-description.php' ); + } +} +if ( ! function_exists( 'woocommerce_template_single_meta' ) ) { + + /** + * Output the product meta. + */ + function woocommerce_template_single_meta() { + wc_get_template( 'single-product/meta.php' ); + } +} +if ( ! function_exists( 'woocommerce_template_single_sharing' ) ) { + + /** + * Output the product sharing. + */ + function woocommerce_template_single_sharing() { + wc_get_template( 'single-product/share.php' ); + } +} +if ( ! function_exists( 'woocommerce_show_product_sale_flash' ) ) { + + /** + * Output the product sale flash. + */ + function woocommerce_show_product_sale_flash() { + wc_get_template( 'single-product/sale-flash.php' ); + } +} + +if ( ! function_exists( 'woocommerce_template_single_add_to_cart' ) ) { + + /** + * Trigger the single product add to cart action. + */ + function woocommerce_template_single_add_to_cart() { + global $product; + do_action( 'woocommerce_' . $product->get_type() . '_add_to_cart' ); + } +} +if ( ! function_exists( 'woocommerce_simple_add_to_cart' ) ) { + + /** + * Output the simple product add to cart area. + */ + function woocommerce_simple_add_to_cart() { + wc_get_template( 'single-product/add-to-cart/simple.php' ); + } +} +if ( ! function_exists( 'woocommerce_grouped_add_to_cart' ) ) { + + /** + * Output the grouped product add to cart area. + */ + function woocommerce_grouped_add_to_cart() { + global $product; + + $products = array_filter( array_map( 'wc_get_product', $product->get_children() ), 'wc_products_array_filter_visible_grouped' ); + + if ( $products ) { + wc_get_template( + 'single-product/add-to-cart/grouped.php', + array( + 'grouped_product' => $product, + 'grouped_products' => $products, + 'quantites_required' => false, + ) + ); + } + } +} +if ( ! function_exists( 'woocommerce_variable_add_to_cart' ) ) { + + /** + * Output the variable product add to cart area. + */ + function woocommerce_variable_add_to_cart() { + global $product; + + // Enqueue variation scripts. + wp_enqueue_script( 'wc-add-to-cart-variation' ); + + // Get Available variations? + $get_variations = count( $product->get_children() ) <= apply_filters( 'woocommerce_ajax_variation_threshold', 30, $product ); + + // Load the template. + wc_get_template( + 'single-product/add-to-cart/variable.php', + array( + 'available_variations' => $get_variations ? $product->get_available_variations() : false, + 'attributes' => $product->get_variation_attributes(), + 'selected_attributes' => $product->get_default_attributes(), + ) + ); + } +} +if ( ! function_exists( 'woocommerce_external_add_to_cart' ) ) { + + /** + * Output the external product add to cart area. + */ + function woocommerce_external_add_to_cart() { + global $product; + + if ( ! $product->add_to_cart_url() ) { + return; + } + + wc_get_template( + 'single-product/add-to-cart/external.php', + array( + 'product_url' => $product->add_to_cart_url(), + 'button_text' => $product->single_add_to_cart_text(), + ) + ); + } +} + +if ( ! function_exists( 'woocommerce_quantity_input' ) ) { + + /** + * Output the quantity input for add to cart forms. + * + * @param array $args Args for the input. + * @param WC_Product|null $product Product. + * @param boolean $echo Whether to return or echo|string. + * + * @return string + */ + function woocommerce_quantity_input( $args = array(), $product = null, $echo = true ) { + if ( is_null( $product ) ) { + $product = $GLOBALS['product']; + } + + $defaults = array( + 'input_id' => uniqid( 'quantity_' ), + 'input_name' => 'quantity', + 'input_value' => '1', + 'classes' => apply_filters( 'woocommerce_quantity_input_classes', array( 'input-text', 'qty', 'text' ), $product ), + 'max_value' => apply_filters( 'woocommerce_quantity_input_max', -1, $product ), + 'min_value' => apply_filters( 'woocommerce_quantity_input_min', 0, $product ), + 'step' => apply_filters( 'woocommerce_quantity_input_step', 1, $product ), + 'pattern' => apply_filters( 'woocommerce_quantity_input_pattern', has_filter( 'woocommerce_stock_amount', 'intval' ) ? '[0-9]*' : '' ), + 'inputmode' => apply_filters( 'woocommerce_quantity_input_inputmode', has_filter( 'woocommerce_stock_amount', 'intval' ) ? 'numeric' : '' ), + 'product_name' => $product ? $product->get_title() : '', + 'placeholder' => apply_filters( 'woocommerce_quantity_input_placeholder', '', $product ), + // When autocomplete is enabled in firefox, it will overwrite actual value with what user entered last. So we default to off. + // See @link https://github.com/woocommerce/woocommerce/issues/30733. + 'autocomplete' => apply_filters( 'woocommerce_quantity_input_autocomplete', 'off', $product ), + ); + + $args = apply_filters( 'woocommerce_quantity_input_args', wp_parse_args( $args, $defaults ), $product ); + + // Apply sanity to min/max args - min cannot be lower than 0. + $args['min_value'] = max( $args['min_value'], 0 ); + $args['max_value'] = 0 < $args['max_value'] ? $args['max_value'] : ''; + + // Max cannot be lower than min if defined. + if ( '' !== $args['max_value'] && $args['max_value'] < $args['min_value'] ) { + $args['max_value'] = $args['min_value']; + } + + ob_start(); + + wc_get_template( 'global/quantity-input.php', $args ); + + if ( $echo ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo ob_get_clean(); + } else { + return ob_get_clean(); + } + } +} + +if ( ! function_exists( 'woocommerce_product_description_tab' ) ) { + + /** + * Output the description tab content. + */ + function woocommerce_product_description_tab() { + wc_get_template( 'single-product/tabs/description.php' ); + } +} +if ( ! function_exists( 'woocommerce_product_additional_information_tab' ) ) { + + /** + * Output the attributes tab content. + */ + function woocommerce_product_additional_information_tab() { + wc_get_template( 'single-product/tabs/additional-information.php' ); + } +} +if ( ! function_exists( 'woocommerce_default_product_tabs' ) ) { + + /** + * Add default product tabs to product pages. + * + * @param array $tabs Array of tabs. + * @return array + */ + function woocommerce_default_product_tabs( $tabs = array() ) { + global $product, $post; + + // Description tab - shows product content. + if ( $post->post_content ) { + $tabs['description'] = array( + 'title' => __( 'Description', 'woocommerce' ), + 'priority' => 10, + 'callback' => 'woocommerce_product_description_tab', + ); + } + + // Additional information tab - shows attributes. + if ( $product && ( $product->has_attributes() || apply_filters( 'wc_product_enable_dimensions_display', $product->has_weight() || $product->has_dimensions() ) ) ) { + $tabs['additional_information'] = array( + 'title' => __( 'Additional information', 'woocommerce' ), + 'priority' => 20, + 'callback' => 'woocommerce_product_additional_information_tab', + ); + } + + // Reviews tab - shows comments. + if ( comments_open() ) { + $tabs['reviews'] = array( + /* translators: %s: reviews count */ + 'title' => sprintf( __( 'Reviews (%d)', 'woocommerce' ), $product->get_review_count() ), + 'priority' => 30, + 'callback' => 'comments_template', + ); + } + + return $tabs; + } +} + +if ( ! function_exists( 'woocommerce_sort_product_tabs' ) ) { + + /** + * Sort tabs by priority. + * + * @param array $tabs Array of tabs. + * @return array + */ + function woocommerce_sort_product_tabs( $tabs = array() ) { + + // Make sure the $tabs parameter is an array. + if ( ! is_array( $tabs ) ) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error + trigger_error( 'Function woocommerce_sort_product_tabs() expects an array as the first parameter. Defaulting to empty array.' ); + $tabs = array(); + } + + // Re-order tabs by priority. + if ( ! function_exists( '_sort_priority_callback' ) ) { + /** + * Sort Priority Callback Function + * + * @param array $a Comparison A. + * @param array $b Comparison B. + * @return bool + */ + function _sort_priority_callback( $a, $b ) { + if ( ! isset( $a['priority'], $b['priority'] ) || $a['priority'] === $b['priority'] ) { + return 0; + } + return ( $a['priority'] < $b['priority'] ) ? -1 : 1; + } + } + + uasort( $tabs, '_sort_priority_callback' ); + + return $tabs; + } +} + +if ( ! function_exists( 'woocommerce_comments' ) ) { + + /** + * Output the Review comments template. + * + * @param WP_Comment $comment Comment object. + * @param array $args Arguments. + * @param int $depth Depth. + */ + function woocommerce_comments( $comment, $args, $depth ) { + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $GLOBALS['comment'] = $comment; + wc_get_template( + 'single-product/review.php', + array( + 'comment' => $comment, + 'args' => $args, + 'depth' => $depth, + ) + ); + } +} + +if ( ! function_exists( 'woocommerce_review_display_gravatar' ) ) { + /** + * Display the review authors gravatar + * + * @param array $comment WP_Comment. + * @return void + */ + function woocommerce_review_display_gravatar( $comment ) { + echo get_avatar( $comment, apply_filters( 'woocommerce_review_gravatar_size', '60' ), '' ); + } +} + +if ( ! function_exists( 'woocommerce_review_display_rating' ) ) { + /** + * Display the reviewers star rating + * + * @return void + */ + function woocommerce_review_display_rating() { + if ( post_type_supports( 'product', 'comments' ) ) { + wc_get_template( 'single-product/review-rating.php' ); + } + } +} + +if ( ! function_exists( 'woocommerce_review_display_meta' ) ) { + /** + * Display the review authors meta (name, verified owner, review date) + * + * @return void + */ + function woocommerce_review_display_meta() { + wc_get_template( 'single-product/review-meta.php' ); + } +} + +if ( ! function_exists( 'woocommerce_review_display_comment_text' ) ) { + + /** + * Display the review content. + */ + function woocommerce_review_display_comment_text() { + echo '
    '; + comment_text(); + echo '
    '; + } +} + +if ( ! function_exists( 'woocommerce_output_related_products' ) ) { + + /** + * Output the related products. + */ + function woocommerce_output_related_products() { + + $args = array( + 'posts_per_page' => 4, + 'columns' => 4, + 'orderby' => 'rand', // @codingStandardsIgnoreLine. + ); + + woocommerce_related_products( apply_filters( 'woocommerce_output_related_products_args', $args ) ); + } +} + +if ( ! function_exists( 'woocommerce_related_products' ) ) { + + /** + * Output the related products. + * + * @param array $args Provided arguments. + */ + function woocommerce_related_products( $args = array() ) { + global $product; + + if ( ! $product ) { + return; + } + + $defaults = array( + 'posts_per_page' => 2, + 'columns' => 2, + 'orderby' => 'rand', // @codingStandardsIgnoreLine. + 'order' => 'desc', + ); + + $args = wp_parse_args( $args, $defaults ); + + // Get visible related products then sort them at random. + $args['related_products'] = array_filter( array_map( 'wc_get_product', wc_get_related_products( $product->get_id(), $args['posts_per_page'], $product->get_upsell_ids() ) ), 'wc_products_array_filter_visible' ); + + // Handle orderby. + $args['related_products'] = wc_products_array_orderby( $args['related_products'], $args['orderby'], $args['order'] ); + + // Set global loop values. + wc_set_loop_prop( 'name', 'related' ); + wc_set_loop_prop( 'columns', apply_filters( 'woocommerce_related_products_columns', $args['columns'] ) ); + + wc_get_template( 'single-product/related.php', $args ); + } +} + +if ( ! function_exists( 'woocommerce_upsell_display' ) ) { + + /** + * Output product up sells. + * + * @param int $limit (default: -1). + * @param int $columns (default: 4). + * @param string $orderby Supported values - rand, title, ID, date, modified, menu_order, price. + * @param string $order Sort direction. + */ + function woocommerce_upsell_display( $limit = '-1', $columns = 4, $orderby = 'rand', $order = 'desc' ) { + global $product; + + if ( ! $product ) { + return; + } + + // Handle the legacy filter which controlled posts per page etc. + $args = apply_filters( + 'woocommerce_upsell_display_args', + array( + 'posts_per_page' => $limit, + 'orderby' => $orderby, + 'order' => $order, + 'columns' => $columns, + ) + ); + wc_set_loop_prop( 'name', 'up-sells' ); + wc_set_loop_prop( 'columns', apply_filters( 'woocommerce_upsells_columns', isset( $args['columns'] ) ? $args['columns'] : $columns ) ); + + $orderby = apply_filters( 'woocommerce_upsells_orderby', isset( $args['orderby'] ) ? $args['orderby'] : $orderby ); + $order = apply_filters( 'woocommerce_upsells_order', isset( $args['order'] ) ? $args['order'] : $order ); + $limit = apply_filters( 'woocommerce_upsells_total', isset( $args['posts_per_page'] ) ? $args['posts_per_page'] : $limit ); + + // Get visible upsells then sort them at random, then limit result set. + $upsells = wc_products_array_orderby( array_filter( array_map( 'wc_get_product', $product->get_upsell_ids() ), 'wc_products_array_filter_visible' ), $orderby, $order ); + $upsells = $limit > 0 ? array_slice( $upsells, 0, $limit ) : $upsells; + + wc_get_template( + 'single-product/up-sells.php', + array( + 'upsells' => $upsells, + + // Not used now, but used in previous version of up-sells.php. + 'posts_per_page' => $limit, + 'orderby' => $orderby, + 'columns' => $columns, + ) + ); + } +} + +/** Cart */ + +if ( ! function_exists( 'woocommerce_shipping_calculator' ) ) { + + /** + * Output the cart shipping calculator. + * + * @param string $button_text Text for the shipping calculation toggle. + */ + function woocommerce_shipping_calculator( $button_text = '' ) { + if ( 'no' === get_option( 'woocommerce_enable_shipping_calc' ) || ! WC()->cart->needs_shipping() ) { + return; + } + wp_enqueue_script( 'wc-country-select' ); + wc_get_template( + 'cart/shipping-calculator.php', + array( + 'button_text' => $button_text, + ) + ); + } +} + +if ( ! function_exists( 'woocommerce_cart_totals' ) ) { + + /** + * Output the cart totals. + */ + function woocommerce_cart_totals() { + if ( is_checkout() ) { + return; + } + wc_get_template( 'cart/cart-totals.php' ); + } +} + +if ( ! function_exists( 'woocommerce_cross_sell_display' ) ) { + + /** + * Output the cart cross-sells. + * + * @param int $limit (default: 2). + * @param int $columns (default: 2). + * @param string $orderby (default: 'rand'). + * @param string $order (default: 'desc'). + */ + function woocommerce_cross_sell_display( $limit = 2, $columns = 2, $orderby = 'rand', $order = 'desc' ) { + if ( is_checkout() ) { + return; + } + // Get visible cross sells then sort them at random. + $cross_sells = array_filter( array_map( 'wc_get_product', WC()->cart->get_cross_sells() ), 'wc_products_array_filter_visible' ); + + wc_set_loop_prop( 'name', 'cross-sells' ); + wc_set_loop_prop( 'columns', apply_filters( 'woocommerce_cross_sells_columns', $columns ) ); + + // Handle orderby and limit results. + $orderby = apply_filters( 'woocommerce_cross_sells_orderby', $orderby ); + $order = apply_filters( 'woocommerce_cross_sells_order', $order ); + $cross_sells = wc_products_array_orderby( $cross_sells, $orderby, $order ); + $limit = apply_filters( 'woocommerce_cross_sells_total', $limit ); + $cross_sells = $limit > 0 ? array_slice( $cross_sells, 0, $limit ) : $cross_sells; + + wc_get_template( + 'cart/cross-sells.php', + array( + 'cross_sells' => $cross_sells, + + // Not used now, but used in previous version of up-sells.php. + 'posts_per_page' => $limit, + 'orderby' => $orderby, + 'columns' => $columns, + ) + ); + } +} + +if ( ! function_exists( 'woocommerce_button_proceed_to_checkout' ) ) { + + /** + * Output the proceed to checkout button. + */ + function woocommerce_button_proceed_to_checkout() { + wc_get_template( 'cart/proceed-to-checkout-button.php' ); + } +} + +if ( ! function_exists( 'woocommerce_widget_shopping_cart_button_view_cart' ) ) { + + /** + * Output the view cart button. + */ + function woocommerce_widget_shopping_cart_button_view_cart() { + echo '' . esc_html__( 'View cart', 'woocommerce' ) . ''; + } +} + +if ( ! function_exists( 'woocommerce_widget_shopping_cart_proceed_to_checkout' ) ) { + + /** + * Output the proceed to checkout button. + */ + function woocommerce_widget_shopping_cart_proceed_to_checkout() { + echo '' . esc_html__( 'Checkout', 'woocommerce' ) . ''; + } +} + +if ( ! function_exists( 'woocommerce_widget_shopping_cart_subtotal' ) ) { + /** + * Output to view cart subtotal. + * + * @since 3.7.0 + */ + function woocommerce_widget_shopping_cart_subtotal() { + echo '' . esc_html__( 'Subtotal:', 'woocommerce' ) . ' ' . WC()->cart->get_cart_subtotal(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } +} + +/** Mini-Cart */ + +if ( ! function_exists( 'woocommerce_mini_cart' ) ) { + + /** + * Output the Mini-cart - used by cart widget. + * + * @param array $args Arguments. + */ + function woocommerce_mini_cart( $args = array() ) { + + $defaults = array( + 'list_class' => '', + ); + + $args = wp_parse_args( $args, $defaults ); + + wc_get_template( 'cart/mini-cart.php', $args ); + } +} + +/** Login */ + +if ( ! function_exists( 'woocommerce_login_form' ) ) { + + /** + * Output the WooCommerce Login Form. + * + * @param array $args Arguments. + */ + function woocommerce_login_form( $args = array() ) { + + $defaults = array( + 'message' => '', + 'redirect' => '', + 'hidden' => false, + ); + + $args = wp_parse_args( $args, $defaults ); + + wc_get_template( 'global/form-login.php', $args ); + } +} + +if ( ! function_exists( 'woocommerce_checkout_login_form' ) ) { + + /** + * Output the WooCommerce Checkout Login Form. + */ + function woocommerce_checkout_login_form() { + wc_get_template( + 'checkout/form-login.php', + array( + 'checkout' => WC()->checkout(), + ) + ); + } +} + +if ( ! function_exists( 'woocommerce_breadcrumb' ) ) { + + /** + * Output the WooCommerce Breadcrumb. + * + * @param array $args Arguments. + */ + function woocommerce_breadcrumb( $args = array() ) { + $args = wp_parse_args( + $args, + apply_filters( + 'woocommerce_breadcrumb_defaults', + array( + 'delimiter' => ' / ', + 'wrap_before' => '', + 'before' => '', + 'after' => '', + 'home' => _x( 'Home', 'breadcrumb', 'woocommerce' ), + ) + ) + ); + + $breadcrumbs = new WC_Breadcrumb(); + + if ( ! empty( $args['home'] ) ) { + $breadcrumbs->add_crumb( $args['home'], apply_filters( 'woocommerce_breadcrumb_home_url', home_url() ) ); + } + + $args['breadcrumb'] = $breadcrumbs->generate(); + + /** + * WooCommerce Breadcrumb hook + * + * @hooked WC_Structured_Data::generate_breadcrumblist_data() - 10 + */ + do_action( 'woocommerce_breadcrumb', $breadcrumbs, $args ); + + wc_get_template( 'global/breadcrumb.php', $args ); + } +} + +if ( ! function_exists( 'woocommerce_order_review' ) ) { + + /** + * Output the Order review table for the checkout. + * + * @param bool $deprecated Deprecated param. + */ + function woocommerce_order_review( $deprecated = false ) { + wc_get_template( + 'checkout/review-order.php', + array( + 'checkout' => WC()->checkout(), + ) + ); + } +} + +if ( ! function_exists( 'woocommerce_checkout_payment' ) ) { + + /** + * Output the Payment Methods on the checkout. + */ + function woocommerce_checkout_payment() { + if ( WC()->cart->needs_payment() ) { + $available_gateways = WC()->payment_gateways()->get_available_payment_gateways(); + WC()->payment_gateways()->set_current_gateway( $available_gateways ); + } else { + $available_gateways = array(); + } + + wc_get_template( + 'checkout/payment.php', + array( + 'checkout' => WC()->checkout(), + 'available_gateways' => $available_gateways, + 'order_button_text' => apply_filters( 'woocommerce_order_button_text', __( 'Place order', 'woocommerce' ) ), + ) + ); + } +} + +if ( ! function_exists( 'woocommerce_checkout_coupon_form' ) ) { + + /** + * Output the Coupon form for the checkout. + */ + function woocommerce_checkout_coupon_form() { + if ( is_user_logged_in() || WC()->checkout()->is_registration_enabled() || ! WC()->checkout()->is_registration_required() ) { + wc_get_template( + 'checkout/form-coupon.php', + array( + 'checkout' => WC()->checkout(), + ) + ); + } + } +} + +if ( ! function_exists( 'woocommerce_products_will_display' ) ) { + + /** + * Check if we will be showing products or not (and not sub-categories only). + * + * @return bool + */ + function woocommerce_products_will_display() { + $display_type = woocommerce_get_loop_display_mode(); + + return 0 < wc_get_loop_prop( 'total', 0 ) && 'subcategories' !== $display_type; + } +} + +if ( ! function_exists( 'woocommerce_get_loop_display_mode' ) ) { + + /** + * See what is going to display in the loop. + * + * @since 3.3.0 + * @return string Either products, subcategories, or both, based on current page. + */ + function woocommerce_get_loop_display_mode() { + // Only return products when filtering things. + if ( wc_get_loop_prop( 'is_search' ) || wc_get_loop_prop( 'is_filtered' ) ) { + return 'products'; + } + + $parent_id = 0; + $display_type = ''; + + if ( is_shop() ) { + $display_type = get_option( 'woocommerce_shop_page_display', '' ); + } elseif ( is_product_category() ) { + $parent_id = get_queried_object_id(); + $display_type = get_term_meta( $parent_id, 'display_type', true ); + $display_type = '' === $display_type ? get_option( 'woocommerce_category_archive_display', '' ) : $display_type; + } + + if ( ( ! is_shop() || 'subcategories' !== $display_type ) && 1 < wc_get_loop_prop( 'current_page' ) ) { + return 'products'; + } + + // Ensure valid value. + if ( '' === $display_type || ! in_array( $display_type, array( 'products', 'subcategories', 'both' ), true ) ) { + $display_type = 'products'; + } + + // If we're showing categories, ensure we actually have something to show. + if ( in_array( $display_type, array( 'subcategories', 'both' ), true ) ) { + $subcategories = woocommerce_get_product_subcategories( $parent_id ); + + if ( empty( $subcategories ) ) { + $display_type = 'products'; + } + } + + return $display_type; + } +} + +if ( ! function_exists( 'woocommerce_maybe_show_product_subcategories' ) ) { + + /** + * Maybe display categories before, or instead of, a product loop. + * + * @since 3.3.0 + * @param string $loop_html HTML. + * @return string + */ + function woocommerce_maybe_show_product_subcategories( $loop_html = '' ) { + if ( wc_get_loop_prop( 'is_shortcode' ) && ! WC_Template_Loader::in_content_filter() ) { + return $loop_html; + } + + $display_type = woocommerce_get_loop_display_mode(); + + // If displaying categories, append to the loop. + if ( 'subcategories' === $display_type || 'both' === $display_type ) { + ob_start(); + woocommerce_output_product_categories( + array( + 'parent_id' => is_product_category() ? get_queried_object_id() : 0, + ) + ); + $loop_html .= ob_get_clean(); + + if ( 'subcategories' === $display_type ) { + wc_set_loop_prop( 'total', 0 ); + + // This removes pagination and products from display for themes not using wc_get_loop_prop in their product loops. @todo Remove in future major version. + global $wp_query; + + if ( $wp_query->is_main_query() ) { + $wp_query->post_count = 0; + $wp_query->max_num_pages = 0; + } + } + } + + return $loop_html; + } +} + +if ( ! function_exists( 'woocommerce_product_subcategories' ) ) { + /** + * This is a legacy function which used to check if we needed to display subcats and then output them. It was called by templates. + * + * From 3.3 onwards this is all handled via hooks and the woocommerce_maybe_show_product_subcategories function. + * + * Since some templates have not updated compatibility, to avoid showing incorrect categories this function has been deprecated and will + * return nothing. Replace usage with woocommerce_output_product_categories to render the category list manually. + * + * This is a legacy function which also checks if things should display. + * Themes no longer need to call these functions. It's all done via hooks. + * + * @deprecated 3.3.1 @todo Add a notice in a future version. + * @param array $args Arguments. + * @return null|boolean + */ + function woocommerce_product_subcategories( $args = array() ) { + $defaults = array( + 'before' => '', + 'after' => '', + 'force_display' => false, + ); + + $args = wp_parse_args( $args, $defaults ); + + if ( $args['force_display'] ) { + // We can still render if display is forced. + woocommerce_output_product_categories( + array( + 'before' => $args['before'], + 'after' => $args['after'], + 'parent_id' => is_product_category() ? get_queried_object_id() : 0, + ) + ); + return true; + } else { + // Output nothing. woocommerce_maybe_show_product_subcategories will handle the output of cats. + $display_type = woocommerce_get_loop_display_mode(); + + if ( 'subcategories' === $display_type ) { + // This removes pagination and products from display for themes not using wc_get_loop_prop in their product loops. @todo Remove in future major version. + global $wp_query; + + if ( $wp_query->is_main_query() ) { + $wp_query->post_count = 0; + $wp_query->max_num_pages = 0; + } + } + + return 'subcategories' === $display_type || 'both' === $display_type; + } + } +} + +if ( ! function_exists( 'woocommerce_output_product_categories' ) ) { + /** + * Display product sub categories as thumbnails. + * + * This is a replacement for woocommerce_product_subcategories which also does some logic + * based on the loop. This function however just outputs when called. + * + * @since 3.3.1 + * @param array $args Arguments. + * @return boolean + */ + function woocommerce_output_product_categories( $args = array() ) { + $args = wp_parse_args( + $args, + array( + 'before' => apply_filters( 'woocommerce_before_output_product_categories', '' ), + 'after' => apply_filters( 'woocommerce_after_output_product_categories', '' ), + 'parent_id' => 0, + ) + ); + + $product_categories = woocommerce_get_product_subcategories( $args['parent_id'] ); + + if ( ! $product_categories ) { + return false; + } + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $args['before']; + + foreach ( $product_categories as $category ) { + wc_get_template( + 'content-product_cat.php', + array( + 'category' => $category, + ) + ); + } + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $args['after']; + + return true; + } +} + +if ( ! function_exists( 'woocommerce_get_product_subcategories' ) ) { + /** + * Get (and cache) product subcategories. + * + * @param int $parent_id Get subcategories of this ID. + * @return array + */ + function woocommerce_get_product_subcategories( $parent_id = 0 ) { + $parent_id = absint( $parent_id ); + $cache_key = apply_filters( 'woocommerce_get_product_subcategories_cache_key', 'product-category-hierarchy-' . $parent_id, $parent_id ); + $product_categories = $cache_key ? wp_cache_get( $cache_key, 'product_cat' ) : false; + + if ( false === $product_categories ) { + // NOTE: using child_of instead of parent - this is not ideal but due to a WP bug ( https://core.trac.wordpress.org/ticket/15626 ) pad_counts won't work. + $product_categories = get_categories( + apply_filters( + 'woocommerce_product_subcategories_args', + array( + 'parent' => $parent_id, + 'hide_empty' => 0, + 'hierarchical' => 1, + 'taxonomy' => 'product_cat', + 'pad_counts' => 1, + ) + ) + ); + + if ( $cache_key ) { + wp_cache_set( $cache_key, $product_categories, 'product_cat' ); + } + } + + if ( apply_filters( 'woocommerce_product_subcategories_hide_empty', true ) ) { + $product_categories = wp_list_filter( $product_categories, array( 'count' => 0 ), 'NOT' ); + } + + return $product_categories; + } +} + +if ( ! function_exists( 'woocommerce_subcategory_thumbnail' ) ) { + + /** + * Show subcategory thumbnails. + * + * @param mixed $category Category. + */ + function woocommerce_subcategory_thumbnail( $category ) { + $small_thumbnail_size = apply_filters( 'subcategory_archive_thumbnail_size', 'woocommerce_thumbnail' ); + $dimensions = wc_get_image_size( $small_thumbnail_size ); + $thumbnail_id = get_term_meta( $category->term_id, 'thumbnail_id', true ); + + if ( $thumbnail_id ) { + $image = wp_get_attachment_image_src( $thumbnail_id, $small_thumbnail_size ); + $image = $image[0]; + $image_srcset = function_exists( 'wp_get_attachment_image_srcset' ) ? wp_get_attachment_image_srcset( $thumbnail_id, $small_thumbnail_size ) : false; + $image_sizes = function_exists( 'wp_get_attachment_image_sizes' ) ? wp_get_attachment_image_sizes( $thumbnail_id, $small_thumbnail_size ) : false; + } else { + $image = wc_placeholder_img_src(); + $image_srcset = false; + $image_sizes = false; + } + + if ( $image ) { + // Prevent esc_url from breaking spaces in urls for image embeds. + // Ref: https://core.trac.wordpress.org/ticket/23605. + $image = str_replace( ' ', '%20', $image ); + + // Add responsive image markup if available. + if ( $image_srcset && $image_sizes ) { + echo '' . esc_attr( $category->name ) . ''; + } else { + echo '' . esc_attr( $category->name ) . ''; + } + } + } +} + +if ( ! function_exists( 'woocommerce_order_details_table' ) ) { + + /** + * Displays order details in a table. + * + * @param mixed $order_id Order ID. + */ + function woocommerce_order_details_table( $order_id ) { + if ( ! $order_id ) { + return; + } + + wc_get_template( + 'order/order-details.php', + array( + 'order_id' => $order_id, + ) + ); + } +} + +if ( ! function_exists( 'woocommerce_order_downloads_table' ) ) { + + /** + * Displays order downloads in a table. + * + * @since 3.2.0 + * @param array $downloads Downloads. + */ + function woocommerce_order_downloads_table( $downloads ) { + if ( ! $downloads ) { + return; + } + wc_get_template( + 'order/order-downloads.php', + array( + 'downloads' => $downloads, + ) + ); + } +} + +if ( ! function_exists( 'woocommerce_order_again_button' ) ) { + + /** + * Display an 'order again' button on the view order page. + * + * @param object $order Order. + */ + function woocommerce_order_again_button( $order ) { + if ( ! $order || ! $order->has_status( apply_filters( 'woocommerce_valid_order_statuses_for_order_again', array( 'completed' ) ) ) || ! is_user_logged_in() ) { + return; + } + + wc_get_template( + 'order/order-again.php', + array( + 'order' => $order, + 'order_again_url' => wp_nonce_url( add_query_arg( 'order_again', $order->get_id(), wc_get_cart_url() ), 'woocommerce-order_again' ), + ) + ); + } +} + +/** Forms */ + +if ( ! function_exists( 'woocommerce_form_field' ) ) { + + /** + * Outputs a checkout/address form field. + * + * @param string $key Key. + * @param mixed $args Arguments. + * @param string $value (default: null). + * @return string + */ + function woocommerce_form_field( $key, $args, $value = null ) { + $defaults = array( + 'type' => 'text', + 'label' => '', + 'description' => '', + 'placeholder' => '', + 'maxlength' => false, + 'required' => false, + 'autocomplete' => false, + 'id' => $key, + 'class' => array(), + 'label_class' => array(), + 'input_class' => array(), + 'return' => false, + 'options' => array(), + 'custom_attributes' => array(), + 'validate' => array(), + 'default' => '', + 'autofocus' => '', + 'priority' => '', + ); + + $args = wp_parse_args( $args, $defaults ); + $args = apply_filters( 'woocommerce_form_field_args', $args, $key, $value ); + + if ( $args['required'] ) { + $args['class'][] = 'validate-required'; + $required = ' *'; + } else { + $required = ' (' . esc_html__( 'optional', 'woocommerce' ) . ')'; + } + + if ( is_string( $args['label_class'] ) ) { + $args['label_class'] = array( $args['label_class'] ); + } + + if ( is_null( $value ) ) { + $value = $args['default']; + } + + // Custom attribute handling. + $custom_attributes = array(); + $args['custom_attributes'] = array_filter( (array) $args['custom_attributes'], 'strlen' ); + + if ( $args['maxlength'] ) { + $args['custom_attributes']['maxlength'] = absint( $args['maxlength'] ); + } + + if ( ! empty( $args['autocomplete'] ) ) { + $args['custom_attributes']['autocomplete'] = $args['autocomplete']; + } + + if ( true === $args['autofocus'] ) { + $args['custom_attributes']['autofocus'] = 'autofocus'; + } + + if ( $args['description'] ) { + $args['custom_attributes']['aria-describedby'] = $args['id'] . '-description'; + } + + if ( ! empty( $args['custom_attributes'] ) && is_array( $args['custom_attributes'] ) ) { + foreach ( $args['custom_attributes'] as $attribute => $attribute_value ) { + $custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $attribute_value ) . '"'; + } + } + + if ( ! empty( $args['validate'] ) ) { + foreach ( $args['validate'] as $validate ) { + $args['class'][] = 'validate-' . $validate; + } + } + + $field = ''; + $label_id = $args['id']; + $sort = $args['priority'] ? $args['priority'] : ''; + $field_container = '

    %3$s

    '; + + switch ( $args['type'] ) { + case 'country': + $countries = 'shipping_country' === $key ? WC()->countries->get_shipping_countries() : WC()->countries->get_allowed_countries(); + + if ( 1 === count( $countries ) ) { + + $field .= '' . current( array_values( $countries ) ) . ''; + + $field .= ''; + + } else { + $data_label = ! empty( $args['label'] ) ? 'data-label="' . esc_attr( $args['label'] ) . '"' : ''; + + $field = ''; + + $field .= ''; + + } + + break; + case 'state': + /* Get country this state field is representing */ + $for_country = isset( $args['country'] ) ? $args['country'] : WC()->checkout->get_value( 'billing_state' === $key ? 'billing_country' : 'shipping_country' ); + $states = WC()->countries->get_states( $for_country ); + + if ( is_array( $states ) && empty( $states ) ) { + + $field_container = ''; + + $field .= ''; + + } elseif ( ! is_null( $for_country ) && is_array( $states ) ) { + $data_label = ! empty( $args['label'] ) ? 'data-label="' . esc_attr( $args['label'] ) . '"' : ''; + + $field .= ''; + + } else { + + $field .= ''; + + } + + break; + case 'textarea': + $field .= ''; + + break; + case 'checkbox': + $field = ''; + + break; + case 'text': + case 'password': + case 'datetime': + case 'datetime-local': + case 'date': + case 'month': + case 'time': + case 'week': + case 'number': + case 'email': + case 'url': + case 'tel': + $field .= ''; + + break; + case 'hidden': + $field .= ''; + + break; + case 'select': + $field = ''; + $options = ''; + + if ( ! empty( $args['options'] ) ) { + foreach ( $args['options'] as $option_key => $option_text ) { + if ( '' === $option_key ) { + // If we have a blank option, select2 needs a placeholder. + if ( empty( $args['placeholder'] ) ) { + $args['placeholder'] = $option_text ? $option_text : __( 'Choose an option', 'woocommerce' ); + } + $custom_attributes[] = 'data-allow_clear="true"'; + } + $options .= ''; + } + + $field .= ''; + } + + break; + case 'radio': + $label_id .= '_' . current( array_keys( $args['options'] ) ); + + if ( ! empty( $args['options'] ) ) { + foreach ( $args['options'] as $option_key => $option_text ) { + $field .= ''; + $field .= ''; + } + } + + break; + } + + if ( ! empty( $field ) ) { + $field_html = ''; + + if ( $args['label'] && 'checkbox' !== $args['type'] ) { + $field_html .= ''; + } + + $field_html .= '' . $field; + + if ( $args['description'] ) { + $field_html .= ''; + } + + $field_html .= ''; + + $container_class = esc_attr( implode( ' ', $args['class'] ) ); + $container_id = esc_attr( $args['id'] ) . '_field'; + $field = sprintf( $field_container, $container_class, $container_id, $field_html ); + } + + /** + * Filter by type. + */ + $field = apply_filters( 'woocommerce_form_field_' . $args['type'], $field, $key, $args, $value ); + + /** + * General filter on form fields. + * + * @since 3.4.0 + */ + $field = apply_filters( 'woocommerce_form_field', $field, $key, $args, $value ); + + if ( $args['return'] ) { + return $field; + } else { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $field; + } + } +} + +if ( ! function_exists( 'get_product_search_form' ) ) { + + /** + * Display product search form. + * + * Will first attempt to locate the product-searchform.php file in either the child or. + * the parent, then load it. If it doesn't exist, then the default search form. + * will be displayed. + * + * The default searchform uses html5. + * + * @param bool $echo (default: true). + * @return string + */ + function get_product_search_form( $echo = true ) { + global $product_search_form_index; + + ob_start(); + + if ( empty( $product_search_form_index ) ) { + $product_search_form_index = 0; + } + + do_action( 'pre_get_product_search_form' ); + + wc_get_template( + 'product-searchform.php', + array( + 'index' => $product_search_form_index++, + ) + ); + + $form = apply_filters( 'get_product_search_form', ob_get_clean() ); + + if ( ! $echo ) { + return $form; + } + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $form; + } +} + +if ( ! function_exists( 'woocommerce_output_auth_header' ) ) { + + /** + * Output the Auth header. + */ + function woocommerce_output_auth_header() { + wc_get_template( 'auth/header.php' ); + } +} + +if ( ! function_exists( 'woocommerce_output_auth_footer' ) ) { + + /** + * Output the Auth footer. + */ + function woocommerce_output_auth_footer() { + wc_get_template( 'auth/footer.php' ); + } +} + +if ( ! function_exists( 'woocommerce_single_variation' ) ) { + + /** + * Output placeholders for the single variation. + */ + function woocommerce_single_variation() { + echo '
    '; + } +} + +if ( ! function_exists( 'woocommerce_single_variation_add_to_cart_button' ) ) { + + /** + * Output the add to cart button for variations. + */ + function woocommerce_single_variation_add_to_cart_button() { + wc_get_template( 'single-product/add-to-cart/variation-add-to-cart-button.php' ); + } +} + +if ( ! function_exists( 'wc_dropdown_variation_attribute_options' ) ) { + + /** + * Output a list of variation attributes for use in the cart forms. + * + * @param array $args Arguments. + * @since 2.4.0 + */ + function wc_dropdown_variation_attribute_options( $args = array() ) { + $args = wp_parse_args( + apply_filters( 'woocommerce_dropdown_variation_attribute_options_args', $args ), + array( + 'options' => false, + 'attribute' => false, + 'product' => false, + 'selected' => false, + 'name' => '', + 'id' => '', + 'class' => '', + 'show_option_none' => __( 'Choose an option', 'woocommerce' ), + ) + ); + + // Get selected value. + if ( false === $args['selected'] && $args['attribute'] && $args['product'] instanceof WC_Product ) { + $selected_key = 'attribute_' . sanitize_title( $args['attribute'] ); + // phpcs:disable WordPress.Security.NonceVerification.Recommended + $args['selected'] = isset( $_REQUEST[ $selected_key ] ) ? wc_clean( wp_unslash( $_REQUEST[ $selected_key ] ) ) : $args['product']->get_variation_default_attribute( $args['attribute'] ); + // phpcs:enable WordPress.Security.NonceVerification.Recommended + } + + $options = $args['options']; + $product = $args['product']; + $attribute = $args['attribute']; + $name = $args['name'] ? $args['name'] : 'attribute_' . sanitize_title( $attribute ); + $id = $args['id'] ? $args['id'] : sanitize_title( $attribute ); + $class = $args['class']; + $show_option_none = (bool) $args['show_option_none']; + $show_option_none_text = $args['show_option_none'] ? $args['show_option_none'] : __( 'Choose an option', 'woocommerce' ); // We'll do our best to hide the placeholder, but we'll need to show something when resetting options. + + if ( empty( $options ) && ! empty( $product ) && ! empty( $attribute ) ) { + $attributes = $product->get_variation_attributes(); + $options = $attributes[ $attribute ]; + } + + $html = ''; + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo apply_filters( 'woocommerce_dropdown_variation_attribute_options_html', $html, $args ); + } +} + +if ( ! function_exists( 'woocommerce_account_content' ) ) { + + /** + * My Account content output. + */ + function woocommerce_account_content() { + global $wp; + + if ( ! empty( $wp->query_vars ) ) { + foreach ( $wp->query_vars as $key => $value ) { + // Ignore pagename param. + if ( 'pagename' === $key ) { + continue; + } + + if ( has_action( 'woocommerce_account_' . $key . '_endpoint' ) ) { + do_action( 'woocommerce_account_' . $key . '_endpoint', $value ); + return; + } + } + } + + // No endpoint found? Default to dashboard. + wc_get_template( + 'myaccount/dashboard.php', + array( + 'current_user' => get_user_by( 'id', get_current_user_id() ), + ) + ); + } +} + +if ( ! function_exists( 'woocommerce_account_navigation' ) ) { + + /** + * My Account navigation template. + */ + function woocommerce_account_navigation() { + wc_get_template( 'myaccount/navigation.php' ); + } +} + +if ( ! function_exists( 'woocommerce_account_orders' ) ) { + + /** + * My Account > Orders template. + * + * @param int $current_page Current page number. + */ + function woocommerce_account_orders( $current_page ) { + $current_page = empty( $current_page ) ? 1 : absint( $current_page ); + $customer_orders = wc_get_orders( + apply_filters( + 'woocommerce_my_account_my_orders_query', + array( + 'customer' => get_current_user_id(), + 'page' => $current_page, + 'paginate' => true, + ) + ) + ); + + wc_get_template( + 'myaccount/orders.php', + array( + 'current_page' => absint( $current_page ), + 'customer_orders' => $customer_orders, + 'has_orders' => 0 < $customer_orders->total, + ) + ); + } +} + +if ( ! function_exists( 'woocommerce_account_view_order' ) ) { + + /** + * My Account > View order template. + * + * @param int $order_id Order ID. + */ + function woocommerce_account_view_order( $order_id ) { + WC_Shortcode_My_Account::view_order( absint( $order_id ) ); + } +} + +if ( ! function_exists( 'woocommerce_account_downloads' ) ) { + + /** + * My Account > Downloads template. + */ + function woocommerce_account_downloads() { + wc_get_template( 'myaccount/downloads.php' ); + } +} + +if ( ! function_exists( 'woocommerce_account_edit_address' ) ) { + + /** + * My Account > Edit address template. + * + * @param string $type Address type. + */ + function woocommerce_account_edit_address( $type ) { + $type = wc_edit_address_i18n( sanitize_title( $type ), true ); + + WC_Shortcode_My_Account::edit_address( $type ); + } +} + +if ( ! function_exists( 'woocommerce_account_payment_methods' ) ) { + + /** + * My Account > Downloads template. + */ + function woocommerce_account_payment_methods() { + wc_get_template( 'myaccount/payment-methods.php' ); + } +} + +if ( ! function_exists( 'woocommerce_account_add_payment_method' ) ) { + + /** + * My Account > Add payment method template. + */ + function woocommerce_account_add_payment_method() { + WC_Shortcode_My_Account::add_payment_method(); + } +} + +if ( ! function_exists( 'woocommerce_account_edit_account' ) ) { + + /** + * My Account > Edit account template. + */ + function woocommerce_account_edit_account() { + WC_Shortcode_My_Account::edit_account(); + } +} + +if ( ! function_exists( 'wc_no_products_found' ) ) { + + /** + * Handles the loop when no products were found/no product exist. + */ + function wc_no_products_found() { + wc_get_template( 'loop/no-products-found.php' ); + } +} + + +if ( ! function_exists( 'wc_get_email_order_items' ) ) { + /** + * Get HTML for the order items to be shown in emails. + * + * @param WC_Order $order Order object. + * @param array $args Arguments. + * + * @since 3.0.0 + * @return string + */ + function wc_get_email_order_items( $order, $args = array() ) { + ob_start(); + + $defaults = array( + 'show_sku' => false, + 'show_image' => false, + 'image_size' => array( 32, 32 ), + 'plain_text' => false, + 'sent_to_admin' => false, + ); + + $args = wp_parse_args( $args, $defaults ); + $template = $args['plain_text'] ? 'emails/plain/email-order-items.php' : 'emails/email-order-items.php'; + + wc_get_template( + $template, + apply_filters( + 'woocommerce_email_order_items_args', + array( + 'order' => $order, + 'items' => $order->get_items(), + 'show_download_links' => $order->is_download_permitted() && ! $args['sent_to_admin'], + 'show_sku' => $args['show_sku'], + 'show_purchase_note' => $order->is_paid() && ! $args['sent_to_admin'], + 'show_image' => $args['show_image'], + 'image_size' => $args['image_size'], + 'plain_text' => $args['plain_text'], + 'sent_to_admin' => $args['sent_to_admin'], + ) + ) + ); + + return apply_filters( 'woocommerce_email_order_items_table', ob_get_clean(), $order ); + } +} + +if ( ! function_exists( 'wc_display_item_meta' ) ) { + /** + * Display item meta data. + * + * @since 3.0.0 + * @param WC_Order_Item $item Order Item. + * @param array $args Arguments. + * @return string|void + */ + function wc_display_item_meta( $item, $args = array() ) { + $strings = array(); + $html = ''; + $args = wp_parse_args( + $args, + array( + 'before' => '
    • ', + 'after' => '
    ', + 'separator' => '
  • ', + 'echo' => true, + 'autop' => false, + 'label_before' => '', + 'label_after' => ': ', + ) + ); + + foreach ( $item->get_all_formatted_meta_data() as $meta_id => $meta ) { + $value = $args['autop'] ? wp_kses_post( $meta->display_value ) : wp_kses_post( make_clickable( trim( $meta->display_value ) ) ); + $strings[] = $args['label_before'] . wp_kses_post( $meta->display_key ) . $args['label_after'] . $value; + } + + if ( $strings ) { + $html = $args['before'] . implode( $args['separator'], $strings ) . $args['after']; + } + + $html = apply_filters( 'woocommerce_display_item_meta', $html, $item, $args ); + + if ( $args['echo'] ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $html; + } else { + return $html; + } + } +} + +if ( ! function_exists( 'wc_display_item_downloads' ) ) { + /** + * Display item download links. + * + * @since 3.0.0 + * @param WC_Order_Item $item Order Item. + * @param array $args Arguments. + * @return string|void + */ + function wc_display_item_downloads( $item, $args = array() ) { + $strings = array(); + $html = ''; + $args = wp_parse_args( + $args, + array( + 'before' => '
    • ', + 'after' => '
    ', + 'separator' => '
  • ', + 'echo' => true, + 'show_url' => false, + ) + ); + + $downloads = is_object( $item ) && $item->is_type( 'line_item' ) ? $item->get_item_downloads() : array(); + + if ( $downloads ) { + $i = 0; + foreach ( $downloads as $file ) { + $i ++; + + if ( $args['show_url'] ) { + $strings[] = '' . esc_html( $file['name'] ) . ': ' . esc_html( $file['download_url'] ); + } else { + /* translators: %d: downloads count */ + $prefix = count( $downloads ) > 1 ? sprintf( __( 'Download %d', 'woocommerce' ), $i ) : __( 'Download', 'woocommerce' ); + $strings[] = '' . $prefix . ': ' . esc_html( $file['name'] ) . ''; + } + } + } + + if ( $strings ) { + $html = $args['before'] . implode( $args['separator'], $strings ) . $args['after']; + } + + $html = apply_filters( 'woocommerce_display_item_downloads', $html, $item, $args ); + + if ( $args['echo'] ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $html; + } else { + return $html; + } + } +} + +if ( ! function_exists( 'woocommerce_photoswipe' ) ) { + + /** + * Get the shop sidebar template. + */ + function woocommerce_photoswipe() { + if ( current_theme_supports( 'wc-product-gallery-lightbox' ) ) { + wc_get_template( 'single-product/photoswipe.php' ); + } + } +} + +/** + * Outputs a list of product attributes for a product. + * + * @since 3.0.0 + * @param WC_Product $product Product Object. + */ +function wc_display_product_attributes( $product ) { + $product_attributes = array(); + + // Display weight and dimensions before attribute list. + $display_dimensions = apply_filters( 'wc_product_enable_dimensions_display', $product->has_weight() || $product->has_dimensions() ); + + if ( $display_dimensions && $product->has_weight() ) { + $product_attributes['weight'] = array( + 'label' => __( 'Weight', 'woocommerce' ), + 'value' => wc_format_weight( $product->get_weight() ), + ); + } + + if ( $display_dimensions && $product->has_dimensions() ) { + $product_attributes['dimensions'] = array( + 'label' => __( 'Dimensions', 'woocommerce' ), + 'value' => wc_format_dimensions( $product->get_dimensions( false ) ), + ); + } + + // Add product attributes to list. + $attributes = array_filter( $product->get_attributes(), 'wc_attributes_array_filter_visible' ); + + foreach ( $attributes as $attribute ) { + $values = array(); + + if ( $attribute->is_taxonomy() ) { + $attribute_taxonomy = $attribute->get_taxonomy_object(); + $attribute_values = wc_get_product_terms( $product->get_id(), $attribute->get_name(), array( 'fields' => 'all' ) ); + + foreach ( $attribute_values as $attribute_value ) { + $value_name = esc_html( $attribute_value->name ); + + if ( $attribute_taxonomy->attribute_public ) { + $values[] = ''; + } else { + $values[] = $value_name; + } + } + } else { + $values = $attribute->get_options(); + + foreach ( $values as &$value ) { + $value = make_clickable( esc_html( $value ) ); + } + } + + $product_attributes[ 'attribute_' . sanitize_title_with_dashes( $attribute->get_name() ) ] = array( + 'label' => wc_attribute_label( $attribute->get_name() ), + 'value' => apply_filters( 'woocommerce_attribute', wpautop( wptexturize( implode( ', ', $values ) ) ), $attribute, $values ), + ); + } + + /** + * Hook: woocommerce_display_product_attributes. + * + * @since 3.6.0. + * @param array $product_attributes Array of atributes to display; label, value. + * @param WC_Product $product Showing attributes for this product. + */ + $product_attributes = apply_filters( 'woocommerce_display_product_attributes', $product_attributes, $product ); + + wc_get_template( + 'single-product/product-attributes.php', + array( + 'product_attributes' => $product_attributes, + // Legacy params. + 'product' => $product, + 'attributes' => $attributes, + 'display_dimensions' => $display_dimensions, + ) + ); +} + +/** + * Get HTML to show product stock. + * + * @since 3.0.0 + * @param WC_Product $product Product Object. + * @return string + */ +function wc_get_stock_html( $product ) { + $html = ''; + $availability = $product->get_availability(); + + if ( ! empty( $availability['availability'] ) ) { + ob_start(); + + wc_get_template( + 'single-product/stock.php', + array( + 'product' => $product, + 'class' => $availability['class'], + 'availability' => $availability['availability'], + ) + ); + + $html = ob_get_clean(); + } + + if ( has_filter( 'woocommerce_stock_html' ) ) { + wc_deprecated_function( 'The woocommerce_stock_html filter', '', 'woocommerce_get_stock_html' ); + $html = apply_filters( 'woocommerce_stock_html', $html, $availability['availability'], $product ); + } + + return apply_filters( 'woocommerce_get_stock_html', $html, $product ); +} + +/** + * Get HTML for ratings. + * + * @since 3.0.0 + * @param float $rating Rating being shown. + * @param int $count Total number of ratings. + * @return string + */ +function wc_get_rating_html( $rating, $count = 0 ) { + $html = ''; + + if ( 0 < $rating ) { + /* translators: %s: rating */ + $label = sprintf( __( 'Rated %s out of 5', 'woocommerce' ), $rating ); + $html = ''; + } + + return apply_filters( 'woocommerce_product_get_rating_html', $html, $rating, $count ); +} + +/** + * Get HTML for star rating. + * + * @since 3.1.0 + * @param float $rating Rating being shown. + * @param int $count Total number of ratings. + * @return string + */ +function wc_get_star_rating_html( $rating, $count = 0 ) { + $html = ''; + + if ( 0 < $count ) { + /* translators: 1: rating 2: rating count */ + $html .= sprintf( _n( 'Rated %1$s out of 5 based on %2$s customer rating', 'Rated %1$s out of 5 based on %2$s customer ratings', $count, 'woocommerce' ), '' . esc_html( $rating ) . '', '' . esc_html( $count ) . '' ); + } else { + /* translators: %s: rating */ + $html .= sprintf( esc_html__( 'Rated %s out of 5', 'woocommerce' ), '' . esc_html( $rating ) . '' ); + } + + $html .= ''; + + return apply_filters( 'woocommerce_get_star_rating_html', $html, $rating, $count ); +} + +/** + * Returns a 'from' prefix if you want to show where prices start at. + * + * @since 3.0.0 + * @return string + */ +function wc_get_price_html_from_text() { + return apply_filters( 'woocommerce_get_price_html_from_text', '' . _x( 'From:', 'min_price', 'woocommerce' ) . ' ' ); +} + +/** + * Get logout endpoint. + * + * @since 2.6.9 + * + * @param string $redirect Redirect URL. + * + * @return string + */ +function wc_logout_url( $redirect = '' ) { + $redirect = $redirect ? $redirect : apply_filters( 'woocommerce_logout_default_redirect_url', wc_get_page_permalink( 'myaccount' ) ); + + if ( get_option( 'woocommerce_logout_endpoint' ) ) { + return wp_nonce_url( wc_get_endpoint_url( 'customer-logout', '', $redirect ), 'customer-logout' ); + } + + return wp_logout_url( $redirect ); +} + +/** + * Show notice if cart is empty. + * + * @since 3.1.0 + */ +function wc_empty_cart_message() { + echo '

    ' . wp_kses_post( apply_filters( 'wc_empty_cart_message', __( 'Your cart is currently empty.', 'woocommerce' ) ) ) . '

    '; +} + +/** + * Disable search engines indexing core, dynamic, cart/checkout pages. + * + * @todo Deprecated this function after dropping support for WP 5.6. + * @since 3.2.0 + */ +function wc_page_noindex() { + // wp_no_robots is deprecated since WP 5.7. + if ( function_exists( 'wp_robots_no_robots' ) ) { + return; + } + + if ( is_page( wc_get_page_id( 'cart' ) ) || is_page( wc_get_page_id( 'checkout' ) ) || is_page( wc_get_page_id( 'myaccount' ) ) ) { + wp_no_robots(); + } +} +add_action( 'wp_head', 'wc_page_noindex' ); + +/** + * Disable search engines indexing core, dynamic, cart/checkout pages. + * Uses "wp_robots" filter introduced in WP 5.7. + * + * @since 5.0.0 + * @param array $robots Associative array of robots directives. + * @return array Filtered robots directives. + */ +function wc_page_no_robots( $robots ) { + if ( is_page( wc_get_page_id( 'cart' ) ) || is_page( wc_get_page_id( 'checkout' ) ) || is_page( wc_get_page_id( 'myaccount' ) ) ) { + return wp_robots_no_robots( $robots ); + } + + return $robots; +} +add_filter( 'wp_robots', 'wc_page_no_robots' ); + +/** + * Get a slug identifying the current theme. + * + * @since 3.3.0 + * @return string + */ +function wc_get_theme_slug_for_templates() { + return apply_filters( 'woocommerce_theme_slug_for_templates', get_option( 'template' ) ); +} + +/** + * Gets and formats a list of cart item data + variations for display on the frontend. + * + * @since 3.3.0 + * @param array $cart_item Cart item object. + * @param bool $flat Should the data be returned flat or in a list. + * @return string + */ +function wc_get_formatted_cart_item_data( $cart_item, $flat = false ) { + $item_data = array(); + + // Variation values are shown only if they are not found in the title as of 3.0. + // This is because variation titles display the attributes. + if ( $cart_item['data']->is_type( 'variation' ) && is_array( $cart_item['variation'] ) ) { + foreach ( $cart_item['variation'] as $name => $value ) { + $taxonomy = wc_attribute_taxonomy_name( str_replace( 'attribute_pa_', '', urldecode( $name ) ) ); + + if ( taxonomy_exists( $taxonomy ) ) { + // If this is a term slug, get the term's nice name. + $term = get_term_by( 'slug', $value, $taxonomy ); + if ( ! is_wp_error( $term ) && $term && $term->name ) { + $value = $term->name; + } + $label = wc_attribute_label( $taxonomy ); + } else { + // If this is a custom option slug, get the options name. + $value = apply_filters( 'woocommerce_variation_option_name', $value, null, $taxonomy, $cart_item['data'] ); + $label = wc_attribute_label( str_replace( 'attribute_', '', $name ), $cart_item['data'] ); + } + + // Check the nicename against the title. + if ( '' === $value || wc_is_attribute_in_product_name( $value, $cart_item['data']->get_name() ) ) { + continue; + } + + $item_data[] = array( + 'key' => $label, + 'value' => $value, + ); + } + } + + // Filter item data to allow 3rd parties to add more to the array. + $item_data = apply_filters( 'woocommerce_get_item_data', $item_data, $cart_item ); + + // Format item data ready to display. + foreach ( $item_data as $key => $data ) { + // Set hidden to true to not display meta on cart. + if ( ! empty( $data['hidden'] ) ) { + unset( $item_data[ $key ] ); + continue; + } + $item_data[ $key ]['key'] = ! empty( $data['key'] ) ? $data['key'] : $data['name']; + $item_data[ $key ]['display'] = ! empty( $data['display'] ) ? $data['display'] : $data['value']; + } + + // Output flat or in list format. + if ( count( $item_data ) > 0 ) { + ob_start(); + + if ( $flat ) { + foreach ( $item_data as $data ) { + echo esc_html( $data['key'] ) . ': ' . wp_kses_post( $data['display'] ) . "\n"; + } + } else { + wc_get_template( 'cart/cart-item-data.php', array( 'item_data' => $item_data ) ); + } + + return ob_get_clean(); + } + + return ''; +} + +/** + * Gets the url to remove an item from the cart. + * + * @since 3.3.0 + * @param string $cart_item_key contains the id of the cart item. + * @return string url to page + */ +function wc_get_cart_remove_url( $cart_item_key ) { + $cart_page_url = wc_get_cart_url(); + return apply_filters( 'woocommerce_get_remove_url', $cart_page_url ? wp_nonce_url( add_query_arg( 'remove_item', $cart_item_key, $cart_page_url ), 'woocommerce-cart' ) : '' ); +} + +/** + * Gets the url to re-add an item into the cart. + * + * @since 3.3.0 + * @param string $cart_item_key Cart item key to undo. + * @return string url to page + */ +function wc_get_cart_undo_url( $cart_item_key ) { + $cart_page_url = wc_get_cart_url(); + + $query_args = array( + 'undo_item' => $cart_item_key, + ); + + return apply_filters( 'woocommerce_get_undo_url', $cart_page_url ? wp_nonce_url( add_query_arg( $query_args, $cart_page_url ), 'woocommerce-cart' ) : '', $cart_item_key ); +} + +/** + * Outputs all queued notices on WC pages. + * + * @since 3.5.0 + */ +function woocommerce_output_all_notices() { + echo '
    '; + wc_print_notices(); + echo '
    '; +} + +/** + * Products RSS Feed. + * + * @deprecated 2.6 + */ +function wc_products_rss_feed() { + wc_deprecated_function( 'wc_products_rss_feed', '2.6' ); +} + +if ( ! function_exists( 'woocommerce_reset_loop' ) ) { + + /** + * Reset the loop's index and columns when we're done outputting a product loop. + * + * @deprecated 3.3 + */ + function woocommerce_reset_loop() { + wc_reset_loop(); + } +} + +if ( ! function_exists( 'woocommerce_product_reviews_tab' ) ) { + /** + * Output the reviews tab content. + * + * @deprecated 2.4.0 Unused. + */ + function woocommerce_product_reviews_tab() { + wc_deprecated_function( 'woocommerce_product_reviews_tab', '2.4' ); + } +} + +/** + * Display pay buttons HTML. + * + * @since 3.9.0 + */ +function wc_get_pay_buttons() { + $supported_gateways = array(); + $available_gateways = WC()->payment_gateways()->get_available_payment_gateways(); + + foreach ( $available_gateways as $gateway ) { + if ( $gateway->supports( 'pay_button' ) ) { + $supported_gateways[] = $gateway->get_pay_button_id(); + } + } + + if ( ! $supported_gateways ) { + return; + } + + echo '
    '; + foreach ( $supported_gateways as $pay_button_id ) { + echo sprintf( '
    ', esc_attr( $pay_button_id ) ); + } + echo '
    '; +} + +// phpcs:enable Generic.Commenting.Todo.TaskFound diff --git a/includes/wc-template-hooks.php b/plugins/woocommerce/includes/wc-template-hooks.php similarity index 100% rename from includes/wc-template-hooks.php rename to plugins/woocommerce/includes/wc-template-hooks.php diff --git a/plugins/woocommerce/includes/wc-term-functions.php b/plugins/woocommerce/includes/wc-term-functions.php new file mode 100644 index 00000000000..650c9014df7 --- /dev/null +++ b/plugins/woocommerce/includes/wc-term-functions.php @@ -0,0 +1,692 @@ +query_vars; + + // Put back valid orderby values. + if ( 'menu_order' === $args['orderby'] ) { + $args['orderby'] = 'name'; + $args['force_menu_order_sort'] = true; + } + + if ( 'name_num' === $args['orderby'] ) { + $args['orderby'] = 'name'; + $args['force_numeric_name'] = true; + } + + // When COUNTING, disable custom sorting. + if ( 'count' === $args['fields'] ) { + return; + } + + // Support menu_order arg used in previous versions. + if ( ! empty( $args['menu_order'] ) ) { + $args['order'] = 'DESC' === strtoupper( $args['menu_order'] ) ? 'DESC' : 'ASC'; + $args['force_menu_order_sort'] = true; + } + + if ( ! empty( $args['force_menu_order_sort'] ) ) { + $args['orderby'] = 'meta_value_num'; + $args['meta_key'] = 'order'; // phpcs:ignore + $terms_query->meta_query->parse_query_vars( $args ); + } +} +add_action( 'pre_get_terms', 'wc_change_pre_get_terms', 10, 1 ); + +/** + * Adjust term query to handle custom sorting parameters. + * + * @param array $clauses Clauses. + * @param array $taxonomies Taxonomies. + * @param array $args Arguments. + * @return array + */ +function wc_terms_clauses( $clauses, $taxonomies, $args ) { + global $wpdb; + + // No need to filter when counting. + if ( strpos( $clauses['fields'], 'COUNT(*)' ) !== false ) { + return $clauses; + } + + // Force numeric sort if using name_num custom sorting param. + if ( ! empty( $args['force_numeric_name'] ) ) { + $clauses['orderby'] = str_replace( 'ORDER BY t.name', 'ORDER BY t.name+0', $clauses['orderby'] ); + } + + // For sorting, force left join in case order meta is missing. + if ( ! empty( $args['force_menu_order_sort'] ) ) { + $clauses['join'] = str_replace( "INNER JOIN {$wpdb->termmeta} ON ( t.term_id = {$wpdb->termmeta}.term_id )", "LEFT JOIN {$wpdb->termmeta} ON ( t.term_id = {$wpdb->termmeta}.term_id AND {$wpdb->termmeta}.meta_key='order')", $clauses['join'] ); + $clauses['where'] = str_replace( "{$wpdb->termmeta}.meta_key = 'order'", "( {$wpdb->termmeta}.meta_key = 'order' OR {$wpdb->termmeta}.meta_key IS NULL )", $clauses['where'] ); + $clauses['orderby'] = 'DESC' === $args['order'] ? str_replace( 'meta_value+0', 'meta_value+0 DESC, t.name', $clauses['orderby'] ) : str_replace( 'meta_value+0', 'meta_value+0 ASC, t.name', $clauses['orderby'] ); + } + + return $clauses; +} +add_filter( 'terms_clauses', 'wc_terms_clauses', 99, 3 ); + +/** + * Helper to get cached object terms and filter by field using wp_list_pluck(). + * Works as a cached alternative for wp_get_post_terms() and wp_get_object_terms(). + * + * @since 3.0.0 + * @param int $object_id Object ID. + * @param string $taxonomy Taxonomy slug. + * @param string $field Field name. + * @param string $index_key Index key name. + * @return array + */ +function wc_get_object_terms( $object_id, $taxonomy, $field = null, $index_key = null ) { + // Test if terms exists. get_the_terms() return false when it finds no terms. + $terms = get_the_terms( $object_id, $taxonomy ); + + if ( ! $terms || is_wp_error( $terms ) ) { + return array(); + } + + return is_null( $field ) ? $terms : wp_list_pluck( $terms, $field, $index_key ); +} + +/** + * Cached version of wp_get_post_terms(). + * This is a private function (internal use ONLY). + * + * @since 3.0.0 + * @param int $product_id Product ID. + * @param string $taxonomy Taxonomy slug. + * @param array $args Query arguments. + * @return array + */ +function _wc_get_cached_product_terms( $product_id, $taxonomy, $args = array() ) { + $cache_key = 'wc_' . $taxonomy . md5( wp_json_encode( $args ) ); + $cache_group = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . $product_id; + $terms = wp_cache_get( $cache_key, $cache_group ); + + if ( false !== $terms ) { + return $terms; + } + + $terms = wp_get_post_terms( $product_id, $taxonomy, $args ); + + wp_cache_add( $cache_key, $terms, $cache_group ); + + return $terms; +} + +/** + * Wrapper used to get terms for a product. + * + * @param int $product_id Product ID. + * @param string $taxonomy Taxonomy slug. + * @param array $args Query arguments. + * @return array + */ +function wc_get_product_terms( $product_id, $taxonomy, $args = array() ) { + if ( ! taxonomy_exists( $taxonomy ) ) { + return array(); + } + + return apply_filters( 'woocommerce_get_product_terms', _wc_get_cached_product_terms( $product_id, $taxonomy, $args ), $product_id, $taxonomy, $args ); +} + +/** + * Sort by name (numeric). + * + * @param WP_Post $a First item to compare. + * @param WP_Post $b Second item to compare. + * @return int + */ +function _wc_get_product_terms_name_num_usort_callback( $a, $b ) { + $a_name = (float) $a->name; + $b_name = (float) $b->name; + + if ( abs( $a_name - $b_name ) < 0.001 ) { + return 0; + } + + return ( $a_name < $b_name ) ? -1 : 1; +} + +/** + * Sort by parent. + * + * @param WP_Post $a First item to compare. + * @param WP_Post $b Second item to compare. + * @return int + */ +function _wc_get_product_terms_parent_usort_callback( $a, $b ) { + if ( $a->parent === $b->parent ) { + return 0; + } + return ( $a->parent < $b->parent ) ? 1 : -1; +} + +/** + * WooCommerce Dropdown categories. + * + * @param array $args Args to control display of dropdown. + */ +function wc_product_dropdown_categories( $args = array() ) { + global $wp_query; + + $args = wp_parse_args( + $args, + array( + 'pad_counts' => 1, + 'show_count' => 1, + 'hierarchical' => 1, + 'hide_empty' => 1, + 'show_uncategorized' => 1, + 'orderby' => 'name', + 'selected' => isset( $wp_query->query_vars['product_cat'] ) ? $wp_query->query_vars['product_cat'] : '', + 'show_option_none' => __( 'Select a category', 'woocommerce' ), + 'option_none_value' => '', + 'value_field' => 'slug', + 'taxonomy' => 'product_cat', + 'name' => 'product_cat', + 'class' => 'dropdown_product_cat', + ) + ); + + if ( 'order' === $args['orderby'] ) { + $args['orderby'] = 'meta_value_num'; + $args['meta_key'] = 'order'; // phpcs:ignore + } + + wp_dropdown_categories( $args ); +} + +/** + * Custom walker for Product Categories. + * + * Previously used by wc_product_dropdown_categories, but wp_dropdown_categories has been fixed in core. + * + * @param mixed ...$args Variable number of parameters to be passed to the walker. + * @return mixed + */ +function wc_walk_category_dropdown_tree( ...$args ) { + if ( ! class_exists( 'WC_Product_Cat_Dropdown_Walker', false ) ) { + include_once WC()->plugin_path() . '/includes/walkers/class-wc-product-cat-dropdown-walker.php'; + } + + // The user's options are the third parameter. + if ( empty( $args[2]['walker'] ) || ! is_a( $args[2]['walker'], 'Walker' ) ) { + $walker = new WC_Product_Cat_Dropdown_Walker(); + } else { + $walker = $args[2]['walker']; + } + + return $walker->walk( ...$args ); +} + +/** + * Migrate data from WC term meta to WP term meta. + * + * When the database is updated to support term meta, migrate WC term meta data across. + * We do this when the new version is >= 34370, and the old version is < 34370 (34370 is when term meta table was added). + * + * @param string $wp_db_version The new $wp_db_version. + * @param string $wp_current_db_version The old (current) $wp_db_version. + */ +function wc_taxonomy_metadata_migrate_data( $wp_db_version, $wp_current_db_version ) { + if ( $wp_db_version >= 34370 && $wp_current_db_version < 34370 ) { + global $wpdb; + if ( $wpdb->query( "INSERT INTO {$wpdb->termmeta} ( term_id, meta_key, meta_value ) SELECT woocommerce_term_id, meta_key, meta_value FROM {$wpdb->prefix}woocommerce_termmeta;" ) ) { + $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}woocommerce_termmeta" ); + } + } +} +add_action( 'wp_upgrade', 'wc_taxonomy_metadata_migrate_data', 10, 2 ); + +/** + * Move a term before the a given element of its hierarchy level. + * + * @param int $the_term Term ID. + * @param int $next_id The id of the next sibling element in save hierarchy level. + * @param string $taxonomy Taxnomy. + * @param int $index Term index (default: 0). + * @param mixed $terms List of terms. (default: null). + * @return int + */ +function wc_reorder_terms( $the_term, $next_id, $taxonomy, $index = 0, $terms = null ) { + if ( ! $terms ) { + $terms = get_terms( $taxonomy, 'hide_empty=0&parent=0&menu_order=ASC' ); + } + if ( empty( $terms ) ) { + return $index; + } + + $id = intval( $the_term->term_id ); + + $term_in_level = false; // Flag: is our term to order in this level of terms. + + foreach ( $terms as $term ) { + $term_id = intval( $term->term_id ); + + if ( $term_id === $id ) { // Our term to order, we skip. + $term_in_level = true; + continue; // Our term to order, we skip. + } + // the nextid of our term to order, lets move our term here. + if ( null !== $next_id && $term_id === $next_id ) { + $index++; + $index = wc_set_term_order( $id, $index, $taxonomy, true ); + } + + // Set order. + $index++; + $index = wc_set_term_order( $term_id, $index, $taxonomy ); + + /** + * After a term has had it's order set. + */ + do_action( 'woocommerce_after_set_term_order', $term, $index, $taxonomy ); + + // If that term has children we walk through them. + $children = get_terms( $taxonomy, "parent={$term_id}&hide_empty=0&menu_order=ASC" ); + if ( ! empty( $children ) ) { + $index = wc_reorder_terms( $the_term, $next_id, $taxonomy, $index, $children ); + } + } + + // No nextid meaning our term is in last position. + if ( $term_in_level && null === $next_id ) { + $index = wc_set_term_order( $id, $index + 1, $taxonomy, true ); + } + + return $index; +} + +/** + * Set the sort order of a term. + * + * @param int $term_id Term ID. + * @param int $index Index. + * @param string $taxonomy Taxonomy. + * @param bool $recursive Recursive (default: false). + * @return int + */ +function wc_set_term_order( $term_id, $index, $taxonomy, $recursive = false ) { + + $term_id = (int) $term_id; + $index = (int) $index; + + update_term_meta( $term_id, 'order', $index ); + + if ( ! $recursive ) { + return $index; + } + + $children = get_terms( $taxonomy, "parent=$term_id&hide_empty=0&menu_order=ASC" ); + + foreach ( $children as $term ) { + $index++; + $index = wc_set_term_order( $term->term_id, $index, $taxonomy, true ); + } + + clean_term_cache( $term_id, $taxonomy ); + + return $index; +} + +/** + * Function for recounting product terms, ignoring hidden products. + * + * @param array $terms List of terms. + * @param object $taxonomy Taxonomy. + * @param bool $callback Callback. + * @param bool $terms_are_term_taxonomy_ids If terms are from term_taxonomy_id column. + */ +function _wc_term_recount( $terms, $taxonomy, $callback = true, $terms_are_term_taxonomy_ids = true ) { + global $wpdb; + + /** + * Filter to allow/prevent recounting of terms as it could be expensive. + * A likely scenario for this is when bulk importing products. We could + * then prevent it from recounting per product but instead recount it once + * when import is done. Of course this means the import logic has to support this. + * + * @since 5.2 + * @param bool + */ + if ( ! apply_filters( 'woocommerce_product_recount_terms', true ) ) { + return; + } + + // Standard callback. + if ( $callback ) { + _update_post_term_count( $terms, $taxonomy ); + } + + $exclude_term_ids = array(); + $product_visibility_term_ids = wc_get_product_visibility_term_ids(); + + if ( $product_visibility_term_ids['exclude-from-catalog'] ) { + $exclude_term_ids[] = $product_visibility_term_ids['exclude-from-catalog']; + } + + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) { + $exclude_term_ids[] = $product_visibility_term_ids['outofstock']; + } + + $query = array( + 'fields' => " + SELECT COUNT( DISTINCT ID ) FROM {$wpdb->posts} p + ", + 'join' => '', + 'where' => " + WHERE 1=1 + AND p.post_status = 'publish' + AND p.post_type = 'product' + + ", + ); + + if ( count( $exclude_term_ids ) ) { + $query['join'] .= " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = p.ID'; + $query['where'] .= ' AND exclude_join.object_id IS NULL'; + } + + // Pre-process term taxonomy ids. + if ( ! $terms_are_term_taxonomy_ids ) { + // We passed in an array of TERMS in format id=>parent. + $terms = array_filter( (array) array_keys( $terms ) ); + } else { + // If we have term taxonomy IDs we need to get the term ID. + $term_taxonomy_ids = $terms; + $terms = array(); + foreach ( $term_taxonomy_ids as $term_taxonomy_id ) { + $term = get_term_by( 'term_taxonomy_id', $term_taxonomy_id, $taxonomy->name ); + $terms[] = $term->term_id; + } + } + + // Exit if we have no terms to count. + if ( empty( $terms ) ) { + return; + } + + // Ancestors need counting. + if ( is_taxonomy_hierarchical( $taxonomy->name ) ) { + foreach ( $terms as $term_id ) { + $terms = array_merge( $terms, get_ancestors( $term_id, $taxonomy->name ) ); + } + } + + // Unique terms only. + $terms = array_unique( $terms ); + + // Count the terms. + foreach ( $terms as $term_id ) { + $terms_to_count = array( absint( $term_id ) ); + + if ( is_taxonomy_hierarchical( $taxonomy->name ) ) { + // We need to get the $term's hierarchy so we can count its children too. + $children = get_term_children( $term_id, $taxonomy->name ); + + if ( $children && ! is_wp_error( $children ) ) { + $terms_to_count = array_unique( array_map( 'absint', array_merge( $terms_to_count, $children ) ) ); + } + } + + // Generate term query. + $term_query = $query; + $term_query['join'] .= " INNER JOIN ( SELECT object_id FROM {$wpdb->term_relationships} INNER JOIN {$wpdb->term_taxonomy} using( term_taxonomy_id ) WHERE term_id IN ( " . implode( ',', array_map( 'absint', $terms_to_count ) ) . ' ) ) AS include_join ON include_join.object_id = p.ID'; + + // Get the count. + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $count = $wpdb->get_var( implode( ' ', $term_query ) ); + + // Update the count. + update_term_meta( $term_id, 'product_count_' . $taxonomy->name, absint( $count ) ); + } + + delete_transient( 'wc_term_counts' ); +} + +/** + * Recount terms after the stock amount changes. + * + * @param int $product_id Product ID. + */ +function wc_recount_after_stock_change( $product_id ) { + if ( 'yes' !== get_option( 'woocommerce_hide_out_of_stock_items' ) ) { + return; + } + + _wc_recount_terms_by_product( $product_id ); +} +add_action( 'woocommerce_product_set_stock_status', 'wc_recount_after_stock_change' ); + + +/** + * Overrides the original term count for product categories and tags with the product count. + * that takes catalog visibility into account. + * + * @param array $terms List of terms. + * @param string|array $taxonomies Single taxonomy or list of taxonomies. + * @return array + */ +function wc_change_term_counts( $terms, $taxonomies ) { + if ( is_admin() || wp_doing_ajax() ) { + return $terms; + } + + if ( ! isset( $taxonomies[0] ) || ! in_array( $taxonomies[0], apply_filters( 'woocommerce_change_term_counts', array( 'product_cat', 'product_tag' ) ), true ) ) { + return $terms; + } + + $o_term_counts = get_transient( 'wc_term_counts' ); + $term_counts = $o_term_counts; + + foreach ( $terms as &$term ) { + if ( is_object( $term ) ) { + $term_counts[ $term->term_id ] = isset( $term_counts[ $term->term_id ] ) ? $term_counts[ $term->term_id ] : get_term_meta( $term->term_id, 'product_count_' . $taxonomies[0], true ); + + if ( '' !== $term_counts[ $term->term_id ] ) { + $term->count = absint( $term_counts[ $term->term_id ] ); + } + } + } + + // Update transient. + if ( $term_counts !== $o_term_counts ) { + set_transient( 'wc_term_counts', $term_counts, DAY_IN_SECONDS * 30 ); + } + + return $terms; +} +add_filter( 'get_terms', 'wc_change_term_counts', 10, 2 ); + +/** + * Return products in a given term, and cache value. + * + * To keep in sync, product_count will be cleared on "set_object_terms". + * + * @param int $term_id Term ID. + * @param string $taxonomy Taxonomy. + * @return array + */ +function wc_get_term_product_ids( $term_id, $taxonomy ) { + $product_ids = get_term_meta( $term_id, 'product_ids', true ); + + if ( false === $product_ids || ! is_array( $product_ids ) ) { + $product_ids = get_objects_in_term( $term_id, $taxonomy ); + update_term_meta( $term_id, 'product_ids', $product_ids ); + } + + return $product_ids; +} + +/** + * When a post is updated and terms recounted (called by _update_post_term_count), clear the ids. + * + * @param int $object_id Object ID. + * @param array $terms An array of object terms. + * @param array $tt_ids An array of term taxonomy IDs. + * @param string $taxonomy Taxonomy slug. + * @param bool $append Whether to append new terms to the old terms. + * @param array $old_tt_ids Old array of term taxonomy IDs. + */ +function wc_clear_term_product_ids( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) { + foreach ( $old_tt_ids as $term_id ) { + delete_term_meta( $term_id, 'product_ids' ); + } + foreach ( $tt_ids as $term_id ) { + delete_term_meta( $term_id, 'product_ids' ); + } +} +add_action( 'set_object_terms', 'wc_clear_term_product_ids', 10, 6 ); + +/** + * Get full list of product visibilty term ids. + * + * @since 3.0.0 + * @return int[] + */ +function wc_get_product_visibility_term_ids() { + if ( ! taxonomy_exists( 'product_visibility' ) ) { + wc_doing_it_wrong( __FUNCTION__, 'wc_get_product_visibility_term_ids should not be called before taxonomies are registered (woocommerce_after_register_post_type action).', '3.1' ); + return array(); + } + return array_map( + 'absint', + wp_parse_args( + wp_list_pluck( + get_terms( + array( + 'taxonomy' => 'product_visibility', + 'hide_empty' => false, + ) + ), + 'term_taxonomy_id', + 'name' + ), + array( + 'exclude-from-catalog' => 0, + 'exclude-from-search' => 0, + 'featured' => 0, + 'outofstock' => 0, + 'rated-1' => 0, + 'rated-2' => 0, + 'rated-3' => 0, + 'rated-4' => 0, + 'rated-5' => 0, + ) + ) + ); +} + +/** + * Recounts all terms. + * + * @since 5.2 + * @return void + */ +function wc_recount_all_terms() { + $product_cats = get_terms( + 'product_cat', + array( + 'hide_empty' => false, + 'fields' => 'id=>parent', + ) + ); + _wc_term_recount( $product_cats, get_taxonomy( 'product_cat' ), true, false ); + $product_tags = get_terms( + 'product_tag', + array( + 'hide_empty' => false, + 'fields' => 'id=>parent', + ) + ); + _wc_term_recount( $product_tags, get_taxonomy( 'product_tag' ), true, false ); +} + +/** + * Recounts terms by product. + * + * @since 5.2 + * @param int $product_id The ID of the product. + * @return void + */ +function _wc_recount_terms_by_product( $product_id = '' ) { + if ( empty( $product_id ) ) { + return; + } + + $product_terms = get_the_terms( $product_id, 'product_cat' ); + + if ( $product_terms ) { + $product_cats = array(); + + foreach ( $product_terms as $term ) { + $product_cats[ $term->term_id ] = $term->parent; + } + + _wc_term_recount( $product_cats, get_taxonomy( 'product_cat' ), false, false ); + } + + $product_terms = get_the_terms( $product_id, 'product_tag' ); + + if ( $product_terms ) { + $product_tags = array(); + + foreach ( $product_terms as $term ) { + $product_tags[ $term->term_id ] = $term->parent; + } + + _wc_term_recount( $product_tags, get_taxonomy( 'product_tag' ), false, false ); + } +} diff --git a/plugins/woocommerce/includes/wc-update-functions.php b/plugins/woocommerce/includes/wc-update-functions.php new file mode 100644 index 00000000000..10c1d0322ca --- /dev/null +++ b/plugins/woocommerce/includes/wc-update-functions.php @@ -0,0 +1,2404 @@ +get_results( "SELECT meta_value, meta_id, post_id FROM {$wpdb->postmeta} WHERE meta_key = '_file_path' AND meta_value != '';" ); + + if ( $existing_file_paths ) { + + foreach ( $existing_file_paths as $existing_file_path ) { + + $old_file_path = trim( $existing_file_path->meta_value ); + + if ( ! empty( $old_file_path ) ) { + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize + $file_paths = serialize( array( md5( $old_file_path ) => $old_file_path ) ); + + $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_key = '_file_paths', meta_value = %s WHERE meta_id = %d", $file_paths, $existing_file_path->meta_id ) ); + + $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}woocommerce_downloadable_product_permissions SET download_id = %s WHERE product_id = %d", md5( $old_file_path ), $existing_file_path->post_id ) ); + + } + } + } +} + +/** + * Update permalinks for 2.0 + * + * @return void + */ +function wc_update_200_permalinks() { + // Setup default permalinks if shop page is defined. + $permalinks = get_option( 'woocommerce_permalinks' ); + $shop_page_id = wc_get_page_id( 'shop' ); + + if ( empty( $permalinks ) && $shop_page_id > 0 ) { + + $base_slug = $shop_page_id > 0 && get_post( $shop_page_id ) ? get_page_uri( $shop_page_id ) : 'shop'; + + $category_base = 'yes' === get_option( 'woocommerce_prepend_shop_page_to_urls' ) ? trailingslashit( $base_slug ) : ''; + $category_slug = get_option( 'woocommerce_product_category_slug' ) ? get_option( 'woocommerce_product_category_slug' ) : _x( 'product-category', 'slug', 'woocommerce' ); + $tag_slug = get_option( 'woocommerce_product_tag_slug' ) ? get_option( 'woocommerce_product_tag_slug' ) : _x( 'product-tag', 'slug', 'woocommerce' ); + + if ( 'yes' === get_option( 'woocommerce_prepend_shop_page_to_products' ) ) { + $product_base = trailingslashit( $base_slug ); + } else { + $product_slug = get_option( 'woocommerce_product_slug' ); + if ( false !== $product_slug && ! empty( $product_slug ) ) { + $product_base = trailingslashit( $product_slug ); + } else { + $product_base = trailingslashit( _x( 'product', 'slug', 'woocommerce' ) ); + } + } + + if ( 'yes' === get_option( 'woocommerce_prepend_category_to_products' ) ) { + $product_base .= trailingslashit( '%product_cat%' ); + } + + $permalinks = array( + 'product_base' => untrailingslashit( $product_base ), + 'category_base' => untrailingslashit( $category_base . $category_slug ), + 'attribute_base' => untrailingslashit( $category_base ), + 'tag_base' => untrailingslashit( $category_base . $tag_slug ), + ); + + update_option( 'woocommerce_permalinks', $permalinks ); + } +} + +/** + * Update sub-category display options for 2.0 + * + * @return void + */ +function wc_update_200_subcat_display() { + // Update subcat display settings. + if ( 'yes' === get_option( 'woocommerce_shop_show_subcategories' ) ) { + if ( 'yes' === get_option( 'woocommerce_hide_products_when_showing_subcategories' ) ) { + update_option( 'woocommerce_shop_page_display', 'subcategories' ); + } else { + update_option( 'woocommerce_shop_page_display', 'both' ); + } + } + + if ( 'yes' === get_option( 'woocommerce_show_subcategories' ) ) { + if ( 'yes' === get_option( 'woocommerce_hide_products_when_showing_subcategories' ) ) { + update_option( 'woocommerce_category_archive_display', 'subcategories' ); + } else { + update_option( 'woocommerce_category_archive_display', 'both' ); + } + } +} + +/** + * Update tax rates for 2.0 + * + * @return void + */ +function wc_update_200_taxrates() { + global $wpdb; + + // Update tax rates. + $loop = 0; + $tax_rates = get_option( 'woocommerce_tax_rates' ); + + if ( $tax_rates ) { + foreach ( $tax_rates as $tax_rate ) { + + foreach ( $tax_rate['countries'] as $country => $states ) { + + $states = array_reverse( $states ); + + foreach ( $states as $state ) { + + if ( '*' === $state ) { + $state = ''; + } + + $wpdb->insert( + $wpdb->prefix . 'woocommerce_tax_rates', + array( + 'tax_rate_country' => $country, + 'tax_rate_state' => $state, + 'tax_rate' => $tax_rate['rate'], + 'tax_rate_name' => $tax_rate['label'], + 'tax_rate_priority' => 1, + 'tax_rate_compound' => ( 'yes' === $tax_rate['compound'] ) ? 1 : 0, + 'tax_rate_shipping' => ( 'yes' === $tax_rate['shipping'] ) ? 1 : 0, + 'tax_rate_order' => $loop, + 'tax_rate_class' => $tax_rate['class'], + ) + ); + + $loop++; + } + } + } + } + + $local_tax_rates = get_option( 'woocommerce_local_tax_rates' ); + + if ( $local_tax_rates ) { + foreach ( $local_tax_rates as $tax_rate ) { + + $location_type = ( 'postcode' === $tax_rate['location_type'] ) ? 'postcode' : 'city'; + + if ( '*' === $tax_rate['state'] ) { + $tax_rate['state'] = ''; + } + + $wpdb->insert( + $wpdb->prefix . 'woocommerce_tax_rates', + array( + 'tax_rate_country' => $tax_rate['country'], + 'tax_rate_state' => $tax_rate['state'], + 'tax_rate' => $tax_rate['rate'], + 'tax_rate_name' => $tax_rate['label'], + 'tax_rate_priority' => 2, + 'tax_rate_compound' => ( 'yes' === $tax_rate['compound'] ) ? 1 : 0, + 'tax_rate_shipping' => ( 'yes' === $tax_rate['shipping'] ) ? 1 : 0, + 'tax_rate_order' => $loop, + 'tax_rate_class' => $tax_rate['class'], + ) + ); + + $tax_rate_id = $wpdb->insert_id; + + if ( $tax_rate['locations'] ) { + foreach ( $tax_rate['locations'] as $location ) { + + $wpdb->insert( + $wpdb->prefix . 'woocommerce_tax_rate_locations', + array( + 'location_code' => $location, + 'tax_rate_id' => $tax_rate_id, + 'location_type' => $location_type, + ) + ); + + } + } + + $loop++; + } + } + + update_option( 'woocommerce_tax_rates_backup', $tax_rates ); + update_option( 'woocommerce_local_tax_rates_backup', $local_tax_rates ); + delete_option( 'woocommerce_tax_rates' ); + delete_option( 'woocommerce_local_tax_rates' ); +} + +/** + * Update order item line items for 2.0 + * + * @return void + */ +function wc_update_200_line_items() { + global $wpdb; + + // Now its time for the massive update to line items - move them to the new DB tables. + // Reverse with UPDATE `wpwc_postmeta` SET meta_key = '_order_items' WHERE meta_key = '_order_items_old'. + $order_item_rows = $wpdb->get_results( + "SELECT meta_value, post_id FROM {$wpdb->postmeta} WHERE meta_key = '_order_items'" + ); + + foreach ( $order_item_rows as $order_item_row ) { + + $order_items = (array) maybe_unserialize( $order_item_row->meta_value ); + + foreach ( $order_items as $order_item ) { + + if ( ! isset( $order_item['line_total'] ) && isset( $order_item['taxrate'] ) && isset( $order_item['cost'] ) ) { + $order_item['line_tax'] = number_format( ( $order_item['cost'] * $order_item['qty'] ) * ( $order_item['taxrate'] / 100 ), 2, '.', '' ); + $order_item['line_total'] = $order_item['cost'] * $order_item['qty']; + $order_item['line_subtotal_tax'] = $order_item['line_tax']; + $order_item['line_subtotal'] = $order_item['line_total']; + } + + $order_item['line_tax'] = isset( $order_item['line_tax'] ) ? $order_item['line_tax'] : 0; + $order_item['line_total'] = isset( $order_item['line_total'] ) ? $order_item['line_total'] : 0; + $order_item['line_subtotal_tax'] = isset( $order_item['line_subtotal_tax'] ) ? $order_item['line_subtotal_tax'] : 0; + $order_item['line_subtotal'] = isset( $order_item['line_subtotal'] ) ? $order_item['line_subtotal'] : 0; + + $item_id = wc_add_order_item( + $order_item_row->post_id, + array( + 'order_item_name' => $order_item['name'], + 'order_item_type' => 'line_item', + ) + ); + + // Add line item meta. + if ( $item_id ) { + wc_add_order_item_meta( $item_id, '_qty', absint( $order_item['qty'] ) ); + wc_add_order_item_meta( $item_id, '_tax_class', $order_item['tax_class'] ); + wc_add_order_item_meta( $item_id, '_product_id', $order_item['id'] ); + wc_add_order_item_meta( $item_id, '_variation_id', $order_item['variation_id'] ); + wc_add_order_item_meta( $item_id, '_line_subtotal', wc_format_decimal( $order_item['line_subtotal'] ) ); + wc_add_order_item_meta( $item_id, '_line_subtotal_tax', wc_format_decimal( $order_item['line_subtotal_tax'] ) ); + wc_add_order_item_meta( $item_id, '_line_total', wc_format_decimal( $order_item['line_total'] ) ); + wc_add_order_item_meta( $item_id, '_line_tax', wc_format_decimal( $order_item['line_tax'] ) ); + + $meta_rows = array(); + + // Insert meta. + if ( ! empty( $order_item['item_meta'] ) ) { + foreach ( $order_item['item_meta'] as $key => $meta ) { + // Backwards compatibility. + if ( is_array( $meta ) && isset( $meta['meta_name'] ) ) { + $meta_rows[] = '(' . $item_id . ',"' . esc_sql( $meta['meta_name'] ) . '","' . esc_sql( $meta['meta_value'] ) . '")'; + } else { + $meta_rows[] = '(' . $item_id . ',"' . esc_sql( $key ) . '","' . esc_sql( $meta ) . '")'; + } + } + } + + // Insert meta rows at once. + if ( count( $meta_rows ) > 0 ) { + $wpdb->query( + $wpdb->prepare( + "INSERT INTO {$wpdb->prefix}woocommerce_order_itemmeta ( order_item_id, meta_key, meta_value ) + VALUES " . implode( ',', $meta_rows ) . ';', // @codingStandardsIgnoreLine + $order_item_row->post_id + ) + ); + } + + // Delete from DB (rename). + $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->postmeta} + SET meta_key = '_order_items_old' + WHERE meta_key = '_order_items' + AND post_id = %d", + $order_item_row->post_id + ) + ); + } + + unset( $meta_rows, $item_id, $order_item ); + } + } + + // Do the same kind of update for order_taxes - move to lines. + // Reverse with UPDATE `wpwc_postmeta` SET meta_key = '_order_taxes' WHERE meta_key = '_order_taxes_old'. + $order_tax_rows = $wpdb->get_results( + "SELECT meta_value, post_id FROM {$wpdb->postmeta} + WHERE meta_key = '_order_taxes'" + ); + + foreach ( $order_tax_rows as $order_tax_row ) { + + $order_taxes = (array) maybe_unserialize( $order_tax_row->meta_value ); + + if ( ! empty( $order_taxes ) ) { + foreach ( $order_taxes as $order_tax ) { + + if ( ! isset( $order_tax['label'] ) || ! isset( $order_tax['cart_tax'] ) || ! isset( $order_tax['shipping_tax'] ) ) { + continue; + } + + $item_id = wc_add_order_item( + $order_tax_row->post_id, + array( + 'order_item_name' => $order_tax['label'], + 'order_item_type' => 'tax', + ) + ); + + // Add line item meta. + if ( $item_id ) { + wc_add_order_item_meta( $item_id, 'compound', absint( isset( $order_tax['compound'] ) ? $order_tax['compound'] : 0 ) ); + wc_add_order_item_meta( $item_id, 'tax_amount', wc_clean( $order_tax['cart_tax'] ) ); + wc_add_order_item_meta( $item_id, 'shipping_tax_amount', wc_clean( $order_tax['shipping_tax'] ) ); + } + + // Delete from DB (rename). + $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->postmeta} + SET meta_key = '_order_taxes_old' + WHERE meta_key = '_order_taxes' + AND post_id = %d", + $order_tax_row->post_id + ) + ); + } + } + } +} + +/** + * Update image settings for 2.0 + * + * @return void + */ +function wc_update_200_images() { + // Grab the pre 2.0 Image options and use to populate the new image options settings, + // cleaning up afterwards like nice people do. + foreach ( array( 'catalog', 'single', 'thumbnail' ) as $value ) { + + $old_settings = array_filter( + array( + 'width' => get_option( 'woocommerce_' . $value . '_image_width' ), + 'height' => get_option( 'woocommerce_' . $value . '_image_height' ), + 'crop' => get_option( 'woocommerce_' . $value . '_image_crop' ), + ) + ); + + if ( ! empty( $old_settings ) && update_option( 'shop_' . $value . '_image_size', $old_settings ) ) { + + delete_option( 'woocommerce_' . $value . '_image_width' ); + delete_option( 'woocommerce_' . $value . '_image_height' ); + delete_option( 'woocommerce_' . $value . '_image_crop' ); + + } + } +} + +/** + * Update DB version for 2.0 + * + * @return void + */ +function wc_update_200_db_version() { + WC_Install::update_db_version( '2.0.0' ); +} + +/** + * Update Brazilian States for 2.0.9 + * + * @return void + */ +function wc_update_209_brazillian_state() { + global $wpdb; + + // phpcs:disable WordPress.DB.SlowDBQuery + + // Update brazillian state codes. + $wpdb->update( + $wpdb->postmeta, + array( + 'meta_value' => 'BA', + ), + array( + 'meta_key' => '_billing_state', + 'meta_value' => 'BH', + ) + ); + $wpdb->update( + $wpdb->postmeta, + array( + 'meta_value' => 'BA', + ), + array( + 'meta_key' => '_shipping_state', + 'meta_value' => 'BH', + ) + ); + $wpdb->update( + $wpdb->usermeta, + array( + 'meta_value' => 'BA', + ), + array( + 'meta_key' => 'billing_state', + 'meta_value' => 'BH', + ) + ); + $wpdb->update( + $wpdb->usermeta, + array( + 'meta_value' => 'BA', + ), + array( + 'meta_key' => 'shipping_state', + 'meta_value' => 'BH', + ) + ); + + // phpcs:enable WordPress.DB.SlowDBQuery +} + +/** + * Update DB version for 2.0.9 + * + * @return void + */ +function wc_update_209_db_version() { + WC_Install::update_db_version( '2.0.9' ); +} + +/** + * Remove pages for 2.1 + * + * @return void + */ +function wc_update_210_remove_pages() { + // Pages no longer used. + wp_trash_post( get_option( 'woocommerce_pay_page_id' ) ); + wp_trash_post( get_option( 'woocommerce_thanks_page_id' ) ); + wp_trash_post( get_option( 'woocommerce_view_order_page_id' ) ); + wp_trash_post( get_option( 'woocommerce_change_password_page_id' ) ); + wp_trash_post( get_option( 'woocommerce_edit_address_page_id' ) ); + wp_trash_post( get_option( 'woocommerce_lost_password_page_id' ) ); +} + +/** + * Update file paths to support multiple files for 2.1 + * + * @return void + */ +function wc_update_210_file_paths() { + global $wpdb; + + // Upgrade file paths to support multiple file paths + names etc. + $existing_file_paths = $wpdb->get_results( "SELECT meta_value, meta_id FROM {$wpdb->postmeta} WHERE meta_key = '_file_paths' AND meta_value != '';" ); + + if ( $existing_file_paths ) { + + foreach ( $existing_file_paths as $existing_file_path ) { + + $needs_update = false; + $new_value = array(); + $value = maybe_unserialize( trim( $existing_file_path->meta_value ) ); + + if ( $value ) { + foreach ( $value as $key => $file ) { + if ( ! is_array( $file ) ) { + $needs_update = true; + $new_value[ $key ] = array( + 'file' => $file, + 'name' => wc_get_filename_from_url( $file ), + ); + } else { + $new_value[ $key ] = $file; + } + } + if ( $needs_update ) { + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize + $new_value = serialize( $new_value ); + + $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_key = %s, meta_value = %s WHERE meta_id = %d", '_downloadable_files', $new_value, $existing_file_path->meta_id ) ); + } + } + } + } +} + +/** + * Update DB version for 2.1 + * + * @return void + */ +function wc_update_210_db_version() { + WC_Install::update_db_version( '2.1.0' ); +} + +/** + * Update shipping options for 2.2 + * + * @return void + */ +function wc_update_220_shipping() { + $woocommerce_ship_to_destination = 'shipping'; + + if ( get_option( 'woocommerce_ship_to_billing_address_only' ) === 'yes' ) { + $woocommerce_ship_to_destination = 'billing_only'; + } elseif ( get_option( 'woocommerce_ship_to_billing' ) === 'yes' ) { + $woocommerce_ship_to_destination = 'billing'; + } + + add_option( 'woocommerce_ship_to_destination', $woocommerce_ship_to_destination, '', 'no' ); +} + +/** + * Update order statuses for 2.2 + * + * @return void + */ +function wc_update_220_order_status() { + global $wpdb; + $wpdb->query( + "UPDATE {$wpdb->posts} as posts + LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id + LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) + LEFT JOIN {$wpdb->terms} AS term USING( term_id ) + SET posts.post_status = 'wc-pending' + WHERE posts.post_type = 'shop_order' + AND posts.post_status = 'publish' + AND tax.taxonomy = 'shop_order_status' + AND term.slug LIKE 'pending%';" + ); + $wpdb->query( + "UPDATE {$wpdb->posts} as posts + LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id + LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) + LEFT JOIN {$wpdb->terms} AS term USING( term_id ) + SET posts.post_status = 'wc-processing' + WHERE posts.post_type = 'shop_order' + AND posts.post_status = 'publish' + AND tax.taxonomy = 'shop_order_status' + AND term.slug LIKE 'processing%';" + ); + $wpdb->query( + "UPDATE {$wpdb->posts} as posts + LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id + LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) + LEFT JOIN {$wpdb->terms} AS term USING( term_id ) + SET posts.post_status = 'wc-on-hold' + WHERE posts.post_type = 'shop_order' + AND posts.post_status = 'publish' + AND tax.taxonomy = 'shop_order_status' + AND term.slug LIKE 'on-hold%';" + ); + $wpdb->query( + "UPDATE {$wpdb->posts} as posts + LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id + LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) + LEFT JOIN {$wpdb->terms} AS term USING( term_id ) + SET posts.post_status = 'wc-completed' + WHERE posts.post_type = 'shop_order' + AND posts.post_status = 'publish' + AND tax.taxonomy = 'shop_order_status' + AND term.slug LIKE 'completed%';" + ); + $wpdb->query( + "UPDATE {$wpdb->posts} as posts + LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id + LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) + LEFT JOIN {$wpdb->terms} AS term USING( term_id ) + SET posts.post_status = 'wc-cancelled' + WHERE posts.post_type = 'shop_order' + AND posts.post_status = 'publish' + AND tax.taxonomy = 'shop_order_status' + AND term.slug LIKE 'cancelled%';" + ); + $wpdb->query( + "UPDATE {$wpdb->posts} as posts + LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id + LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) + LEFT JOIN {$wpdb->terms} AS term USING( term_id ) + SET posts.post_status = 'wc-refunded' + WHERE posts.post_type = 'shop_order' + AND posts.post_status = 'publish' + AND tax.taxonomy = 'shop_order_status' + AND term.slug LIKE 'refunded%';" + ); + $wpdb->query( + "UPDATE {$wpdb->posts} as posts + LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id + LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) + LEFT JOIN {$wpdb->terms} AS term USING( term_id ) + SET posts.post_status = 'wc-failed' + WHERE posts.post_type = 'shop_order' + AND posts.post_status = 'publish' + AND tax.taxonomy = 'shop_order_status' + AND term.slug LIKE 'failed%';" + ); +} + +/** + * Update variations for 2.2 + * + * @return void + */ +function wc_update_220_variations() { + global $wpdb; + // Update variations which manage stock. + $update_variations = $wpdb->get_results( + "SELECT DISTINCT posts.ID AS variation_id, posts.post_parent AS variation_parent FROM {$wpdb->posts} as posts + LEFT OUTER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id AND postmeta.meta_key = '_stock' + LEFT OUTER JOIN {$wpdb->postmeta} as postmeta2 ON posts.ID = postmeta2.post_id AND postmeta2.meta_key = '_manage_stock' + WHERE posts.post_type = 'product_variation' + AND postmeta.meta_value IS NOT NULL + AND postmeta.meta_value != '' + AND postmeta2.meta_value IS NULL" + ); + + foreach ( $update_variations as $variation ) { + $parent_backorders = get_post_meta( $variation->variation_parent, '_backorders', true ); + add_post_meta( $variation->variation_id, '_manage_stock', 'yes', true ); + add_post_meta( $variation->variation_id, '_backorders', $parent_backorders ? $parent_backorders : 'no', true ); + } +} + +/** + * Update attributes for 2.2 + * + * @return void + */ +function wc_update_220_attributes() { + global $wpdb; + // Update taxonomy names with correct sanitized names. + $attribute_taxonomies = $wpdb->get_results( 'SELECT attribute_name, attribute_id FROM ' . $wpdb->prefix . 'woocommerce_attribute_taxonomies' ); + + foreach ( $attribute_taxonomies as $attribute_taxonomy ) { + $sanitized_attribute_name = wc_sanitize_taxonomy_name( $attribute_taxonomy->attribute_name ); + if ( $sanitized_attribute_name !== $attribute_taxonomy->attribute_name ) { + if ( ! $wpdb->get_var( $wpdb->prepare( "SELECT 1=1 FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_name = %s;", $sanitized_attribute_name ) ) ) { + // Update attribute. + $wpdb->update( + "{$wpdb->prefix}woocommerce_attribute_taxonomies", + array( + 'attribute_name' => $sanitized_attribute_name, + ), + array( + 'attribute_id' => $attribute_taxonomy->attribute_id, + ) + ); + + // Update terms. + $wpdb->update( + $wpdb->term_taxonomy, + array( 'taxonomy' => wc_attribute_taxonomy_name( $sanitized_attribute_name ) ), + array( 'taxonomy' => 'pa_' . $attribute_taxonomy->attribute_name ) + ); + } + } + } + + delete_transient( 'wc_attribute_taxonomies' ); + WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); +} + +/** + * Update DB version for 2.2 + * + * @return void + */ +function wc_update_220_db_version() { + WC_Install::update_db_version( '2.2.0' ); +} + +/** + * Update options for 2.3 + * + * @return void + */ +function wc_update_230_options() { + // _money_spent and _order_count may be out of sync - clear them + delete_metadata( 'user', 0, '_money_spent', '', true ); + delete_metadata( 'user', 0, '_order_count', '', true ); + delete_metadata( 'user', 0, '_last_order', '', true ); + + // To prevent taxes being hidden when using a default 'no address' in a store with tax inc prices, set the woocommerce_default_customer_address to use the store base address by default. + if ( '' === get_option( 'woocommerce_default_customer_address', false ) && wc_prices_include_tax() ) { + update_option( 'woocommerce_default_customer_address', 'base' ); + } +} + +/** + * Update DB version for 2.3 + * + * @return void + */ +function wc_update_230_db_version() { + WC_Install::update_db_version( '2.3.0' ); +} + +/** + * Update calc discount options for 2.4 + * + * @return void + */ +function wc_update_240_options() { + /** + * Coupon discount calculations. + * Maintain the old coupon logic for upgrades. + */ + update_option( 'woocommerce_calc_discounts_sequentially', 'yes' ); +} + +/** + * Update shipping methods for 2.4 + * + * @return void + */ +function wc_update_240_shipping_methods() { + /** + * Flat Rate Shipping. + * Update legacy options to new math based options. + */ + $shipping_methods = array( + 'woocommerce_flat_rates' => new WC_Shipping_Legacy_Flat_Rate(), + 'woocommerce_international_delivery_flat_rates' => new WC_Shipping_Legacy_International_Delivery(), + ); + foreach ( $shipping_methods as $flat_rate_option_key => $shipping_method ) { + // Stop this running more than once if routine is repeated. + if ( version_compare( $shipping_method->get_option( 'version', 0 ), '2.4.0', '<' ) ) { + $shipping_classes = WC()->shipping()->get_shipping_classes(); + $has_classes = count( $shipping_classes ) > 0; + $cost_key = $has_classes ? 'no_class_cost' : 'cost'; + $min_fee = $shipping_method->get_option( 'minimum_fee' ); + $math_cost_strings = array( + 'cost' => array(), + 'no_class_cost' => array(), + ); + + $math_cost_strings[ $cost_key ][] = $shipping_method->get_option( 'cost' ); + $fee = $shipping_method->get_option( 'fee' ); + + if ( $fee ) { + $math_cost_strings[ $cost_key ][] = strstr( $fee, '%' ) ? '[fee percent="' . str_replace( '%', '', $fee ) . '" min="' . esc_attr( $min_fee ) . '"]' : $fee; + } + + foreach ( $shipping_classes as $shipping_class ) { + $rate_key = 'class_cost_' . $shipping_class->slug; + $math_cost_strings[ $rate_key ] = $math_cost_strings['no_class_cost']; + } + + $flat_rates = array_filter( (array) get_option( $flat_rate_option_key, array() ) ); + + if ( $flat_rates ) { + foreach ( $flat_rates as $shipping_class => $rate ) { + $rate_key = 'class_cost_' . $shipping_class; + if ( $rate['cost'] || $rate['fee'] ) { + $math_cost_strings[ $rate_key ][] = $rate['cost']; + $math_cost_strings[ $rate_key ][] = strstr( $rate['fee'], '%' ) ? '[fee percent="' . str_replace( '%', '', $rate['fee'] ) . '" min="' . esc_attr( $min_fee ) . '"]' : $rate['fee']; + } + } + } + + if ( 'item' === $shipping_method->type ) { + foreach ( $math_cost_strings as $key => $math_cost_string ) { + $math_cost_strings[ $key ] = array_filter( array_map( 'trim', $math_cost_strings[ $key ] ) ); + if ( ! empty( $math_cost_strings[ $key ] ) ) { + $last_key = max( 0, count( $math_cost_strings[ $key ] ) - 1 ); + $math_cost_strings[ $key ][0] = '( ' . $math_cost_strings[ $key ][0]; + $math_cost_strings[ $key ][ $last_key ] .= ' ) * [qty]'; + } + } + } + + $math_cost_strings['cost'][] = $shipping_method->get_option( 'cost_per_order' ); + + // Save settings. + foreach ( $math_cost_strings as $option_id => $math_cost_string ) { + $shipping_method->settings[ $option_id ] = implode( ' + ', array_filter( $math_cost_string ) ); + } + + $shipping_method->settings['version'] = '2.4.0'; + $shipping_method->settings['type'] = 'item' === $shipping_method->settings['type'] ? 'class' : $shipping_method->settings['type']; + + update_option( $shipping_method->plugin_id . $shipping_method->id . '_settings', $shipping_method->settings ); + } + } +} + +/** + * Update API keys for 2.4 + * + * @return void + */ +function wc_update_240_api_keys() { + global $wpdb; + /** + * Update the old user API keys to the new Apps keys. + */ + $api_users = $wpdb->get_results( "SELECT user_id FROM $wpdb->usermeta WHERE meta_key = 'woocommerce_api_consumer_key'" ); + $apps_keys = array(); + + // Get user data. + foreach ( $api_users as $_user ) { + $user = get_userdata( $_user->user_id ); + $apps_keys[] = array( + 'user_id' => $user->ID, + 'permissions' => $user->woocommerce_api_key_permissions, + 'consumer_key' => wc_api_hash( $user->woocommerce_api_consumer_key ), + 'consumer_secret' => $user->woocommerce_api_consumer_secret, + 'truncated_key' => substr( $user->woocommerce_api_consumer_secret, -7 ), + ); + } + + if ( ! empty( $apps_keys ) ) { + // Create new apps. + foreach ( $apps_keys as $app ) { + $wpdb->insert( + $wpdb->prefix . 'woocommerce_api_keys', + $app, + array( + '%d', + '%s', + '%s', + '%s', + '%s', + ) + ); + } + + // Delete old user keys from usermeta. + foreach ( $api_users as $_user ) { + $user_id = intval( $_user->user_id ); + delete_user_meta( $user_id, 'woocommerce_api_consumer_key' ); + delete_user_meta( $user_id, 'woocommerce_api_consumer_secret' ); + delete_user_meta( $user_id, 'woocommerce_api_key_permissions' ); + } + } +} + +/** + * Update webhooks for 2.4 + * + * @return void + */ +function wc_update_240_webhooks() { + // phpcs:disable WordPress.DB.SlowDBQuery + + /** + * Webhooks. + * Make sure order.update webhooks get the woocommerce_order_edit_status hook. + */ + $order_update_webhooks = get_posts( + array( + 'posts_per_page' => -1, + 'post_type' => 'shop_webhook', + 'meta_key' => '_topic', + 'meta_value' => 'order.updated', + ) + ); + foreach ( $order_update_webhooks as $order_update_webhook ) { + $webhook = new WC_Webhook( $order_update_webhook->ID ); + $webhook->set_topic( 'order.updated' ); + } + + // phpcs:enable WordPress.DB.SlowDBQuery +} + +/** + * Update refunds for 2.4 + * + * @return void + */ +function wc_update_240_refunds() { + global $wpdb; + /** + * Refunds for full refunded orders. + * Update fully refunded orders to ensure they have a refund line item so reports add up. + */ + $refunded_orders = get_posts( + array( + 'posts_per_page' => -1, + 'post_type' => 'shop_order', + 'post_status' => array( 'wc-refunded' ), + ) + ); + + // Ensure emails are disabled during this update routine. + remove_all_actions( 'woocommerce_order_status_refunded_notification' ); + remove_all_actions( 'woocommerce_order_partially_refunded_notification' ); + remove_action( 'woocommerce_order_status_refunded', array( 'WC_Emails', 'send_transactional_email' ) ); + remove_action( 'woocommerce_order_partially_refunded', array( 'WC_Emails', 'send_transactional_email' ) ); + + foreach ( $refunded_orders as $refunded_order ) { + $order_total = get_post_meta( $refunded_order->ID, '_order_total', true ); + $refunded_total = $wpdb->get_var( + $wpdb->prepare( + "SELECT SUM( postmeta.meta_value ) + FROM $wpdb->postmeta AS postmeta + INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) + WHERE postmeta.meta_key = '_refund_amount' + AND postmeta.post_id = posts.ID", + $refunded_order->ID + ) + ); + + if ( $order_total > $refunded_total ) { + wc_create_refund( + array( + 'amount' => $order_total - $refunded_total, + 'reason' => __( 'Order fully refunded', 'woocommerce' ), + 'order_id' => $refunded_order->ID, + 'line_items' => array(), + 'date' => $refunded_order->post_modified, + ) + ); + } + } + + wc_delete_shop_order_transients(); +} + +/** + * Update DB version for 2.4 + * + * @return void + */ +function wc_update_240_db_version() { + WC_Install::update_db_version( '2.4.0' ); +} + +/** + * Update variations for 2.4.1 + * + * @return void + */ +function wc_update_241_variations() { + global $wpdb; + + // Select variations that don't have any _stock_status implemented on WooCommerce 2.2. + $update_variations = $wpdb->get_results( + "SELECT DISTINCT posts.ID AS variation_id, posts.post_parent AS variation_parent + FROM {$wpdb->posts} as posts + LEFT OUTER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id AND postmeta.meta_key = '_stock_status' + WHERE posts.post_type = 'product_variation' + AND postmeta.meta_value IS NULL" + ); + + foreach ( $update_variations as $variation ) { + // Get the parent _stock_status. + $parent_stock_status = get_post_meta( $variation->variation_parent, '_stock_status', true ); + + // Set the _stock_status. + add_post_meta( $variation->variation_id, '_stock_status', $parent_stock_status ? $parent_stock_status : 'instock', true ); + + // Delete old product children array. + delete_transient( 'wc_product_children_' . $variation->variation_parent ); + } + + // Invalidate old transients such as wc_var_price. + WC_Cache_Helper::get_transient_version( 'product', true ); +} + +/** + * Update DB version for 2.4.1 + * + * @return void + */ +function wc_update_241_db_version() { + WC_Install::update_db_version( '2.4.1' ); +} + +/** + * Update currency settings for 2.5 + * + * @return void + */ +function wc_update_250_currency() { + global $wpdb; + // Fix currency settings for LAK currency. + $current_currency = get_option( 'woocommerce_currency' ); + + if ( 'KIP' === $current_currency ) { + update_option( 'woocommerce_currency', 'LAK' ); + } + + // phpcs:disable WordPress.DB.SlowDBQuery + + // Update LAK currency code. + $wpdb->update( + $wpdb->postmeta, + array( + 'meta_value' => 'LAK', + ), + array( + 'meta_key' => '_order_currency', + 'meta_value' => 'KIP', + ) + ); + + // phpcs:enable WordPress.DB.SlowDBQuery +} + +/** + * Update DB version for 2.5 + * + * @return void + */ +function wc_update_250_db_version() { + WC_Install::update_db_version( '2.5.0' ); +} + +/** + * Update ship to countries options for 2.6 + * + * @return void + */ +function wc_update_260_options() { + // woocommerce_calc_shipping option has been removed in 2.6. + if ( 'no' === get_option( 'woocommerce_calc_shipping' ) ) { + update_option( 'woocommerce_ship_to_countries', 'disabled' ); + } + + WC_Admin_Notices::add_notice( 'legacy_shipping' ); +} + +/** + * Update term meta for 2.6 + * + * @return void + */ +function wc_update_260_termmeta() { + global $wpdb; + /** + * Migrate term meta to WordPress tables. + */ + if ( get_option( 'db_version' ) >= 34370 && $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_termmeta';" ) ) { + if ( $wpdb->query( "INSERT INTO {$wpdb->termmeta} ( term_id, meta_key, meta_value ) SELECT woocommerce_term_id, meta_key, meta_value FROM {$wpdb->prefix}woocommerce_termmeta;" ) ) { + $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}woocommerce_termmeta" ); + wp_cache_flush(); + } + } +} + +/** + * Update zones for 2.6 + * + * @return void + */ +function wc_update_260_zones() { + global $wpdb; + /** + * Old (table rate) shipping zones to new core shipping zones migration. + * zone_enabled and zone_type are no longer used, but it's safe to leave them be. + */ + if ( $wpdb->get_var( "SHOW COLUMNS FROM `{$wpdb->prefix}woocommerce_shipping_zones` LIKE 'zone_enabled';" ) ) { + $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_shipping_zones CHANGE `zone_type` `zone_type` VARCHAR(40) NOT NULL DEFAULT '';" ); + $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_shipping_zones CHANGE `zone_enabled` `zone_enabled` INT(1) NOT NULL DEFAULT 1;" ); + } +} + +/** + * Update zone methods for 2.6 + * + * @return void + */ +function wc_update_260_zone_methods() { + global $wpdb; + + /** + * Shipping zones in WC 2.6.0 use a table named woocommerce_shipping_zone_methods. + * Migrate the old data out of woocommerce_shipping_zone_shipping_methods into the new table and port over any known options (used by table rates and flat rate boxes). + */ + if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_shipping_zone_shipping_methods';" ) ) { + $old_methods = $wpdb->get_results( "SELECT zone_id, shipping_method_type, shipping_method_order, shipping_method_id FROM {$wpdb->prefix}woocommerce_shipping_zone_shipping_methods;" ); + + if ( $old_methods ) { + $max_new_id = $wpdb->get_var( "SELECT MAX(instance_id) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods" ); + $max_old_id = $wpdb->get_var( "SELECT MAX(shipping_method_id) FROM {$wpdb->prefix}woocommerce_shipping_zone_shipping_methods" ); + + // Avoid ID conflicts. + $wpdb->query( $wpdb->prepare( "ALTER TABLE {$wpdb->prefix}woocommerce_shipping_zone_methods AUTO_INCREMENT = %d;", max( $max_new_id, $max_old_id ) + 1 ) ); + + // Store changes. + $changes = array(); + + // Move data. + foreach ( $old_methods as $old_method ) { + $wpdb->insert( + $wpdb->prefix . 'woocommerce_shipping_zone_methods', + array( + 'zone_id' => $old_method->zone_id, + 'method_id' => $old_method->shipping_method_type, + 'method_order' => $old_method->shipping_method_order, + ) + ); + + $new_instance_id = $wpdb->insert_id; + + // Move main settings. + $older_settings_key = 'woocommerce_' . $old_method->shipping_method_type . '-' . $old_method->shipping_method_id . '_settings'; + $old_settings_key = 'woocommerce_' . $old_method->shipping_method_type . '_' . $old_method->shipping_method_id . '_settings'; + add_option( 'woocommerce_' . $old_method->shipping_method_type . '_' . $new_instance_id . '_settings', get_option( $old_settings_key, get_option( $older_settings_key ) ) ); + + // Handling for table rate and flat rate box shipping. + if ( 'table_rate' === $old_method->shipping_method_type ) { + // Move priority settings. + add_option( 'woocommerce_table_rate_default_priority_' . $new_instance_id, get_option( 'woocommerce_table_rate_default_priority_' . $old_method->shipping_method_id ) ); + add_option( 'woocommerce_table_rate_priorities_' . $new_instance_id, get_option( 'woocommerce_table_rate_priorities_' . $old_method->shipping_method_id ) ); + + // Move rates. + $wpdb->update( + $wpdb->prefix . 'woocommerce_shipping_table_rates', + array( + 'shipping_method_id' => $new_instance_id, + ), + array( + 'shipping_method_id' => $old_method->shipping_method_id, + ) + ); + } elseif ( 'flat_rate_boxes' === $old_method->shipping_method_type ) { + $wpdb->update( + $wpdb->prefix . 'woocommerce_shipping_flat_rate_boxes', + array( + 'shipping_method_id' => $new_instance_id, + ), + array( + 'shipping_method_id' => $old_method->shipping_method_id, + ) + ); + } + + $changes[ $old_method->shipping_method_id ] = $new_instance_id; + } + + // $changes contains keys (old method ids) and values (new instance ids) if extra processing is needed in plugins. + // Store this to an option so extensions can pick it up later, then fire an action. + update_option( 'woocommerce_updated_instance_ids', $changes ); + do_action( 'woocommerce_updated_instance_ids', $changes ); + } + } + + // Change ranges used to ... + $wpdb->query( "UPDATE {$wpdb->prefix}woocommerce_shipping_zone_locations SET location_code = REPLACE( location_code, '-', '...' );" ); +} + +/** + * Update refunds for 2.6 + * + * @return void + */ +function wc_update_260_refunds() { + global $wpdb; + /** + * Refund item qty should be negative. + */ + $wpdb->query( + "UPDATE {$wpdb->prefix}woocommerce_order_itemmeta as item_meta + LEFT JOIN {$wpdb->prefix}woocommerce_order_items as items ON item_meta.order_item_id = items.order_item_id + LEFT JOIN {$wpdb->posts} as posts ON items.order_id = posts.ID + SET item_meta.meta_value = item_meta.meta_value * -1 + WHERE item_meta.meta_value > 0 AND item_meta.meta_key = '_qty' AND posts.post_type = 'shop_order_refund'" + ); +} + +/** + * Update DB version for 2.6 + * + * @return void + */ +function wc_update_260_db_version() { + WC_Install::update_db_version( '2.6.0' ); +} + +/** + * Update webhooks for 3.0 + * + * @return void + */ +function wc_update_300_webhooks() { + // phpcs:disable WordPress.DB.SlowDBQuery + + /** + * Make sure product.update webhooks get the woocommerce_product_quick_edit_save + * and woocommerce_product_bulk_edit_save hooks. + */ + $product_update_webhooks = get_posts( + array( + 'posts_per_page' => -1, + 'post_type' => 'shop_webhook', + 'meta_key' => '_topic', + 'meta_value' => 'product.updated', + ) + ); + foreach ( $product_update_webhooks as $product_update_webhook ) { + $webhook = new WC_Webhook( $product_update_webhook->ID ); + $webhook->set_topic( 'product.updated' ); + } + + // phpcs:enable WordPress.DB.SlowDBQuery +} + +/** + * Add an index to the field comment_type to improve the response time of the query + * used by WC_Comments::wp_count_comments() to get the number of comments by type. + */ +function wc_update_300_comment_type_index() { + global $wpdb; + + $index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->comments} WHERE column_name = 'comment_type' and key_name = 'woo_idx_comment_type'" ); + + if ( is_null( $index_exists ) ) { + // Add an index to the field comment_type to improve the response time of the query + // used by WC_Comments::wp_count_comments() to get the number of comments by type. + $wpdb->query( "ALTER TABLE {$wpdb->comments} ADD INDEX woo_idx_comment_type (comment_type)" ); + } +} + +/** + * Update grouped products for 3.0 + * + * @return void + */ +function wc_update_300_grouped_products() { + global $wpdb; + $parents = $wpdb->get_col( "SELECT DISTINCT( post_parent ) FROM {$wpdb->posts} WHERE post_parent > 0 AND post_type = 'product';" ); + foreach ( $parents as $parent_id ) { + $parent = wc_get_product( $parent_id ); + if ( $parent && $parent->is_type( 'grouped' ) ) { + $children_ids = get_posts( + array( + 'post_parent' => $parent_id, + 'posts_per_page' => -1, + 'post_type' => 'product', + 'fields' => 'ids', + ) + ); + update_post_meta( $parent_id, '_children', $children_ids ); + + // Update children to remove the parent. + $wpdb->update( + $wpdb->posts, + array( + 'post_parent' => 0, + ), + array( + 'post_parent' => $parent_id, + ) + ); + } + } +} + +/** + * Update shipping tax classes for 3.0 + * + * @return void + */ +function wc_update_300_settings() { + $woocommerce_shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' ); + if ( '' === $woocommerce_shipping_tax_class ) { + update_option( 'woocommerce_shipping_tax_class', 'inherit' ); + } elseif ( 'standard' === $woocommerce_shipping_tax_class ) { + update_option( 'woocommerce_shipping_tax_class', '' ); + } +} + +/** + * Convert meta values into term for product visibility. + */ +function wc_update_300_product_visibility() { + global $wpdb; + + WC_Install::create_terms(); + + $featured_term = get_term_by( 'name', 'featured', 'product_visibility' ); + + if ( $featured_term ) { + $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_featured' AND meta_value = 'yes';", $featured_term->term_taxonomy_id ) ); + } + + $exclude_search_term = get_term_by( 'name', 'exclude-from-search', 'product_visibility' ); + + if ( $exclude_search_term ) { + $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_visibility' AND meta_value IN ('hidden', 'catalog');", $exclude_search_term->term_taxonomy_id ) ); + } + + $exclude_catalog_term = get_term_by( 'name', 'exclude-from-catalog', 'product_visibility' ); + + if ( $exclude_catalog_term ) { + $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_visibility' AND meta_value IN ('hidden', 'search');", $exclude_catalog_term->term_taxonomy_id ) ); + } + + $outofstock_term = get_term_by( 'name', 'outofstock', 'product_visibility' ); + + if ( $outofstock_term ) { + $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_stock_status' AND meta_value = 'outofstock';", $outofstock_term->term_taxonomy_id ) ); + } + + $rating_term = get_term_by( 'name', 'rated-1', 'product_visibility' ); + + if ( $rating_term ) { + $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 1;", $rating_term->term_taxonomy_id ) ); + } + + $rating_term = get_term_by( 'name', 'rated-2', 'product_visibility' ); + + if ( $rating_term ) { + $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 2;", $rating_term->term_taxonomy_id ) ); + } + + $rating_term = get_term_by( 'name', 'rated-3', 'product_visibility' ); + + if ( $rating_term ) { + $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 3;", $rating_term->term_taxonomy_id ) ); + } + + $rating_term = get_term_by( 'name', 'rated-4', 'product_visibility' ); + + if ( $rating_term ) { + $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 4;", $rating_term->term_taxonomy_id ) ); + } + + $rating_term = get_term_by( 'name', 'rated-5', 'product_visibility' ); + + if ( $rating_term ) { + $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 5;", $rating_term->term_taxonomy_id ) ); + } +} + +/** + * Update DB Version. + */ +function wc_update_300_db_version() { + WC_Install::update_db_version( '3.0.0' ); +} + +/** + * Add an index to the downloadable product permissions table to improve performance of update_user_by_order_id. + */ +function wc_update_310_downloadable_products() { + global $wpdb; + + $index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE column_name = 'order_id' and key_name = 'order_id'" ); + + if ( is_null( $index_exists ) ) { + $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions ADD INDEX order_id (order_id)" ); + } +} + +/** + * Find old order notes and ensure they have the correct type for exclusion. + */ +function wc_update_310_old_comments() { + global $wpdb; + + $wpdb->query( "UPDATE $wpdb->comments comments LEFT JOIN $wpdb->posts as posts ON comments.comment_post_ID = posts.ID SET comment_type = 'order_note' WHERE posts.post_type = 'shop_order' AND comment_type = '';" ); +} + +/** + * Update DB Version. + */ +function wc_update_310_db_version() { + WC_Install::update_db_version( '3.1.0' ); +} + +/** + * Update shop_manager capabilities. + */ +function wc_update_312_shop_manager_capabilities() { + $role = get_role( 'shop_manager' ); + $role->remove_cap( 'unfiltered_html' ); +} + +/** + * Update DB Version. + */ +function wc_update_312_db_version() { + WC_Install::update_db_version( '3.1.2' ); +} + +/** + * Update state codes for Mexico. + */ +function wc_update_320_mexican_states() { + global $wpdb; + + $mx_states = array( + 'Distrito Federal' => 'CMX', + 'Jalisco' => 'JAL', + 'Nuevo Leon' => 'NLE', + 'Aguascalientes' => 'AGS', + 'Baja California' => 'BCN', + 'Baja California Sur' => 'BCS', + 'Campeche' => 'CAM', + 'Chiapas' => 'CHP', + 'Chihuahua' => 'CHH', + 'Coahuila' => 'COA', + 'Colima' => 'COL', + 'Durango' => 'DGO', + 'Guanajuato' => 'GTO', + 'Guerrero' => 'GRO', + 'Hidalgo' => 'HGO', + 'Estado de Mexico' => 'MEX', + 'Michoacan' => 'MIC', + 'Morelos' => 'MOR', + 'Nayarit' => 'NAY', + 'Oaxaca' => 'OAX', + 'Puebla' => 'PUE', + 'Queretaro' => 'QRO', + 'Quintana Roo' => 'ROO', + 'San Luis Potosi' => 'SLP', + 'Sinaloa' => 'SIN', + 'Sonora' => 'SON', + 'Tabasco' => 'TAB', + 'Tamaulipas' => 'TMP', + 'Tlaxcala' => 'TLA', + 'Veracruz' => 'VER', + 'Yucatan' => 'YUC', + 'Zacatecas' => 'ZAC', + ); + + foreach ( $mx_states as $old => $new ) { + $wpdb->query( + $wpdb->prepare( + "UPDATE $wpdb->postmeta + SET meta_value = %s + WHERE meta_key IN ( '_billing_state', '_shipping_state' ) + AND meta_value = %s", + $new, + $old + ) + ); + $wpdb->update( + "{$wpdb->prefix}woocommerce_shipping_zone_locations", + array( + 'location_code' => 'MX:' . $new, + ), + array( + 'location_code' => 'MX:' . $old, + ) + ); + $wpdb->update( + "{$wpdb->prefix}woocommerce_tax_rates", + array( + 'tax_rate_state' => strtoupper( $new ), + ), + array( + 'tax_rate_state' => strtoupper( $old ), + ) + ); + } +} + +/** + * Update DB Version. + */ +function wc_update_320_db_version() { + WC_Install::update_db_version( '3.2.0' ); +} + +/** + * Update image settings to use new aspect ratios and widths. + */ +function wc_update_330_image_options() { + $old_thumbnail_size = get_option( 'shop_catalog_image_size', array() ); + $old_single_size = get_option( 'shop_single_image_size', array() ); + + if ( ! empty( $old_thumbnail_size['width'] ) ) { + $width = absint( $old_thumbnail_size['width'] ); + $height = absint( $old_thumbnail_size['height'] ); + $hard_crop = ! empty( $old_thumbnail_size['crop'] ); + + if ( ! $width ) { + $width = 300; + } + + if ( ! $height ) { + $height = $width; + } + + update_option( 'woocommerce_thumbnail_image_width', $width ); + + // Calculate cropping mode from old image options. + if ( ! $hard_crop ) { + update_option( 'woocommerce_thumbnail_cropping', 'uncropped' ); + } elseif ( $width === $height ) { + update_option( 'woocommerce_thumbnail_cropping', '1:1' ); + } else { + $ratio = $width / $height; + $fraction = wc_decimal_to_fraction( $ratio ); + + if ( $fraction ) { + update_option( 'woocommerce_thumbnail_cropping', 'custom' ); + update_option( 'woocommerce_thumbnail_cropping_custom_width', $fraction[0] ); + update_option( 'woocommerce_thumbnail_cropping_custom_height', $fraction[1] ); + } + } + } + + // Single is uncropped. + if ( ! empty( $old_single_size['width'] ) ) { + update_option( 'woocommerce_single_image_width', absint( $old_single_size['width'] ) ); + } +} + +/** + * Migrate webhooks from post type to CRUD. + */ +function wc_update_330_webhooks() { + register_post_type( 'shop_webhook' ); + + // Map statuses from post_type to Webhooks CRUD. + $statuses = array( + 'publish' => 'active', + 'draft' => 'paused', + 'pending' => 'disabled', + ); + + $posts = get_posts( + array( + 'posts_per_page' => -1, + 'post_type' => 'shop_webhook', + 'post_status' => 'any', + ) + ); + + foreach ( $posts as $post ) { + $webhook = new WC_Webhook(); + $webhook->set_name( $post->post_title ); + $webhook->set_status( isset( $statuses[ $post->post_status ] ) ? $statuses[ $post->post_status ] : 'disabled' ); + $webhook->set_delivery_url( get_post_meta( $post->ID, '_delivery_url', true ) ); + $webhook->set_secret( get_post_meta( $post->ID, '_secret', true ) ); + $webhook->set_topic( get_post_meta( $post->ID, '_topic', true ) ); + $webhook->set_api_version( get_post_meta( $post->ID, '_api_version', true ) ); + $webhook->set_user_id( $post->post_author ); + $webhook->set_pending_delivery( false ); + $webhook->save(); + + wp_delete_post( $post->ID, true ); + } + + unregister_post_type( 'shop_webhook' ); +} + +/** + * Assign default cat to all products with no cats. + */ +function wc_update_330_set_default_product_cat() { + /* + * When a product category is deleted, we need to check + * if the product has no categories assigned. Then assign + * it a default category. + */ + wc_get_container()->get( AssignDefaultCategory::class )->maybe_assign_default_product_cat(); +} + +/** + * Update product stock status to use the new onbackorder status. + */ +function wc_update_330_product_stock_status() { + global $wpdb; + + if ( 'yes' !== get_option( 'woocommerce_manage_stock' ) ) { + return; + } + + $min_stock_amount = (int) get_option( 'woocommerce_notify_no_stock_amount', 0 ); + + // Get all products that have stock management enabled, stock less than or equal to min stock amount, and backorders enabled. + $post_ids = $wpdb->get_col( + $wpdb->prepare( + "SELECT t1.post_id FROM $wpdb->postmeta t1 + INNER JOIN $wpdb->postmeta t2 + ON t1.post_id = t2.post_id + AND t1.meta_key = '_manage_stock' AND t1.meta_value = 'yes' + AND t2.meta_key = '_stock' AND t2.meta_value <= %d + INNER JOIN $wpdb->postmeta t3 + ON t2.post_id = t3.post_id + AND t3.meta_key = '_backorders' AND ( t3.meta_value = 'yes' OR t3.meta_value = 'notify' )", + $min_stock_amount + ) + ); + + if ( empty( $post_ids ) ) { + return; + } + + $post_ids = array_map( 'absint', $post_ids ); + + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + // Set the status to onbackorder for those products. + $wpdb->query( + "UPDATE $wpdb->postmeta + SET meta_value = 'onbackorder' + WHERE meta_key = '_stock_status' AND post_id IN ( " . implode( ',', $post_ids ) . ' )' + ); + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared +} + +/** + * Clear addons page transients + */ +function wc_update_330_clear_transients() { + delete_transient( 'wc_addons_sections' ); + delete_transient( 'wc_addons_featured' ); +} + +/** + * Set PayPal's sandbox credentials. + */ +function wc_update_330_set_paypal_sandbox_credentials() { + + $paypal_settings = get_option( 'woocommerce_paypal_settings' ); + + if ( isset( $paypal_settings['testmode'] ) && 'yes' === $paypal_settings['testmode'] ) { + foreach ( array( 'api_username', 'api_password', 'api_signature' ) as $credential ) { + if ( ! empty( $paypal_settings[ $credential ] ) ) { + $paypal_settings[ 'sandbox_' . $credential ] = $paypal_settings[ $credential ]; + } + } + + update_option( 'woocommerce_paypal_settings', $paypal_settings ); + } +} + +/** + * Update DB Version. + */ +function wc_update_330_db_version() { + WC_Install::update_db_version( '3.3.0' ); +} + +/** + * Update state codes for Ireland and BD. + */ +function wc_update_340_states() { + $country_states = array( + 'IE' => array( + 'CK' => 'CO', + 'DN' => 'D', + 'GY' => 'G', + 'TY' => 'TA', + ), + 'BD' => array( + 'BAG' => 'BD-05', + 'BAN' => 'BD-01', + 'BAR' => 'BD-02', + 'BARI' => 'BD-06', + 'BHO' => 'BD-07', + 'BOG' => 'BD-03', + 'BRA' => 'BD-04', + 'CHA' => 'BD-09', + 'CHI' => 'BD-10', + 'CHU' => 'BD-12', + 'COX' => 'BD-11', + 'COM' => 'BD-08', + 'DHA' => 'BD-13', + 'DIN' => 'BD-14', + 'FAR' => 'BD-15', + 'FEN' => 'BD-16', + 'GAI' => 'BD-19', + 'GAZI' => 'BD-18', + 'GOP' => 'BD-17', + 'HAB' => 'BD-20', + 'JAM' => 'BD-21', + 'JES' => 'BD-22', + 'JHA' => 'BD-25', + 'JHE' => 'BD-23', + 'JOY' => 'BD-24', + 'KHA' => 'BD-29', + 'KHU' => 'BD-27', + 'KIS' => 'BD-26', + 'KUR' => 'BD-28', + 'KUS' => 'BD-30', + 'LAK' => 'BD-31', + 'LAL' => 'BD-32', + 'MAD' => 'BD-36', + 'MAG' => 'BD-37', + 'MAN' => 'BD-33', + 'MEH' => 'BD-39', + 'MOU' => 'BD-38', + 'MUN' => 'BD-35', + 'MYM' => 'BD-34', + 'NAO' => 'BD-48', + 'NAR' => 'BD-43', + 'NARG' => 'BD-40', + 'NARD' => 'BD-42', + 'NAT' => 'BD-44', + 'NAW' => 'BD-45', + 'NET' => 'BD-41', + 'NIL' => 'BD-46', + 'NOA' => 'BD-47', + 'PAB' => 'BD-49', + 'PAN' => 'BD-52', + 'PAT' => 'BD-51', + 'PIR' => 'BD-50', + 'RAJB' => 'BD-53', + 'RAJ' => 'BD-54', + 'RAN' => 'BD-56', + 'RANP' => 'BD-55', + 'SAT' => 'BD-58', + 'SHA' => 'BD-57', + 'SIR' => 'BD-59', + 'SUN' => 'BD-61', + 'SYL' => 'BD-60', + 'TAN' => 'BD-63', + 'THA' => 'BD-64', + ), + ); + + update_option( 'woocommerce_update_340_states', $country_states ); +} + +/** + * Update next state in the queue. + * + * @return bool True to run again, false if completed. + */ +function wc_update_340_state() { + global $wpdb; + + $country_states = array_filter( (array) get_option( 'woocommerce_update_340_states', array() ) ); + + if ( empty( $country_states ) ) { + return false; + } + + foreach ( $country_states as $country => $states ) { + foreach ( $states as $old => $new ) { + $wpdb->query( + $wpdb->prepare( + "UPDATE $wpdb->postmeta + SET meta_value = %s + WHERE meta_key IN ( '_billing_state', '_shipping_state' ) + AND meta_value = %s", + $new, + $old + ) + ); + $wpdb->update( + "{$wpdb->prefix}woocommerce_shipping_zone_locations", + array( + 'location_code' => $country . ':' . $new, + ), + array( + 'location_code' => $country . ':' . $old, + ) + ); + $wpdb->update( + "{$wpdb->prefix}woocommerce_tax_rates", + array( + 'tax_rate_state' => strtoupper( $new ), + ), + array( + 'tax_rate_state' => strtoupper( $old ), + ) + ); + unset( $country_states[ $country ][ $old ] ); + + if ( empty( $country_states[ $country ] ) ) { + unset( $country_states[ $country ] ); + } + break 2; + } + } + + if ( ! empty( $country_states ) ) { + return update_option( 'woocommerce_update_340_states', $country_states ); + } + + delete_option( 'woocommerce_update_340_states' ); + + return false; +} + +/** + * Set last active prop for users. + */ +function wc_update_340_last_active() { + global $wpdb; + // @codingStandardsIgnoreStart. + $wpdb->query( + $wpdb->prepare( " + INSERT INTO {$wpdb->usermeta} (user_id, meta_key, meta_value) + SELECT DISTINCT users.ID, 'wc_last_active', %s + FROM {$wpdb->users} as users + LEFT OUTER JOIN {$wpdb->usermeta} AS usermeta ON users.ID = usermeta.user_id AND usermeta.meta_key = 'wc_last_active' + WHERE usermeta.meta_value IS NULL + ", + (string) strtotime( date( 'Y-m-d', current_time( 'timestamp', true ) ) ) + ) + ); + // @codingStandardsIgnoreEnd. +} + +/** + * Update DB Version. + */ +function wc_update_340_db_version() { + WC_Install::update_db_version( '3.4.0' ); +} + +/** + * Remove duplicate foreign keys + * + * @return void + */ +function wc_update_343_cleanup_foreign_keys() { + global $wpdb; + + $results = $wpdb->get_results( + "SELECT CONSTRAINT_NAME + FROM information_schema.TABLE_CONSTRAINTS + WHERE CONSTRAINT_SCHEMA = '{$wpdb->dbname}' + AND CONSTRAINT_NAME LIKE '%wc_download_log_ib%' + AND CONSTRAINT_TYPE = 'FOREIGN KEY' + AND TABLE_NAME = '{$wpdb->prefix}wc_download_log'" + ); + + if ( $results ) { + foreach ( $results as $fk ) { + $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_download_log DROP FOREIGN KEY {$fk->CONSTRAINT_NAME}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } + } +} + +/** + * Update DB version. + * + * @return void + */ +function wc_update_343_db_version() { + WC_Install::update_db_version( '3.4.3' ); +} + +/** + * Recreate user roles so existing users will get the new capabilities. + * + * @return void + */ +function wc_update_344_recreate_roles() { + WC_Install::remove_roles(); + WC_Install::create_roles(); +} + +/** + * Update DB version. + * + * @return void + */ +function wc_update_344_db_version() { + WC_Install::update_db_version( '3.4.4' ); +} + +/** + * Set the comment type to 'review' for product reviews that don't have a comment type. + */ +function wc_update_350_reviews_comment_type() { + global $wpdb; + + $wpdb->query( + "UPDATE {$wpdb->prefix}comments JOIN {$wpdb->prefix}posts ON {$wpdb->prefix}posts.ID = {$wpdb->prefix}comments.comment_post_ID AND ( {$wpdb->prefix}posts.post_type = 'product' OR {$wpdb->prefix}posts.post_type = 'product_variation' ) SET {$wpdb->prefix}comments.comment_type = 'review' WHERE {$wpdb->prefix}comments.comment_type = ''" + ); +} + +/** + * Update DB Version. + */ +function wc_update_350_db_version() { + WC_Install::update_db_version( '3.5.0' ); +} + +/** + * Drop the fk_wc_download_log_permission_id FK as we use a new one with the table and blog prefix for MS compatability. + * + * @return void + */ +function wc_update_352_drop_download_log_fk() { + global $wpdb; + $results = $wpdb->get_results( + "SELECT CONSTRAINT_NAME + FROM information_schema.TABLE_CONSTRAINTS + WHERE CONSTRAINT_SCHEMA = '{$wpdb->dbname}' + AND CONSTRAINT_NAME = 'fk_wc_download_log_permission_id' + AND CONSTRAINT_TYPE = 'FOREIGN KEY' + AND TABLE_NAME = '{$wpdb->prefix}wc_download_log'" + ); + + // We only need to drop the old key as WC_Install::create_tables() takes care of creating the new FK. + if ( $results ) { + $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_download_log DROP FOREIGN KEY fk_wc_download_log_permission_id" ); // phpcs:ignore WordPress.WP.PreparedSQL.NotPrepared + } +} + +/** + * Remove edit_user capabilities from shop managers and use "translated" capabilities instead. + * See wc_shop_manager_has_capability function. + */ +function wc_update_354_modify_shop_manager_caps() { + global $wp_roles; + + if ( ! class_exists( 'WP_Roles' ) ) { + return; + } + + if ( ! isset( $wp_roles ) ) { + $wp_roles = new WP_Roles(); // @codingStandardsIgnoreLine + } + + $wp_roles->remove_cap( 'shop_manager', 'edit_users' ); +} + +/** + * Update DB Version. + */ +function wc_update_354_db_version() { + WC_Install::update_db_version( '3.5.4' ); +} + +/** + * Update product lookup tables in bulk. + */ +function wc_update_360_product_lookup_tables() { + wc_update_product_lookup_tables(); +} + +/** + * Renames ordering meta to be consistent across taxonomies. + */ +function wc_update_360_term_meta() { + global $wpdb; + + $wpdb->query( "UPDATE {$wpdb->termmeta} SET meta_key = 'order' WHERE meta_key LIKE 'order_pa_%';" ); +} + +/** + * Add new user_order_remaining_expires to speed up user download permission fetching. + * + * @return void + */ +function wc_update_360_downloadable_product_permissions_index() { + global $wpdb; + + $index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE key_name = 'user_order_remaining_expires'" ); + + if ( is_null( $index_exists ) ) { + $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions ADD INDEX user_order_remaining_expires (user_id,order_id,downloads_remaining,access_expires)" ); + } +} + +/** + * Update DB Version. + */ +function wc_update_360_db_version() { + WC_Install::update_db_version( '3.6.0' ); +} + +/** + * Put tax classes into a DB table. + * + * @return void + */ +function wc_update_370_tax_rate_classes() { + global $wpdb; + + $classes = array_map( 'trim', explode( "\n", get_option( 'woocommerce_tax_classes' ) ) ); + + if ( $classes ) { + foreach ( $classes as $class ) { + if ( empty( $class ) ) { + continue; + } + WC_Tax::create_tax_class( $class ); + } + } + delete_option( 'woocommerce_tax_classes' ); +} + +/** + * Update currency settings for 3.7.0 + * + * @return void + */ +function wc_update_370_mro_std_currency() { + global $wpdb; + + // Fix currency settings for MRU and STN currency. + $current_currency = get_option( 'woocommerce_currency' ); + + if ( 'MRO' === $current_currency ) { + update_option( 'woocommerce_currency', 'MRU' ); + } + + if ( 'STD' === $current_currency ) { + update_option( 'woocommerce_currency', 'STN' ); + } + + // Update MRU currency code. + $wpdb->update( + $wpdb->postmeta, + array( + 'meta_value' => 'MRU', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + ), + array( + 'meta_key' => '_order_currency', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + 'meta_value' => 'MRO', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + ) + ); + + // Update STN currency code. + $wpdb->update( + $wpdb->postmeta, + array( + 'meta_value' => 'STN', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + ), + array( + 'meta_key' => '_order_currency', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + 'meta_value' => 'STD', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + ) + ); +} + +/** + * Update DB Version. + */ +function wc_update_370_db_version() { + WC_Install::update_db_version( '3.7.0' ); +} + +/** + * We've moved the MaxMind database to a new location, as per the TOS' requirement that the database not + * be publicly accessible. + */ +function wc_update_390_move_maxmind_database() { + // Make sure to use all of the correct filters to pull the local database path. + $old_path = apply_filters( 'woocommerce_geolocation_local_database_path', WP_CONTENT_DIR . '/uploads/GeoLite2-Country.mmdb', 2 ); + + // Generate a prefix for the old file and store it in the integration as it would expect it. + $prefix = wp_generate_password( 32, false ); + update_option( 'woocommerce_maxmind_geolocation_settings', array( 'database_prefix' => $prefix ) ); + + // Generate the new path in the same way that the integration will. + $uploads_dir = wp_upload_dir(); + $new_path = trailingslashit( $uploads_dir['basedir'] ) . 'woocommerce_uploads/' . $prefix . '-GeoLite2-Country.mmdb'; + $new_path = apply_filters( 'woocommerce_geolocation_local_database_path', $new_path, 2 ); + $new_path = apply_filters( 'woocommerce_maxmind_geolocation_database_path', $new_path ); + + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + @rename( $old_path, $new_path ); +} + +/** + * So that we can best meet MaxMind's TOS, the geolocation database update cron should run once per 15 days. + */ +function wc_update_390_change_geolocation_database_update_cron() { + wp_clear_scheduled_hook( 'woocommerce_geoip_updater' ); + wp_schedule_event( time() + ( DAY_IN_SECONDS * 15 ), 'fifteendays', 'woocommerce_geoip_updater' ); +} + +/** + * Update DB version. + */ +function wc_update_390_db_version() { + WC_Install::update_db_version( '3.9.0' ); +} + +/** + * Increase column size + */ +function wc_update_400_increase_size_of_column() { + global $wpdb; + $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_product_meta_lookup MODIFY COLUMN `min_price` decimal(19,4) NULL default NULL" ); + $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_product_meta_lookup MODIFY COLUMN `max_price` decimal(19,4) NULL default NULL" ); +} + +/** + * Reset ActionScheduler migration status. Needs AS >= 3.0 shipped with WC >= 4.0. + */ +function wc_update_400_reset_action_scheduler_migration_status() { + if ( + class_exists( 'ActionScheduler_DataController' ) && + method_exists( 'ActionScheduler_DataController', 'mark_migration_incomplete' ) + ) { + \ActionScheduler_DataController::mark_migration_incomplete(); + } +} + +/** + * Update DB version. + */ +function wc_update_400_db_version() { + WC_Install::update_db_version( '4.0.0' ); +} + +/** + * Register attributes as terms for variable products, in increments of 100 products. + * + * This migration was added to support a new mechanism to improve the filtering of + * variable products by attribute (https://github.com/woocommerce/woocommerce/pull/26260), + * however that mechanism was later reverted (https://github.com/woocommerce/woocommerce/pull/27625) + * due to numerous issues found. Thus the migration is no longer needed. + * + * @return bool true if the migration needs to be run again. + */ +function wc_update_440_insert_attribute_terms_for_variable_products() { + return false; +} + +/** + * Update DB version. + */ +function wc_update_440_db_version() { + WC_Install::update_db_version( '4.4.0' ); +} + +/** + * Update DB version to 4.5.0. + */ +function wc_update_450_db_version() { + WC_Install::update_db_version( '4.5.0' ); +} + +/** + * Sanitize all coupons code. + * + * @return bool True to run again, false if completed. + */ +function wc_update_450_sanitize_coupons_code() { + global $wpdb; + + $coupon_id = 0; + $last_coupon_id = get_option( 'woocommerce_update_450_last_coupon_id', '0' ); + + $coupons = $wpdb->get_results( + $wpdb->prepare( + "SELECT ID, post_title FROM $wpdb->posts WHERE ID > %d AND post_type = 'shop_coupon' LIMIT 10", + $last_coupon_id + ), + ARRAY_A + ); + + if ( empty( $coupons ) ) { + delete_option( 'woocommerce_update_450_last_coupon_id' ); + return false; + } + + foreach ( $coupons as $key => $data ) { + $coupon_id = intval( $data['ID'] ); + $code = trim( wp_filter_kses( $data['post_title'] ) ); + + if ( ! empty( $code ) && $data['post_title'] !== $code ) { + $wpdb->update( + $wpdb->posts, + array( + 'post_title' => $code, + ), + array( + 'ID' => $coupon_id, + ), + array( + '%s', + ), + array( + '%d', + ) + ); + + // Clean cache. + clean_post_cache( $coupon_id ); + wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $data['post_title'], 'coupons' ); + } + } + + // Start the run again. + if ( $coupon_id ) { + return update_option( 'woocommerce_update_450_last_coupon_id', $coupon_id ); + } + + delete_option( 'woocommerce_update_450_last_coupon_id' ); + return false; +} + +/** + * Fixes product review count that might have been incorrect. + * + * See @link https://github.com/woocommerce/woocommerce/issues/27688. + */ +function wc_update_500_fix_product_review_count() { + global $wpdb; + + $product_id = 0; + $last_product_id = get_option( 'woocommerce_update_500_last_product_id', '0' ); + + $products_data = $wpdb->get_results( + $wpdb->prepare( + " + SELECT post_id, meta_value + FROM $wpdb->postmeta + JOIN $wpdb->posts + ON $wpdb->postmeta.post_id = $wpdb->posts.ID + WHERE + post_type = 'product' + AND post_status = 'publish' + AND post_id > %d + AND meta_key = '_wc_review_count' + ORDER BY post_id ASC + LIMIT 10 + ", + $last_product_id + ), + ARRAY_A + ); + + if ( empty( $products_data ) ) { + delete_option( 'woocommerce_update_500_last_product_id' ); + return false; + } + + $product_ids_to_check = array_column( $products_data, 'post_id' ); + $actual_review_counts = WC_Comments::get_review_counts_for_product_ids( $product_ids_to_check ); + + foreach ( $products_data as $product_data ) { + $product_id = intval( $product_data['post_id'] ); + $current_review_count = intval( $product_data['meta_value'] ); + + if ( intval( $actual_review_counts[ $product_id ] ) !== $current_review_count ) { + WC_Comments::clear_transients( $product_id ); + } + } + + // Start the run again. + if ( $product_id ) { + return update_option( 'woocommerce_update_500_last_product_id', $product_id ); + } + + delete_option( 'woocommerce_update_500_last_product_id' ); + return false; +} + +/** + * Update DB version to 5.0.0. + */ +function wc_update_500_db_version() { + WC_Install::update_db_version( '5.0.0' ); +} + +/** + * Creates the refund and returns policy page. + * + * See @link https://github.com/woocommerce/woocommerce/issues/29235. + */ +function wc_update_560_create_refund_returns_page() { + /** + * Filter on the pages created to return what we expect. + * + * @param array $pages The default WC pages. + */ + function filter_created_pages( $pages ) { + $page_to_create = array( 'refund_returns' ); + + return array_intersect_key( $pages, array_flip( $page_to_create ) ); + } + + add_filter( 'woocommerce_create_pages', 'filter_created_pages' ); + + WC_Install::create_pages(); + + remove_filter( 'woocommerce_create_pages', 'filter_created_pages' ); +} + +/** + * Update DB version to 5.6.0. + */ +function wc_update_560_db_version() { + WC_Install::update_db_version( '5.6.0' ); +} + +/** + * Migrate rate limit options to the new table. + * + * See @link https://github.com/woocommerce/woocommerce/issues/27103. + */ +function wc_update_600_migrate_rate_limit_options() { + global $wpdb; + + $rate_limits = $wpdb->get_results( + " + SELECT option_name, option_value + FROM $wpdb->options + WHERE option_name LIKE 'woocommerce_rate_limit_add_payment_method_%' + ", + ARRAY_A + ); + $prefix_length = strlen( 'woocommerce_rate_limit_' ); + + foreach ( $rate_limits as $rate_limit ) { + $new_delay = (int) $rate_limit['option_value'] - time(); + + // Migrate the limit if it hasn't expired yet. + if ( 0 < $new_delay ) { + $action_id = substr( $rate_limit['option_name'], $prefix_length ); + WC_Rate_Limiter::set_rate_limit( $action_id, $new_delay ); + } + + delete_option( $rate_limit['option_name'] ); + } +} + +/** + * Update DB version to 6.0.0. + */ +function wc_update_600_db_version() { + WC_Install::update_db_version( '6.0.0' ); +} + +/** + * Create the product attributes lookup table and initiate its filling, + * unless the table had been already created manually (via the tools page). + * + * @return false Always false, since the LookupDataStore class handles all the data filling process. + */ +function wc_update_630_create_product_attributes_lookup_table() { + $data_store = wc_get_container()->get( LookupDataStore::class ); + $data_regenerator = wc_get_container()->get( DataRegenerator::class ); + + /** + * If the table exists and contains data, it was manually created by user before the migration ran. + * If the table exists but is empty, it was likely created right now via dbDelta, so a table regenerations is needed. + */ + if ( ! $data_store->check_lookup_table_exists() || ! $data_store->lookup_table_has_data() ) { + $data_regenerator->initiate_regeneration(); + } + + return false; +} + +/** + * + * Update DB version to 6.3.0. + */ +function wc_update_630_db_version() { + WC_Install::update_db_version( '6.3.0' ); +} + +/** + * Add the standard WooCommerce upload directories to the Approved Product Download Directories list + * and start populating it based on existing product download URLs, but do not enable the feature + * (for existing installations, a site admin should review and make a conscious decision to enable). + */ +function wc_update_640_approved_download_directories() { + wc_get_container()->get( Download_Directories_Sync::class )->init_feature( true, false ); +} + +/** + * Create the primary key for the product attributes lookup table if it doesn't exist already. + * + * @return bool Always false. + */ +function wc_update_640_add_primary_key_to_product_attributes_lookup_table() { + wc_get_container()->get( DataRegenerator::class )->create_table_primary_index(); + + return false; +} + +/** + * + * Update DB version to 6.4.0. + */ +function wc_update_640_db_version() { + WC_Install::update_db_version( '6.4.0' ); +} diff --git a/plugins/woocommerce/includes/wc-user-functions.php b/plugins/woocommerce/includes/wc-user-functions.php new file mode 100644 index 00000000000..a874c92a052 --- /dev/null +++ b/plugins/woocommerce/includes/wc-user-functions.php @@ -0,0 +1,931 @@ +Please log in.', 'woocommerce' ), $email ) ); + } + + if ( 'yes' === get_option( 'woocommerce_registration_generate_username', 'yes' ) && empty( $username ) ) { + $username = wc_create_new_customer_username( $email, $args ); + } + + $username = sanitize_user( $username ); + + if ( empty( $username ) || ! validate_username( $username ) ) { + return new WP_Error( 'registration-error-invalid-username', __( 'Please enter a valid account username.', 'woocommerce' ) ); + } + + if ( username_exists( $username ) ) { + return new WP_Error( 'registration-error-username-exists', __( 'An account is already registered with that username. Please choose another.', 'woocommerce' ) ); + } + + // Handle password creation. + $password_generated = false; + if ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) && empty( $password ) ) { + $password = wp_generate_password(); + $password_generated = true; + } + + if ( empty( $password ) ) { + return new WP_Error( 'registration-error-missing-password', __( 'Please enter an account password.', 'woocommerce' ) ); + } + + // Use WP_Error to handle registration errors. + $errors = new WP_Error(); + + do_action( 'woocommerce_register_post', $username, $email, $errors ); + + $errors = apply_filters( 'woocommerce_registration_errors', $errors, $username, $email ); + + if ( $errors->get_error_code() ) { + return $errors; + } + + $new_customer_data = apply_filters( + 'woocommerce_new_customer_data', + array_merge( + $args, + array( + 'user_login' => $username, + 'user_pass' => $password, + 'user_email' => $email, + 'role' => 'customer', + ) + ) + ); + + $customer_id = wp_insert_user( $new_customer_data ); + + if ( is_wp_error( $customer_id ) ) { + return $customer_id; + } + + do_action( 'woocommerce_created_customer', $customer_id, $new_customer_data, $password_generated ); + + return $customer_id; + } +} + +/** + * Create a unique username for a new customer. + * + * @since 3.6.0 + * @param string $email New customer email address. + * @param array $new_user_args Array of new user args, maybe including first and last names. + * @param string $suffix Append string to username to make it unique. + * @return string Generated username. + */ +function wc_create_new_customer_username( $email, $new_user_args = array(), $suffix = '' ) { + $username_parts = array(); + + if ( isset( $new_user_args['first_name'] ) ) { + $username_parts[] = sanitize_user( $new_user_args['first_name'], true ); + } + + if ( isset( $new_user_args['last_name'] ) ) { + $username_parts[] = sanitize_user( $new_user_args['last_name'], true ); + } + + // Remove empty parts. + $username_parts = array_filter( $username_parts ); + + // If there are no parts, e.g. name had unicode chars, or was not provided, fallback to email. + if ( empty( $username_parts ) ) { + $email_parts = explode( '@', $email ); + $email_username = $email_parts[0]; + + // Exclude common prefixes. + if ( in_array( + $email_username, + array( + 'sales', + 'hello', + 'mail', + 'contact', + 'info', + ), + true + ) ) { + // Get the domain part. + $email_username = $email_parts[1]; + } + + $username_parts[] = sanitize_user( $email_username, true ); + } + + $username = wc_strtolower( implode( '.', $username_parts ) ); + + if ( $suffix ) { + $username .= $suffix; + } + + /** + * WordPress 4.4 - filters the list of blocked usernames. + * + * @since 3.7.0 + * @param array $usernames Array of blocked usernames. + */ + $illegal_logins = (array) apply_filters( 'illegal_user_logins', array() ); + + // Stop illegal logins and generate a new random username. + if ( in_array( strtolower( $username ), array_map( 'strtolower', $illegal_logins ), true ) ) { + $new_args = array(); + + /** + * Filter generated customer username. + * + * @since 3.7.0 + * @param string $username Generated username. + * @param string $email New customer email address. + * @param array $new_user_args Array of new user args, maybe including first and last names. + * @param string $suffix Append string to username to make it unique. + */ + $new_args['first_name'] = apply_filters( + 'woocommerce_generated_customer_username', + 'woo_user_' . zeroise( wp_rand( 0, 9999 ), 4 ), + $email, + $new_user_args, + $suffix + ); + + return wc_create_new_customer_username( $email, $new_args, $suffix ); + } + + if ( username_exists( $username ) ) { + // Generate something unique to append to the username in case of a conflict with another user. + $suffix = '-' . zeroise( wp_rand( 0, 9999 ), 4 ); + return wc_create_new_customer_username( $email, $new_user_args, $suffix ); + } + + /** + * Filter new customer username. + * + * @since 3.7.0 + * @param string $username Customer username. + * @param string $email New customer email address. + * @param array $new_user_args Array of new user args, maybe including first and last names. + * @param string $suffix Append string to username to make it unique. + */ + return apply_filters( 'woocommerce_new_customer_username', $username, $email, $new_user_args, $suffix ); +} + +/** + * Login a customer (set auth cookie and set global user object). + * + * @param int $customer_id Customer ID. + */ +function wc_set_customer_auth_cookie( $customer_id ) { + wp_set_current_user( $customer_id ); + wp_set_auth_cookie( $customer_id, true ); + + // Update session. + WC()->session->init_session_cookie(); +} + +/** + * Get past orders (by email) and update them. + * + * @param int $customer_id Customer ID. + * @return int + */ +function wc_update_new_customer_past_orders( $customer_id ) { + $linked = 0; + $complete = 0; + $customer = get_user_by( 'id', absint( $customer_id ) ); + $customer_orders = wc_get_orders( + array( + 'limit' => -1, + 'customer' => array( array( 0, $customer->user_email ) ), + 'return' => 'ids', + ) + ); + + if ( ! empty( $customer_orders ) ) { + foreach ( $customer_orders as $order_id ) { + $order = wc_get_order( $order_id ); + if ( ! $order ) { + continue; + } + + $order->set_customer_id( $customer->ID ); + $order->save(); + + if ( $order->has_downloadable_item() ) { + $data_store = WC_Data_Store::load( 'customer-download' ); + $data_store->delete_by_order_id( $order->get_id() ); + wc_downloadable_product_permissions( $order->get_id(), true ); + } + + do_action( 'woocommerce_update_new_customer_past_order', $order_id, $customer ); + + if ( get_post_status( $order_id ) === 'wc-completed' ) { + $complete++; + } + + $linked++; + } + } + + if ( $complete ) { + update_user_meta( $customer_id, 'paying_customer', 1 ); + update_user_meta( $customer_id, '_order_count', '' ); + update_user_meta( $customer_id, '_money_spent', '' ); + delete_user_meta( $customer_id, '_last_order' ); + } + + return $linked; +} + +/** + * Order payment completed - This is a paying customer. + * + * @param int $order_id Order ID. + */ +function wc_paying_customer( $order_id ) { + $order = wc_get_order( $order_id ); + $customer_id = $order->get_customer_id(); + + if ( $customer_id > 0 && 'shop_order_refund' !== $order->get_type() ) { + $customer = new WC_Customer( $customer_id ); + + if ( ! $customer->get_is_paying_customer() ) { + $customer->set_is_paying_customer( true ); + $customer->save(); + } + } +} +add_action( 'woocommerce_payment_complete', 'wc_paying_customer' ); +add_action( 'woocommerce_order_status_completed', 'wc_paying_customer' ); + +/** + * Checks if a user (by email or ID or both) has bought an item. + * + * @param string $customer_email Customer email to check. + * @param int $user_id User ID to check. + * @param int $product_id Product ID to check. + * @return bool + */ +function wc_customer_bought_product( $customer_email, $user_id, $product_id ) { + global $wpdb; + + $result = apply_filters( 'woocommerce_pre_customer_bought_product', null, $customer_email, $user_id, $product_id ); + + if ( null !== $result ) { + return $result; + } + + $transient_name = 'wc_customer_bought_product_' . md5( $customer_email . $user_id ); + $transient_version = WC_Cache_Helper::get_transient_version( 'orders' ); + $transient_value = get_transient( $transient_name ); + + if ( isset( $transient_value['value'], $transient_value['version'] ) && $transient_value['version'] === $transient_version ) { + $result = $transient_value['value']; + } else { + $customer_data = array( $user_id ); + + if ( $user_id ) { + $user = get_user_by( 'id', $user_id ); + + if ( isset( $user->user_email ) ) { + $customer_data[] = $user->user_email; + } + } + + if ( is_email( $customer_email ) ) { + $customer_data[] = $customer_email; + } + + $customer_data = array_map( 'esc_sql', array_filter( array_unique( $customer_data ) ) ); + $statuses = array_map( 'esc_sql', wc_get_is_paid_statuses() ); + + if ( count( $customer_data ) === 0 ) { + return false; + } + + $result = $wpdb->get_col( + " + SELECT im.meta_value FROM {$wpdb->posts} AS p + INNER JOIN {$wpdb->postmeta} AS pm ON p.ID = pm.post_id + INNER JOIN {$wpdb->prefix}woocommerce_order_items AS i ON p.ID = i.order_id + INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS im ON i.order_item_id = im.order_item_id + WHERE p.post_status IN ( 'wc-" . implode( "','wc-", $statuses ) . "' ) + AND pm.meta_key IN ( '_billing_email', '_customer_user' ) + AND im.meta_key IN ( '_product_id', '_variation_id' ) + AND im.meta_value != 0 + AND pm.meta_value IN ( '" . implode( "','", $customer_data ) . "' ) + " + ); // WPCS: unprepared SQL ok. + $result = array_map( 'absint', $result ); + + $transient_value = array( + 'version' => $transient_version, + 'value' => $result, + ); + + set_transient( $transient_name, $transient_value, DAY_IN_SECONDS * 30 ); + } + return in_array( absint( $product_id ), $result, true ); +} + +/** + * Checks if the current user has a role. + * + * @param string $role The role. + * @return bool + */ +function wc_current_user_has_role( $role ) { + return wc_user_has_role( wp_get_current_user(), $role ); +} + +/** + * Checks if a user has a role. + * + * @param int|\WP_User $user The user. + * @param string $role The role. + * @return bool + */ +function wc_user_has_role( $user, $role ) { + if ( ! is_object( $user ) ) { + $user = get_userdata( $user ); + } + + if ( ! $user || ! $user->exists() ) { + return false; + } + + return in_array( $role, $user->roles, true ); +} + +/** + * Checks if a user has a certain capability. + * + * @param array $allcaps All capabilities. + * @param array $caps Capabilities. + * @param array $args Arguments. + * + * @return array The filtered array of all capabilities. + */ +function wc_customer_has_capability( $allcaps, $caps, $args ) { + if ( isset( $caps[0] ) ) { + switch ( $caps[0] ) { + case 'view_order': + $user_id = intval( $args[1] ); + $order = wc_get_order( $args[2] ); + + if ( $order && $user_id === $order->get_user_id() ) { + $allcaps['view_order'] = true; + } + break; + case 'pay_for_order': + $user_id = intval( $args[1] ); + $order_id = isset( $args[2] ) ? $args[2] : null; + + // When no order ID, we assume it's a new order + // and thus, customer can pay for it. + if ( ! $order_id ) { + $allcaps['pay_for_order'] = true; + break; + } + + $order = wc_get_order( $order_id ); + + if ( $order && ( $user_id === $order->get_user_id() || ! $order->get_user_id() ) ) { + $allcaps['pay_for_order'] = true; + } + break; + case 'order_again': + $user_id = intval( $args[1] ); + $order = wc_get_order( $args[2] ); + + if ( $order && $user_id === $order->get_user_id() ) { + $allcaps['order_again'] = true; + } + break; + case 'cancel_order': + $user_id = intval( $args[1] ); + $order = wc_get_order( $args[2] ); + + if ( $order && $user_id === $order->get_user_id() ) { + $allcaps['cancel_order'] = true; + } + break; + case 'download_file': + $user_id = intval( $args[1] ); + $download = $args[2]; + + if ( $download && $user_id === $download->get_user_id() ) { + $allcaps['download_file'] = true; + } + break; + } + } + return $allcaps; +} +add_filter( 'user_has_cap', 'wc_customer_has_capability', 10, 3 ); + +/** + * Safe way of allowing shop managers restricted capabilities that will remove + * access to the capabilities if WooCommerce is deactivated. + * + * @since 3.5.4 + * @param bool[] $allcaps Array of key/value pairs where keys represent a capability name and boolean values + * represent whether the user has that capability. + * @param string[] $caps Required primitive capabilities for the requested capability. + * @param array $args Arguments that accompany the requested capability check. + * @param WP_User $user The user object. + * @return bool[] + */ +function wc_shop_manager_has_capability( $allcaps, $caps, $args, $user ) { + + if ( wc_user_has_role( $user, 'shop_manager' ) ) { + // @see wc_modify_map_meta_cap, which limits editing to customers. + $allcaps['edit_users'] = true; + } + + return $allcaps; +} +add_filter( 'user_has_cap', 'wc_shop_manager_has_capability', 10, 4 ); + +/** + * Modify the list of editable roles to prevent non-admin adding admin users. + * + * @param array $roles Roles. + * @return array + */ +function wc_modify_editable_roles( $roles ) { + if ( is_multisite() && is_super_admin() ) { + return $roles; + } + if ( ! wc_current_user_has_role( 'administrator' ) ) { + unset( $roles['administrator'] ); + + if ( wc_current_user_has_role( 'shop_manager' ) ) { + $shop_manager_editable_roles = apply_filters( 'woocommerce_shop_manager_editable_roles', array( 'customer' ) ); + return array_intersect_key( $roles, array_flip( $shop_manager_editable_roles ) ); + } + } + + return $roles; +} +add_filter( 'editable_roles', 'wc_modify_editable_roles' ); + +/** + * Modify capabilities to prevent non-admin users editing admin users. + * + * $args[0] will be the user being edited in this case. + * + * @param array $caps Array of caps. + * @param string $cap Name of the cap we are checking. + * @param int $user_id ID of the user being checked against. + * @param array $args Arguments. + * @return array + */ +function wc_modify_map_meta_cap( $caps, $cap, $user_id, $args ) { + if ( is_multisite() && is_super_admin() ) { + return $caps; + } + switch ( $cap ) { + case 'edit_user': + case 'remove_user': + case 'promote_user': + case 'delete_user': + if ( ! isset( $args[0] ) || $args[0] === $user_id ) { + break; + } else { + if ( ! wc_current_user_has_role( 'administrator' ) ) { + if ( wc_user_has_role( $args[0], 'administrator' ) ) { + $caps[] = 'do_not_allow'; + } elseif ( wc_current_user_has_role( 'shop_manager' ) ) { + // Shop managers can only edit customer info. + $userdata = get_userdata( $args[0] ); + $shop_manager_editable_roles = apply_filters( 'woocommerce_shop_manager_editable_roles', array( 'customer' ) ); + if ( property_exists( $userdata, 'roles' ) && ! empty( $userdata->roles ) && ! array_intersect( $userdata->roles, $shop_manager_editable_roles ) ) { + $caps[] = 'do_not_allow'; + } + } + } + } + break; + } + return $caps; +} +add_filter( 'map_meta_cap', 'wc_modify_map_meta_cap', 10, 4 ); + +/** + * Get customer download permissions from the database. + * + * @param int $customer_id Customer/User ID. + * @return array + */ +function wc_get_customer_download_permissions( $customer_id ) { + $data_store = WC_Data_Store::load( 'customer-download' ); + return apply_filters( 'woocommerce_permission_list', $data_store->get_downloads_for_customer( $customer_id ), $customer_id ); +} + +/** + * Get customer available downloads. + * + * @param int $customer_id Customer/User ID. + * @return array + */ +function wc_get_customer_available_downloads( $customer_id ) { + $downloads = array(); + $_product = null; + $order = null; + $file_number = 0; + + // Get results from valid orders only. + $results = wc_get_customer_download_permissions( $customer_id ); + + if ( $results ) { + foreach ( $results as $result ) { + $order_id = intval( $result->order_id ); + + if ( ! $order || $order->get_id() !== $order_id ) { + // New order. + $order = wc_get_order( $order_id ); + $_product = null; + } + + // Make sure the order exists for this download. + if ( ! $order ) { + continue; + } + + // Check if downloads are permitted. + if ( ! $order->is_download_permitted() ) { + continue; + } + + $product_id = intval( $result->product_id ); + + if ( ! $_product || $_product->get_id() !== $product_id ) { + // New product. + $file_number = 0; + $_product = wc_get_product( $product_id ); + } + + // Check product exists and has the file. + if ( ! $_product || ! $_product->exists() || ! $_product->has_file( $result->download_id ) ) { + continue; + } + + $download_file = $_product->get_file( $result->download_id ); + + // If the downloadable file has been disabled (it may be located in an untrusted location) then do not return it. + if ( ! $download_file->get_enabled() ) { + continue; + } + + // Download name will be 'Product Name' for products with a single downloadable file, and 'Product Name - File X' for products with multiple files. + $download_name = apply_filters( + 'woocommerce_downloadable_product_name', + $download_file['name'], + $_product, + $result->download_id, + $file_number + ); + + $downloads[] = array( + 'download_url' => add_query_arg( + array( + 'download_file' => $product_id, + 'order' => $result->order_key, + 'email' => rawurlencode( $result->user_email ), + 'key' => $result->download_id, + ), + home_url( '/' ) + ), + 'download_id' => $result->download_id, + 'product_id' => $_product->get_id(), + 'product_name' => $_product->get_name(), + 'product_url' => $_product->is_visible() ? $_product->get_permalink() : '', // Since 3.3.0. + 'download_name' => $download_name, + 'order_id' => $order->get_id(), + 'order_key' => $order->get_order_key(), + 'downloads_remaining' => $result->downloads_remaining, + 'access_expires' => $result->access_expires, + 'file' => array( + 'name' => $download_file->get_name(), + 'file' => $download_file->get_file(), + ), + ); + + $file_number++; + } + } + + return apply_filters( 'woocommerce_customer_available_downloads', $downloads, $customer_id ); +} + +/** + * Get total spent by customer. + * + * @param int $user_id User ID. + * @return string + */ +function wc_get_customer_total_spent( $user_id ) { + $customer = new WC_Customer( $user_id ); + return $customer->get_total_spent(); +} + +/** + * Get total orders by customer. + * + * @param int $user_id User ID. + * @return int + */ +function wc_get_customer_order_count( $user_id ) { + $customer = new WC_Customer( $user_id ); + return $customer->get_order_count(); +} + +/** + * Reset _customer_user on orders when a user is deleted. + * + * @param int $user_id User ID. + */ +function wc_reset_order_customer_id_on_deleted_user( $user_id ) { + global $wpdb; + + $wpdb->update( + $wpdb->postmeta, + array( + 'meta_value' => 0, + ), + array( + 'meta_key' => '_customer_user', + 'meta_value' => $user_id, + ) + ); // WPCS: slow query ok. +} + +add_action( 'deleted_user', 'wc_reset_order_customer_id_on_deleted_user' ); + +/** + * Get review verification status. + * + * @param int $comment_id Comment ID. + * @return bool + */ +function wc_review_is_from_verified_owner( $comment_id ) { + $verified = get_comment_meta( $comment_id, 'verified', true ); + return '' === $verified ? WC_Comments::add_comment_purchase_verification( $comment_id ) : (bool) $verified; +} + +/** + * Disable author archives for customers. + * + * @since 2.5.0 + */ +function wc_disable_author_archives_for_customers() { + global $author; + + if ( is_author() ) { + $user = get_user_by( 'id', $author ); + + if ( user_can( $user, 'customer' ) && ! user_can( $user, 'edit_posts' ) ) { + wp_safe_redirect( wc_get_page_permalink( 'shop' ) ); + exit; + } + } +} + +add_action( 'template_redirect', 'wc_disable_author_archives_for_customers' ); + +/** + * Hooks into the `profile_update` hook to set the user last updated timestamp. + * + * @since 2.6.0 + * @param int $user_id The user that was updated. + * @param array $old The profile fields pre-change. + */ +function wc_update_profile_last_update_time( $user_id, $old ) { + wc_set_user_last_update_time( $user_id ); +} + +add_action( 'profile_update', 'wc_update_profile_last_update_time', 10, 2 ); + +/** + * Hooks into the update user meta function to set the user last updated timestamp. + * + * @since 2.6.0 + * @param int $meta_id ID of the meta object that was changed. + * @param int $user_id The user that was updated. + * @param string $meta_key Name of the meta key that was changed. + * @param string $_meta_value Value of the meta that was changed. + */ +function wc_meta_update_last_update_time( $meta_id, $user_id, $meta_key, $_meta_value ) { + $keys_to_track = apply_filters( 'woocommerce_user_last_update_fields', array( 'first_name', 'last_name' ) ); + + $update_time = in_array( $meta_key, $keys_to_track, true ) ? true : false; + $update_time = 'billing_' === substr( $meta_key, 0, 8 ) ? true : $update_time; + $update_time = 'shipping_' === substr( $meta_key, 0, 9 ) ? true : $update_time; + + if ( $update_time ) { + wc_set_user_last_update_time( $user_id ); + } +} + +add_action( 'update_user_meta', 'wc_meta_update_last_update_time', 10, 4 ); + +/** + * Sets a user's "last update" time to the current timestamp. + * + * @since 2.6.0 + * @param int $user_id The user to set a timestamp for. + */ +function wc_set_user_last_update_time( $user_id ) { + update_user_meta( $user_id, 'last_update', gmdate( 'U' ) ); +} + +/** + * Get customer saved payment methods list. + * + * @since 2.6.0 + * @param int $customer_id Customer ID. + * @return array + */ +function wc_get_customer_saved_methods_list( $customer_id ) { + return apply_filters( 'woocommerce_saved_payment_methods_list', array(), $customer_id ); +} + +/** + * Get info about customer's last order. + * + * @since 2.6.0 + * @param int $customer_id Customer ID. + * @return WC_Order|bool Order object if successful or false. + */ +function wc_get_customer_last_order( $customer_id ) { + $customer = new WC_Customer( $customer_id ); + + return $customer->get_last_order(); +} + +/** + * Add support for searching by display_name. + * + * @since 3.2.0 + * @param array $search_columns Column names. + * @return array + */ +function wc_user_search_columns( $search_columns ) { + $search_columns[] = 'display_name'; + return $search_columns; +} +add_filter( 'user_search_columns', 'wc_user_search_columns' ); + +/** + * When a user is deleted in WordPress, delete corresponding WooCommerce data. + * + * @param int $user_id User ID being deleted. + */ +function wc_delete_user_data( $user_id ) { + global $wpdb; + + // Clean up sessions. + $wpdb->delete( + $wpdb->prefix . 'woocommerce_sessions', + array( + 'session_key' => $user_id, + ) + ); + + // Revoke API keys. + $wpdb->delete( + $wpdb->prefix . 'woocommerce_api_keys', + array( + 'user_id' => $user_id, + ) + ); + + // Clean up payment tokens. + $payment_tokens = WC_Payment_Tokens::get_customer_tokens( $user_id ); + + foreach ( $payment_tokens as $payment_token ) { + $payment_token->delete(); + } +} +add_action( 'delete_user', 'wc_delete_user_data' ); + +/** + * Store user agents. Used for tracker. + * + * @since 3.0.0 + * @param string $user_login User login. + * @param int|object $user User. + */ +function wc_maybe_store_user_agent( $user_login, $user ) { + if ( 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ) && user_can( $user, 'manage_woocommerce' ) ) { + $admin_user_agents = array_filter( (array) get_option( 'woocommerce_tracker_ua', array() ) ); + $admin_user_agents[] = wc_get_user_agent(); + update_option( 'woocommerce_tracker_ua', array_unique( $admin_user_agents ) ); + } +} +add_action( 'wp_login', 'wc_maybe_store_user_agent', 10, 2 ); + +/** + * Update logic triggered on login. + * + * @since 3.4.0 + * @param string $user_login User login. + * @param object $user User. + */ +function wc_user_logged_in( $user_login, $user ) { + wc_update_user_last_active( $user->ID ); + update_user_meta( $user->ID, '_woocommerce_load_saved_cart_after_login', 1 ); +} +add_action( 'wp_login', 'wc_user_logged_in', 10, 2 ); + +/** + * Update when the user was last active. + * + * @since 3.4.0 + */ +function wc_current_user_is_active() { + if ( ! is_user_logged_in() ) { + return; + } + wc_update_user_last_active( get_current_user_id() ); +} +add_action( 'wp', 'wc_current_user_is_active', 10 ); + +/** + * Set the user last active timestamp to now. + * + * @since 3.4.0 + * @param int $user_id User ID to mark active. + */ +function wc_update_user_last_active( $user_id ) { + if ( ! $user_id ) { + return; + } + update_user_meta( $user_id, 'wc_last_active', (string) strtotime( gmdate( 'Y-m-d', time() ) ) ); +} + +/** + * Translate WC roles using the woocommerce textdomain. + * + * @since 3.7.0 + * @param string $translation Translated text. + * @param string $text Text to translate. + * @param string $context Context information for the translators. + * @param string $domain Text domain. Unique identifier for retrieving translated strings. + * @return string + */ +function wc_translate_user_roles( $translation, $text, $context, $domain ) { + // translate_user_role() only accepts a second parameter starting in WP 5.2. + if ( version_compare( get_bloginfo( 'version' ), '5.2', '<' ) ) { + return $translation; + } + + if ( 'User role' === $context && 'default' === $domain && in_array( $text, array( 'Shop manager', 'Customer' ), true ) ) { + return translate_user_role( $text, 'woocommerce' ); + } + + return $translation; +} +add_filter( 'gettext_with_context', 'wc_translate_user_roles', 10, 4 ); diff --git a/plugins/woocommerce/includes/wc-webhook-functions.php b/plugins/woocommerce/includes/wc-webhook-functions.php new file mode 100644 index 00000000000..25dc7509107 --- /dev/null +++ b/plugins/woocommerce/includes/wc-webhook-functions.php @@ -0,0 +1,204 @@ + $data['webhook']->get_id(), + 'arg' => $data['arg'], + ); + + $next_scheduled_date = WC()->queue()->get_next( 'woocommerce_deliver_webhook_async', $queue_args, 'woocommerce-webhooks' ); + + // Make webhooks unique - only schedule one webhook every 10 minutes to maintain backward compatibility with WP Cron behaviour seen in WC < 3.5.0. + if ( is_null( $next_scheduled_date ) || $next_scheduled_date->getTimestamp() >= ( 600 + gmdate( 'U' ) ) ) { + WC()->queue()->add( 'woocommerce_deliver_webhook_async', $queue_args, 'woocommerce-webhooks' ); + } + } else { + // Deliver immediately. + $data['webhook']->deliver( $data['arg'] ); + } + } +} +add_action( 'shutdown', 'wc_webhook_execute_queue' ); + +/** + * Process webhook delivery. + * + * @since 3.3.0 + * @param WC_Webhook $webhook Webhook instance. + * @param array $arg Delivery arguments. + */ +function wc_webhook_process_delivery( $webhook, $arg ) { + // We need to queue the webhook so that it can be ran after the request has finished processing. + global $wc_queued_webhooks; + if ( ! isset( $wc_queued_webhooks ) ) { + $wc_queued_webhooks = array(); + } + $wc_queued_webhooks[] = array( + 'webhook' => $webhook, + 'arg' => $arg, + ); +} +add_action( 'woocommerce_webhook_process_delivery', 'wc_webhook_process_delivery', 10, 2 ); + +/** + * Wrapper function to execute the `woocommerce_deliver_webhook_async` cron. + * hook, see WC_Webhook::process(). + * + * @since 2.2.0 + * @param int $webhook_id Webhook ID to deliver. + * @throws Exception If webhook cannot be read/found and $data parameter of WC_Webhook class constructor is set. + * @param mixed $arg Hook argument. + */ +function wc_deliver_webhook_async( $webhook_id, $arg ) { + $webhook = new WC_Webhook( $webhook_id ); + $webhook->deliver( $arg ); +} +add_action( 'woocommerce_deliver_webhook_async', 'wc_deliver_webhook_async', 10, 2 ); + +/** + * Check if the given topic is a valid webhook topic, a topic is valid if: + * + * + starts with `action.woocommerce_` or `action.wc_`. + * + it has a valid resource & event. + * + * @since 2.2.0 + * @param string $topic Webhook topic. + * @return bool + */ +function wc_is_webhook_valid_topic( $topic ) { + $invalid_topics = array( + 'action.woocommerce_login_credentials', + 'action.woocommerce_product_csv_importer_check_import_file_path', + 'action.woocommerce_webhook_should_deliver', + ); + + if ( in_array( $topic, $invalid_topics, true ) ) { + return false; + } + + // Custom topics are prefixed with woocommerce_ or wc_ are valid. + if ( 0 === strpos( $topic, 'action.woocommerce_' ) || 0 === strpos( $topic, 'action.wc_' ) ) { + return true; + } + + $data = explode( '.', $topic ); + + if ( ! isset( $data[0] ) || ! isset( $data[1] ) ) { + return false; + } + + $valid_resources = apply_filters( 'woocommerce_valid_webhook_resources', array( 'coupon', 'customer', 'order', 'product' ) ); + $valid_events = apply_filters( 'woocommerce_valid_webhook_events', array( 'created', 'updated', 'deleted', 'restored' ) ); + + if ( in_array( $data[0], $valid_resources, true ) && in_array( $data[1], $valid_events, true ) ) { + return true; + } + + return false; +} + +/** + * Check if given status is a valid webhook status. + * + * @since 3.5.3 + * @param string $status Status to check. + * @return bool + */ +function wc_is_webhook_valid_status( $status ) { + return in_array( $status, array_keys( wc_get_webhook_statuses() ), true ); +} + +/** + * Get Webhook statuses. + * + * @since 2.3.0 + * @return array + */ +function wc_get_webhook_statuses() { + return apply_filters( + 'woocommerce_webhook_statuses', + array( + 'active' => __( 'Active', 'woocommerce' ), + 'paused' => __( 'Paused', 'woocommerce' ), + 'disabled' => __( 'Disabled', 'woocommerce' ), + ) + ); +} + +/** + * Load webhooks. + * + * @since 3.3.0 + * @throws Exception If webhook cannot be read/found and $data parameter of WC_Webhook class constructor is set. + * @param string $status Optional - status to filter results by. Must be a key in return value of @see wc_get_webhook_statuses(). @since 3.5.0. + * @param null|int $limit Limit number of webhooks loaded. @since 3.6.0. + * @return bool + */ +function wc_load_webhooks( $status = '', $limit = null ) { + $data_store = WC_Data_Store::load( 'webhook' ); + $webhooks = $data_store->get_webhooks_ids( $status ); + $loaded = 0; + + foreach ( $webhooks as $webhook_id ) { + if ( ! is_null( $limit ) && $loaded >= $limit ) { + break; + } + + $webhook = new WC_Webhook( $webhook_id ); + $webhook->enqueue(); + $loaded ++; + } + + return 0 < $loaded; +} + +/** + * Get webhook. + * + * @param int|WC_Webhook $id Webhook ID or object. + * @throws Exception If webhook cannot be read/found and $data parameter of WC_Webhook class constructor is set. + * @return WC_Webhook|null + */ +function wc_get_webhook( $id ) { + $webhook = new WC_Webhook( $id ); + + return 0 !== $webhook->get_id() ? $webhook : null; +} + +/** + * Get webhoook REST API versions. + * + * @since 3.5.1 + * @return array + */ +function wc_get_webhook_rest_api_versions() { + return array( + 'wp_api_v1', + 'wp_api_v2', + 'wp_api_v3', + ); +} diff --git a/includes/wc-widget-functions.php b/plugins/woocommerce/includes/wc-widget-functions.php similarity index 100% rename from includes/wc-widget-functions.php rename to plugins/woocommerce/includes/wc-widget-functions.php diff --git a/includes/wccom-site/class-wc-wccom-site-installer-requirements-check.php b/plugins/woocommerce/includes/wccom-site/class-wc-wccom-site-installer-requirements-check.php similarity index 100% rename from includes/wccom-site/class-wc-wccom-site-installer-requirements-check.php rename to plugins/woocommerce/includes/wccom-site/class-wc-wccom-site-installer-requirements-check.php diff --git a/includes/wccom-site/class-wc-wccom-site-installer.php b/plugins/woocommerce/includes/wccom-site/class-wc-wccom-site-installer.php similarity index 100% rename from includes/wccom-site/class-wc-wccom-site-installer.php rename to plugins/woocommerce/includes/wccom-site/class-wc-wccom-site-installer.php diff --git a/includes/wccom-site/class-wc-wccom-site.php b/plugins/woocommerce/includes/wccom-site/class-wc-wccom-site.php similarity index 100% rename from includes/wccom-site/class-wc-wccom-site.php rename to plugins/woocommerce/includes/wccom-site/class-wc-wccom-site.php diff --git a/includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php b/plugins/woocommerce/includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php similarity index 100% rename from includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php rename to plugins/woocommerce/includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php diff --git a/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller.php b/plugins/woocommerce/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller.php similarity index 100% rename from includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller.php rename to plugins/woocommerce/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller.php diff --git a/includes/widgets/class-wc-widget-cart.php b/plugins/woocommerce/includes/widgets/class-wc-widget-cart.php similarity index 100% rename from includes/widgets/class-wc-widget-cart.php rename to plugins/woocommerce/includes/widgets/class-wc-widget-cart.php diff --git a/includes/widgets/class-wc-widget-layered-nav-filters.php b/plugins/woocommerce/includes/widgets/class-wc-widget-layered-nav-filters.php similarity index 100% rename from includes/widgets/class-wc-widget-layered-nav-filters.php rename to plugins/woocommerce/includes/widgets/class-wc-widget-layered-nav-filters.php diff --git a/plugins/woocommerce/includes/widgets/class-wc-widget-layered-nav.php b/plugins/woocommerce/includes/widgets/class-wc-widget-layered-nav.php new file mode 100644 index 00000000000..c4a4bb141a6 --- /dev/null +++ b/plugins/woocommerce/includes/widgets/class-wc-widget-layered-nav.php @@ -0,0 +1,469 @@ +widget_cssclass = 'woocommerce widget_layered_nav woocommerce-widget-layered-nav'; + $this->widget_description = __( 'Display a list of attributes to filter products in your store.', 'woocommerce' ); + $this->widget_id = 'woocommerce_layered_nav'; + $this->widget_name = __( 'Filter Products by Attribute', 'woocommerce' ); + parent::__construct(); + } + + /** + * Updates a particular instance of a widget. + * + * @see WP_Widget->update + * + * @param array $new_instance New Instance. + * @param array $old_instance Old Instance. + * + * @return array + */ + public function update( $new_instance, $old_instance ) { + $this->init_settings(); + return parent::update( $new_instance, $old_instance ); + } + + /** + * Outputs the settings update form. + * + * @see WP_Widget->form + * + * @param array $instance Instance. + */ + public function form( $instance ) { + $this->init_settings(); + parent::form( $instance ); + } + + /** + * Init settings after post types are registered. + */ + public function init_settings() { + $attribute_array = array(); + $std_attribute = ''; + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + if ( ! empty( $attribute_taxonomies ) ) { + foreach ( $attribute_taxonomies as $tax ) { + if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) { + $attribute_array[ $tax->attribute_name ] = $tax->attribute_name; + } + } + $std_attribute = current( $attribute_array ); + } + + $this->settings = array( + 'title' => array( + 'type' => 'text', + 'std' => __( 'Filter by', 'woocommerce' ), + 'label' => __( 'Title', 'woocommerce' ), + ), + 'attribute' => array( + 'type' => 'select', + 'std' => $std_attribute, + 'label' => __( 'Attribute', 'woocommerce' ), + 'options' => $attribute_array, + ), + 'display_type' => array( + 'type' => 'select', + 'std' => 'list', + 'label' => __( 'Display type', 'woocommerce' ), + 'options' => array( + 'list' => __( 'List', 'woocommerce' ), + 'dropdown' => __( 'Dropdown', 'woocommerce' ), + ), + ), + 'query_type' => array( + 'type' => 'select', + 'std' => 'and', + 'label' => __( 'Query type', 'woocommerce' ), + 'options' => array( + 'and' => __( 'AND', 'woocommerce' ), + 'or' => __( 'OR', 'woocommerce' ), + ), + ), + ); + } + + /** + * Get this widgets taxonomy. + * + * @param array $instance Array of instance options. + * @return string + */ + protected function get_instance_taxonomy( $instance ) { + if ( isset( $instance['attribute'] ) ) { + return wc_attribute_taxonomy_name( $instance['attribute'] ); + } + + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + if ( ! empty( $attribute_taxonomies ) ) { + foreach ( $attribute_taxonomies as $tax ) { + if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) { + return wc_attribute_taxonomy_name( $tax->attribute_name ); + } + } + } + + return ''; + } + + /** + * Get this widgets query type. + * + * @param array $instance Array of instance options. + * @return string + */ + protected function get_instance_query_type( $instance ) { + return isset( $instance['query_type'] ) ? $instance['query_type'] : 'and'; + } + + /** + * Get this widgets display type. + * + * @param array $instance Array of instance options. + * @return string + */ + protected function get_instance_display_type( $instance ) { + return isset( $instance['display_type'] ) ? $instance['display_type'] : 'list'; + } + + /** + * Output widget. + * + * @see WP_Widget + * + * @param array $args Arguments. + * @param array $instance Instance. + */ + public function widget( $args, $instance ) { + if ( ! is_shop() && ! is_product_taxonomy() ) { + return; + } + + $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes(); + $taxonomy = $this->get_instance_taxonomy( $instance ); + $query_type = $this->get_instance_query_type( $instance ); + $display_type = $this->get_instance_display_type( $instance ); + + if ( ! taxonomy_exists( $taxonomy ) ) { + return; + } + + $terms = get_terms( $taxonomy, array( 'hide_empty' => '1' ) ); + + if ( 0 === count( $terms ) ) { + return; + } + + ob_start(); + + $this->widget_start( $args, $instance ); + + if ( 'dropdown' === $display_type ) { + wp_enqueue_script( 'selectWoo' ); + wp_enqueue_style( 'select2' ); + $found = $this->layered_nav_dropdown( $terms, $taxonomy, $query_type ); + } else { + $found = $this->layered_nav_list( $terms, $taxonomy, $query_type ); + } + + $this->widget_end( $args ); + + // Force found when option is selected - do not force found on taxonomy attributes. + if ( ! is_tax() && is_array( $_chosen_attributes ) && array_key_exists( $taxonomy, $_chosen_attributes ) ) { + $found = true; + } + + if ( ! $found ) { + ob_end_clean(); + } else { + echo ob_get_clean(); // @codingStandardsIgnoreLine + } + } + + /** + * Return the currently viewed taxonomy name. + * + * @return string + */ + protected function get_current_taxonomy() { + return is_tax() ? get_queried_object()->taxonomy : ''; + } + + /** + * Return the currently viewed term ID. + * + * @return int + */ + protected function get_current_term_id() { + return absint( is_tax() ? get_queried_object()->term_id : 0 ); + } + + /** + * Return the currently viewed term slug. + * + * @return int + */ + protected function get_current_term_slug() { + return absint( is_tax() ? get_queried_object()->slug : 0 ); + } + + /** + * Show dropdown layered nav. + * + * @param array $terms Terms. + * @param string $taxonomy Taxonomy. + * @param string $query_type Query Type. + * @return bool Will nav display? + */ + protected function layered_nav_dropdown( $terms, $taxonomy, $query_type ) { + global $wp; + $found = false; + + if ( $taxonomy !== $this->get_current_taxonomy() ) { + $term_counts = $this->get_filtered_term_product_counts( wp_list_pluck( $terms, 'term_id' ), $taxonomy, $query_type ); + $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes(); + $taxonomy_filter_name = wc_attribute_taxonomy_slug( $taxonomy ); + $taxonomy_label = wc_attribute_label( $taxonomy ); + + /* translators: %s: taxonomy name */ + $any_label = apply_filters( 'woocommerce_layered_nav_any_label', sprintf( __( 'Any %s', 'woocommerce' ), $taxonomy_label ), $taxonomy_label, $taxonomy ); + $multiple = 'or' === $query_type; + $current_values = isset( $_chosen_attributes[ $taxonomy ]['terms'] ) ? $_chosen_attributes[ $taxonomy ]['terms'] : array(); + + if ( '' === get_option( 'permalink_structure' ) ) { + $form_action = remove_query_arg( array( 'page', 'paged' ), add_query_arg( $wp->query_string, '', home_url( $wp->request ) ) ); + } else { + $form_action = preg_replace( '%\/page/[0-9]+%', '', home_url( user_trailingslashit( $wp->request ) ) ); + } + + echo '
    '; + echo ''; + + if ( $multiple ) { + echo ''; + } + + if ( 'or' === $query_type ) { + echo ''; + } + + echo ''; + echo wc_query_string_form_fields( null, array( 'filter_' . $taxonomy_filter_name, 'query_type_' . $taxonomy_filter_name ), '', true ); // @codingStandardsIgnoreLine + echo '
    '; + + wc_enqueue_js( + " + // Update value on change. + jQuery( '.dropdown_layered_nav_" . esc_js( $taxonomy_filter_name ) . "' ).on( 'change', function() { + var slug = jQuery( this ).val(); + jQuery( ':input[name=\"filter_" . esc_js( $taxonomy_filter_name ) . "\"]' ).val( slug ); + + // Submit form on change if standard dropdown. + if ( ! jQuery( this ).attr( 'multiple' ) ) { + jQuery( this ).closest( 'form' ).trigger( 'submit' ); + } + }); + + // Use Select2 enhancement if possible + if ( jQuery().selectWoo ) { + var wc_layered_nav_select = function() { + jQuery( '.dropdown_layered_nav_" . esc_js( $taxonomy_filter_name ) . "' ).selectWoo( { + placeholder: decodeURIComponent('" . rawurlencode( (string) wp_specialchars_decode( $any_label ) ) . "'), + minimumResultsForSearch: 5, + width: '100%', + allowClear: " . ( $multiple ? 'false' : 'true' ) . ", + language: { + noResults: function() { + return '" . esc_js( _x( 'No matches found', 'enhanced select', 'woocommerce' ) ) . "'; + } + } + } ); + }; + wc_layered_nav_select(); + } + " + ); + } + + return $found; + } + + /** + * Count products within certain terms, taking the main WP query into consideration. + * + * This query allows counts to be generated based on the viewed products, not all products. + * + * @param array $term_ids Term IDs. + * @param string $taxonomy Taxonomy. + * @param string $query_type Query Type. + * @return array + */ + protected function get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type ) { + return wc_get_container()->get( Filterer::class )->get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type ); + } + + /** + * Wrapper for WC_Query::get_main_tax_query() to ease unit testing. + * + * @since 4.4.0 + * @return array + */ + protected function get_main_tax_query() { + return WC_Query::get_main_tax_query(); + } + + /** + * Wrapper for WC_Query::get_main_search_query_sql() to ease unit testing. + * + * @since 4.4.0 + * @return string + */ + protected function get_main_search_query_sql() { + return WC_Query::get_main_search_query_sql(); + } + + /** + * Wrapper for WC_Query::get_main_search_queryget_main_meta_query to ease unit testing. + * + * @since 4.4.0 + * @return array + */ + protected function get_main_meta_query() { + return WC_Query::get_main_meta_query(); + } + + /** + * Show list based layered nav. + * + * @param array $terms Terms. + * @param string $taxonomy Taxonomy. + * @param string $query_type Query Type. + * @return bool Will nav display? + */ + protected function layered_nav_list( $terms, $taxonomy, $query_type ) { + // List display. + echo '
      '; + + $term_counts = $this->get_filtered_term_product_counts( wp_list_pluck( $terms, 'term_id' ), $taxonomy, $query_type ); + $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes(); + $found = false; + $base_link = $this->get_current_page_url(); + + foreach ( $terms as $term ) { + $current_values = isset( $_chosen_attributes[ $taxonomy ]['terms'] ) ? $_chosen_attributes[ $taxonomy ]['terms'] : array(); + $option_is_set = in_array( $term->slug, $current_values, true ); + $count = isset( $term_counts[ $term->term_id ] ) ? $term_counts[ $term->term_id ] : 0; + + // Skip the term for the current archive. + if ( $this->get_current_term_id() === $term->term_id ) { + continue; + } + + // Only show options with count > 0. + if ( 0 < $count ) { + $found = true; + } elseif ( 0 === $count && ! $option_is_set ) { + continue; + } + + $filter_name = 'filter_' . wc_attribute_taxonomy_slug( $taxonomy ); + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $current_filter = isset( $_GET[ $filter_name ] ) ? explode( ',', wc_clean( wp_unslash( $_GET[ $filter_name ] ) ) ) : array(); + $current_filter = array_map( 'sanitize_title', $current_filter ); + + if ( ! in_array( $term->slug, $current_filter, true ) ) { + $current_filter[] = $term->slug; + } + + $link = remove_query_arg( $filter_name, $base_link ); + + // Add current filters to URL. + foreach ( $current_filter as $key => $value ) { + // Exclude query arg for current term archive term. + if ( $value === $this->get_current_term_slug() ) { + unset( $current_filter[ $key ] ); + } + + // Exclude self so filter can be unset on click. + if ( $option_is_set && $value === $term->slug ) { + unset( $current_filter[ $key ] ); + } + } + + if ( ! empty( $current_filter ) ) { + asort( $current_filter ); + $link = add_query_arg( $filter_name, implode( ',', $current_filter ), $link ); + + // Add Query type Arg to URL. + if ( 'or' === $query_type && ! ( 1 === count( $current_filter ) && $option_is_set ) ) { + $link = add_query_arg( 'query_type_' . wc_attribute_taxonomy_slug( $taxonomy ), 'or', $link ); + } + $link = str_replace( '%2C', ',', $link ); + } + + if ( $count > 0 || $option_is_set ) { + $link = apply_filters( 'woocommerce_layered_nav_link', $link, $term, $taxonomy ); + $term_html = '' . esc_html( $term->name ) . ''; + } else { + $link = false; + $term_html = '' . esc_html( $term->name ) . ''; + } + + $term_html .= ' ' . apply_filters( 'woocommerce_layered_nav_count', '(' . absint( $count ) . ')', $count, $term ); + + echo '
    • '; + // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.EscapeOutput.OutputNotEscaped + echo apply_filters( 'woocommerce_layered_nav_term_html', $term_html, $term, $link, $count ); + echo '
    • '; + } + + echo '
    '; + + return $found; + } +} diff --git a/includes/widgets/class-wc-widget-price-filter.php b/plugins/woocommerce/includes/widgets/class-wc-widget-price-filter.php similarity index 100% rename from includes/widgets/class-wc-widget-price-filter.php rename to plugins/woocommerce/includes/widgets/class-wc-widget-price-filter.php diff --git a/plugins/woocommerce/includes/widgets/class-wc-widget-product-categories.php b/plugins/woocommerce/includes/widgets/class-wc-widget-product-categories.php new file mode 100644 index 00000000000..559cb13e3a3 --- /dev/null +++ b/plugins/woocommerce/includes/widgets/class-wc-widget-product-categories.php @@ -0,0 +1,301 @@ +widget_cssclass = 'woocommerce widget_product_categories'; + $this->widget_description = __( 'A list or dropdown of product categories.', 'woocommerce' ); + $this->widget_id = 'woocommerce_product_categories'; + $this->widget_name = __( 'Product Categories', 'woocommerce' ); + $this->settings = array( + 'title' => array( + 'type' => 'text', + 'std' => __( 'Product categories', 'woocommerce' ), + 'label' => __( 'Title', 'woocommerce' ), + ), + 'orderby' => array( + 'type' => 'select', + 'std' => 'name', + 'label' => __( 'Order by', 'woocommerce' ), + 'options' => array( + 'order' => __( 'Category order', 'woocommerce' ), + 'name' => __( 'Name', 'woocommerce' ), + ), + ), + 'dropdown' => array( + 'type' => 'checkbox', + 'std' => 0, + 'label' => __( 'Show as dropdown', 'woocommerce' ), + ), + 'count' => array( + 'type' => 'checkbox', + 'std' => 0, + 'label' => __( 'Show product counts', 'woocommerce' ), + ), + 'hierarchical' => array( + 'type' => 'checkbox', + 'std' => 1, + 'label' => __( 'Show hierarchy', 'woocommerce' ), + ), + 'show_children_only' => array( + 'type' => 'checkbox', + 'std' => 0, + 'label' => __( 'Only show children of the current category', 'woocommerce' ), + ), + 'hide_empty' => array( + 'type' => 'checkbox', + 'std' => 0, + 'label' => __( 'Hide empty categories', 'woocommerce' ), + ), + 'max_depth' => array( + 'type' => 'text', + 'std' => '', + 'label' => __( 'Maximum depth', 'woocommerce' ), + ), + ); + + parent::__construct(); + } + + /** + * Output widget. + * + * @see WP_Widget + * @param array $args Widget arguments. + * @param array $instance Widget instance. + */ + public function widget( $args, $instance ) { + global $wp_query, $post; + + $count = isset( $instance['count'] ) ? $instance['count'] : $this->settings['count']['std']; + $hierarchical = isset( $instance['hierarchical'] ) ? $instance['hierarchical'] : $this->settings['hierarchical']['std']; + $show_children_only = isset( $instance['show_children_only'] ) ? $instance['show_children_only'] : $this->settings['show_children_only']['std']; + $dropdown = isset( $instance['dropdown'] ) ? $instance['dropdown'] : $this->settings['dropdown']['std']; + $orderby = isset( $instance['orderby'] ) ? $instance['orderby'] : $this->settings['orderby']['std']; + $hide_empty = isset( $instance['hide_empty'] ) ? $instance['hide_empty'] : $this->settings['hide_empty']['std']; + $dropdown_args = array( + 'hide_empty' => $hide_empty, + ); + $list_args = array( + 'show_count' => $count, + 'hierarchical' => $hierarchical, + 'taxonomy' => 'product_cat', + 'hide_empty' => $hide_empty, + ); + $max_depth = absint( isset( $instance['max_depth'] ) ? $instance['max_depth'] : $this->settings['max_depth']['std'] ); + + $list_args['menu_order'] = false; + $dropdown_args['depth'] = $max_depth; + $list_args['depth'] = $max_depth; + + if ( 'order' === $orderby ) { + $list_args['orderby'] = 'meta_value_num'; + $dropdown_args['orderby'] = 'meta_value_num'; + $list_args['meta_key'] = 'order'; + $dropdown_args['meta_key'] = 'order'; + } + + $this->current_cat = false; + $this->cat_ancestors = array(); + + if ( is_tax( 'product_cat' ) ) { + $this->current_cat = $wp_query->queried_object; + $this->cat_ancestors = get_ancestors( $this->current_cat->term_id, 'product_cat' ); + + } elseif ( is_singular( 'product' ) ) { + $terms = wc_get_product_terms( + $post->ID, + 'product_cat', + apply_filters( + 'woocommerce_product_categories_widget_product_terms_args', + array( + 'orderby' => 'parent', + 'order' => 'DESC', + ) + ) + ); + + if ( $terms ) { + $main_term = apply_filters( 'woocommerce_product_categories_widget_main_term', $terms[0], $terms ); + $this->current_cat = $main_term; + $this->cat_ancestors = get_ancestors( $main_term->term_id, 'product_cat' ); + } + } + + // Show Siblings and Children Only. + if ( $show_children_only && $this->current_cat ) { + if ( $hierarchical ) { + $include = array_merge( + $this->cat_ancestors, + array( $this->current_cat->term_id ), + get_terms( + 'product_cat', + array( + 'fields' => 'ids', + 'parent' => 0, + 'hierarchical' => true, + 'hide_empty' => false, + ) + ), + get_terms( + 'product_cat', + array( + 'fields' => 'ids', + 'parent' => $this->current_cat->term_id, + 'hierarchical' => true, + 'hide_empty' => false, + ) + ) + ); + // Gather siblings of ancestors. + if ( $this->cat_ancestors ) { + foreach ( $this->cat_ancestors as $ancestor ) { + $include = array_merge( + $include, + get_terms( + 'product_cat', + array( + 'fields' => 'ids', + 'parent' => $ancestor, + 'hierarchical' => false, + 'hide_empty' => false, + ) + ) + ); + } + } + } else { + // Direct children. + $include = get_terms( + 'product_cat', + array( + 'fields' => 'ids', + 'parent' => $this->current_cat->term_id, + 'hierarchical' => true, + 'hide_empty' => false, + ) + ); + } + + $list_args['include'] = implode( ',', $include ); + $dropdown_args['include'] = $list_args['include']; + + if ( empty( $include ) ) { + return; + } + } elseif ( $show_children_only ) { + $dropdown_args['depth'] = 1; + $dropdown_args['child_of'] = 0; + $dropdown_args['hierarchical'] = 1; + $list_args['depth'] = 1; + $list_args['child_of'] = 0; + $list_args['hierarchical'] = 1; + } + + $this->widget_start( $args, $instance ); + + if ( $dropdown ) { + wc_product_dropdown_categories( + apply_filters( + 'woocommerce_product_categories_widget_dropdown_args', + wp_parse_args( + $dropdown_args, + array( + 'show_count' => $count, + 'hierarchical' => $hierarchical, + 'show_uncategorized' => 0, + 'selected' => $this->current_cat ? $this->current_cat->slug : '', + ) + ) + ) + ); + + wp_enqueue_script( 'selectWoo' ); + wp_enqueue_style( 'select2' ); + + wc_enqueue_js( + " + jQuery( '.dropdown_product_cat' ).on( 'change', function() { + if ( jQuery(this).val() != '' ) { + var this_page = ''; + var home_url = '" . esc_js( home_url( '/' ) ) . "'; + if ( home_url.indexOf( '?' ) > 0 ) { + this_page = home_url + '&product_cat=' + jQuery(this).val(); + } else { + this_page = home_url + '?product_cat=' + jQuery(this).val(); + } + location.href = this_page; + } else { + location.href = '" . esc_js( wc_get_page_permalink( 'shop' ) ) . "'; + } + }); + + if ( jQuery().selectWoo ) { + var wc_product_cat_select = function() { + jQuery( '.dropdown_product_cat' ).selectWoo( { + placeholder: '" . esc_js( __( 'Select a category', 'woocommerce' ) ) . "', + minimumResultsForSearch: 5, + width: '100%', + allowClear: true, + language: { + noResults: function() { + return '" . esc_js( _x( 'No matches found', 'enhanced select', 'woocommerce' ) ) . "'; + } + } + } ); + }; + wc_product_cat_select(); + } + " + ); + } else { + include_once WC()->plugin_path() . '/includes/walkers/class-wc-product-cat-list-walker.php'; + + $list_args['walker'] = new WC_Product_Cat_List_Walker(); + $list_args['title_li'] = ''; + $list_args['pad_counts'] = 1; + $list_args['show_option_none'] = __( 'No product categories exist.', 'woocommerce' ); + $list_args['current_category'] = ( $this->current_cat ) ? $this->current_cat->term_id : ''; + $list_args['current_category_ancestors'] = $this->cat_ancestors; + $list_args['max_depth'] = $max_depth; + + echo '
      '; + + wp_list_categories( apply_filters( 'woocommerce_product_categories_widget_args', $list_args ) ); + + echo '
    '; + } + + $this->widget_end( $args ); + } +} diff --git a/includes/widgets/class-wc-widget-product-search.php b/plugins/woocommerce/includes/widgets/class-wc-widget-product-search.php similarity index 100% rename from includes/widgets/class-wc-widget-product-search.php rename to plugins/woocommerce/includes/widgets/class-wc-widget-product-search.php diff --git a/includes/widgets/class-wc-widget-product-tag-cloud.php b/plugins/woocommerce/includes/widgets/class-wc-widget-product-tag-cloud.php similarity index 100% rename from includes/widgets/class-wc-widget-product-tag-cloud.php rename to plugins/woocommerce/includes/widgets/class-wc-widget-product-tag-cloud.php diff --git a/plugins/woocommerce/includes/widgets/class-wc-widget-products.php b/plugins/woocommerce/includes/widgets/class-wc-widget-products.php new file mode 100644 index 00000000000..a40c7028408 --- /dev/null +++ b/plugins/woocommerce/includes/widgets/class-wc-widget-products.php @@ -0,0 +1,216 @@ +widget_cssclass = 'woocommerce widget_products'; + $this->widget_description = __( "A list of your store's products.", 'woocommerce' ); + $this->widget_id = 'woocommerce_products'; + $this->widget_name = __( 'Products list', 'woocommerce' ); + $this->settings = array( + 'title' => array( + 'type' => 'text', + 'std' => __( 'Products', 'woocommerce' ), + 'label' => __( 'Title', 'woocommerce' ), + ), + 'number' => array( + 'type' => 'number', + 'step' => 1, + 'min' => 1, + 'max' => '', + 'std' => 5, + 'label' => __( 'Number of products to show', 'woocommerce' ), + ), + 'show' => array( + 'type' => 'select', + 'std' => '', + 'label' => __( 'Show', 'woocommerce' ), + 'options' => array( + '' => __( 'All products', 'woocommerce' ), + 'featured' => __( 'Featured products', 'woocommerce' ), + 'onsale' => __( 'On-sale products', 'woocommerce' ), + ), + ), + 'orderby' => array( + 'type' => 'select', + 'std' => 'date', + 'label' => __( 'Order by', 'woocommerce' ), + 'options' => array( + 'date' => __( 'Date', 'woocommerce' ), + 'price' => __( 'Price', 'woocommerce' ), + 'rand' => __( 'Random', 'woocommerce' ), + 'sales' => __( 'Sales', 'woocommerce' ), + ), + ), + 'order' => array( + 'type' => 'select', + 'std' => 'desc', + 'label' => _x( 'Order', 'Sorting order', 'woocommerce' ), + 'options' => array( + 'asc' => __( 'ASC', 'woocommerce' ), + 'desc' => __( 'DESC', 'woocommerce' ), + ), + ), + 'hide_free' => array( + 'type' => 'checkbox', + 'std' => 0, + 'label' => __( 'Hide free products', 'woocommerce' ), + ), + 'show_hidden' => array( + 'type' => 'checkbox', + 'std' => 0, + 'label' => __( 'Show hidden products', 'woocommerce' ), + ), + ); + + parent::__construct(); + } + + /** + * Query the products and return them. + * + * @param array $args Arguments. + * @param array $instance Widget instance. + * + * @return WP_Query + */ + public function get_products( $args, $instance ) { + $number = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : $this->settings['number']['std']; + $show = ! empty( $instance['show'] ) ? sanitize_title( $instance['show'] ) : $this->settings['show']['std']; + $orderby = ! empty( $instance['orderby'] ) ? sanitize_title( $instance['orderby'] ) : $this->settings['orderby']['std']; + $order = ! empty( $instance['order'] ) ? sanitize_title( $instance['order'] ) : $this->settings['order']['std']; + $product_visibility_term_ids = wc_get_product_visibility_term_ids(); + + $query_args = array( + 'posts_per_page' => $number, + 'post_status' => 'publish', + 'post_type' => 'product', + 'no_found_rows' => 1, + 'order' => $order, + 'meta_query' => array(), + 'tax_query' => array( + 'relation' => 'AND', + ), + ); // WPCS: slow query ok. + + if ( empty( $instance['show_hidden'] ) ) { + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => is_search() ? $product_visibility_term_ids['exclude-from-search'] : $product_visibility_term_ids['exclude-from-catalog'], + 'operator' => 'NOT IN', + ); + $query_args['post_parent'] = 0; + } + + if ( ! empty( $instance['hide_free'] ) ) { + $query_args['meta_query'][] = array( + 'key' => '_price', + 'value' => 0, + 'compare' => '>', + 'type' => 'DECIMAL', + ); + } + + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { + $query_args['tax_query'][] = array( + array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => $product_visibility_term_ids['outofstock'], + 'operator' => 'NOT IN', + ), + ); // WPCS: slow query ok. + } + + switch ( $show ) { + case 'featured': + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => $product_visibility_term_ids['featured'], + ); + break; + case 'onsale': + $product_ids_on_sale = wc_get_product_ids_on_sale(); + $product_ids_on_sale[] = 0; + $query_args['post__in'] = $product_ids_on_sale; + break; + } + + switch ( $orderby ) { + case 'price': + $query_args['meta_key'] = '_price'; // WPCS: slow query ok. + $query_args['orderby'] = 'meta_value_num'; + break; + case 'rand': + $query_args['orderby'] = 'rand'; + break; + case 'sales': + $query_args['meta_key'] = 'total_sales'; // WPCS: slow query ok. + $query_args['orderby'] = 'meta_value_num'; + break; + default: + $query_args['orderby'] = 'date'; + } + + return new WP_Query( apply_filters( 'woocommerce_products_widget_query_args', $query_args ) ); + } + + /** + * Output widget. + * + * @param array $args Arguments. + * @param array $instance Widget instance. + * + * @see WP_Widget + */ + public function widget( $args, $instance ) { + if ( $this->get_cached_widget( $args ) ) { + return; + } + + ob_start(); + + wc_set_loop_prop( 'name', 'widget' ); + + $products = $this->get_products( $args, $instance ); + if ( $products && $products->have_posts() ) { + $this->widget_start( $args, $instance ); + + echo wp_kses_post( apply_filters( 'woocommerce_before_widget_product_list', '
      ' ) ); + + $template_args = array( + 'widget_id' => isset( $args['widget_id'] ) ? $args['widget_id'] : $this->widget_id, + 'show_rating' => true, + ); + + while ( $products->have_posts() ) { + $products->the_post(); + wc_get_template( 'content-widget-product.php', $template_args ); + } + + echo wp_kses_post( apply_filters( 'woocommerce_after_widget_product_list', '
    ' ) ); + + $this->widget_end( $args ); + } + + wp_reset_postdata(); + + echo $this->cache_widget( $args, ob_get_clean() ); // WPCS: XSS ok. + } +} diff --git a/includes/widgets/class-wc-widget-rating-filter.php b/plugins/woocommerce/includes/widgets/class-wc-widget-rating-filter.php similarity index 100% rename from includes/widgets/class-wc-widget-rating-filter.php rename to plugins/woocommerce/includes/widgets/class-wc-widget-rating-filter.php diff --git a/includes/widgets/class-wc-widget-recent-reviews.php b/plugins/woocommerce/includes/widgets/class-wc-widget-recent-reviews.php similarity index 100% rename from includes/widgets/class-wc-widget-recent-reviews.php rename to plugins/woocommerce/includes/widgets/class-wc-widget-recent-reviews.php diff --git a/plugins/woocommerce/includes/widgets/class-wc-widget-recently-viewed.php b/plugins/woocommerce/includes/widgets/class-wc-widget-recently-viewed.php new file mode 100644 index 00000000000..e2b50ce581a --- /dev/null +++ b/plugins/woocommerce/includes/widgets/class-wc-widget-recently-viewed.php @@ -0,0 +1,110 @@ +widget_cssclass = 'woocommerce widget_recently_viewed_products'; + $this->widget_description = __( "Display a list of a customer's recently viewed products.", 'woocommerce' ); + $this->widget_id = 'woocommerce_recently_viewed_products'; + $this->widget_name = __( 'Recently Viewed Products list', 'woocommerce' ); + $this->settings = array( + 'title' => array( + 'type' => 'text', + 'std' => __( 'Recently Viewed Products', 'woocommerce' ), + 'label' => __( 'Title', 'woocommerce' ), + ), + 'number' => array( + 'type' => 'number', + 'step' => 1, + 'min' => 1, + 'max' => 15, + 'std' => 10, + 'label' => __( 'Number of products to show', 'woocommerce' ), + ), + ); + + parent::__construct(); + } + + /** + * Output widget. + * + * @see WP_Widget + * @param array $args Arguments. + * @param array $instance Widget instance. + */ + public function widget( $args, $instance ) { + $viewed_products = ! empty( $_COOKIE['woocommerce_recently_viewed'] ) ? (array) explode( '|', wp_unslash( $_COOKIE['woocommerce_recently_viewed'] ) ) : array(); // @codingStandardsIgnoreLine + $viewed_products = array_reverse( array_filter( array_map( 'absint', $viewed_products ) ) ); + + if ( empty( $viewed_products ) ) { + return; + } + + ob_start(); + + $number = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : $this->settings['number']['std']; + + $query_args = array( + 'posts_per_page' => $number, + 'no_found_rows' => 1, + 'post_status' => 'publish', + 'post_type' => 'product', + 'post__in' => $viewed_products, + 'orderby' => 'post__in', + ); + + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { + $query_args['tax_query'] = array( + array( + 'taxonomy' => 'product_visibility', + 'field' => 'name', + 'terms' => 'outofstock', + 'operator' => 'NOT IN', + ), + ); // WPCS: slow query ok. + } + + $r = new WP_Query( apply_filters( 'woocommerce_recently_viewed_products_widget_query_args', $query_args ) ); + + if ( $r->have_posts() ) { + + $this->widget_start( $args, $instance ); + + echo wp_kses_post( apply_filters( 'woocommerce_before_widget_product_list', '
      ' ) ); + + $template_args = array( + 'widget_id' => isset( $args['widget_id'] ) ? $args['widget_id'] : $this->widget_id, + ); + + while ( $r->have_posts() ) { + $r->the_post(); + wc_get_template( 'content-widget-product.php', $template_args ); + } + + echo wp_kses_post( apply_filters( 'woocommerce_after_widget_product_list', '
    ' ) ); + + $this->widget_end( $args ); + } + + wp_reset_postdata(); + + $content = ob_get_clean(); + + echo $content; // WPCS: XSS ok. + } +} diff --git a/plugins/woocommerce/includes/widgets/class-wc-widget-top-rated-products.php b/plugins/woocommerce/includes/widgets/class-wc-widget-top-rated-products.php new file mode 100644 index 00000000000..e6d95bf17db --- /dev/null +++ b/plugins/woocommerce/includes/widgets/class-wc-widget-top-rated-products.php @@ -0,0 +1,107 @@ +widget_cssclass = 'woocommerce widget_top_rated_products'; + $this->widget_description = __( "A list of your store's top-rated products.", 'woocommerce' ); + $this->widget_id = 'woocommerce_top_rated_products'; + $this->widget_name = __( 'Products by Rating list', 'woocommerce' ); + $this->settings = array( + 'title' => array( + 'type' => 'text', + 'std' => __( 'Top rated products', 'woocommerce' ), + 'label' => __( 'Title', 'woocommerce' ), + ), + 'number' => array( + 'type' => 'number', + 'step' => 1, + 'min' => 1, + 'max' => '', + 'std' => 5, + 'label' => __( 'Number of products to show', 'woocommerce' ), + ), + ); + + parent::__construct(); + } + + /** + * Output widget. + * + * @see WP_Widget + * @param array $args Arguments. + * @param array $instance Widget instance. + */ + public function widget( $args, $instance ) { + + if ( $this->get_cached_widget( $args ) ) { + return; + } + + ob_start(); + + $number = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : $this->settings['number']['std']; + + $query_args = apply_filters( + 'woocommerce_top_rated_products_widget_args', + array( + 'posts_per_page' => $number, + 'no_found_rows' => 1, + 'post_status' => 'publish', + 'post_type' => 'product', + 'meta_key' => '_wc_average_rating', + 'orderby' => 'meta_value_num', + 'order' => 'DESC', + 'meta_query' => WC()->query->get_meta_query(), + 'tax_query' => WC()->query->get_tax_query(), + ) + ); // WPCS: slow query ok. + + $r = new WP_Query( $query_args ); + + if ( $r->have_posts() ) { + + $this->widget_start( $args, $instance ); + + echo wp_kses_post( apply_filters( 'woocommerce_before_widget_product_list', '
      ' ) ); + + $template_args = array( + 'widget_id' => isset( $args['widget_id'] ) ? $args['widget_id'] : $this->widget_id, + 'show_rating' => true, + ); + + while ( $r->have_posts() ) { + $r->the_post(); + wc_get_template( 'content-widget-product.php', $template_args ); + } + + echo wp_kses_post( apply_filters( 'woocommerce_after_widget_product_list', '
    ' ) ); + + $this->widget_end( $args ); + } + + wp_reset_postdata(); + + $content = ob_get_clean(); + + echo $content; // WPCS: XSS ok. + + $this->cache_widget( $args, $content ); + } +} diff --git a/plugins/woocommerce/legacy/.browserslistrc b/plugins/woocommerce/legacy/.browserslistrc new file mode 100644 index 00000000000..5d191931ac1 --- /dev/null +++ b/plugins/woocommerce/legacy/.browserslistrc @@ -0,0 +1,3 @@ +> 0.1% +ie 8 +ie 9 diff --git a/plugins/woocommerce/legacy/.eslintignore b/plugins/woocommerce/legacy/.eslintignore new file mode 100644 index 00000000000..a711d73d391 --- /dev/null +++ b/plugins/woocommerce/legacy/.eslintignore @@ -0,0 +1,20 @@ +*.min.js + +/js/accounting/** +/js/flexslider/** +/js/jquery-blockui/** +/js/jquery-cookie/** +/js/jquery-flot/** +/js/jquery-payment/** +/js/jquery-qrcode/** +/js/jquery-serializejson/** +/js/jquery-tiptip/** +/js/jquery-ui-touch-punch/** +/js/js-cookie/** +/js/photoswipe/** +/js/prettyPhoto/** +/js/round/** +/js/select2/** +/js/selectWoo/** +/js/stupidtable/** +/js/zoom/** diff --git a/plugins/woocommerce/legacy/.eslintrc.js b/plugins/woocommerce/legacy/.eslintrc.js new file mode 100644 index 00000000000..7c90a4dbbb8 --- /dev/null +++ b/plugins/woocommerce/legacy/.eslintrc.js @@ -0,0 +1,31 @@ +/** @format */ + +module.exports = { + root: true, + env: { + browser: true, + es6: true, + node: true + }, + globals: { + wp: true, + wpApiSettings: true, + wcSettings: true, + es6: true + }, + rules: { + camelcase: 0, + indent: 0, + 'max-len': [ 2, { 'code': 140 } ], + 'no-console': 1 + }, + parser: 'babel-eslint', + parserOptions: { + ecmaVersion: 8, + ecmaFeatures: { + modules: true, + experimentalObjectRestSpread: true, + jsx: true + } + }, +}; diff --git a/plugins/woocommerce/legacy/.gitignore b/plugins/woocommerce/legacy/.gitignore new file mode 100644 index 00000000000..e966da9c4be --- /dev/null +++ b/plugins/woocommerce/legacy/.gitignore @@ -0,0 +1,3 @@ +js/**/*.min.js +css/*.css +css/photoswipe/**/*.min.css diff --git a/plugins/woocommerce/legacy/.stylelintrc b/plugins/woocommerce/legacy/.stylelintrc new file mode 100644 index 00000000000..b727699279b --- /dev/null +++ b/plugins/woocommerce/legacy/.stylelintrc @@ -0,0 +1,3 @@ +{ + "extends": "@wordpress/stylelint-config", +} diff --git a/plugins/woocommerce/legacy/Gruntfile.js b/plugins/woocommerce/legacy/Gruntfile.js new file mode 100644 index 00000000000..e67ea826ec6 --- /dev/null +++ b/plugins/woocommerce/legacy/Gruntfile.js @@ -0,0 +1,271 @@ +module.exports = function ( grunt ) { + 'use strict'; + var sass = require( 'sass' ); + + grunt.initConfig( { + // Setting folder templates. + dirs: { + css: 'css', + cssDest: '../assets/css', + fonts: 'assets/fonts', + images: 'assets/images', + js: 'js', + jsDest: '../assets/js', + php: 'includes', + }, + + // JavaScript linting with ESLint. + eslint: { + src: [ + '<%= dirs.js %>/admin/*.js', + '!<%= dirs.js %>/admin/*.min.js', + '<%= dirs.js %>/frontend/*.js', + '!<%= dirs.js %>/frontend/*.min.js', + ], + }, + + // Sass linting with Stylelint. + stylelint: { + options: { + configFile: '.stylelintrc', + }, + all: [ '<%= dirs.css %>/*.scss', '!<%= dirs.css %>/select2.scss' ], + }, + + // Minify .js files. + uglify: { + options: { + ie8: true, + parse: { + strict: false, + }, + output: { + comments: /@license|@preserve|^!/, + }, + }, + js_assets: { + files: [ + { + expand: true, + cwd: '<%= dirs.jsDest %>/', + src: [ '**/*.js', '!**/*.min.js' ], + extDot: 'last', + dest: '<%= dirs.jsDest %>', + ext: '.min.js', + }, + ], + }, + }, + + // Compile all .scss files. + sass: { + compile: { + options: { + implementation: sass, + sourceMap: false, + }, + files: [ + { + expand: true, + cwd: '<%= dirs.css %>/', + src: [ '*.scss' ], + dest: '<%= dirs.css %>/', + ext: '.css', + }, + ], + }, + }, + + // Generate RTL .css files. + rtlcss: { + woocommerce: { + expand: true, + cwd: '<%= dirs.css %>', + src: [ '*.css', '!select2.css', '!*-rtl.css' ], + dest: '<%= dirs.css %>/', + ext: '-rtl.css', + }, + }, + + // Minify all .css files. + cssmin: { + minify: { + files: [ + { + expand: true, + cwd: '<%= dirs.css %>/', + src: [ '*.css' ], + dest: '<%= dirs.css %>/', + ext: '.css', + }, + { + expand: true, + cwd: '<%= dirs.css %>/photoswipe/', + src: [ '*.css', '!*.min.css' ], + dest: '<%= dirs.css %>/photoswipe/', + ext: '.min.css', + }, + { + expand: true, + cwd: '<%= dirs.css %>/photoswipe/default-skin/', + src: [ '*.css', '!*.min.css' ], + dest: '<%= dirs.css %>/photoswipe/default-skin/', + ext: '.min.css', + }, + ], + }, + }, + + // Concatenate select2.css onto the admin.css files. + concat: { + admin: { + files: { + '<%= dirs.css %>/admin.css': [ + '<%= dirs.css %>/select2.css', + '<%= dirs.css %>/admin.css', + ], + '<%= dirs.css %>/admin-rtl.css': [ + '<%= dirs.css %>/select2.css', + '<%= dirs.css %>/admin-rtl.css', + ], + }, + }, + }, + + // Watch changes for assets. + watch: { + css: { + files: [ '<%= dirs.css %>/*.scss' ], + tasks: [ + 'sass', + 'rtlcss', + 'postcss', + 'cssmin', + 'concat', + 'move:css', + 'copy:css', + ], + }, + js: { + files: [ + 'GruntFile.js', + '<%= dirs.js %>/**/*.js', + '!<%= dirs.js %>/**/*.min.js', + ], + tasks: [ 'eslint', 'copy:js', 'newer:uglify' ], + }, + }, + + // PHP Code Sniffer. + phpcs: { + options: { + bin: 'vendor/bin/phpcs', + }, + dist: { + src: [ + '**/*.php', // Include all php files. + '!includes/api/legacy/**', + '!includes/libraries/**', + '!node_modules/**', + '!tests/cli/**', + '!tmp/**', + '!vendor/**', + ], + }, + }, + + // Autoprefixer. + postcss: { + options: { + processors: [ require( 'autoprefixer' ) ], + }, + dist: { + src: [ '<%= dirs.css %>/*.css' ], + }, + }, + + // Specifying different src/dest for postcss broke everything, + // so we'll just move files to their new location afterwards. + move: { + css: { + files: [ + { + src: '<%= dirs.css %>/*.css', + dest: '<%= dirs.cssDest %>/', + }, + { + src: '<%= dirs.css %>/photoswipe/*.min.css', + dest: '<%= dirs.cssDest %>/photoswipe/', + }, + { + src: + '<%= dirs.css %>/photoswipe/default-skin/*.min.css', + dest: '<%= dirs.cssDest %>/photoswipe/default-skin/', + }, + ], + }, + }, + copy: { + css: { + files: [ + { + cwd: '<%= dirs.css %>', + expand: true, + src: 'photoswipe/**', + dest: '<%= dirs.cssDest %>/', + }, + { + cwd: '<%= dirs.css %>', + expand: true, + src: 'jquery-ui/**', + dest: '<%= dirs.cssDest %>/', + }, + ], + }, + js: { + cwd: '<%= dirs.js %>/', + expand: true, + src: '**', + dest: '<%= dirs.jsDest %>/', + }, + }, + } ); + + // Load NPM tasks to be used here. + grunt.loadNpmTasks( 'grunt-sass' ); + grunt.loadNpmTasks( 'grunt-phpcs' ); + grunt.loadNpmTasks( 'grunt-rtlcss' ); + grunt.loadNpmTasks( 'grunt-postcss' ); + grunt.loadNpmTasks( 'grunt-stylelint' ); + grunt.loadNpmTasks( 'gruntify-eslint' ); + grunt.loadNpmTasks( 'grunt-contrib-uglify' ); + grunt.loadNpmTasks( 'grunt-contrib-cssmin' ); + grunt.loadNpmTasks( 'grunt-contrib-concat' ); + grunt.loadNpmTasks( 'grunt-contrib-copy' ); + grunt.loadNpmTasks( 'grunt-contrib-watch' ); + grunt.loadNpmTasks( 'grunt-contrib-clean' ); + grunt.loadNpmTasks( 'grunt-newer' ); + grunt.loadNpmTasks( 'grunt-move' ); + + // Register tasks. + grunt.registerTask( 'default', [ 'js', 'css' ] ); + + grunt.registerTask( 'js', [ 'eslint', 'copy:js', 'uglify:js_assets' ] ); + + grunt.registerTask( 'css', [ + 'sass', + 'rtlcss', + 'postcss', + 'cssmin', + 'concat', + 'move:css', + 'copy:css', + ] ); + + grunt.registerTask( 'assets', [ 'js', 'css' ] ); + + grunt.registerTask( 'e2e-build', [ 'uglify:js_assets', 'css' ] ); + + // Only an alias to 'default' task. + grunt.registerTask( 'dev', [ 'default' ] ); +}; diff --git a/assets/css/_animation.scss b/plugins/woocommerce/legacy/css/_animation.scss similarity index 100% rename from assets/css/_animation.scss rename to plugins/woocommerce/legacy/css/_animation.scss diff --git a/assets/css/_fonts.scss b/plugins/woocommerce/legacy/css/_fonts.scss similarity index 100% rename from assets/css/_fonts.scss rename to plugins/woocommerce/legacy/css/_fonts.scss diff --git a/plugins/woocommerce/legacy/css/_mixins.scss b/plugins/woocommerce/legacy/css/_mixins.scss new file mode 100644 index 00000000000..2827c082325 --- /dev/null +++ b/plugins/woocommerce/legacy/css/_mixins.scss @@ -0,0 +1,302 @@ +/** + * Deprecated + * Fallback for bourbon equivalent + */ +@mixin clearfix() { + *zoom: 1; + + &::before, + &::after { + content: " "; + display: table; + } + + &::after { + clear: both; + } +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin border_radius($radius: 4px) { + border-radius: $radius; +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin border_radius_right($radius: 4px) { + border-top-right-radius: $radius; + border-bottom-right-radius: $radius; +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin border_radius_left($radius: 4px) { + border-top-left-radius: $radius; + border-bottom-left-radius: $radius; +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin border_radius_bottom($radius: 4px) { + border-bottom-left-radius: $radius; + border-bottom-right-radius: $radius; +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin border_radius_top($radius: 4px) { + border-top-left-radius: $radius; + border-top-right-radius: $radius; +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin opacity( $opacity: 0.75 ) { + opacity: $opacity; +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin box_shadow($shadow_x: 3px, $shadow_y: 3px, $shadow_rad: 3px, $shadow_in: 3px, $shadow_color: #888) { + box-shadow: $shadow_x $shadow_y $shadow_rad $shadow_in $shadow_color; +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin inset_box_shadow($shadow_x: 3px, $shadow_y: 3px, $shadow_rad: 3px, $shadow_in: 3px, $shadow_color: #888) { + box-shadow: inset $shadow_x $shadow_y $shadow_rad $shadow_in $shadow_color; +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin text_shadow($shadow_x: 3px, $shadow_y: 3px, $shadow_rad: 3px, $shadow_color: #fff) { + text-shadow: $shadow_x $shadow_y $shadow_rad $shadow_color; +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin vertical_gradient($from: #000, $to: #fff) { + background-color: $from; + background: -webkit-linear-gradient($from, $to); +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin transition($selector: all, $animation: ease-in-out, $duration: 0.2s) { + transition: $selector $animation $duration; +} + +/** + * Deprecated + * Use bourbon mixin instead `@include transform(scale(1.5));` + */ +@mixin scale($ratio: 1.5) { + -webkit-transform: scale($ratio); + transform: scale($ratio); +} + +/** + * Deprecated + * Use bourbon mixin instead `@include box-sizing(border-box);` + */ +@mixin borderbox() { + box-sizing: border-box; +} + +@mixin darkorlighttextshadow($a, $opacity: 0.8) { + + @if lightness($a) >= 65% { + + @include text_shadow(0, -1px, 0, rgba(0, 0, 0, $opacity)); + } + + @else { + + @include text_shadow(0, 1px, 0, rgba(255, 255, 255, $opacity)); + } +} + +/** + * Objects + */ +@mixin menu() { + + @include clearfix(); + + li { + display: inline-block; + } +} + +@mixin mediaright() { + + @include clearfix(); + + img { + float: right; + height: auto; + } +} + +@mixin medialeft() { + + @include clearfix(); + + img { + float: right; + height: auto; + } +} + +@mixin ir() { + display: block; + text-indent: -9999px; + position: relative; + height: 1em; + width: 1em; +} + +@mixin icon( $glyph: "\e001" ) { + font-family: "WooCommerce"; + speak: never; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + margin: 0; + text-indent: 0; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + text-align: center; + content: $glyph; +} + +@mixin icon_dashicons( $glyph: "\f333" ) { + font-family: "Dashicons"; + speak: never; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + margin: 0; + text-indent: 0; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + text-align: center; + content: $glyph; +} + +@mixin iconbefore( $glyph: "\e001" ) { + font-family: "WooCommerce"; + speak: never; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + margin-right: 0.618em; + content: $glyph; + text-decoration: none; +} + +@mixin iconbeforedashicons( $glyph: "\f333" ) { + font-family: "Dashicons"; + speak: never; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + content: $glyph; + text-decoration: none; +} + +@mixin iconafter( $glyph: "\e001" ) { + font-family: "WooCommerce"; + speak: never; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + margin-left: 0.618em; + content: $glyph; + text-decoration: none; +} + +@mixin loader() { + + &::before { + height: 1em; + width: 1em; + display: block; + position: absolute; + top: 50%; + left: 50%; + margin-left: -0.5em; + margin-top: -0.5em; + content: ""; + animation: spin 1s ease-in-out infinite; + background: url("../images/icons/loader.svg") center center; + background-size: cover; + line-height: 1; + text-align: center; + font-size: 2em; + color: rgba(#000, 0.75); + } +} + +@mixin inversebuttoncolors { + background-color: transparent !important; + color: var(--button--color-text-hover) !important; + + &:hover { + background-color: var(--button--color-background) !important; + color: var(--button--color-text) !important; + text-decoration: none !important; + } +} + +@mixin table-marks() { + mark { + background: transparent none; + } + + mark.yes { + color: $green; + } + + mark.no { + color: #999; + } +} diff --git a/assets/css/_variables.scss b/plugins/woocommerce/legacy/css/_variables.scss similarity index 100% rename from assets/css/_variables.scss rename to plugins/woocommerce/legacy/css/_variables.scss diff --git a/assets/css/activation.scss b/plugins/woocommerce/legacy/css/activation.scss similarity index 100% rename from assets/css/activation.scss rename to plugins/woocommerce/legacy/css/activation.scss diff --git a/plugins/woocommerce/legacy/css/admin.scss b/plugins/woocommerce/legacy/css/admin.scss new file mode 100644 index 00000000000..b5fd23b2ae4 --- /dev/null +++ b/plugins/woocommerce/legacy/css/admin.scss @@ -0,0 +1,7874 @@ +/** + * admin.scss + * General WooCommerce admin styles. Settings, product data tabs, reports, etc. + */ + +/** + * Imports + */ +@import "mixins"; +@import "variables"; +@import "animation"; +@import "fonts"; + +/** + * Styling begins + */ +.blockUI.blockOverlay { + + @include loader(); +} + +.wc-addons-wrap { + + .marketplace-header { + background-image: url(../images/marketplace-header-bg@2x.png); + background-position: right; + background-size: cover; + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: center; + min-height: 216px; + padding: 24px 16px; + width: 100%; + + &__title { + color: #fff; + font-size: 32px; + font-style: normal; + font-weight: 400; + line-height: 1.15; + margin-bottom: 8px; + padding: 0; + } + + &__description { + color: #fff; + font-size: 16px; + line-height: 24px; + margin-bottom: 24px; + margin-top: 0; + } + + &__search-form { + clear: both; + display: block; + max-width: 318px; + position: relative; + + input { + border: 1px solid #ddd; + box-shadow: none; + font-size: 13px; + height: 48px; + padding-left: 16px; + padding-right: 50px; + width: 100%; + margin: 0; + } + + button { + background: none; + border: none; + cursor: pointer; + height: 48px; + position: absolute; + right: 0; + width: 53px; + } + } + } + + .top-bar { + background: #fff; + box-shadow: inset 0 -1px 0 #ccc; + display: block; + height: 60px; + margin: 0 0 16px; + + @media only screen and ( min-width: 768px ) { + margin-bottom: 24px; + } + } + + .current-section-dropdown { + background: #fff; + border: 1px solid #a7aaad; + margin-bottom: 20px; + position: relative; + width: 100%; + + @media only screen and (min-width: 600px) { + width: 288px; + } + + ul { + background: #fff; + border-radius: 2px; + display: none; + flex-direction: column; + justify-content: left; + left: 0; + margin: 0; + padding: 14px 0; + position: absolute; + top: 50px; + width: 100%; + z-index: 10; + + @media only screen and (min-width: 600px) { + border: 1px solid #1e1e1e; + left: -1px; + top: 48px; + } + + @media only screen and (min-width: 1100px) { + justify-content: center; + } + + li { + font-size: 13px; + line-height: 16px; + margin: 0; + + &.current a::after { + background-image: url(../images/icons/gridicons-checkmark.svg); + content: ""; + display: block; + height: 20px; + position: absolute; + right: 20px; + top: 7px; + width: 20px; + } + } + + a, + a:visited, + a:hover, + a:focus { + border: none; + box-shadow: none; + box-sizing: border-box; + color: #1e1e1e; + display: inline-block; + text-decoration: none; + outline: none; + padding: 14px 18px; + position: relative; + width: 100%; + + @media only screen and (min-width: 600px) { + padding: 10px 18px; + } + } + } + } + + .current-section-name { + cursor: pointer; + font-size: 14px; + line-height: 24px; + padding: 12px 16px; + position: relative; + } + + .current-section-name::after { + background-image: url(../images/icons/gridicons-chevron-down.svg); + background-size: contain; + content: ""; + display: block; + height: 20px; + position: absolute; + right: 20px; + top: 16px; + width: 20px; + } + + .current-section-dropdown.is-open { + + ul { + display: flex; + } + + .current-section-name::after { + transform: rotate(0.5turn); + } + } + + .update-plugins .update-count { + background-color: #d54e21; + border-radius: 10px; + color: #fff; + display: inline-block; + font-size: 9px; + font-weight: 600; + line-height: 17px; + margin: 1px 0 0 4px; + padding: 0 6px; + vertical-align: text-top; + } + + /** + * Marketplace related variables + */ + $font-sf-pro-text: + helveticaneue-light, + "Helvetica Neue Light", + "Helvetica Neue", + sans-serif; + + $font-sf-pro-display: sans-serif; + + h1.search-form-title { + clear: left; + font-size: 20px; + font-family: $font-sf-pro-display; + line-height: 1.2; + margin: 48px 0 12px; + padding: 0; + } + + .addons-featured { + margin: 0; + } + + ul.subsubsub.subsubsub { + margin: -2px 0 12px; + } + + .subsubsub li::after { + content: "|"; + } + + .subsubsub li:last-child::after { + content: ""; + } + + .addons-button { + border-radius: 3px; + cursor: pointer; + display: block; + height: 37px; + line-height: 37px; + margin-top: 16px; + text-align: center; + text-decoration: none; + width: 124px; + } + + .addons-wcs-banner-block { + align-items: center; + background: #fff; + border: 1px solid #ddd; + display: flex; + margin: 0 0 1em 0; + padding: 2em 2em 1em; + } + + .addons-wcs-banner-block-image { + background: #f7f7f7; + border: 1px solid #e6e6e6; + margin-right: 2em; + padding: 4em; + max-width: 200px; + + .addons-img { + max-height: 86px; + max-width: 97px; + } + + &.is-full-image { + padding: 0; + background: none; + border: none; + + .addons-img { + max-height: 100%; + max-width: 100%; + } + } + } + + .addons-shipping-methods .addons-wcs-banner-block { + margin-left: 0; + margin-right: 0; + margin-top: 1em; + } + + .addons-wcs-banner-block-content { + display: flex; + flex-direction: column; + justify-content: space-around; + align-self: stretch; + padding: 1em 0; + + h1 { + padding-bottom: 0; + } + + p { + margin-bottom: 0; + } + + .wcs-logos-container { + display: flex; + align-items: center; + flex-direction: row; + justify-content: center; + + @media screen and (min-width: 500px) { + justify-content: left; + } + + li { + margin-right: 8px; + + &:last-child { + margin-right: 0; + } + } + } + + .wcs-service-logo { + max-width: 45px; + } + } + + .addons-column { + flex: 1; + width: 50%; + padding: 0 0.5em; + } + + .addons-column:nth-child(2) { + margin-right: 0; + } + + .addons-small-dark-items { + display: flex; + flex-wrap: wrap; + justify-content: space-around; + } + + .addons-small-dark-item { + margin: 0 0 20px; + } + + .addons-small-dark-item-icon img { + height: 30px; + } + + .addons-small-dark-item a { + margin: 28px auto 0; + } + + .addons-button-solid { + background-color: #674399; + color: #fff; + } + + .addons-button-promoted { + float: right; + width: auto; + padding: 0 20px; + margin-top: 0; + } + + .addons-button-promoted:hover { + opacity: 0.8; + } + + .addons-button-expandable { + display: inline-block; + padding: 0 16px; + width: auto; + } + + .addons-button-solid:hover { + color: #fff; + opacity: 0.8; + } + + .addons-button-outline-green { + border: 1px solid #73ae39; + color: #73ae39; + } + + .addons-button-outline-green:hover { + color: #73ae39; + opacity: 0.8; + } + + .addons-button-outline-purple { + border: 1px solid #674399; + color: #674399; + } + + .addons-button-outline-purple:hover { + color: #674399; + opacity: 0.8; + } + + .addons-button-outline-white { + border: 1px solid #fff; + color: #fff; + } + + .addons-button-outline-white:hover { + color: #fff; + opacity: 0.8; + } + + .addons-button-installed { + background: #e6e6e6; + color: #3c3c3c; + } + + .addons-button-installed:hover { + color: #3c3c3c; + opacity: 0.8; + } + + @media only screen and (max-width: 400px) { + + .addons-button { + width: 100%; + } + + .addons-small-dark-item { + width: 100%; + } + } + + .marketplace-content-wrapper { + font-family: $font-sf-pro-text; + margin: 0 auto; + max-width: 1032px; + width: 100%; + } + + .addon-product-group { + margin-bottom: 24px; + } + + .addon-product-group-title { + font-family: $font-sf-pro-display; + font-size: 20px; + font-weight: 400; + line-height: 24px; + margin: 0 0 4px; + } + + .current-section-dropdown__title { + display: none; + font-family: $font-sf-pro-display; + } + + .addon-product-group-description-container { + align-items: center; + display: flex; + flex-direction: row; + font-size: 14px; + justify-content: space-between; + line-height: 20px; + + .addon-product-group-see-more, + .addon-product-group-see-more:visited { + color: #007cba; /* Primary / Blue */ + display: block; + font-size: 13px; + text-decoration: none; + } + } + + .products { + display: flex; + flex-flow: row; + flex-wrap: wrap; + font-weight: normal; + justify-content: space-between; + margin: 0; + max-width: 1032px; + overflow: hidden; + + .product.addons-product-banner, + .product.addons-buttons-banner { + max-width: calc(100% - 2px); + } + + @media screen and (min-width: 960px) { + // Adjust heading titles font for three-column product groups + &.addons-products-three-column li.product { + max-width: calc(33.33% - 12px); + + h2, + h3 { + font-size: 16px; + } + } + } + + li { + background: #fff; + border: 1px solid #dcdcde; + border-radius: 2px; + box-sizing: border-box; + display: flex; + flex: 1 0 auto; + flex-direction: column; + justify-content: space-between; + margin: 12px 0; + max-width: calc(50% - 12px); + min-width: 280px; + min-height: 220px; + overflow: hidden; + padding: 0; + vertical-align: top; + + &.addons-full-width { + max-width: 100%; + } + + @media only screen and ( max-width: 768px ) { + max-width: none; + width: 100%; + } + + a { + text-decoration: none; + } + + .product-details { + padding: 24px; + position: relative; + + /* Display an image (product's icon) top right */ + .product-img-wrap { + display: block; + margin-left: 24px; + position: absolute; + right: 24px; + top: 24px; + + img { + border-radius: 3px; + display: block; + margin: 0; + max-width: 48px; + max-height: 48px; + } + } + + /* Align aproduct-related banner image vertically centered */ + &.addon-product-banner-details { + align-items: center; + display: flex; + flex-direction: row; + justify-content: space-between; + + .product-img-wrap { + position: unset; + + img { + max-width: 150px; + max-height: 150px; + } + } + } + + h2, + h3 { + color: #007cba; + font-size: 20px; + font-weight: 400; + letter-spacing: -0.32px; + line-height: 28px; + margin: 0 !important; + // Don't cover a product icon + max-width: calc(100% - 48px); + } + + .addons-buttons-banner-details h2 { + color: #1d2327; // Gray / Gray 90 + } + + &.featured, + &.promoted { + + .label { + align-items: center; + border-radius: 2px; + background: #dcdcde; + display: flex; + flex-direction: row; + height: 20px; + justify-content: flex-end; + margin-bottom: 8px; + max-width: 52px; + padding: 3px 12px; + top: 28px; + right: 24px; + text-align: center; + + &.promoted { + float: right; + max-width: 58px; + } + } + + h2 { + color: #2c3338; + } + } + + p { + color: #2c3338; + font-size: 14px; + line-height: 20px; + margin: 14px 64px 0 0; + width: 100%; + } + + .addons-buttons-banner-details p { + font-size: 14px; + margin-bottom: 14px; + max-width: none; + } + + .product-developed-by { + color: #50575e; /* Gray 60 */ + font-size: 12px; + line-height: 20px; + margin-top: 4px; + + .product-vendor-link { + color: #50575e; /* Gray 60 */ + } + } + + .product-developed-by { + color: #50575e; // Gray 60 + font-size: 12px; + font-family: sans-serif; + line-height: 20px; + margin-top: 4px; + + .product-vendor-link { + color: #50575e; // Gray 60 + } + } + } + + .product-footer { + align-items: center; + border-top: 1px solid #dcdcde; + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 24px; + + .price { + font-size: 16px; + color: #1d2327; + } + + .price-suffix { + color: #646970; // Gray 50 + } + + .product-reviews-block { + display: flex; + flex-direction: row; + margin-top: 4px; + + .product-rating-star { + background-repeat: no-repeat; + background-size: contain; + height: 16px; + margin: 4px 4px 4px 0; + width: 17px; + + &__fill { + background-image: url(../images/icons/star-golden.svg); + } + + &__half-fill { + background-image: url(../images/icons/star-half-filled.svg); + } + + &__no-fill { + background-image: url(../images/icons/star-gray.svg); + } + } + + .product-reviews-count { + color: #646970; // Gray 50 + font-size: 12px; + font-family: sans-serif; + line-height: 24px; + letter-spacing: -0.154px; + margin-left: 4px; + } + } + + .button { + background-color: #fff; + border-color: #007cba; + color: #007cba; + float: right; + font-size: 13px; + height: 36px; + line-height: 30px; + padding: 2px 14px; + } + } + } + + .product-footer-promoted { + align-items: flex-end; + display: flex; + justify-content: space-between; + padding: 24px; + + .icon img { + border-radius: 4px; + width: 80px; + } + } + + .addons-buttons-banner { + display: flex; + flex-direction: row; + + .addons-buttons-banner-image { + background-repeat: no-repeat; + background-size: cover; + height: 190px; + margin: 24px; + width: 200px; + } + + .addons-buttons-banner-details-container { + padding-left: 0; + width: calc(100% - 198px - 24px - 24px); + } + + .addons-buttons-banner-details-container { + display: flex; + flex-direction: column; + justify-content: space-between; + } + + .button.addons-buttons-banner-button, + .button.addons-buttons-banner-button:hover { + background: #fff; + border: 1.5px solid #624594; + color: #624594; + padding: 4px 12px; + margin-right: 16px; + + &.addons-buttons-banner-button-primary { + background-color: #624594; + color: #fff; + } + } + } + } + + .storefront { + max-width: 990px; + background: url(../images/storefront-bg.jpg) bottom right #f6f6f6; + border: 1px solid #ddd; + margin: 1em auto; + padding: 24px; + overflow: hidden; + zoom: 1; + + img { + display: block; + width: 100%; + max-width: 400px; + height: auto; + margin: 0 auto 16px; + box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1); + } + + p:last-of-type { + margin-bottom: 0; + } + + p { + max-width: 750px; + } + } +} + +.marketplace-header__tabs { + display: flex; + margin: 0; +} + +.marketplace-header__tab { + display: flex; + flex: 1; + margin: 0; +} + +.marketplace-header__tab-link { + align-items: center; + border-bottom: 2px solid transparent; + box-sizing: border-box; + display: flex; + font-size: 14px; + height: 60px; + justify-content: center; + line-height: 20px; + padding: 0 24px; + text-decoration: none; + width: 100%; + + &.is-current { + border-bottom: 2px solid #1e1e1e; + color: #1e1e1e; + } +} + +.no-touch, +.no-js { + + .wc-addons-wrap { + + .current-section-dropdown:hover { + + ul { + display: flex; + } + + .current-section-name::after { + transform: rotate(0.5turn); + } + } + } +} + +.wc-subscriptions-wrap { + max-width: 1200px; + + .update-plugins .update-count { + background-color: #d54e21; + border-radius: 10px; + color: #fff; + display: inline-block; + font-size: 9px; + font-weight: 600; + line-height: 17px; + margin: 1px 0 0 2px; + padding: 0 6px; + vertical-align: text-top; + } +} + +.woocommerce-page-wc-marketplace { + + .notice { + margin-left: 20px; + margin-right: 20px; + } + + &.woocommerce-page { + + .wrap { + margin-top: 32px; + } + } +} + +.woocommerce-page-wc-subscriptions { + + #wpbody-content { + + .screen-reader-text + .notice { + margin-top: 32px; + } + } +} + +.woocommerce-embed-page.woocommerce-page-wc-marketplace { + + #screen-meta-links { + position: absolute; + right: 0; + } +} + +.woocommerce-message, +.woocommerce-BlankState { + + a.button-primary, + button.button-primary { + background: #bb77ae; + border-color: #a36597; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; + color: #fff; + text-shadow: + 0 -1px 1px #a36597, + 1px 0 1px #a36597, + 0 1px 1px #a36597, + -1px 0 1px #a36597; + display: inline-block; + + &:hover, + &:focus, + &:active { + background: #a36597; + border-color: #a36597; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; + } + } +} + +.woocommerce-message { + position: relative; + overflow: hidden; + + &.updated { + border-left-color: #cc99c2 !important; + } + + a.skip, + a.docs { + text-decoration: none !important; + } + + a.woocommerce-message-close { + position: static; + float: right; + padding: 0 15px 10px 28px; + margin-top: -10px; + font-size: 13px; + line-height: 1.23076923; + text-decoration: none; + + &::before { + position: relative; + top: 18px; + left: -20px; + transition: all 0.1s ease-in-out; + } + } + + .twitter-share-button { + margin-top: -3px; + margin-left: 3px; + vertical-align: middle; + } +} + +#variable_product_options #message, +#variable_product_options .notice { + margin: 10px; +} + +#variable_product_options { + + .form-row select { + max-width: 100%; + } + + .toolbar-top { + + .button { + margin: 1px; + } + } +} + +#product_attributes { + + .toolbar-top { + + .button { + margin: 1px; + } + } +} + +.clear { + clear: both; +} + +.wrap.woocommerce div.updated, +.wrap.woocommerce div.error { + margin-top: 10px; +} + +mark.amount { + background: transparent none; + color: inherit; +} + +/** + * Help Tip + */ +.woocommerce-help-tip { + color: #666; + display: inline-block; + font-size: 1.1em; + font-style: normal; + height: 16px; + line-height: 16px; + position: relative; + vertical-align: middle; + width: 16px; + + &::after { + + @include icon_dashicons("\f223"); + cursor: help; + } +} + +.wc-wp-version-gte-53 { + + .woocommerce-help-tip { + font-size: 1.2em; + cursor: help; + } +} + +h2 .woocommerce-help-tip { + margin-top: -5px; + margin-left: 0.25em; +} + +table.wc_status_table { + margin-bottom: 1em; + + h2 { + font-size: 14px; + margin: 0; + } + + tr:nth-child(2n) { + + th, + td { + background: #fcfcfc; + } + } + + th { + font-weight: 700; + padding: 9px; + } + + td:first-child { + width: 33%; + } + + td.help { + width: 1em; + } + + td, + th { + font-size: 1.1em; + font-weight: normal; + + &.run-tool { + text-align: right; + } + + strong.name { + display: block; + margin-bottom: 0.5em; + } + + @include table-marks(); + + mark.error, + .red { + color: $red; + } + + ul { + margin: 0; + } + } + + .help_tip { + cursor: help; + } +} + +table.wp-list-table.urls { + td, + th { + @include table-marks(); + } +} + +table.wc_status_table--tools { + + td, + th { + padding: 2em; + } +} + +.taxonomy-product_cat { + + .check-column .woocommerce-help-tip { + font-size: 1.5em; + margin: -3px 0 0 5px; + display: block; + position: absolute; + } +} + +#debug-report { + display: none; + margin: 10px 0; + padding: 0; + position: relative; + + textarea { + font-family: monospace; + width: 100%; + margin: 0; + height: 300px; + padding: 20px; + border-radius: 0; + resize: none; + font-size: 12px; + line-height: 20px; + outline: 0; + } +} + +/** + * DB log viewer + */ +.wp-list-table.logs { + + .log-level { + display: inline; + padding: 0.2em 0.6em 0.3em; + font-size: 80%; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.2em; + + &:empty { + display: none; + } + } + + /** + * Add color to levels + * + * Descending severity: + * emergency, alert -> red + * critical, error -> orange + * warning, notice -> yellow + * info -> blue + * debug -> gree + */ + + .log-level--emergency, + .log-level--alert { + background-color: #ff4136; + } + + .log-level--critical, + .log-level--error { + background-color: #ff851b; + } + + .log-level--warning, + .log-level--notice { + color: #222; + background-color: #ffdc00; + } + + .log-level--info { + background-color: #0074d9; + } + + .log-level--debug { + background-color: #3d9970; + } + + // Adjust log table columns only when table is not collapsed + @media screen and (min-width: 783px) { + + .column-timestamp { + width: 18%; + } + + .column-level { + width: 14%; + } + + .column-source { + width: 15%; + } + } +} + +#log-viewer-select { + padding: 10px 0 8px; + line-height: 28px; + + h2 a { + vertical-align: middle; + } +} + +#log-viewer { + background: #fff; + border: 1px solid #e5e5e5; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); + padding: 5px 20px; + + pre { + font-family: monospace; + white-space: pre-wrap; + word-wrap: break-word; + } +} + +.inline-edit-product.quick-edit-row { + + .inline-edit-col-center, + .inline-edit-col-right { + float: right !important; + } +} + +#woocommerce-fields.inline-edit-col { + clear: left; + + label.featured, + label.manage_stock { + margin-left: 10px; + } + + label.stock_status_field { + clear: both; + float: left; + } + + .dimensions div { + display: block; + margin: 0.2em 0; + + span.title { + display: block; + float: left; + width: 5em; + } + + span.input-text-wrap { + display: block; + margin-left: 5em; + } + } + + .text { + box-sizing: border-box; + width: 99%; + float: left; + margin: 1px 1% 1px 1px; + } + + .length, + .width, + .height { + width: 32.33%; + } + + .height { + margin-right: 0; + } +} + +#woocommerce-fields-bulk.inline-edit-col { + + label { + clear: left; + } + + .inline-edit-group { + + label { + clear: none; + width: 49%; + margin: 0.2em 0; + } + + &.dimensions label { + width: 75%; + max-width: 75%; + } + } + + .regular_price, + .sale_price, + .weight, + .stock, + .length { + box-sizing: border-box; + width: 100%; + margin-left: 4.4em; + } + + .length, + .width, + .height { + box-sizing: border-box; + width: 25%; + } +} + +.column-coupon_code { + line-height: 2.25em; +} + +ul.wc_coupon_list, +.column-coupon_code { + margin: 0; + overflow: hidden; + zoom: 1; + clear: both; +} + +ul.wc_coupon_list { + padding-bottom: 5px; + + li { + margin: 0; + + &.code { + display: inline-block; + position: relative; + padding: 0 0.5em; + background-color: #fff; + border: 1px solid #aaa; + -webkit-box-shadow: 0 1px 0 #dfdfdf; + box-shadow: 0 1px 0 #dfdfdf; + + border-radius: 4px; + margin-right: 5px; + margin-top: 5px; + + &.editable { + padding-right: 2em; + } + + .tips { + cursor: pointer; + + span { + color: #888; + + &:hover { + color: #000; + } + } + } + + .remove-coupon { + text-decoration: none; + color: #888; + position: absolute; + top: 7px; + right: 20px; + + /*rtl:raw: + left: 7px; + */ + + &::before { + + @include icon_dashicons("\f158"); + } + + &:hover::before { + color: $red; + } + } + } + } +} + +ul.wc_coupon_list_block { + margin: 0; + padding-bottom: 2px; + + li { + border-top: 1px solid #fff; + border-bottom: 1px solid #ccc; + line-height: 2.5em; + margin: 0; + padding: 0.5em 0; + } + + li:first-child { + border-top: 0; + padding-top: 0; + } + + li:last-child { + border-bottom: 0; + padding-bottom: 0; + } +} + +.button.wc-reload { + + @include ir(); + padding: 0; + height: 28px; + width: 28px !important; + display: inline-block; + + &::after { + + @include icon_dashicons("\f345"); + line-height: 28px; + } +} + +#woocommerce-order-data { + + .postbox-header, + .hndle, + .handlediv { + display: none; + } + + .inside { + display: block !important; + } +} + +#order_data { + padding: 23px 24px 12px; + + h2 { + margin: 0; + font-family: + "HelveticaNeue-Light", + "Helvetica Neue Light", + "Helvetica Neue", + sans-serif; + font-size: 21px; + font-weight: normal; + line-height: 1.2; + text-shadow: 1px 1px 1px white; + padding: 0; + } + + h3 { + font-size: 14px; + } + + h3, + h4 { + color: #333; + margin: 1.33em 0 0; + } + + p { + color: #777; + } + + p.order_number { + margin: 0; + font-family: + "HelveticaNeue-Light", + "Helvetica Neue Light", + "Helvetica Neue", + sans-serif; + font-weight: normal; + line-height: 1.6em; + font-size: 16px; + } + + .order_data_column_container { + clear: both; + + p._billing_email_field { + margin-top: 13px; + } + } + + .order_data_column { + width: 32%; + padding: 0 2% 0 0; + float: left; + + > h3 span { + display: block; + } + + &:last-child { + padding-right: 0; + } + + p { + padding: 0 !important; + } + + .address strong { + display: block; + } + + .form-field { + float: left; + clear: left; + width: 48%; + padding: 0; + margin: 9px 0 0; + + label { + display: block; + padding: 0 0 3px; + } + + input, + textarea { + width: 100%; + } + + select { + width: 100%; + max-width: 100%; + } + + .select2-container { + width: 100% !important; + } + + .date-picker { + width: 50%; + } + + .hour, + .minute { + width: 3.5em; + } + + small { + display: block; + margin: 5px 0 0; + color: #999; + } + } + + .form-field.last, + ._billing_last_name_field, + ._billing_address_2_field, + ._billing_postcode_field, + ._billing_state_field, + ._billing_phone_field, + ._shipping_last_name_field, + ._shipping_address_2_field, + ._shipping_postcode_field, + ._shipping_state_field { + float: right; + clear: right; + } + + .form-field-wide, + ._billing_company_field, + ._shipping_company_field, + ._transaction_id_field { + width: 100%; + clear: both; + + input, + textarea, + select, + .wc-enhanced-select, + .wc-category-search, + .wc-customer-search { + width: 100%; + } + } + + p.none_set { + color: #999; + } + + div.edit_address { + display: none; + zoom: 1; + padding-right: 1px; + + .select2-container { + + .select2-selection--single { + height: 32px; + + .select2-selection__rendered { + line-height: 32px; + } + } + } + } + + .wc-customer-user, + .wc-order-status { + + label a { + float: right; + margin-left: 8px; + } + } + + a.edit_address { + width: 14px; + height: 0; + padding: 14px 0 0; + margin: 0 0 0 6px; + overflow: hidden; + position: relative; + color: #999; + border: 0; + float: right; + + &:hover, + &:focus { + color: #000; + } + + &::after { + font-family: "WooCommerce"; + position: absolute; + top: 0; + left: 0; + text-align: center; + vertical-align: top; + line-height: 14px; + font-size: 14px; + font-weight: 400; + } + } + + a.edit_address::after { + font-family: "Dashicons"; + content: "\f464"; + } + + .billing-same-as-shipping, + .load_customer_shipping, + .load_customer_billing { + font-size: 13px; + display: inline-block; + font-weight: normal; + } + + .load_customer_shipping { + margin-right: 0.3em; + } + } +} + +.order_actions { + margin: 0; + overflow: hidden; + zoom: 1; + + li { + border-top: 1px solid #fff; + border-bottom: 1px solid #ddd; + padding: 6px 0; + margin: 0; + line-height: 1.6em; + float: left; + width: 50%; + text-align: center; + + a { + float: none; + text-align: center; + text-decoration: underline; + } + + &.wide { + width: auto; + float: none; + clear: both; + padding: 6px; + text-align: left; + overflow: hidden; + } + + #delete-action { + line-height: 25px; + vertical-align: middle; + text-align: left; + float: left; + } + + .save_order { + float: right; + } + + &#actions { + overflow: hidden; + + .button { + width: 24px; + box-sizing: border-box; + float: right; + } + + select { + width: 225px; + box-sizing: border-box; + float: left; + } + } + } +} + +#woocommerce-order-items { + + .inside { + margin: 0; + padding: 0; + background: #fefefe; + } + + .wc-order-data-row { + border-bottom: 1px solid #dfdfdf; + padding: 1.5em 2em; + background: #f8f8f8; + + @include clearfix(); + line-height: 2em; + text-align: right; + + p { + margin: 0; + line-height: 2em; + } + + .wc-used-coupons { + text-align: left; + + .tips { + display: inline-block; + } + } + } + + .wc-used-coupons { + float: left; + width: 50%; + } + + .wc-order-totals { + float: right; + width: 50%; + margin: 0; + padding: 0; + text-align: right; + + .amount { + font-weight: 700; + } + + .label { + vertical-align: top; + } + + .total { + font-size: 1em !important; + width: 10em; + margin: 0 0 0 0.5em; + box-sizing: border-box; + + input[type="text"] { + width: 96%; + float: right; + } + } + + .refunded-total { + color: $red; + } + + .label-highlight { + font-weight: bold; + } + } + + .refund-actions { + margin-top: 5px; + padding-top: 12px; + border-top: 1px solid #dfdfdf; + + .button { + float: right; + margin-left: 4px; + } + + .cancel-action { + float: left; + margin-left: 0; + } + } + + .add_meta { + margin-left: 0 !important; + } + + h3 small { + color: #999; + } + + .amount { + white-space: nowrap; + } + + .add-items { + + .description { + margin-right: 10px; + } + + .button { + float: left; + margin-right: 0.25em; + } + + .button-primary { + float: none; + margin-right: 0; + } + } +} + +#woocommerce-order-items { + + .inside { + display: block !important; + } + + .postbox-header, + .hndle, + .handlediv { + display: none; + } + + .woocommerce_order_items_wrapper { + margin: 0; + overflow-x: auto; + + table.woocommerce_order_items { + width: 100%; + background: #fff; + + thead th { + text-align: left; + padding: 1em; + font-weight: normal; + color: #999; + background: #f8f8f8; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + &.sortable { + cursor: pointer; + } + + &:last-child { + padding-right: 2em; + } + + &:first-child { + padding-left: 2em; + } + + .wc-arrow { + float: right; + position: relative; + margin-right: -1em; + } + } + + tbody th, + td { + padding: 1.5em 1em 1em; + text-align: left; + line-height: 1.5em; + vertical-align: top; + border-bottom: 1px solid #f8f8f8; + + textarea { + width: 100%; + } + + select { + width: 50%; + } + + input, + textarea { + font-size: 14px; + padding: 4px; + color: #555; + } + + &:last-child { + padding-right: 2em; + } + + &:first-child { + padding-left: 2em; + } + } + + tbody tr:last-child td { + border-bottom: 1px solid #dfdfdf; + } + + tbody tr:first-child td { + border-top: 8px solid #f8f8f8; + } + + tbody#order_line_items tr:first-child td { + border-top: none; + } + + td.thumb { + text-align: left; + width: 38px; + padding-bottom: 1.5em; + + .wc-order-item-thumbnail { + width: 38px; + height: 38px; + border: 2px solid #e8e8e8; + background: #f8f8f8; + color: #ccc; + position: relative; + font-size: 21px; + display: block; + text-align: center; + + &::before { + + @include icon_dashicons("\f128"); + width: 38px; + line-height: 38px; + display: block; + } + + img { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + position: relative; + } + } + } + + td.name { + + .wc-order-item-sku, + .wc-order-item-variation { + display: block; + margin-top: 0.5em; + font-size: 0.92em !important; + color: #888; + } + } + + .item { + min-width: 200px; + } + + .center, + .variation-id { + text-align: center; + } + + .cost, + .tax, + .quantity, + .line_cost, + .line_tax, + .tax_class, + .item_cost { + text-align: right; + + label { + white-space: nowrap; + color: #999; + font-size: 0.833em; + + input { + display: inline; + } + } + + input { + width: 70px; + vertical-align: middle; + text-align: right; + } + + select { + width: 85px; + height: 26px; + vertical-align: middle; + font-size: 1em; + } + + .split-input { + display: inline-block; + background: #fff; + border: 1px solid #ddd; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.07); + margin: 1px 0; + min-width: 80px; + overflow: hidden; + line-height: 1em; + text-align: right; + + div.input { + width: 100%; + box-sizing: border-box; + + label { + font-size: 0.75em; + padding: 4px 6px 0; + color: #555; + display: block; + } + + input { + width: 100%; + box-sizing: border-box; + border: 0; + box-shadow: none; + margin: 0; + padding: 0 6px 4px; + color: #555; + background: transparent; + + &::-webkit-input-placeholder { + color: #ddd; + } + } + } + + div.input:first-child { + border-bottom: 1px dashed #ddd; + background: #fff; + + label { + color: #ccc; + } + + input { + color: #ccc; + } + } + } + + .view { + white-space: nowrap; + } + + .edit { + text-align: left; + } + + small.times, + del, + .wc-order-item-taxes, + .wc-order-item-discount, + .wc-order-item-refund-fields { + font-size: 0.92em !important; + color: #888; + } + + .wc-order-item-taxes, + .wc-order-item-refund-fields { + margin: 0; + + label { + display: block; + } + } + + .wc-order-item-discount { + display: block; + margin-top: 0.5em; + } + + small.times { + margin-right: 0.25em; + } + } + + .quantity { + text-align: center; + + input { + text-align: center; + width: 50px; + } + } + + span.subtotal { + opacity: 0.5; + } + + td.tax_class, + th.tax_class { + text-align: left; + } + + .calculated { + border-color: #ae8ca2; + border-style: dotted; + } + + table.meta { + width: 100%; + } + + table.meta, + table.display_meta { + margin: 0.5em 0 0; + font-size: 0.92em !important; + color: #888; + + tr { + + th { + border: 0; + padding: 0 4px 0.5em 0; + line-height: 1.5em; + width: 20%; + } + + td { + padding: 0 4px 0.5em 0; + border: 0; + line-height: 1.5em; + + input { + width: 100%; + margin: 0; + position: relative; + border-bottom: 0; + box-shadow: none; + } + + textarea { + width: 100%; + height: 4em; + margin: 0; + box-shadow: none; + } + + input:focus + textarea { + border-top-color: #999; + } + + p { + margin: 0 0 0.5em; + line-height: 1.5em; + } + + p:last-child { + margin: 0; + } + } + } + } + + .refund_by { + border-bottom: 1px dotted #999; + } + + tr.fee .thumb div { + + @include ir(); + font-size: 1.5em; + line-height: 1em; + vertical-align: middle; + margin: 0 auto; + + &::before { + + @include icon("\e007"); + color: #ccc; + } + } + + tr.refund .thumb div { + + @include ir(); + font-size: 1.5em; + line-height: 1em; + vertical-align: middle; + margin: 0 auto; + + &::before { + + @include icon("\e014"); + color: #ccc; + } + } + + tr.shipping { + + .thumb div { + + @include ir(); + font-size: 1.5em; + line-height: 1em; + vertical-align: middle; + margin: 0 auto; + + &::before { + + @include icon("\e01a"); + color: #ccc; + } + } + + .shipping_method_name, + .shipping_method { + width: 100%; + margin: 0 0 0.5em; + } + } + + th.line_tax { + white-space: nowrap; + } + + th.line_tax, + td.line_tax { + + .delete-order-tax { + + @include ir(); + float: right; + font-size: 14px; + visibility: hidden; + margin: 3px -18px 0 0; + + &::before { + + @include icon_dashicons("\f153"); + color: #999; + } + + &:hover::before { + color: $red; + } + } + + &:hover .delete-order-tax { + visibility: visible; + } + } + + small.refunded { + display: block; + color: $red; + white-space: nowrap; + margin-top: 0.5em; + + &::before { + + @include icon_dashicons("\f171"); + position: relative; + top: auto; + left: auto; + margin: -1px 4px 0 0; + vertical-align: middle; + line-height: 1em; + } + } + } + } + + .wc-order-edit-line-item { + padding-left: 0; + } + + .wc-order-edit-line-item-actions { + width: 44px; + text-align: right; + padding-left: 0; + vertical-align: middle; + + a { + color: #ccc; + display: inline-block; + cursor: pointer; + padding: 0 0 0.5em; + margin: 0 0 0 12px; + vertical-align: middle; + text-decoration: none; + line-height: 16px; + width: 16px; + overflow: hidden; + + &::before { + margin: 0; + padding: 0; + font-size: 16px; + width: 16px; + height: 16px; + } + + &:hover { + + &::before { + color: #999; + } + } + + &:first-child { + margin-left: 0; + } + } + + .edit-order-item::before { + + @include icon_dashicons("\f464"); + position: relative; + } + + .delete-order-item, + .delete_refund { + + &::before { + + @include icon_dashicons("\f158"); + position: relative; + } + + &:hover::before { + color: $red; + } + } + } + + tbody tr .wc-order-edit-line-item-actions { + visibility: hidden; + } + + tbody tr:hover .wc-order-edit-line-item-actions { + visibility: visible; + } + + .wc-order-totals .wc-order-edit-line-item-actions { + width: 1.5em; + visibility: visible !important; + + a { + padding: 0; + } + } +} + +#woocommerce-order-downloads { + + .buttons { + float: left; + padding: 0; + margin: 0; + vertical-align: top; + + .add_item_id, + .select2-container { + width: 400px !important; + margin-right: 9px; + vertical-align: top; + float: left; + } + + button { + margin: 2px 0 0; + } + } + + h3 small { + color: #999; + } +} + +#side-sortables #woocommerce-order-downloads { + + .buttons, + .select2-container { + max-width: 100%; + } +} + +#poststuff #woocommerce-order-actions .inside { + margin: 0; + padding: 0; + + ul.order_actions li { + padding: 6px 10px; + box-sizing: border-box; + + &:last-child { + border-bottom: 0; + } + } + + button { + margin: 1px; + } +} + +#poststuff #woocommerce-order-notes .inside { + margin: 0; + padding: 0; + + ul.order_notes li { + padding: 0 10px; + } + + button { + margin: 1px; + vertical-align: top; + } +} + +#woocommerce_customers { + + p.search-box { + margin: 6px 0 4px; + float: left; + } + + .tablenav { + float: right; + clear: none; + } +} + +.widefat { + + &.customers td { + vertical-align: middle; + padding: 4px 7px; + } + + .column-order_title { + width: 15%; + + time { + display: block; + color: #999; + margin: 3px 0; + } + } + + .column-orders, + .column-paying, + .column-spent { + text-align: center; + width: 8%; + } + + .column-last_order { + width: 11%; + } + + .column-wc_actions { + width: 110px; + + a.button { + + @include ir(); + display: inline-block; + margin: 2px 4px 2px 0; + padding: 0 !important; + height: 2em !important; + width: 2em; + overflow: hidden; + vertical-align: middle; + + &::after { + font-family: "Dashicons"; + speak: never; + font-weight: normal; + font-variant: normal; + text-transform: none; + margin: 0; + text-indent: 0; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + text-align: center; + line-height: 1.85; + } + + img { + display: block; + width: 12px; + height: auto; + } + } + + a.edit::after { + content: "\f464"; + } + + a.link::after { + font-family: "WooCommerce"; + content: "\e00d"; + } + + a.view::after { + content: "\f177"; + } + + a.refresh::after { + font-family: "WooCommerce"; + content: "\e031"; + } + + a.processing::after { + font-family: "WooCommerce"; + content: "\e00f"; + } + + a.complete::after { + content: "\f147"; + } + } + + small.meta { + display: block; + color: #999; + font-size: inherit; + margin: 3px 0; + } +} + +.wc-wp-version-gte-53 { + + .widefat { + + .column-wc_actions { + + a.button { + + &::after { + margin-top: 2px; + } + } + } + } +} + +.post-type-shop_order { + + .tablenav .one-page .displaying-num { + display: none; + } + + .tablenav { + + .select2-selection--single { + height: 32px; + + .select2-selection__rendered { + line-height: 29px; + } + + .select2-selection__arrow { + height: 30px; + } + } + } + + .wp-list-table { + margin-top: 1em; + + thead, + tfoot { + + th { + padding: 0.75em 1em; + } + + th.sortable a, + th.sorted a { + padding: 0; + } + + th:first-child { + padding-left: 2em; + } + + th:last-child { + padding-right: 2em; + } + } + + tbody { + + td, + th { + padding: 1em; + line-height: 26px; + } + + td:first-child { + padding-left: 2em; + } + + td:last-child { + padding-right: 2em; + } + } + + tbody tr { + border-top: 1px solid #f5f5f5; + } + + tbody tr:hover:not(.status-trash):not(.no-link) td { + cursor: pointer; + } + + .no-link { + cursor: default !important; + } + + // Columns. + td, + th { + width: 12ch; + vertical-align: middle; + + p { + margin: 0; + } + } + + .check-column { + width: 1px; + white-space: nowrap; + padding: 1em 1em 1em 1em !important; + vertical-align: middle; + + input { + vertical-align: text-top; + margin: 1px 0; + } + } + + .column-order_number { + width: 20ch; + } + + .column-order_total { + width: 8ch; + text-align: right; + + a span { + float: right; + } + } + + .column-order_date, + .column-order_status { + width: 10ch; + } + + .column-order_status { + width: 14ch; + } + + .column-shipping_address, + .column-billing_address { + width: 20ch; + line-height: 1.5em; + + .description { + display: block; + color: #999; + } + } + + .column-wc_actions { + text-align: right; + + a.button { + text-indent: 9999px; + margin: 2px 0 2px 4px; + } + } + + .order-preview { + float: right; + width: 16px; + padding: 20px 4px 4px 4px; + height: 0; + overflow: hidden; + position: relative; + border: 2px solid transparent; + border-radius: 4px; + + &::before { + + @include icon("\e010"); + line-height: 16px; + font-size: 14px; + vertical-align: middle; + top: 4px; + } + + &:hover { + border: 2px solid #00a0d2; + } + } + + .order-preview.disabled { + + &::before { + content: ""; + background: url("../images/wpspin-2x.gif") no-repeat center top; + background-size: 71%; + } + } + } +} + +.order-status { + display: inline-flex; + line-height: 2.5em; + color: #777; + background: #e5e5e5; + border-radius: 4px; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + margin: -0.25em 0; + cursor: inherit !important; + white-space: nowrap; + max-width: 100%; + + &.status-completed { + background: #c8d7e1; + color: #2e4453; + } + + &.status-on-hold { + background: #f8dda7; + color: #94660c; + } + + &.status-failed { + background: #eba3a3; + color: #761919; + } + + &.status-processing { + background: #c6e1c6; + color: #5b841b; + } + + &.status-trash { + background: #eba3a3; + color: #761919; + } + + > span { + margin: 0 1em; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.wc-order-preview { + + .order-status { + float: right; + margin-right: 54px; + } + + article { + padding: 0 !important; + } + + .modal-close { + border-radius: 0; + } + + .wc-order-preview-table { + width: 100%; + margin: 0; + + th, + td { + padding: 1em 1.5em; + text-align: left; + border: 0; + border-bottom: 1px solid #eee; + margin: 0; + background: transparent; + box-shadow: none; + text-align: right; + vertical-align: top; + } + + td:first-child, + th:first-child { + text-align: left; + } + + th { + border-color: #ccc; + } + + tr:last-child td { + border: 0; + } + + .wc-order-item-sku { + margin-top: 0.5em; + } + + .wc-order-item-meta { + margin-top: 0.5em; + + th, + td { + padding: 0; + border: 0; + text-align: left; + vertical-align: top; + } + + td:last-child { + padding-left: 0.5em; + } + } + } + + .wc-order-preview-addresses { + overflow: hidden; + padding-bottom: 1.5em; + + .wc-order-preview-address, + .wc-order-preview-note { + width: 50%; + float: left; + padding: 1.5em 1.5em 0; + box-sizing: border-box; + word-wrap: break-word; + + h2 { + margin-top: 0; + } + + strong { + display: block; + margin-top: 1.5em; + } + + strong:first-child { + margin-top: 0; + } + } + } + + footer { + + .wc-action-button-group { + display: inline-block; + float: left; + } + + .button.button-large { + margin-left: 10px; + padding: 0 10px !important; + line-height: 28px; + height: auto; + display: inline-block; + } + } + + .wc-action-button-group label { + display: none; + } +} + +.wc-action-button-group { + vertical-align: middle; + line-height: 26px; + text-align: left; + + label { + margin-right: 6px; + cursor: default; + font-weight: bold; + line-height: 28px; + } + + .wc-action-button-group__items { + display: inline-flex; + flex-flow: row wrap; + align-content: flex-start; + justify-content: flex-start; + } + + .wc-action-button { + margin: 0 0 0 -1px !important; + border: 1px solid #ccc; + padding: 0 10px !important; + border-radius: 0 !important; + float: none; + line-height: 28px; + height: auto; + z-index: 1; + position: relative; + overflow: hidden; + text-overflow: ellipsis; + flex: 1 0 auto; + box-sizing: border-box; + text-align: center; + white-space: nowrap; + } + + .wc-action-button:hover, + .wc-action-button:focus { + border: 1px solid #999; + z-index: 2; + } + + .wc-action-button:first-child { + margin-left: 0 !important; + border-top-left-radius: 3px !important; + border-bottom-left-radius: 3px !important; + } + + .wc-action-button:last-child { + border-top-right-radius: 3px !important; + border-bottom-right-radius: 3px !important; + } +} + +@media screen and (max-width: 782px) { + + .wc-order-preview footer { + + .wc-action-button-group .wc-action-button-group__items { + display: flex; + } + + .wc-action-button-group { + float: none; + display: block; + margin-bottom: 4px; + } + + .button.button-large { + width: 100%; + float: none; + text-align: center; + margin: 0; + display: block; + } + } + + .post-type-shop_order .wp-list-table { + + td.check-column { + width: 1em; + } + + td.column-order_number { + padding-left: 0; + padding-bottom: 0.5em; + } + + td.column-order_status, + td.column-order_date { + display: inline-block !important; + padding: 0 1em 1em 1em !important; + + &::before { + display: none !important; + } + } + + td.column-order_date { + padding-left: 0 !important; + } + + td.column-order_status { + float: right; + } + } +} + +.column-customer_message .note-on { + + @include ir(); + margin: 0 auto; + color: #999; + + &::after { + + @include icon("\e026"); + line-height: 16px; + } +} + +.column-order_notes .note-on { + + @include ir(); + margin: 0 auto; + color: #999; + + &::after { + + @include icon("\e027"); + line-height: 16px; + } +} + +.attributes-table { + + td, + th { + width: 15%; + vertical-align: top; + } + + .attribute-terms { + width: 32%; + } + + .attribute-actions { + width: 2em; + + .configure-terms { + + @include ir(); + padding: 0 !important; + height: 2em !important; + width: 2em; + + &::after { + + @include icon("\f111"); + font-family: "Dashicons"; + line-height: 1.85; + } + } + } +} + +/* Order notes */ +ul.order_notes { + padding: 2px 0 0; + + li { + + .note_content { + padding: 10px; + background: #efefef; + position: relative; + + p { + margin: 0; + padding: 0; + word-wrap: break-word; + } + } + + p.meta { + padding: 10px; + color: #999; + margin: 0; + font-size: 11px; + + .exact-date { + border-bottom: 1px dotted #999; + } + } + + a.delete_note { + color: $red; + } + + .note_content::after { + content: ""; + display: block; + position: absolute; + bottom: -10px; + left: 20px; + width: 0; + height: 0; + border-width: 10px 10px 0 0; + border-style: solid; + border-color: #efefef transparent; + } + } + + li.system-note { + + .note_content { + background: #d7cad2; + } + + .note_content::after { + border-color: #d7cad2 transparent; + } + } + + li.customer-note { + + .note_content { + background: #a7cedc; + } + + .note_content::after { + border-color: #a7cedc transparent; + } + } +} + +.add_note { + border-top: 1px solid #ddd; + padding: 10px 10px 0; + + h4 { + margin-top: 5px !important; + } + + #add_order_note { + width: 100%; + height: 50px; + } +} + +table.wp-list-table { + + .column-thumb { + width: 52px; + text-align: center; + white-space: nowrap; + } + + .column-handle { + width: 17px; + display: none; + } + + tbody { + + td.column-handle { + cursor: move; + width: 17px; + text-align: center; + vertical-align: text-top; + + &::before { + content: "\f333"; + font-family: "Dashicons"; + text-align: center; + line-height: 1; + color: #999; + display: block; + width: 17px; + height: 100%; + margin: 4px 0 0 0; + } + } + } + + .column-name { + width: 22%; + } + + .column-product_cat, + .column-product_tag { + width: 11% !important; + } + + .column-featured, + .column-product_type { + width: 48px; + text-align: left !important; + } + + .column-customer_message, + .column-order_notes { + width: 48px; + text-align: center; + + img { + margin: 0 auto; + padding-top: 0 !important; + } + } + + .manage-column.column-featured img, + .manage-column.column-product_type img { + padding-left: 2px; + } + + .column-price .woocommerce-price-suffix { + display: none; + } + + img { + margin: 1px 2px; + } + + .row-actions { + color: #999; + } + + .row-actions span.id { + padding-top: 8px; + } + + td.column-thumb img { + margin: 0; + width: auto; + height: auto; + max-width: 40px; + max-height: 40px; + vertical-align: middle; + } + + span.na { + color: #999; + } + + .column-sku { + width: 10%; + } + + .column-price { + width: 10ch; + } + + .column-is_in_stock { + text-align: left !important; + width: 12ch; + } + + span.wc-image, + span.wc-featured { + + @include ir(); + margin: 0 auto; + + &::before { + + @include icon_dashicons("\f128"); + } + } + + span.wc-featured { + + &::before { + content: "\f155"; + } + + &.not-featured::before { + content: "\f154"; + } + } + + td.column-featured span.wc-featured { + font-size: 1.6em; + cursor: pointer; + } + + mark { + + &.instock, + &.outofstock, + &.onbackorder { + font-weight: 700; + background: transparent none; + line-height: 1; + } + + &.instock { + color: $green; + } + + &.outofstock { + color: #a44; + } + + &.onbackorder { + color: #eaa600; + } + } + + .order-notes_head, + .notes_head, + .status_head { + + @include ir(); + margin: 0 auto; + + &::after { + + @include icon; + } + } + + .order-notes_head::after { + content: "\e028"; + } + + .notes_head::after { + content: "\e026"; + } + + .status_head::after { + content: "\e011"; + } + + .column-order_items { + width: 12%; + + table.order_items { + width: 100%; + margin: 3px 0 0; + padding: 0; + display: none; + + td { + border: 0; + margin: 0; + padding: 0 0 3px; + } + + td.qty { + color: #999; + padding-right: 6px; + text-align: left; + } + } + } +} + +mark.notice { + background: #fff; + color: $red; + margin: 0 0 0 10px; +} + +a.export_rates, +a.import_rates { + float: right; + margin-left: 9px; + margin-top: -2px; + margin-bottom: 0; +} + +#rates-search { + float: right; + + input.wc-tax-rates-search-field { + padding: 4px 8px; + font-size: 1.2em; + } +} + +#rates-pagination { + float: right; + margin-right: 0.5em; + + .tablenav { + margin: 0; + } +} + +.wc_input_table_wrapper { + overflow-x: auto; + display: block; +} + +table.wc_tax_rates, +table.wc_input_table { + width: 100%; + + th, + td { + display: table-cell !important; + } + + span.tips { + color: $blue; + } + + th { + white-space: nowrap; + padding: 10px; + } + + td { + padding: 0; + border-right: 1px solid #dfdfdf; + border-bottom: 1px solid #dfdfdf; + border-top: 0; + background: #fff; + cursor: default; + + input[type="text"], + input[type="number"] { + width: 100% !important; + min-width: 100px; + padding: 8px 10px; + margin: 0; + border: 0; + outline: 0; + background: transparent none; + + &:focus { + outline: 0; + box-shadow: none; + } + } + + &.compound, + &.apply_to_shipping { + padding: 5px 7px; + vertical-align: middle; + + input { + padding: 0; + } + } + } + + td:last-child { + border-right: 0; + } + + tr.current td { + background-color: #fefbcc; + } + + .item_cost, + .cost { + text-align: right; + + input { + text-align: right; + } + } + + th.sort { + width: 17px; + padding: 0 4px; + } + + td.sort { + padding: 0 4px; + } + + .ui-sortable:not(.ui-sortable-disabled) td.sort { + cursor: move; + font-size: 15px; + background: #f9f9f9; + text-align: center; + vertical-align: middle; + + &::before { + content: "\f333"; + font-family: "Dashicons"; + text-align: center; + line-height: 1; + color: #999; + display: block; + width: 17px; + float: left; + height: 100%; + } + + &:hover::before { + color: #333; + } + } + + .button { + float: left; + margin-right: 5px; + } + + .export, + .import { + float: right; + margin-right: 0; + margin-left: 5px; + } + + span.tips { + padding: 0 3px; + } + + .pagination { + float: right; + + .button { + margin-left: 5px; + margin-right: 0; + } + + .current { + background: #bbb; + text-shadow: none; + } + } + + tr:last-child td { + border-bottom: 0; + } +} + +table.wc_tax_rates { + + td.country { + position: relative; + } +} + +table.wc_gateways, +table.wc_emails, +table.wc_shipping { + position: relative; + + th, + td { + display: table-cell !important; + padding: 1em !important; + vertical-align: top; + line-height: 1.75em; + } + + &.wc_emails td { + vertical-align: middle; + } + + tr:nth-child(odd) td { + background: #f9f9f9; + } + + td.name { + font-weight: 700; + } + + .settings { + text-align: right; + } + + .radio, + .default, + .status { + text-align: center; + + .tips { + margin: 0 auto; + } + + input { + margin: 0; + } + } + + td.sort { + font-size: 15px; + text-align: center; + + .wc-item-reorder-nav { + white-space: nowrap; + width: 72px; + + &::before { + content: "\f333"; + font-family: "Dashicons"; + text-align: center; + line-height: 1; + color: #999; + display: block; + width: 24px; + float: left; + height: 100%; + line-height: 24px; + cursor: move; + } + + button { + position: relative; + overflow: hidden; + float: left; + display: block; + width: 24px; + height: 24px; + margin: 0; + background: transparent; + border: none; + box-shadow: none; + color: #82878c; + text-indent: -9999px; + cursor: pointer; + outline: none; + } + + button::before { + display: inline-block; + position: absolute; + top: 0; + right: 0; + width: 100%; + height: 100%; + font: normal 20px/23px dashicons; + text-align: center; + text-indent: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + button:hover, + button:focus { + color: #191e23; + } + + .wc-move-down::before { + content: "\f347"; + } + + .wc-move-up::before { + content: "\f343"; + } + + .wc-move-disabled { + color: #d5d5d5 !important; + cursor: default; + pointer-events: none; + } + } + } + + .wc-payment-gateway-method-name { + font-weight: normal; + } + + .wc-email-settings-table-name { + font-weight: 700; + + span { + font-weight: normal; + color: #999; + margin: 0 0 0 4px !important; + } + } + + .wc-payment-gateway-method-toggle-enabled, + .wc-payment-gateway-method-toggle-disabled { + padding-top: 1px; + display: block; + outline: 0; + box-shadow: none; + } + + .wc-email-settings-table-status { + text-align: center; + width: 1em; + + .tips { + margin: 0 auto; + } + } +} + +.wc-shipping-zone-settings { + + th { + padding: 24px 24px 24px 0; + } + + td.forminp { + + input, + textarea { + padding: 8px; + max-width: 100% !important; + } + + .wc-shipping-zone-region-select { + width: 448px; + max-width: 100% !important; + + .select2-choices { + padding: 8px 8px 4px; + border-color: #ddd; + min-height: 0; + line-height: 1; + + input { + padding: 0; + } + + li { + margin: 0 4px 4px 0; + } + } + } + } + + .wc-shipping-zone-postcodes-toggle { + margin: 0.5em 0 0; + font-size: 0.9em; + text-decoration: underline; + display: block; + } + + .wc-shipping-zone-postcodes-toggle + .wc-shipping-zone-postcodes { + display: none; + } + + .wc-shipping-zone-postcodes { + + textarea { + margin: 10px 0; + } + + .description { + font-size: 0.9em; + color: #999; + } + } +} + +.wc-shipping-zone-settings + p.submit { + margin-top: 0; +} + +.wc-shipping-zone-settings tbody { + display: table-row-group; +} + +table { + + tr, + tr:hover { + + table.wc-shipping-zone-methods { + + tr .row-actions { + position: relative; + } + + tr:hover .row-actions { + position: static; + } + } + } +} + +.wc-shipping-zones-heading .page-title-action { + display: inline-block; +} + +table.wc-shipping-zones, +table.wc-shipping-zone-methods, +table.wc-shipping-classes { + + td, + th { + vertical-align: top; + line-height: 24px; + padding: 1em !important; + font-size: 14px; + background: #fff; + display: table-cell !important; + + li { + line-height: 24px; + font-size: 14px; + } + + .woocommerce-help-tip { + margin: 0 !important; + } + } + + thead { + + th { + vertical-align: middle; + } + + .wc-shipping-zone-sort { + text-align: center; + } + } + + td.wc-shipping-zones-blank-state, + td.wc-shipping-zone-method-blank-state { + background: #f7f1f6 !important; + overflow: hidden; + position: relative; + padding: 7.5em 7.5% !important; + border-bottom: 2px solid #eee2ec; + + &.wc-shipping-zone-method-blank-state { + padding: 2em !important; + + p { + margin-bottom: 0; + } + } + + p, + li { + color: #a46497; + font-size: 1.5em; + line-height: 1.5em; + margin: 0 0 1em; + position: relative; + z-index: 1; + text-shadow: 1px 1px 1px white; + + &.main { + font-size: 2em; + } + } + + li { + margin-left: 1em; + list-style: circle inside; + } + + &::before { + content: "\e01b"; + font-family: "WooCommerce"; + text-align: center; + line-height: 1; + color: #eee2ec; + display: block; + width: 1em; + font-size: 40em; + top: 50%; + right: -3.75%; + margin-top: -0.1875em; + position: absolute; + } + + .button-primary { + background-color: #804877; + border-color: #804877; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.2), + 0 1px 0 rgba(0, 0, 0, 0.15); + margin: 0; + opacity: 1; + text-shadow: + 0 -1px 1px #8a4f7f, + 1px 0 1px #8a4f7f, + 0 1px 1px #8a4f7f, + -1px 0 1px #8a4f7f; + font-size: 1.5em; + padding: 0.75em 1em; + height: auto; + position: relative; + z-index: 1; + } + } + + .wc-shipping-zone-method-rows { + + tr:nth-child(even) td { + background: #f9f9f9; + } + } + + tr.odd, + .wc-shipping-class-rows tr:nth-child(odd) { + + td { + background: #f9f9f9; + } + } + + tbody.wc-shipping-zone-rows { + + td { + border-top: 2px solid #f9f9f9; + } + + tr:first-child { + + td { + border-top: 0; + } + } + } + + tr.wc-shipping-zone-worldwide { + + td { + background: #f9f9f9; + border-top: 2px solid #e1e1e1; + } + } + + ul, + p { + margin: 0; + } + + td.wc-shipping-zone-sort, + td.wc-shipping-zone-method-sort { + cursor: move; + font-size: 15px; + text-align: center; + + &::before { + content: "\f333"; + font-family: "Dashicons"; + text-align: center; + line-height: 1; + color: #999; + display: block; + width: 17px; + float: left; + height: 100%; + line-height: 24px; + } + + &:hover::before { + color: #333; + } + } + + td.wc-shipping-zone-worldwide { + text-align: center; + + &::before { + content: "\f319"; + font-family: "dashicons"; + text-align: center; + line-height: 1; + color: #999; + display: block; + width: 17px; + float: left; + height: 100%; + line-height: 24px; + } + } + + .wc-shipping-zone-name, + .wc-shipping-zone-methods { + width: 25%; + } + + .wc-shipping-class-description, + .wc-shipping-class-name, + .wc-shipping-class-slug, + .wc-shipping-zone-name, + .wc-shipping-zone-region { + + input, + select, + textarea { + width: 100%; + } + + a.wc-shipping-zone-delete, + a.wc-shipping-class-delete { + color: #a00; + } + + a.wc-shipping-zone-delete:hover, + a.wc-shipping-class-delete:hover { + color: red; + } + } + + .wc-shipping-class-count { + text-align: center; + } + + td.wc-shipping-zone-methods { + color: #555; + + .method_disabled { + text-decoration: line-through; + } + + ul { + position: relative; + padding-right: 32px; + + li { + color: #555; + display: inline; + margin: 0; + } + + li::before { + content: ", "; + } + + li:first-child::before { + content: ""; + } + } + + .add_shipping_method { + display: block; + width: 24px; + padding: 24px 0 0; + height: 0; + overflow: hidden; + cursor: pointer; + + &::before { + + @include icon; + font-family: "Dashicons"; + content: "\f502"; + color: #999; + vertical-align: middle; + line-height: 24px; + font-size: 16px; + margin: 0; + } + + &.disabled { + cursor: not-allowed; + + &::before { + color: #ccc; + } + } + } + } + + .wc-shipping-zone-method-title { + width: 25%; + + .wc-shipping-zone-method-delete { + color: red; + } + } + + .wc-shipping-zone-method-enabled { + text-align: center; + + a { + display: inline-block; + } + + .woocommerce-input-toggle { + margin-top: 3px; + } + } + + .wc-shipping-zone-method-type { + display: block; + } + + tfoot { + + input, + select { + vertical-align: middle !important; + } + + .button-secondary { + float: right; + } + } + + .editing { + + .wc-shipping-zone-view, + .wc-shipping-zone-edit { + display: none; + } + } +} + +.woocommerce-input-toggle { + height: 16px; + width: 32px; + border: 2px solid #935687; + background-color: #935687; + display: inline-block; + text-indent: -9999px; + border-radius: 10em; + position: relative; + margin-top: -1px; + vertical-align: text-top; + + &::before { + content: ""; + display: block; + width: 16px; + height: 16px; + background: #fff; + position: absolute; + top: 0; + right: 0; + border-radius: 100%; + } + + &.woocommerce-input-toggle--disabled { + border-color: #999; + background-color: #999; + + &::before { + right: auto; + left: 0; + } + } + + &.woocommerce-input-toggle--loading { + opacity: 0.5; + } +} + +.wc-modal-shipping-method-settings { + background: #f8f8f8; + padding: 1em !important; + + form .form-table { + width: 100%; + background: #fff; + margin: 0 0 1.5em; + + tr { + + th { + width: 30%; + position: relative; + + .woocommerce-help-tip { + float: right; + margin: -8px -0.5em 0 0; + vertical-align: middle; + right: 0; + top: 50%; + position: absolute; + } + } + + td { + + input, + select, + textarea { + width: 50%; + min-width: 250px; + } + + input[type="checkbox"] { + width: auto; + min-width: 16px; + } + } + + td, + th { + vertical-align: middle; + margin: 0; + line-height: 24px; + padding: 1em; + border-bottom: 1px solid #f8f8f8; + } + } + + &:last-of-type { + margin-bottom: 0; + } + } +} + +.wc-backbone-modal .wc-shipping-zone-method-selector { + + p { + margin-top: 0; + } + + .wc-shipping-zone-method-description { + margin: 0.75em 1px 0; + line-height: 1.5em; + color: #999; + font-style: italic; + } + + select { + width: 100%; + cursor: pointer; + } +} + +img.help_tip { + margin: 0 0 0 9px; + vertical-align: middle; +} + +.postbox img.help_tip { + margin-top: 0; +} + +.postbox .woocommerce-help-tip { + margin: 0 0 0 9px; +} + +.status-enabled, +.status-manual, +.status-disabled { + font-size: 1.4em; + + @include ir(); +} + +.status-manual::before { + + @include icon("\e008"); + color: #999; +} + +.status-enabled::before { + + @include icon("\e015"); + color: $woocommerce; +} + +.status-disabled::before { + + @include icon("\e013"); + color: #ccc; +} + +.woocommerce { + + h2.woo-nav-tab-wrapper { + margin-bottom: 1em; + } + + nav.woo-nav-tab-wrapper { + margin: 1.5em 0 1em; + } + + .subsubsub:not(.list-table-filters) { + margin: -8px 0 0; + } + + .wc-admin-breadcrumb { + margin-left: 0.5em; + + a { + color: #a46497; + } + } + + #template div { + margin: 0; + + p .button { + float: right; + margin-left: 10px; + margin-top: -4px; + } + + .editor textarea { + margin-bottom: 8px; + } + } + + textarea[disabled="disabled"] { + background: #dfdfdf !important; + } + + table.form-table { + margin: 0; + position: relative; + table-layout: fixed; + + .forminp-radio ul { + margin: 0; + + li { + line-height: 1.4em; + } + } + + input[type="text"], + input[type="number"], + input[type="email"] { + height: auto; + } + + textarea.input-text { + height: 100%; + min-width: 150px; + display: block; + } + + // Give regular settings inputs a standard width and padding. + textarea, + input[type="text"], + input[type="email"], + input[type="number"], + input[type="password"], + input[type="datetime"], + input[type="datetime-local"], + input[type="date"], + input[type="time"], + input[type="week"], + input[type="url"], + input[type="tel"], + input.regular-input { + width: 400px; + margin: 0; + padding: 6px; + box-sizing: border-box; + vertical-align: top; + } + + input[type="datetime-local"], + input[type="date"], + input[type="time"], + input[type="week"], + input[type="tel"] { + width: 200px; + } + + select { + width: 400px; + margin: 0; + box-sizing: border-box; + line-height: 32px; + vertical-align: top; + } + + input[size] { + width: auto !important; + } + + // Ignore nested inputs. + table { + + select, + textarea, + input[type="text"], + input[type="email"], + input[type="number"], + input.regular-input { + width: auto; + } + } + + textarea.wide-input { + width: 100%; + } + + img.help_tip, + .woocommerce-help-tip { + padding: 0; + margin: -4px 0 0 5px; + vertical-align: middle; + cursor: help; + line-height: 1; + } + + span.help_tip { + cursor: help; + color: $blue; + } + + th { + position: relative; + padding-right: 24px; + } + + th label { + position: relative; + display: block; + + img.help_tip, + .woocommerce-help-tip { + margin: -8px -24px 0 0; + position: absolute; + right: 0; + top: 50%; + } + } + + th label + .woocommerce-help-tip { + margin: 0 0 0 0; + position: absolute; + right: 0; + top: 20px; + } + + .select2-container { + vertical-align: top; + margin-bottom: 3px; + } + + .select2-container + span.description { + display: block; + margin-top: 8px; + } + + table.widefat th { + padding-right: inherit; + } + + .wp-list-table .woocommerce-help-tip { + float: none; + } + + fieldset { + margin-top: 4px; + + img.help_tip, + .woocommerce-help-tip { + margin: -3px 0 0 5px; + } + + p.description { + margin-bottom: 8px; + } + + &:first-child { + margin-top: 0; + } + } + + .iris-picker { + z-index: 100; + display: none; + position: absolute; + border: 1px solid #ccc; + border-radius: 3px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + + .ui-slider { + border: 0 !important; + margin: 0 !important; + width: auto !important; + height: auto !important; + background: none transparent !important; + + .ui-slider-handle { + margin-bottom: 0 !important; + } + } + } + + .iris-error { + background-color: #ffafaf; + } + + .colorpickpreview { + padding: 7px 0; + line-height: 1em; + display: inline-block; + width: 26px; + border: 1px solid #ddd; + font-size: 14px; + } + + .image_width_settings { + vertical-align: middle; + + label { + margin-left: 10px; + } + + input { + width: auto; + } + } + + .wc_payment_gateways_wrapper, + .wc_emails_wrapper { + padding: 0 15px 10px 0; + } + } + + .wc-shipping-zone-settings { + + td.forminp { + + input, + textarea { + width: 448px; + padding: 6px 11px; + } + + .select2-search input { + padding: 6px; + } + } + } +} + +.wc-wp-version-gte-53 { + + .woocommerce { + + h2.wc-table-list-header { + margin: 1em 0 0.35em 0; + } + + input + .subsubsub { + margin: 8px 0 0; + } + + table.form-table { + // Give regular settings inputs a standard width and padding. + textarea, + input[type="text"], + input[type="email"], + input[type="number"], + input[type="password"], + input[type="datetime"], + input[type="datetime-local"], + input[type="date"], + input[type="time"], + input[type="week"], + input[type="url"], + input[type="tel"], + input.regular-input { + padding: 0 8px; + + @media only screen and (max-width: 782px) { + width: 100%; + } + } + + select { + + @media only screen and (max-width: 782px) { + width: 100%; + } + } + + th label { + + img.help_tip, + .woocommerce-help-tip { + margin: -7px -24px 0 0; + + @media only screen and (max-width: 782px) { + right: auto; + margin-left: 5px; + } + } + } + + .forminp-color { + font-size: 0; + } + + .colorpickpreview { + padding: 0; + width: 30px; + height: 30px; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2); + font-size: 16px; + border-radius: 4px; + margin-right: 3px; + + @media only screen and (max-width: 782px) { + float: left; + width: 40px; + height: 40px; + } + } + } + } +} + +.woocommerce #tabs-wrap table a.remove { + margin-left: 4px; +} + +.woocommerce #tabs-wrap table p { + margin: 0 0 4px !important; + overflow: hidden; + zoom: 1; +} + +.woocommerce #tabs-wrap table p a.add { + float: left; +} + +#wp-excerpt-editor-container { + background: #fff; +} + +#product_variation-parent #parent_id { + width: 100%; +} + +#postimagediv img { + border: 1px solid #d5d5d5; + max-width: 100%; +} + +#woocommerce-product-images .inside { + margin: 0; + padding: 0; + + .add_product_images { + padding: 0 12px 12px; + } + + #product_images_container { + padding: 0 0 0 9px; + + ul { + + @include clearfix(); + margin: 0; + padding: 0; + + li.image, + li.add, + li.wc-metabox-sortable-placeholder { + width: 80px; + float: left; + cursor: move; + border: 1px solid #d5d5d5; + margin: 9px 9px 0 0; + background: #f7f7f7; + + @include border-radius(2px); + position: relative; + box-sizing: border-box; + + img { + width: 100%; + height: auto; + display: block; + } + } + + li.wc-metabox-sortable-placeholder { + border: 3px dashed #ddd; + position: relative; + + &::after { + + @include icon_dashicons("\f161"); + font-size: 2.618em; + line-height: 72px; + color: #ddd; + } + } + + ul.actions { + position: absolute; + top: -8px; + right: -8px; + padding: 2px; + display: none; + + @media (max-width: 768px) { + display: block; + } + + li { + float: right; + margin: 0 0 0 2px; + + a { + width: 1em; + height: 1em; + margin: 0; + height: 0; + display: block; + overflow: hidden; + + &.tips { + cursor: pointer; + } + } + + a.delete { + + @include ir(); + font-size: 1.4em; + + &::before { + + @include icon_dashicons("\f153"); + color: #999; + background: #fff; + border-radius: 50%; + height: 1em; + width: 1em; + line-height: 1em; + } + + &:hover::before { + color: $red; + } + } + } + } + + li:hover ul.actions { + display: block; + } + } + } +} + +#woocommerce-product-data { + + .hndle { + padding: 10px; + + span { + display: block; + line-height: 24px; + } + + .type_box { + display: inline; + line-height: inherit; + vertical-align: baseline; + } + + select { + margin: 0; + } + + label { + padding-right: 1em; + font-size: 12px; + vertical-align: baseline; + } + + label:first-child { + margin-right: 1em; + border-right: 1px solid #dfdfdf; + } + + input, + select { + margin-top: -3px 0 0; + vertical-align: middle; + } + + select { + margin-left: 0.5em; + } + } + + > .handlediv { + margin-top: 4px; + } + + .wrap { + margin: 0; + } +} + +#woocommerce-coupon-description { + padding: 3px 8px; + font-size: 1.7em; + line-height: 1.42em; + height: auto; + width: 100%; + outline: 0; + margin: 10px 0; + display: block; + + &::-webkit-input-placeholder { + line-height: 1.42em; + color: #bbb; + } + + &::-moz-placeholder { + line-height: 1.42em; + color: #bbb; + } + + &:-ms-input-placeholder { + line-height: 1.42em; + color: #bbb; + } + + &:-moz-placeholder { + line-height: 1.42em; + color: #bbb; + } +} + +#woocommerce-product-data, +#woocommerce-coupon-data { + + .panel-wrap { + background: #fff; + } + + .woocommerce_options_panel, + .wc-metaboxes-wrapper { + float: left; + width: 80%; + + .wc-radios { + display: block; + float: left; + margin: 0; + + li { + display: block; + padding: 0 0 10px; + + input { + width: auto; + } + } + } + } +} + +#woocommerce-product-data, +#woocommerce-coupon-data, +.woocommerce { + + .panel-wrap { + overflow: hidden; + } + + ul.wc-tabs { + margin: 0; + width: 20%; + float: left; + line-height: 1em; + padding: 0 0 10px; + position: relative; + background-color: #fafafa; + border-right: 1px solid #eee; + box-sizing: border-box; + + &::after { + content: ""; + display: block; + width: 100%; + height: 9999em; + position: absolute; + bottom: -9999em; + left: 0; + background-color: #fafafa; + border-right: 1px solid #eee; + } + + li { + margin: 0; + padding: 0; + display: block; + position: relative; + + a { + margin: 0; + padding: 10px; + display: block; + box-shadow: none; + text-decoration: none; + line-height: 20px !important; + border-bottom: 1px solid #eee; + + span { + margin-left: 0.618em; + margin-right: 0.618em; + } + + &::before { + + @include iconbeforedashicons("\f107"); + } + } + + &.general_options a::before { + content: "\f107"; + } + + &.inventory_options a::before { + content: "\f481"; + } + + &.shipping_options a::before { + font-family: "WooCommerce"; + content: "\e01a"; + } + + &.linked_product_options a::before { + content: "\f103"; + } + + &.attribute_options a::before { + content: "\f175"; + } + + &.advanced_options a::before { + font-family: "Dashicons"; + content: "\f111"; + } + + &.marketplace-suggestions_options a::before { + content: none; + } + + &.variations_options a::before { + content: "\f509"; + } + + &.usage_restriction_options a::before { + font-family: "WooCommerce"; + content: "\e602"; + } + + &.usage_limit_options a::before { + font-family: "WooCommerce"; + content: "\e601"; + } + + &.general_coupon_data a::before { + font-family: "WooCommerce"; + content: "\e600"; + } + + &.active a { + color: #555; + position: relative; + background-color: #eee; + } + } + } +} + +/** + * Shipping + */ +.woocommerce_page_wc-settings { + + input[type="url"], + input[type="email"] { + direction: ltr; + } + + .shippingrows { + + th.check-column { + padding-top: 20px; + } + + tfoot th { + padding-left: 10px; + } + + .add.button::before { + + @include iconbefore("\e007"); + } + } + + h3.wc-settings-sub-title { + font-size: 1.2em; + } +} + +#woocommerce-product-data, +#woocommerce-product-type-options, +#woocommerce-order-data, +#woocommerce-order-downloads, +#woocommerce-coupon-data { + + .inside { + margin: 0; + padding: 0; + } +} + +.woocommerce_options_panel, +.panel { + padding: 9px; + color: #555; + + .form-field .woocommerce-help-tip { + font-size: 1.4em; + } +} + +.woocommerce_page_settings .woocommerce_options_panel, +.panel { + padding: 0; +} + +#woocommerce-product-type-options .panel, +#woocommerce-product-specs .inside { + margin: 0; + padding: 9px; +} + +.woocommerce_options_panel p, +#woocommerce-product-type-options .panel p, +.woocommerce_options_panel fieldset.form-field { + margin: 0 0 9px; + font-size: 12px; + padding: 5px 9px; + line-height: 24px; + + &::after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; + } +} + +.woocommerce_options_panel .checkbox, +.woocommerce_variable_attributes .checkbox { + margin: 4px 0 !important; + vertical-align: middle; + float: left; +} + +.woocommerce_variations, +.woocommerce_options_panel { + + .downloadable_files table { + width: 100%; + padding: 0 !important; + + th { + padding: 7px 0 7px 7px !important; + + &.sort { + width: 17px; + padding: 7px !important; + } + + .woocommerce-help-tip { + font-size: 1.1em; + margin-left: 0; + } + } + + td { + vertical-align: middle !important; + padding: 4px 0 4px 7px !important; + position: relative; + + &:last-child { + padding-right: 7px !important; + } + + input.input_text { + width: 100%; + float: none; + min-width: 0; + margin: 1px 0; + } + + &.file_url { + /* Reduce the size of this field to make space for a warning asterisk. */ + input { + display: inline-block; + width: 96%; + } + } + + .upload_file_button { + width: auto; + float: right; + cursor: pointer; + } + + .delete { + + @include ir(); + font-size: 1.2em; + + &::before { + + @include icon_dashicons("\f153"); + color: #999; + } + + &:hover { + + &::before { + color: $red; + } + } + } + } + + td.sort { + width: 17px; + cursor: move; + font-size: 15px; + text-align: center; + background: #f9f9f9; + padding-right: 7px !important; + + &::before { + content: "\f333"; + font-family: "Dashicons"; + text-align: center; + line-height: 1; + color: #999; + display: block; + width: 17px; + float: left; + height: 100%; + } + + &:hover::before { + color: #333; + } + } + + /* Warning asterisk (indicates if there is a problem with a downloadable file). */ + span.disabled { + color: var( --wc-red ); + } + } +} + +.woocommerce_attribute, +.woocommerce_variation { + + h3 .sort { + width: 17px; + height: 26px; + cursor: move; + float: right; + font-size: 15px; + font-weight: 400; + margin-right: 0.5em; + visibility: hidden; + text-align: center; + vertical-align: middle; + + &::before { + content: "\f333"; + font-family: "Dashicons"; + text-align: center; + line-height: 28px; + color: #999; + display: block; + width: 17px; + float: left; + height: 100%; + } + + &:hover::before { + color: #777; + } + } + + h3:hover, + &.ui-sortable-helper { + + .sort { + visibility: visible; + } + } +} + +.woocommerce_options_panel { + min-height: 175px; + box-sizing: border-box; + + .downloadable_files { + padding: 0 9px 0 162px; + position: relative; + margin: 9px 0; + + label { + position: absolute; + left: 0; + margin: 0 0 0 12px; + line-height: 24px; + } + } + + p { + margin: 9px 0; + } + + p.form-field, + fieldset.form-field { + padding: 5px 20px 5px 162px !important; /** Padding for aligning labels left - 12px + 150 label width **/ + } + + .sale_price_dates_fields { + + .short:first-of-type { + margin-bottom: 1em; + } + + .short:nth-of-type(2) { + clear: left; + } + } + + label, + legend { + float: left; + width: 150px; + padding: 0; + margin: 0 0 0 -150px; + + .req { + font-weight: 700; + font-style: normal; + color: $red; + } + } + + .description { + padding: 0; + margin: 0 0 0 7px; + clear: none; + display: inline; + } + + .description-block { + margin-left: 0; + display: block; + } + + textarea, + input, + select { + margin: 0; + } + + textarea { + float: left; + height: 3.5em; + line-height: 1.5em; + vertical-align: top; + } + + input[type="text"], + input[type="email"], + input[type="number"], + input[type="password"] { + width: 50%; + float: left; + } + + input.button { + width: auto; + margin-left: 8px; + } + + select { + float: left; + } + + input[type="text"].short, + input[type="email"].short, + input[type="number"].short, + input[type="password"].short, + .short { + width: 50%; + } + + .sized { + width: auto !important; + margin-right: 6px; + } + + .options_group { + border-top: 1px solid white; + border-bottom: 1px solid #eee; + + &:first-child { + border-top: 0; + } + + &:last-child { + border-bottom: 0; + } + + fieldset { + margin: 9px 0; + font-size: 12px; + padding: 5px 9px; + line-height: 24px; + + label { + width: auto; + float: none; + } + + ul { + float: left; + width: 50%; + margin: 0; + padding: 0; + + li { + margin: 0; + width: auto; + + input { + width: auto; + float: none; + margin-right: 4px; + } + } + } + + ul.wc-radios label { + margin-left: 0; + } + } + } + + .dimensions_field .wrap { + display: block; + width: 50%; + + input { + width: 30.75%; + margin-right: 3.8%; + } + + .last { + margin-right: 0; + } + } + + &.padded { + padding: 1em; + } + + .select2-container { + float: left; + } +} + +#woocommerce-product-data input.dp-applied { + float: left; +} + +#grouped_product_options, +#virtual_product_options, +#simple_product_options { + padding: 12px; + font-style: italic; + color: #666; +} + +/** + * WooCommerce meta boxes + */ +.wc-metaboxes-wrapper { + + .toolbar { + margin: 0 !important; + border-top: 1px solid white; + border-bottom: 1px solid #eee; + padding: 9px 12px !important; + + &:first-child { + border-top: 0; + } + + &:last-child { + border-bottom: 0; + } + + .add_variation { + float: right; + margin-left: 5px; + } + + .save-variation-changes, + .cancel-variation-changes { + float: left; + margin-right: 5px; + } + } + + p.toolbar { + overflow: hidden; + zoom: 1; + } + + .expand-close { + margin-right: 2px; + color: #777; + font-size: 12px; + font-style: italic; + + a { + background: none; + padding: 0; + font-size: 12px; + text-decoration: none; + } + } + + &#product_attributes .expand-close { + float: right; + line-height: 28px; + } + + button.add_variable_attribute, + .fr { + float: right; + margin: 0 0 0 6px; + } + + .wc-metaboxes { + border-bottom: 1px solid #eee; + } + + .wc-metabox-sortable-placeholder { + border-color: #bbb; + background-color: #f5f5f5; + margin-bottom: 9px; + border-width: 1px; + border-style: dashed; + } + + .wc-metabox { + background: #fff; + border-bottom: 1px solid #eee; + margin: 0 !important; + + select { + font-weight: 400; + } + + &:last-of-type { + border-bottom: 0; + } + + .handlediv { + width: 27px; + float: right; + + &::before { + content: "\f142" !important; + cursor: pointer; + display: inline-block; + font: 400 20px/1 "Dashicons"; + line-height: 0.5 !important; + padding: 8px 10px; + position: relative; + right: 12px; + top: 0; + } + } + + &.closed { + + @include border-radius(3px); + + .handlediv::before { + content: "\f140" !important; + } + + h3 { + border: 0; + } + } + + h3 { + margin: 0 !important; + padding: 0.75em 0.75em 0.75em 1em !important; + font-size: 1em !important; + overflow: hidden; + zoom: 1; + cursor: move; + + button, + a.delete { + float: right; + } + + a.delete { + color: red; + font-weight: normal; + line-height: 26px; + text-decoration: none; + position: relative; + visibility: hidden; + } + + strong { + font-weight: normal; + line-height: 26px; + font-weight: 700; + } + + select { + font-family: sans-serif; + max-width: 20%; + margin: 0.25em 0.25em 0.25em 0; + } + + .handlediv { + background-position: 6px 5px !important; + visibility: hidden; + height: 26px; + } + + &.fixed { + cursor: pointer !important; + } + } + + &.woocommerce_attribute h3, + &.woocommerce_variation h3 { + cursor: pointer; + padding: 0.5em 0.75em 0.5em 1em !important; + + a.delete, + .handlediv, + .sort { + margin-top: 0.25em; + } + } + + h3:hover, + &.ui-sortable-helper { + + a.delete, + .handlediv { + visibility: visible; + } + } + + table { + width: 100%; + position: relative; + background-color: #fdfdfd; + padding: 1em; + border-top: 1px solid #eee; + + td { + text-align: left; + padding: 0 6px 1em 0; + vertical-align: top; + border: 0; + + label { + text-align: left; + display: block; + line-height: 21px; + } + + input { + float: left; + min-width: 200px; + } + + input, + textarea { + width: 100%; + margin: 0; + display: block; + font-size: 14px; + padding: 4px; + color: #555; + } + + select, + .select2-container { + width: 100% !important; + } + + input.short { + width: 200px; + } + + input.checkbox { + width: 16px; + min-width: inherit; + vertical-align: text-bottom; + display: inline-block; + float: none; + } + } + + td.attribute_name { + width: 200px; + } + + .plus, + .minus { + margin-top: 6px; + } + + .fl { + float: left; + } + + .fr { + float: right; + } + } + } +} + +.variations-pagenav { + float: right; + line-height: 24px; + + .displaying-num { + color: #777; + font-size: 12px; + font-style: italic; + } + + a { + padding: 0 10px 3px; + background: rgba(0, 0, 0, 0.05); + font-size: 16px; + font-weight: 400; + text-decoration: none; + } + + a.disabled, + a.disabled:active, + a.disabled:focus, + a.disabled:hover { + color: #a0a5aa; + background: rgba(0, 0, 0, 0.05); + } +} + +.variations-defaults { + float: left; + + select { + margin: 0.25em 0.25em 0.25em 0; + } +} + +.woocommerce_variable_attributes { + background-color: #fdfdfd; + border-top: 1px solid #eee; + + .data { + + @include clearfix; + padding: 1em 2em; + } + + .upload_image_button { + display: block; + width: 64px; + height: 64px; + float: left; + margin-right: 20px; + position: relative; + cursor: pointer; + + img { + width: 100%; + height: auto; + display: none; + } + + &::before { + content: "\f128"; + font-family: "Dashicons"; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + text-align: center; + line-height: 64px; + font-size: 64px; + font-weight: 400; + -webkit-font-smoothing: antialiased; + } + + &.remove { + + img { + display: block; + } + + &::before { + content: "\f335"; + display: none; + } + + &:hover::before { + display: block; + } + } + } + + .options { + border: 1px solid #eee; + border-width: 1px 0; + padding: 0.25em 0; + + label { + display: inline-block; + padding: 4px 1em 2px 0; + } + + input[type="checkbox"] { + margin: 0 5px 0 0.5em !important; + vertical-align: middle; + } + } +} + +.form-row { + + label { + display: inline-block; + } + + .woocommerce-help-tip { + float: right; + } + + input[type="text"], + input[type="number"], + input[type="password"], + input[type="color"], + input[type="date"], + input[type="datetime"], + input[type="datetime-local"], + input[type="email"], + input[type="month"], + input[type="search"], + input[type="tel"], + input[type="time"], + input[type="url"], + input[type="week"], + select, + textarea { + width: 100%; + vertical-align: middle; + margin: 2px 0 0; + padding: 5px; + } + + select { + height: 40px; + } + + &.dimensions_field { + + .wrap { + clear: left; + display: block; + } + + input { + width: 33%; + float: left; + vertical-align: middle; + + &:last-of-type { + margin-right: 0; + width: 34%; + } + } + } + + &.form-row-first, + &.form-row-last { + width: 48%; + float: right; + } + + &.form-row-first { + clear: both; + float: left; + } + + &.form-row-full { + clear: both; + } +} + +/** + * Tooltips + */ +.tips { + cursor: help; + text-decoration: none; +} + +img.tips { + padding: 5px 0 0; +} + +#tiptip_holder { + display: none; + z-index: 8675309; + position: absolute; + top: 0; + + /*rtl:ignore*/ + left: 0; + + &.tip_top { + padding-bottom: 5px; + + #tiptip_arrow_inner { + margin-top: -7px; + margin-left: -6px; + border-top-color: #333; + } + } + + &.tip_bottom { + padding-top: 5px; + + #tiptip_arrow_inner { + margin-top: -5px; + margin-left: -6px; + border-bottom-color: #333; + } + } + + &.tip_right { + padding-left: 5px; + + #tiptip_arrow_inner { + margin-top: -6px; + margin-left: -5px; + border-right-color: #333; + } + } + + &.tip_left { + padding-right: 5px; + + #tiptip_arrow_inner { + margin-top: -6px; + margin-left: -7px; + border-left-color: #333; + } + } +} + +#tiptip_content, +.chart-tooltip, +.wc_error_tip { + color: #fff; + font-size: 0.8em; + max-width: 150px; + background: #333; + text-align: center; + border-radius: 3px; + padding: 0.618em 1em; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + + code { + padding: 1px; + background: #888; + } +} + +#tiptip_arrow, +#tiptip_arrow_inner { + position: absolute; + border-color: transparent; + border-style: solid; + border-width: 6px; + height: 0; + width: 0; +} + +/*rtl:raw: + #tiptip_arrow { + right: 50%; + margin-right: -6px; + } + */ + +.wc_error_tip { + max-width: 20em; + line-height: 1.8em; + position: absolute; + white-space: normal; + background: #d82223; + margin: 1.5em 1px 0 -1em; + z-index: 9999999; + + &::after { + content: ""; + display: block; + border: 8px solid #d82223; + border-right-color: transparent; + border-left-color: transparent; + border-top-color: transparent; + position: absolute; + top: -3px; + left: 50%; + margin: -1em 0 0 -3px; + } +} + +/** + * Date picker + */ +img.ui-datepicker-trigger { + vertical-align: middle; + margin-top: -1px; + cursor: pointer; +} + +.woocommerce_options_panel img.ui-datepicker-trigger, +.wc-metabox-content img.ui-datepicker-trigger { + float: left; + margin-right: 8px; + margin-top: 4px; + margin-left: 4px; +} + +#ui-datepicker-div { + display: none; +} + +/** + * Reports + */ +.woocommerce-reports-remove-filter { + color: red; + text-decoration: none; +} + +.woocommerce-reports-wrap, +.woocommerce-reports-wide { + + &.woocommerce-reports-wrap { + margin-left: 300px; + padding-top: 18px; + } + + &.halved { + margin: 0; + overflow: hidden; + zoom: 1; + } + + .widefat th { + padding: 7px; + } + + .widefat td { + vertical-align: top; + padding: 7px; + + .description { + margin: 4px 0 0; + } + } + + .postbox { + + &::after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; + } + + h3 { + cursor: default !important; + } + + .inside { + padding: 10px; + margin: 0 !important; + } + + div.stats_range, + h3.stats_range { + border-bottom-color: #dfdfdf; + margin: 0; + padding: 0 !important; + + .export_csv { + float: right; + line-height: 26px; + border-left: 1px solid #dfdfdf; + padding: 10px; + display: block; + text-decoration: none; + + &::before { + + @include iconbeforedashicons("\f346"); + margin-right: 4px; + } + } + + ul { + list-style: none outside; + margin: 0; + padding: 0; + zoom: 1; + background: #f5f5f5; + border-bottom: 1px solid #ccc; + + &::before, + &::after { + content: " "; + display: table; + } + + &::after { + clear: both; + } + + li { + float: left; + margin: 0; + padding: 0; + line-height: 26px; + font-weight: bold; + font-size: 14px; + + a { + border-right: 1px solid #dfdfdf; + padding: 10px; + display: block; + text-decoration: none; + } + + &.active { + background: #fff; + box-shadow: 0 4px 0 0 #fff; + + a { + color: #777; + } + } + + &.custom { + padding: 9px 10px; + vertical-align: middle; + + form, + div { + display: inline; + margin: 0; + + input.range_datepicker { + padding: 0; + margin: 0 10px 0 0; + background: transparent; + border: 0; + color: #777; + text-align: center; + box-shadow: none; + + &.from { + margin-right: 0; + } + } + } + } + } + } + } + + .chart-with-sidebar { + padding: 12px 12px 12px 249px; + margin: 0 !important; + + .chart-sidebar { + width: 225px; + margin-left: -237px; + float: left; + } + } + + .chart-widgets { + margin: 0; + padding: 0; + + li.chart-widget { + margin: 0 0 1em; + background: #fafafa; + border: 1px solid #dfdfdf; + + &::after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; + } + + h4 { + background: #fff; + border: 1px solid #dfdfdf; + border-left-width: 0; + border-right-width: 0; + padding: 10px; + margin: 0; + color: $blue; + border-top-width: 0; + background-image: linear-gradient(to top, #ececec, #f9f9f9); + + &.section_title:hover { + color: $red; + } + } + + .section_title { + cursor: pointer; + + span { + display: block; + + &::after { + + @include iconafter("\e035"); + float: right; + font-size: 0.9em; + line-height: 1.618; + } + } + + &.open { + color: #333; + + span::after { + display: none; + } + } + } + + .section { + border-bottom: 1px solid #dfdfdf; + + .select2-container { + width: 100% !important; + } + + &:last-of-type { + border-radius: 0 0 3px 3px; + } + } + + table { + width: 100%; + + td { + padding: 7px 10px; + vertical-align: top; + border-top: 1px solid #e5e5e5; + line-height: 1.4em; + } + + tr:first-child td { + border-top: 0; + } + + td.count { + background: #f5f5f5; + } + + td.name { + max-width: 175px; + + a { + word-wrap: break-word; + } + } + + td.sparkline { + vertical-align: middle; + } + + .wc_sparkline { + width: 32px; + height: 1em; + display: block; + float: right; + } + + tr.active td { + background: #f5f5f5; + } + } + + form, + p { + margin: 0; + padding: 10px; + + .submit { + margin-top: 10px; + } + } + + #product_ids { + width: 100%; + } + + .select_all, + .select_none { + float: right; + color: #999; + margin-left: 4px; + margin-top: 10px; + } + + .description { + margin-left: 0.5em; + font-weight: normal; + opacity: 0.8; + } + } + } + + .chart-legend { + list-style: none outside; + margin: 0 0 1em; + padding: 0; + border: 1px solid #dfdfdf; + border-right-width: 0; + border-bottom-width: 0; + background: #fff; + + li { + border-right: 5px solid #aaa; + color: #aaa; + padding: 1em; + display: block; + margin: 0; + transition: all ease 0.5s; + box-shadow: inset 0 -1px 0 0 #dfdfdf; + + strong { + font-size: 1.618em; + line-height: 1.2em; + color: #464646; + font-weight: normal; + display: block; + font-family: + "HelveticaNeue-Light", + "Helvetica Neue Light", + "Helvetica Neue", + sans-serif; + + del { + color: #e74c3c; + font-weight: normal; + } + } + + &:hover { + box-shadow: + inset 0 -1px 0 0 #dfdfdf, + inset 300px 0 0 rgba(156, 93, 144, 0.1); + border-right: 5px solid #9c5d90 !important; + padding-left: 1.5em; + color: #9c5d90; + } + } + } + + .pie-chart-legend { + margin: 12px 0 0; + overflow: hidden; + + li { + float: left; + margin: 0; + padding: 6px 0 0; + border-top: 4px solid #999; + text-align: center; + box-sizing: border-box; + width: 50%; + } + } + + .stat { + font-size: 1.5em !important; + font-weight: 700; + text-align: center; + } + + .chart-placeholder { + width: 100%; + height: 650px; + overflow: hidden; + position: relative; + } + + .chart-prompt { + line-height: 650px; + margin: 0; + color: #999; + font-size: 1.2em; + font-style: italic; + text-align: center; + } + + .chart-container { + background: #fff; + padding: 12px; + position: relative; + border: 1px solid #dfdfdf; + border-radius: 3px; + } + + .main .chart-legend { + margin-top: 12px; + + li { + border-right: 0; + margin: 0 8px 0 0; + float: left; + border-top: 4px solid #aaa; + } + } + } + + .woocommerce-reports-main { + float: left; + min-width: 100%; + + table td { + padding: 9px; + } + } + + .woocommerce-reports-sidebar { + display: inline; + width: 281px; + margin-left: -300px; + clear: both; + float: left; + } + + .woocommerce-reports-left { + width: 49.5%; + float: left; + } + + .woocommerce-reports-right { + width: 49.5%; + float: right; + } +} + +.woocommerce-wide-reports-wrap { + padding-bottom: 11px; + + .widefat { + + .export-data { + float: right; + } + + th, + td { + vertical-align: middle; + padding: 7px; + } + } +} + +form.report_filters { + + p { + vertical-align: middle; + } + + label, + input, + div { + vertical-align: middle; + } +} + +.chart-tooltip { + position: absolute; + display: none; + line-height: 1; +} + +table.bar_chart { + width: 100%; + + thead th { + text-align: left; + color: #ccc; + padding: 6px 0; + } + + tbody { + + th { + padding: 6px 0; + width: 25%; + text-align: left !important; + font-weight: normal !important; + border-bottom: 1px solid #fee; + } + + td { + text-align: right; + line-height: 24px; + padding: 6px 6px 6px 0; + border-bottom: 1px solid #fee; + + span { + color: #8a4b75; + display: block; + } + + span.alt { + color: #47a03e; + margin-top: 6px; + } + } + + td.bars { + position: relative; + text-align: left; + padding: 6px 6px 6px 0; + border-bottom: 1px solid #fee; + + span, + a { + text-decoration: none; + clear: both; + background: #8a4b75; + float: left; + display: block; + line-height: 24px; + height: 24px; + border-radius: 3px; + } + + span.alt { + clear: both; + background: #47a03e; + + span { + margin: 0; + color: #c5dec2 !important; + text-shadow: 0 1px 0 #47a03e; + background: transparent; + } + } + } + } +} + +.post-type-shop_order .woocommerce-BlankState-message::before { + + @include icon("\e01d"); +} + +.post-type-shop_coupon .woocommerce-BlankState-message::before { + + @include icon("\e600"); +} + +.post-type-product .woocommerce-BlankState-message::before { + + @include icon("\e006"); +} + +.woocommerce-BlankState--api .woocommerce-BlankState-message::before { + + @include icon("\e01c"); +} + +.woocommerce-BlankState--webhooks .woocommerce-BlankState-message::before { + + @include icon("\e01b"); +} + +.woocommerce-BlankState { + text-align: center; + padding: 5em 0 0; + + .woocommerce-BlankState-message { + color: #aaa; + margin: 0 auto 1.5em; + line-height: 1.5em; + font-size: 1.2em; + max-width: 500px; + + &::before { + color: #ddd; + text-shadow: + 0 -1px 1px rgba(0, 0, 0, 0.2), + 0 1px 0 rgba(255, 255, 255, 0.8); + font-size: 8em; + display: block; + position: relative !important; + top: auto; + left: auto; + line-height: 1em; + margin: 0 0 0.1875em; + } + } + + .woocommerce-BlankState-cta { + font-size: 1.2em; + padding: 0.75em 1.5em; + margin: 0 0.25em; + height: auto; + display: inline-block !important; + } +} + +.post-type-product .woocommerce-BlankState, +.post-type-shop_order .woocommerce-BlankState { + max-width: 764px; + text-align: center; + margin: auto; + + .woocommerce-BlankState-message { + color: #444; + font-size: 1.5em; + margin: 0 auto 1em; + } + + .woocommerce-BlankState-message::before { + font-size: 120px; + } + + .woocommerce-BlankState-buttons { + margin-bottom: 4em; + } +} + +.post-type-product { + + #wp-pointer-2 .wp-pointer-arrow { + left: 240px; + } + + #wp-pointer-3 .wp-pointer-arrow, + #wp-pointer-4 .wp-pointer-arrow { + left: 46%; + } +} + +/** + * Small screen optimisation + */ +@media only screen and (max-width: 1280px) { + + #order_data { + + .order_data_column { + width: 48%; + + &:first-child { + width: 100%; + } + } + } + + .woocommerce_options_panel { + + .description { + display: block; + clear: both; + margin-left: 0; + } + + .short, + input[type="text"].short, + input[type="email"].short, + input[type="number"].short, + input[type="password"].short, + .dimensions_field .wrap { + width: 80%; + } + } + + .woocommerce_variations, + .woocommerce_options_panel { + + .downloadable_files { + padding: 0; + clear: both; + + label { + position: static; + } + + table { + margin: 0 12px 24px; + width: 94%; + + .sort { + visibility: hidden; + } + } + } + + .woocommerce_variable_attributes .downloadable_files table { + margin: 0 0 1em; + width: 100%; + } + } +} + +/** + * Optimisation for screens 900px and smaller + */ +@media only screen and (max-width: 900px) { + + #woocommerce-coupon-data ul.coupon_data_tabs, + #woocommerce-product-data ul.product_data_tabs, + #woocommerce-product-data .wc-tabs-back { + width: 10%; + } + + #woocommerce-coupon-data .wc-metaboxes-wrapper, + #woocommerce-coupon-data .woocommerce_options_panel, + #woocommerce-product-data .wc-metaboxes-wrapper, + #woocommerce-product-data .woocommerce_options_panel { + width: 90%; + } + + #woocommerce-coupon-data ul.coupon_data_tabs li a, + #woocommerce-product-data ul.product_data_tabs li a { + position: relative; + text-indent: -999px; + padding: 10px; + + &::before { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + text-indent: 0; + text-align: center; + line-height: 40px; + width: 100%; + height: 40px; + } + } +} + +/** + * Optimisation for screens 782px and smaller + */ +@media only screen and (max-width: 782px) { + + #wp-excerpt-media-buttons a { + font-size: 16px; + line-height: 37px; + height: 39px; + padding: 0 20px 0 15px; + } + + #wp-excerpt-editor-tools { + padding-top: 20px; + padding-right: 15px; + overflow: hidden; + margin-bottom: -1px; + } + + #woocommerce-product-data .checkbox { + width: 25px; + } + + .variations-pagenav { + float: none; + text-align: center; + font-size: 18px; + + .displaying-num { + font-size: 16px; + } + + a { + padding: 8px 20px 11px; + font-size: 18px; + } + + select { + padding: 0 20px; + } + } + + .variations-defaults { + float: none; + text-align: center; + margin-top: 10px; + } + + .post-type-product { + + .wp-list-table { + + .column-thumb { + display: none; + text-align: left; + padding-bottom: 0; + + &::before { + display: none !important; + } + + img { + max-width: 32px; + } + } + + .is-expanded td:not(.hidden) { + overflow: visible; + } + + .toggle-row { + top: -28px; + } + } + } + + .post-type-shop_order { + + .wp-list-table { + + .column-customer_message, + .column-order_notes { + text-align: inherit; + } + + .column-order_notes .note-on { + font-size: 1.3em; + margin: 0; + } + + .is-expanded td:not(.hidden) { + overflow: visible; + } + + .toggle-row { + top: -15px; + } + } + } +} + +@media only screen and (max-width: 500px) { + + .woocommerce_options_panel label, + .woocommerce_options_panel legend { + float: none; + width: auto; + display: block; + margin: 0; + } + + .woocommerce_options_panel fieldset.form-field, + .woocommerce_options_panel p.form-field { + padding: 5px 20px !important; + } + + .addons-wcs-banner-block { + flex-direction: column; + } + + .wc-addons-wrap { + + .addons-wcs-banner-block { + padding: 40px; + } + + .addons-wcs-banner-block-image { + padding: 1em; + text-align: center; + width: 100%; + padding: 2em 0; + margin: 0; + + .addons-img { + margin: 0; + } + } + } +} + +/** + * Backbone modal dialog + */ +.wc-backbone-modal { + + * { + box-sizing: border-box; + } + + .wc-backbone-modal-content { + position: fixed; + background: #fff; + z-index: 100000; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + max-width: 100%; + min-width: 500px; + + article { + overflow: auto; + } + } + + &.wc-backbone-modal-shipping-method-settings .wc-backbone-modal-content { + width: 75%; + min-width: 500px; + } + + .select2-container { + width: 100% !important; + } +} + +@media screen and (max-width: 782px) { + + .wc-backbone-modal .wc-backbone-modal-content { + width: 100%; + height: 100%; + min-width: 100%; + } +} + +.wc-backbone-modal-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + min-height: 360px; + background: #000; + opacity: 0.7; + z-index: 99900; +} + +.wc-backbone-modal-main { + padding-bottom: 55px; + + header, + article { + display: block; + position: relative; + } + + .wc-backbone-modal-header { + height: auto; + background: #fcfcfc; + padding: 1em 1.5em; + border-bottom: 1px solid #ddd; + + h1 { + margin: 0; + font-size: 18px; + font-weight: 700; + line-height: 1.5em; + } + + .modal-close-link { + cursor: pointer; + color: #777; + height: 54px; + width: 54px; + padding: 0; + position: absolute; + top: 0; + right: 0; + text-align: center; + border: 0; + border-left: 1px solid #ddd; + background-color: transparent; + transition: color 0.1s ease-in-out, background 0.1s ease-in-out; + + &::before { + font: normal 22px/50px "dashicons" !important; + color: #666; + display: block; + content: "\f335"; + font-weight: 300; + } + + &:hover, + &:focus { + background: #ddd; + border-color: #ccc; + color: #000; + } + + &:focus { + outline: none; + } + } + } + + article { + padding: 1.5em; + + p { + margin: 1.5em 0; + } + + p:first-child { + margin-top: 0; + } + + p:last-child { + margin-bottom: 0; + } + + .pagination { + padding: 10px 0 0; + text-align: center; + } + + table.widefat { + margin: 0; + width: 100%; + border: 0; + box-shadow: none; + + thead th { + padding: 0 1em 1em 1em; + text-align: left; + + &:first-child { + padding-left: 0; + } + + &:last-child { + padding-right: 0; + text-align: right; + } + } + + tbody td, + tbody th { + padding: 1em; + text-align: left; + vertical-align: middle; + + &:first-child { + padding-left: 0; + } + + &:last-child { + padding-right: 0; + text-align: right; + } + + select, + .select2-container { + width: 100%; + } + } + } + } + + footer { + position: absolute; + left: 0; + right: 0; + bottom: 0; + z-index: 100; + padding: 1em 1.5em; + background: #fcfcfc; + border-top: 1px solid #dfdfdf; + box-shadow: 0 -4px 4px -4px rgba(0, 0, 0, 0.1); + + .inner { + text-align: right; + line-height: 23px; + + .button { + margin-bottom: 0; + } + } + } +} + +/** + * Select2 elements. + */ +.select2-drop, +.select2-dropdown { + z-index: 999999 !important; +} + +.select2-results { + line-height: 1.5em; + + .select2-results__option, + .select2-results__group { + margin: 0; + padding: 8px; + } + + .description { + display: block; + color: #999; + padding-top: 4px; + } +} + +.select2-dropdown { + border-color: #ddd; +} + +.select2-dropdown--below { + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); +} + +.select2-dropdown--above { + box-shadow: 0 -1px 1px rgba(0, 0, 0, 0.1); +} + +.select2-container { + + .select2-selection__rendered.ui-sortable li { + cursor: move; + } + + .select2-selection { + border-color: #ddd; + } + + .select2-search__field { + min-width: 150px; + } + + .select2-selection--single { + height: 40px; + + .select2-selection__rendered { + line-height: 40px; + padding-right: 24px; + } + + .select2-selection__arrow { + right: 3px; + height: 36px; + } + } + + .select2-selection--multiple { + min-height: 28px; + border-radius: 0; + line-height: 1.5; + + li { + margin: 0; + } + + .select2-selection__choice { + padding: 2px 6px; + + .description { + display: none; + } + } + } + + .select2-selection__clear { + color: #999; + margin-top: -1px; + z-index: 1; + } + + .select2-search--inline .select2-search__field { + font-family: inherit; + font-size: inherit; + font-weight: inherit; + padding: 3px 0; + } +} + +.woocommerce table.form-table .select2-container { + min-width: 400px !important; +} + +.wc-wp-version-gte-53 { + + .select2-results { + + .select2-results__option, + .select2-results__group { + + &:focus { + outline: none; + } + } + } + + .select2-dropdown { + border-color: #007cba; + + &::after { + position: absolute; + left: 0; + right: 0; + height: 1px; + background: #fff; + content: ""; + } + } + + .select2-dropdown--below { + box-shadow: 0 0 0 1px #007cba, 0 2px 1px rgba(0, 0, 0, 0.1); + + &::after { + top: -1px; + } + } + + .select2-dropdown--above { + box-shadow: 0 0 0 1px #007cba, 0 -2px 1px rgba(0, 0, 0, 0.1); + + &::after { + bottom: -1px; + } + } + + .select2-container { + + @media only screen and (max-width: 782px) { + font-size: 16px; + } + + &:focus { + outline: none; + } + + .select2-selection--single { + height: 30px; + border-color: #7e8993; + + @media only screen and (max-width: 782px) { + height: 40px; + } + + &:focus { + outline: none; + } + + .select2-selection__rendered { + line-height: 28px; + + @media only screen and (max-width: 782px) { + line-height: 38px; + } + + &:hover { + color: #007cba; + } + } + + .select2-selection__arrow { + right: 1px; + height: 28px; + width: 23px; + background: + url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M5%206l5%205%205-5%202%201-7%207-7-7%202-1z%22%20fill%3D%22%23555%22%2F%3E%3C%2Fsvg%3E") + no-repeat right 5px top 55%; + background-size: 16px 16px; + + @media only screen and (max-width: 782px) { + height: 38px; + } + + b { + display: none; + } + } + } + + &.select2-container--focus .select2-selection--single, + &.select2-container--open .select2-selection--single, + &.select2-container--open .select2-selection--multiple { + border-color: #007cba; + box-shadow: 0 0 0 1px #007cba; + } + + .select2-selection--multiple { + min-height: 30px; + border-color: #7e8993; + border-radius: 4px; + } + + .select2-search--inline .select2-search__field { + padding: 0 0 0 3px; + min-height: 28px; + } + } + + .woocommerce table.form-table .select2-container { + + @media only screen and (max-width: 782px) { + min-width: 100% !important; + } + } +} + +.wc-wp-version-gte-55 { + + #woocommerce-product-data { + + .hndle { + display: block; + line-height: 24px; + + .type_box { + display: inline; + line-height: inherit; + vertical-align: baseline; + } + } + } +} + +/** + * Select2 colors for built-in admin color themes. + */ +.admin-color { + $wp_admin_colors: ( + blue: #096484, + coffee: #c7a589, + ectoplasm: #a3b745, + midnight: #e14d43, + ocean: #9ebaa0, + sunrise: #dd823b, + light: #04a4cc, + ); + + @each $name, $color in $wp_admin_colors { + &-#{$name}.wc-wp-version-gte-53 { + + .select2-dropdown { + border-color: $color; + } + + .select2-dropdown--below { + box-shadow: 0 0 0 1px $color, 0 2px 1px rgba(0, 0, 0, 0.1); + } + + .select2-dropdown--above { + box-shadow: 0 0 0 1px $color, 0 -2px 1px rgba(0, 0, 0, 0.1); + } + + .select2-selection--single .select2-selection__rendered:hover { + color: $color; + } + + .select2-container.select2-container--focus + .select2-selection--single, + .select2-container.select2-container--open + .select2-selection--single, + .select2-container.select2-container--open + .select2-selection--multiple { + border-color: $color; + box-shadow: 0 0 0 1px $color; + } + + .select2-container--default + .select2-results__option--highlighted[aria-selected], + .select2-container--default + .select2-results__option--highlighted[data-selected] { + background-color: $color; + } + } + } +} + +.post-type-product .tablenav, +.post-type-shop_order .tablenav { + + .actions { + overflow: visible; + } + + select, + input { + height: 32px; + } + + .select2-container { + float: left; + width: 240px !important; + font-size: 14px; + vertical-align: middle; + margin: 1px 6px 4px 1px; + } +} + +.woocommerce-progress-form-wrapper, +.woocommerce-exporter-wrapper, +.woocommerce-importer-wrapper { + text-align: center; + max-width: 700px; + margin: 40px auto; + + .error { + text-align: left; + } + + .wc-progress-steps { + padding: 0 0 24px; + margin: 0; + list-style: none outside; + overflow: hidden; + color: #ccc; + width: 100%; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + + li { + width: 25%; + float: left; + padding: 0 0 0.8em; + margin: 0; + text-align: center; + position: relative; + border-bottom: 4px solid #ccc; + line-height: 1.4em; + } + + li::before { + content: ""; + border: 4px solid #ccc; + border-radius: 100%; + width: 4px; + height: 4px; + position: absolute; + bottom: 0; + left: 50%; + margin-left: -6px; + margin-bottom: -8px; + background: #fff; + } + + li.active { + border-color: #a16696; + color: #a16696; + + &::before { + border-color: #a16696; + } + } + + li.done { + border-color: #a16696; + color: #a16696; + + &::before { + border-color: #a16696; + background: #a16696; + } + } + } + + .button { + font-size: 1.25em; + padding: 0.5em 1em !important; + line-height: 1.5em !important; + margin-right: 0.5em; + margin-bottom: 2px; + height: auto !important; + border-radius: 4px; + background-color: #bb77ae; + border-color: #a36597; + -webkit-box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0 #a36597; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; + text-shadow: + 0 -1px 1px #a36597, + 1px 0 1px #a36597, + 0 1px 1px #a36597, + -1px 0 1px #a36597; + margin: 0; + opacity: 1; + + &:hover, + &:focus, + &:active { + background: #a36597; + border-color: #a36597; + -webkit-box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0 #a36597; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; + } + } + + .error .button { + font-size: 1em; + } + + .wc-actions { + overflow: hidden; + border-top: 1px solid #eee; + margin: 0; + padding: 23px 24px 24px; + line-height: 3em; + + .button { + float: right; + } + + .woocommerce-importer-toggle-advanced-options { + color: #999; + } + } + + .woocommerce-exporter, + .woocommerce-importer, + .wc-progress-form-content { + background: #fff; + overflow: hidden; + padding: 0; + margin: 0 0 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.13); + color: #555; + text-align: left; + + header { + border-bottom: 1px solid #eee; + margin: 0; + padding: 24px 24px 0; + } + + section { + padding: 24px 24px 0; + } + + h2 { + margin: 0 0 24px; + color: #555; + font-size: 24px; + font-weight: normal; + line-height: 1em; + } + + p { + font-size: 1em; + line-height: 1.75em; + font-size: 16px; + color: #555; + margin: 0 0 24px; + } + + .form-row { + margin-top: 24px; + } + + .spinner { + display: none; + } + + .woocommerce-importer-options th, + .woocommerce-importer-options td, + .woocommerce-exporter-options th, + .woocommerce-exporter-options td { + vertical-align: top; + line-height: 1.75em; + padding: 0 0 24px 0; + + label { + color: #555; + font-weight: normal; + } + + input[type="checkbox"] { + margin: 0 4px 0 0; + padding: 7px; + } + + input[type="text"], + input[type="number"] { + padding: 7px; + height: auto; + margin: 0; + } + + .woocommerce-importer-file-url-field-wrapper { + border: 1px solid #ddd; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.07); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.07); + background-color: #fff; + color: #32373c; + outline: 0; + line-height: 1; + display: block; + + code { + background: none; + font-size: smaller; + padding: 0; + margin: 0; + color: #999; + padding: 7px 0 0 7px; + display: inline-block; + } + + input { + font-family: Consolas, Monaco, monospace; + border: 0; + margin: 0; + outline: 0; + box-shadow: none; + display: inline-block; + min-width: 100%; + } + } + } + + .woocommerce-exporter-options th, + .woocommerce-importer-options th { + width: 35%; + padding-right: 20px; + } + + progress { + width: 100%; + height: 42px; + margin: 0 auto 24px; + display: block; + -webkit-appearance: none; + border: none; + display: none; + background: #f5f5f5; + border: 2px solid #eee; + border-radius: 4px; + padding: 0; + box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.2); + } + + progress::-webkit-progress-bar { + background: transparent none; + border: 0; + border-radius: 4px; + padding: 0; + box-shadow: none; + } + + progress::-webkit-progress-value { + border-radius: 3px; + box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, 0.4); + background: #a46497; + background: linear-gradient(to bottom, #a46497, #66405f), #a46497; + transition: width 1s ease; + } + + progress::-moz-progress-bar { + border-radius: 3px; + box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, 0.4); + background: #a46497; + background: linear-gradient(to bottom, #a46497, #66405f), #a46497; + transition: width 1s ease; + } + + progress::-ms-fill { + border-radius: 3px; + box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, 0.4); + background: #a46497; + background: linear-gradient(to bottom, #a46497, #66405f), #a46497; + transition: width 1s ease; + } + + &.woocommerce-exporter__exporting, + &.woocommerce-importer__importing { + + .spinner { + display: block; + } + + progress { + display: block; + } + + .wc-actions, + .woocommerce-exporter-options { + display: none; + } + } + + .wc-importer-mapping-table-wrapper, + .wc-importer-error-log { + padding: 0; + } + + .wc-importer-mapping-table, + .wc-importer-error-log-table { + margin: 0; + border: 0; + box-shadow: none; + width: 100%; + table-layout: fixed; + + td, + th { + border: 0; + padding: 12px; + vertical-align: middle; + word-wrap: break-word; + + select { + width: 100%; + } + } + + tbody tr:nth-child(odd) td, + tbody tr:nth-child(odd) th { + background: #fbfbfb; + } + + th { + font-weight: bold; + } + + td:first-child, + th:first-child { + padding-left: 24px; + } + + td:last-child, + th:last-child { + padding-right: 24px; + } + + .wc-importer-mapping-table-name { + width: 50%; + + .description { + color: #999; + margin-top: 4px; + display: block; + + code { + background: none; + padding: 0; + white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */ + word-wrap: break-word; /* IE */ + word-break: break-all; + } + } + } + } + + .woocommerce-importer-done { + text-align: center; + padding: 48px 24px; + font-size: 1.5em; + line-height: 1.75em; + + &::before { + + @include icon("\e015"); + color: #a16696; + position: static; + font-size: 100px; + display: block; + float: none; + margin: 0 0 24px; + } + } + } +} + +.wc-pointer { + + .wc-pointer-buttons { + + .close { + float: left; + margin: 6px 0 0 15px; + } + } +} + +.wc-quick-edit-warning { + color: darkred; + font-weight: bold; +} + +.wc-addons__empty { + margin: 48px auto; + max-width: 640px; + + h2 { + font-size: 20px; + font-weight: 400; + line-height: 1.2; + } + + p { + font-size: 16px; + line-height: 1.5; + } +} + +@media screen and (min-width: 600px) { + + .wc-addons-wrap { + + .marketplace-header { + padding-left: 84px; + } + + .storefront { + + h2 { + margin-top: 0; + } + + img { + float: left; + margin: 0 16px 0 auto; + width: 278px; + } + } + } + + .marketplace-header__tab { + flex: none; + } +} + +@media screen and (min-width: 961px) { + + .marketplace-header__tabs { + margin-left: 84px; + } +} + +@media screen and (min-width: 1024px) { + + .current-section-name { + display: none; + } + + .wc-addons-wrap { + .current-section-dropdown__title { + display: block; + font-size: 20px; + font-weight: 400; + line-height: 24px; + margin: 0 0 16px; + } + + .current-section-dropdown { + background: none; + border: none; + margin-bottom: 32px; + width: 100%; + + ul { + background: none; + border: none; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; + margin-top: -12px; + padding: 0; + position: static; + + li { + background: #fff; + border: 1px solid #ccc; + border-radius: 32px; + font-size: 14px; + line-height: 20px; + margin: 12px 12px 0 0; + + &.current { + background: #007cba; + border: 1px solid #007cba; + + a { + color: #fff; + } + + a::after { + background: none; + } + } + } + + a, + a:visited, + a:hover, + a:active { + color: #2c3338; + padding: 10px 16px !important; + } + } + + li:last-child { + a::after { + display: none; + } + } + } + } +} diff --git a/assets/css/auth.scss b/plugins/woocommerce/legacy/css/auth.scss similarity index 100% rename from assets/css/auth.scss rename to plugins/woocommerce/legacy/css/auth.scss diff --git a/plugins/woocommerce/legacy/css/dashboard-setup.scss b/plugins/woocommerce/legacy/css/dashboard-setup.scss new file mode 100644 index 00000000000..dba246e1f18 --- /dev/null +++ b/plugins/woocommerce/legacy/css/dashboard-setup.scss @@ -0,0 +1,52 @@ +/** + * dashboard-setup.scss + * Styles for WooCommerce dashboard finish setup widgets + * only loaded on the dashboard itself. + */ + +/** + * Styling begins + */ + +.dashboard-widget-finish-setup { + + .progress-wrapper { + border: 1px solid #757575; + border-radius: 16px; + font-size: 0.9em; + padding: 2px 8px 2px 8px; + display: inline-block; + box-sizing: border-box; + } + + .progress-wrapper > span { + position: relative; + top: -3px; + color: #757575; + } + + .description div { + margin-top: 11px; + float: left; + width: 70%; + } + + .description img { + float: right; + width: 30%; + } + + .circle-progress { + margin-top: 1px; + margin-left: -3px; + + circle { + stroke: #f0f0f0; + stroke-width: 1px; + } + + .bar { + stroke: #949494; + } + } +} diff --git a/assets/css/dashboard.scss b/plugins/woocommerce/legacy/css/dashboard.scss similarity index 100% rename from assets/css/dashboard.scss rename to plugins/woocommerce/legacy/css/dashboard.scss diff --git a/assets/css/helper.scss b/plugins/woocommerce/legacy/css/helper.scss similarity index 100% rename from assets/css/helper.scss rename to plugins/woocommerce/legacy/css/helper.scss diff --git a/assets/css/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png b/plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png similarity index 100% rename from assets/css/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png rename to plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png diff --git a/assets/css/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png b/plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png similarity index 100% rename from assets/css/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png rename to plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png diff --git a/assets/css/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png b/plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png similarity index 100% rename from assets/css/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png rename to plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png diff --git a/assets/css/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png b/plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png similarity index 100% rename from assets/css/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png rename to plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png diff --git a/assets/css/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png b/plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png similarity index 100% rename from assets/css/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png rename to plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png diff --git a/assets/css/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png b/plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png similarity index 100% rename from assets/css/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png rename to plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png diff --git a/assets/css/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png b/plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png similarity index 100% rename from assets/css/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png rename to plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png diff --git a/assets/css/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png similarity index 100% rename from assets/css/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png rename to plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png diff --git a/assets/css/jquery-ui/images/ui-icons_222222_256x240.png b/plugins/woocommerce/legacy/css/jquery-ui/images/ui-icons_222222_256x240.png similarity index 100% rename from assets/css/jquery-ui/images/ui-icons_222222_256x240.png rename to plugins/woocommerce/legacy/css/jquery-ui/images/ui-icons_222222_256x240.png diff --git a/assets/css/jquery-ui/images/ui-icons_2e83ff_256x240.png b/plugins/woocommerce/legacy/css/jquery-ui/images/ui-icons_2e83ff_256x240.png similarity index 100% rename from assets/css/jquery-ui/images/ui-icons_2e83ff_256x240.png rename to plugins/woocommerce/legacy/css/jquery-ui/images/ui-icons_2e83ff_256x240.png diff --git a/assets/css/jquery-ui/images/ui-icons_454545_256x240.png b/plugins/woocommerce/legacy/css/jquery-ui/images/ui-icons_454545_256x240.png similarity index 100% rename from assets/css/jquery-ui/images/ui-icons_454545_256x240.png rename to plugins/woocommerce/legacy/css/jquery-ui/images/ui-icons_454545_256x240.png diff --git a/assets/css/jquery-ui/images/ui-icons_888888_256x240.png b/plugins/woocommerce/legacy/css/jquery-ui/images/ui-icons_888888_256x240.png similarity index 100% rename from assets/css/jquery-ui/images/ui-icons_888888_256x240.png rename to plugins/woocommerce/legacy/css/jquery-ui/images/ui-icons_888888_256x240.png diff --git a/assets/css/jquery-ui/images/ui-icons_cd0a0a_256x240.png b/plugins/woocommerce/legacy/css/jquery-ui/images/ui-icons_cd0a0a_256x240.png similarity index 100% rename from assets/css/jquery-ui/images/ui-icons_cd0a0a_256x240.png rename to plugins/woocommerce/legacy/css/jquery-ui/images/ui-icons_cd0a0a_256x240.png diff --git a/assets/css/jquery-ui/jquery-ui-rtl.css b/plugins/woocommerce/legacy/css/jquery-ui/jquery-ui-rtl.css similarity index 100% rename from assets/css/jquery-ui/jquery-ui-rtl.css rename to plugins/woocommerce/legacy/css/jquery-ui/jquery-ui-rtl.css diff --git a/assets/css/jquery-ui/jquery-ui.css b/plugins/woocommerce/legacy/css/jquery-ui/jquery-ui.css similarity index 100% rename from assets/css/jquery-ui/jquery-ui.css rename to plugins/woocommerce/legacy/css/jquery-ui/jquery-ui.css diff --git a/assets/css/jquery-ui/jquery-ui.min.css b/plugins/woocommerce/legacy/css/jquery-ui/jquery-ui.min.css similarity index 100% rename from assets/css/jquery-ui/jquery-ui.min.css rename to plugins/woocommerce/legacy/css/jquery-ui/jquery-ui.min.css diff --git a/assets/css/marketplace-suggestions.scss b/plugins/woocommerce/legacy/css/marketplace-suggestions.scss similarity index 100% rename from assets/css/marketplace-suggestions.scss rename to plugins/woocommerce/legacy/css/marketplace-suggestions.scss diff --git a/assets/css/menu.scss b/plugins/woocommerce/legacy/css/menu.scss similarity index 100% rename from assets/css/menu.scss rename to plugins/woocommerce/legacy/css/menu.scss diff --git a/assets/css/network-order-widget.scss b/plugins/woocommerce/legacy/css/network-order-widget.scss similarity index 100% rename from assets/css/network-order-widget.scss rename to plugins/woocommerce/legacy/css/network-order-widget.scss diff --git a/assets/css/photoswipe/default-skin/default-skin.css b/plugins/woocommerce/legacy/css/photoswipe/default-skin/default-skin.css similarity index 100% rename from assets/css/photoswipe/default-skin/default-skin.css rename to plugins/woocommerce/legacy/css/photoswipe/default-skin/default-skin.css diff --git a/assets/css/photoswipe/default-skin/default-skin.png b/plugins/woocommerce/legacy/css/photoswipe/default-skin/default-skin.png similarity index 100% rename from assets/css/photoswipe/default-skin/default-skin.png rename to plugins/woocommerce/legacy/css/photoswipe/default-skin/default-skin.png diff --git a/assets/css/photoswipe/default-skin/default-skin.svg b/plugins/woocommerce/legacy/css/photoswipe/default-skin/default-skin.svg similarity index 100% rename from assets/css/photoswipe/default-skin/default-skin.svg rename to plugins/woocommerce/legacy/css/photoswipe/default-skin/default-skin.svg diff --git a/assets/css/photoswipe/default-skin/preloader.gif b/plugins/woocommerce/legacy/css/photoswipe/default-skin/preloader.gif similarity index 100% rename from assets/css/photoswipe/default-skin/preloader.gif rename to plugins/woocommerce/legacy/css/photoswipe/default-skin/preloader.gif diff --git a/assets/css/photoswipe/photoswipe.css b/plugins/woocommerce/legacy/css/photoswipe/photoswipe.css similarity index 100% rename from assets/css/photoswipe/photoswipe.css rename to plugins/woocommerce/legacy/css/photoswipe/photoswipe.css diff --git a/assets/css/prettyPhoto.scss b/plugins/woocommerce/legacy/css/prettyPhoto.scss similarity index 100% rename from assets/css/prettyPhoto.scss rename to plugins/woocommerce/legacy/css/prettyPhoto.scss diff --git a/assets/css/privacy.scss b/plugins/woocommerce/legacy/css/privacy.scss similarity index 100% rename from assets/css/privacy.scss rename to plugins/woocommerce/legacy/css/privacy.scss diff --git a/assets/css/reports-print.scss b/plugins/woocommerce/legacy/css/reports-print.scss similarity index 100% rename from assets/css/reports-print.scss rename to plugins/woocommerce/legacy/css/reports-print.scss diff --git a/assets/css/select2.scss b/plugins/woocommerce/legacy/css/select2.scss similarity index 100% rename from assets/css/select2.scss rename to plugins/woocommerce/legacy/css/select2.scss diff --git a/assets/css/twenty-nineteen.scss b/plugins/woocommerce/legacy/css/twenty-nineteen.scss similarity index 100% rename from assets/css/twenty-nineteen.scss rename to plugins/woocommerce/legacy/css/twenty-nineteen.scss diff --git a/assets/css/twenty-seventeen.scss b/plugins/woocommerce/legacy/css/twenty-seventeen.scss similarity index 100% rename from assets/css/twenty-seventeen.scss rename to plugins/woocommerce/legacy/css/twenty-seventeen.scss diff --git a/assets/css/twenty-twenty-one-admin.scss b/plugins/woocommerce/legacy/css/twenty-twenty-one-admin.scss similarity index 100% rename from assets/css/twenty-twenty-one-admin.scss rename to plugins/woocommerce/legacy/css/twenty-twenty-one-admin.scss diff --git a/plugins/woocommerce/legacy/css/twenty-twenty-one.scss b/plugins/woocommerce/legacy/css/twenty-twenty-one.scss new file mode 100644 index 00000000000..a59a9de2d01 --- /dev/null +++ b/plugins/woocommerce/legacy/css/twenty-twenty-one.scss @@ -0,0 +1,3100 @@ +@import "mixins"; + +/** + * Sass variables + */ + +$headings: var(--heading--font-family); +$body: var(--global--font-secondary); + +$body-color: currentColor; +$highlights-color: #88a171; + +/** + * Fonts + */ +@font-face { + font-family: star; + src: url(../fonts/star.eot); + src: + url(../fonts/star.eot?#iefix) format("embedded-opentype"), + url(../fonts/star.woff) format("woff"), + url(../fonts/star.ttf) format("truetype"), + url(../fonts/star.svg#star) format("svg"); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: WooCommerce; + src: url(../fonts/WooCommerce.eot); + src: + url(../fonts/WooCommerce.eot?#iefix) format("embedded-opentype"), + url(../fonts/WooCommerce.woff) format("woff"), + url(../fonts/WooCommerce.ttf) format("truetype"), + url(../fonts/WooCommerce.svg#WooCommerce) format("svg"); + font-weight: 400; + font-style: normal; +} + +/** + * Global elements + */ +a.button { + display: inline-block; + text-align: center; + box-sizing: border-box; + word-break: break-word; + text-decoration: none !important; + + &:hover, + &:visited { + text-decoration: underline !important; + } +} + +.woocommerce { + + form .form-row { + + .required { + color: #b22222; + text-decoration: none; + visibility: hidden; // Only show optional by default. + + &[title] { + border: 0 !important; + } + } + + .optional { + visibility: visible; + } + } + + form.woocommerce-form-login, + form.woocommerce-form-register { + + p, + label { + font-family: $headings; + } + + input { + border: 1px solid #ddd; + } + } + + .woocommerce-form-login__rememberme { + margin: 1rem 0 3rem 0; + } +} + +.woocommerce-notices-wrapper:empty { + margin: 0 auto; +} + +.woocommerce-view-order { + + .woocommerce-MyAccount-content { + + table { + + border: 0; + + tbody { + border-bottom: 1px solid $body-color; + } + + tfoot { + + tr:last-of-type { + border-top: 1px solid $body-color; + + .woocommerce-Price-amount { + font-weight: 700; + } + } + } + + td, + tr, + th { + border: 0; + } + } + } +} + +.site-main { + .woocommerce-breadcrumb { + margin-bottom: var(--global--spacing-vertical); + font-size: 0.88889em; + font-family: $headings; + } + .woocommerce-products-header { + margin-top: var(--global--spacing-vertical); + } +} + + +.woocommerce-pagination { + font-family: $headings; + font-size: 0.88889em; + + ul.page-numbers { + margin: 0; + padding: 0; + display: block; + font-weight: 700; + letter-spacing: -0.02em; + line-height: 1.2; + } + + span.page-numbers, + a.page-numbers, + .next.page-numbers, + .prev.page-numbers { + padding: 0 calc(0.5 * 1rem); + display: inline-block; + } +} + +.onsale { + position: absolute; + top: -0.7rem; + right: -0.7rem; + background: $highlights-color; + color: #fff; + font-family: $headings; + font-size: 1.2rem; + font-weight: 700; + letter-spacing: -0.02em; + z-index: 1; + border-radius: 50%; + text-align: center; + padding: 0.8rem; + margin: 0; + display: inline-flex; + align-items: center; + justify-content: center; + + &::before { + content: ""; + float: left; + padding-top: 100%; + } +} + +.onsale + .woocommerce-product-gallery .woocommerce-product-gallery__trigger { + top: 2.2em; + right: 2.2em; +} + +.single-product .type-product.sale > .onsale { + right: calc(52% - 0.7rem); +} + +.price { + font-family: $headings; + font-size: 1rem; + + del { + opacity: 0.5; + display: inline-block; + } + + ins { + display: inline-block; + text-decoration: none; + } +} + +.woocommerce-message, +.woocommerce-error, +.woocommerce-info { + margin-bottom: 2rem; + margin-left: 0; + background: var(--global--color-background); + font-size: 0.88889em; + font-family: $headings; + list-style: none; + overflow: hidden; +} + +.woocommerce-message, +.woocommerce-error li, +.woocommerce-info { + padding: 1.5rem 3rem; + justify-content: space-between; + align-items: center; + + .button { + order: 2; + } +} + +.woocommerce-error { + color: #fff; + background: #b22222; + + a { + color: #fff; + + &:hover { + color: #fff; + } + + &.button { + background: #111; + } + } + + > li { + margin: 0; + } +} + +#main { + + .woocommerce-error, + .woocommerce-info { + font-family: $headings; + } +} + +.woocommerce-message, +.woocommerce-info { + background: #eee; + color: #000; + border-top: 2px solid $highlights-color; + + a { + color: #444; + + &:hover { + color: #000; + } + + &.button { + background: $highlights-color; + color: #f5efe0; + } + } +} + +.woocommerce-store-notice { + background: #eee; + color: #000; + border-top: 2px solid $highlights-color; + padding: 2rem; + position: absolute; + top: 0; + left: 0; + width: 100%; + z-index: 999; +} + +.admin-bar .woocommerce-store-notice { + top: 32px; +} + +.woocommerce-store-notice__dismiss-link { + float: right; + color: #000; + + &:hover { + text-decoration: none; + color: #000; + } +} + +.flex-viewport { + margin-bottom: 1.5em; +} + +#main { + + .post-inner { + padding-top: 0; + } + + .wp-block-cover { + margin-top: 0; + } +} + +.cross-sells { + + .woocommerce-loop-product__title { + font-family: $headings; + } + + .star-rating { + font-size: 1.4rem; + } +} + +/* Make thumbnails in the gallery affect parent's height and wrapping */ +.flex-control-nav::after { + clear: both; + content: ""; + display: table; +} + +/** +* Tables +*/ +.woocommerce, +.woocommerce-page { + + &.is-dark-theme { + .select2-dropdown { + color: var(--global--color-dark-gray); + } + } + + table.shop_table { + + td, + th { + word-break: normal; + border-left: none; + border-right: none; + } + + .product-thumbnail { + max-width: 120px; + } + } +} + +/** + * Shop page + */ +.woocommerce-result-count, +.woocommerce-ordering { + margin: 0 0 1rem; + padding: 0.75rem 0; +} + +/** + * Products + */ +ul.products { + margin: 0; + padding: 0; + + li.product { + list-style: none; + + .woocommerce-loop-product__link { + display: block; + text-decoration: none; + position: relative; + } + + .woocommerce-loop-product__title { + margin: 0.5rem 0 0.5rem; + font-size: 1.5rem; + font-weight: 400; + + &::before { + content: none; + } + } + + .woocommerce-loop-product__title, + .price, + .star-rating { + color: $body-color; + } + + .star-rating { + margin-bottom: 0.8rem; + } + + .price { + margin-bottom: 1rem; + } + + .price, + .star-rating { + display: block; + } + + .woocommerce-placeholder { + border: 1px solid #f2f2f2; + } + + .button { + vertical-align: middle; + background-color: transparent; + color: var(--button--color-text-hover); + text-decoration: none !important; + + &.loading { + opacity: 0.5; + } + + &:hover { + background-color: var(--button--color-background); + color: var(--button--color-text); + } + } + + .added_to_cart { + margin: 0.5rem; + } + } +} + +.star-rating { + overflow: hidden; + position: relative; + height: 1em; + line-height: 1; + font-size: 1em; + width: 5.4em; + font-family: star; + margin-bottom: 0.7rem; + + &::before { + content: "\73\73\73\73\73"; + float: left; + top: 0; + left: 0; + position: absolute; + } + + span { + overflow: hidden; + float: left; + top: 0; + left: 0; + position: absolute; + padding-top: 1.5em; + } + + span::before { + content: "\53\53\53\53\53"; + top: 0; + position: absolute; + left: 0; + } +} + +a.remove { + display: inline-block; + width: 20px; + height: 20px; + line-height: 18px; + font-size: 20px; + font-weight: 700; + text-align: center; + border-radius: 100%; + text-decoration: none !important; + background: #fff; + color: #000; + + &:hover { + background: $highlights-color; + color: #fff !important; + } +} + +dl.variation, +.wc-item-meta { + list-style: none outside; + + dt, + .wc-item-meta-label { + float: left; + clear: both; + margin-right: 0.25rem; + margin-top: 0; + list-style: none outside; + font-weight: 400; + } + + dd { + margin: 0; + } + + p, + &:last-child { + margin-bottom: 0; + } +} + +/** + * Single product + */ +.single-product { + + div.product { + position: relative; + + .product_meta { + clear: both; + font-size: 0.7em; + padding-top: 0.5em; + margin-top: 3rem; + } + } + + .single_add_to_cart_button { + line-height: var(--global--line-height-body) !important; + padding-top: var(--form--spacing-unit) !important; + padding-bottom: var(--form--spacing-unit) !important; + font-size: 1.6rem; + } + + .single-featured-image-header { + display: none; + } + + + &.singular { // Needed for higher specificity to target the entry title font size + .entry-title { + font-size: var(--global--font-size-xl); + font-weight: normal; + margin: 0 0 2.5rem; + + &::before { + margin-top: 0; + } + } + } + + .summary { + margin-bottom: 8rem; + + p.price { + margin-bottom: 2rem; + } + + .woocommerce-product-details__short-description { + margin-bottom: 1rem; + } + } + + .woocommerce-variation-price { + margin: 2rem 0; + } + + .woocommerce-product-rating { + margin: -1rem 0 4rem; + line-height: 1; + font-size: 1.4rem; + + .star-rating { + float: left; + margin-right: 0.25rem; + } + } + + form.cart { + + .quantity { + float: left; + margin-right: 0.5rem; + } + + input[type="number"] { + width: 5em; + } + } + + .woocommerce-variation-add-to-cart { + + .button { + padding-top: 1.55rem; + padding-bottom: 1.59rem; + font-size: 1.6rem; + } + + .button.disabled { + opacity: 0.2; + } + } + + .woocommerce-message { + flex-direction: row-reverse; + } + + .woocommerce-Tabs-panel--additional_information, + .woocommerce-Tabs-panel--reviews { + + table { + border: 1px solid #ddd; + + tr, + td, + th { + border: 1px solid #ddd; + } + } + + p { + font-family: $headings; + } + + input { + border: 1px solid #ddd; + } + } + + .woocommerce-product-attributes-item__value { + + p { + margin-bottom: 0; + } + } +} + +table.variations { + margin: 1rem 0; + + label { + margin: 0; + padding: 6px 0; + } + + select { + margin-right: 0.5rem; + } +} + +a.reset_variations { + margin-left: 0.5em; +} + +.woocommerce-product-gallery { + max-width: 600px; + position: relative; + margin-bottom: 2rem; + + figure { + margin: 0; + padding: 0; + } + + .woocommerce-product-gallery__wrapper { + margin: 0; + padding: 0; + } + + .zoomImg { + background-color: #fff; + opacity: 0; + } + + .woocommerce-product-gallery__image--placeholder { + border: 1px solid #f2f2f2; + } + + .woocommerce-product-gallery__image:nth-child(n+2) { + width: 25%; + display: inline-block; + } + + .flex-control-thumbs { + + li { + list-style: none; + cursor: pointer; + float: left; + } + + img { + opacity: 0.5; + + &:hover, + &.flex-active { + opacity: 1; + } + } + } + + img { + display: block; + height: auto; + } +} + +.woocommerce-product-gallery--columns-3 { + + .flex-control-thumbs li { + width: 33.3333%; + } + + .flex-control-thumbs li:nth-child(3n+1) { + clear: left; + } +} + +.woocommerce-product-gallery--columns-4 { + + ol { + margin-left: 0; + margin-bottom: 0; + } + + .flex-control-thumbs li { + width: 14.2857142857%; + margin: 0 14.2857142857% 1.6em 0; + } + + .flex-control-thumbs li:nth-child(4n) { + margin-right: 0; + } + + .flex-control-thumbs li:nth-child(4n+1) { + clear: left; + } +} + +.woocommerce-product-gallery--columns-5 { + + .flex-control-thumbs li { + width: 20%; + } + + .flex-control-thumbs li:nth-child(5n+1) { + clear: left; + } +} + +.woocommerce-product-gallery__trigger { + position: absolute; + top: 1rem; + right: 1rem; + z-index: 99; +} + +.woocommerce-tabs { + margin: 4rem 0 2rem; + + /* reset description tab width to full width */ + #tab-description { + + h2, + p { + max-width: 100vw; + width: 100%; + } + } + + /* reset additional info tab width to full width */ + #tab-additional_information { + + .woocommerce-product-attributes { + max-width: 100vw; + width: 100%; + } + } + + #tab-reviews { + + /* reset reviews tab width to full width */ + .woocommerce-Reviews { + max-width: 100vw; + width: 100%; + } + + #submit { + float: right; + } + } + + + ul { + margin: 0 0 1.5rem; + padding: 0; + font-family: $headings; + border-bottom: var(--button--border-width) solid var(--button--color-background); + + li { + display: inline-flex !important; + + a { + color: $body-color; + text-decoration: none; + font-weight: 700; + padding: var(--button--padding-vertical) var(--button--padding-horizontal); + } + + &.active { + + a { + color: var(--button--color-text); + background-color: var(--button--color-background); + border: var(--button--border-width) solid var(--button--color-background); + } + } + } + } + + .panel { + + > * { + margin-top: 0 !important; + } + + h1, + h2 { + + &::before { + content: none; + } + } + + h2:first-of-type { + font-size: var(--global--font-size-lg); + margin: 0 0 2rem !important; + } + } + + #comments { + padding-top: 0; + } + + .comment-reply-title { + font-family: $headings; + font-size: 1em; + font-weight: 700; + display: block; + } + + #reviews { + + ol.commentlist { + padding: 0; + margin: 0; + } + + li.review, + li.comment { + list-style: none; + margin: 0.5rem 0 2.5rem 0; + + .avatar { + max-height: 36px; + width: auto; + float: right; + } + + p.meta { + margin-bottom: 0.5em; + } + } + + .comment-form-rating { + + label { + max-width: 58rem; + margin: 0 auto; + } + } + + p.stars { + margin-top: 0; + + a { + position: relative; + height: 1em; + width: 1em; + text-indent: -999em; + display: inline-block; + text-decoration: none; + box-shadow: none; + + &::before { + display: block; + position: absolute; + top: 0; + left: 0; + width: 1em; + height: 1em; + line-height: 1; + font-family: WooCommerce; + content: "\e021"; + text-indent: 0; + } + + &:hover { + + ~ a::before { + content: "\e021"; + } + } + } + + &:hover { + + a { + + &::before { + content: "\e020"; + } + } + } + + &.selected { + + a.active { + + &::before { + content: "\e020"; + } + + ~ a::before { + content: "\e021"; + } + } + + a:not(.active) { + + &::before { + content: "\e020"; + } + } + } + } + + .comment-form-author, + .comment-form-email { + float: none; + margin-left: auto; + } + } +} + +/** + * Related products + */ + +.related.products, +.up-sells { + + h2 { + margin-bottom: 2rem; + } + + clear: both; + + ul.products { + display: flex; + justify-content: space-evenly; + align-items: stretch; + + li.product { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + } + } +} + +/** + * Widgets + */ +.widget.woocommerce { + + ul { + padding-left: 0; + + li { + list-style: none; + } + } +} + +.widget .product_list_widget, +.site-footer .widget .product_list_widget { + margin-bottom: 1.5rem; + + a { + display: block; + box-shadow: none; + + &:hover { + box-shadow: none; + } + } + + li { + padding: 0.5rem 0; + + a.remove { + float: left; + margin-top: 7px; + line-height: 20px; + color: #fff; + margin-right: 0.5rem; + } + } + + img { + display: none; + } +} + +.widget_shopping_cart { + + .buttons { + + a { + display: inline-block; + margin: 0 0.5rem 0 0; + } + } +} + +.woocommerce-shopping-totals { + vertical-align: text-top; +} + +.widget_layered_nav { + + .chosen { + + &::before { + content: "×"; + display: inline-block; + width: 16px; + height: 16px; + line-height: 16px; + font-size: 16px; + text-align: center; + border-radius: 100%; + border: 1px solid #000; + margin-right: 0.25rem; + } + } +} + +.widget_price_filter { + + .price_slider { + margin-bottom: 1rem; + } + + .price_slider_amount { + text-align: right; + line-height: 2.4; + font-size: 0.8751em; + + .button { + float: left; + padding: 0.4rem 1rem; + } + } + + .ui-slider { + position: relative; + text-align: left; + margin-left: 0.5rem; + margin-right: 0.5rem; + } + + .ui-slider .ui-slider-handle { + position: absolute; + z-index: 2; + width: 1em; + height: 1em; + background-color: #000; + border-radius: 1em; + cursor: ew-resize; + outline: none; + top: -0.3em; + margin-left: -0.5em; + } + + .ui-slider .ui-slider-range { + position: absolute; + z-index: 1; + font-size: 0.7em; + display: block; + border: 0; + border-radius: 1em; + background-color: #000; + } + + .price_slider_wrapper .ui-widget-content { + border-radius: 1em; + background-color: #666; + border: 0; + } + + .ui-slider-horizontal { + height: 0.5em; + } + + .ui-slider-horizontal .ui-slider-range { + top: 0; + height: 100%; + } + + .ui-slider-horizontal .ui-slider-range-min { + left: -1px; + } + + .ui-slider-horizontal .ui-slider-range-max { + right: -1px; + } +} + +.widget_rating_filter { + + li { + text-align: right; + + .star-rating { + float: left; + margin-top: 0.3rem; + } + } +} + +.widget_product_search { + + form { + position: relative; + } + + .search-field { + padding-right: 100px; + } + + input[type="submit"] { + position: absolute; + top: 0.5rem; + right: 0.5rem; + padding-left: 1rem; + padding-right: 1rem; + } +} + +/** + * Account section + */ +.woocommerce-account { + + #main { + + .post-inner { + padding-top: 0; + } + + .woocommerce { + max-width: 1600px; + padding: 0 6vw; + margin: 0 auto; + } + } + + .woocommerce-MyAccount-navigation { + font-family: $headings; + margin: 0 0 2rem; + + ul { + margin: 0; + padding: 0; + } + + li { + list-style: none; + padding: 0.5rem 0; + font-family: $headings; + font-size: 2rem; + + &:first-child { + padding-top: 0; + } + + a { + box-shadow: none; + text-decoration: none; + font-weight: 600; + color: #aaa; + + &:hover { + color: #000; + text-decoration: underline; + } + } + + &.is-active { + + a { + text-decoration: underline; + color: $highlights-color; + } + } + } + } + + .woocommerce-MyAccount-content { + + p { + font-family: $headings; + font-size: 2rem; + } + + form { + + h3 { + margin-top: 0; + } + } + + .woocommerce-Addresses { + margin-top: -1rem; + + .woocommerce-Address-title { + + h3 { + display: inline-block; + margin-right: 1rem; + font-size: 1.8rem; + margin-top: 2rem; + } + } + + address { + line-height: 1.8rem; + } + } + + .woocommerce-address-fields { + + label { + font-size: 1.5rem; + margin-bottom: 0.1rem; + } + + input, + .selection { + font-size: 1.5rem; + padding-top: 0.3rem; + padding-bottom: 0.3rem; + } + + input { + border: 3px solid black; + } + + .form-row { + margin-top: 1.5rem !important; + margin-bottom: 0 !important; + } + + #billing_company_field { + padding-top: 1.5rem !important; + } + + .select2-selection { + border: 2px solid black; + height: 3rem; + padding-top: 0.5rem; + margin-top: -1rem; + } + + .select2-selection__arrow { + position: absolute; + top: -0.2rem; + } + + .select2-dropdown { + border: 2px solid black !important; + } + + .woocommerce-address-fields__field-wrapper { + margin-bottom: 2rem; + } + } + } + + &.woocommerce-lost-password { + .woocommerce { + + max-width: var(--responsive--alignwide-width) !important; + padding: 0 !important; + flex-wrap: wrap; + + .woocommerce-notices-wrapper { + flex: 1 0 100%; + } + + .woocommerce-ResetPassword { + + .woocommerce-form-row--first { + float: none; + } + + #user_login { + margin-bottom: 10px; + } + } + } + } + + table.account-orders-table { + margin-top: 0; + border: 0; + + tr, + td, + th { + border: 0; + } + + td { + padding-left: 1.5rem; + } + + thead { + border-bottom: 1px solid #ddd; + } + + .button { + margin: 0 0.35rem 0.35rem 0; + width: 80%; + } + } + + table.account-orders-table:not(.has-background) { + + tbody { + + tr:nth-child(2n+1) { + + td { + background: var(--global--color-background); + filter: brightness(88%); + + .is-dark-theme & { + filter: brightness(112%); + } + } + } + } + } + + .woocommerce-EditAccountForm { + + label { + font-size: 1.5rem; + } + + input { + border: var(--form--border-width) solid var(--form--border-color); + font-size: 1.5rem; + } + + fieldset { + border: none; + padding-left: 0; + padding-right: 0; + margin-top: 30px; + + legend { + display: contents; + font-size: 2rem; + } + + p { + margin-top: 20px; + margin-bottom: 0 !important; + } + + .show-password-input { + display: inherit; + } + } + + button { + margin-top: 0; + } + + #account_display_name + span { + font-size: 1.5rem; + } + + p { + margin-top: 20px; + + &:nth-of-type(4) { + margin-top: 30px; + } + } + } +} + +.logged-in.woocommerce-account { + + #main { + + .woocommerce { + display: flex; + flex-direction: row; + } + } +} + +.checkout-button { + display: block; + padding: 1rem 2rem; + border: 2px solid #000; + text-align: center; + font-weight: 800; + + &:hover { + border-color: #999; + } + + &::after { + content: "→"; + margin-left: 0.5rem; + } +} + +.woocommerce-cart { + table.woocommerce-cart-form__contents { + thead, tfoot { + text-align: left; + } + } + + .post-inner { + padding-top: 0; + } + + #main { + + .woocommerce { + max-width: var(--responsive--alignwide-width); + margin: 0 auto; + + } + } + + .select2-container .select2-dropdown { + border: var(--form--border-width) solid var(--form--border-color); + border-radius: var(--form--border-radius); + border-top: none; + } + + .select2-container .select2-selection { + border: var(--form--border-width) solid var(--form--border-color); + border-radius: var(--form--border-radius); + } + + .select2-container--focus .select2-selection, + .select2-container--open .select2-selection { + outline-offset: 2px; + outline: 2px dotted var(--form--border-color); + } + + .select2-results__option { + margin-left: 0; + } + + .select2-container { + + .select2-search__field { + height: 3rem; + background: #eee; + } + } + + p.form-row { + + input { + border: 1px solid #ddd; + } + } + + table.cart img.woocommerce-placeholder { + height: auto !important; + } +} + +/** + * Checkout + */ +.woocommerce-form-coupon-toggle .woocommerce-info { + display: block; + margin-bottom: 2rem; + padding: 1rem; +} + +.woocommerce-form-coupon { + background: #eee; + padding: 1rem; + font-size: 0.88889em; + color: var(--form--color-text); + + #coupon_code { + border: var(--form--border-width) solid var(--form--border-color); + } + + button[name="apply_coupon"] { + padding: 0.5rem; + + .is-dark-theme & { + border-color: var(--global--color-background); + + &:hover, + &:active { + background: var(--global--color-background); + } + } + } +} + +#ship-to-different-address { + font-size: 1em; + display: inline-block; + margin: 1.42em 0; + + label { + font-weight: 400; + cursor: pointer; + + span { + position: relative; + display: block; + text-align: right; + padding-right: 45px; + + &::before { + content: ""; + display: block; + height: 16px; + width: 30px; + border: 2px solid var(--form--border-color); + background: var(--global--color-primary); + border-radius: 13rem; + box-sizing: content-box; + transition: all ease-in-out 0.3s; + position: absolute; + top: 0; + right: 0; + } + + &::after { + content: ""; + display: block; + width: 14px; + height: 14px; + background: var(--global--color-background); + position: absolute; + top: 3px; + right: 17px; + border-radius: 13rem; + transition: all ease-in-out 0.3s; + } + } + + input[type="checkbox"] { + display: none; + } + + input[type="checkbox"]:checked + span::after { + right: 3px; + background: var(--global--color-primary); + } + + input[type="checkbox"]:checked + span::before { + background: var(--global--color-background); + } + } +} + +.woocommerce-no-js { + + form.woocommerce-form-login, + form.woocommerce-form-coupon { + display: block !important; + } + + .woocommerce-form-login-toggle, + .woocommerce-form-coupon-toggle, + .showcoupon { + display: none !important; + } +} + +.woocommerce-terms-and-conditions { + border: 1px solid rgba(0, 0, 0, 0.2); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + background: rgba(0, 0, 0, 0.05); +} + +.woocommerce-terms-and-conditions-link { + display: inline-block; + + &::after { + content: ""; + display: inline-block; + border-style: solid; + margin-bottom: 2px; + margin-left: 0.25rem; + border-width: 6px 6px 0 6px; + border-color: $body-color transparent transparent transparent; + } + + &.woocommerce-terms-and-conditions-link--open::after { + border-width: 0 6px 6px 6px; + border-color: transparent transparent $body-color transparent; + } +} + +.woocommerce-checkout { + + .woocommerce { + max-width: var(--responsive--alignwide-width); + margin: 0 auto; + } + + ul.woocommerce-error { + flex-direction: column; + align-items: flex-start; + + li { + font-family: $headings; + margin: 0.5rem 0 0.5rem; + } + } + + .post-inner { + padding-top: 0; + } + + .woocommerce-billing-fields { + + h3 { + margin: 2rem 0; + } + } + + form[name="checkout"] { + display: table; + } + + .blockUI.blockOverlay { + position: relative; + + @include loader(); + } + + form { + + .col2-set { + width: 50%; + float: left; + padding-right: 1.5vw; + + .col-1, + .col-2 { + float: none; + width: 100%; + } + + label { + font-family: $headings; + letter-spacing: normal; + } + + p { + margin-bottom: 1.15em; + } + } + + #order_review_heading { + margin-top: 2rem; + } + + #order_review_heading, + #order_review { + width: 50%; + padding-left: 1.5vw; + float: right; + clear: right; + + .woocommerce-checkout-review-order-table { + margin-top: 2rem; + border: 0; + + th, + td { + border: 0; + } + + thead { + display: none; + } + + .woocommerce-Price-amount { + font-weight: bold; + } + + .cart-subtotal, + .order-total { + border-top: 2px solid var(--form--border-color); + } + } + } + + .form-row.woocommerce-invalid { + + input.input-text { + border: 2px solid $highlights-color; + } + } + + } + + .woocommerce-input-wrapper { + + .description { + background: #4169e1; + color: #fff; + border-radius: 3px; + padding: 1rem; + margin: 0.5rem 0 0; + clear: both; + display: none; + position: relative; + + a { + color: #fff; + text-decoration: underline; + border: 0; + box-shadow: none; + } + + &::before { + left: 50%; + top: 0; + margin-top: -4px; + transform: translateX(-50%) rotate(180deg); + content: ""; + position: absolute; + border-width: 4px 6px 0 6px; + border-style: solid; + border-color: #4169e1 transparent transparent transparent; + z-index: 100; + display: block; + } + } + } + + .woocommerce-form-login { + + p.form-row.form-row-first, + p.form-row.form-row-last { + float: none; + } + } + + .select2-choice, + .select2-choice:hover { + box-shadow: none !important; + } + + .select2-choice { + padding: 0.7rem 0 0.7rem 0.7rem; + } + + .select2-container .select2-selection--single { + height: 48px; + } + + .select2-container .select2-selection--single .select2-selection__rendered { + line-height: 48px; + } + + .select2-container .select2-selection { + border: var(--form--border-width) solid var(--form--border-color); + border-radius: var(--form--border-radius); + } + + .select2-container .select2-dropdown { + border: var(--form--border-width) solid var(--form--border-color); + border-radius: var(--form--border-radius); + border-top: none; + } + + .select2-container--default .select2-selection--single .select2-selection__arrow { + height: 46px; + } + + .select2-container--focus .select2-selection, + .select2-container--open .select2-selection { + outline-offset: 2px; + outline: 2px dotted var(--form--border-color); + } + + .select2-results__option { + margin-left: 0; + } + + .select2-container { + + .select2-search__field { + height: 3rem; + background: #eee; + } + } +} + +.woocommerce-checkout-review-order-table { + + ul li { + list-style-type: none; + } + + input[type="radio"].shipping_method { + display: none; + + & + label { + + &::before { + content: ""; + display: inline-block; + width: 14px !important; + height: 14px; + border: var(--form--border-width) solid var(--form--border-color); + background: var(--global--color-white); + margin-left: 4px; + margin-right: 1.2rem; + border-radius: 100%; + transform: translateY(2px); + } + } + + &:checked + label { + + &::before { + background: var(--global--color-border); + } + + .is-dark-theme &::before { + background: var(--global--color-background); + } + } + } + + td { + padding: 1rem 0.5em; + } + + dl.variation { + margin: 0; + + p { + margin: 0; + } + + dt, + dd { + font-family: $headings; + + p { + padding-top: 1px; + font-family: $headings; + } + } + } + + tfoot { + text-align: left; + } +} + +.woocommerce-order-received { + + .woocommerce-order { + + p, + li { + font-family: $headings; + } + } + + table { + border: 0; + + td, + th, + tr { + border: 0; + } + + tr { + height: 5rem; + } + + tfoot { + border-top: 1px solid #ddd; + + /* Targeting total */ + tr:last-of-type { + border-top: 1px solid #ddd; + + .woocommerce-Price-amount { + font-weight: 700; + } + } + } + + } +} + +.woocommerce-checkout-review-order { + + ul { + margin: 2rem 0 3rem; + padding-left: 0; + } + + #place_order { + width: 100%; + } +} + +.wc_payment_method { + list-style: none; + + .payment_box { + padding: 1rem; + background: #eee; + color: var(--global--color-dark-gray); + + a, + a:hover, + a:visited { + color: var(--global--color-dark-gray); + } + + ul, + ol { + + &:last-of-type { + margin-bottom: 0; + } + } + + fieldset { + padding: 1.5rem; + padding-bottom: 0; + border: 0; + background: #f6f6f6; + } + + li { + list-style: none; + } + + p { + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + } + + input[type=checkbox] { + width: 25px !important; + } + + input[type=radio] { + + & + label::before { + background: #fff !important; + border: var(--form--border-width) solid #000 !important; + } + + &:checked + label::before { + background: #000 !important; + } + } + } + + > label:first-of-type { + display: block; + margin: 1rem 0; + + img { + max-height: 24px; + max-width: 200px; + float: right; + } + } + + label { + cursor: pointer; + } + + input[type="radio"] { + display: none; + + & + label { + font-family: $headings; + + &::before { + content: ""; + display: inline-block; + width: 14px; + height: 14px; + border: var(--form--border-width) solid var(--form--border-color); + background: var(--global--color-white); + margin-left: 4px; + margin-right: 1.2rem; + border-radius: 100%; + transform: translateY(2px); + } + + } + + &:checked + label { + + &::before { + background: var(--global--color-border); + } + + .is-dark-theme &::before { + background: var(--global--color-background); + } + } + } +} + +.wc_payment_methods { + + .payment_box { + + p { + font-family: $headings; + } + } +} + +.account-payment-methods-table { + padding-top: 0 !important; + margin-bottom: 1rem; + + table, + tr { + border-style: hidden; + } + + tr:nth-child(2n) { + + td { + background: transparent !important; + } + } + + tr:nth-child(2n+1) { + + td { + background: var(--global--color-background); + filter: brightness(88%); + + .is-dark-theme & { + filter: brightness(112%); + } + } + } + + td.payment-method-actions { + padding-right: 0.5rem; + padding-left: 0.5rem; + padding-top: 0.3rem; + padding-bottom: 0.3rem; + + display: grid; + border: none; + + font-size: 0; + + a { + width: 100%; + padding-top: 0.3rem !important; + padding-bottom: 0.3rem !important; + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + + @include inversebuttoncolors(); + } + } +} + + +.woocommerce-terms-and-conditions-wrapper { + margin-bottom: 5rem; + + .woocommerce-privacy-policy-text { + + p { + font-family: $headings; + font-size: 1.6rem; + } + } +} + +.woocommerce-order-overview { + margin-bottom: 2rem; +} + +.woocommerce-table--order-details { + margin-bottom: 2rem; + + thead, tfoot { + text-align: left; + } +} + +/** + * Layout stuff + */ +.woocommerce { + + section { + padding-top: 2rem; + padding-bottom: 0; + } + + .content-area { + + .site-main { + margin: 0 5vw; + } + } + + /* Shop layout */ + ul.products { + display: flex; + align-items: stretch; + flex-direction: row; + flex-wrap: wrap; + box-sizing: border-box; + word-break: break-word; + min-width: 12vw; + + &.columns-2 { + + li.product { + width: calc(100% / 2 - 16px) !important; + } + } + + &.columns-3 { + + li.product { + width: calc(100% / 3 - 16px) !important; + } + } + + &.columns-4 { + + li.product { + width: calc(100% / 4 - 16px) !important; + } + } + + &.columns-5 { + + li.product { + width: calc(100% / 5 - 16px) !important; + } + } + + &.columns-6 { + + li.product { + width: calc(100% / 6 - 16px) !important; + } + } + + li.product { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + margin: 0 8px 16px 8px; + box-sizing: border-box; + + img.attachment-woocommerce_thumbnail, + img.woocommerce-placeholder { + height: auto !important; + } + } + + li.product-category { + + a { + text-align: left; + text-decoration: none; + + h2.woocommerce-loop-category__title { + margin-top: 0.4rem; + font-family: $headings; + font-size: 1.5rem; + + .count { + background-color: transparent; + color: $body-color; + } + } + } + + mark { + background-color: initial; + } + } + } +} + +@media only screen and (max-width: 600px) { + + .woocommerce { + + .woocommerce-ordering { + float: left; + clear: both; + margin-top: 0; + } + + .woocommerce-result-count { + margin-top: 0; + margin-bottom: 20px; + } + } +} + +@media only screen and (max-width: 667px) { + + .woocommerce, + .woocommerce-page { + + ul.products[class*=columns-] { + + li.product { + width: auto !important; + margin-left: auto; + margin-right: auto; + } + } + } +} + +@media only screen and (min-width: 668px) and (max-width: 768px) { + + .woocommerce, + .woocommerce-page { + + .related.products { + + ul.products[class*=columns-] { + + li.product { + padding: 0 2vw 3em 0 !important; + margin-bottom: 2em; + } + } + } + + ul.products[class*=columns-] { + justify-content: center; + + li.product { + width: 50%; + padding: 0 2vw 3em 0; + } + + } + + .onsale { + font-size: 1rem; + } + + .onsale + .woocommerce-product-gallery .woocommerce-product-gallery__trigger { + top: 1.8em; + right: 1.8em; + } + + } +} + +@media only screen and (max-width: 768px) { + + .woocommerce section.content-area { + padding-top: 0; + } + + #main { + + .woocommerce { + + .woocommerce-cart-form { + + .actions { + + .coupon { + margin-bottom: 2rem; + + button { + width: 100%; + } + } + } + + #coupon_code { + width: 100% !important; + } + } + } + + #shipping_method { + + li { + display: flex; + justify-content: flex-end; + } + } + } + + .woocommerce, + .woocommerce-page { + + .onsale { + right: -0.7rem !important; + } + + .woocommerce-tabs { + + ul { + + li { + font-size: 1rem; + + a { + padding: calc(0.75 * var(--button--padding-vertical)) calc(0.75 * var(--button--padding-horizontal)); + } + } + } + } + + table.shop_table_responsive { + + .button { + + @include inversebuttoncolors(); + } + + tr { + margin: 0 0 1.5rem; + + &:first-child { + border-top: 1px solid; + } + + &:last-child { + margin-bottom: 0; + } + + &:nth-child(2n) { + + td { + background: transparent; + } + } + + &:nth-child(2n+1) { + + td { + background: var(--global--color-background); + filter: brightness(88%); + + .is-dark-theme & { + filter: brightness(112%); + } + } + } + + td { + border-bottom-width: 0; + + &:last-child { + border-bottom-width: 1px; + } + } + + td.product-quantity::before { + padding-top: 0.9rem; + } + + .product-remove { + float: right; + } + + .product-thumbnail { + display: block; + + img { + width: 70px; + } + + &::before { + content: ""; + } + } + } + + } + + .woocommerce-breadcrumb { + margin-bottom: 4rem; + font-size: 0.8em; + font-family: $headings; + } + + .related.products { + + ul.products { + display: flex; + flex-direction: column; + align-items: flex-start; + + li.product { + margin-bottom: 5em; + } + } + } + + .woocommerce-products-header__title.page-title { + margin: 3rem auto 4rem; + } + + .woocommerce-result-count, + .woocommerce-ordering { + font-size: 0.8em; + } + + .woocommerce-ordering { + margin-bottom: 3rem; + } + } + + .woocommerce-cart-form { + + table { + + td.product-name { + padding-left: 0.5em; + } + + input.qty { + padding: 1rem 1.5rem; + } + } + } + + .woocommerce-checkout { + + form { + + .col2-set { + width: 100%; + float: none; + padding-right: 0; + + .col-1, + .col-2 { + float: none; + width: 100%; + } + } + + #order_review_heading { + margin-top: 2rem; + } + + #order_review_heading, + #order_review { + width: 100%; + padding-left: 0; + float: none; + } + + table { + + tbody { + + td.product-total { + text-align: end; + } + } + + tfoot { + + .cart-subtotal, + .order-total { + + td { + text-align: end; + } + } + } + } + } + } + + .logged-in.woocommerce-account { + + #main { + + .woocommerce { + flex-direction: column; + } + + .woocommerce-MyAccount-navigation, + .woocommerce-MyAccount-content { + width: 100%; + } + + table.account-orders-table { + + .button { + padding-left: 0.5em; + padding-right: 0.5em; + width: 100%; + margin: 2rem 0; + } + } + } + + table.account-orders-table { + + td { + padding-bottom: 1.5rem; + } + } + } +} + +@media only screen and (min-width: 768px) { + + /** + * Tables + */ + .woocommerce, + .woocommerce-page { + + table.shop_table { + + tbody { + + tr { + font-size: 0.88889em; + } + } + } + + .onsale { + font-size: 1rem; + } + + } + + /** + * Home page + */ + .home #main { + + [class*="woocommerce columns-"] { + word-break: break-word; + max-width: var(--responsive--aligndefault-width); + margin-left: auto; + margin-right: auto; + } + } + + /** + * Shop page + */ + + .woocommerce-pagination { + + span.page-numbers, + a.page-numbers, + .next.page-numbers, + .prev.page-numbers { + padding: 1rem; + } + } + + /** + * Account section + */ + .woocommerce-account { + + .woocommerce-MyAccount-navigation { + float: none; + width: 20%; + margin-bottom: 1.5rem; + margin-right: 3rem; + + li { + margin: 0 1rem 3rem 0; + padding: 0; + border-bottom: 0; + + &:last-child { + margin-right: 0; + } + } + } + + .woocommerce-MyAccount-content { + float: none; + width: 75%; + } + + table.account-orders-table { + margin-top: 0; + border: 0; + margin-bottom: 1rem; + + tr, + td, + th { + border: 0; + padding: 0; + } + + th, + td, + td.woocommerce-orders-table__cell-order-actions { + width: 1%; + padding-right: 0.5rem; + padding-left: 0.5rem; + + a { + padding-top: 0.3rem !important; + padding-bottom: 0.3rem !important; + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + } + + td.woocommerce-orders-table__cell-order-date { + padding-right: 0; + } + + thead { + border-bottom: 1px solid $body-color; + } + + .button { + padding-left: 0.5em; + padding-right: 0.5em; + width: 100%; + margin: 1.5rem 0; + + @include inversebuttoncolors(); + } + } + } + + /** + * Layout stuff + */ + .woocommerce { + + .content-area { + margin: 0 auto; + padding: 0 6vw; + + .site-main { + margin: 0; + } + } + } + + .single-product { + + .entry { + + .entry-content, + .entry-summary { + max-width: none; + margin: 0 0 3rem; + padding: 0; + + > * { + max-width: none; + } + } + } + } + + .woocommerce-breadcrumb { + margin-bottom: 5rem; + font-size: 0.88889em; + font-family: $headings; + } + + .woocommerce-product-gallery { + margin-bottom: 8rem; + } + + .woocommerce-checkout { + + #main { + + .woocommerce { + + max-width: 1600px; + padding: 0 6vw; + margin: 0 auto; + } + } + } + +} + +@media only screen and (min-width: 1168px) { + + .woocommerce { + + .content-area { + max-width: 1600px; + margin: 0 auto; + padding: 0 6vw; + + .site-main { + + } + } + + .onsale { + font-size: 1.2rem; + } + } + + .woocommerce-breadcrumb { + margin-bottom: 5rem; + font-size: 0.88889em; + font-family: $headings; + } + + .woocommerce-product-gallery { + margin-bottom: 8rem; + } + + .woocommerce-account { + + table.account-orders-table { + + th, + td, + td.woocommerce-orders-table__cell-order-actions { + padding-right: 1.5rem; + padding-left: 1.5rem; + } + } + } +} + +@media only screen and (max-width: 768px) { + + .woocommerce-products-header { + border-bottom: none !important; + padding-bottom: 0; + margin-bottom: 0 !important; + } +} + +@media only screen and (min-width: 600px) { + + .woocommerce-products-header { + padding-bottom: 1.5vw; + } + + .woocommerce-ordering, + .woocommerce-result-count { + margin-top: 0 !important; + } +} + +@media only screen and (min-width: 690px) { + + .woocommerce-products-header { + border-bottom: 3px solid var(--global--color-border); + } +} + +.woocommerce-account { + + .woocommerce-MyAccount-content { + + p:first-of-type { + margin-bottom: 2rem; + } + + #add_payment_method { + + ul { + list-style-type: none !important; + } + + .woocommerce-PaymentMethod { + margin-bottom: 1.5rem; + } + } + + input[type=radio] { + float: left; + margin-top: 0.5rem; + margin-right: 0.5rem; + } + + label { + font-size: 1.5rem; + display: flex; + justify-content: flex-end; + + img { + margin-left: 10px !important; + } + + img:first-child { + margin-left: auto !important; + } + + img:last-child { + margin-right: 5px !important; + } + } + + .woocommerce-PaymentBox { + + p, + label { + font-size: 1.3rem; + } + + p { + margin-bottom: 1.5rem; + } + + br { + display: none; + } + + .woocommerce_error { + margin-top: 1rem; + margin-bottom: 0; + } + } + } + + .woocommerce-MyAccount-navigation-link { + + margin-bottom: 20px !important; + + a { + color: $body-color !important; + font-weight: normal !important; + font-size: 1.8rem; + + &:hover { + color: $body-color !important; + text-decoration: underline solid $body-color 1px !important; + } + } + } +} + +.alignwide .woocommerce { + + & > * { + max-width: var(--responsive--alignwide-width); + display: block; + margin: var(--global--spacing-vertical) auto; + } +} + +.woocommerce { + + .woocommerce-notices-wrapper { + + & > * { + padding: 15px; + list-style: none; + } + } + + .return-to-shop, + .wc-proceed-to-checkout { + + a.button { + margin-top: var(--global--spacing-vertical); + float: left; + display: inline-block; + width: 100%; + } + } + + .woocommerce-cart-form { + + .shop_table_responsive { + margin-top: var(--global--spacing-vertical); + margin-bottom: var(--global--spacing-vertical); + + th { + border: none; + } + + #coupon_code { + min-width: 9rem; + } + } + + button[name="update_cart"], + button[name="apply_coupon"] { + padding: 0.5rem; + color: var(--global--color-primary); + background: var(--global--color-background); + border: var(--form--border-width) solid var(--global--color-primary); + + &:hover, + &:active { + color: var(--global--color-background); + background: var(--global--color-primary); + } + } + + .product-thumbnail { + + .attachment-woocommerce_thumbnail { + height: auto !important; + } + } + } + + .cart-collaterals { + + h2 { + margin-bottom: var(--global--spacing-vertical); + } + + #shipping_method { + list-style: none; + padding-left: 0; + } + + .shipping-calculator-form { + + p { + margin-bottom: 0.5rem; + } + + .select2-container { + + .select2-selection { + height: auto; + } + + .select2-selection__rendered { + border: var(--form--border-width) solid var(--form--border-color); + border-radius: var(--form--border-radius); + color: var(--form--color-text); + height: var(--global--line-height-body); + padding: var(--form--spacing-unit); + } + + .select2-selection__arrow { + height: 100%; + } + } + } + + .cross-sells { + + li { + list-style: none; + } + + li > em, + a { + display: inline-block; + } + } + } +} + +/** + * Downloads + */ + +.woocommerce-order-downloads { + + padding-top: 0 !important; + + table, + tr { + border-style: hidden; + + td.download-remaining { + text-align: center !important; + } + } + + tr:nth-child(2n) { + + td { + background: transparent !important; + } + } + + tr:nth-child(2n+1) { + + td { + background: var(--global--color-background); + filter: brightness(88%); + + .is-dark-theme & { + filter: brightness(112%); + } + } + } + + td.download-file { + padding-right: 0.5rem; + padding-left: 0.5rem; + padding-top: 0.3rem; + padding-bottom: 0.3rem; + + a { + width: 100%; + padding-top: 0.3rem !important; + padding-bottom: 0.3rem !important; + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + + @include inversebuttoncolors(); + } + } +} diff --git a/plugins/woocommerce/legacy/css/twenty-twenty-two.scss b/plugins/woocommerce/legacy/css/twenty-twenty-two.scss new file mode 100644 index 00000000000..3fae3d41ffd --- /dev/null +++ b/plugins/woocommerce/legacy/css/twenty-twenty-two.scss @@ -0,0 +1,1285 @@ +/** +* Fonts +*/ +@font-face { + font-family: star; + src: url(../fonts/star.eot); + src: + url(../fonts/star.eot?#iefix) format("embedded-opentype"), + url(../fonts/star.woff) format("woff"), + url(../fonts/star.ttf) format("truetype"), + url(../fonts/star.svg#star) format("svg"); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: WooCommerce; + src: url(../fonts/WooCommerce.eot); + src: + url(../fonts/WooCommerce.eot?#iefix) format("embedded-opentype"), + url(../fonts/WooCommerce.woff) format("woff"), + url(../fonts/WooCommerce.ttf) format("truetype"), + url(../fonts/WooCommerce.svg#WooCommerce) format("svg"); + font-weight: 400; + font-style: normal; +} + +@import "mixins"; +@import "animation"; + +$tt2-gray: #f7f7f7; + +/** + * Main layout. + */ + +.woocommerce-page { + + h1.wp-block-post-title { + font-size: var(--wp--preset--font-size--huge); + } + + h2 { + font-size: var(--wp--preset--font-size--large); + } +} + +.woocommerce-page { + + main { + + .woocommerce { + @include clearfix(); + max-width: 1000px; + } + } +} + +.woocommerce { + + .woocommerce-products-header, + .woocommerce-notices-wrapper, + .woocommerce-result-count, + .woocommerce-ordering, + .woocommerce-breadcrumb, + .products { + max-width: 1000px; + } + + .wp-site-blocks > .wp-block-group { + max-width: 1000px; + margin-left: auto; + margin-right: auto; + } + + // Common + .woocommerce-products-header { + h1.page-title { + font-family: var(--wp--preset--font-family--source-serif-pro); + font-size: var(--wp--custom--typography--font-size--gigantic); + font-weight: 300; + line-height: var(--wp--custom--typography--line-height--tiny); + margin-bottom: var(--wp--custom--spacing--medium); + } + } + + span.onsale { + top: -1rem; + right: -1rem; + position: absolute; + background: var( --wp--preset--color--secondary ); + border-radius: 2rem; + line-height: 2.6rem; + font-size: 0.8rem; + padding: 0rem 0.5rem 0rem 0.5rem; + } + + .price ins, bdi { + text-decoration: none; + } + + .quantity { + input[type='number'] { + width: 3em; + } + + input[type='number']::-webkit-inner-spin-button, + input[type='number']::-webkit-outer-spin-button{ + opacity: 1; + } + } + + &.woocommerce-shop .woocommerce-breadcrumb { + display: none; + } + + .woocommerce-message, + .woocommerce-error, + .woocommerce-info { + background: $tt2-gray; + border-top-color: var( --wp--preset--color--primary ); + border-top-style: solid; + padding: 1rem 1.5rem; + margin-bottom: 2rem; + list-style: none; + font-size: var(--wp--preset--font-size--small); + + &[role='alert']::before { + color: var( --wp--preset--color--background ); + background: var( --wp--preset--color--primary ); + border-radius: 5rem; + font-size: 1rem; + padding-left: 3px; + padding-right: 3px; + margin-right: 1rem; + } + + a.button { + margin-top: -0.5rem; + border: none; + background: #ebe9eb; + color: var(--wp--preset--color--black); + padding: 0.5rem 1rem; + + &:hover, + &:visited { + color: var(--wp--preset--color--black); + } + } + } + + .woocommerce-error[role='alert'] { + margin: 0; + + &::before { + content: 'X'; + padding-right: 4px; + padding-left: 4px; + } + + li { + display: inline-block; + } + } + + .woocommerce-message { + &[role='alert']::before { + content: '\2713'; + } + } + + .woocommerce-NoticeGroup-checkout { + ul.woocommerce-error[role='alert'] { + &::before { + display: none; + } + li { + display: inherit; + margin-bottom: 1rem; + } + } + } + + a.button, + button[name='add-to-cart'], + input[name='submit'], + button.single_add_to_cart_button, + button[type='submit']:not(.wp-block-search__button) { + display: inline-block; + text-align: center; + word-break: break-word; + background-color: var( --wp--preset--color--primary ); + color: #fff; + border: 1px solid var(--wp--preset--color--black); + padding: 1rem 2rem; + margin-top: 1rem; + text-decoration: none; + font-size: medium; + cursor: pointer; + + &:hover, + &:visited { + color: var( --wp--preset--color--white ); + text-decoration: underline; + } + } + + button.woocommerce-form-login__submit, + button.single_add_to_cart_button, + a.checkout-button { + font-size: 18px; + padding: 1.5rem 3.5rem; + } + + // Shop page + + .woocommerce-result-count { + margin-top: 0; + font-size: 0.9rem; + } + + .woocommerce-ordering { + margin-top: -0.2rem; + margin-bottom: 3rem; + + select { + padding: 0.2rem 0 0.2rem 0.5rem; + } + } + + // Products. + ul.products { + + padding-inline-start: 0; + + li.product { + list-style: none; + margin-top: var(--wp--style--block-gap); + text-align: center; + + a.woocommerce-loop-product__link { + text-decoration: none; + display: block; + } + + h2.woocommerce-loop-product__title { + color: var( --wp--preset--color--primary ); + font-family: var( --wp--preset--font-family--system-font ); + font-size: 1.2rem; + text-decoration: none; + margin-bottom: 0; + } + + a.add_to_cart_button { + padding: 0.8rem 2.7rem; + + &.loading { + opacity: 0.5; + } + } + + a.added_to_cart { + display: block; + margin-top: 1rem; + } + } + } + + ul.page-numbers { + text-align: center; + } + + div.product { + position: relative; + max-width: 1000px; + + > span.onsale { + position: absolute; + left: -1rem; + top: -1rem; + width: 1.8rem; + z-index: 1; + } + + div.woocommerce-product-gallery { + position: relative; + + a.woocommerce-product-gallery__trigger { + position: absolute; + top: 1rem; + right: 1rem; + z-index: 1; + text-decoration: none; + border-radius: 1rem; + border-style: solid; + line-height: 1.5rem; + padding: 0; + font-size: 0.6rem; + background: var( --wp--preset--color--white ); + border-color: var( --wp--preset--color--white ); + height: 1.5rem; + width: 1.5rem; + overflow: hidden; + + &::before { + content: url('data:image/svg+xml;utf8,'); + display: block; + transform: rotateY(180deg); + margin-left: 1.55rem; + } + } + + figure.woocommerce-product-gallery__wrapper { + margin: 0; + } + + } + + div.summary { + font-size: 1rem; + + > *{ + margin-bottom: var( --wp--style--block-gap ); + } + + h1.product_title { + font-size: 2.5rem; + margin: 0; + } + + figure.woocommerce-product-gallery__wrapper { + margin: 0; + } + + .woocommerce-product-rating { + .star-rating { + display: inline-block; + } + .woocommerce-review-link { + display: inline-block; + overflow: hidden; + position: relative; + top: -0.5em; + font-size: 1em; + } + } + } + + button[name='add-to-cart'], button.single_add_to_cart_button { + margin-top: 0.5rem; + margin-bottom: var( --wp--style--block-gap ); + } + + table.variations { + tr { + display: block; + margin-bottom: var( --wp--style--block-gap ); + + th { + padding-right: 1rem; + } + + td select { + padding: 2px; + } + } + } + + ol.flex-control-thumbs { + padding-left: 0; + float: left; + + li { + list-style: none; + cursor: pointer; + float: left; + width: 18%; + margin-right: 1rem; + } + + } + + a.reset_variations { + margin-left: 0.5em; + } + + table.group_table { + td { + padding-right: 0.5rem; + padding-bottom: 1rem; + } + + margin-bottom: var( --wp--style--block-gap ); + } + + .related.products { + margin-top: 7rem; + } + } + + .woocommerce-tabs { + padding-top: var( --wp--style--block-gap ); + + ul.wc-tabs { + padding: 0; + border-bottom-style: solid; + border-bottom-width: 1px; + border-bottom-color: #EAE9EB; + + li { + background: #EAE9EB; + margin: 0; + padding: 0.5em 1em 0.5em 1em; + border-color: #EAE9EB; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + float: left; + border-style: solid; + border-width: 1px; + border-left-color: var( --wp--preset--color--background ); + font-weight: 600; + font-size: var(--wp--preset--font-size--medium); + + &:first-child { + border-left-color: #EAE9EB; + margin-left: 1em; + } + + &.active { + background: var( --wp--preset--color--background ); + border-bottom-color: var( --wp--preset--color--background ); + box-shadow: 0 1px var( --wp--preset--color--background ); + } + + a { + text-decoration: none; + } + } + } + + .woocommerce-Tabs-panel { + padding-top: var( --wp--style--block-gap ); + font-size: var(--wp--preset--font-size--small); + margin-left: 1em; + + h2 { + display: none; + } + } + } + + .woocommerce-Reviews { + ol.commentlist { + list-style: none; + padding-left: 0; + + img.avatar { + float: left; + } + + p.meta { + font-size: 1rem; + } + + .comment-text { + display: inline-block; + padding-left: var( --wp--style--block-gap ); + + .star-rating { + margin-top: 0; + margin-right: unset; + margin-left: unset; + } + } + } + + .comment-form-rating { + label { + display: inline-block; + padding-right: var( --wp--style--block-gap ); + padding-top: var( --wp--style--block-gap ); + } + + p.stars { + display: inline; + a::before { + color: var( --wp--preset--color--secondary ); + } + } + } + + .comment-form-comment { + label { + float: left; + padding-right: var( --wp--style--block-gap ); + } + } + + #review_form_wrapper { + margin-top: 5rem; + } + + .comment-reply-title { + font-size: var(--wp--preset--font-size--medium); + font-weight: 700; + } + } + + + .star-rating { + overflow: hidden; + position: relative; + height: 1em; + line-height: 1; + width: 5.4rem; + font-family: star; + color: var( --wp--preset--color--secondary ); + margin: 1rem auto .7rem auto; + + &::before { + content: "\73\73\73\73\73"; + float: left; + top: 0; + left: 0; + position: absolute; + font-size: 1rem; + } + + span { + overflow: hidden; + float: left; + top: 0; + left: 0; + position: absolute; + padding-top: 1.5em; + } + + span::before { + content: "\53\53\53\53\53"; + top: 0; + position: absolute; + left: 0; + font-size: 1rem; + } + } + + p.stars { + margin-top: 0; + + a { + position: relative; + height: 1em; + width: 1em; + text-indent: -999em; + display: inline-block; + text-decoration: none; + box-shadow: none; + + &::before { + display: block; + position: absolute; + top: 0; + left: 0; + width: 1em; + height: 1em; + line-height: 1; + font-family: WooCommerce; + content: "\e021"; + text-indent: 0; + } + + &:hover { + + ~ a::before { + content: "\e021"; + } + } + } + + &:hover { + + a { + + &::before { + content: "\e020"; + } + } + } + + &.selected { + + a.active { + + &::before { + content: "\e020"; + } + + ~ a::before { + content: "\e021"; + } + } + + a:not(.active) { + + &::before { + content: "\e020"; + } + } + } + } + + .woocommerce-product-gallery__trigger { + position: absolute; + top: 1rem; + right: 1rem; + z-index: 99; + } + + .return-to-shop { + a.button { + background-color: #fff; + color: var( --wp--preset--color--primary ); + border: 2px solid var( --wp--preset--color--primary ); + padding: 0.7rem 2rem; + } + } + + mark { + background: var( --wp--preset--color--secondary ); + } +} + +/** + * Form fields + */ +.woocommerce-page { + + form { + + .input-text { + border: 1px solid var(--wp--preset--color--black); + border-radius: 0; + font-size: var(--wp--preset--font-size--small); + padding: .9rem 1.1rem; + } + + label { + margin-bottom: .7rem; + } + + abbr.required { + text-decoration: none; + } + + ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; + padding-left: 0; + } + } + + input[type="radio"][name='payment_method'], + input[type="radio"].shipping_method { + display: none; + + & + label { + + &::before { + content: ""; + display: inline-block; + width: 1rem; + height: 1rem; + border: 2px solid var(--wp--preset--color--black); + background: var(--wp--preset--color--white); + margin-left: 4px; + margin-right: 1.2rem; + border-radius: 100%; + transform: translateY(.2rem); + } + } + + & ~ .payment_box { + padding-left: 3rem; + margin-top: 1rem; + } + + &:checked + label { + + &::before { + background: radial-gradient(circle at center, black 45%, white 0); + } + } + } + + label.woocommerce-form__label-for-checkbox { + font-weight: normal; + cursor: pointer; + + span { + + &::before { + content: ""; + display: inline-block; + height: 1rem; + width: 1rem; + border: 2px solid var(--wp--preset--color--black); + background: var(--wp--preset--color--white); + margin-right: .5rem; + transform: translateY(.2rem); + } + } + + input[type="checkbox"] { + display: none; + } + + input[type="checkbox"]:checked + span::before { + background: var(--wp--preset--color--black); + box-shadow: inset .2rem .2rem var(--wp--preset--color--white), inset -.2rem -.2rem var(--wp--preset--color--white); + } + } + + table.shop_table_responsive { + + width: 100%; + text-align: left; + border-collapse: collapse; + + th, + td { + font-size: var(--wp--preset--font-size--small); + font-weight: normal; + } + + th { + padding-bottom: 1rem; + } + + tbody { + + tr { + border-top: 1px solid var( --wp--preset--color--black ); + } + + td { + a.button, + button { + margin-bottom: 1rem; + border: none; + background: #ebe9eb; + color: var(--wp--preset--color--black); + padding: 0.5rem 1rem 0.5rem 1rem; + + &:hover, + &:visited { + color: var(--wp--preset--color--black); + } + } + + &.woocommerce-orders-table__cell-order-actions { + a.button { + display: block; + } + } + } + } + } + + table.shop_table, + table.shop_table_responsive { + tbody { + .product-name { + a:not(:hover) { + text-decoration: none; + } + + a:hover { + text-decoration: underline; + text-decoration-thickness: 1px; + } + + .variation { + dt { + font-style: italic; + margin-right: 0.25rem; + float: left; + } + + dd { + font-style: normal; + + a { + font-style: normal; + } + } + } + } + } + } + + /* + * Cart / Checkout + */ + .woocommerce-cart-form { + + #coupon_code { + width: auto; + } + + table.shop_table_responsive { + + td, th { + padding: 1rem 0 0.5rem 1rem; + } + + tbody { + + tr:last-of-type { + border-top: none; + } + + @media only screen and (max-width: 768px) { + td { + padding-left: 0; + } + + .product-remove { + width: auto; + text-align: left !important; + } + + #coupon_code { + width: 50%; + float: left; + margin-bottom: 1rem; + } + } + } + + button[name='apply_coupon'], + button[name='update_cart'] { + padding: 1rem 2rem; + border: 2px solid #ebe9eb; + margin: 0; + } + + .product-remove { + width: 1rem; + font-size: var(--wp--preset--font-size--large); + + a { + text-decoration: none; + } + } + + .product-thumbnail { + width: 7.5rem; + + a { + img { + width: 117px; + } + } + } + } + } + + .cart-collaterals { + margin-top: 1.5rem; + + h2 { + text-transform: uppercase; + font-family: inherit; + font-size: var(--wp--preset--font-size--medium); + } + + table.shop_table_responsive { + + tr { + border-top: none; + } + + th { + width: 11rem; + } + + td, th { + padding: 1rem 0; + vertical-align: text-top; + } + } + + button[name='calc_shipping'] { + padding: 1rem 2rem; + } + + .woocommerce-Price-amount { + font-weight: normal; + } + } + + .woocommerce-checkout, + &.woocommerce-order-pay { + display: table; + + h3 { + font-family: inherit; + font-size: var(--wp--preset--font-size--normal); + font-weight: 700; + } + + .col2-set { + width: 43%; + float: right; + } + + .blockUI.blockOverlay { + position: relative; + @include loader(); + } + + #customer_details { + width: 53%; + float: left; + + .col-1, .col-2 { + width: 100%; + float: none; + } + } + + @media only screen and (max-width: 768px) { + .col2-set, + #customer_details { + width: 100%; + float: none; + } + } + + .woocommerce-billing-fields__field-wrapper, + .woocommerce-checkout-review-order-table, + .woocommerce-checkout-payment, + #payment { + margin-top: 4rem; + } + + .woocommerce-checkout-review-order-table, + #order_review .shop_table { + border-collapse: collapse; + width: 100%; + + thead { + display: none; + } + + th { + text-align: left; + font-weight: normal; + } + + th, td { + padding: 1rem 1rem 1rem 0; + vertical-align: text-top; + } + + tbody { + border-bottom: 1px solid #d2ced2; + } + + tr.order-total { + border-top: 1px solid #d2ced2; + } + + .product-quantity { + font-weight: normal; + } + + .product-total, + .cart-subtotal, + .order-total, + .tax-rate, + input[type="radio"].shipping_method:checked + label, + input[type="hidden"].shipping_method + label { + .woocommerce-Price-amount { + font-weight: bold; + } + } + } + + button#place_order { + width: 100%; + text-transform: uppercase; + } + } + + form.checkout_coupon { + background: $tt2-gray; + padding-left: 1.5rem; + float: left; + // 1.5 rem is to account for extra padding we added above. + width: calc(100% - 1.5rem); + + .form-row { + button[name='apply_coupon'] { + margin-top: 0; + } + } + } + + ul.wc_payment_methods, + ul.woocommerce-shipping-methods { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; + padding-left: 0; + + input[type='radio'] { + margin-right: .6rem; + } + + li.wc_payment_method { + margin-bottom: 1rem; + } + } + + .woocommerce-thankyou-order-received { + margin-top: 0; + } + + .woocommerce-thankyou-order-received, + h2.woocommerce-column__title { + font-family: var(--wp--preset--font-family--source-serif-pro); + font-size: var(--wp--preset--font-size--large); + font-weight: 300; + } + + .woocommerce-order > * { + margin-bottom: var( --wp--style--block-gap ); + } + + ul.woocommerce-order-overview { + font-size: var(--wp--preset--font-size--small); + display: flex; + padding-left: 0; + width: 100%; + + li { + display: inline; + flex-grow: 1; + margin-bottom: 1rem; + text-transform: uppercase; + + strong { + text-transform: none; + display: block; + } + } + + @media only screen and (max-width: 768px) { + flex-direction: column; + } + } + + .woocommerce-customer-details { + address { + padding: 2rem; + border: 1px solid var(--wp--preset--color--black); + font-style: inherit; + + p[class^='woocommerce-customer-details--'] { + &:first-of-type { + margin-top: 2rem; + } + + margin-top: 1rem; + margin-bottom: 0; + } + + .woocommerce-customer-details--phone::before { + content: '\01F4DE'; + margin-right: 1rem; + } + + .woocommerce-customer-details--email::before { + content: '\2709'; + margin-right: 1rem; + font-size: 1.8rem; + } + } + } + + .woocommerce-table--order-details { + border: 1px solid var(--wp--preset--color--black); + border-collapse: collapse; + width: 70%; + + th, td { + text-align: left; + padding: 1rem; + border-top: 1px solid var(--wp--preset--color--black); + border-bottom: 1px solid var(--wp--preset--color--black); + font-weight: normal; + } + + thead th { + text-transform: uppercase; + } + + @media only screen and (max-width: 768px) { + width: 100%; + } + } +} + +.select2-container { + + .select2-selection, + .select2-search__field { + height: 3.5rem; + font-size: var(--wp--preset--font-size--small); + padding: .9rem 1.1rem; + } + + .select2-selection, + .select2-dropdown { + border: 1px solid var(--wp--preset--color--black); + border-radius: 0; + } + + .select2-dropdown { + border-top: 0; + padding: .9rem 1.1rem; + + .select2-search__field { + border: 1px solid var(--wp--preset--color--black); + border-radius: 0; + margin-bottom: 1rem; + } + } + + .select2-selection .select2-selection__arrow { + height: 3.5rem; + position: absolute; + top: 0; + right: 0; + width: 3rem; + } +} + +/** + * Account section + */ +.woocommerce-account { + + main { + + .woocommerce { + @include clearfix(); + max-width: 1000px; + } + } + + .woocommerce-MyAccount-navigation { + + ul { + margin: 0; + padding: 0; + } + + li { + list-style: none; + padding: 1rem 0; + + &:first-child { + padding-top: 0; + } + + a { + box-shadow: none; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + &.is-active { + + a { + color: var( --wp--preset--color--primary ); + text-decoration: underline; + } + } + } + } + + .woocommerce-MyAccount-content { + + > p:first-of-type, + p.form-row-first, + p.form-row-last { + margin-block-start: 0px; + } + } + + &.woocommerce-edit-address { + + .woocommerce-MyAccount-content { + form > h3 { + margin-block-start: 0px; + } + } + } + + .woocommerce-form-login { + max-width: 516px; + margin: 0 auto; + + .show-password-input { + top: .8rem; + right: 1.2rem; + } + } +} + +.wp-block-search { + .wp-block-search__label { + font-weight: normal; + } + .wp-block-search__input { + padding: .9rem 1.1rem; + border: 1px solid var(--wp--preset--color--black); + } + .wp-block-search__button { + padding: 1rem 1.2rem; + } +} + +.wc-block-product-search { + form { + .wc-block-product-search__fields { + display: flex; + flex: auto; + flex-wrap: nowrap; + max-width: 100%; + + .wc-block-product-search__field { + padding: .9rem 1.1rem; + flex-grow: 1; + border: 1px solid var(--wp--preset--color--black); + font-size: inherit; + font-family: inherit; + } + + .wc-block-product-search__button { + display: flex; + background-color: var( --wp--preset--color--primary ); + color: #fff; + border: 1px solid var(--wp--preset--color--black); + padding: 1rem 1.2rem; + margin: 0 0 0 .7rem; + } + } + } +} + +.woocommerce-store-notice { + color: var(--wp--preset--color--black); + border-top: 2px solid var( --wp--preset--color--primary ); + background: $tt2-gray; + padding: 2rem; + position: fixed; + bottom: 0; + left: 0; + width: 100%; + z-index: 999; + margin: 0; + + .woocommerce-store-notice__dismiss-link { + float: right; + margin-right: 4rem; + } +} + diff --git a/plugins/woocommerce/legacy/css/twenty-twenty.scss b/plugins/woocommerce/legacy/css/twenty-twenty.scss new file mode 100644 index 00000000000..6cf9a2dd094 --- /dev/null +++ b/plugins/woocommerce/legacy/css/twenty-twenty.scss @@ -0,0 +1,2582 @@ +@import "mixins"; + +/** + * Sass variables + */ + +$headings: -apple-system, blinkmacsystemfont, "Helvetica Neue", helvetica, sans-serif; +$body: nonbreakingspaceoverride, "Hoefler Text", garamond, "Times New Roman", serif; + +$body-color: #111; +$highlights-color: #cd2653; + +/** + * Fonts + */ +@font-face { + font-family: star; + src: url(../fonts/star.eot); + src: + url(../fonts/star.eot?#iefix) format("embedded-opentype"), + url(../fonts/star.woff) format("woff"), + url(../fonts/star.ttf) format("truetype"), + url(../fonts/star.svg#star) format("svg"); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: WooCommerce; + src: url(../fonts/WooCommerce.eot); + src: + url(../fonts/WooCommerce.eot?#iefix) format("embedded-opentype"), + url(../fonts/WooCommerce.woff) format("woff"), + url(../fonts/WooCommerce.ttf) format("truetype"), + url(../fonts/WooCommerce.svg#WooCommerce) format("svg"); + font-weight: 400; + font-style: normal; +} + +/** + * Global elements + */ +a.button { + display: inline-block; + text-align: center; + box-sizing: border-box; + word-break: break-word; + color: #fff; + text-decoration: none !important; + + &:hover, + &:visited { + color: #fff; + text-decoration: underline !important; + } +} + +.woocommerce { + + form .form-row { + + .required { + color: #b22222; + text-decoration: none; + visibility: hidden; // Only show optional by default. + + &[title] { + border: 0 !important; + } + } + + .optional { + visibility: visible; + } + } + + form.woocommerce-form-login, + form.woocommerce-form-register { + + p, + label { + font-family: $headings; + } + + input { + border: 1px solid #ddd; + } + } + + .woocommerce-form-login__rememberme { + margin: 1rem 0 3rem 0; + } +} + +.woocommerce-view-order { + + .woocommerce-MyAccount-content { + + table { + + border: 0; + + tbody { + border-bottom: 1px solid #ddd; + } + + tfoot { + + tr:last-of-type { + border-top: 1px solid #ddd; + + .woocommerce-Price-amount { + font-weight: 700; + } + } + } + + td, + tr, + th { + border: 0; + } + } + } +} + +.woocommerce-breadcrumb { + margin-bottom: 5rem; + font-size: 0.88889em; + font-family: $headings; +} + +.woocommerce-pagination { + font-family: $headings; + font-size: 0.88889em; + + ul.page-numbers { + margin: 0; + padding: 0; + display: block; + font-weight: 700; + letter-spacing: -0.02em; + line-height: 1.2; + } + + span.page-numbers, + a.page-numbers, + .next.page-numbers, + .prev.page-numbers { + padding: 0 calc(0.5 * 1rem); + display: inline-block; + } +} + +.onsale { + position: absolute; + top: 0; + left: 0; + display: inline-block; + background: $highlights-color; + color: #fff; + font-family: $headings; + font-size: 1.7rem; + font-weight: 700; + letter-spacing: -0.02em; + line-height: 1.2; + padding: 1.5rem; + text-transform: uppercase; + z-index: 1; +} + +.price { + font-family: $headings; + + del { + opacity: 0.5; + display: inline-block; + } + + ins { + display: inline-block; + text-decoration: none; + } +} + +.woocommerce-message, +.woocommerce-error, +.woocommerce-info { + margin-bottom: 5rem; + margin-left: 0; + background: #eee; + font-size: 0.88889em; + font-family: $headings; + list-style: none; + overflow: hidden; + width: 100%; +} + +.woocommerce-message, +.woocommerce-error li, +.woocommerce-info { + padding: 1.5rem 3rem; + display: flex; + justify-content: space-between; + align-items: center; + + .button { + order: 2; + } +} + +.woocommerce-message { + background: #eee; + color: $body-color; +} + +.woocommerce-error { + color: #fff; + background: #b22222; + + a { + color: #fff; + + &:hover { + color: #fff; + } + + &.button { + background: #111; + } + } + + > li { + margin: 0; + } +} + +#site-content { + + .woocommerce-error, + .woocommerce-info { + font-family: $headings; + } +} + +.woocommerce-info { + background: #eee; + color: #000; + border-top: 2px solid $highlights-color; + + a { + color: #444; + + &:hover { + color: #000; + } + + &.button { + background: $highlights-color; + color: #f5efe0; + } + } +} + +.woocommerce-store-notice { + background: #eee; + color: #000; + border-top: 2px solid $highlights-color; + padding: 2rem; + position: absolute; + top: 0; + left: 0; + width: 100%; + z-index: 999; +} + +.admin-bar .woocommerce-store-notice { + top: 32px; +} + +.woocommerce-store-notice__dismiss-link { + float: right; + color: #000; + + &:hover { + text-decoration: none; + color: #000; + } +} + +.flex-viewport { + margin-bottom: 1.5em; +} + +#site-content { + + .post-inner { + padding-top: 0; + } + + .wp-block-cover { + margin-top: 0; + } +} + +.cross-sells { + + .woocommerce-loop-product__title { + font-family: $headings; + } + + .star-rating { + font-size: 1.4rem; + } +} + +/* Make thumbnails in the gallery affect parent's height and wrapping */ +.flex-control-nav::after { + clear: both; + content: ""; + display: table; +} + +/** +* Tables +*/ +.woocommerce, +.woocommerce-page { + + table.shop_table { + + td, + th { + word-break: normal; + } + } +} + +/** + * Shop page + */ +.woocommerce-products-header__title.page-title { + font-size: 6rem; + text-align: center; +} + +.woocommerce-result-count, +.woocommerce-ordering { + margin: 0 0 1rem; + padding: 0.75rem 0; +} + +/** + * Products + */ +ul.products { + margin: 0; + padding: 0; + + li.product { + list-style: none; + + .woocommerce-loop-product__link { + display: block; + text-decoration: none; + } + + .woocommerce-loop-product__title { + margin: 1.5rem 0 0.5rem; + font-size: 2.5rem; + + &::before { + content: none; + } + } + + .woocommerce-loop-product__title, + .price, + .star-rating { + color: $body-color; + } + + .star-rating { + margin-bottom: 0.8rem; + } + + .price { + margin-bottom: 2rem; + } + + .price, + .star-rating { + display: block; + } + + .woocommerce-placeholder { + border: 1px solid #f2f2f2; + } + + .button { + vertical-align: middle; + + &.loading { + opacity: 0.5; + } + } + + .added_to_cart { + margin: 0.5rem; + } + } +} + +.star-rating { + overflow: hidden; + position: relative; + height: 1em; + line-height: 1; + font-size: 1em; + width: 5.4em; + font-family: star; + margin-bottom: 0.7rem; + + &::before { + content: "\73\73\73\73\73"; + float: left; + top: 0; + left: 0; + position: absolute; + } + + span { + overflow: hidden; + float: left; + top: 0; + left: 0; + position: absolute; + padding-top: 1.5em; + } + + span::before { + content: "\53\53\53\53\53"; + top: 0; + position: absolute; + left: 0; + } +} + +a.remove { + display: inline-block; + width: 20px; + height: 20px; + line-height: 18px; + font-size: 20px; + font-weight: 700; + text-align: center; + border-radius: 100%; + text-decoration: none !important; + background: #fff; + color: #000; + + &:hover { + background: $highlights-color; + color: #fff !important; + } +} + +dl.variation, +.wc-item-meta { + list-style: none outside; + + dt, + .wc-item-meta-label { + float: left; + clear: both; + margin-right: 0.25rem; + margin-top: 0; + list-style: none outside; + font-weight: 400; + } + + dd { + margin: 0; + } + + p, + &:last-child { + margin-bottom: 0; + } +} + +/** + * Single product + */ +.single-product { + + div.product { + position: relative; + + .product_meta { + clear: both; + font-size: 0.7em; + padding-top: 0.5em; + margin-top: 3rem; + } + } + + .single_add_to_cart_button { + padding-top: 1.55rem; + padding-bottom: 1.59rem; + font-size: 1.6rem; + } + + .single-featured-image-header { + display: none; + } + + .entry-title { + margin: 0 0 2.5rem; + + &::before { + margin-top: 0; + } + } + + .summary { + margin-bottom: 8rem; + + p.price { + margin-bottom: 3.5rem; + } + } + + .woocommerce-product-rating { + margin: -1rem 0 4rem; + line-height: 1; + font-size: 1.4rem; + + .star-rating { + float: left; + margin-right: 0.25rem; + } + } + + form.cart { + + .quantity { + float: left; + margin-right: 0.5rem; + } + + input[type="number"] { + width: 5em; + } + } + + .woocommerce-variation-add-to-cart { + + .button { + padding-top: 1.55rem; + padding-bottom: 1.59rem; + font-size: 1.6rem; + } + + .button.disabled { + opacity: 0.2; + } + } + + .woocommerce-message { + flex-direction: row-reverse; + } + + .woocommerce-Tabs-panel--additional_information, + .woocommerce-Tabs-panel--reviews { + + table { + border: 1px solid #ddd; + + tr, + td, + th { + border: 1px solid #ddd; + } + } + + p { + font-family: $headings; + } + + input { + border: 1px solid #ddd; + } + } + + .woocommerce-product-attributes-item__value { + + p { + margin-bottom: 0; + } + } +} + +table.variations { + + label { + margin: 0; + padding: 6px 0; + } + + select { + margin-right: 0.5rem; + } +} + +a.reset_variations { + margin-left: 0.5em; +} + +.woocommerce-product-gallery { + max-width: 600px; + position: relative; + margin-bottom: 2rem; + + figure { + margin: 0; + padding: 0; + } + + .woocommerce-product-gallery__wrapper { + margin: 0; + padding: 0; + } + + .zoomImg { + background-color: #fff; + opacity: 0; + } + + .woocommerce-product-gallery__image--placeholder { + border: 1px solid #f2f2f2; + } + + .woocommerce-product-gallery__image:nth-child(n+2) { + width: 25%; + display: inline-block; + } + + .flex-control-thumbs { + + li { + list-style: none; + cursor: pointer; + float: left; + } + + img { + opacity: 0.5; + + &:hover, + &.flex-active { + opacity: 1; + } + } + } + + img { + display: block; + height: auto; + } +} + +.woocommerce-product-gallery--columns-3 { + + .flex-control-thumbs li { + width: 33.3333%; + } + + .flex-control-thumbs li:nth-child(3n+1) { + clear: left; + } +} + +.woocommerce-product-gallery--columns-4 { + + ol { + margin-left: 0; + margin-bottom: 0; + } + + .flex-control-thumbs li { + width: 14.2857142857%; + margin: 0 14.2857142857% 1.6em 0; + } + + .flex-control-thumbs li:nth-child(4n) { + margin-right: 0; + } + + .flex-control-thumbs li:nth-child(4n+1) { + clear: left; + } +} + +.woocommerce-product-gallery--columns-5 { + + .flex-control-thumbs li { + width: 20%; + } + + .flex-control-thumbs li:nth-child(5n+1) { + clear: left; + } +} + +.woocommerce-product-gallery__trigger { + position: absolute; + top: 1rem; + right: 1rem; + z-index: 99; +} + +.woocommerce-tabs { + margin: 4rem 0 2rem; + + /* reset description tab width to full width */ + #tab-description { + + h2, + p { + max-width: 100vw; + width: 100%; + } + } + + /* reset additional info tab width to full width */ + #tab-additional_information { + + .woocommerce-product-attributes { + max-width: 100vw; + width: 100%; + } + } + + #tab-reviews { + + /* reset reviews tab width to full width */ + .woocommerce-Reviews { + max-width: 100vw; + width: 100%; + } + + #submit { + float: right; + } + } + + + ul { + margin: 0 0 1.5rem; + padding: 0; + font-family: $headings; + + li { + margin: 0.5rem 4rem 2rem 0; + + a { + color: $body-color; + text-decoration: none; + font-weight: 700; + } + + &.active { + + a { + color: $highlights-color; + box-shadow: 0 2px 0 $highlights-color; + } + } + } + } + + .panel { + + > * { + margin-top: 0 !important; + } + + h1, + h2 { + + &::before { + content: none; + } + } + + h2:first-of-type { + font-size: 3rem; + margin: 0 0 2rem; + } + } + + #comments { + padding-top: 0; + } + + .comment-reply-title { + font-family: $headings; + font-size: 1em; + font-weight: 700; + display: block; + } + + #reviews { + + ol.commentlist { + padding: 0; + margin: 0; + } + + li.review, + li.comment { + list-style: none; + margin: 0.5rem 0 2.5rem 0; + + .avatar { + max-height: 36px; + width: auto; + float: right; + } + + p.meta { + margin-bottom: 0.5em; + } + } + + .comment-form-rating { + + label { + max-width: 58rem; + margin: 0 auto; + } + } + + p.stars { + margin-top: 0; + + a { + position: relative; + height: 1em; + width: 1em; + text-indent: -999em; + display: inline-block; + text-decoration: none; + box-shadow: none; + + &::before { + display: block; + position: absolute; + top: 0; + left: 0; + width: 1em; + height: 1em; + line-height: 1; + font-family: WooCommerce; + content: "\e021"; + text-indent: 0; + } + + &:hover { + + ~ a::before { + content: "\e021"; + } + } + } + + &:hover { + + a { + + &::before { + content: "\e020"; + } + } + } + + &.selected { + + a.active { + + &::before { + content: "\e020"; + } + + ~ a::before { + content: "\e021"; + } + } + + a:not(.active) { + + &::before { + content: "\e020"; + } + } + } + } + + .comment-form-author, + .comment-form-email { + float: none; + margin-left: auto; + } + } +} + +/** + * Related products + */ + +.related.products, +.up-sells { + + clear: both; + + ul.products { + display: flex; + justify-content: space-evenly; + align-items: stretch; + + li.product { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + } + } +} + +/** + * Widgets + */ +.widget.woocommerce { + + ul { + padding-left: 0; + + li { + list-style: none; + } + } +} + +.widget .product_list_widget, +.site-footer .widget .product_list_widget { + margin-bottom: 1.5rem; + + a { + display: block; + box-shadow: none; + + &:hover { + box-shadow: none; + } + } + + li { + padding: 0.5rem 0; + + a.remove { + float: left; + margin-top: 7px; + line-height: 20px; + color: #fff; + margin-right: 0.5rem; + } + } + + img { + display: none; + } +} + +.widget_shopping_cart { + + .buttons { + + a { + display: inline-block; + margin: 0 0.5rem 0 0; + } + } +} + +.woocommerce-shopping-totals { + vertical-align: text-top; +} + +.widget_layered_nav { + + .chosen { + + &::before { + content: "×"; + display: inline-block; + width: 16px; + height: 16px; + line-height: 16px; + font-size: 16px; + text-align: center; + border-radius: 100%; + border: 1px solid #000; + margin-right: 0.25rem; + } + } +} + +.widget_price_filter { + + .price_slider { + margin-bottom: 1rem; + } + + .price_slider_amount { + text-align: right; + line-height: 2.4; + font-size: 0.8751em; + + .button { + float: left; + padding: 0.4rem 1rem; + } + } + + .ui-slider { + position: relative; + text-align: left; + margin-left: 0.5rem; + margin-right: 0.5rem; + } + + .ui-slider .ui-slider-handle { + position: absolute; + z-index: 2; + width: 1em; + height: 1em; + background-color: #000; + border-radius: 1em; + cursor: ew-resize; + outline: none; + top: -0.3em; + margin-left: -0.5em; + } + + .ui-slider .ui-slider-range { + position: absolute; + z-index: 1; + font-size: 0.7em; + display: block; + border: 0; + border-radius: 1em; + background-color: #000; + } + + .price_slider_wrapper .ui-widget-content { + border-radius: 1em; + background-color: #666; + border: 0; + } + + .ui-slider-horizontal { + height: 0.5em; + } + + .ui-slider-horizontal .ui-slider-range { + top: 0; + height: 100%; + } + + .ui-slider-horizontal .ui-slider-range-min { + left: -1px; + } + + .ui-slider-horizontal .ui-slider-range-max { + right: -1px; + } +} + +.widget_rating_filter { + + li { + text-align: right; + + .star-rating { + float: left; + margin-top: 0.3rem; + } + } +} + +.widget_product_search { + + form { + position: relative; + } + + .search-field { + padding-right: 100px; + } + + input[type="submit"] { + position: absolute; + top: 0.5rem; + right: 0.5rem; + padding-left: 1rem; + padding-right: 1rem; + } +} + +/** + * Account section + */ +.woocommerce-account { + + #site-content { + + .post-inner { + padding-top: 0; + } + + .woocommerce { + max-width: 1600px; + padding: 0 6vw; + margin: 0 auto; + } + } + + .woocommerce-MyAccount-navigation { + font-family: $headings; + margin: 0 0 2rem; + + ul { + margin: 0; + padding: 0; + } + + li { + list-style: none; + padding: 0.5rem 0; + font-family: $headings; + font-size: 2rem; + + &:first-child { + padding-top: 0; + } + + a { + box-shadow: none; + text-decoration: none; + font-weight: 600; + color: #aaa; + + &:hover { + color: #000; + text-decoration: underline; + } + } + + &.is-active { + + a { + text-decoration: underline; + color: $highlights-color; + } + } + } + } + + .woocommerce-MyAccount-content { + + p { + font-family: $headings; + font-size: 2rem; + } + + form { + + h3 { + margin-top: 0; + } + } + } + + table.account-orders-table { + margin-top: 0; + border: 0; + + tr, + td, + th { + border: 0; + } + + td { + padding-left: 1.5rem; + } + + thead { + border-bottom: 1px solid #ddd; + } + + .button { + margin: 0 0.35rem 0.35rem 0; + width: 80%; + } + } + + table.account-orders-table:not(.has-background) { + + tbody { + + tr:nth-child(2n) { + + td { + background: #eee; + } + } + + tr:nth-child(2n+1) { + + td { + background: #fff; + } + } + } + } + + .woocommerce-EditAccountForm { + + input { + border: 1px solid #ddd; + } + + fieldset { + border: 0.2rem solid #ddd; + } + + button { + margin-top: 3rem; + } + } +} + +.logged-in.woocommerce-account { + + #site-content { + + .woocommerce { + display: flex; + flex-direction: row; + } + } +} + +/** + * Cart + */ +.woocommerce-cart-form { + + img { + max-width: 120px; + height: auto; + display: block; + } + + dl.variation { + margin-top: 1rem; + + dt, + dd, + p { + font-family: $headings; + font-size: 1.4rem; + } + + p, + &:last-child { + margin-bottom: 0; + } + } + + .product-remove { + text-align: center; + } + + .actions { + + .input-text { + width: 200px !important; + float: left; + margin-right: 0.25rem; + border: 1px solid #ddd; + padding-top: 1.55rem; + padding-bottom: 1.59rem; + } + + .button { + background: #f9f9f9; + border: 1px solid #555; + color: #555; + } + + button[name="update_cart"] { + background: #fff; + color: #000; + } + } + + .quantity { + + input { + width: 8rem; + border: 1px solid #eee; + } + } + + table { + border: 0; + + th, + tbody, + td { + border: 0; + } + + td.product-thumbnail { + padding: 1.4rem; + width: 10%; + } + + td.product-name { + padding-left: 1.5vw; + } + + tbody { + + tr { + border-top: 1px solid #eee; + } + } + + input.qty { + display: inline-block; + } + } + + .actions { + + button { + padding-top: 1.55rem; + padding-bottom: 1.59rem; + font-size: 1.6rem; + } + } +} + +.cart_totals { + + th, + td { + vertical-align: top; + } + + th { + padding-right: 1rem; + } + + .woocommerce-shipping-destination { + margin-bottom: 1.5rem; + font-family: $headings; + } + + table { + border: 0; + + tbody, + th, + tr, + td { + border: 0; + padding: 1rem; + } + + th { + width: 33%; + } + } + + .checkout-button { + width: 100%; + } + + input[type="radio"].shipping_method { + display: none; + + & + label { + + &::before { + content: ""; + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid #fff; + box-shadow: 0 0 0 2px #6d6d6d; + background: #fff; + margin-left: 4px; + margin-right: 1.2rem; + border-radius: 100%; + transform: translateY(2px); + } + } + + &:checked + label { + + &::before { + background: #555; + } + } + } +} + +.shipping-calculator-button { + margin-top: 0.5rem; + display: inline-block; +} + +.shipping-calculator-form { + margin: 1rem 0 0 0; +} + +#shipping_method { + list-style: none; + margin: 0; + padding: 0 0 1.5rem; + font-family: $headings; + + li { + margin-bottom: 0.5rem; + margin-left: 0; + + input { + float: left; + margin-top: 0.5rem; + margin-right: 0.6rem; + } + + label { + line-height: 2.5rem; + } + } +} + +.checkout-button { + display: block; + padding: 1rem 2rem; + border: 2px solid #000; + text-align: center; + font-weight: 800; + + &:hover { + border-color: #999; + } + + &::after { + content: "→"; + margin-left: 0.5rem; + } +} + +.woocommerce-cart { + + .post-inner { + padding-top: 0; + } + + #site-content { + + .entry-header { + padding: 3vw 0 1.5vw; + } + + .woocommerce { + max-width: 1600px; + padding: 0 5vw; + margin: 0 auto; + + } + } + + .select2-container .select2-selection--single { + height: 48px; + } + + .select2-container .select2-selection--single .select2-selection__rendered { + line-height: 48px; + font-family: $headings; + font-size: 1.6rem; + color: #000; + padding-left: 1.8rem; + } + + .select2-container--default .select2-selection--single .select2-selection__arrow { + height: 46px; + } + + .select2-container--focus .select2-selection { + border-color: #000; + } + + .select2-results__option { + margin-left: 0; + } + + .select2-container { + + .select2-search__field { + height: 4rem; + background: #eee; + } + } + + p.form-row { + + input { + border: 1px solid #ddd; + } + } +} + +/** + * Checkout + */ +#ship-to-different-address { + font-size: 1em; + display: inline-block; + margin: 1.42em 0; + + label { + font-weight: 400; + cursor: pointer; + + span { + position: relative; + display: block; + text-align: right; + padding-right: 45px; + + &::before { + content: ""; + display: block; + height: 16px; + width: 30px; + border: 2px solid #bbb; + background: #bbb; + border-radius: 13rem; + box-sizing: content-box; + transition: all ease-in-out 0.3s; + position: absolute; + top: 0; + right: 0; + } + + &::after { + content: ""; + display: block; + width: 14px; + height: 14px; + background: #fff; + position: absolute; + top: 3px; + right: 17px; + border-radius: 13rem; + transition: all ease-in-out 0.3s; + } + } + + input[type="checkbox"] { + display: none; + } + + input[type="checkbox"]:checked + span::after { + right: 3px; + } + + input[type="checkbox"]:checked + span::before { + border-color: #000; + background: #000; + } + } +} + +.woocommerce-no-js { + + form.woocommerce-form-login, + form.woocommerce-form-coupon { + display: block !important; + } + + .woocommerce-form-login-toggle, + .woocommerce-form-coupon-toggle, + .showcoupon { + display: none !important; + } +} + +.woocommerce-terms-and-conditions { + border: 1px solid rgba(0, 0, 0, 0.2); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + background: rgba(0, 0, 0, 0.05); +} + +.woocommerce-terms-and-conditions-link { + display: inline-block; + + &::after { + content: ""; + display: inline-block; + border-style: solid; + margin-bottom: 2px; + margin-left: 0.25rem; + border-width: 6px 6px 0 6px; + border-color: $body-color transparent transparent transparent; + } + + &.woocommerce-terms-and-conditions-link--open::after { + border-width: 0 6px 6px 6px; + border-color: transparent transparent $body-color transparent; + } +} + +.woocommerce-checkout { + + ul.woocommerce-error { + flex-direction: column; + align-items: flex-start; + + li { + font-family: $headings; + margin: 0.5rem 0 0.5rem; + } + } + + .post-inner { + padding-top: 0; + } + + .woocommerce-billing-fields { + + h3 { + margin-top: 4rem; + } + } + + form[name="checkout"] { + display: table; + width: 100%; + } + + .blockUI.blockOverlay { + position: relative; + + @include loader(); + } + + form { + + .col2-set { + width: 50%; + float: left; + padding-right: 1.5vw; + + .col-1, + .col-2 { + float: none; + width: 100%; + } + + input { + border: 1px solid #ddd; + } + + label { + font-family: $headings; + letter-spacing: normal; + } + + p { + margin-bottom: 1.15em; + } + } + + #order_review_heading { + margin-top: 4rem; + } + + #order_review_heading, + #order_review { + width: 50%; + padding-left: 1.5vw; + float: right; + clear: right; + + .woocommerce-checkout-review-order-table { + margin-top: 2.85rem; + border: 0; + + th, + td { + border: 0; + } + + thead { + display: none; + } + + tbody::after { + content: ""; + display: block; + height: 2rem; + } + + .woocommerce-Price-amount { + font-weight: bold; + } + + .cart-subtotal, + .order-total { + border-top: 1px solid #ddd; + } + } + } + + .form-row.woocommerce-invalid { + + input.input-text { + border: 2px solid $highlights-color; + } + } + + } + + .woocommerce-input-wrapper { + + .description { + background: #4169e1; + color: #fff; + border-radius: 3px; + padding: 1rem; + margin: 0.5rem 0 0; + clear: both; + display: none; + position: relative; + + a { + color: #fff; + text-decoration: underline; + border: 0; + box-shadow: none; + } + + &::before { + left: 50%; + top: 0; + margin-top: -4px; + transform: translateX(-50%) rotate(180deg); + content: ""; + position: absolute; + border-width: 4px 6px 0 6px; + border-style: solid; + border-color: #4169e1 transparent transparent transparent; + z-index: 100; + display: block; + } + } + } + + .woocommerce-form-login { + + p.form-row.form-row-first, + p.form-row.form-row-last { + float: none; + } + } + + input#coupon_code { + padding-top: 1.55rem; + padding-bottom: 1.59rem; + border: 1px solid #ddd; + } + + button[name="apply_coupon"] { + padding-top: 1.55rem; + padding-bottom: 1.8rem; + font-size: 1.6rem; + } + + .select2-choice, + .select2-choice:hover { + box-shadow: none !important; + } + + .select2-choice { + padding: 0.7rem 0 0.7rem 0.7rem; + } + + .select2-container .select2-selection--single { + height: 48px; + } + + .select2-container .select2-selection--single .select2-selection__rendered { + line-height: 48px; + font-family: $headings; + font-size: 1.6rem; + color: #000; + padding-left: 1.8rem; + } + + .select2-container--default .select2-selection--single .select2-selection__arrow { + height: 46px; + } + + .select2-container--focus .select2-selection { + border-color: #000; + } + + .select2-results__option { + margin-left: 0; + } + + .select2-container { + + .select2-search__field { + height: 4rem; + background: #eee; + } + } +} + +.woocommerce-checkout-review-order-table { + + input[type="radio"].shipping_method { + display: none; + + & + label { + + &::before { + content: ""; + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid #fff; + box-shadow: 0 0 0 2px #6d6d6d; + background: #fff; + margin-left: 4px; + margin-right: 1.2rem; + border-radius: 100%; + transform: translateY(2px); + } + } + + &:checked + label { + + &::before { + background: #555; + } + } + } + + td { + padding: 1rem 0.5em; + } + + dl.variation { + margin: 0; + + p { + margin: 0; + } + + dt, + dd { + font-family: $headings; + + p { + padding-top: 1px; + font-family: $headings; + } + } + } +} + +.woocommerce-order-received { + + .woocommerce-order { + + p, + li { + font-family: $headings; + } + } + + table { + border: 0; + + td, + th, + tr { + border: 0; + } + + tr { + height: 5rem; + } + + tfoot { + border-top: 1px solid #ddd; + + /* Targeting total */ + tr:last-of-type { + border-top: 1px solid #ddd; + + .woocommerce-Price-amount { + font-weight: 700; + } + } + } + + } +} + +.woocommerce-checkout-review-order { + + ul { + margin: 2rem 0 3rem; + padding-left: 0; + } + + #place_order { + width: 100%; + } +} + +.wc_payment_method { + list-style: none; + + .payment_box { + padding: 1rem; + background: #eee; + + ul, + ol { + + &:last-of-type { + margin-bottom: 0; + } + } + + fieldset { + padding: 1.5rem; + padding-bottom: 0; + border: 0; + background: #f6f6f6; + } + + li { + list-style: none; + } + + p { + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + } + } + + > label:first-of-type { + display: block; + margin: 1rem 0; + + img { + max-height: 24px; + max-width: 200px; + float: right; + } + } + + label { + cursor: pointer; + } + + input.input-radio[name="payment_method"] { + display: none; + + & + label { + font-family: $headings; + + &::before { + content: ""; + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid #fff; + box-shadow: 0 0 0 2px #6d6d6d; + background: #fff; + margin-left: 4px; + margin-right: 1.2rem; + border-radius: 100%; + transform: translateY(2px); + } + } + + &:checked + label { + + &::before { + background: #555; + } + } + } +} + +.wc_payment_methods { + + .payment_box { + + p { + font-family: $headings; + font-size: 1.6rem; + } + } +} + + +.woocommerce-terms-and-conditions-wrapper { + margin-bottom: 5rem; + + .woocommerce-privacy-policy-text { + + p { + font-family: $headings; + font-size: 1.6rem; + } + } +} + +.woocommerce-order-overview { + margin-bottom: 2rem; +} + +.woocommerce-table--order-details { + margin-bottom: 2rem; +} + +/** + * Layout stuff + */ +.woocommerce { + + section { + padding-top: 2rem; + padding-bottom: 0; + } + + .content-area { + + .site-main { + margin: 0 5vw; + } + } + + /* Shop layout */ + ul.products { + display: flex; + align-items: stretch; + flex-direction: row; + flex-wrap: wrap; + + li.product { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 5em; + + } + + li.product-category { + + a { + text-align: center; + + h2.woocommerce-loop-category__title { + font-family: $headings; + font-size: 3rem; + } + } + } + } +} + +@media only screen and (max-width: 600px) { + + .woocommerce { + + .woocommerce-ordering { + float: left; + clear: both; + } + } +} + +@media only screen and (max-width: 667px) { + + .woocommerce, + .woocommerce-page { + + ul.products[class*=columns-] { + + li.product { + width: 100%; + } + } + } +} + +@media only screen and (min-width: 668px) and (max-width: 768px) { + + .woocommerce, + .woocommerce-page { + + ul.products[class*=columns-] { + + li.product { + width: 50%; + } + + li.product:nth-of-type(2n+1) { + padding: 0 2vw 3em 0; + } + + li.product:nth-of-type(2n) { + padding: 0 0 3em 2vw; + } + } + } +} + +@media only screen and (max-width: 768px) { + + #site-content { + + .woocommerce { + + .woocommerce-cart-form { + + .actions { + + .coupon { + margin-bottom: 2rem; + + button { + width: 100%; + } + } + } + + #coupon_code { + width: 100% !important; + } + } + } + + #shipping_method { + + li { + display: flex; + justify-content: flex-end; + } + } + } + + .woocommerce, + .woocommerce-page { + + table.shop_table_responsive { + + tr { + margin: 0 0 1.5rem; + + &:first-child { + border-top: 1px solid; + } + + &:last-child { + margin-bottom: 0; + } + + &:nth-child(2n) { + + td { + background: #fff; + } + } + + td { + border-bottom-width: 0; + + &:last-child { + border-bottom-width: 1px; + } + } + + td.product-quantity::before { + padding-top: 0.9rem; + } + + .product-remove { + float: right; + } + + .product-thumbnail { + display: block; + + img { + width: 70px; + } + + &::before { + content: ""; + } + } + } + + } + + .woocommerce-breadcrumb { + margin-bottom: 4rem; + font-size: 0.8em; + font-family: $headings; + } + + .related.products { + + ul.products { + display: flex; + flex-direction: column; + align-items: flex-start; + + li.product { + margin-bottom: 5em; + } + } + } + + .woocommerce-products-header__title.page-title { + margin: 3rem auto 4rem; + } + + .woocommerce-result-count, + .woocommerce-ordering { + font-size: 0.8em; + } + + .woocommerce-ordering { + margin-bottom: 3rem; + } + } + + .woocommerce-cart-form { + + table { + + td.product-name { + padding-left: 0.5em; + } + + input.qty { + padding: 1rem 1.5rem; + } + } + } + + .woocommerce-checkout { + + form { + + .col2-set { + width: 100%; + float: none; + padding-right: 0; + + .col-1, + .col-2 { + float: none; + width: 100%; + } + } + + #order_review_heading { + margin-top: 4rem; + } + + #order_review_heading, + #order_review { + width: 100%; + padding-left: 0; + float: none; + } + + table { + + tbody { + + td.product-total { + text-align: end; + } + } + + tfoot { + + .cart-subtotal, + .order-total { + + td { + text-align: end; + } + } + } + } + } + } + + .logged-in.woocommerce-account { + + #site-content { + + .woocommerce { + flex-direction: column; + } + + .woocommerce-MyAccount-navigation, + .woocommerce-MyAccount-content { + width: 100%; + } + + table.account-orders-table { + + .button { + padding-left: 0.5em; + padding-right: 0.5em; + width: 100%; + margin: 2rem 0; + } + } + } + + table.account-orders-table { + + td { + padding-bottom: 1.5rem; + } + } + } +} + +@media only screen and (min-width: 768px) { + + /** + * Tables + */ + .woocommerce, + .woocommerce-page { + + table.shop_table { + + tbody { + + tr { + font-size: 0.88889em; + } + } + } + + .onsale { + font-size: 1.5rem; + padding: 1rem; + } + } + + /** + * Shop page + */ + .woocommerce-products-header__title.page-title { + font-size: 8.4rem; + font-weight: 800; + } + + .woocommerce-pagination { + + span.page-numbers, + a.page-numbers, + .next.page-numbers, + .prev.page-numbers { + padding: 1rem; + } + } + + /** + * Account section + */ + .woocommerce-account { + + .woocommerce-MyAccount-navigation { + float: none; + width: 20%; + margin-bottom: 1.5rem; + margin-right: 3rem; + + li { + margin: 0 1rem 3rem 0; + padding: 0; + border-bottom: 0; + + &:last-child { + margin-right: 0; + } + } + } + + .woocommerce-MyAccount-content { + float: none; + width: 75%; + } + + table.account-orders-table { + margin-top: 0; + border: 0; + + tr, + td, + th { + border: 0; + padding: 0; + } + + th, + td, + td.woocommerce-orders-table__cell-order-actions { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + + thead { + border-bottom: 1px solid #ddd; + } + + .button { + padding-left: 0.5em; + padding-right: 0.5em; + width: 100%; + margin: 1.5rem 0; + } + } + + .woocommerce-ResetPassword { + .woocommerce-form-row--first { + float: none; + } + } + } + + /** + * Layout stuff + */ + .woocommerce { + + .content-area { + margin: 0 auto; + padding: 2vw 6vw; + + .site-main { + margin: 0; + } + } + } + + .single-product { + + .entry { + + .entry-content, + .entry-summary { + max-width: none; + margin: 0 0 3rem; + padding: 0; + + > * { + max-width: none; + } + } + } + } + + .woocommerce-breadcrumb { + margin-bottom: 5rem; + font-size: 0.88889em; + font-family: $headings; + } + + .woocommerce-product-gallery { + margin-bottom: 8rem; + } + + .woocommerce-checkout { + + #site-content { + + .woocommerce { + + max-width: 1600px; + padding: 0 6vw; + margin: 0 auto; + } + } + } + +} + +@media only screen and (min-width: 1168px) { + + .woocommerce { + + .content-area { + max-width: 1600px; + padding: 4vw 6vw; + margin: 0 auto; + } + + .onsale { + font-size: 1.7rem; + padding: 1.5rem; + } + } + + .woocommerce-breadcrumb { + margin-bottom: 5rem; + font-size: 0.88889em; + font-family: $headings; + } + + .woocommerce-product-gallery { + margin-bottom: 8rem; + } + + .woocommerce-account { + + table.account-orders-table { + + th, + td, + td.woocommerce-orders-table__cell-order-actions { + padding-right: 1.5rem; + padding-left: 1.5rem; + } + } + } +} diff --git a/plugins/woocommerce/legacy/css/wc-setup.scss b/plugins/woocommerce/legacy/css/wc-setup.scss new file mode 100644 index 00000000000..2a83916e96b --- /dev/null +++ b/plugins/woocommerce/legacy/css/wc-setup.scss @@ -0,0 +1,1628 @@ +/* stylelint-disable no-descending-specificity */ + +/* @deprecated 4.6.0 */ +body { + margin: 65px auto 24px; + box-shadow: none; + background: #f1f1f1; + padding: 0; +} + +.wc-logo { + border: 0; + margin: 0 0 24px; + padding: 0; + text-align: center; + + img { + max-width: 30%; + } +} + +.wc-setup { + text-align: center; + + #wc_tracker_checkbox { + display: none; + } + + .select2-container { + text-align: left; + width: auto; + } + + .hidden { + display: none; + } + + #tiptip_content { + background: #5f6973; + } + + #tiptip_holder.tip_top #tiptip_arrow_inner { + border-top-color: #5f6973; + } +} + +.wc-setup-content { + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.13); + padding: 2em; + margin: 0 0 20px; + background: #fff; + overflow: hidden; + zoom: 1; + text-align: left; + + h1, + h2, + h3, + table { + margin: 0 0 20px; + border: 0; + padding: 0; + color: #666; + clear: none; + font-weight: 500; + } + + p { + margin: 20px 0; + font-size: 1em; + line-height: 1.75; + color: #666; + } + + table { + font-size: 1em; + line-height: 1.75; + color: #666; + } + + a { + color: #a16696; + + &:hover, + &:focus { + color: #111; + } + } + + .form-table { + + th { + width: 35%; + vertical-align: top; + font-weight: 400; + } + + td { + vertical-align: top; + + select, + input { + width: 100%; + box-sizing: border-box; + } + + input[size] { + width: auto; + } + + .description { + line-height: 1.5; + display: block; + margin-top: 0.25em; + color: #999; + font-style: italic; + } + + .input-checkbox, + .input-radio { + width: auto; + box-sizing: inherit; + padding: inherit; + margin: 0 0.5em 0 0; + box-shadow: none; + } + } + + .section_title { + + td { + padding: 0; + + h2, + p { + margin: 12px 0 0; + } + } + } + + th, + td { + padding: 12px 0; + margin: 0; + border: 0; + + &:first-child { + padding-right: 1em; + } + } + } + + table.tax-rates { + width: 100%; + font-size: 0.92em; + + th { + padding: 0; + text-align: center; + width: auto; + vertical-align: middle; + } + + td { + border: 1px solid #f5f5f5; + padding: 6px; + text-align: center; + vertical-align: middle; + + input { + outline: 0; + border: 0; + padding: 0; + box-shadow: none; + text-align: center; + width: 100%; + } + + &.sort { + cursor: move; + color: #ccc; + + &::before { + content: "\f333"; + font-family: dashicons; /* stylelint-disable-line font-family-no-missing-generic-family-keyword */ + } + } + + &.readonly { + background: #f5f5f5; + } + } + + .add { + padding: 1em 0 0 1em; + line-height: 1; + font-size: 1em; + width: 0; + margin: 6px 0 0; + height: 0; + overflow: hidden; + position: relative; + display: inline-block; + + &::before { + content: "\f502"; + font-family: dashicons; /* stylelint-disable-line font-family-no-missing-generic-family-keyword */ + position: absolute; + left: 0; + top: 0; + } + } + + .remove { + padding: 1em 0 0 1em; + line-height: 1; + font-size: 1em; + width: 0; + margin: 0; + height: 0; + overflow: hidden; + position: relative; + display: inline-block; + + &::before { + content: "\f182"; + font-family: dashicons; /* stylelint-disable-line font-family-no-missing-generic-family-keyword */ + position: absolute; + left: 0; + top: 0; + } + } + } + + .wc-setup-pages { + width: 100%; + border-top: 1px solid #eee; + + thead th { + display: none; + } + + .page-name { + width: 30%; + font-weight: 700; + } + + th, + td { + padding: 14px 0; + border-bottom: 1px solid #eee; + + &:first-child { + padding-right: 9px; + } + } + + th { + padding-top: 0; + } + + .page-options { + + p { + color: #777; + margin: 6px 0 0 24px; + line-height: 1.75; + + input { + vertical-align: middle; + margin: 1px 0 0; + height: 1.75em; + width: 1.75em; + line-height: 1.75; + } + + label { + line-height: 1; + } + } + } + } + + @media screen and (max-width: 782px) { + + .form-table { + + tbody { + + th { + width: auto; + } + } + } + } + + .twitter-share-button { + float: right; + } + + .wc-setup-next-steps { + overflow: hidden; + margin: 0 0 24px; + padding-bottom: 2px; + + h2 { + margin-bottom: 12px; + } + + .wc-setup-next-steps-first { + float: left; + width: 50%; + box-sizing: border-box; + } + + .wc-setup-next-steps-last { + float: right; + width: 50%; + box-sizing: border-box; + } + + ul { + padding: 0 2em 0 0; + list-style: none outside; + margin: 0; + + li a { + display: block; + padding: 0 0 0.75em; + } + + .setup-product { + + a.button { + background-color: #f7f7f7; + border-color: #ccc; + color: #23282d; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #ccc; + text-shadow: 1px 0 1px #eee, 0 1px 1px #eee; + font-size: 1em; + height: auto; + line-height: 1.75; + margin: 0 0 0.75em; + opacity: 1; + padding: 1em; + text-align: center; + + &:hover, + &:focus, + &:active { + background: #f5f5f5; + border-color: #aaa; + } + } + + a.button-primary { + color: #fff; + background-color: #bb77ae; + border-color: #a36597; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; + text-shadow: 0 -1px 1px #a36597, 1px 0 1px #a36597, 0 1px 1px #a36597, -1px 0 1px #a36597; + + &:hover, + &:focus, + &:active { + color: #fff; + background: #a36597; + border-color: #a36597; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; + } + } + } + + li a::before { + color: #82878c; + font: 400 20px/1 dashicons; /* stylelint-disable-line font-family-no-missing-generic-family-keyword */ + speak: never; + display: inline-block; + padding: 0 10px 0 0; + top: 1px; + position: relative; + text-decoration: none !important; + vertical-align: top; + } + + .learn-more a::before { + content: "\f105"; + } + + .video-walkthrough a::before { + content: "\f126"; + } + + .newsletter a::before { + content: "\f465"; + } + } + } + + .woocommerce-newsletter, + .updated { + padding: 24px 24px 0; + margin: 0 0 24px; + overflow: hidden; + background: #f5f5f5; + + p { + padding: 0; + margin: 0 0 12px; + } + + form, + p:last-child { + margin: 0 0 24px; + } + } + + .checkbox { + + input[type="checkbox"] { + opacity: 0; + position: absolute; + left: -9999px; + } + + label { + position: relative; + display: inline-block; + padding-left: 28px; + + &::before, + &::after { + position: absolute; + content: ""; + display: inline-block; + } + + &::before { + height: 16px; + width: 16px; + left: 0; + top: 3px; + border: 1px solid #aaa; + background-color: #fff; + border-radius: 3px; + } + + &::after { + height: 5px; + width: 9px; + border-left: 2px solid; + border-bottom: 2px solid; + transform: rotate(-45deg); + left: 4px; + top: 7px; + color: #fff; + } + } + + input[type="checkbox"] + label::after { + content: none; + } + + input[type="checkbox"]:checked + label::after { + content: ""; + } + + input[type="checkbox"]:focus + label::before { + outline: rgb(59, 153, 252) auto 5px; + } + + input[type="checkbox"]:checked + label::before { + background: #935687; + border-color: #935687; + } + } +} + +.woocommerce-tracker { + margin: 24px 0; + border: 1px solid #eee; + padding: 20px; + border-radius: 4px; + overflow: hidden; + text-align: left; + + h1 { + border-bottom: 0; + padding-bottom: 0; + } + + .wc-backbone-modal-header { + border-bottom: 0; + } + + .wc-backbone-modal-main article { + padding-top: 0; + } + + .wc-backbone-modal-main footer { + border-top: 0; + box-shadow: none; + } + + p { + font-size: 14px; + line-height: 1.5; + } + + .woocommerce-tracker-checkbox label { + margin-top: -4px; + display: inline-block; + } +} + +.wc-setup-steps { + padding: 0 0 24px; + margin: 0; + list-style: none outside; + overflow: hidden; + color: #ccc; + width: 100%; + display: inline-flex; + + li { + width: 100%; + float: left; + padding: 0 0 0.8em; + margin: 0; + text-align: center; + position: relative; + border-bottom: 4px solid #ccc; + line-height: 1.4; + + a { + color: #a16696; + text-decoration: none; + padding: 1.5em; + margin: -1.5em; + position: relative; + z-index: 1; + + &:hover, + &:focus { + color: #111; + text-decoration: underline; + } + } + } + + li::before { + content: ""; + border: 4px solid #ccc; + border-radius: 100%; + width: 4px; + height: 4px; + position: absolute; + bottom: 0; + left: 50%; + margin-left: -6px; + margin-bottom: -8px; + background: #fff; + } + + li.active { + border-color: #a16696; + color: #a16696; + font-weight: 700; + + &::before { + border-color: #a16696; + } + } + + li.done { + border-color: #a16696; + color: #a16696; + + &::before { + border-color: #a16696; + background: #a16696; + } + } +} + +.wc-setup .wc-setup-actions { + overflow: hidden; + margin: 20px 0 0; + position: relative; +} + +.wc-setup .wc-setup-actions .button-primary, +.woocommerce-tracker .button-primary { + background-color: #bb77ae; + border-color: #a36597; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; + text-shadow: 0 -1px 1px #a36597, 1px 0 1px #a36597, 0 1px 1px #a36597, -1px 0 1px #a36597; + margin: 0; + opacity: 1; + + &:hover, + &:focus, + &:active { + background: #a36597; + border-color: #a36597; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; + } +} + +.wc-setup-content p:last-child { + margin-bottom: 0; +} + +.wc-setup-content p.store-setup { + margin-top: 0; +} + +.wc-setup-footer-links { + font-size: 0.85em; + color: #7b7b7b; + margin: 1.18em auto; + display: inline-block; + text-align: center; +} + +.wc-wizard-storefront { + + .wc-wizard-storefront-intro { + padding: 40px 40px 0; + background: #f5f5f5; + text-align: center; + + img { + margin: 40px 0 0 0; + width: 100%; + display: block; + } + } + + .wc-wizard-storefront-features { + list-style: none outside; + margin: 0 0 20px; + padding: 0 0 0 30px; + overflow: hidden; + } + + .wc-wizard-storefront-feature { + margin: 0; + padding: 20px 30px 20px 2em; + width: 50%; + box-sizing: border-box; + + &::before { + margin-left: -2em; + position: absolute; + } + + &.first { + clear: both; + float: left; + } + + &.last { + float: right; + } + } + + .wc-wizard-storefront-feature__bulletproof::before { + content: "🔒"; + } + + .wc-wizard-storefront-feature__mobile::before { + content: "📱"; + } + + .wc-wizard-storefront-feature__accessibility::before { + content: "👓"; + } + + .wc-wizard-storefront-feature__search::before { + content: "🔍"; + } + + .wc-wizard-storefront-feature__compatibility::before { + content: "🔧"; + } + + .wc-wizard-storefront-feature__extendable::before { + content: "🎨"; + } +} + +// List of services +.wc-wizard-services { + border: 1px solid #eee; + padding: 0; + margin: 0 0 1em; + list-style: none outside; + border-radius: 4px; + overflow: hidden; // Make sure dark background doesn't cover curved border + + p { + margin: 0 0 1em 0; + padding: 0; + font-size: 1em; + line-height: 1.5; + } +} + +.wc-wizard-service-item, +.wc-wizard-services-list-toggle { + display: flex; + flex-wrap: nowrap; + justify-content: space-between; + padding: 0; + border-bottom: 1px solid #eee; + color: #666; + align-items: center; + + &:last-child { + border-bottom: 0; + } + + .payment-gateway-fee { + color: #a6a6a6; + } + + .wc-wizard-service-name { + flex-basis: 0; + min-width: 160px; + text-align: center; + font-weight: 700; + padding: 2em 0; + align-self: stretch; + display: flex; + align-items: baseline; + + .wc-wizard-payment-gateway-form & { + justify-content: center; + } + + img { + max-width: 75px; + } + } + + &.stripe-logo .wc-wizard-service-name img { + padding: 8px 0; + } + + &.paypal-logo .wc-wizard-service-name img { + max-width: 87px; + padding: 2px 0; + } + + &.klarna-logo .wc-wizard-service-name img { + max-width: 87px; + padding: 12px 0; + } + + &.square-logo .wc-wizard-service-name img { + max-width: 95px; + padding: 12px 0; + } + + &.eway-logo .wc-wizard-service-name img { + max-width: 87px; + } + + &.payfast-logo .wc-wizard-service-name img { + max-width: 140px; + } + + .wc-wizard-service-description { + flex-grow: 1; + padding: 20px; + + p { + margin-bottom: 1em; + } + + p:last-child { + margin-bottom: 0; + } + + .wc-wizard-service-settings-description { + display: block; + font-style: italic; + color: #999; + } + } + + .wc-wizard-service-enable { + flex-basis: 0; + min-width: 75px; + text-align: center; + cursor: pointer; + padding: 2em 0; + position: relative; + max-height: 1.5em; + align-self: flex-start; + order: 3; + } + + .wc-wizard-service-toggle { + height: 16px; + width: 32px; + border: 2px solid #935687; + background-color: #935687; + display: inline-block; + text-indent: -9999px; + border-radius: 10em; + position: relative; + + input[type="checkbox"] { + display: none; + } + + &::before { + content: ""; + display: block; + width: 16px; + height: 16px; + background: #fff; + position: absolute; + top: 0; + right: 0; + border-radius: 100%; + } + + &.disabled { + border-color: #999; + background-color: #999; + + &::before { + right: auto; + left: 0; + } + } + } + + .wc-wizard-service-settings { + display: none; + margin-top: 0.75em; + margin-bottom: 0; + cursor: default; + + &.hide { + display: none; + } + } + + &.checked { + + .wc-wizard-service-settings { + display: inline-block; + + &.hide { + display: none; + } + } + } + + &.closed { + border-bottom: 0; + } +} + +// Toggle display a list of services. +.wc-wizard-services-list-toggle { + cursor: pointer; + + .wc-wizard-service-enable::before { + content: "\f343"; // up chevron + font-family: dashicons; /* stylelint-disable-line font-family-no-missing-generic-family-keyword */ + color: #666; + font-size: 25px; + margin-top: -7px; + margin-left: -5px; + position: absolute; + visibility: visible; + } + + &.closed { + + .wc-wizard-service-enable::before { + content: "\f347"; // down chevron + } + } + + .wc-wizard-service-enable input { + visibility: hidden; + position: relative; + } +} + +.wc-wizard-services.manual .wc-wizard-service-item { + display: none; +} + +// Shipping list of services +.wc-wizard-services.shipping { + margin: 0; + + .wc-wizard-service-name { + font-weight: 400; + text-align: left; + align-items: center; + max-height: 5em; + padding: 0; + } + + // List header + .wc-wizard-service-item { + padding-left: 2em; + padding-top: 0.67em; + + &:first-child { + border-bottom: 0; + padding-bottom: 0; + font-weight: 700; + + .wc-wizard-service-name { + font-weight: 700; + } + } + } + + .wc-wizard-shipping-method-select, + .shipping-method-setting { + display: flex; + + &.hide { + display: none; + } + } + + .wc-wizard-shipping-method-dropdown, + .shipping-method-setting input { + margin-right: 2em; + margin-bottom: 1em; + + .select2 { + min-width: 130px; + } + } + + .wc-wizard-service-description { + display: flex; + flex-direction: column; + color: #a6a6a6; + } + + .wc-wizard-service-item:not(:first-child) .wc-wizard-service-description { + font-size: 0.92em; + padding-bottom: 10px; + } + + .shipping-method-setting { + + input { + width: 95px; // match dropdown height + border: 1px solid #aaa; + border-color: #ddd; + border-radius: 4px; + height: 28px; + padding-left: 8px; + padding-right: 24px; + font-size: 14px; + color: #444; + background-color: #fff; + display: inline-block; + } + } + + .shipping-method-description, + .shipping-method-setting .description { + color: #7e7e7e; + font-size: 0.9em; + } + + .shipping-method-setting input::placeholder { + color: #e1e1e1; + } +} + +.wc-setup-shipping-units { + + p { + line-height: 1.5; + font-size: 13px; + margin-bottom: 0.25em; + text-align: center; + } + + .wc-setup-shipping-unit { + margin-bottom: 1.75em; + + .select2 { + min-width: 125px; // widen dropdowns + top: -5px; // vertically align dropdown value with surrounding text + } + } +} + +.hide { + display: none; +} + +.wc-wizard-features { + display: flex; + flex-wrap: wrap; + list-style: none; + padding: 0; + + .wc-wizard-feature-item { + flex-basis: calc(50% - 4em - 3px); // two columns, account for padding and borders + border: 1px solid #eee; + padding: 2em; + } + + .wc-wizard-feature-item:nth-child(1) { + border-radius: 4px 0 0 0; + } + + .wc-wizard-feature-item:nth-child(2) { + border-left: 0; + border-radius: 0 4px 0 0; + } + + .wc-wizard-feature-item:nth-child(3) { + border-top: 0; + border-radius: 0 0 0 4px; + } + + .wc-wizard-feature-item:nth-child(4) { + border-top: 0; + border-left: 0; + border-radius: 0 0 4px 0; + } + + p.wc-wizard-feature-name, + p.wc-wizard-feature-description { + margin: 0; + line-height: 1.5; + } +} + +h3.jetpack-reasons { + text-align: center; + margin: 3em 0 1em 0; + font-size: 14px; +} + +.jetpack-logo, +.wcs-notice { + display: block; + margin: 1.75em auto 2em auto; + max-height: 175px; +} + +.activate-splash { + + .jetpack-logo { + width: 170px; + margin-bottom: 0; + } + + .wcs-notice { + margin-top: 1em; + padding-left: 57px; + } +} + +.wc-setup-step__new_onboarding { + + .wc-logo, + .wc-setup-steps { + display: none; + } + + .wc-setup-step__new_onboarding-wrapper { + + .wc-logo { + display: block; + } + + p { + text-align: center; + } + + .wc-setup-step__new_onboarding-welcome, + .wc-setup-step__new_onboarding-plugin-info { + color: #7c7c7c; + font-size: 12px; + } + } +} + +.step { + text-align: center; +} + +.wc-setup .wc-setup-actions .button { + font-weight: 300; + font-size: 16px; + padding: 1em 2em; + box-shadow: none; + min-width: 12em; + margin-top: 10px; + line-height: 1; + margin-right: 0.5em; + margin-bottom: 2px; + height: auto; + border-radius: 4px; + + &:focus, + &:hover, + &:active { + box-shadow: none; + } +} + +.wc-setup .wc-setup-actions .plugin-install-info { + display: block; + font-style: italic; + color: #999; + font-size: 14px; + line-height: 1.5; + margin: 5px 0; + + & > * { + display: block; + } + + .plugin-install-info-list-item::after { + content: ", "; + } + + .plugin-install-info-list-item:last-of-type::after { + content: ". "; + } + + a { + white-space: nowrap; + + &:not(:hover):not(:focus) { + color: inherit; + } + } +} + +.plugin-install-source { + $background: rgba(#bb77ae, 0.15); + background: $background; + + &:not(.wc-wizard-service-item) { + box-shadow: 0 0 0 10px $background; + } +} + +.location-prompt { + color: #666; + font-size: 13px; + font-weight: 500; + margin-bottom: 0.5em; + margin-top: 0.85em; + display: inline-block; +} + +.location-input { + border: 1px solid #aaa; + border-color: #ddd; + border-radius: 4px; + height: 30px; + width: calc(100% - 8px - 8px - 2px); + padding-left: 8px; + padding-right: 8px; + font-size: 16px; + color: #444; + background-color: #fff; + display: block; + + &.dropdown { + width: 100%; + } +} + +.branch-5-2, +.wc-wp-version-gte-53 { + + .location-input { + margin: 0; + width: 100%; + } +} + +.address-step { + + .select2 { + min-width: 100%; // widen currency, product type dropdowns + } +} + +.store-address-container { + + .city-and-postcode { + display: flex; + + div { + flex-basis: 50%; + margin-right: 1em; + + &:last-of-type { + margin-right: 0; + } + } + } + + input[type="text"], + select, + .select2-container { + margin-bottom: 10px; + } +} + +.product-type-container, +.sell-in-person-container { + margin-top: 14px; + margin-bottom: 1px; +} + +#woocommerce_sell_in_person { + margin-left: 0; + margin-top: calc(0.85em / 2); +} + +.wc-wizard-service-settings { + + .payment-email-input { + border: 1px solid #aaa; + border-color: #ddd; + border-radius: 4px; + height: 30px; + padding: 0 8px; + font-size: 14px; + color: #444; + background-color: #fff; + display: inline-block; + + &[disabled] { + color: #aaa; + } + } +} + +.newsletter-form-container { + display: flex; + + .newsletter-form-email { + border: 1px solid #aaa; + border-color: #ddd; + border-radius: 4px; + height: 42px; + padding: 0 8px; + font-size: 16px; + color: #666; + background-color: #fff; + display: inline-block; + margin-right: 6px; + flex-grow: 1; + } + + .newsletter-form-button-container { + flex-grow: 0; + } +} + +.wc-setup .wc-setup-actions .button.newsletter-form-button { + height: 42px; + padding: 0 1em; + margin: 0; +} + +.wc-wizard-next-steps { + border: 1px solid #eee; + border-radius: 4px; + list-style: none; + padding: 0; + + li { + padding: 0; + } + + .wc-wizard-next-step-item { + display: flex; + border-top: 1px solid #eee; + + &:first-child { + border-top: 0; + } + } + + .wc-wizard-next-step-description { + flex-grow: 1; + margin: 1.5em; + } + + .wc-wizard-next-step-action { + flex-grow: 0; + display: flex; + align-items: center; + + .button { + margin: 1em 1.5em; + } + } + + p { + + &.next-step-heading { + margin: 0; + font-size: 0.95em; + font-weight: 400; + font-variant: all-petite-caps; + } + + &.next-step-extra-info { + margin: 0; + } + } + + h3 { + + &.next-step-description { + margin: 0; + font-size: 16px; + font-weight: 600; + } + } + + .wc-wizard-additional-steps { + border-top: 1px solid #eee; + + .wc-wizard-next-step-description { + margin-bottom: 0; + } + + .wc-setup-actions { + margin: 0 0 1.5em 0; + + .button { + font-size: 15px; + margin: 1em 0 1em 1.5em; + } + } + } +} + +p.next-steps-help-text { + color: #9f9f9f; + padding: 0 2em; + text-align: center; + font-size: 0.9em; +} + +p.jetpack-terms { + font-size: 0.8em; + text-align: center; + max-width: 480px; + margin: 0 auto; + line-height: 1.5; +} + +.woocommerce-error { + background: #ffe6e5; + border-color: #ffc5c2; + padding: 1em; + margin-bottom: 1em; + + p { + margin-top: 0; + margin-bottom: 0.5em; + color: #444; + } + + a { + color: #ff645c; + } + + .reconnect-reminder { + font-size: 0.85em; + } + + .wc-setup-actions .button { + font-size: 14px; + } +} + +.wc-wizard-service-setting-stripe_create_account, +.wc-wizard-service-setting-ppec_paypal_reroute_requests { + display: flex; + align-items: flex-start; + + .payment-checkbox-input { + order: 1; + margin-top: 5px; + margin-left: 0; + margin-right: 0; + } + + .stripe_create_account, + .ppec_paypal_reroute_requests { + order: 2; + margin-left: 0.3em; + } +} + +.branch-5-2, +.wc-wp-version-gte-53 { + + .wc-wizard-service-setting-stripe_create_account, + .wc-wizard-service-setting-ppec_paypal_reroute_requests { + + .payment-checkbox-input { + margin-top: 3px; + } + } +} + +.wc-wizard-service-setting-stripe_email, +.wc-wizard-service-setting-ppec_paypal_email { + margin-top: 0.75em; + margin-left: 1.5em; + + label.stripe_email, + label.ppec_paypal_email { + position: absolute; + margin: -1px; + padding: 0; + height: 1px; + width: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + border: 0; + } + + input.payment-email-input { + box-sizing: border-box; + margin-bottom: 0.5em; + width: 100%; + height: 32px; + } +} + +.wc-setup-content .recommended-step { + border: 1px solid #ebebeb; + border-radius: 4px; + padding: 2.5em; +} + +.wc-setup-content .recommended-item { + list-style: none; + margin-bottom: 1.5em; + + &:last-child { + margin-bottom: 0; // Avoid extra space at the end of the list. + } + + label { + display: flex; + align-items: center; + + &::before, + &::after { + top: auto; + } + + &::after { + margin-top: -1.5px; + } + } + + .recommended-item-icon { + border: 1px solid #fff; + border-radius: 7px; + height: 3.5em; + margin-right: 1em; + margin-left: 4px; + + &.recommended-item-icon-wc_admin { + background-color: #7f54b3; + padding: 0.5em; + height: 2em; + } + + &.recommended-item-icon-storefront_theme { + background-color: #f4a224; + max-height: 3em; + max-width: 3em; + padding: calc( ( 3.5em - 3em ) / 2 ); + } + + &.recommended-item-icon-automated_taxes { + background-color: #d0011b; + max-height: 1.75em; + padding: calc( ( 3.5em - 1.75em ) / 2 ); + } + + &.recommended-item-icon-mailchimp { + background-color: #ffe01b; + height: 2em; + padding: calc( ( 3.5em - 2em ) / 2 ); + } + + &.recommended-item-icon-woocommerce_services { + background-color: #f0f0f0; + max-height: 1.5em; + padding: 1.3em 0.7em; + } + + &.recommended-item-icon-shipstation { + background-color: #f0f0f0; + padding: 0.3em; + } + } + + .recommended-item-description-container { + + h3 { + font-size: 15px; + font-weight: 700; + letter-spacing: 0.5px; + margin-bottom: 0; + } + + p { + margin-top: 0; + line-height: 1.5; + } + } +} + +.wc-wizard-service-info { + padding: 1em 2em; + background-color: #fafafa; +} + +.help_tip { + text-decoration: underline dotted; +} + +@media only screen and (max-width: 400px) { + + .wc-logo img { + max-width: 80%; + } + + .wc-setup-steps { + display: none; + } + + .store-address-container { + + .city-and-postcode { + display: block; + + div { + margin-right: 0; + } + } + } + + .wc-wizard-service-item, + .wc-wizard-services-list-toggle { + flex-wrap: wrap; + + .wc-wizard-service-enable { + order: 2; + padding: 20px 0 0; + } + + .wc-wizard-service-description { + order: 3; + } + + .wc-wizard-service-name { + padding: 20px 20px 0; + text-align: left; + justify-content: space-between !important; + + img { + margin: 0; + } + } + } + + .newsletter-form-container { + display: block; + + .newsletter-form-email { + display: block; + box-sizing: border-box; + width: 100%; + margin-bottom: 10px; + } + + .button.newsletter-form-button { + float: left; + } + } + + .wc-wizard-next-steps .wc-wizard-next-step-item { + flex-wrap: wrap; + + .wc-wizard-next-step-description { + margin-bottom: 0; + } + + .wc-wizard-next-step-action { + + p { + margin: 0; + } + } + } +} +/* stylelint-enable */ diff --git a/assets/css/woocommerce-layout.scss b/plugins/woocommerce/legacy/css/woocommerce-layout.scss similarity index 100% rename from assets/css/woocommerce-layout.scss rename to plugins/woocommerce/legacy/css/woocommerce-layout.scss diff --git a/assets/css/woocommerce-smallscreen.scss b/plugins/woocommerce/legacy/css/woocommerce-smallscreen.scss similarity index 100% rename from assets/css/woocommerce-smallscreen.scss rename to plugins/woocommerce/legacy/css/woocommerce-smallscreen.scss diff --git a/plugins/woocommerce/legacy/css/woocommerce.scss b/plugins/woocommerce/legacy/css/woocommerce.scss new file mode 100644 index 00000000000..da71b3321ed --- /dev/null +++ b/plugins/woocommerce/legacy/css/woocommerce.scss @@ -0,0 +1,2332 @@ +/** + * woocommerce.scss + * Governs the general look and feel of WooCommerce sections of stores using themes that do not + * integrate with WooCommerce specifically. + */ + +/** + * Imports + */ +@import "mixins"; +@import "variables"; +@import "animation"; +@import "fonts"; + +/** + * Global styles + */ +p.demo_store, +.woocommerce-store-notice { + position: absolute; + top: 0; + left: 0; + right: 0; + margin: 0; + width: 100%; + font-size: 1em; + padding: 1em 0; + text-align: center; + background-color: $primary; + color: $primarytext; + z-index: 99998; + box-shadow: 0 1px 1em rgba(0, 0, 0, 0.2); + display: none; + + a { + color: $primarytext; + text-decoration: underline; + } +} + +.screen-reader-text { + clip: rect(1px, 1px, 1px, 1px); + height: 1px; + overflow: hidden; + position: absolute !important; + width: 1px; + word-wrap: normal !important; +} + +.admin-bar p.demo_store { + top: 32px; +} + +/** + * Utility classes + */ +.clear { + clear: both; +} + +/** + * Main WooCommerce styles + */ +.woocommerce { + + .blockUI.blockOverlay { + position: relative; + + @include loader(); + } + + .loader { + + @include loader(); + } + + a.remove { + display: block; + font-size: 1.5em; + height: 1em; + width: 1em; + text-align: center; + line-height: 1; + border-radius: 100%; + color: red !important; // Required for default theme compatibility + text-decoration: none; + font-weight: 700; + border: 0; + + &:hover { + color: #fff !important; // Required for default theme compatibility + background: red; + } + } + + small.note { + display: block; + color: $subtext; + font-size: 0.857em; + margin-top: 10px; + } + + .woocommerce-breadcrumb { + + @include clearfix(); + margin: 0 0 1em; + padding: 0; + font-size: 0.92em; + color: $subtext; + + a { + color: $subtext; + } + } + + .quantity .qty { + width: 3.631em; + text-align: center; + } + + /** + * Product Page + */ + div.product { + margin-bottom: 0; + position: relative; + + .product_title { + clear: none; + margin-top: 0; + padding: 0; + } + + span.price, + p.price { + color: $highlight; + font-size: 1.25em; + + ins { + background: inherit; + font-weight: 700; + display: inline-block; + } + + del { + opacity: 0.5; + display: inline-block; + } + } + + p.stock { + font-size: 0.92em; + } + + .stock { + color: $highlight; + } + + .out-of-stock { + color: red; + } + + .woocommerce-product-rating { + margin-bottom: 1.618em; + } + + div.images { + margin-bottom: 2em; + + img { + display: block; + width: 100%; + height: auto; + box-shadow: none; + } + + div.thumbnails { + padding-top: 1em; + } + + &.woocommerce-product-gallery { + position: relative; + } + + .woocommerce-product-gallery__wrapper { + transition: all cubic-bezier(0.795, -0.035, 0, 1) 0.5s; + margin: 0; + padding: 0; + } + + .woocommerce-product-gallery__wrapper .zoomImg { + background-color: #fff; + opacity: 0; + } + + .woocommerce-product-gallery__image--placeholder { + border: 1px solid #f2f2f2; + } + + .woocommerce-product-gallery__image:nth-child(n+2) { + width: 25%; + display: inline-block; + } + + .woocommerce-product-gallery__trigger { + position: absolute; + top: 0.5em; + right: 0.5em; + font-size: 2em; + z-index: 9; + width: 36px; + height: 36px; + background: #fff; + text-indent: -9999px; + border-radius: 100%; + box-sizing: content-box; + + &::before { + content: ""; + display: block; + width: 10px; + height: 10px; + border: 2px solid #000; + border-radius: 100%; + position: absolute; + top: 9px; + left: 9px; + box-sizing: content-box; + } + + &::after { + content: ""; + display: block; + width: 2px; + height: 8px; + background: #000; + border-radius: 6px; + position: absolute; + top: 19px; + left: 22px; + transform: rotate(-45deg); + box-sizing: content-box; + } + } + + .flex-control-thumbs { + overflow: hidden; + zoom: 1; + margin: 0; + padding: 0; + + li { + width: 25%; + float: left; + margin: 0; + list-style: none; + + img { + cursor: pointer; + opacity: 0.5; + margin: 0; + + &.flex-active, + &:hover { + opacity: 1; + } + } + } + } + } + + .woocommerce-product-gallery--columns-3 { + + .flex-control-thumbs li:nth-child(3n+1) { + clear: left; + } + } + + .woocommerce-product-gallery--columns-4 { + + .flex-control-thumbs li:nth-child(4n+1) { + clear: left; + } + } + + .woocommerce-product-gallery--columns-5 { + + .flex-control-thumbs li:nth-child(5n+1) { + clear: left; + } + } + + div.summary { + margin-bottom: 2em; + } + + div.social { + text-align: right; + margin: 0 0 1em; + + span { + margin: 0 0 0 2px; + + span { + margin: 0; + } + + .stButton .chicklets { + padding-left: 16px; + width: 0; + } + } + + iframe { + float: left; + margin-top: 3px; + } + } + + .woocommerce-tabs { + + ul.tabs { + list-style: none; + padding: 0 0 0 1em; + margin: 0 0 1.618em; + overflow: hidden; + position: relative; + + li { + border: 1px solid darken($secondary, 10%); + background-color: $secondary; + display: inline-block; + position: relative; + z-index: 0; + border-radius: 4px 4px 0 0; + margin: 0 -5px; + padding: 0 1em; + + a { + display: inline-block; + padding: 0.5em 0; + font-weight: 700; + color: $secondarytext; + text-decoration: none; + + &:hover { + text-decoration: none; + color: lighten($secondarytext, 10%); + } + } + + &.active { + background: $contentbg; + z-index: 2; + border-bottom-color: $contentbg; + + a { + color: inherit; + text-shadow: inherit; + } + + &::before { + box-shadow: 2px 2px 0 $contentbg; + } + + &::after { + box-shadow: -2px 2px 0 $contentbg; + } + } + + &::before, + &::after { + border: 1px solid darken($secondary, 10%); + position: absolute; + bottom: -1px; + width: 5px; + height: 5px; + content: " "; + box-sizing: border-box; + } + + &::before { + left: -5px; + border-bottom-right-radius: 4px; + border-width: 0 1px 1px 0; + box-shadow: 2px 2px 0 $secondary; + } + + &::after { + right: -5px; + border-bottom-left-radius: 4px; + border-width: 0 0 1px 1px; + box-shadow: -2px 2px 0 $secondary; + } + } + + &::before { + position: absolute; + content: " "; + width: 100%; + bottom: 0; + left: 0; + border-bottom: 1px solid darken($secondary, 10%); + z-index: 1; + } + } + + .panel { + margin: 0 0 2em; + padding: 0; + } + } + + p.cart { + margin-bottom: 2em; + + @include clearfix(); + } + + form.cart { + margin-bottom: 2em; + + @include clearfix(); + + div.quantity { + float: left; + margin: 0 4px 0 0; + } + + table { + border-width: 0 0 1px; + + td { + padding-left: 0; + } + + div.quantity { + float: none; + margin: 0; + } + + small.stock { + display: block; + float: none; + } + } + + .variations { + margin-bottom: 1em; + border: 0; + width: 100%; + + td, + th { + border: 0; + line-height: 2em; + vertical-align: top; + } + + label { + font-weight: 700; + text-align: left; + } + + select { + max-width: 100%; + min-width: 75%; + display: inline-block; + margin-right: 1em; + } + + td.label { + padding-right: 1em; + } + } + + .woocommerce-variation-description p { + margin-bottom: 1em; + } + + .reset_variations { + visibility: hidden; + font-size: 0.83em; + } + + .wc-no-matching-variations { + display: none; + } + + .button { + vertical-align: middle; + float: left; + } + + .group_table { + + td.woocommerce-grouped-product-list-item__label { + padding-right: 1em; + padding-left: 1em; + } + + td { + vertical-align: top; + padding-bottom: 0.5em; + border: 0; + } + + td:first-child { + width: 4em; + text-align: center; + } + + .wc-grouped-product-add-to-cart-checkbox { + display: inline-block; + width: auto; + margin: 0 auto; + transform: scale(1.5, 1.5); + } + } + } + } + + span.onsale { + min-height: 3.236em; + min-width: 3.236em; + padding: 0.202em; + font-size: 1em; + font-weight: 700; + position: absolute; + text-align: center; + line-height: 3.236; + top: -0.5em; + left: -0.5em; + margin: 0; + border-radius: 100%; + background-color: $highlight; + color: $highlightext; + font-size: 0.857em; + z-index: 9; + } + + /** + * Product loops + */ + .products ul, + ul.products { + margin: 0 0 1em; + padding: 0; + list-style: none outside; + clear: both; + + @include clearfix(); + + li { + list-style: none outside; + } + } + + ul.products li.product { + + .onsale { + top: 0; + right: 0; + left: auto; + margin: -0.5em -0.5em 0 0; + } + + h3, + .woocommerce-loop-product__title, + .woocommerce-loop-category__title { + padding: 0.5em 0; + margin: 0; + font-size: 1em; + } + + a { + text-decoration: none; + } + + a img { + width: 100%; + height: auto; + display: block; + margin: 0 0 1em; + box-shadow: none; + } + + strong { + display: block; + } + + .woocommerce-placeholder { + border: 1px solid #f2f2f2; + } + + .star-rating { + font-size: 0.857em; + } + + .button { + margin-top: 1em; + } + + .price { + color: $highlight; + display: block; + font-weight: normal; + margin-bottom: 0.5em; + font-size: 0.857em; + + del { + color: inherit; + opacity: 0.5; + display: inline-block; + } + + ins { + background: none; + font-weight: 700; + display: inline-block; + } + + .from { + font-size: 0.67em; + margin: -2px 0 0 0; + text-transform: uppercase; + color: rgba(desaturate($highlight, 75%), 0.5); + } + } + } + + .woocommerce-result-count { + margin: 0 0 1em; + } + + .woocommerce-ordering { + margin: 0 0 1em; + + select { + vertical-align: top; + } + } + + nav.woocommerce-pagination { + text-align: center; + + ul { + display: inline-block; + white-space: nowrap; + padding: 0; + clear: both; + border: 1px solid darken($secondary, 10%); + border-right: 0; + margin: 1px; + + li { + border-right: 1px solid darken($secondary, 10%); + padding: 0; + margin: 0; + float: left; + display: inline; + overflow: hidden; + + a, + span { + margin: 0; + text-decoration: none; + padding: 0; + line-height: 1; + font-size: 1em; + font-weight: normal; + padding: 0.5em; + min-width: 1em; + display: block; + } + + span.current, + a:hover, + a:focus { + background: $secondary; + color: darken($secondary, 40%); + } + } + } + } + + /** + * Buttons + */ + a.button, + button.button, + input.button, + #respond input#submit { + font-size: 100%; + margin: 0; + line-height: 1; + cursor: pointer; + position: relative; + text-decoration: none; + overflow: visible; + padding: 0.618em 1em; + font-weight: 700; + border-radius: 3px; + left: auto; + color: $secondarytext; + background-color: $secondary; + border: 0; + display: inline-block; + background-image: none; + box-shadow: none; + text-shadow: none; + + &.loading { + opacity: 0.25; + padding-right: 2.618em; + + &::after { + font-family: "WooCommerce"; + content: "\e01c"; + vertical-align: top; + font-weight: 400; + position: absolute; + top: 0.618em; + right: 1em; + animation: spin 2s linear infinite; + } + } + + &.added::after { + font-family: "WooCommerce"; + content: "\e017"; + margin-left: 0.53em; + vertical-align: bottom; + } + + &:hover { + background-color: darken($secondary, 5%); + text-decoration: none; + background-image: none; + color: $secondarytext; + } + + &.alt { + background-color: $primary; + color: $primarytext; + -webkit-font-smoothing: antialiased; + + &:hover { + background-color: darken($primary, 5%); + color: $primarytext; + } + + &.disabled, + &:disabled, + &:disabled[disabled], + &.disabled:hover, + &:disabled:hover, + &:disabled[disabled]:hover { + background-color: $primary; + color: $primarytext; + } + } + + &:disabled, + &.disabled, + &:disabled[disabled] { + color: inherit; + cursor: not-allowed; + opacity: 0.5; + padding: 0.618em 1em; + + &:hover { + color: inherit; + background-color: $secondary; + } + } + } + + .cart .button, + .cart input.button { + float: none; + } + + a.added_to_cart { + padding-top: 0.5em; + display: inline-block; + } + + /** + * Reviews + */ + #reviews { + + h2 small { + float: right; + color: $subtext; + font-size: 15px; + margin: 10px 0 0; + + a { + text-decoration: none; + color: $subtext; + } + } + + h3 { + margin: 0; + } + + #respond { + margin: 0; + border: 0; + padding: 0; + } + + #comment { + height: 75px; + } + + #comments { + + .add_review { + + @include clearfix(); + } + + h2 { + clear: none; + } + + ol.commentlist { + + @include clearfix(); + margin: 0; + width: 100%; + background: none; + list-style: none; + + li { + padding: 0; + margin: 0 0 20px; + border: 0; + position: relative; + background: 0; + border: 0; + + .meta { + color: $subtext; + font-size: 0.75em; + } + + img.avatar { + float: left; + position: absolute; + top: 0; + left: 0; + padding: 3px; + width: 32px; + height: auto; + background: $secondary; + border: 1px solid darken($secondary, 3%); + margin: 0; + box-shadow: none; + } + + .comment-text { + margin: 0 0 0 50px; + border: 1px solid darken($secondary, 3%); + border-radius: 4px; + padding: 1em 1em 0; + + @include clearfix(); + + p { + margin: 0 0 1em; + } + + p.meta { + font-size: 0.83em; + } + } + } + + ul.children { + list-style: none outside; + margin: 20px 0 0 50px; + + .star-rating { + display: none; + } + } + + #respond { + border: 1px solid darken($secondary, 3%); + border-radius: 4px; + padding: 1em 1em 0; + margin: 20px 0 0 50px; + } + } + + .commentlist > li::before { + content: ""; + } + } + } + + /** + * Star ratings + */ + .star-rating { + float: right; + overflow: hidden; + position: relative; + height: 1em; + line-height: 1; + font-size: 1em; + width: 5.4em; + font-family: "star"; + + &::before { + content: "\73\73\73\73\73"; + color: darken($secondary, 10%); + float: left; + top: 0; + left: 0; + position: absolute; + } + + span { + overflow: hidden; + float: left; + top: 0; + left: 0; + position: absolute; + padding-top: 1.5em; + } + + span::before { + content: "\53\53\53\53\53"; + top: 0; + position: absolute; + left: 0; + } + } + + .woocommerce-product-rating { + + @include clearfix(); + line-height: 2; + display: block; + + .star-rating { + margin: 0.5em 4px 0 0; + float: left; + } + } + + .products .star-rating { + display: block; + margin: 0 0 0.5em; + float: none; + } + + .hreview-aggregate .star-rating { + margin: 10px 0 0; + } + + #review_form #respond { + + @include clearfix(); + position: static; + margin: 0; + width: auto; + padding: 0; + background: transparent none; + border: 0; + + p { + margin: 0 0 10px; + } + + .form-submit input { + left: auto; + } + + textarea { + box-sizing: border-box; + width: 100%; + } + } + + p.stars { + + a { + position: relative; + height: 1em; + width: 1em; + text-indent: -999em; + display: inline-block; + text-decoration: none; + + &::before { + display: block; + position: absolute; + top: 0; + left: 0; + width: 1em; + height: 1em; + line-height: 1; + font-family: "WooCommerce"; + content: "\e021"; + text-indent: 0; + } + + &:hover ~ a::before { + content: "\e021"; + } + } + + &:hover a::before { + content: "\e020"; + } + + &.selected { + + a.active { + + &::before { + content: "\e020"; + } + + ~ a::before { + content: "\e021"; + } + } + + a:not(.active)::before { + content: "\e020"; + } + } + } + + /** + * Tables + */ + table.shop_attributes { + border: 0; + border-top: 1px dotted rgba(0, 0, 0, 0.1); + margin-bottom: 1.618em; + width: 100%; + + th { + width: 150px; + font-weight: 700; + padding: 8px; + border-top: 0; + border-bottom: 1px dotted rgba(0, 0, 0, 0.1); + margin: 0; + line-height: 1.5; + } + + td { + font-style: italic; + padding: 0; + border-top: 0; + border-bottom: 1px dotted rgba(0, 0, 0, 0.1); + margin: 0; + line-height: 1.5; + + p { + margin: 0; + padding: 8px 0; + } + } + + tr:nth-child(even) td, + tr:nth-child(even) th { + background: rgba(0, 0, 0, 0.025); + } + } + + table.shop_table { + border: 1px solid rgba(0, 0, 0, 0.1); + margin: 0 -1px 24px 0; + text-align: left; + width: 100%; + border-collapse: separate; + border-radius: 5px; + + th { + font-weight: 700; + padding: 9px 12px; + line-height: 1.5em; + } + + td { + border-top: 1px solid rgba(0, 0, 0, 0.1); + padding: 9px 12px; + vertical-align: middle; + line-height: 1.5em; + + small { + font-weight: normal; + } + + del { + font-weight: normal; + } + } + + tbody:first-child tr:first-child { + + th, + td { + border-top: 0; + } + } + + tfoot td, + tfoot th, + tbody th { + font-weight: 700; + border-top: 1px solid rgba(0, 0, 0, 0.1); + } + } + + table.my_account_orders { + font-size: 0.85em; + + th, + td { + padding: 4px 8px; + vertical-align: middle; + } + + .button { + white-space: nowrap; + } + } + + table.woocommerce-MyAccount-downloads { + + td, + th { + vertical-align: top; + text-align: center; + + &:first-child { + text-align: left; + } + + &:last-child { + text-align: left; + } + + .woocommerce-MyAccount-downloads-file::before { + content: "\2193"; + display: inline-block; + } + } + } + + td.product-name { + + dl.variation, + .wc-item-meta { + list-style: none outside; + + dt, + .wc-item-meta-label { + float: left; + clear: both; + margin-right: 0.25em; + display: inline-block; + list-style: none outside; + } + + dd { + margin: 0; + } + + p, + &:last-child { + margin-bottom: 0; + } + } + + p.backorder_notification { + font-size: 0.83em; + } + } + + td.product-quantity { + min-width: 80px; + } + + /** + * Cart sidebar + */ + ul.cart_list, + ul.product_list_widget { + list-style: none outside; + padding: 0; + margin: 0; + + li { + padding: 4px 0; + margin: 0; + + @include clearfix(); + list-style: none; + + a { + display: block; + font-weight: 700; + } + + img { + float: right; + margin-left: 4px; + width: 32px; + height: auto; + box-shadow: none; + } + + dl { + margin: 0; + padding-left: 1em; + border-left: 2px solid rgba(0, 0, 0, 0.1); + + @include clearfix(); + + dt, + dd { + display: inline-block; + float: left; + margin-bottom: 1em; + } + + dt { + font-weight: 700; + padding: 0 0 0.25em; + margin: 0 4px 0 0; + clear: left; + } + + dd { + padding: 0 0 0.25em; + + p:last-child { + margin-bottom: 0; + } + } + } + + .star-rating { + float: none; + } + } + } + + &.widget_shopping_cart, + .widget_shopping_cart { + + .total { + border-top: 3px double $secondary; + padding: 4px 0 0; + + strong { + min-width: 40px; + display: inline-block; + } + } + + .cart_list li { + padding-left: 2em; + position: relative; + padding-top: 0; + + a.remove { + position: absolute; + top: 0; + left: 0; + } + } + + .buttons { + + @include clearfix(); + + a { + margin-right: 5px; + margin-bottom: 5px; + } + } + } + + /** + * Forms + */ + form .form-row { + padding: 3px; + margin: 0 0 6px; + + [placeholder]:focus::-webkit-input-placeholder { + transition: opacity 0.5s 0.5s ease; + opacity: 0; + } + + label { + line-height: 2; + } + + label.hidden { + visibility: hidden; + } + + label.inline { + display: inline; + } + + .woocommerce-input-wrapper { + + .description { + background: #1e85be; + color: #fff; + border-radius: 3px; + padding: 1em; + margin: 0.5em 0 0; + clear: both; + display: none; + position: relative; + + a { + color: #fff; + text-decoration: underline; + border: 0; + box-shadow: none; + } + + &::before { + left: 50%; + top: 0%; + margin-top: -4px; + transform: translateX(-50%) rotate(180deg); + content: ""; + position: absolute; + border-width: 4px 6px 0 6px; + border-style: solid; + border-color: #1e85be transparent transparent transparent; + z-index: 100; + display: block; + } + } + } + + select { + cursor: pointer; + margin: 0; + } + + .required { + color: red; + font-weight: 700; + border: 0 !important; + text-decoration: none; + visibility: hidden; // Only show optional by default. + } + + .optional { + visibility: visible; + } + + .input-checkbox { + display: inline; + margin: -2px 8px 0 0; + text-align: center; + vertical-align: middle; + } + + input.input-text, + textarea { + box-sizing: border-box; + width: 100%; + margin: 0; + outline: 0; + line-height: normal; + } + + textarea { + height: 4em; + line-height: 1.5; + display: block; + box-shadow: none; + } + + .select2-container { + width: 100%; + line-height: 2em; + } + + &.woocommerce-invalid { + + label { + color: $red; + } + + .select2-container, + input.input-text, + select { + border-color: $red; + } + } + + &.woocommerce-validated { + + .select2-container, + input.input-text, + select { + border-color: darken($green, 5%); + } + } + + ::-webkit-input-placeholder { + line-height: normal; + } + + :-moz-placeholder { + line-height: normal; + } + + :-ms-input-placeholder { + line-height: normal; + } + } + + form.login, + form.checkout_coupon, + form.register { + border: 1px solid darken($secondary, 10%); + padding: 20px; + margin: 2em 0; + text-align: left; + border-radius: 5px; + } + + ul#shipping_method { + list-style: none outside; + margin: 0; + padding: 0; + + li { + margin: 0 0 0.5em; + line-height: 1.5em; + list-style: none outside; + + input { + margin: 3px 0.4375em 0 0; + vertical-align: top; + } + + label { + display: inline; + } + } + + .amount { + font-weight: 700; + } + } + + p.woocommerce-shipping-contents { + margin: 0; + } + + /** + * Order page + */ + ul.order_details { + + @include clearfix(); + margin: 0 0 3em; + list-style: none; + + li { + float: left; + margin-right: 2em; + text-transform: uppercase; + font-size: 0.715em; + line-height: 1; + border-right: 1px dashed darken($secondary, 10%); + padding-right: 2em; + margin-left: 0; + padding-left: 0; + list-style-type: none; + + strong { + display: block; + font-size: 1.4em; + text-transform: none; + line-height: 1.5; + } + + &:last-of-type { + border: none; + } + } + } + + .wc-bacs-bank-details-account-name { + font-weight: bold; + } + + .woocommerce-order-downloads, + .woocommerce-customer-details, + .woocommerce-order-details { + margin-bottom: 2em; + + *:last-child { + margin-bottom: 0; + } + } + + .woocommerce-customer-details { + + address { + font-style: normal; + margin-bottom: 0; + border: 1px solid rgba(0, 0, 0, 0.1); + border-bottom-width: 2px; + border-right-width: 2px; + text-align: left; + width: 100%; + border-radius: 5px; + padding: 6px 12px; + } + + .woocommerce-customer-details--phone, + .woocommerce-customer-details--email { + margin-bottom: 0; + padding-left: 1.5em; + } + + .woocommerce-customer-details--phone::before { + + @include iconbefore( "\e037" ); + margin-left: -1.5em; + line-height: 1.75; + position: absolute; + } + + .woocommerce-customer-details--email::before { + + @include iconbefore( "\e02d" ); + margin-left: -1.5em; + line-height: 1.75; + position: absolute; + } + } + + /** + * Layered nav widget + */ + .woocommerce-widget-layered-nav-list { + margin: 0; + padding: 0; + border: 0; + list-style: none outside; + + .woocommerce-widget-layered-nav-list__item { + + @include clearfix(); + padding: 0 0 1px; + list-style: none; + + a, + span { + padding: 1px 0; + } + } + + .woocommerce-widget-layered-nav-list__item--chosen a::before { + + @include iconbefore( "\e013" ); + color: $red; + } + } + + .woocommerce-widget-layered-nav-dropdown__submit { + margin-top: 1em; + } + + .widget_layered_nav_filters ul { + margin: 0; + padding: 0; + border: 0; + list-style: none outside; + overflow: hidden; + zoom: 1; + + li { + float: left; + padding: 0 1em 1px 1px; + list-style: none; + + a { + text-decoration: none; + + &::before { + + @include iconbefore( "\e013" ); + color: $red; + vertical-align: inherit; + margin-right: 0.5em; + } + } + } + } + + /** + * Price filter widget + */ + .widget_price_filter { + + .price_slider { + margin-bottom: 1em; + } + + .price_slider_amount { + text-align: right; + line-height: 2.4; + font-size: 0.8751em; + + .button { + font-size: 1.15em; + float: left; + } + } + + .ui-slider { + position: relative; + text-align: left; + margin-left: 0.5em; + margin-right: 0.5em; + } + + .ui-slider .ui-slider-handle { + position: absolute; + z-index: 2; + width: 1em; + height: 1em; + background-color: $primary; + border-radius: 1em; + cursor: ew-resize; + outline: none; + top: -0.3em; + + /* rtl:ignore */ + margin-left: -0.5em; + } + + .ui-slider .ui-slider-range { + position: absolute; + z-index: 1; + font-size: 0.7em; + display: block; + border: 0; + border-radius: 1em; + background-color: $primary; + } + + .price_slider_wrapper .ui-widget-content { + border-radius: 1em; + background-color: darken($primary, 30%); + border: 0; + } + + .ui-slider-horizontal { + height: 0.5em; + } + + .ui-slider-horizontal .ui-slider-range { + top: 0; + height: 100%; + } + + .ui-slider-horizontal .ui-slider-range-min { + left: -1px; + } + + .ui-slider-horizontal .ui-slider-range-max { + right: -1px; + } + } + + /** + * Rating Filter Widget + */ + .widget_rating_filter ul { + margin: 0; + padding: 0; + border: 0; + list-style: none outside; + + li { + + @include clearfix(); + padding: 0 0 1px; + list-style: none; + + a { + padding: 1px 0; + text-decoration: none; + } + + .star-rating { + float: none; + display: inline-block; + } + } + + li.chosen a::before { + + @include iconbefore( "\e013" ); + color: $red; + } + } + + .woocommerce-form-login { + + .woocommerce-form-login__submit { + float: left; + margin-right: 1em; + } + + .woocommerce-form-login__rememberme { + display: inline-block; + } + } +} + +.woocommerce-no-js { + + form.woocommerce-form-login, + form.woocommerce-form-coupon { + display: block !important; + } + + .woocommerce-form-login-toggle, + .woocommerce-form-coupon-toggle, + .showcoupon { + display: none !important; + } +} + +.woocommerce-message, +.woocommerce-error, +.woocommerce-info { + padding: 1em 2em 1em 3.5em; + margin: 0 0 2em; + position: relative; + background-color: lighten($secondary, 5%); + color: $secondarytext; + border-top: 3px solid $primary; + list-style: none outside; + + @include clearfix(); + width: auto; + word-wrap: break-word; + + &::before { + font-family: "WooCommerce"; + content: "\e028"; + display: inline-block; + position: absolute; + top: 1em; + left: 1.5em; + } + + .button { + float: right; + } + + li { + list-style: none outside !important; // Required for default theme compatibility + padding-left: 0 !important; // Required for default theme compatibility + margin-left: 0 !important; // Required for default theme compatibility + } +} + +/** + * Right to left styles + */ +.rtl.woocommerce .price_label, +.rtl.woocommerce .price_label span { + + /* rtl:ignore */ + direction: ltr; + unicode-bidi: embed; +} + +.woocommerce-message { + border-top-color: #8fae1b; + + &::before { + content: "\e015"; + color: #8fae1b; + } +} + +.woocommerce-info { + border-top-color: #1e85be; + + &::before { + color: #1e85be; + } +} + +.woocommerce-error { + border-top-color: #b81c23; + + &::before { + content: "\e016"; + color: #b81c23; + } +} + +/** + * Account page + */ +.woocommerce-account { + + .woocommerce { + + @include clearfix(); + } + + .addresses .title { + + @include clearfix(); + + h3 { + float: left; + } + + .edit { + float: right; + } + } + + ol.commentlist.notes li.note { + + p.meta { + font-weight: 700; + margin-bottom: 0; + } + + .description p:last-child { + margin-bottom: 0; + } + } + + ul.digital-downloads { + margin-left: 0; + padding-left: 0; + + li { + list-style: none; + margin-left: 0; + padding-left: 0; + + &::before { + + @include iconbefore( "\e00a" ); + } + + .count { + float: right; + } + } + } +} + +/** + * Cart/checkout page + */ +.woocommerce-cart, +.woocommerce-checkout, +#add_payment_method { + + table.cart { + + .product-thumbnail { + min-width: 32px; + } + + img { + width: 32px; + box-shadow: none; + } + + th, + td { + vertical-align: middle; + } + + td.actions .coupon .input-text { + float: left; + box-sizing: border-box; + border: 1px solid darken($secondary, 10%); + padding: 6px 6px 5px; + margin: 0 4px 0 0; + outline: 0; + } + + input { + margin: 0; + vertical-align: middle; + } + } + + .wc-proceed-to-checkout { + + @include clearfix; + padding: 1em 0; + + a.checkout-button { + display: block; + text-align: center; + margin-bottom: 1em; + font-size: 1.25em; + padding: 1em; + } + } + + .cart-collaterals { + + .shipping-calculator-button { + float: none; + margin-top: 0.5em; + display: inline-block; + } + + .shipping-calculator-button::after { + + @include iconafter( "\e019" ); + } + + .shipping-calculator-form { + margin: 1em 0 0 0; + } + + .cart_totals { + + p small { + color: $subtext; + font-size: 0.83em; + } + + table { + border-collapse: separate; + margin: 0 0 6px; + padding: 0; + + tr:first-child { + + th, + td { + border-top: 0; + } + } + + th { + width: 35%; + } + + td, + th { + vertical-align: top; + border-left: 0; + border-right: 0; + line-height: 1.5em; + } + + small { + color: $subtext; + } + + select { + width: 100%; + } + } + + .discount td { + color: $highlight; + } + + tr td, + tr th { + border-top: 1px solid $secondary; + } + + .woocommerce-shipping-destination { + margin-bottom: 0; + } + } + + .cross-sells ul.products li.product { + margin-top: 0; + } + } + + .checkout { + + .col-2 { + + h3#ship-to-different-address { + float: left; + clear: none; + } + + .notes { + clear: left; + } + + .form-row-first { + clear: left; + } + } + + .create-account small { + font-size: 11px; + color: $subtext; + font-weight: normal; + } + + div.shipping-address { + padding: 0; + clear: left; + width: 100%; + } + + .shipping_address { + clear: both; + } + } + + #payment { + background: $secondary; + border-radius: 5px; + + ul.payment_methods { + + @include clearfix(); + text-align: left; + padding: 1em; + border-bottom: 1px solid darken($secondary, 10%); + margin: 0; + list-style: none outside; + + li { + line-height: 2; + text-align: left; + margin: 0; + font-weight: normal; + + input { + margin: 0 1em 0 0; + } + + img { + vertical-align: middle; + margin: -2px 0 0 0.5em; + padding: 0; + position: relative; + box-shadow: none; + } + + img + img { + margin-left: 2px; + } + } + + li:not(.woocommerce-notice) { + + @include clearfix; + } + } + + div.form-row { + padding: 1em; + } + + div.payment_box { + position: relative; + box-sizing: border-box; + width: 100%; + padding: 1em; + margin: 1em 0; + font-size: 0.92em; + border-radius: 2px; + line-height: 1.5; + background-color: darken($secondary, 5%); + color: $secondarytext; + + input.input-text, + textarea { + border-color: darken($secondary, 15%); + border-top-color: darken($secondary, 20%); + } + + ::-webkit-input-placeholder { + color: darken($secondary, 20%); + } + + :-moz-placeholder { + color: darken($secondary, 20%); + } + + :-ms-input-placeholder { + color: darken($secondary, 20%); + } + + .woocommerce-SavedPaymentMethods { + list-style: none outside; + margin: 0; + + .woocommerce-SavedPaymentMethods-token, + .woocommerce-SavedPaymentMethods-new { + margin: 0 0 0.5em; + + label { + cursor: pointer; + } + } + + .woocommerce-SavedPaymentMethods-tokenInput { + vertical-align: middle; + margin: -3px 1em 0 0; + position: relative; + } + } + + .wc-credit-card-form { + border: 0; + padding: 0; + margin: 1em 0 0; + } + + .wc-credit-card-form-card-number, + .wc-credit-card-form-card-expiry, + .wc-credit-card-form-card-cvc { + font-size: 1.5em; + padding: 8px; + background-repeat: no-repeat; + background-position: right 0.618em center; + background-size: 32px 20px; + + &.visa { + background-image: url("../images/icons/credit-cards/visa.svg"); + } + + &.mastercard { + background-image: url("../images/icons/credit-cards/mastercard.svg"); + } + + &.laser { + background-image: url("../images/icons/credit-cards/laser.svg"); + } + + &.dinersclub { + background-image: url("../images/icons/credit-cards/diners.svg"); + } + + &.maestro { + background-image: url("../images/icons/credit-cards/maestro.svg"); + } + + &.jcb { + background-image: url("../images/icons/credit-cards/jcb.svg"); + } + + &.amex { + background-image: url("../images/icons/credit-cards/amex.svg"); + } + + &.discover { + background-image: url("../images/icons/credit-cards/discover.svg"); + } + } + + span.help { + font-size: 0.857em; + color: $subtext; + font-weight: normal; + } + + .form-row { + margin: 0 0 1em; + } + + p:last-child { + margin-bottom: 0; + } + + &::before { + content: ""; + display: block; + border: 1em solid darken($secondary, 5%); /* arrow size / color */ + border-right-color: transparent; + border-left-color: transparent; + border-top-color: transparent; + position: absolute; + top: -0.75em; + left: 0; + margin: -1em 0 0 2em; + } + } + + .payment_method_paypal { + + .about_paypal { + float: right; + line-height: 52px; + font-size: 0.83em; + } + + img { + max-height: 52px; + vertical-align: middle; + } + } + } +} + +.woocommerce-terms-and-conditions { + border: 1px solid rgba(0, 0, 0, 0.2); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + background: rgba(0, 0, 0, 0.05); +} + +.woocommerce-invalid { + + #terms { + outline: 2px solid red; + outline-offset: 2px; + } +} + +/** + * Password strength meter + */ +.woocommerce-password-strength { + text-align: center; + font-weight: 600; + padding: 3px 0.5em; + font-size: 1em; + + &.strong { + background-color: #c1e1b9; + border-color: #83c373; + } + + &.short { + background-color: #f1adad; + border-color: #e35b5b; + } + + &.bad { + background-color: #fbc5a9; + border-color: #f78b53; + } + + &.good { + background-color: #ffe399; + border-color: #ffc733; + } +} + +.woocommerce-password-hint { + margin: 0.5em 0 0; + display: block; +} + +/** + * Twenty Eleven specific styles + */ +#content.twentyeleven .woocommerce-pagination a { + font-size: 1em; + line-height: 1; +} + +/** + * Twenty Thirteen specific styles + */ +.single-product .twentythirteen { + + .entry-summary, + #reply-title, + #respond #commentform { + padding: 0; + } + + p.stars { + clear: both; + } +} + +.twentythirteen .woocommerce-breadcrumb { + padding-top: 40px; +} + +/** + * Twenty Fourteen specific styles + */ +.twentyfourteen ul.products li.product { + margin-top: 0 !important; +} + +/** + * Twenty Sixteen specific styles + */ +body:not(.search-results) .twentysixteen .entry-summary { + color: inherit; + font-size: inherit; + line-height: inherit; +} + +.twentysixteen .price ins { + background: inherit; + color: inherit; +} diff --git a/assets/js/accounting/accounting.js b/plugins/woocommerce/legacy/js/accounting/accounting.js similarity index 100% rename from assets/js/accounting/accounting.js rename to plugins/woocommerce/legacy/js/accounting/accounting.js diff --git a/plugins/woocommerce/legacy/js/admin/api-keys.js b/plugins/woocommerce/legacy/js/admin/api-keys.js new file mode 100644 index 00000000000..5c22ef3640e --- /dev/null +++ b/plugins/woocommerce/legacy/js/admin/api-keys.js @@ -0,0 +1,158 @@ +/*global jQuery, Backbone, _, woocommerce_admin_api_keys, wcSetClipboard, wcClearClipboard */ +(function( $ ) { + + var APIView = Backbone.View.extend({ + /** + * Element + * + * @param {Object} '#key-fields' + */ + el: $( '#key-fields' ), + + /** + * Events + * + * @type {Object} + */ + events: { + 'click input#update_api_key': 'saveKey' + }, + + /** + * Initialize actions + */ + initialize: function(){ + _.bindAll( this, 'saveKey' ); + }, + + /** + * Init jQuery.BlockUI + */ + block: function() { + $( this.el ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + }, + + /** + * Remove jQuery.BlockUI + */ + unblock: function() { + $( this.el ).unblock(); + }, + + /** + * Init TipTip + */ + initTipTip: function( css_class ) { + $( document.body ) + .on( 'click', css_class, function( evt ) { + evt.preventDefault(); + if ( ! document.queryCommandSupported( 'copy' ) ) { + $( css_class ).parent().find( 'input' ).trigger( 'focus' ).trigger( 'select' ); + $( '#copy-error' ).text( woocommerce_admin_api_keys.clipboard_failed ); + } else { + $( '#copy-error' ).text( '' ); + wcClearClipboard(); + wcSetClipboard( $( this ).prev( 'input' ).val().trim(), $( css_class ) ); + } + } ) + .on( 'aftercopy', css_class, function() { + $( '#copy-error' ).text( '' ); + $( css_class ).tipTip( { + 'attribute': 'data-tip', + 'activation': 'focus', + 'fadeIn': 50, + 'fadeOut': 50, + 'delay': 0 + } ).trigger( 'focus' ); + } ) + .on( 'aftercopyerror', css_class, function() { + $( css_class ).parent().find( 'input' ).trigger( 'focus' ).trigger( 'select' ); + $( '#copy-error' ).text( woocommerce_admin_api_keys.clipboard_failed ); + } ); + }, + + /** + * Create qrcode + * + * @param {string} consumer_key + * @param {string} consumer_secret + */ + createQRCode: function( consumer_key, consumer_secret ) { + $( '#keys-qrcode' ).qrcode({ + text: consumer_key + '|' + consumer_secret, + width: 120, + height: 120 + }); + }, + + /** + * Save API Key using ajax + * + * @param {Object} e + */ + saveKey: function( e ) { + e.preventDefault(); + + var self = this; + + self.block(); + + Backbone.ajax({ + method: 'POST', + dataType: 'json', + url: woocommerce_admin_api_keys.ajax_url, + data: { + action: 'woocommerce_update_api_key', + security: woocommerce_admin_api_keys.update_api_nonce, + key_id: $( '#key_id', self.el ).val(), + description: $( '#key_description', self.el ).val(), + user: $( '#key_user', self.el ).val(), + permissions: $( '#key_permissions', self.el ).val() + }, + success: function( response ) { + $( '.wc-api-message', self.el ).remove(); + + if ( response.success ) { + var data = response.data; + + $( 'h2, h3', self.el ).first().append( '

    ' + data.message + '

    ' ); + + if ( 0 < data.consumer_key.length && 0 < data.consumer_secret.length ) { + $( '#api-keys-options', self.el ).remove(); + $( 'p.submit', self.el ).empty().append( data.revoke_url ); + + var template = wp.template( 'api-keys-template' ); + + $( 'p.submit', self.el ).before( template({ + consumer_key: data.consumer_key, + consumer_secret: data.consumer_secret + }) ); + self.createQRCode( data.consumer_key, data.consumer_secret ); + self.initTipTip( '.copy-key' ); + self.initTipTip( '.copy-secret' ); + } else { + $( '#key_description', self.el ).val( data.description ); + $( '#key_user', self.el ).val( data.user_id ); + $( '#key_permissions', self.el ).val( data.permissions ); + } + } else { + $( 'h2, h3', self.el ) + .first() + .append( '

    ' + response.data.message + '

    ' ); + } + + self.unblock(); + } + }); + } + }); + + new APIView(); + +})( jQuery ); diff --git a/plugins/woocommerce/legacy/js/admin/backbone-modal.js b/plugins/woocommerce/legacy/js/admin/backbone-modal.js new file mode 100644 index 00000000000..061a37c8bae --- /dev/null +++ b/plugins/woocommerce/legacy/js/admin/backbone-modal.js @@ -0,0 +1,147 @@ +/*global jQuery, Backbone, _ */ +( function( $, Backbone, _ ) { + 'use strict'; + + /** + * WooCommerce Backbone Modal plugin + * + * @param {object} options + */ + $.fn.WCBackboneModal = function( options ) { + return this.each( function() { + ( new $.WCBackboneModal( $( this ), options ) ); + }); + }; + + /** + * Initialize the Backbone Modal + * + * @param {object} element [description] + * @param {object} options [description] + */ + $.WCBackboneModal = function( element, options ) { + // Set settings + var settings = $.extend( {}, $.WCBackboneModal.defaultOptions, options ); + + if ( settings.template ) { + new $.WCBackboneModal.View({ + target: settings.template, + string: settings.variable + }); + } + }; + + /** + * Set default options + * + * @type {object} + */ + $.WCBackboneModal.defaultOptions = { + template: '', + variable: {} + }; + + /** + * Create the Backbone Modal + * + * @return {null} + */ + $.WCBackboneModal.View = Backbone.View.extend({ + tagName: 'div', + id: 'wc-backbone-modal-dialog', + _target: undefined, + _string: undefined, + events: { + 'click .modal-close': 'closeButton', + 'click #btn-ok' : 'addButton', + 'touchstart #btn-ok': 'addButton', + 'keydown' : 'keyboardActions' + }, + resizeContent: function() { + var $content = $( '.wc-backbone-modal-content' ).find( 'article' ); + var max_h = $( window ).height() * 0.75; + + $content.css({ + 'max-height': max_h + 'px' + }); + }, + initialize: function( data ) { + var view = this; + this._target = data.target; + this._string = data.string; + _.bindAll( this, 'render' ); + this.render(); + + $( window ).on( 'resize', function() { + view.resizeContent(); + }); + }, + render: function() { + var template = wp.template( this._target ); + + this.$el.append( + template( this._string ) + ); + + $( document.body ).css({ + 'overflow': 'hidden' + }).append( this.$el ); + + this.resizeContent(); + this.$( '.wc-backbone-modal-content' ).attr( 'tabindex' , '0' ).trigger( 'focus' ); + + $( document.body ).trigger( 'init_tooltips' ); + + $( document.body ).trigger( 'wc_backbone_modal_loaded', this._target ); + }, + closeButton: function( e ) { + e.preventDefault(); + $( document.body ).trigger( 'wc_backbone_modal_before_remove', this._target ); + this.undelegateEvents(); + $( document ).off( 'focusin' ); + $( document.body ).css({ + 'overflow': 'auto' + }); + this.remove(); + $( document.body ).trigger( 'wc_backbone_modal_removed', this._target ); + }, + addButton: function( e ) { + $( document.body ).trigger( 'wc_backbone_modal_response', [ this._target, this.getFormData() ] ); + this.closeButton( e ); + }, + getFormData: function() { + var data = {}; + + $( document.body ).trigger( 'wc_backbone_modal_before_update', this._target ); + + $.each( $( 'form', this.$el ).serializeArray(), function( index, item ) { + if ( item.name.indexOf( '[]' ) !== -1 ) { + item.name = item.name.replace( '[]', '' ); + data[ item.name ] = $.makeArray( data[ item.name ] ); + data[ item.name ].push( item.value ); + } else { + data[ item.name ] = item.value; + } + }); + + return data; + }, + keyboardActions: function( e ) { + var button = e.keyCode || e.which; + + // Enter key + if ( + 13 === button && + ! ( e.target.tagName && ( e.target.tagName.toLowerCase() === 'input' || e.target.tagName.toLowerCase() === 'textarea' ) ) + ) { + this.addButton( e ); + } + + // ESC key + if ( 27 === button ) { + this.closeButton( e ); + } + } + }); + +}( jQuery, Backbone, _ )); diff --git a/plugins/woocommerce/legacy/js/admin/marketplace-suggestions.js b/plugins/woocommerce/legacy/js/admin/marketplace-suggestions.js new file mode 100644 index 00000000000..010cb351258 --- /dev/null +++ b/plugins/woocommerce/legacy/js/admin/marketplace-suggestions.js @@ -0,0 +1,450 @@ +/* global marketplace_suggestions, ajaxurl, Cookies */ +( function( $, marketplace_suggestions, ajaxurl ) { + $( function() { + if ( 'undefined' === typeof marketplace_suggestions ) { + return; + } + + // Stand-in wcTracks.recordEvent in case tracks is not available (for any reason). + window.wcTracks = window.wcTracks || {}; + window.wcTracks.recordEvent = window.wcTracks.recordEvent || function() { }; + + // Tracks events sent in this file: + // - marketplace_suggestion_displayed + // - marketplace_suggestion_clicked + // - marketplace_suggestion_dismissed + // All are prefixed by {WC_Tracks::PREFIX}. + // All have one property for `suggestionSlug`, to identify the specific suggestion message. + + // Dismiss the specified suggestion from the UI, and save the dismissal in settings. + function dismissSuggestion( context, product, promoted, url, suggestionSlug ) { + // hide the suggestion in the UI + var selector = '[data-suggestion-slug=' + suggestionSlug + ']'; + $( selector ).fadeOut( function() { + $( this ).remove(); + tidyProductEditMetabox(); + } ); + + // save dismissal in user settings + jQuery.post( + ajaxurl, + { + 'action': 'woocommerce_add_dismissed_marketplace_suggestion', + '_wpnonce': marketplace_suggestions.dismiss_suggestion_nonce, + 'slug': suggestionSlug + } + ); + + // if this is a high-use area, delay new suggestion that area for a short while + var highUseSuggestionContexts = [ 'products-list-inline' ]; + if ( _.contains( highUseSuggestionContexts, context ) ) { + // snooze suggestions in that area for 2 days + var contextSnoozeCookie = 'woocommerce_snooze_suggestions__' + context; + Cookies.set( contextSnoozeCookie, 'true', { expires: 2 } ); + + // keep track of how often this area gets dismissed in a cookie + var contextDismissalCountCookie = 'woocommerce_dismissed_suggestions__' + context; + var previousDismissalsInThisContext = parseInt( Cookies.get( contextDismissalCountCookie ), 10 ) || 0; + Cookies.set( contextDismissalCountCookie, previousDismissalsInThisContext + 1, { expires: 31 } ); + } + + window.wcTracks.recordEvent( 'marketplace_suggestion_dismissed', { + suggestion_slug: suggestionSlug, + context: context, + product: product || '', + promoted: promoted || '', + target: url || '' + } ); + } + + // Render DOM element for suggestion dismiss button. + function renderDismissButton( context, product, promoted, url, suggestionSlug ) { + var dismissButton = document.createElement( 'a' ); + + dismissButton.classList.add( 'suggestion-dismiss' ); + dismissButton.setAttribute( 'title', marketplace_suggestions.i18n_marketplace_suggestions_dismiss_tooltip ); + dismissButton.setAttribute( 'href', '#' ); + dismissButton.onclick = function( event ) { + event.preventDefault(); + dismissSuggestion( context, product, promoted, url, suggestionSlug ); + }; + + return dismissButton; + } + + function addURLParameters( context, url ) { + var urlParams = marketplace_suggestions.in_app_purchase_params; + urlParams.utm_source = 'unknown'; + urlParams.utm_campaign = 'marketplacesuggestions'; + urlParams.utm_medium = 'product'; + + var sourceContextMap = { + 'productstable': [ + 'products-list-inline' + ], + 'productsempty': [ + 'products-list-empty-header', + 'products-list-empty-footer', + 'products-list-empty-body' + ], + 'ordersempty': [ + 'orders-list-empty-header', + 'orders-list-empty-footer', + 'orders-list-empty-body' + ], + 'editproduct': [ + 'product-edit-meta-tab-header', + 'product-edit-meta-tab-footer', + 'product-edit-meta-tab-body' + ] + }; + var utmSource = _.findKey( sourceContextMap, function( sourceInfo ) { + return _.contains( sourceInfo, context ); + } ); + if ( utmSource ) { + urlParams.utm_source = utmSource; + } + + return url + '?' + jQuery.param( urlParams ); + } + + // Render DOM element for suggestion linkout, optionally with button style. + function renderLinkout( context, product, promoted, slug, url, text, isButton ) { + var linkoutButton = document.createElement( 'a' ); + + var utmUrl = addURLParameters( context, url ); + linkoutButton.setAttribute( 'href', utmUrl ); + + // By default, CTA links should open in same tab (and feel integrated with Woo). + // Exception: when editing products, use new tab. User may have product edits + // that need to be saved. + var newTabContexts = [ + 'product-edit-meta-tab-header', + 'product-edit-meta-tab-footer', + 'product-edit-meta-tab-body', + 'products-list-empty-footer' + ]; + if ( _.includes( newTabContexts, context ) ) { + linkoutButton.setAttribute( 'target', 'blank' ); + } + + linkoutButton.textContent = text; + + linkoutButton.onclick = function() { + window.wcTracks.recordEvent( 'marketplace_suggestion_clicked', { + suggestion_slug: slug, + context: context, + product: product || '', + promoted: promoted || '', + target: url || '' + } ); + }; + + if ( isButton ) { + linkoutButton.classList.add( 'button' ); + } else { + linkoutButton.classList.add( 'linkout' ); + var linkoutIcon = document.createElement( 'span' ); + linkoutIcon.classList.add( 'dashicons', 'dashicons-external' ); + linkoutButton.appendChild( linkoutIcon ); + } + + return linkoutButton; + } + + // Render DOM element for suggestion icon image. + function renderSuggestionIcon( iconUrl ) { + if ( ! iconUrl ) { + return null; + } + + var image = document.createElement( 'img' ); + image.src = iconUrl; + image.classList.add( 'marketplace-suggestion-icon' ); + + return image; + } + + // Render DOM elements for suggestion content. + function renderSuggestionContent( slug, title, copy ) { + var container = document.createElement( 'div' ); + + container.classList.add( 'marketplace-suggestion-container-content' ); + + if ( title ) { + var titleHeading = document.createElement( 'h4' ); + titleHeading.textContent = title; + container.appendChild( titleHeading ); + } + + if ( copy ) { + var body = document.createElement( 'p' ); + body.textContent = copy; + container.appendChild( body ); + } + + // Conditionally add in a Manage suggestions link to product edit + // metabox footer (based on suggestion slug). + var slugsWithManage = [ + 'product-edit-empty-footer-browse-all', + 'product-edit-meta-tab-footer-browse-all' + ]; + if ( -1 !== slugsWithManage.indexOf( slug ) ) { + container.classList.add( 'has-manage-link' ); + + var manageSuggestionsLink = document.createElement( 'a' ); + manageSuggestionsLink.classList.add( 'marketplace-suggestion-manage-link', 'linkout' ); + manageSuggestionsLink.setAttribute( + 'href', + marketplace_suggestions.manage_suggestions_url + ); + manageSuggestionsLink.textContent = marketplace_suggestions.i18n_marketplace_suggestions_manage_suggestions; + + container.appendChild( manageSuggestionsLink ); + } + + return container; + } + + // Render DOM elements for suggestion call-to-action – button or link with dismiss 'x'. + function renderSuggestionCTA( context, product, promoted, slug, url, linkText, linkIsButton, allowDismiss ) { + var container = document.createElement( 'div' ); + + if ( ! linkText ) { + linkText = marketplace_suggestions.i18n_marketplace_suggestions_default_cta; + } + + container.classList.add( 'marketplace-suggestion-container-cta' ); + if ( url && linkText ) { + var linkoutElement = renderLinkout( context, product, promoted, slug, url, linkText, linkIsButton ); + container.appendChild( linkoutElement ); + } + + if ( allowDismiss ) { + container.appendChild( renderDismissButton( context, product, promoted, url, slug ) ); + } + + return container; + } + + // Render a "list item" style suggestion. + // These are used in onboarding style contexts, e.g. products list empty state. + function renderListItem( context, product, promoted, slug, iconUrl, title, copy, url, linkText, linkIsButton, allowDismiss ) { + var container = document.createElement( 'div' ); + container.classList.add( 'marketplace-suggestion-container' ); + container.dataset.suggestionSlug = slug; + + var icon = renderSuggestionIcon( iconUrl ); + if ( icon ) { + container.appendChild( icon ); + } + container.appendChild( + renderSuggestionContent( slug, title, copy ) + ); + container.appendChild( + renderSuggestionCTA( context, product, promoted, slug, url, linkText, linkIsButton, allowDismiss ) + ); + + return container; + } + + // Filter suggestion data to remove less-relevant suggestions. + function getRelevantPromotions( marketplaceSuggestionsApiData, displayContext ) { + // select based on display context + var promos = _.filter( marketplaceSuggestionsApiData, function( promo ) { + if ( _.isArray( promo.context ) ) { + return _.contains( promo.context, displayContext ); + } + return ( displayContext === promo.context ); + } ); + + // hide promos the user has dismissed + promos = _.filter( promos, function( promo ) { + return ! _.contains( marketplace_suggestions.dismissed_suggestions, promo.slug ); + } ); + + // hide promos for things the user already has installed + promos = _.filter( promos, function( promo ) { + return ! _.contains( marketplace_suggestions.active_plugins, promo.product ); + } ); + + // hide promos that are not applicable based on user's installed extensions + promos = _.filter( promos, function( promo ) { + if ( ! promo['show-if-active'] ) { + // this promotion is relevant to all + return true; + } + + // if the user has any of the prerequisites, show the promo + return ( _.intersection( marketplace_suggestions.active_plugins, promo['show-if-active'] ).length > 0 ); + } ); + + return promos; + } + + // Show and hide page elements dependent on suggestion state. + function hidePageElementsForSuggestionState( usedSuggestionsContexts ) { + var showingEmptyStateSuggestions = _.intersection( + usedSuggestionsContexts, + [ 'products-list-empty-body', 'orders-list-empty-body' ] + ).length > 0; + + // Streamline onboarding UI if we're in 'empty state' welcome mode. + if ( showingEmptyStateSuggestions ) { + $( '#screen-meta-links' ).hide(); + $( '#wpfooter' ).hide(); + } + + // Hide the header & footer, they don't make sense without specific promotion content + if ( ! showingEmptyStateSuggestions ) { + $( '.marketplace-suggestions-container[data-marketplace-suggestions-context="products-list-empty-header"]' ).hide(); + $( '.marketplace-suggestions-container[data-marketplace-suggestions-context="products-list-empty-footer"]' ).hide(); + $( '.marketplace-suggestions-container[data-marketplace-suggestions-context="orders-list-empty-header"]' ).hide(); + $( '.marketplace-suggestions-container[data-marketplace-suggestions-context="orders-list-empty-footer"]' ).hide(); + } + } + + // Streamline the product edit suggestions tab dependent on what's visible. + function tidyProductEditMetabox() { + var productMetaboxSuggestions = $( + '.marketplace-suggestions-container[data-marketplace-suggestions-context="product-edit-meta-tab-body"]' + ).children(); + if ( 0 >= productMetaboxSuggestions.length ) { + var metaboxSuggestionsUISelector = + '.marketplace-suggestions-container[data-marketplace-suggestions-context="product-edit-meta-tab-body"]'; + metaboxSuggestionsUISelector += + ', .marketplace-suggestions-container[data-marketplace-suggestions-context="product-edit-meta-tab-header"]'; + metaboxSuggestionsUISelector += + ', .marketplace-suggestions-container[data-marketplace-suggestions-context="product-edit-meta-tab-footer"]'; + $( metaboxSuggestionsUISelector ).fadeOut( { + complete: function() { + $( '.marketplace-suggestions-metabox-nosuggestions-placeholder' ).fadeIn(); + } + } ); + + } + } + + function addManageSuggestionsTracksHandler() { + $( 'a.marketplace-suggestion-manage-link' ).on( 'click', function() { + window.wcTracks.recordEvent( 'marketplace_suggestions_manage_clicked' ); + } ); + } + + function isContextHiddenOnPageLoad( context ) { + // Some suggestions are not visible on page load; + // e.g. the user reveals them by selecting a tab. + var revealableSuggestionsContexts = [ + 'product-edit-meta-tab-header', + 'product-edit-meta-tab-body', + 'product-edit-meta-tab-footer' + ]; + return _.includes( revealableSuggestionsContexts, context ); + } + + // track the current product data tab to avoid over-tracking suggestions + var currentTab = false; + + // Render suggestion data in appropriate places in UI. + function displaySuggestions( marketplaceSuggestionsApiData ) { + var usedSuggestionsContexts = []; + + // iterate over all suggestions containers, rendering promos + $( '.marketplace-suggestions-container' ).each( function() { + // determine the context / placement we're populating + var context = this.dataset.marketplaceSuggestionsContext; + + // find promotions that target this context + var promos = getRelevantPromotions( marketplaceSuggestionsApiData, context ); + + // shuffle/randomly select five suggestions to display + var suggestionsToDisplay = _.sample( promos, 5 ); + + // render the promo content + for ( var i in suggestionsToDisplay ) { + + var linkText = suggestionsToDisplay[ i ]['link-text']; + var linkoutIsButton = true; + if ( suggestionsToDisplay[ i ]['link-text'] ) { + linkText = suggestionsToDisplay[ i ]['link-text']; + linkoutIsButton = false; + } + + // dismiss is allowed by default + var allowDismiss = true; + if ( suggestionsToDisplay[ i ]['allow-dismiss'] === false ) { + allowDismiss = false; + } + + var content = renderListItem( + context, + suggestionsToDisplay[ i ].product, + suggestionsToDisplay[ i ].promoted, + suggestionsToDisplay[ i ].slug, + suggestionsToDisplay[ i ].icon, + suggestionsToDisplay[ i ].title, + suggestionsToDisplay[ i ].copy, + suggestionsToDisplay[ i ].url, + linkText, + linkoutIsButton, + allowDismiss + ); + $( this ).append( content ); + $( this ).addClass( 'showing-suggestion' ); + usedSuggestionsContexts.push( context ); + + if ( ! isContextHiddenOnPageLoad( context ) ) { + // Fire 'displayed' tracks events for immediately visible suggestions. + window.wcTracks.recordEvent( 'marketplace_suggestion_displayed', { + suggestion_slug: suggestionsToDisplay[ i ].slug, + context: context, + product: suggestionsToDisplay[ i ].product || '', + promoted: suggestionsToDisplay[ i ].promoted || '', + target: suggestionsToDisplay[ i ].url || '' + } ); + } + } + + // Track when suggestions are displayed (and not already visible). + $( 'ul.product_data_tabs li.marketplace-suggestions_options a' ).on( 'click', function( e ) { + e.preventDefault(); + + if ( '#marketplace_suggestions' === currentTab ) { + return; + } + + if ( ! isContextHiddenOnPageLoad( context ) ) { + // We've already fired 'displayed' event above. + return; + } + + for ( var i in suggestionsToDisplay ) { + window.wcTracks.recordEvent( 'marketplace_suggestion_displayed', { + suggestion_slug: suggestionsToDisplay[ i ].slug, + context: context, + product: suggestionsToDisplay[ i ].product || '', + promoted: suggestionsToDisplay[ i ].promoted || '', + target: suggestionsToDisplay[ i ].url || '' + } ); + } + } ); + } ); + + hidePageElementsForSuggestionState( usedSuggestionsContexts ); + tidyProductEditMetabox(); + } + + if ( marketplace_suggestions.suggestions_data ) { + displaySuggestions( marketplace_suggestions.suggestions_data ); + + // track the current product data tab to avoid over-reporting suggestion views + $( 'ul.product_data_tabs' ).on( 'click', 'li a', function( e ) { + e.preventDefault(); + currentTab = $( this ).attr( 'href' ); + } ); + } + + addManageSuggestionsTracksHandler(); + }); + +})( jQuery, marketplace_suggestions, ajaxurl ); diff --git a/plugins/woocommerce/legacy/js/admin/meta-boxes-coupon.js b/plugins/woocommerce/legacy/js/admin/meta-boxes-coupon.js new file mode 100644 index 00000000000..594726739c0 --- /dev/null +++ b/plugins/woocommerce/legacy/js/admin/meta-boxes-coupon.js @@ -0,0 +1,70 @@ +/* global woocommerce_admin_meta_boxes_coupon */ +jQuery(function( $ ) { + + /** + * Coupon actions + */ + var wc_meta_boxes_coupon_actions = { + + /** + * Initialize variations actions + */ + init: function() { + $( 'select#discount_type' ) + .on( 'change', this.type_options ) + .trigger( 'change' ); + + this.insert_generate_coupon_code_button(); + $( '.button.generate-coupon-code' ).on( 'click', this.generate_coupon_code ); + }, + + /** + * Show/hide fields by coupon type options + */ + type_options: function() { + // Get value + var select_val = $( this ).val(); + + if ( 'percent' === select_val ) { + $( '#coupon_amount' ).removeClass( 'wc_input_price' ).addClass( 'wc_input_decimal' ); + } else { + $( '#coupon_amount' ).removeClass( 'wc_input_decimal' ).addClass( 'wc_input_price' ); + } + + if ( select_val !== 'fixed_cart' ) { + $( '.limit_usage_to_x_items_field' ).show(); + } else { + $( '.limit_usage_to_x_items_field' ).hide(); + } + }, + + /** + * Insert generate coupon code buttom HTML. + */ + insert_generate_coupon_code_button: function() { + $( '.post-type-shop_coupon' ).find( '#title' ).after( + '' + woocommerce_admin_meta_boxes_coupon.generate_button_text + '' + ); + }, + + /** + * Generate a random coupon code + */ + generate_coupon_code: function( e ) { + e.preventDefault(); + var $coupon_code_field = $( '#title' ), + $coupon_code_label = $( '#title-prompt-text' ), + $result = ''; + for ( var i = 0; i < woocommerce_admin_meta_boxes_coupon.char_length; i++ ) { + $result += woocommerce_admin_meta_boxes_coupon.characters.charAt( + Math.floor( Math.random() * woocommerce_admin_meta_boxes_coupon.characters.length ) + ); + } + $result = woocommerce_admin_meta_boxes_coupon.prefix + $result + woocommerce_admin_meta_boxes_coupon.suffix; + $coupon_code_field.trigger( 'focus' ).val( $result ); + $coupon_code_label.addClass( 'screen-reader-text' ); + } + }; + + wc_meta_boxes_coupon_actions.init(); +}); diff --git a/plugins/woocommerce/legacy/js/admin/meta-boxes-order.js b/plugins/woocommerce/legacy/js/admin/meta-boxes-order.js new file mode 100644 index 00000000000..78d3d95d8a8 --- /dev/null +++ b/plugins/woocommerce/legacy/js/admin/meta-boxes-order.js @@ -0,0 +1,1504 @@ +// eslint-disable-next-line max-len +/*global woocommerce_admin_meta_boxes, woocommerce_admin, accounting, woocommerce_admin_meta_boxes_order, wcSetClipboard, wcClearClipboard */ +jQuery( function ( $ ) { + + // Stand-in wcTracks.recordEvent in case tracks is not available (for any reason). + window.wcTracks = window.wcTracks || {}; + window.wcTracks.recordEvent = window.wcTracks.recordEvent || function() { }; + + /** + * Order Data Panel + */ + var wc_meta_boxes_order = { + states: null, + init: function() { + if ( + ! ( + typeof woocommerce_admin_meta_boxes_order === 'undefined' || + typeof woocommerce_admin_meta_boxes_order.countries === 'undefined' + ) + ) { + /* State/Country select boxes */ + this.states = JSON.parse( woocommerce_admin_meta_boxes_order.countries.replace( /"/g, '"' ) ); + } + + $( '.js_field-country' ).selectWoo().on( 'change', this.change_country ); + $( '.js_field-country' ).trigger( 'change', [ true ] ); + $( document.body ).on( 'change', 'select.js_field-state', this.change_state ); + $( '#woocommerce-order-actions input, #woocommerce-order-actions a' ).on( 'click', function() { + window.onbeforeunload = ''; + }); + $( 'a.edit_address' ).on( 'click', this.edit_address ); + $( 'a.billing-same-as-shipping' ).on( 'click', this.copy_billing_to_shipping ); + $( 'a.load_customer_billing' ).on( 'click', this.load_billing ); + $( 'a.load_customer_shipping' ).on( 'click', this.load_shipping ); + $( '#customer_user' ).on( 'change', this.change_customer_user ); + }, + + change_country: function( e, stickValue ) { + // Check for stickValue before using it + if ( typeof stickValue === 'undefined' ){ + stickValue = false; + } + + // Prevent if we don't have the metabox data + if ( wc_meta_boxes_order.states === null ){ + return; + } + + var $this = $( this ), + country = $this.val(), + $state = $this.parents( 'div.edit_address' ).find( ':input.js_field-state' ), + $parent = $state.parent(), + stateValue = $state.val(), + input_name = $state.attr( 'name' ), + input_id = $state.attr( 'id' ), + value = $this.data( 'woocommerce.stickState-' + country ) ? $this.data( 'woocommerce.stickState-' + country ) : stateValue, + placeholder = $state.attr( 'placeholder' ), + $newstate; + + if ( stickValue ){ + $this.data( 'woocommerce.stickState-' + country, value ); + } + + // Remove the previous DOM element + $parent.show().find( '.select2-container' ).remove(); + + if ( ! $.isEmptyObject( wc_meta_boxes_order.states[ country ] ) ) { + var state = wc_meta_boxes_order.states[ country ], + $defaultOption = $( '' ) + .text( woocommerce_admin_meta_boxes_order.i18n_select_state_text ); + + $newstate = $( '' ) + .prop( 'id', input_id ) + .prop( 'name', input_name ) + .prop( 'placeholder', placeholder ) + .addClass( 'js_field-state select short' ) + .append( $defaultOption ); + + $.each( state, function( index ) { + var $option = $( '' ) + .prop( 'value', index ) + .text( state[ index ] ); + if ( index === stateValue ) { + $option.prop( 'selected' ); + } + $newstate.append( $option ); + } ); + + $newstate.val( value ); + + $state.replaceWith( $newstate ); + + $newstate.show().selectWoo().hide().trigger( 'change' ); + } else { + $newstate = $( '' ) + .prop( 'id', input_id ) + .prop( 'name', input_name ) + .prop( 'placeholder', placeholder ) + .addClass( 'js_field-state' ) + .val( stateValue ); + $state.replaceWith( $newstate ); + } + + // This event has a typo - deprecated in 2.5.0 + $( document.body ).trigger( 'contry-change.woocommerce', [country, $( this ).closest( 'div' )] ); + $( document.body ).trigger( 'country-change.woocommerce', [country, $( this ).closest( 'div' )] ); + }, + + change_state: function() { + // Here we will find if state value on a select has changed and stick it to the country data + var $this = $( this ), + state = $this.val(), + $country = $this.parents( 'div.edit_address' ).find( ':input.js_field-country' ), + country = $country.val(); + + $country.data( 'woocommerce.stickState-' + country, state ); + }, + + init_tiptip: function() { + $( '#tiptip_holder' ).removeAttr( 'style' ); + $( '#tiptip_arrow' ).removeAttr( 'style' ); + $( '.tips' ).tipTip({ + 'attribute': 'data-tip', + 'fadeIn': 50, + 'fadeOut': 50, + 'delay': 200, + 'keepAlive': true + }); + }, + + edit_address: function( e ) { + e.preventDefault(); + + var $this = $( this ), + $wrapper = $this.closest( '.order_data_column' ), + $edit_address = $wrapper.find( 'div.edit_address' ), + $address = $wrapper.find( 'div.address' ), + $country_input = $edit_address.find( '.js_field-country' ), + $state_input = $edit_address.find( '.js_field-state' ), + is_billing = Boolean( $edit_address.find( 'input[name^="_billing_"]' ).length ); + + $address.hide(); + $this.parent().find( 'a' ).toggle(); + + if ( ! $country_input.val() ) { + $country_input.val( woocommerce_admin_meta_boxes_order.default_country ).trigger( 'change' ); + $state_input.val( woocommerce_admin_meta_boxes_order.default_state ).trigger( 'change' ); + } + + $edit_address.show(); + + var event_name = is_billing ? 'order_edit_billing_address_click' : 'order_edit_shipping_address_click'; + window.wcTracks.recordEvent( event_name, { + order_id: woocommerce_admin_meta_boxes.post_id, + status: $( '#order_status' ).val() + } ); + }, + + change_customer_user: function() { + if ( ! $( '#_billing_country' ).val() ) { + $( 'a.edit_address' ).trigger( 'click' ); + wc_meta_boxes_order.load_billing( true ); + wc_meta_boxes_order.load_shipping( true ); + } + }, + + load_billing: function( force ) { + if ( true === force || window.confirm( woocommerce_admin_meta_boxes.load_billing ) ) { + + // Get user ID to load data for + var user_id = $( '#customer_user' ).val(); + + if ( ! user_id ) { + window.alert( woocommerce_admin_meta_boxes.no_customer_selected ); + return false; + } + + var data = { + user_id : user_id, + action : 'woocommerce_get_customer_details', + security: woocommerce_admin_meta_boxes.get_customer_details_nonce + }; + + $( this ).closest( 'div.edit_address' ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + + $.ajax({ + url: woocommerce_admin_meta_boxes.ajax_url, + data: data, + type: 'POST', + success: function( response ) { + if ( response && response.billing ) { + $.each( response.billing, function( key, data ) { + $( ':input#_billing_' + key ).val( data ).trigger( 'change' ); + }); + } + $( 'div.edit_address' ).unblock(); + } + }); + } + return false; + }, + + load_shipping: function( force ) { + if ( true === force || window.confirm( woocommerce_admin_meta_boxes.load_shipping ) ) { + + // Get user ID to load data for + var user_id = $( '#customer_user' ).val(); + + if ( ! user_id ) { + window.alert( woocommerce_admin_meta_boxes.no_customer_selected ); + return false; + } + + var data = { + user_id: user_id, + action: 'woocommerce_get_customer_details', + security: woocommerce_admin_meta_boxes.get_customer_details_nonce + }; + + $( this ).closest( 'div.edit_address' ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + + $.ajax({ + url: woocommerce_admin_meta_boxes.ajax_url, + data: data, + type: 'POST', + success: function( response ) { + if ( response && response.billing ) { + $.each( response.shipping, function( key, data ) { + $( ':input#_shipping_' + key ).val( data ).trigger( 'change' ); + }); + } + $( 'div.edit_address' ).unblock(); + } + }); + } + return false; + }, + + copy_billing_to_shipping: function() { + if ( window.confirm( woocommerce_admin_meta_boxes.copy_billing ) ) { + $('.order_data_column :input[name^="_billing_"]').each( function() { + var input_name = $(this).attr('name'); + input_name = input_name.replace( '_billing_', '_shipping_' ); + $( ':input#' + input_name ).val( $(this).val() ).trigger( 'change' ); + }); + } + return false; + } + }; + + /** + * Order Items Panel + */ + var wc_meta_boxes_order_items = { + init: function() { + this.stupidtable.init(); + + $( '#woocommerce-order-items' ) + .on( 'click', 'button.add-line-item', this.add_line_item ) + .on( 'click', 'button.add-coupon', this.add_coupon ) + .on( 'click', 'a.remove-coupon', this.remove_coupon ) + .on( 'click', 'button.refund-items', this.refund_items ) + .on( 'click', '.cancel-action', this.cancel ) + .on( 'click', '.refund-actions .cancel-action', this.track_cancel ) + .on( 'click', 'button.add-order-item', this.add_item ) + .on( 'click', 'button.add-order-fee', this.add_fee ) + .on( 'click', 'button.add-order-shipping', this.add_shipping ) + .on( 'click', 'button.add-order-tax', this.add_tax ) + .on( 'click', 'button.save-action', this.save_line_items ) + .on( 'click', 'a.delete-order-tax', this.delete_tax ) + .on( 'click', 'button.calculate-action', this.recalculate ) + .on( 'click', 'a.edit-order-item', this.edit_item ) + .on( 'click', 'a.delete-order-item', this.delete_item ) + + // Refunds + .on( 'click', '.delete_refund', this.refunds.delete_refund ) + .on( 'click', 'button.do-api-refund, button.do-manual-refund', this.refunds.do_refund ) + .on( 'change', '.refund input.refund_line_total, .refund input.refund_line_tax', this.refunds.input_changed ) + .on( 'change keyup', '.wc-order-refund-items #refund_amount', this.refunds.amount_changed ) + .on( 'change', 'input.refund_order_item_qty', this.refunds.refund_quantity_changed ) + + // Qty + .on( 'change', 'input.quantity', this.quantity_changed ) + + // Subtotal/total + .on( 'keyup change', '.split-input :input', function() { + var $subtotal = $( this ).parent().prev().find(':input'); + if ( $subtotal && ( $subtotal.val() === '' || $subtotal.is( '.match-total' ) ) ) { + $subtotal.val( $( this ).val() ).addClass( 'match-total' ); + } + }) + + .on( 'keyup', '.split-input :input', function() { + $( this ).removeClass( 'match-total' ); + }) + + // Meta + .on( 'click', 'button.add_order_item_meta', this.item_meta.add ) + .on( 'click', 'button.remove_order_item_meta', this.item_meta.remove ) + + // Reload items + .on( 'wc_order_items_reload', this.reload_items ) + .on( 'wc_order_items_reloaded', this.reloaded_items ); + + $( document.body ) + .on( 'wc_backbone_modal_loaded', this.backbone.init ) + .on( 'wc_backbone_modal_response', this.backbone.response ); + }, + + block: function() { + $( '#woocommerce-order-items' ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + }, + + unblock: function() { + $( '#woocommerce-order-items' ).unblock(); + }, + + reload_items: function() { + var data = { + order_id: woocommerce_admin_meta_boxes.post_id, + action: 'woocommerce_load_order_items', + security: woocommerce_admin_meta_boxes.order_item_nonce + }; + + wc_meta_boxes_order_items.block(); + + $.ajax({ + url: woocommerce_admin_meta_boxes.ajax_url, + data: data, + type: 'POST', + success: function( response ) { + $( '#woocommerce-order-items' ).find( '.inside' ).empty(); + $( '#woocommerce-order-items' ).find( '.inside' ).append( response ); + wc_meta_boxes_order_items.reloaded_items(); + wc_meta_boxes_order_items.unblock(); + } + }); + }, + + reloaded_items: function() { + wc_meta_boxes_order.init_tiptip(); + wc_meta_boxes_order_items.stupidtable.init(); + }, + + // When the qty is changed, increase or decrease costs + quantity_changed: function() { + var $row = $( this ).closest( 'tr.item' ); + var qty = $( this ).val(); + var o_qty = $( this ).attr( 'data-qty' ); + var line_total = $( 'input.line_total', $row ); + var line_subtotal = $( 'input.line_subtotal', $row ); + + // Totals + var unit_total = accounting.unformat( line_total.attr( 'data-total' ), woocommerce_admin.mon_decimal_point ) / o_qty; + line_total.val( + parseFloat( accounting.formatNumber( unit_total * qty, woocommerce_admin_meta_boxes.rounding_precision, '' ) ) + .toString() + .replace( '.', woocommerce_admin.mon_decimal_point ) + ); + + var unit_subtotal = accounting.unformat( line_subtotal.attr( 'data-subtotal' ), woocommerce_admin.mon_decimal_point ) / o_qty; + line_subtotal.val( + parseFloat( accounting.formatNumber( unit_subtotal * qty, woocommerce_admin_meta_boxes.rounding_precision, '' ) ) + .toString() + .replace( '.', woocommerce_admin.mon_decimal_point ) + ); + + // Taxes + $( 'input.line_tax', $row ).each( function() { + var $line_total_tax = $( this ); + var tax_id = $line_total_tax.data( 'tax_id' ); + var unit_total_tax = accounting.unformat( + $line_total_tax.attr( 'data-total_tax' ), + woocommerce_admin.mon_decimal_point + ) / o_qty; + var $line_subtotal_tax = $( 'input.line_subtotal_tax[data-tax_id="' + tax_id + '"]', $row ); + var unit_subtotal_tax = accounting.unformat( + $line_subtotal_tax.attr( 'data-subtotal_tax' ), + woocommerce_admin.mon_decimal_point + ) / o_qty; + var round_at_subtotal = 'yes' === woocommerce_admin_meta_boxes.round_at_subtotal; + var precision = woocommerce_admin_meta_boxes[ + round_at_subtotal ? 'rounding_precision' : 'currency_format_num_decimals' + ]; + + if ( 0 < unit_total_tax ) { + $line_total_tax.val( + parseFloat( accounting.formatNumber( unit_total_tax * qty, precision, '' ) ) + .toString() + .replace( '.', woocommerce_admin.mon_decimal_point ) + ); + } + + if ( 0 < unit_subtotal_tax ) { + $line_subtotal_tax.val( + parseFloat( accounting.formatNumber( unit_subtotal_tax * qty, precision, '' ) ) + .toString() + .replace( '.', woocommerce_admin.mon_decimal_point ) + ); + } + }); + + $( this ).trigger( 'quantity_changed' ); + }, + + add_line_item: function() { + $( 'div.wc-order-add-item' ).slideDown(); + $( 'div.wc-order-data-row-toggle' ).not( 'div.wc-order-add-item' ).slideUp(); + + window.wcTracks.recordEvent( 'order_edit_add_items_click', { + order_id: woocommerce_admin_meta_boxes.post_id, + status: $( '#order_status' ).val() + } ); + + return false; + }, + + add_coupon: function() { + window.wcTracks.recordEvent( 'order_edit_add_coupon_click', { + order_id: woocommerce_admin_meta_boxes.post_id, + status: $( '#order_status' ).val() + } ); + + var value = window.prompt( woocommerce_admin_meta_boxes.i18n_apply_coupon ); + + if ( null == value ) { + window.wcTracks.recordEvent( 'order_edit_add_coupon_cancel', { + order_id: woocommerce_admin_meta_boxes.post_id, + status: $( '#order_status' ).val() + } ); + } else { + wc_meta_boxes_order_items.block(); + + var user_id = $( '#customer_user' ).val(); + var user_email = $( '#_billing_email' ).val(); + + var data = $.extend( {}, wc_meta_boxes_order_items.get_taxable_address(), { + action : 'woocommerce_add_coupon_discount', + dataType : 'json', + order_id : woocommerce_admin_meta_boxes.post_id, + security : woocommerce_admin_meta_boxes.order_item_nonce, + coupon : value, + user_id : user_id, + user_email : user_email + } ); + + $.ajax( { + url: woocommerce_admin_meta_boxes.ajax_url, + data: data, + type: 'POST', + success: function( response ) { + if ( response.success ) { + $( '#woocommerce-order-items' ).find( '.inside' ).empty(); + $( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html ); + wc_meta_boxes_order_items.reloaded_items(); + wc_meta_boxes_order_items.unblock(); + } else { + window.alert( response.data.error ); + } + wc_meta_boxes_order_items.unblock(); + }, + complete: function() { + window.wcTracks.recordEvent( 'order_edit_added_coupon', { + order_id: data.order_id, + status: $( '#order_status' ).val() + } ); + } + } ); + } + return false; + }, + + remove_coupon: function() { + var $this = $( this ); + wc_meta_boxes_order_items.block(); + + var data = $.extend( {}, wc_meta_boxes_order_items.get_taxable_address(), { + action : 'woocommerce_remove_order_coupon', + dataType : 'json', + order_id : woocommerce_admin_meta_boxes.post_id, + security : woocommerce_admin_meta_boxes.order_item_nonce, + coupon : $this.data( 'code' ) + } ); + + $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { + if ( response.success ) { + $( '#woocommerce-order-items' ).find( '.inside' ).empty(); + $( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html ); + wc_meta_boxes_order_items.reloaded_items(); + wc_meta_boxes_order_items.unblock(); + } else { + window.alert( response.data.error ); + } + wc_meta_boxes_order_items.unblock(); + }); + }, + + refund_items: function() { + $( 'div.wc-order-refund-items' ).slideDown(); + $( 'div.wc-order-data-row-toggle' ).not( 'div.wc-order-refund-items' ).slideUp(); + $( 'div.wc-order-totals-items' ).slideUp(); + $( '#woocommerce-order-items' ).find( 'div.refund' ).show(); + $( '.wc-order-edit-line-item .wc-order-edit-line-item-actions' ).hide(); + + window.wcTracks.recordEvent( 'order_edit_refund_button_click', { + order_id: woocommerce_admin_meta_boxes.post_id, + status: $( '#order_status' ).val() + } ); + + return false; + }, + + cancel: function() { + $( 'div.wc-order-data-row-toggle' ).not( 'div.wc-order-bulk-actions' ).slideUp(); + $( 'div.wc-order-bulk-actions' ).slideDown(); + $( 'div.wc-order-totals-items' ).slideDown(); + $( '#woocommerce-order-items' ).find( 'div.refund' ).hide(); + $( '.wc-order-edit-line-item .wc-order-edit-line-item-actions' ).show(); + + // Reload the items + if ( 'true' === $( this ).attr( 'data-reload' ) ) { + wc_meta_boxes_order_items.reload_items(); + } + + window.wcTracks.recordEvent( 'order_edit_add_items_cancelled', { + order_id: woocommerce_admin_meta_boxes.post_id, + status: $( '#order_status' ).val() + } ); + + return false; + }, + + track_cancel: function() { + window.wcTracks.recordEvent( 'order_edit_refund_cancel', { + order_id: woocommerce_admin_meta_boxes.post_id, + status: $( '#order_status' ).val() + } ); + }, + + add_item: function() { + $( this ).WCBackboneModal({ + template: 'wc-modal-add-products' + }); + + return false; + }, + + add_fee: function() { + window.wcTracks.recordEvent( 'order_edit_add_fee_click', { + order_id: woocommerce_admin_meta_boxes.post_id, + status: $( '#order_status' ).val() + } ); + + var value = window.prompt( woocommerce_admin_meta_boxes.i18n_add_fee ); + + if ( null == value ) { + window.wcTracks.recordEvent( 'order_edit_add_fee_cancel', { + order_id: woocommerce_admin_meta_boxes.post_id, + status: $( '#order_status' ).val() + } ); + } else { + wc_meta_boxes_order_items.block(); + + var data = $.extend( {}, wc_meta_boxes_order_items.get_taxable_address(), { + action : 'woocommerce_add_order_fee', + dataType: 'json', + order_id: woocommerce_admin_meta_boxes.post_id, + security: woocommerce_admin_meta_boxes.order_item_nonce, + amount : value + } ); + + $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { + if ( response.success ) { + $( '#woocommerce-order-items' ).find( '.inside' ).empty(); + $( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html ); + wc_meta_boxes_order_items.reloaded_items(); + wc_meta_boxes_order_items.unblock(); + window.wcTracks.recordEvent( 'order_edit_added_fee', { + order_id: data.post_id, + status: $( '#order_status' ).val() + } ); + } else { + window.alert( response.data.error ); + } + wc_meta_boxes_order_items.unblock(); + }); + } + return false; + }, + + add_shipping: function() { + wc_meta_boxes_order_items.block(); + + var data = { + action : 'woocommerce_add_order_shipping', + order_id : woocommerce_admin_meta_boxes.post_id, + security : woocommerce_admin_meta_boxes.order_item_nonce, + dataType : 'json' + }; + + $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { + if ( response.success ) { + $( 'table.woocommerce_order_items tbody#order_shipping_line_items' ).append( response.data.html ); + window.wcTracks.recordEvent( 'order_edit_add_shipping', { + order_id: data.post_id, + status: $( '#order_status' ).val() + } ); + } else { + window.alert( response.data.error ); + } + wc_meta_boxes_order_items.unblock(); + }); + + return false; + }, + + add_tax: function() { + $( this ).WCBackboneModal({ + template: 'wc-modal-add-tax' + }); + return false; + }, + + edit_item: function() { + $( this ).closest( 'tr' ).find( '.view' ).hide(); + $( this ).closest( 'tr' ).find( '.edit' ).show(); + $( this ).hide(); + $( 'button.add-line-item' ).trigger( 'click' ); + $( 'button.cancel-action' ).attr( 'data-reload', true ); + window.wcTracks.recordEvent( 'order_edit_edit_item_click', { + order_id: woocommerce_admin_meta_boxes.post_id, + status: $( '#order_status' ).val() + } ); + return false; + }, + + delete_item: function() { + var notice = woocommerce_admin_meta_boxes.remove_item_notice; + + if ( $( this ).parents( 'tbody#order_fee_line_items' ).length ) { + notice = woocommerce_admin_meta_boxes.remove_fee_notice; + } + + if ( $( this ).parents( 'tbody#order_shipping_line_items' ).length ) { + notice = woocommerce_admin_meta_boxes.remove_shipping_notice; + } + + var answer = window.confirm( notice ); + + if ( answer ) { + var $item = $( this ).closest( 'tr.item, tr.fee, tr.shipping' ); + var order_item_id = $item.attr( 'data-order_item_id' ); + + wc_meta_boxes_order_items.block(); + + var data = $.extend( {}, wc_meta_boxes_order_items.get_taxable_address(), { + order_id : woocommerce_admin_meta_boxes.post_id, + order_item_ids: order_item_id, + action : 'woocommerce_remove_order_item', + security : woocommerce_admin_meta_boxes.order_item_nonce + } ); + + // Check if items have changed, if so pass them through so we can save them before deleting. + if ( 'true' === $( 'button.cancel-action' ).attr( 'data-reload' ) ) { + data.items = $( 'table.woocommerce_order_items :input[name], .wc-order-totals-items :input[name]' ).serialize(); + } + + $.ajax({ + url: woocommerce_admin_meta_boxes.ajax_url, + data: data, + type: 'POST', + success: function( response ) { + if ( response.success ) { + $( '#woocommerce-order-items' ).find( '.inside' ).empty(); + $( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html ); + + // Update notes. + if ( response.data.notes_html ) { + $( 'ul.order_notes' ).empty(); + $( 'ul.order_notes' ).append( $( response.data.notes_html ).find( 'li' ) ); + } + + wc_meta_boxes_order_items.reloaded_items(); + wc_meta_boxes_order_items.unblock(); + } else { + window.alert( response.data.error ); + } + wc_meta_boxes_order_items.unblock(); + }, + complete: function() { + window.wcTracks.recordEvent( 'order_edit_remove_item', { + order_id: data.post_id, + status: $( '#order_status' ).val() + } ); + } + }); + } + return false; + }, + + delete_tax: function() { + if ( window.confirm( woocommerce_admin_meta_boxes.i18n_delete_tax ) ) { + wc_meta_boxes_order_items.block(); + + var data = { + action: 'woocommerce_remove_order_tax', + rate_id: $( this ).attr( 'data-rate_id' ), + order_id: woocommerce_admin_meta_boxes.post_id, + security: woocommerce_admin_meta_boxes.order_item_nonce + }; + + $.ajax({ + url: woocommerce_admin_meta_boxes.ajax_url, + data: data, + type: 'POST', + success: function( response ) { + if ( response.success ) { + $( '#woocommerce-order-items' ).find( '.inside' ).empty(); + $( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html ); + wc_meta_boxes_order_items.reloaded_items(); + wc_meta_boxes_order_items.unblock(); + } else { + window.alert( response.data.error ); + } + wc_meta_boxes_order_items.unblock(); + }, + complete: function() { + window.wcTracks.recordEvent( 'order_edit_delete_tax', { + order_id: data.order_id, + status: $( '#order_status' ).val() + } ); + } + }); + } else { + window.wcTracks.recordEvent( 'order_edit_delete_tax_cancel', { + order_id: woocommerce_admin_meta_boxes.post_id, + status: $( '#order_status' ).val() + } ); + } + return false; + }, + + get_taxable_address: function() { + var country = ''; + var state = ''; + var postcode = ''; + var city = ''; + + if ( 'shipping' === woocommerce_admin_meta_boxes.tax_based_on ) { + country = $( '#_shipping_country' ).val(); + state = $( '#_shipping_state' ).val(); + postcode = $( '#_shipping_postcode' ).val(); + city = $( '#_shipping_city' ).val(); + } + + if ( 'billing' === woocommerce_admin_meta_boxes.tax_based_on || ! country ) { + country = $( '#_billing_country' ).val(); + state = $( '#_billing_state' ).val(); + postcode = $( '#_billing_postcode' ).val(); + city = $( '#_billing_city' ).val(); + } + + return { + country: country, + state: state, + postcode: postcode, + city: city + }; + }, + + recalculate: function() { + if ( window.confirm( woocommerce_admin_meta_boxes.calc_totals ) ) { + wc_meta_boxes_order_items.block(); + + var data = $.extend( {}, wc_meta_boxes_order_items.get_taxable_address(), { + action: 'woocommerce_calc_line_taxes', + order_id: woocommerce_admin_meta_boxes.post_id, + items: $( 'table.woocommerce_order_items :input[name], .wc-order-totals-items :input[name]' ).serialize(), + security: woocommerce_admin_meta_boxes.calc_totals_nonce + } ); + + $( document.body ).trigger( 'order-totals-recalculate-before', data ); + + $.ajax({ + url: woocommerce_admin_meta_boxes.ajax_url, + data: data, + type: 'POST', + success: function( response ) { + $( '#woocommerce-order-items' ).find( '.inside' ).empty(); + $( '#woocommerce-order-items' ).find( '.inside' ).append( response ); + wc_meta_boxes_order_items.reloaded_items(); + wc_meta_boxes_order_items.unblock(); + + $( document.body ).trigger( 'order-totals-recalculate-success', response ); + }, + complete: function( response ) { + $( document.body ).trigger( 'order-totals-recalculate-complete', response ); + + window.wcTracks.recordEvent( 'order_edit_recalc_totals', { + order_id: data.post_id, + ok_cancel: 'OK', + status: $( '#order_status' ).val() + } ); + } + }); + } else { + window.wcTracks.recordEvent( 'order_edit_recalc_totals', { + order_id: woocommerce_admin_meta_boxes.post_id, + ok_cancel: 'cancel', + status: $( '#order_status' ).val() + } ); + } + + return false; + }, + + save_line_items: function() { + var data = { + order_id: woocommerce_admin_meta_boxes.post_id, + items: $( 'table.woocommerce_order_items :input[name], .wc-order-totals-items :input[name]' ).serialize(), + action: 'woocommerce_save_order_items', + security: woocommerce_admin_meta_boxes.order_item_nonce + }; + + wc_meta_boxes_order_items.block(); + + $.ajax({ + url: woocommerce_admin_meta_boxes.ajax_url, + data: data, + type: 'POST', + success: function( response ) { + if ( response.success ) { + $( '#woocommerce-order-items' ).find( '.inside' ).empty(); + $( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html ); + + // Update notes. + if ( response.data.notes_html ) { + $( 'ul.order_notes' ).empty(); + $( 'ul.order_notes' ).append( $( response.data.notes_html ).find( 'li' ) ); + } + + wc_meta_boxes_order_items.reloaded_items(); + wc_meta_boxes_order_items.unblock(); + } else { + wc_meta_boxes_order_items.unblock(); + window.alert( response.data.error ); + } + }, + complete: function() { + window.wcTracks.recordEvent( 'order_edit_save_line_items', { + order_id: data.post_id, + status: $( '#order_status' ).val() + } ); + } + }); + + $( this ).trigger( 'items_saved' ); + + return false; + }, + + refunds: { + + do_refund: function() { + wc_meta_boxes_order_items.block(); + + if ( window.confirm( woocommerce_admin_meta_boxes.i18n_do_refund ) ) { + var refund_amount = $( 'input#refund_amount' ).val(); + var refund_reason = $( 'input#refund_reason' ).val(); + var refunded_amount = $( 'input#refunded_amount' ).val(); + + // Get line item refunds + var line_item_qtys = {}; + var line_item_totals = {}; + var line_item_tax_totals = {}; + + $( '.refund input.refund_order_item_qty' ).each(function( index, item ) { + if ( $( item ).closest( 'tr' ).data( 'order_item_id' ) ) { + if ( item.value ) { + line_item_qtys[ $( item ).closest( 'tr' ).data( 'order_item_id' ) ] = item.value; + } + } + }); + + $( '.refund input.refund_line_total' ).each(function( index, item ) { + if ( $( item ).closest( 'tr' ).data( 'order_item_id' ) ) { + line_item_totals[ $( item ).closest( 'tr' ).data( 'order_item_id' ) ] = accounting.unformat( + item.value, + woocommerce_admin.mon_decimal_point + ); + } + }); + + $( '.refund input.refund_line_tax' ).each(function( index, item ) { + if ( $( item ).closest( 'tr' ).data( 'order_item_id' ) ) { + var tax_id = $( item ).data( 'tax_id' ); + + if ( ! line_item_tax_totals[ $( item ).closest( 'tr' ).data( 'order_item_id' ) ] ) { + line_item_tax_totals[ $( item ).closest( 'tr' ).data( 'order_item_id' ) ] = {}; + } + + line_item_tax_totals[ $( item ).closest( 'tr' ).data( 'order_item_id' ) ][ tax_id ] = accounting.unformat( + item.value, + woocommerce_admin.mon_decimal_point + ); + } + }); + + var data = { + action : 'woocommerce_refund_line_items', + order_id : woocommerce_admin_meta_boxes.post_id, + refund_amount : refund_amount, + refunded_amount : refunded_amount, + refund_reason : refund_reason, + line_item_qtys : JSON.stringify( line_item_qtys, null, '' ), + line_item_totals : JSON.stringify( line_item_totals, null, '' ), + line_item_tax_totals : JSON.stringify( line_item_tax_totals, null, '' ), + api_refund : $( this ).is( '.do-api-refund' ), + restock_refunded_items: $( '#restock_refunded_items:checked' ).length ? 'true': 'false', + security : woocommerce_admin_meta_boxes.order_item_nonce + }; + + $.ajax( { + url: woocommerce_admin_meta_boxes.ajax_url, + data: data, + type: 'POST', + success: function( response ) { + if ( true === response.success ) { + // Redirect to same page for show the refunded status + window.location.reload(); + } else { + window.alert( response.data.error ); + wc_meta_boxes_order_items.reload_items(); + wc_meta_boxes_order_items.unblock(); + } + }, + complete: function() { + window.wcTracks.recordEvent( 'order_edit_refunded', { + order_id: data.order_id, + status: $( '#order_status' ).val(), + api_refund: data.api_refund, + has_reason: Boolean( data.refund_reason.length ), + restock: 'true' === data.restock_refunded_items + } ); + } + } ); + } else { + wc_meta_boxes_order_items.unblock(); + } + }, + + delete_refund: function() { + if ( window.confirm( woocommerce_admin_meta_boxes.i18n_delete_refund ) ) { + var $refund = $( this ).closest( 'tr.refund' ); + var refund_id = $refund.attr( 'data-order_refund_id' ); + + wc_meta_boxes_order_items.block(); + + var data = { + action: 'woocommerce_delete_refund', + refund_id: refund_id, + security: woocommerce_admin_meta_boxes.order_item_nonce + }; + + $.ajax({ + url: woocommerce_admin_meta_boxes.ajax_url, + data: data, + type: 'POST', + success: function() { + wc_meta_boxes_order_items.reload_items(); + } + }); + } + return false; + }, + + input_changed: function() { + var refund_amount = 0; + var $items = $( '.woocommerce_order_items' ).find( 'tr.item, tr.fee, tr.shipping' ); + + $items.each(function() { + var $row = $( this ); + var refund_cost_fields = $row.find( '.refund input:not(.refund_order_item_qty)' ); + + refund_cost_fields.each(function( index, el ) { + refund_amount += parseFloat( accounting.unformat( $( el ).val() || 0, woocommerce_admin.mon_decimal_point ) ); + }); + }); + + $( '#refund_amount' ) + .val( accounting.formatNumber( + refund_amount, + woocommerce_admin_meta_boxes.currency_format_num_decimals, + '', + woocommerce_admin.mon_decimal_point + ) ) + .trigger( 'change' ); + }, + + amount_changed: function() { + var total = accounting.unformat( $( this ).val(), woocommerce_admin.mon_decimal_point ); + + $( 'button .wc-order-refund-amount .amount' ).text( accounting.formatMoney( total, { + symbol: woocommerce_admin_meta_boxes.currency_format_symbol, + decimal: woocommerce_admin_meta_boxes.currency_format_decimal_sep, + thousand: woocommerce_admin_meta_boxes.currency_format_thousand_sep, + precision: woocommerce_admin_meta_boxes.currency_format_num_decimals, + format: woocommerce_admin_meta_boxes.currency_format + } ) ); + }, + + // When the refund qty is changed, increase or decrease costs + refund_quantity_changed: function() { + var $row = $( this ).closest( 'tr.item' ); + var qty = $row.find( 'input.quantity' ).val(); + var refund_qty = $( this ).val(); + var line_total = $( 'input.line_total', $row ); + var refund_line_total = $( 'input.refund_line_total', $row ); + + // Totals + var unit_total = accounting.unformat( line_total.attr( 'data-total' ), woocommerce_admin.mon_decimal_point ) / qty; + + refund_line_total.val( + parseFloat( accounting.formatNumber( unit_total * refund_qty, woocommerce_admin_meta_boxes.rounding_precision, '' ) ) + .toString() + .replace( '.', woocommerce_admin.mon_decimal_point ) + ).trigger( 'change' ); + + // Taxes + $( '.refund_line_tax', $row ).each( function() { + var $refund_line_total_tax = $( this ); + var tax_id = $refund_line_total_tax.data( 'tax_id' ); + var line_total_tax = $( 'input.line_tax[data-tax_id="' + tax_id + '"]', $row ); + var unit_total_tax = accounting.unformat( + line_total_tax.data( 'total_tax' ), + woocommerce_admin.mon_decimal_point + ) / qty; + + if ( 0 < unit_total_tax ) { + var round_at_subtotal = 'yes' === woocommerce_admin_meta_boxes.round_at_subtotal; + var precision = woocommerce_admin_meta_boxes[ + round_at_subtotal ? 'rounding_precision' : 'currency_format_num_decimals' + ]; + + $refund_line_total_tax.val( + parseFloat( accounting.formatNumber( unit_total_tax * refund_qty, precision, '' ) ) + .toString() + .replace( '.', woocommerce_admin.mon_decimal_point ) + ).trigger( 'change' ); + } else { + $refund_line_total_tax.val( 0 ).trigger( 'change' ); + } + }); + + // Restock checkbox + if ( refund_qty > 0 ) { + $( '#restock_refunded_items' ).closest( 'tr' ).show(); + } else { + $( '#restock_refunded_items' ).closest( 'tr' ).hide(); + $( '.woocommerce_order_items input.refund_order_item_qty' ).each( function() { + if ( $( this ).val() > 0 ) { + $( '#restock_refunded_items' ).closest( 'tr' ).show(); + } + }); + } + + $( this ).trigger( 'refund_quantity_changed' ); + } + }, + + item_meta: { + + add: function() { + var $button = $( this ); + var $item = $button.closest( 'tr.item, tr.shipping' ); + var $items = $item.find('tbody.meta_items'); + var index = $items.find('tr').length + 1; + var $row = '' + + '' + + '' + + '' + + '' + + '' + + ''; + $items.append( $row ); + + return false; + }, + + remove: function() { + if ( window.confirm( woocommerce_admin_meta_boxes.remove_item_meta ) ) { + var $row = $( this ).closest( 'tr' ); + $row.find( ':input' ).val( '' ); + $row.hide(); + } + return false; + } + }, + + backbone: { + + init: function( e, target ) { + if ( 'wc-modal-add-products' === target ) { + $( document.body ).trigger( 'wc-enhanced-select-init' ); + + $( this ).on( 'change', '.wc-product-search', function() { + if ( ! $( this ).closest( 'tr' ).is( ':last-child' ) ) { + return; + } + var item_table = $( this ).closest( 'table.widefat' ), + item_table_body = item_table.find( 'tbody' ), + index = item_table_body.find( 'tr' ).length, + row = item_table_body.data( 'row' ).replace( /\[0\]/g, '[' + index + ']' ); + + item_table_body.append( '' + row + '' ); + $( document.body ).trigger( 'wc-enhanced-select-init' ); + } ); + } + }, + + response: function( e, target, data ) { + if ( 'wc-modal-add-tax' === target ) { + var rate_id = data.add_order_tax; + var manual_rate_id = ''; + + if ( data.manual_tax_rate_id ) { + manual_rate_id = data.manual_tax_rate_id; + } + + wc_meta_boxes_order_items.backbone.add_tax( rate_id, manual_rate_id ); + } + if ( 'wc-modal-add-products' === target ) { + // Build array of data. + var item_table = $( this ).find( 'table.widefat' ), + item_table_body = item_table.find( 'tbody' ), + rows = item_table_body.find( 'tr' ), + add_items = []; + + $( rows ).each( function() { + var item_id = $( this ).find( ':input[name="item_id"]' ).val(), + item_qty = $( this ).find( ':input[name="item_qty"]' ).val(); + + add_items.push( { + 'id' : item_id, + 'qty': item_qty ? item_qty: 1 + } ); + } ); + + return wc_meta_boxes_order_items.backbone.add_items( add_items ); + } + }, + + add_items: function( add_items ) { + wc_meta_boxes_order_items.block(); + + var data = { + action : 'woocommerce_add_order_item', + order_id : woocommerce_admin_meta_boxes.post_id, + security : woocommerce_admin_meta_boxes.order_item_nonce, + data : add_items + }; + + // Check if items have changed, if so pass them through so we can save them before adding a new item. + if ( 'true' === $( 'button.cancel-action' ).attr( 'data-reload' ) ) { + data.items = $( 'table.woocommerce_order_items :input[name], .wc-order-totals-items :input[name]' ).serialize(); + } + + $.ajax({ + type: 'POST', + url: woocommerce_admin_meta_boxes.ajax_url, + data: data, + success: function( response ) { + if ( response.success ) { + $( '#woocommerce-order-items' ).find( '.inside' ).empty(); + $( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html ); + + // Update notes. + if ( response.data.notes_html ) { + $( 'ul.order_notes' ).empty(); + $( 'ul.order_notes' ).append( $( response.data.notes_html ).find( 'li' ) ); + } + + wc_meta_boxes_order_items.reloaded_items(); + wc_meta_boxes_order_items.unblock(); + } else { + wc_meta_boxes_order_items.unblock(); + window.alert( response.data.error ); + } + }, + complete: function() { + window.wcTracks.recordEvent( 'order_edit_add_products', { + order_id: data.post_id, + status: $( '#order_status' ).val() + } ); + }, + dataType: 'json' + }); + }, + + add_tax: function( rate_id, manual_rate_id ) { + if ( manual_rate_id ) { + rate_id = manual_rate_id; + } + + if ( ! rate_id ) { + return false; + } + + var rates = $( '.order-tax-id' ).map( function() { + return $( this ).val(); + }).get(); + + // Test if already exists + if ( -1 === $.inArray( rate_id, rates ) ) { + wc_meta_boxes_order_items.block(); + + var data = { + action: 'woocommerce_add_order_tax', + rate_id: rate_id, + order_id: woocommerce_admin_meta_boxes.post_id, + security: woocommerce_admin_meta_boxes.order_item_nonce + }; + + $.ajax({ + url : woocommerce_admin_meta_boxes.ajax_url, + data : data, + dataType : 'json', + type : 'POST', + success : function( response ) { + if ( response.success ) { + $( '#woocommerce-order-items' ).find( '.inside' ).empty(); + $( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html ); + wc_meta_boxes_order_items.reloaded_items(); + } else { + window.alert( response.data.error ); + } + wc_meta_boxes_order_items.unblock(); + }, + complete: function() { + window.wcTracks.recordEvent( 'order_edit_add_tax', { + order_id: data.post_id, + status: $( '#order_status' ).val() + } ); + } + }); + } else { + window.alert( woocommerce_admin_meta_boxes.i18n_tax_rate_already_exists ); + } + } + }, + + stupidtable: { + init: function() { + $( '.woocommerce_order_items' ).stupidtable(); + $( '.woocommerce_order_items' ).on( 'aftertablesort', this.add_arrows ); + }, + + add_arrows: function( event, data ) { + var th = $( this ).find( 'th' ); + var arrow = data.direction === 'asc' ? '↑' : '↓'; + var index = data.column; + th.find( '.wc-arrow' ).remove(); + th.eq( index ).append( '' + arrow + '' ); + } + } + }; + + /** + * Order Notes Panel + */ + var wc_meta_boxes_order_notes = { + init: function() { + $( '#woocommerce-order-notes' ) + .on( 'click', 'button.add_note', this.add_order_note ) + .on( 'click', 'a.delete_note', this.delete_order_note ); + + }, + + add_order_note: function() { + if ( ! $( 'textarea#add_order_note' ).val() ) { + return; + } + + $( '#woocommerce-order-notes' ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + + var data = { + action: 'woocommerce_add_order_note', + post_id: woocommerce_admin_meta_boxes.post_id, + note: $( 'textarea#add_order_note' ).val(), + note_type: $( 'select#order_note_type' ).val(), + security: woocommerce_admin_meta_boxes.add_order_note_nonce + }; + + $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { + $( 'ul.order_notes .no-items' ).remove(); + $( 'ul.order_notes' ).prepend( response ); + $( '#woocommerce-order-notes' ).unblock(); + $( '#add_order_note' ).val( '' ); + window.wcTracks.recordEvent( 'order_edit_add_order_note', { + order_id: data.post_id, + note_type: data.note_type || 'private', + status: $( '#order_status' ).val() + } ); + }); + + return false; + }, + + delete_order_note: function() { + if ( window.confirm( woocommerce_admin_meta_boxes.i18n_delete_note ) ) { + var note = $( this ).closest( 'li.note' ); + + $( note ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + + var data = { + action: 'woocommerce_delete_order_note', + note_id: $( note ).attr( 'rel' ), + security: woocommerce_admin_meta_boxes.delete_order_note_nonce + }; + + $.post( woocommerce_admin_meta_boxes.ajax_url, data, function() { + $( note ).remove(); + }); + } + + return false; + } + }; + + /** + * Order Downloads Panel + */ + var wc_meta_boxes_order_downloads = { + init: function() { + $( '.order_download_permissions' ) + .on( 'click', 'button.grant_access', this.grant_access ) + .on( 'click', 'button.revoke_access', this.revoke_access ) + .on( 'click', '#copy-download-link', this.copy_link ) + .on( 'aftercopy', '#copy-download-link', this.copy_success ) + .on( 'aftercopyfailure', '#copy-download-link', this.copy_fail ); + }, + + grant_access: function() { + var products = $( '#grant_access_id' ).val(); + + if ( ! products ) { + return; + } + + $( '.order_download_permissions' ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + + var data = { + action: 'woocommerce_grant_access_to_download', + product_ids: products, + loop: $('.order_download_permissions .wc-metabox').length, + order_id: woocommerce_admin_meta_boxes.post_id, + security: woocommerce_admin_meta_boxes.grant_access_nonce + }; + + $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { + + if ( response ) { + $( '.order_download_permissions .wc-metaboxes' ).append( response ); + } else { + window.alert( woocommerce_admin_meta_boxes.i18n_download_permission_fail ); + } + + $( document.body ).trigger( 'wc-init-datepickers' ); + $( '#grant_access_id' ).val( '' ).trigger( 'change' ); + $( '.order_download_permissions' ).unblock(); + }); + + return false; + }, + + revoke_access: function () { + if ( window.confirm( woocommerce_admin_meta_boxes.i18n_permission_revoke ) ) { + var el = $( this ).parent().parent(); + var product = $( this ).attr( 'rel' ).split( ',' )[0]; + var file = $( this ).attr( 'rel' ).split( ',' )[1]; + var permission_id = $( this ).data( 'permission_id' ); + + if ( product > 0 ) { + $( el ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + + var data = { + action: 'woocommerce_revoke_access_to_download', + product_id: product, + download_id: file, + permission_id: permission_id, + order_id: woocommerce_admin_meta_boxes.post_id, + security: woocommerce_admin_meta_boxes.revoke_access_nonce + }; + + $.post( woocommerce_admin_meta_boxes.ajax_url, data, function() { + // Success + $( el ).fadeOut( '300', function () { + $( el ).remove(); + }); + }); + + } else { + $( el ).fadeOut( '300', function () { + $( el ).remove(); + }); + } + } + return false; + }, + + /** + * Copy download link. + * + * @param {Object} evt Copy event. + */ + copy_link: function( evt ) { + wcClearClipboard(); + wcSetClipboard( $( this ).attr( 'href' ), $( this ) ); + evt.preventDefault(); + }, + + /** + * Display a "Copied!" tip when success copying + */ + copy_success: function() { + $( this ).tipTip({ + 'attribute': 'data-tip', + 'activation': 'focus', + 'fadeIn': 50, + 'fadeOut': 50, + 'delay': 0 + }).trigger( 'focus' ); + }, + + /** + * Displays the copy error message when failure copying. + */ + copy_fail: function() { + $( this ).tipTip({ + 'attribute': 'data-tip-failed', + 'activation': 'focus', + 'fadeIn': 50, + 'fadeOut': 50, + 'delay': 0 + }).trigger( 'focus' ); + } + }; + + wc_meta_boxes_order.init(); + wc_meta_boxes_order_items.init(); + wc_meta_boxes_order_notes.init(); + wc_meta_boxes_order_downloads.init(); +}); diff --git a/plugins/woocommerce/legacy/js/admin/meta-boxes-product-variation.js b/plugins/woocommerce/legacy/js/admin/meta-boxes-product-variation.js new file mode 100644 index 00000000000..79d3e7b322b --- /dev/null +++ b/plugins/woocommerce/legacy/js/admin/meta-boxes-product-variation.js @@ -0,0 +1,1101 @@ +/* global wp, woocommerce_admin_meta_boxes_variations, woocommerce_admin, accounting */ +jQuery( function( $ ) { + 'use strict'; + + /** + * Variations actions + */ + var wc_meta_boxes_product_variations_actions = { + + /** + * Initialize variations actions + */ + init: function() { + $( '#variable_product_options' ) + .on( 'change', 'input.variable_is_downloadable', this.variable_is_downloadable ) + .on( 'change', 'input.variable_is_virtual', this.variable_is_virtual ) + .on( 'change', 'input.variable_manage_stock', this.variable_manage_stock ) + .on( 'click', 'button.notice-dismiss', this.notice_dismiss ) + .on( 'click', 'h3 .sort', this.set_menu_order ) + .on( 'reload', this.reload ); + + $( 'input.variable_is_downloadable, input.variable_is_virtual, input.variable_manage_stock' ).trigger( 'change' ); + $( '#woocommerce-product-data' ).on( 'woocommerce_variations_loaded', this.variations_loaded ); + $( document.body ).on( 'woocommerce_variations_added', this.variation_added ); + }, + + /** + * Reload UI + * + * @param {Object} event + * @param {Int} qty + */ + reload: function() { + wc_meta_boxes_product_variations_ajax.load_variations( 1 ); + wc_meta_boxes_product_variations_pagenav.set_paginav( 0 ); + }, + + /** + * Check if variation is downloadable and show/hide elements + */ + variable_is_downloadable: function() { + $( this ).closest( '.woocommerce_variation' ).find( '.show_if_variation_downloadable' ).hide(); + + if ( $( this ).is( ':checked' ) ) { + $( this ).closest( '.woocommerce_variation' ).find( '.show_if_variation_downloadable' ).show(); + } + }, + + /** + * Check if variation is virtual and show/hide elements + */ + variable_is_virtual: function() { + $( this ).closest( '.woocommerce_variation' ).find( '.hide_if_variation_virtual' ).show(); + + if ( $( this ).is( ':checked' ) ) { + $( this ).closest( '.woocommerce_variation' ).find( '.hide_if_variation_virtual' ).hide(); + } + }, + + /** + * Check if variation manage stock and show/hide elements + */ + variable_manage_stock: function() { + $( this ).closest( '.woocommerce_variation' ).find( '.show_if_variation_manage_stock' ).hide(); + $( this ).closest( '.woocommerce_variation' ).find( '.variable_stock_status' ).show(); + + if ( $( this ).is( ':checked' ) ) { + $( this ).closest( '.woocommerce_variation' ).find( '.show_if_variation_manage_stock' ).show(); + $( this ).closest( '.woocommerce_variation' ).find( '.variable_stock_status' ).hide(); + } + + // Parent level. + if ( $( 'input#_manage_stock:checked' ).length ) { + $( this ).closest( '.woocommerce_variation' ).find( '.variable_stock_status' ).hide(); + } + }, + + /** + * Notice dismiss + */ + notice_dismiss: function() { + $( this ).closest( 'div.notice' ).remove(); + }, + + /** + * Run actions when variations is loaded + * + * @param {Object} event + * @param {Int} needsUpdate + */ + variations_loaded: function( event, needsUpdate ) { + needsUpdate = needsUpdate || false; + + var wrapper = $( '#woocommerce-product-data' ); + + if ( ! needsUpdate ) { + // Show/hide downloadable, virtual and stock fields + $( 'input.variable_is_downloadable, input.variable_is_virtual, input.variable_manage_stock', wrapper ).trigger( 'change' ); + + // Open sale schedule fields when have some sale price date + $( '.woocommerce_variation', wrapper ).each( function( index, el ) { + var $el = $( el ), + date_from = $( '.sale_price_dates_from', $el ).val(), + date_to = $( '.sale_price_dates_to', $el ).val(); + + if ( '' !== date_from || '' !== date_to ) { + $( 'a.sale_schedule', $el ).trigger( 'click' ); + } + }); + + // Remove variation-needs-update classes + $( '.woocommerce_variations .variation-needs-update', wrapper ).removeClass( 'variation-needs-update' ); + + // Disable cancel and save buttons + $( 'button.cancel-variation-changes, button.save-variation-changes', wrapper ).attr( 'disabled', 'disabled' ); + } + + // Init TipTip + $( '#tiptip_holder' ).removeAttr( 'style' ); + $( '#tiptip_arrow' ).removeAttr( 'style' ); + $( '.woocommerce_variations .tips, .woocommerce_variations .help_tip, .woocommerce_variations .woocommerce-help-tip', wrapper ) + .tipTip({ + 'attribute': 'data-tip', + 'fadeIn': 50, + 'fadeOut': 50, + 'delay': 200 + }); + + // Datepicker fields + $( '.sale_price_dates_fields', wrapper ).find( 'input' ).datepicker({ + defaultDate: '', + dateFormat: 'yy-mm-dd', + numberOfMonths: 1, + showButtonPanel: true, + onSelect: function() { + var option = $( this ).is( '.sale_price_dates_from' ) ? 'minDate' : 'maxDate', + dates = $( this ).closest( '.sale_price_dates_fields' ).find( 'input' ), + date = $( this ).datepicker( 'getDate' ); + + dates.not( this ).datepicker( 'option', option, date ); + $( this ).trigger( 'change' ); + } + }); + + // Allow sorting + $( '.woocommerce_variations', wrapper ).sortable({ + items: '.woocommerce_variation', + cursor: 'move', + axis: 'y', + handle: '.sort', + scrollSensitivity: 40, + forcePlaceholderSize: true, + helper: 'clone', + opacity: 0.65, + stop: function() { + wc_meta_boxes_product_variations_actions.variation_row_indexes(); + } + }); + + $( document.body ).trigger( 'wc-enhanced-select-init' ); + }, + + /** + * Run actions when added a variation + * + * @param {Object} event + * @param {Int} qty + */ + variation_added: function( event, qty ) { + if ( 1 === qty ) { + wc_meta_boxes_product_variations_actions.variations_loaded( null, true ); + } + }, + + /** + * Lets the user manually input menu order to move items around pages + */ + set_menu_order: function( event ) { + event.preventDefault(); + var $menu_order = $( this ).closest( '.woocommerce_variation' ).find( '.variation_menu_order' ); + var variation_id = $( this ).closest( '.woocommerce_variation' ).find( '.variable_post_id' ).val(); + var value = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_enter_menu_order, $menu_order.val() ); + + if ( value != null ) { + // Set value, save changes and reload view + $menu_order.val( parseInt( value, 10 ) ).trigger( 'change' ); + + $( this ).closest( '.woocommerce_variation' ) + .append( '' ); + + $( this ).closest( '.woocommerce_variation' ) + .append( '' ); + + wc_meta_boxes_product_variations_ajax.save_variations(); + } + }, + + /** + * Set menu order + */ + variation_row_indexes: function() { + var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + current_page = parseInt( wrapper.attr( 'data-page' ), 10 ), + offset = parseInt( ( current_page - 1 ) * woocommerce_admin_meta_boxes_variations.variations_per_page, 10 ); + + $( '.woocommerce_variations .woocommerce_variation' ).each( function ( index, el ) { + $( '.variation_menu_order', el ) + .val( parseInt( $( el ) + .index( '.woocommerce_variations .woocommerce_variation' ), 10 ) + 1 + offset ) + .trigger( 'change' ); + }); + } + }; + + /** + * Variations media actions + */ + var wc_meta_boxes_product_variations_media = { + + /** + * wp.media frame object + * + * @type {Object} + */ + variable_image_frame: null, + + /** + * Variation image ID + * + * @type {Int} + */ + setting_variation_image_id: null, + + /** + * Variation image object + * + * @type {Object} + */ + setting_variation_image: null, + + /** + * wp.media post ID + * + * @type {Int} + */ + wp_media_post_id: wp.media.model.settings.post.id, + + /** + * Initialize media actions + */ + init: function() { + $( '#variable_product_options' ).on( 'click', '.upload_image_button', this.add_image ); + $( 'a.add_media' ).on( 'click', this.restore_wp_media_post_id ); + }, + + /** + * Added new image + * + * @param {Object} event + */ + add_image: function( event ) { + var $button = $( this ), + post_id = $button.attr( 'rel' ), + $parent = $button.closest( '.upload_image' ); + + wc_meta_boxes_product_variations_media.setting_variation_image = $parent; + wc_meta_boxes_product_variations_media.setting_variation_image_id = post_id; + + event.preventDefault(); + + if ( $button.is( '.remove' ) ) { + + $( '.upload_image_id', wc_meta_boxes_product_variations_media.setting_variation_image ).val( '' ).trigger( 'change' ); + wc_meta_boxes_product_variations_media.setting_variation_image.find( 'img' ).eq( 0 ) + .attr( 'src', woocommerce_admin_meta_boxes_variations.woocommerce_placeholder_img_src ); + wc_meta_boxes_product_variations_media.setting_variation_image.find( '.upload_image_button' ).removeClass( 'remove' ); + + } else { + + // If the media frame already exists, reopen it. + if ( wc_meta_boxes_product_variations_media.variable_image_frame ) { + wc_meta_boxes_product_variations_media.variable_image_frame.uploader.uploader + .param( 'post_id', wc_meta_boxes_product_variations_media.setting_variation_image_id ); + wc_meta_boxes_product_variations_media.variable_image_frame.open(); + return; + } else { + wp.media.model.settings.post.id = wc_meta_boxes_product_variations_media.setting_variation_image_id; + } + + // Create the media frame. + wc_meta_boxes_product_variations_media.variable_image_frame = wp.media.frames.variable_image = wp.media({ + // Set the title of the modal. + title: woocommerce_admin_meta_boxes_variations.i18n_choose_image, + button: { + text: woocommerce_admin_meta_boxes_variations.i18n_set_image + }, + states: [ + new wp.media.controller.Library({ + title: woocommerce_admin_meta_boxes_variations.i18n_choose_image, + filterable: 'all' + }) + ] + }); + + // When an image is selected, run a callback. + wc_meta_boxes_product_variations_media.variable_image_frame.on( 'select', function () { + + var attachment = wc_meta_boxes_product_variations_media.variable_image_frame.state() + .get( 'selection' ).first().toJSON(), + url = attachment.sizes && attachment.sizes.thumbnail ? attachment.sizes.thumbnail.url : attachment.url; + + $( '.upload_image_id', wc_meta_boxes_product_variations_media.setting_variation_image ).val( attachment.id ) + .trigger( 'change' ); + wc_meta_boxes_product_variations_media.setting_variation_image.find( '.upload_image_button' ).addClass( 'remove' ); + wc_meta_boxes_product_variations_media.setting_variation_image.find( 'img' ).eq( 0 ).attr( 'src', url ); + + wp.media.model.settings.post.id = wc_meta_boxes_product_variations_media.wp_media_post_id; + }); + + // Finally, open the modal. + wc_meta_boxes_product_variations_media.variable_image_frame.open(); + } + }, + + /** + * Restore wp.media post ID. + */ + restore_wp_media_post_id: function() { + wp.media.model.settings.post.id = wc_meta_boxes_product_variations_media.wp_media_post_id; + } + }; + + /** + * Product variations metabox ajax methods + */ + var wc_meta_boxes_product_variations_ajax = { + + /** + * Initialize variations ajax methods + */ + init: function() { + $( 'li.variations_tab a' ).on( 'click', this.initial_load ); + + $( '#variable_product_options' ) + .on( 'click', 'button.save-variation-changes', this.save_variations ) + .on( 'click', 'button.cancel-variation-changes', this.cancel_variations ) + .on( 'click', '.remove_variation', this.remove_variation ) + .on( 'click','.downloadable_files a.delete', this.input_changed ); + + $( document.body ) + .on( 'change input', '#variable_product_options .woocommerce_variations :input', this.input_changed ) + .on( 'change', '.variations-defaults select', this.defaults_changed ); + + var postForm = $( 'form#post' ); + + postForm.on( 'submit', this.save_on_submit ); + + $( 'input:submit', postForm ).on( 'click keypress', function() { + postForm.data( 'callerid', this.id ); + }); + + $( '.wc-metaboxes-wrapper' ).on( 'click', 'a.do_variation_action', this.do_variation_action ); + }, + + /** + * Check if have some changes before leave the page + * + * @return {Bool} + */ + check_for_changes: function() { + var need_update = $( '#variable_product_options' ).find( '.woocommerce_variations .variation-needs-update' ); + + if ( 0 < need_update.length ) { + if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_edited_variations ) ) { + wc_meta_boxes_product_variations_ajax.save_changes(); + } else { + need_update.removeClass( 'variation-needs-update' ); + return false; + } + } + + return true; + }, + + /** + * Block edit screen + */ + block: function() { + $( '#woocommerce-product-data' ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + }, + + /** + * Unblock edit screen + */ + unblock: function() { + $( '#woocommerce-product-data' ).unblock(); + }, + + /** + * Initial load variations + * + * @return {Bool} + */ + initial_load: function() { + if ( 0 === $( '#variable_product_options' ).find( '.woocommerce_variations .woocommerce_variation' ).length ) { + wc_meta_boxes_product_variations_pagenav.go_to_page(); + } + }, + + /** + * Load variations via Ajax + * + * @param {Int} page (default: 1) + * @param {Int} per_page (default: 10) + */ + load_variations: function( page, per_page ) { + page = page || 1; + per_page = per_page || woocommerce_admin_meta_boxes_variations.variations_per_page; + + var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ); + + wc_meta_boxes_product_variations_ajax.block(); + + $.ajax({ + url: woocommerce_admin_meta_boxes_variations.ajax_url, + data: { + action: 'woocommerce_load_variations', + security: woocommerce_admin_meta_boxes_variations.load_variations_nonce, + product_id: woocommerce_admin_meta_boxes_variations.post_id, + attributes: wrapper.data( 'attributes' ), + page: page, + per_page: per_page + }, + type: 'POST', + success: function( response ) { + wrapper.empty().append( response ).attr( 'data-page', page ); + + $( '#woocommerce-product-data' ).trigger( 'woocommerce_variations_loaded' ); + + wc_meta_boxes_product_variations_ajax.unblock(); + } + }); + }, + + /** + * Ger variations fields and convert to object + * + * @param {Object} fields + * + * @return {Object} + */ + get_variations_fields: function( fields ) { + var data = $( ':input', fields ).serializeJSON(); + + $( '.variations-defaults select' ).each( function( index, element ) { + var select = $( element ); + data[ select.attr( 'name' ) ] = select.val(); + }); + + return data; + }, + + /** + * Save variations changes + * + * @param {Function} callback Called once saving is complete + */ + save_changes: function( callback ) { + var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + need_update = $( '.variation-needs-update', wrapper ), + data = {}; + + // Save only with products need update. + if ( 0 < need_update.length ) { + wc_meta_boxes_product_variations_ajax.block(); + + data = wc_meta_boxes_product_variations_ajax.get_variations_fields( need_update ); + data.action = 'woocommerce_save_variations'; + data.security = woocommerce_admin_meta_boxes_variations.save_variations_nonce; + data.product_id = woocommerce_admin_meta_boxes_variations.post_id; + data['product-type'] = $( '#product-type' ).val(); + + $.ajax({ + url: woocommerce_admin_meta_boxes_variations.ajax_url, + data: data, + type: 'POST', + success: function( response ) { + // Allow change page, delete and add new variations + need_update.removeClass( 'variation-needs-update' ); + $( 'button.cancel-variation-changes, button.save-variation-changes' ).attr( 'disabled', 'disabled' ); + + $( '#woocommerce-product-data' ).trigger( 'woocommerce_variations_saved' ); + + if ( typeof callback === 'function' ) { + callback( response ); + } + + wc_meta_boxes_product_variations_ajax.unblock(); + } + }); + } + }, + + /** + * Save variations + * + * @return {Bool} + */ + save_variations: function() { + $( '#variable_product_options' ).trigger( 'woocommerce_variations_save_variations_button' ); + + wc_meta_boxes_product_variations_ajax.save_changes( function( error ) { + var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + current = wrapper.attr( 'data-page' ); + + $( '#variable_product_options' ).find( '#woocommerce_errors' ).remove(); + + if ( error ) { + wrapper.before( error ); + } + + $( '.variations-defaults select' ).each( function() { + $( this ).attr( 'data-current', $( this ).val() ); + }); + + wc_meta_boxes_product_variations_pagenav.go_to_page( current ); + }); + + return false; + }, + + /** + * Save on post form submit + */ + save_on_submit: function( e ) { + var need_update = $( '#variable_product_options' ).find( '.woocommerce_variations .variation-needs-update' ); + + if ( 0 < need_update.length ) { + e.preventDefault(); + $( '#variable_product_options' ).trigger( 'woocommerce_variations_save_variations_on_submit' ); + wc_meta_boxes_product_variations_ajax.save_changes( wc_meta_boxes_product_variations_ajax.save_on_submit_done ); + } + }, + + /** + * After saved, continue with form submission + */ + save_on_submit_done: function() { + var postForm = $( 'form#post' ), + callerid = postForm.data( 'callerid' ); + + if ( 'publish' === callerid ) { + postForm.append('').trigger( 'submit' ); + } else { + postForm.append('').trigger( 'submit' ); + } + }, + + /** + * Discart changes. + * + * @return {Bool} + */ + cancel_variations: function() { + var current = parseInt( $( '#variable_product_options' ).find( '.woocommerce_variations' ).attr( 'data-page' ), 10 ); + + $( '#variable_product_options' ).find( '.woocommerce_variations .variation-needs-update' ) + .removeClass( 'variation-needs-update' ); + $( '.variations-defaults select' ).each( function() { + $( this ).val( $( this ).attr( 'data-current' ) ); + }); + + wc_meta_boxes_product_variations_pagenav.go_to_page( current ); + + return false; + }, + + /** + * Add variation + * + * @return {Bool} + */ + add_variation: function() { + wc_meta_boxes_product_variations_ajax.block(); + + var data = { + action: 'woocommerce_add_variation', + post_id: woocommerce_admin_meta_boxes_variations.post_id, + loop: $( '.woocommerce_variation' ).length, + security: woocommerce_admin_meta_boxes_variations.add_variation_nonce + }; + + $.post( woocommerce_admin_meta_boxes_variations.ajax_url, data, function( response ) { + var variation = $( response ); + variation.addClass( 'variation-needs-update' ); + + $( '.woocommerce-notice-invalid-variation' ).remove(); + $( '#variable_product_options' ).find( '.woocommerce_variations' ).prepend( variation ); + $( 'button.cancel-variation-changes, button.save-variation-changes' ).prop( 'disabled', false ); + $( '#variable_product_options' ).trigger( 'woocommerce_variations_added', 1 ); + wc_meta_boxes_product_variations_ajax.unblock(); + }); + + return false; + }, + + /** + * Remove variation + * + * @return {Bool} + */ + remove_variation: function() { + wc_meta_boxes_product_variations_ajax.check_for_changes(); + + if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_remove_variation ) ) { + var variation = $( this ).attr( 'rel' ), + variation_ids = [], + data = { + action: 'woocommerce_remove_variations' + }; + + wc_meta_boxes_product_variations_ajax.block(); + + if ( 0 < variation ) { + variation_ids.push( variation ); + + data.variation_ids = variation_ids; + data.security = woocommerce_admin_meta_boxes_variations.delete_variations_nonce; + + $.post( woocommerce_admin_meta_boxes_variations.ajax_url, data, function() { + var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + current_page = parseInt( wrapper.attr( 'data-page' ), 10 ), + total_pages = Math.ceil( ( + parseInt( wrapper.attr( 'data-total' ), 10 ) - 1 + ) / woocommerce_admin_meta_boxes_variations.variations_per_page ), + page = 1; + + $( '#woocommerce-product-data' ).trigger( 'woocommerce_variations_removed' ); + + if ( current_page === total_pages || current_page <= total_pages ) { + page = current_page; + } else if ( current_page > total_pages && 0 !== total_pages ) { + page = total_pages; + } + + wc_meta_boxes_product_variations_pagenav.go_to_page( page, -1 ); + }); + + } else { + wc_meta_boxes_product_variations_ajax.unblock(); + } + } + + return false; + }, + + /** + * Link all variations (or at least try :p) + * + * @return {Bool} + */ + link_all_variations: function() { + wc_meta_boxes_product_variations_ajax.check_for_changes(); + + if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_link_all_variations ) ) { + wc_meta_boxes_product_variations_ajax.block(); + + var data = { + action: 'woocommerce_link_all_variations', + post_id: woocommerce_admin_meta_boxes_variations.post_id, + security: woocommerce_admin_meta_boxes_variations.link_variation_nonce + }; + + $.post( woocommerce_admin_meta_boxes_variations.ajax_url, data, function( response ) { + var count = parseInt( response, 10 ); + + if ( 1 === count ) { + window.alert( count + ' ' + woocommerce_admin_meta_boxes_variations.i18n_variation_added ); + } else if ( 0 === count || count > 1 ) { + window.alert( count + ' ' + woocommerce_admin_meta_boxes_variations.i18n_variations_added ); + } else { + window.alert( woocommerce_admin_meta_boxes_variations.i18n_no_variations_added ); + } + + if ( count > 0 ) { + wc_meta_boxes_product_variations_pagenav.go_to_page( 1, count ); + $( '#variable_product_options' ).trigger( 'woocommerce_variations_added', count ); + } else { + wc_meta_boxes_product_variations_ajax.unblock(); + } + }); + } + + return false; + }, + + /** + * Add new class when have changes in some input + */ + input_changed: function( event ) { + $( this ) + .closest( '.woocommerce_variation' ) + .addClass( 'variation-needs-update' ); + + $( 'button.cancel-variation-changes, button.save-variation-changes' ).prop( 'disabled', false ); + + // Do not trigger 'woocommerce_variations_input_changed' for 'input' events for backwards compat. + if ( 'input' === event.type && $( this ).is( ':text' ) ) { + return; + } + + $( '#variable_product_options' ).trigger( 'woocommerce_variations_input_changed' ); + }, + + /** + * Added new .variation-needs-update class when defaults is changed + */ + defaults_changed: function() { + $( this ) + .closest( '#variable_product_options' ) + .find( '.woocommerce_variation:first' ) + .addClass( 'variation-needs-update' ); + + $( 'button.cancel-variation-changes, button.save-variation-changes' ).prop( 'disabled', false ); + + $( '#variable_product_options' ).trigger( 'woocommerce_variations_defaults_changed' ); + }, + + /** + * Actions + */ + do_variation_action: function() { + var do_variation_action = $( 'select.variation_actions' ).val(), + data = {}, + changes = 0, + value; + + switch ( do_variation_action ) { + case 'add_variation' : + wc_meta_boxes_product_variations_ajax.add_variation(); + return; + case 'link_all_variations' : + wc_meta_boxes_product_variations_ajax.link_all_variations(); + return; + case 'delete_all' : + if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_delete_all_variations ) ) { + if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_last_warning ) ) { + data.allowed = true; + changes = parseInt( $( '#variable_product_options' ).find( '.woocommerce_variations' ) + .attr( 'data-total' ), 10 ) * -1; + } + } + break; + case 'variable_regular_price_increase' : + case 'variable_regular_price_decrease' : + case 'variable_sale_price_increase' : + case 'variable_sale_price_decrease' : + value = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_enter_a_value_fixed_or_percent ); + + if ( value != null ) { + if ( value.indexOf( '%' ) >= 0 ) { + data.value = accounting.unformat( value.replace( /\%/, '' ), woocommerce_admin.mon_decimal_point ) + '%'; + } else { + data.value = accounting.unformat( value, woocommerce_admin.mon_decimal_point ); + } + } else { + return; + } + break; + case 'variable_regular_price' : + case 'variable_sale_price' : + case 'variable_stock' : + case 'variable_low_stock_amount' : + case 'variable_weight' : + case 'variable_length' : + case 'variable_width' : + case 'variable_height' : + case 'variable_download_limit' : + case 'variable_download_expiry' : + value = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_enter_a_value ); + + if ( value != null ) { + data.value = value; + } else { + return; + } + break; + case 'variable_sale_schedule' : + data.date_from = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_scheduled_sale_start ); + data.date_to = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_scheduled_sale_end ); + + if ( null === data.date_from ) { + data.date_from = false; + } + + if ( null === data.date_to ) { + data.date_to = false; + } + + if ( false === data.date_to && false === data.date_from ) { + return; + } + break; + default : + $( 'select.variation_actions' ).trigger( do_variation_action ); + data = $( 'select.variation_actions' ).triggerHandler( do_variation_action + '_ajax_data', data ); + + if ( null === data ) { + return; + } + break; + } + + if ( 'delete_all' === do_variation_action && data.allowed ) { + $( '#variable_product_options' ).find( '.variation-needs-update' ).removeClass( 'variation-needs-update' ); + } else { + wc_meta_boxes_product_variations_ajax.check_for_changes(); + } + + wc_meta_boxes_product_variations_ajax.block(); + + $.ajax({ + url: woocommerce_admin_meta_boxes_variations.ajax_url, + data: { + action: 'woocommerce_bulk_edit_variations', + security: woocommerce_admin_meta_boxes_variations.bulk_edit_variations_nonce, + product_id: woocommerce_admin_meta_boxes_variations.post_id, + product_type: $( '#product-type' ).val(), + bulk_action: do_variation_action, + data: data + }, + type: 'POST', + success: function() { + wc_meta_boxes_product_variations_pagenav.go_to_page( 1, changes ); + } + }); + } + }; + + /** + * Product variations pagenav + */ + var wc_meta_boxes_product_variations_pagenav = { + + /** + * Initialize products variations meta box + */ + init: function() { + $( document.body ) + .on( 'woocommerce_variations_added', this.update_single_quantity ) + .on( 'change', '.variations-pagenav .page-selector', this.page_selector ) + .on( 'click', '.variations-pagenav .first-page', this.first_page ) + .on( 'click', '.variations-pagenav .prev-page', this.prev_page ) + .on( 'click', '.variations-pagenav .next-page', this.next_page ) + .on( 'click', '.variations-pagenav .last-page', this.last_page ); + }, + + /** + * Set variations count + * + * @param {Int} qty + * + * @return {Int} + */ + update_variations_count: function( qty ) { + var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + total = parseInt( wrapper.attr( 'data-total' ), 10 ) + qty, + displaying_num = $( '.variations-pagenav .displaying-num' ); + + // Set the new total of variations + wrapper.attr( 'data-total', total ); + + if ( 1 === total ) { + displaying_num.text( woocommerce_admin_meta_boxes_variations.i18n_variation_count_single.replace( '%qty%', total ) ); + } else { + displaying_num.text( woocommerce_admin_meta_boxes_variations.i18n_variation_count_plural.replace( '%qty%', total ) ); + } + + return total; + }, + + /** + * Update variations quantity when add a new variation + * + * @param {Object} event + * @param {Int} qty + */ + update_single_quantity: function( event, qty ) { + if ( 1 === qty ) { + var page_nav = $( '.variations-pagenav' ); + + wc_meta_boxes_product_variations_pagenav.update_variations_count( qty ); + + if ( page_nav.is( ':hidden' ) ) { + $( 'option, optgroup', '.variation_actions' ).show(); + $( '.variation_actions' ).val( 'add_variation' ); + $( '#variable_product_options' ).find( '.toolbar' ).show(); + page_nav.show(); + $( '.pagination-links', page_nav ).hide(); + } + } + }, + + /** + * Set the pagenav fields + * + * @param {Int} qty + */ + set_paginav: function( qty ) { + var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + new_qty = wc_meta_boxes_product_variations_pagenav.update_variations_count( qty ), + toolbar = $( '#variable_product_options' ).find( '.toolbar' ), + variation_action = $( '.variation_actions' ), + page_nav = $( '.variations-pagenav' ), + displaying_links = $( '.pagination-links', page_nav ), + total_pages = Math.ceil( new_qty / woocommerce_admin_meta_boxes_variations.variations_per_page ), + options = ''; + + // Set the new total of pages + wrapper.attr( 'data-total_pages', total_pages ); + + $( '.total-pages', page_nav ).text( total_pages ); + + // Set the new pagenav options + for ( var i = 1; i <= total_pages; i++ ) { + options += ''; + } + + $( '.page-selector', page_nav ).empty().html( options ); + + // Show/hide pagenav + if ( 0 === new_qty ) { + toolbar.not( '.toolbar-top, .toolbar-buttons' ).hide(); + page_nav.hide(); + $( 'option, optgroup', variation_action ).hide(); + $( '.variation_actions' ).val( 'add_variation' ); + $( 'option[data-global="true"]', variation_action ).show(); + + } else { + toolbar.show(); + page_nav.show(); + $( 'option, optgroup', variation_action ).show(); + $( '.variation_actions' ).val( 'add_variation' ); + + // Show/hide links + if ( 1 === total_pages ) { + displaying_links.hide(); + } else { + displaying_links.show(); + } + } + }, + + /** + * Check button if enabled and if don't have changes + * + * @return {Bool} + */ + check_is_enabled: function( current ) { + return ! $( current ).hasClass( 'disabled' ); + }, + + /** + * Change "disabled" class on pagenav + */ + change_classes: function( selected, total ) { + var first_page = $( '.variations-pagenav .first-page' ), + prev_page = $( '.variations-pagenav .prev-page' ), + next_page = $( '.variations-pagenav .next-page' ), + last_page = $( '.variations-pagenav .last-page' ); + + if ( 1 === selected ) { + first_page.addClass( 'disabled' ); + prev_page.addClass( 'disabled' ); + } else { + first_page.removeClass( 'disabled' ); + prev_page.removeClass( 'disabled' ); + } + + if ( total === selected ) { + next_page.addClass( 'disabled' ); + last_page.addClass( 'disabled' ); + } else { + next_page.removeClass( 'disabled' ); + last_page.removeClass( 'disabled' ); + } + }, + + /** + * Set page + */ + set_page: function( page ) { + $( '.variations-pagenav .page-selector' ).val( page ).first().trigger( 'change' ); + }, + + /** + * Navigate on variations pages + * + * @param {Int} page + * @param {Int} qty + */ + go_to_page: function( page, qty ) { + page = page || 1; + qty = qty || 0; + + wc_meta_boxes_product_variations_pagenav.set_paginav( qty ); + wc_meta_boxes_product_variations_pagenav.set_page( page ); + }, + + /** + * Paginav pagination selector + */ + page_selector: function() { + var selected = parseInt( $( this ).val(), 10 ), + wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ); + + $( '.variations-pagenav .page-selector' ).val( selected ); + + wc_meta_boxes_product_variations_ajax.check_for_changes(); + wc_meta_boxes_product_variations_pagenav.change_classes( selected, parseInt( wrapper.attr( 'data-total_pages' ), 10 ) ); + wc_meta_boxes_product_variations_ajax.load_variations( selected ); + }, + + /** + * Go to first page + * + * @return {Bool} + */ + first_page: function() { + if ( wc_meta_boxes_product_variations_pagenav.check_is_enabled( this ) ) { + wc_meta_boxes_product_variations_pagenav.set_page( 1 ); + } + + return false; + }, + + /** + * Go to previous page + * + * @return {Bool} + */ + prev_page: function() { + if ( wc_meta_boxes_product_variations_pagenav.check_is_enabled( this ) ) { + var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + prev_page = parseInt( wrapper.attr( 'data-page' ), 10 ) - 1, + new_page = ( 0 < prev_page ) ? prev_page : 1; + + wc_meta_boxes_product_variations_pagenav.set_page( new_page ); + } + + return false; + }, + + /** + * Go to next page + * + * @return {Bool} + */ + next_page: function() { + if ( wc_meta_boxes_product_variations_pagenav.check_is_enabled( this ) ) { + var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + total_pages = parseInt( wrapper.attr( 'data-total_pages' ), 10 ), + next_page = parseInt( wrapper.attr( 'data-page' ), 10 ) + 1, + new_page = ( total_pages >= next_page ) ? next_page : total_pages; + + wc_meta_boxes_product_variations_pagenav.set_page( new_page ); + } + + return false; + }, + + /** + * Go to last page + * + * @return {Bool} + */ + last_page: function() { + if ( wc_meta_boxes_product_variations_pagenav.check_is_enabled( this ) ) { + var last_page = $( '#variable_product_options' ).find( '.woocommerce_variations' ).attr( 'data-total_pages' ); + + wc_meta_boxes_product_variations_pagenav.set_page( last_page ); + } + + return false; + } + }; + + wc_meta_boxes_product_variations_actions.init(); + wc_meta_boxes_product_variations_media.init(); + wc_meta_boxes_product_variations_ajax.init(); + wc_meta_boxes_product_variations_pagenav.init(); + +}); diff --git a/plugins/woocommerce/legacy/js/admin/meta-boxes-product.js b/plugins/woocommerce/legacy/js/admin/meta-boxes-product.js new file mode 100644 index 00000000000..e9cad28e4c6 --- /dev/null +++ b/plugins/woocommerce/legacy/js/admin/meta-boxes-product.js @@ -0,0 +1,697 @@ +/*global woocommerce_admin_meta_boxes */ +jQuery( function( $ ) { + + // Scroll to first checked category + // https://github.com/scribu/wp-category-checklist-tree/blob/d1c3c1f449e1144542efa17dde84a9f52ade1739/category-checklist-tree.php + $( function() { + $( '[id$="-all"] > ul.categorychecklist' ).each( function() { + var $list = $( this ); + var $firstChecked = $list.find( ':checked' ).first(); + + if ( ! $firstChecked.length ) { + return; + } + + var pos_first = $list.find( 'input' ).position().top; + var pos_checked = $firstChecked.position().top; + + $list.closest( '.tabs-panel' ).scrollTop( pos_checked - pos_first + 5 ); + }); + }); + + // Prevent enter submitting post form. + $( '#upsell_product_data' ).on( 'keypress', function( e ) { + if ( e.keyCode === 13 ) { + return false; + } + }); + + // Type box. + if ( $( 'body' ).hasClass( 'wc-wp-version-gte-55' ) ) { + $( '.type_box' ).appendTo( '#woocommerce-product-data .hndle' ); + } else { + $( '.type_box' ).appendTo( '#woocommerce-product-data .hndle span' ); + } + + $( function() { + var woocommerce_product_data = $( '#woocommerce-product-data' ); + + // Prevent inputs in meta box headings opening/closing contents. + woocommerce_product_data.find( '.hndle' ).off( 'click.postboxes' ); + + woocommerce_product_data.on( 'click', '.hndle', function( event ) { + + // If the user clicks on some form input inside the h3 the box should not be toggled. + if ( $( event.target ).filter( 'input, option, label, select' ).length ) { + return; + } + + if ( woocommerce_product_data.hasClass( 'closed' ) ) { + woocommerce_product_data.removeClass( 'closed' ); + } else { + woocommerce_product_data.addClass( 'closed' ); + } + }); + }); + + // Catalog Visibility. + $( '#catalog-visibility' ).find( '.edit-catalog-visibility' ).on( 'click', function() { + if ( $( '#catalog-visibility-select' ).is( ':hidden' ) ) { + $( '#catalog-visibility-select' ).slideDown( 'fast' ); + $( this ).hide(); + } + return false; + }); + $( '#catalog-visibility' ).find( '.save-post-visibility' ).on( 'click', function() { + $( '#catalog-visibility-select' ).slideUp( 'fast' ); + $( '#catalog-visibility' ).find( '.edit-catalog-visibility' ).show(); + + var label = $( 'input[name=_visibility]:checked' ).attr( 'data-label' ); + + if ( $( 'input[name=_featured]' ).is( ':checked' ) ) { + label = label + ', ' + woocommerce_admin_meta_boxes.featured_label; + $( 'input[name=_featured]' ).attr( 'checked', 'checked' ); + } + + $( '#catalog-visibility-display' ).text( label ); + return false; + }); + $( '#catalog-visibility' ).find( '.cancel-post-visibility' ).on( 'click', function() { + $( '#catalog-visibility-select' ).slideUp( 'fast' ); + $( '#catalog-visibility' ).find( '.edit-catalog-visibility' ).show(); + + var current_visibility = $( '#current_visibility' ).val(); + var current_featured = $( '#current_featured' ).val(); + + $( 'input[name=_visibility]' ).prop( 'checked', false ); + $( 'input[name=_visibility][value=' + current_visibility + ']' ).attr( 'checked', 'checked' ); + + var label = $( 'input[name=_visibility]:checked' ).attr( 'data-label' ); + + if ( 'yes' === current_featured ) { + label = label + ', ' + woocommerce_admin_meta_boxes.featured_label; + $( 'input[name=_featured]' ).attr( 'checked', 'checked' ); + } else { + $( 'input[name=_featured]' ).prop( 'checked', false ); + } + + $( '#catalog-visibility-display' ).text( label ); + return false; + }); + + // Product type specific options. + $( 'select#product-type' ).on( 'change', function() { + + // Get value. + var select_val = $( this ).val(); + + if ( 'variable' === select_val ) { + $( 'input#_manage_stock' ).trigger( 'change' ); + $( 'input#_downloadable' ).prop( 'checked', false ); + $( 'input#_virtual' ).prop( 'checked', false ); + } else if ( 'grouped' === select_val ) { + $( 'input#_downloadable' ).prop( 'checked', false ); + $( 'input#_virtual' ).prop( 'checked', false ); + } else if ( 'external' === select_val ) { + $( 'input#_downloadable' ).prop( 'checked', false ); + $( 'input#_virtual' ).prop( 'checked', false ); + } + + show_and_hide_panels(); + + $( 'ul.wc-tabs li:visible' ).eq( 0 ).find( 'a' ).trigger( 'click' ); + + $( document.body ).trigger( 'woocommerce-product-type-change', select_val, $( this ) ); + + }).trigger( 'change' ); + + $( 'input#_downloadable, input#_virtual' ).on( 'change', function() { + show_and_hide_panels(); + }); + + function show_and_hide_panels() { + var product_type = $( 'select#product-type' ).val(); + var is_virtual = $( 'input#_virtual:checked' ).length; + var is_downloadable = $( 'input#_downloadable:checked' ).length; + + // Hide/Show all with rules. + var hide_classes = '.hide_if_downloadable, .hide_if_virtual'; + var show_classes = '.show_if_downloadable, .show_if_virtual'; + + $.each( woocommerce_admin_meta_boxes.product_types, function( index, value ) { + hide_classes = hide_classes + ', .hide_if_' + value; + show_classes = show_classes + ', .show_if_' + value; + }); + + $( hide_classes ).show(); + $( show_classes ).hide(); + + // Shows rules. + if ( is_downloadable ) { + $( '.show_if_downloadable' ).show(); + } + if ( is_virtual ) { + $( '.show_if_virtual' ).show(); + + // If user enables virtual while on shipping tab, switch to general tab. + if ( $( '.shipping_options.shipping_tab' ).hasClass( 'active' ) ) { + $( '.general_options.general_tab > a' ).trigger( 'click' ); + } + } + + $( '.show_if_' + product_type ).show(); + + // Hide rules. + if ( is_downloadable ) { + $( '.hide_if_downloadable' ).hide(); + } + if ( is_virtual ) { + $( '.hide_if_virtual' ).hide(); + } + + $( '.hide_if_' + product_type ).hide(); + + $( 'input#_manage_stock' ).trigger( 'change' ); + + // Hide empty panels/tabs after display. + $( '.woocommerce_options_panel' ).each( function() { + var $children = $( this ).children( '.options_group' ); + + if ( 0 === $children.length ) { + return; + } + + var $invisble = $children.filter( function() { + return 'none' === $( this ).css( 'display' ); + }); + + // Hide panel. + if ( $invisble.length === $children.length ) { + var $id = $( this ).prop( 'id' ); + $( '.product_data_tabs' ).find( 'li a[href="#' + $id + '"]' ).parent().hide(); + } + }); + } + + // Sale price schedule. + $( '.sale_price_dates_fields' ).each( function() { + var $these_sale_dates = $( this ); + var sale_schedule_set = false; + var $wrap = $these_sale_dates.closest( 'div, table' ); + + $these_sale_dates.find( 'input' ).each( function() { + if ( '' !== $( this ).val() ) { + sale_schedule_set = true; + } + }); + + if ( sale_schedule_set ) { + $wrap.find( '.sale_schedule' ).hide(); + $wrap.find( '.sale_price_dates_fields' ).show(); + } else { + $wrap.find( '.sale_schedule' ).show(); + $wrap.find( '.sale_price_dates_fields' ).hide(); + } + }); + + $( '#woocommerce-product-data' ).on( 'click', '.sale_schedule', function() { + var $wrap = $( this ).closest( 'div, table' ); + + $( this ).hide(); + $wrap.find( '.cancel_sale_schedule' ).show(); + $wrap.find( '.sale_price_dates_fields' ).show(); + + return false; + }); + $( '#woocommerce-product-data' ).on( 'click', '.cancel_sale_schedule', function() { + var $wrap = $( this ).closest( 'div, table' ); + + $( this ).hide(); + $wrap.find( '.sale_schedule' ).show(); + $wrap.find( '.sale_price_dates_fields' ).hide(); + $wrap.find( '.sale_price_dates_fields' ).find( 'input' ).val(''); + + return false; + }); + + // File inputs. + $( '#woocommerce-product-data' ).on( 'click','.downloadable_files a.insert', function() { + $( this ).closest( '.downloadable_files' ).find( 'tbody' ).append( $( this ).data( 'row' ) ); + return false; + }); + $( '#woocommerce-product-data' ).on( 'click','.downloadable_files a.delete',function() { + $( this ).closest( 'tr' ).remove(); + return false; + }); + + // Stock options. + $( 'input#_manage_stock' ).on( 'change', function() { + if ( $( this ).is( ':checked' ) ) { + $( 'div.stock_fields' ).show(); + $( 'p.stock_status_field' ).hide(); + } else { + var product_type = $( 'select#product-type' ).val(); + + $( 'div.stock_fields' ).hide(); + $( 'p.stock_status_field:not( .hide_if_' + product_type + ' )' ).show(); + } + + $( 'input.variable_manage_stock' ).trigger( 'change' ); + }).trigger( 'change' ); + + // Date picker fields. + function date_picker_select( datepicker ) { + var option = $( datepicker ).next().is( '.hasDatepicker' ) ? 'minDate' : 'maxDate', + otherDateField = 'minDate' === option ? $( datepicker ).next() : $( datepicker ).prev(), + date = $( datepicker ).datepicker( 'getDate' ); + + $( otherDateField ).datepicker( 'option', option, date ); + $( datepicker ).trigger( 'change' ); + } + + $( '.sale_price_dates_fields' ).each( function() { + $( this ).find( 'input' ).datepicker({ + defaultDate: '', + dateFormat: 'yy-mm-dd', + numberOfMonths: 1, + showButtonPanel: true, + onSelect: function() { + date_picker_select( $( this ) ); + } + }); + $( this ).find( 'input' ).each( function() { date_picker_select( $( this ) ); } ); + }); + + // Attribute Tables. + + // Initial order. + var woocommerce_attribute_items = $( '.product_attributes' ).find( '.woocommerce_attribute' ).get(); + + woocommerce_attribute_items.sort( function( a, b ) { + var compA = parseInt( $( a ).attr( 'rel' ), 10 ); + var compB = parseInt( $( b ).attr( 'rel' ), 10 ); + return ( compA < compB ) ? -1 : ( compA > compB ) ? 1 : 0; + }); + $( woocommerce_attribute_items ).each( function( index, el ) { + $( '.product_attributes' ).append( el ); + }); + + function attribute_row_indexes() { + $( '.product_attributes .woocommerce_attribute' ).each( function( index, el ) { + $( '.attribute_position', el ).val( parseInt( $( el ).index( '.product_attributes .woocommerce_attribute' ), 10 ) ); + }); + } + + $( '.product_attributes .woocommerce_attribute' ).each( function( index, el ) { + if ( $( el ).css( 'display' ) !== 'none' && $( el ).is( '.taxonomy' ) ) { + $( 'select.attribute_taxonomy' ).find( 'option[value="' + $( el ).data( 'taxonomy' ) + '"]' ).attr( 'disabled', 'disabled' ); + } + }); + + // Add rows. + $( 'button.add_attribute' ).on( 'click', function() { + var size = $( '.product_attributes .woocommerce_attribute' ).length; + var attribute = $( 'select.attribute_taxonomy' ).val(); + var $wrapper = $( this ).closest( '#product_attributes' ); + var $attributes = $wrapper.find( '.product_attributes' ); + var product_type = $( 'select#product-type' ).val(); + var data = { + action: 'woocommerce_add_attribute', + taxonomy: attribute, + i: size, + security: woocommerce_admin_meta_boxes.add_attribute_nonce + }; + + $wrapper.block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + + $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { + $attributes.append( response ); + + if ( 'variable' !== product_type ) { + $attributes.find( '.enable_variation' ).hide(); + } + + $( document.body ).trigger( 'wc-enhanced-select-init' ); + + attribute_row_indexes(); + + $attributes.find( '.woocommerce_attribute' ).last().find( 'h3' ).trigger( 'click' ); + + $wrapper.unblock(); + + $( document.body ).trigger( 'woocommerce_added_attribute' ); + }); + + if ( attribute ) { + $( 'select.attribute_taxonomy' ).find( 'option[value="' + attribute + '"]' ).attr( 'disabled','disabled' ); + $( 'select.attribute_taxonomy' ).val( '' ); + } + + return false; + }); + + $( '.product_attributes' ).on( 'blur', 'input.attribute_name', function() { + $( this ).closest( '.woocommerce_attribute' ).find( 'strong.attribute_name' ).text( $( this ).val() ); + }); + + $( '.product_attributes' ).on( 'click', 'button.select_all_attributes', function() { + $( this ).closest( 'td' ).find( 'select option' ).prop( 'selected', 'selected' ); + $( this ).closest( 'td' ).find( 'select' ).trigger( 'change' ); + return false; + }); + + $( '.product_attributes' ).on( 'click', 'button.select_no_attributes', function() { + $( this ).closest( 'td' ).find( 'select option' ).prop( 'selected', false ); + $( this ).closest( 'td' ).find( 'select' ).trigger( 'change' ); + return false; + }); + + $( '.product_attributes' ).on( 'click', '.remove_row', function() { + if ( window.confirm( woocommerce_admin_meta_boxes.remove_attribute ) ) { + var $parent = $( this ).parent().parent(); + + if ( $parent.is( '.taxonomy' ) ) { + $parent.find( 'select, input[type=text]' ).val( '' ); + $parent.hide(); + $( 'select.attribute_taxonomy' ).find( 'option[value="' + $parent.data( 'taxonomy' ) + '"]' ).prop( 'disabled', false ); + } else { + $parent.find( 'select, input[type=text]' ).val( '' ); + $parent.hide(); + attribute_row_indexes(); + } + } + return false; + }); + + // Attribute ordering. + $( '.product_attributes' ).sortable({ + items: '.woocommerce_attribute', + cursor: 'move', + axis: 'y', + handle: 'h3', + scrollSensitivity: 40, + forcePlaceholderSize: true, + helper: 'clone', + opacity: 0.65, + placeholder: 'wc-metabox-sortable-placeholder', + start: function( event, ui ) { + ui.item.css( 'background-color', '#f6f6f6' ); + }, + stop: function( event, ui ) { + ui.item.removeAttr( 'style' ); + attribute_row_indexes(); + } + }); + + // Add a new attribute (via ajax). + $( '.product_attributes' ).on( 'click', 'button.add_new_attribute', function() { + + $( '.product_attributes' ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + + var $wrapper = $( this ).closest( '.woocommerce_attribute' ); + var attribute = $wrapper.data( 'taxonomy' ); + var new_attribute_name = window.prompt( woocommerce_admin_meta_boxes.new_attribute_prompt ); + + if ( new_attribute_name ) { + + var data = { + action: 'woocommerce_add_new_attribute', + taxonomy: attribute, + term: new_attribute_name, + security: woocommerce_admin_meta_boxes.add_attribute_nonce + }; + + $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { + + if ( response.error ) { + // Error. + window.alert( response.error ); + } else if ( response.slug ) { + // Success. + $wrapper.find( 'select.attribute_values' ) + .append( '' ); + $wrapper.find( 'select.attribute_values' ).trigger( 'change' ); + } + + $( '.product_attributes' ).unblock(); + }); + + } else { + $( '.product_attributes' ).unblock(); + } + + return false; + }); + + // Save attributes and update variations. + $( '.save_attributes' ).on( 'click', function() { + + $( '.product_attributes' ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + var original_data = $( '.product_attributes' ).find( 'input, select, textarea' ); + var data = { + post_id : woocommerce_admin_meta_boxes.post_id, + product_type: $( '#product-type' ).val(), + data : original_data.serialize(), + action : 'woocommerce_save_attributes', + security : woocommerce_admin_meta_boxes.save_attributes_nonce + }; + + $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { + if ( response.error ) { + // Error. + window.alert( response.error ); + } else if ( response.data ) { + // Success. + $( '.product_attributes' ).html( response.data.html ); + $( '.product_attributes' ).unblock(); + + // Hide the 'Used for variations' checkbox if not viewing a variable product + show_and_hide_panels(); + + // Make sure the dropdown is not disabled for empty value attributes. + $( 'select.attribute_taxonomy' ).find( 'option' ).prop( 'disabled', false ); + + $( '.product_attributes .woocommerce_attribute' ).each( function( index, el ) { + if ( $( el ).css( 'display' ) !== 'none' && $( el ).is( '.taxonomy' ) ) { + $( 'select.attribute_taxonomy' ) + .find( 'option[value="' + $( el ).data( 'taxonomy' ) + '"]' ) + .prop( 'disabled', true ); + } + }); + + // Reload variations panel. + var this_page = window.location.toString(); + this_page = this_page.replace( 'post-new.php?', 'post.php?post=' + woocommerce_admin_meta_boxes.post_id + '&action=edit&' ); + + $( '#variable_product_options' ).load( this_page + ' #variable_product_options_inner', function() { + $( '#variable_product_options' ).trigger( 'reload' ); + } ); + } + }); + }); + + // Uploading files. + var downloadable_file_frame; + var file_path_field; + + $( document.body ).on( 'click', '.upload_file_button', function( event ) { + var $el = $( this ); + + file_path_field = $el.closest( 'tr' ).find( 'td.file_url input' ); + + event.preventDefault(); + + // If the media frame already exists, reopen it. + if ( downloadable_file_frame ) { + downloadable_file_frame.open(); + return; + } + + var downloadable_file_states = [ + // Main states. + new wp.media.controller.Library({ + library: wp.media.query(), + multiple: true, + title: $el.data('choose'), + priority: 20, + filterable: 'uploaded' + }) + ]; + + // Create the media frame. + downloadable_file_frame = wp.media.frames.downloadable_file = wp.media({ + // Set the title of the modal. + title: $el.data('choose'), + library: { + type: '' + }, + button: { + text: $el.data('update') + }, + multiple: true, + states: downloadable_file_states + }); + + // When an image is selected, run a callback. + downloadable_file_frame.on( 'select', function() { + var file_path = ''; + var selection = downloadable_file_frame.state().get( 'selection' ); + + selection.map( function( attachment ) { + attachment = attachment.toJSON(); + if ( attachment.url ) { + file_path = attachment.url; + } + }); + + file_path_field.val( file_path ).trigger( 'change' ); + }); + + // Set post to 0 and set our custom type. + downloadable_file_frame.on( 'ready', function() { + downloadable_file_frame.uploader.options.uploader.params = { + type: 'downloadable_product' + }; + }); + + // Finally, open the modal. + downloadable_file_frame.open(); + }); + + // Download ordering. + $( '.downloadable_files tbody' ).sortable({ + items: 'tr', + cursor: 'move', + axis: 'y', + handle: 'td.sort', + scrollSensitivity: 40, + forcePlaceholderSize: true, + helper: 'clone', + opacity: 0.65 + }); + + // Product gallery file uploads. + var product_gallery_frame; + var $image_gallery_ids = $( '#product_image_gallery' ); + var $product_images = $( '#product_images_container' ).find( 'ul.product_images' ); + + $( '.add_product_images' ).on( 'click', 'a', function( event ) { + var $el = $( this ); + + event.preventDefault(); + + // If the media frame already exists, reopen it. + if ( product_gallery_frame ) { + product_gallery_frame.open(); + return; + } + + // Create the media frame. + product_gallery_frame = wp.media.frames.product_gallery = wp.media({ + // Set the title of the modal. + title: $el.data( 'choose' ), + button: { + text: $el.data( 'update' ) + }, + states: [ + new wp.media.controller.Library({ + title: $el.data( 'choose' ), + filterable: 'all', + multiple: true + }) + ] + }); + + // When an image is selected, run a callback. + product_gallery_frame.on( 'select', function() { + var selection = product_gallery_frame.state().get( 'selection' ); + var attachment_ids = $image_gallery_ids.val(); + + selection.map( function( attachment ) { + attachment = attachment.toJSON(); + + if ( attachment.id ) { + attachment_ids = attachment_ids ? attachment_ids + ',' + attachment.id : attachment.id; + var attachment_image = attachment.sizes && attachment.sizes.thumbnail ? attachment.sizes.thumbnail.url : attachment.url; + + $product_images.append( + '
  • ' + ); + } + }); + + $image_gallery_ids.val( attachment_ids ); + }); + + // Finally, open the modal. + product_gallery_frame.open(); + }); + + // Image ordering. + $product_images.sortable({ + items: 'li.image', + cursor: 'move', + scrollSensitivity: 40, + forcePlaceholderSize: true, + forceHelperSize: false, + helper: 'clone', + opacity: 0.65, + placeholder: 'wc-metabox-sortable-placeholder', + start: function( event, ui ) { + ui.item.css( 'background-color', '#f6f6f6' ); + }, + stop: function( event, ui ) { + ui.item.removeAttr( 'style' ); + }, + update: function() { + var attachment_ids = ''; + + $( '#product_images_container' ).find( 'ul li.image' ).css( 'cursor', 'default' ).each( function() { + var attachment_id = $( this ).attr( 'data-attachment_id' ); + attachment_ids = attachment_ids + attachment_id + ','; + }); + + $image_gallery_ids.val( attachment_ids ); + } + }); + + // Remove images. + $( '#product_images_container' ).on( 'click', 'a.delete', function() { + $( this ).closest( 'li.image' ).remove(); + + var attachment_ids = ''; + + $( '#product_images_container' ).find( 'ul li.image' ).css( 'cursor', 'default' ).each( function() { + var attachment_id = $( this ).attr( 'data-attachment_id' ); + attachment_ids = attachment_ids + attachment_id + ','; + }); + + $image_gallery_ids.val( attachment_ids ); + + // Remove any lingering tooltips. + $( '#tiptip_holder' ).removeAttr( 'style' ); + $( '#tiptip_arrow' ).removeAttr( 'style' ); + + return false; + }); +}); diff --git a/plugins/woocommerce/legacy/js/admin/meta-boxes.js b/plugins/woocommerce/legacy/js/admin/meta-boxes.js new file mode 100644 index 00000000000..a815b38315c --- /dev/null +++ b/plugins/woocommerce/legacy/js/admin/meta-boxes.js @@ -0,0 +1,80 @@ +jQuery( function ( $ ) { + + // Run tipTip + function runTipTip() { + // Remove any lingering tooltips + $( '#tiptip_holder' ).removeAttr( 'style' ); + $( '#tiptip_arrow' ).removeAttr( 'style' ); + $( '.tips' ).tipTip({ + 'attribute': 'data-tip', + 'fadeIn': 50, + 'fadeOut': 50, + 'delay': 200, + 'keepAlive': true + }); + } + + runTipTip(); + + $( '.wc-metaboxes-wrapper' ).on( 'click', '.wc-metabox > h3', function() { + var metabox = $( this ).parent( '.wc-metabox' ); + + if ( metabox.hasClass( 'closed' ) ) { + metabox.removeClass( 'closed' ); + } else { + metabox.addClass( 'closed' ); + } + + if ( metabox.hasClass( 'open' ) ) { + metabox.removeClass( 'open' ); + } else { + metabox.addClass( 'open' ); + } + }); + + // Tabbed Panels + $( document.body ).on( 'wc-init-tabbed-panels', function() { + $( 'ul.wc-tabs' ).show(); + $( 'ul.wc-tabs a' ).on( 'click', function( e ) { + e.preventDefault(); + var panel_wrap = $( this ).closest( 'div.panel-wrap' ); + $( 'ul.wc-tabs li', panel_wrap ).removeClass( 'active' ); + $( this ).parent().addClass( 'active' ); + $( 'div.panel', panel_wrap ).hide(); + $( $( this ).attr( 'href' ) ).show(); + }); + $( 'div.panel-wrap' ).each( function() { + $( this ).find( 'ul.wc-tabs li' ).eq( 0 ).find( 'a' ).trigger( 'click' ); + }); + }).trigger( 'wc-init-tabbed-panels' ); + + // Date Picker + $( document.body ).on( 'wc-init-datepickers', function() { + $( '.date-picker-field, .date-picker' ).datepicker({ + dateFormat: 'yy-mm-dd', + numberOfMonths: 1, + showButtonPanel: true + }); + }).trigger( 'wc-init-datepickers' ); + + // Meta-Boxes - Open/close + $( '.wc-metaboxes-wrapper' ).on( 'click', '.wc-metabox h3', function( event ) { + // If the user clicks on some form input inside the h3, like a select list (for variations), the box should not be toggled + if ( $( event.target ).filter( ':input, option, .sort' ).length ) { + return; + } + + $( this ).next( '.wc-metabox-content' ).stop().slideToggle(); + }) + .on( 'click', '.expand_all', function() { + $( this ).closest( '.wc-metaboxes-wrapper' ).find( '.wc-metabox > .wc-metabox-content' ).show(); + return false; + }) + .on( 'click', '.close_all', function() { + $( this ).closest( '.wc-metaboxes-wrapper' ).find( '.wc-metabox > .wc-metabox-content' ).hide(); + return false; + }); + $( '.wc-metabox.closed' ).each( function() { + $( this ).find( '.wc-metabox-content' ).hide(); + }); +}); diff --git a/assets/js/admin/network-orders.js b/plugins/woocommerce/legacy/js/admin/network-orders.js similarity index 100% rename from assets/js/admin/network-orders.js rename to plugins/woocommerce/legacy/js/admin/network-orders.js diff --git a/assets/js/admin/product-ordering.js b/plugins/woocommerce/legacy/js/admin/product-ordering.js similarity index 100% rename from assets/js/admin/product-ordering.js rename to plugins/woocommerce/legacy/js/admin/product-ordering.js diff --git a/plugins/woocommerce/legacy/js/admin/quick-edit.js b/plugins/woocommerce/legacy/js/admin/quick-edit.js new file mode 100644 index 00000000000..8e2222fede0 --- /dev/null +++ b/plugins/woocommerce/legacy/js/admin/quick-edit.js @@ -0,0 +1,167 @@ +/*global inlineEditPost, woocommerce_admin, woocommerce_quick_edit */ +jQuery( + function( $ ) { + $( '#the-list' ).on( + 'click', + '.editinline', + function() { + + inlineEditPost.revert(); + + var post_id = $( this ).closest( 'tr' ).attr( 'id' ); + + post_id = post_id.replace( 'post-', '' ); + + var $wc_inline_data = $( '#woocommerce_inline_' + post_id ); + + var sku = $wc_inline_data.find( '.sku' ).text(), + regular_price = $wc_inline_data.find( '.regular_price' ).text(), + sale_price = $wc_inline_data.find( '.sale_price ' ).text(), + weight = $wc_inline_data.find( '.weight' ).text(), + length = $wc_inline_data.find( '.length' ).text(), + width = $wc_inline_data.find( '.width' ).text(), + height = $wc_inline_data.find( '.height' ).text(), + shipping_class = $wc_inline_data.find( '.shipping_class' ).text(), + visibility = $wc_inline_data.find( '.visibility' ).text(), + stock_status = $wc_inline_data.find( '.stock_status' ).text(), + stock = $wc_inline_data.find( '.stock' ).text(), + featured = $wc_inline_data.find( '.featured' ).text(), + manage_stock = $wc_inline_data.find( '.manage_stock' ).text(), + menu_order = $wc_inline_data.find( '.menu_order' ).text(), + tax_status = $wc_inline_data.find( '.tax_status' ).text(), + tax_class = $wc_inline_data.find( '.tax_class' ).text(), + backorders = $wc_inline_data.find( '.backorders' ).text(), + product_type = $wc_inline_data.find( '.product_type' ).text(); + + var formatted_regular_price = regular_price.replace( '.', woocommerce_admin.mon_decimal_point ), + formatted_sale_price = sale_price.replace( '.', woocommerce_admin.mon_decimal_point ); + + $( 'input[name="_sku"]', '.inline-edit-row' ).val( sku ); + $( 'input[name="_regular_price"]', '.inline-edit-row' ).val( formatted_regular_price ); + $( 'input[name="_sale_price"]', '.inline-edit-row' ).val( formatted_sale_price ); + $( 'input[name="_weight"]', '.inline-edit-row' ).val( weight ); + $( 'input[name="_length"]', '.inline-edit-row' ).val( length ); + $( 'input[name="_width"]', '.inline-edit-row' ).val( width ); + $( 'input[name="_height"]', '.inline-edit-row' ).val( height ); + + $( 'select[name="_shipping_class"] option:selected', '.inline-edit-row' ).attr( 'selected', false ).trigger( 'change' ); + $( 'select[name="_shipping_class"] option[value="' + shipping_class + '"]' ).attr( 'selected', 'selected' ) + .trigger( 'change' ); + + $( 'input[name="_stock"]', '.inline-edit-row' ).val( stock ); + $( 'input[name="menu_order"]', '.inline-edit-row' ).val( menu_order ); + + $( + 'select[name="_tax_status"] option, ' + + 'select[name="_tax_class"] option, ' + + 'select[name="_visibility"] option, ' + + 'select[name="_stock_status"] option, ' + + 'select[name="_backorders"] option' + ).prop( 'selected', false ).removeAttr( 'selected' ); + + var is_variable_product = 'variable' === product_type; + $( 'select[name="_stock_status"] ~ .wc-quick-edit-warning', '.inline-edit-row' ).toggle( is_variable_product ); + $( 'select[name="_stock_status"] option[value="' + (is_variable_product ? '' : stock_status) + '"]', '.inline-edit-row' ) + .attr( 'selected', 'selected' ); + + $( 'select[name="_tax_status"] option[value="' + tax_status + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' ); + $( 'select[name="_tax_class"] option[value="' + tax_class + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' ); + $( 'select[name="_visibility"] option[value="' + visibility + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' ); + $( 'select[name="_backorders"] option[value="' + backorders + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' ); + + if ( 'yes' === featured ) { + $( 'input[name="_featured"]', '.inline-edit-row' ).prop( 'checked', true ); + } else { + $( 'input[name="_featured"]', '.inline-edit-row' ).prop( 'checked', false ); + } + + // Conditional display. + var product_is_virtual = $wc_inline_data.find( '.product_is_virtual' ).text(); + + var product_supports_stock_status = 'external' !== product_type; + var product_supports_stock_fields = 'external' !== product_type && 'grouped' !== product_type; + + $( '.stock_fields, .manage_stock_field, .stock_status_field, .backorder_field' ).show(); + + if ( product_supports_stock_fields ) { + if ( 'yes' === manage_stock ) { + $( '.stock_qty_field, .backorder_field', '.inline-edit-row' ).show().removeAttr( 'style' ); + $( '.stock_status_field' ).hide(); + $( '.manage_stock_field input' ).prop( 'checked', true ); + } else { + $( '.stock_qty_field, .backorder_field', '.inline-edit-row' ).hide(); + $( '.stock_status_field' ).show().removeAttr( 'style' ); + $( '.manage_stock_field input' ).prop( 'checked', false ); + } + } else if ( product_supports_stock_status ) { + $( '.stock_fields, .manage_stock_field, .backorder_field' ).hide(); + } else { + $( '.stock_fields, .manage_stock_field, .stock_status_field, .backorder_field' ).hide(); + } + + if ( 'simple' === product_type || 'external' === product_type ) { + $( '.price_fields', '.inline-edit-row' ).show().removeAttr( 'style' ); + } else { + $( '.price_fields', '.inline-edit-row' ).hide(); + } + + if ( 'yes' === product_is_virtual ) { + $( '.dimension_fields', '.inline-edit-row' ).hide(); + } else { + $( '.dimension_fields', '.inline-edit-row' ).show().removeAttr( 'style' ); + } + + // Rename core strings. + $( 'input[name="comment_status"]' ).parent().find( '.checkbox-title' ).text( woocommerce_quick_edit.strings.allow_reviews ); + } + ); + + $( '#the-list' ).on( + 'change', + '.inline-edit-row input[name="_manage_stock"]', + function() { + + if ( $( this ).is( ':checked' ) ) { + $( '.stock_qty_field, .backorder_field', '.inline-edit-row' ).show().removeAttr( 'style' ); + $( '.stock_status_field' ).hide(); + } else { + $( '.stock_qty_field, .backorder_field', '.inline-edit-row' ).hide(); + $( '.stock_status_field' ).show().removeAttr( 'style' ); + } + + } + ); + + $( '#wpbody' ).on( + 'click', + '#doaction, #doaction2', + function() { + $( 'input.text', '.inline-edit-row' ).val( '' ); + $( '#woocommerce-fields' ).find( 'select' ).prop( 'selectedIndex', 0 ); + $( '#woocommerce-fields-bulk' ).find( '.inline-edit-group .change-input' ).hide(); + } + ); + + $( '#wpbody' ).on( + 'change', + '#woocommerce-fields-bulk .inline-edit-group .change_to', + function() { + + if ( 0 < $( this ).val() ) { + $( this ).closest( 'div' ).find( '.change-input' ).show(); + } else { + $( this ).closest( 'div' ).find( '.change-input' ).hide(); + } + + } + ); + + $( '#wpbody' ).on( + 'click', + '.trash-product', + function() { + return window.confirm( woocommerce_admin.i18n_delete_product_notice ); + } + ); + } +); diff --git a/assets/js/admin/reports.js b/plugins/woocommerce/legacy/js/admin/reports.js similarity index 100% rename from assets/js/admin/reports.js rename to plugins/woocommerce/legacy/js/admin/reports.js diff --git a/plugins/woocommerce/legacy/js/admin/settings-views-html-settings-tax.js b/plugins/woocommerce/legacy/js/admin/settings-views-html-settings-tax.js new file mode 100644 index 00000000000..f7c79fe01c1 --- /dev/null +++ b/plugins/woocommerce/legacy/js/admin/settings-views-html-settings-tax.js @@ -0,0 +1,383 @@ +/* global htmlSettingsTaxLocalizeScript, ajaxurl */ + +/** + * Used by woocommerce/includes/admin/settings/views/html-settings-tax.php + */ +( function( $, data, wp, ajaxurl ) { + $( function() { + + if ( ! String.prototype.trim ) { + String.prototype.trim = function () { + return this.replace( /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '' ); + }; + } + + var rowTemplate = wp.template( 'wc-tax-table-row' ), + rowTemplateEmpty = wp.template( 'wc-tax-table-row-empty' ), + paginationTemplate = wp.template( 'wc-tax-table-pagination' ), + $table = $( '.wc_tax_rates' ), + $tbody = $( '#rates' ), + $save_button = $( ':input[name="save"]' ), + $pagination = $( '#rates-pagination' ), + $search_field = $( '#rates-search .wc-tax-rates-search-field' ), + $submit = $( '.submit .button-primary[type=submit]' ), + WCTaxTableModelConstructor = Backbone.Model.extend({ + changes: {}, + setRateAttribute: function( rateID, attribute, value ) { + var rates = _.indexBy( this.get( 'rates' ), 'tax_rate_id' ), + changes = {}; + + if ( rates[ rateID ][ attribute ] !== value ) { + changes[ rateID ] = {}; + changes[ rateID ][ attribute ] = value; + rates[ rateID ][ attribute ] = value; + } + + this.logChanges( changes ); + }, + logChanges: function( changedRows ) { + var changes = this.changes || {}; + + _.each( changedRows, function( row, id ) { + changes[ id ] = _.extend( changes[ id ] || { + tax_rate_id : id + }, row ); + } ); + + this.changes = changes; + this.trigger( 'change:rates' ); + }, + getFilteredRates: function() { + var rates = this.get( 'rates' ), + search = $search_field.val().toLowerCase(); + + if ( search.length ) { + rates = _.filter( rates, function( rate ) { + var search_text = _.toArray( rate ).join( ' ' ).toLowerCase(); + return ( -1 !== search_text.indexOf( search ) ); + } ); + } + + rates = _.sortBy( rates, function( rate ) { + return parseInt( rate.tax_rate_order, 10 ); + } ); + + return rates; + }, + block: function() { + $( '.wc_tax_rates' ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + }, + unblock: function() { + $( '.wc_tax_rates' ).unblock(); + }, + save: function() { + var self = this; + + self.block(); + + Backbone.ajax({ + method: 'POST', + dataType: 'json', + url: ajaxurl + ( ajaxurl.indexOf( '?' ) > 0 ? '&' : '?' ) + 'action=woocommerce_tax_rates_save_changes', + data: { + current_class: data.current_class, + wc_tax_nonce: data.wc_tax_nonce, + changes: self.changes + }, + success: function( response, textStatus ) { + if ( 'success' === textStatus && response.success ) { + WCTaxTableModelInstance.set( 'rates', response.data.rates ); + WCTaxTableModelInstance.trigger( 'change:rates' ); + + WCTaxTableModelInstance.changes = {}; + WCTaxTableModelInstance.trigger( 'saved:rates' ); + + // Reload view. + WCTaxTableInstance.render(); + } + + self.unblock(); + } + }); + } + } ), + WCTaxTableViewConstructor = Backbone.View.extend({ + rowTemplate: rowTemplate, + per_page: data.limit, + page: data.page, + initialize: function() { + var qty_pages = Math.ceil( _.toArray( this.model.get( 'rates' ) ).length / this.per_page ); + + this.qty_pages = 0 === qty_pages ? 1 : qty_pages; + this.page = this.sanitizePage( data.page ); + + this.listenTo( this.model, 'change:rates', this.setUnloadConfirmation ); + this.listenTo( this.model, 'saved:rates', this.clearUnloadConfirmation ); + $tbody.on( 'change autocompletechange', ':input', { view: this }, this.updateModelOnChange ); + $search_field.on( 'keyup search', { view: this }, this.onSearchField ); + $pagination.on( 'click', 'a', { view: this }, this.onPageChange ); + $pagination.on( 'change', 'input', { view: this }, this.onPageChange ); + $( window ).on( 'beforeunload', { view: this }, this.unloadConfirmation ); + $submit.on( 'click', { view: this }, this.onSubmit ); + $save_button.prop( 'disabled', true ); + + // Can bind these directly to the buttons, as they won't get overwritten. + $table.find( '.insert' ).on( 'click', { view: this }, this.onAddNewRow ); + $table.find( '.remove_tax_rates' ).on( 'click', { view: this }, this.onDeleteRow ); + $table.find( '.export' ).on( 'click', { view: this }, this.onExport ); + }, + render: function() { + var rates = this.model.getFilteredRates(), + qty_rates = _.size( rates ), + qty_pages = Math.ceil( qty_rates / this.per_page ), + first_index = 0 === qty_rates ? 0 : this.per_page * ( this.page - 1 ), + last_index = this.per_page * this.page, + paged_rates = _.toArray( rates ).slice( first_index, last_index ), + view = this; + + // Blank out the contents. + this.$el.empty(); + + if ( paged_rates.length ) { + // Populate $tbody with the current page of results. + $.each( paged_rates, function( id, rowData ) { + view.$el.append( view.rowTemplate( rowData ) ); + } ); + } else { + view.$el.append( rowTemplateEmpty() ); + } + + // Initialize autocomplete for countries. + this.$el.find( 'td.country input' ).autocomplete({ + source: data.countries, + minLength: 2 + }); + + // Initialize autocomplete for states. + this.$el.find( 'td.state input' ).autocomplete({ + source: data.states, + minLength: 3 + }); + + // Postcode and city don't have `name` values by default. + // They're only created if the contents changes, to save on database queries (I think) + this.$el.find( 'td.postcode input, td.city input' ).on( 'change', function() { + $( this ).attr( 'name', $( this ).data( 'name' ) ); + }); + + if ( qty_pages > 1 ) { + // We've now displayed our initial page, time to render the pagination box. + $pagination.html( paginationTemplate( { + qty_rates: qty_rates, + current_page: this.page, + qty_pages: qty_pages + } ) ); + } else { + $pagination.empty(); + view.page = 1; + } + }, + updateUrl: function() { + if ( ! window.history.replaceState ) { + return; + } + + var url = data.base_url, + search = $search_field.val(); + + if ( 1 < this.page ) { + url += '&p=' + encodeURIComponent( this.page ); + } + + if ( search.length ) { + url += '&s=' + encodeURIComponent( search ); + } + + window.history.replaceState( {}, '', url ); + }, + onSubmit: function( event ) { + event.data.view.model.save(); + event.preventDefault(); + }, + onAddNewRow: function( event ) { + var view = event.data.view, + model = view.model, + rates = _.indexBy( model.get( 'rates' ), 'tax_rate_id' ), + changes = {}, + size = _.size( rates ), + newRow = _.extend( {}, data.default_rate, { + tax_rate_id: 'new-' + size + '-' + Date.now(), + newRow: true + } ), + $current, current_id, current_order, rates_to_reorder, reordered_rates; + + $current = $tbody.children( '.current' ); + + if ( $current.length ) { + current_id = $current.last().data( 'id' ); + current_order = parseInt( rates[ current_id ].tax_rate_order, 10 ); + newRow.tax_rate_order = 1 + current_order; + + rates_to_reorder = _.filter( rates, function( rate ) { + if ( parseInt( rate.tax_rate_order, 10 ) > current_order ) { + return true; + } + return false; + } ); + + reordered_rates = _.map( rates_to_reorder, function( rate ) { + rate.tax_rate_order++; + changes[ rate.tax_rate_id ] = _.extend( + changes[ rate.tax_rate_id ] || {}, { tax_rate_order : rate.tax_rate_order } + ); + return rate; + } ); + } else { + newRow.tax_rate_order = 1 + _.max( + _.pluck( rates, 'tax_rate_order' ), + function ( val ) { + // Cast them all to integers, because strings compare funky. Sighhh. + return parseInt( val, 10 ); + } + ); + // Move the last page + view.page = view.qty_pages; + } + + rates[ newRow.tax_rate_id ] = newRow; + changes[ newRow.tax_rate_id ] = newRow; + + model.set( 'rates', rates ); + model.logChanges( changes ); + + view.render(); + }, + onDeleteRow: function( event ) { + var view = event.data.view, + model = view.model, + rates = _.indexBy( model.get( 'rates' ), 'tax_rate_id' ), + changes = {}, + $current, current_id; + + event.preventDefault(); + + if ( $current = $tbody.children( '.current' ) ) { + $current.each(function(){ + current_id = $( this ).data('id'); + + delete rates[ current_id ]; + + changes[ current_id ] = _.extend( changes[ current_id ] || {}, { deleted : 'deleted' } ); + }); + + model.set( 'rates', rates ); + model.logChanges( changes ); + + view.render(); + } else { + window.alert( data.strings.no_rows_selected ); + } + }, + onSearchField: function( event ){ + event.data.view.updateUrl(); + event.data.view.render(); + }, + onPageChange: function( event ) { + var $target = $( event.currentTarget ); + + event.preventDefault(); + event.data.view.page = $target.data( 'goto' ) ? $target.data( 'goto' ) : $target.val(); + event.data.view.render(); + event.data.view.updateUrl(); + }, + onExport: function( event ) { + var csv_data = 'data:application/csv;charset=utf-8,' + data.strings.csv_data_cols.join(',') + '\n'; + + $.each( event.data.view.model.getFilteredRates(), function( id, rowData ) { + var row = ''; + + row += rowData.tax_rate_country + ','; + row += rowData.tax_rate_state + ','; + row += ( rowData.postcode ? rowData.postcode.join( '; ' ) : '' ) + ','; + row += ( rowData.city ? rowData.city.join( '; ' ) : '' ) + ','; + row += rowData.tax_rate + ','; + row += rowData.tax_rate_name + ','; + row += rowData.tax_rate_priority + ','; + row += rowData.tax_rate_compound + ','; + row += rowData.tax_rate_shipping + ','; + row += data.current_class; + + csv_data += row + '\n'; + }); + + $( this ).attr( 'href', encodeURI( csv_data ) ); + + return true; + }, + setUnloadConfirmation: function() { + this.needsUnloadConfirm = true; + $save_button.prop( 'disabled', false ); + }, + clearUnloadConfirmation: function() { + this.needsUnloadConfirm = false; + $save_button.prop( 'disabled', true ); + }, + unloadConfirmation: function( event ) { + if ( event.data.view.needsUnloadConfirm ) { + event.returnValue = data.strings.unload_confirmation_msg; + window.event.returnValue = data.strings.unload_confirmation_msg; + return data.strings.unload_confirmation_msg; + } + }, + updateModelOnChange: function( event ) { + var model = event.data.view.model, + $target = $( event.target ), + id = $target.closest( 'tr' ).data( 'id' ), + attribute = $target.data( 'attribute' ), + val = $target.val(); + + if ( 'city' === attribute || 'postcode' === attribute ) { + val = val.split( ';' ); + val = $.map( val, function( thing ) { + return thing.trim(); + }); + } + + if ( 'tax_rate_compound' === attribute || 'tax_rate_shipping' === attribute ) { + if ( $target.is( ':checked' ) ) { + val = 1; + } else { + val = 0; + } + } + + model.setRateAttribute( id, attribute, val ); + }, + sanitizePage: function( page_num ) { + page_num = parseInt( page_num, 10 ); + if ( page_num < 1 ) { + page_num = 1; + } else if ( page_num > this.qty_pages ) { + page_num = this.qty_pages; + } + return page_num; + } + } ), + WCTaxTableModelInstance = new WCTaxTableModelConstructor({ + rates: data.rates + } ), + WCTaxTableInstance = new WCTaxTableViewConstructor({ + model: WCTaxTableModelInstance, + el: '#rates' + } ); + + WCTaxTableInstance.render(); + + }); +})( jQuery, htmlSettingsTaxLocalizeScript, wp, ajaxurl ); diff --git a/plugins/woocommerce/legacy/js/admin/settings.js b/plugins/woocommerce/legacy/js/admin/settings.js new file mode 100644 index 00000000000..8464191f0e3 --- /dev/null +++ b/plugins/woocommerce/legacy/js/admin/settings.js @@ -0,0 +1,265 @@ +/* global woocommerce_settings_params, wp */ +( function ( $, params, wp ) { + $( function () { + // Sell Countries + $( 'select#woocommerce_allowed_countries' ) + .on( 'change', function () { + if ( 'specific' === $( this ).val() ) { + $( this ).closest( 'tr' ).next( 'tr' ).hide(); + $( this ).closest( 'tr' ).next().next( 'tr' ).show(); + } else if ( 'all_except' === $( this ).val() ) { + $( this ).closest( 'tr' ).next( 'tr' ).show(); + $( this ).closest( 'tr' ).next().next( 'tr' ).hide(); + } else { + $( this ).closest( 'tr' ).next( 'tr' ).hide(); + $( this ).closest( 'tr' ).next().next( 'tr' ).hide(); + } + } ) + .trigger( 'change' ); + + // Ship Countries + $( 'select#woocommerce_ship_to_countries' ) + .on( 'change', function () { + if ( 'specific' === $( this ).val() ) { + $( this ).closest( 'tr' ).next( 'tr' ).show(); + } else { + $( this ).closest( 'tr' ).next( 'tr' ).hide(); + } + } ) + .trigger( 'change' ); + + // Stock management + $( 'input#woocommerce_manage_stock' ) + .on( 'change', function () { + if ( $( this ).is( ':checked' ) ) { + $( this ) + .closest( 'tbody' ) + .find( '.manage_stock_field' ) + .closest( 'tr' ) + .show(); + } else { + $( this ) + .closest( 'tbody' ) + .find( '.manage_stock_field' ) + .closest( 'tr' ) + .hide(); + } + } ) + .trigger( 'change' ); + + // Color picker + $( '.colorpick' ) + .iris( { + change: function ( event, ui ) { + $( this ) + .parent() + .find( '.colorpickpreview' ) + .css( { backgroundColor: ui.color.toString() } ); + }, + hide: true, + border: true, + } ) + + .on( 'click focus', function ( event ) { + event.stopPropagation(); + $( '.iris-picker' ).hide(); + $( this ).closest( 'td' ).find( '.iris-picker' ).show(); + $( this ).data( 'originalValue', $( this ).val() ); + } ) + + .on( 'change', function () { + if ( $( this ).is( '.iris-error' ) ) { + var original_value = $( this ).data( 'originalValue' ); + + if ( + original_value.match( + /^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/ + ) + ) { + $( this ) + .val( $( this ).data( 'originalValue' ) ) + .trigger( 'change' ); + } else { + $( this ).val( '' ).trigger( 'change' ); + } + } + } ); + + $( 'body' ).on( 'click', function () { + $( '.iris-picker' ).hide(); + } ); + + // Edit prompt + $( function () { + var changed = false; + let $check_column = $( '.wp-list-table .check-column' ); + + $( 'input, textarea, select, checkbox' ).on( 'change', function ( + event + ) { + // Toggling WP List Table checkboxes should not trigger navigation warnings. + if ( + $check_column.length && + $check_column.has( event.target ) + ) { + return; + } + + if ( ! changed ) { + window.onbeforeunload = function () { + return params.i18n_nav_warning; + }; + changed = true; + } + } ); + + $( '.submit :input, input#search-submit' ).on( + 'click', + function () { + window.onbeforeunload = ''; + } + ); + } ); + + // Sorting + $( 'table.wc_gateways tbody, table.wc_shipping tbody' ).sortable( { + items: 'tr', + cursor: 'move', + axis: 'y', + handle: 'td.sort', + scrollSensitivity: 40, + helper: function ( event, ui ) { + ui.children().each( function () { + $( this ).width( $( this ).width() ); + } ); + ui.css( 'left', '0' ); + return ui; + }, + start: function ( event, ui ) { + ui.item.css( 'background-color', '#f6f6f6' ); + }, + stop: function ( event, ui ) { + ui.item.removeAttr( 'style' ); + ui.item.trigger( 'updateMoveButtons' ); + }, + } ); + + // Select all/none + $( '.woocommerce' ).on( 'click', '.select_all', function () { + $( this ) + .closest( 'td' ) + .find( 'select option' ) + .prop( 'selected', true ); + $( this ).closest( 'td' ).find( 'select' ).trigger( 'change' ); + return false; + } ); + + $( '.woocommerce' ).on( 'click', '.select_none', function () { + $( this ) + .closest( 'td' ) + .find( 'select option' ) + .prop( 'selected', false ); + $( this ).closest( 'td' ).find( 'select' ).trigger( 'change' ); + return false; + } ); + + // Re-order buttons. + $( '.wc-item-reorder-nav' ) + .find( '.wc-move-up, .wc-move-down' ) + .on( 'click', function () { + var moveBtn = $( this ), + $row = moveBtn.closest( 'tr' ); + + moveBtn.trigger( 'focus' ); + + var isMoveUp = moveBtn.is( '.wc-move-up' ), + isMoveDown = moveBtn.is( '.wc-move-down' ); + + if ( isMoveUp ) { + var $previewRow = $row.prev( 'tr' ); + + if ( $previewRow && $previewRow.length ) { + $previewRow.before( $row ); + wp.a11y.speak( params.i18n_moved_up ); + } + } else if ( isMoveDown ) { + var $nextRow = $row.next( 'tr' ); + + if ( $nextRow && $nextRow.length ) { + $nextRow.after( $row ); + wp.a11y.speak( params.i18n_moved_down ); + } + } + + moveBtn.trigger( 'focus' ); // Re-focus after the container was moved. + moveBtn.closest( 'table' ).trigger( 'updateMoveButtons' ); + } ); + + $( '.wc-item-reorder-nav' ) + .closest( 'table' ) + .on( 'updateMoveButtons', function () { + var table = $( this ), + lastRow = $( this ).find( 'tbody tr:last' ), + firstRow = $( this ).find( 'tbody tr:first' ); + + table + .find( '.wc-item-reorder-nav .wc-move-disabled' ) + .removeClass( 'wc-move-disabled' ) + .attr( { tabindex: '0', 'aria-hidden': 'false' } ); + firstRow + .find( '.wc-item-reorder-nav .wc-move-up' ) + .addClass( 'wc-move-disabled' ) + .attr( { tabindex: '-1', 'aria-hidden': 'true' } ); + lastRow + .find( '.wc-item-reorder-nav .wc-move-down' ) + .addClass( 'wc-move-disabled' ) + .attr( { tabindex: '-1', 'aria-hidden': 'true' } ); + } ); + + $( '.wc-item-reorder-nav' ) + .closest( 'table' ) + .trigger( 'updateMoveButtons' ); + + $( '.submit button' ).on( 'click', function () { + if ( + $( 'select#woocommerce_allowed_countries' ).val() === + 'specific' && + ! $( '[name="woocommerce_specific_allowed_countries[]"]' ).val() + ) { + if ( + window.confirm( + woocommerce_settings_params.i18n_no_specific_countries_selected + ) + ) { + return true; + } + return false; + } + } ); + + $( '#settings-other-payment-methods' ).on( 'click', function ( e ) { + if ( + typeof window.wcTracks.recordEvent === 'undefined' && + typeof window.wc.tracks.recordEvent === 'undefined' + ) { + return; + } + + var recordEvent = + window.wc.tracks.recordEvent || window.wcTracks.recordEvent; + + var payment_methods = $.map( + $( + 'td.wc_payment_gateways_wrapper tbody tr[data-gateway_id] ' + ), + function ( tr ) { + return $( tr ).attr( 'data-gateway_id' ); + } + ); + + recordEvent( 'settings_payments_recommendations_other_options', { + available_payment_methods: payment_methods, + } ); + } ); + } ); +} )( jQuery, woocommerce_settings_params, wp ); diff --git a/plugins/woocommerce/legacy/js/admin/system-status.js b/plugins/woocommerce/legacy/js/admin/system-status.js new file mode 100644 index 00000000000..a2457530a5a --- /dev/null +++ b/plugins/woocommerce/legacy/js/admin/system-status.js @@ -0,0 +1,126 @@ +/* global jQuery, woocommerce_admin_system_status, wcSetClipboard, wcClearClipboard */ +jQuery( function ( $ ) { + + /** + * Users country and state fields + */ + var wcSystemStatus = { + init: function() { + $( document.body ) + .on( 'click', 'a.help_tip, a.woocommerce-help-tip', this.preventTipTipClick ) + .on( 'click', 'a.debug-report', this.generateReport ) + .on( 'click', '#copy-for-support', this.copyReport ) + .on( 'aftercopy', '#copy-for-support', this.copySuccess ) + .on( 'aftercopyfailure', '#copy-for-support', this.copyFail ); + }, + + /** + * Prevent anchor behavior when click on TipTip. + * + * @return {Bool} + */ + preventTipTipClick: function() { + return false; + }, + + /** + * Generate system status report. + * + * @return {Bool} + */ + generateReport: function() { + var report = ''; + + $( '.wc_status_table thead, .wc_status_table tbody' ).each( function() { + if ( $( this ).is( 'thead' ) ) { + var label = $( this ).find( 'th:eq(0)' ).data( 'exportLabel' ) || $( this ).text(); + report = report + '\n### ' + label.trim() + ' ###\n\n'; + } else { + $( 'tr', $( this ) ).each( function() { + var label = $( this ).find( 'td:eq(0)' ).data( 'exportLabel' ) || $( this ).find( 'td:eq(0)' ).text(); + var the_name = label.trim().replace( /(<([^>]+)>)/ig, '' ); // Remove HTML. + + // Find value + var $value_html = $( this ).find( 'td:eq(2)' ).clone(); + $value_html.find( '.private' ).remove(); + $value_html.find( '.dashicons-yes' ).replaceWith( '✔' ); + $value_html.find( '.dashicons-no-alt, .dashicons-warning' ).replaceWith( '❌' ); + + // Format value + var the_value = $value_html.text().trim(); + var value_array = the_value.split( ', ' ); + + if ( value_array.length > 1 ) { + // If value have a list of plugins ','. + // Split to add new line. + var temp_line =''; + $.each( value_array, function( key, line ) { + temp_line = temp_line + line + '\n'; + }); + + the_value = temp_line; + } + + report = report + '' + the_name + ': ' + the_value + '\n'; + }); + } + }); + + try { + $( '#debug-report' ).slideDown(); + $( '#debug-report' ).find( 'textarea' ).val( '`' + report + '`' ).trigger( 'focus' ).trigger( 'select' ); + $( this ).fadeOut(); + return false; + } catch ( e ) { + /* jshint devel: true */ + console.log( e ); + } + + return false; + }, + + /** + * Copy for report. + * + * @param {Object} evt Copy event. + */ + copyReport: function( evt ) { + wcClearClipboard(); + wcSetClipboard( $( '#debug-report' ).find( 'textarea' ).val(), $( this ) ); + evt.preventDefault(); + }, + + /** + * Display a "Copied!" tip when success copying + */ + copySuccess: function() { + $( '#copy-for-support' ).tipTip({ + 'attribute': 'data-tip', + 'activation': 'focus', + 'fadeIn': 50, + 'fadeOut': 50, + 'delay': 0 + }).trigger( 'focus' ); + }, + + /** + * Displays the copy error message when failure copying. + */ + copyFail: function() { + $( '.copy-error' ).removeClass( 'hidden' ); + $( '#debug-report' ).find( 'textarea' ).trigger( 'focus' ).trigger( 'select' ); + } + }; + + wcSystemStatus.init(); + + $( '.wc_status_table' ).on( 'click', '.run-tool .button', function( evt ) { + evt.stopImmediatePropagation(); + return window.confirm( woocommerce_admin_system_status.run_tool_confirmation ); + }); + + $( '#log-viewer-select' ).on( 'click', 'h2 a.page-title-action', function( evt ) { + evt.stopImmediatePropagation(); + return window.confirm( woocommerce_admin_system_status.delete_log_confirmation ); + }); +}); diff --git a/assets/js/admin/term-ordering.js b/plugins/woocommerce/legacy/js/admin/term-ordering.js similarity index 100% rename from assets/js/admin/term-ordering.js rename to plugins/woocommerce/legacy/js/admin/term-ordering.js diff --git a/plugins/woocommerce/legacy/js/admin/users.js b/plugins/woocommerce/legacy/js/admin/users.js new file mode 100644 index 00000000000..2d6c1fa7138 --- /dev/null +++ b/plugins/woocommerce/legacy/js/admin/users.js @@ -0,0 +1,120 @@ +/*global wc_users_params */ +jQuery( function ( $ ) { + + /** + * Users country and state fields + */ + var wc_users_fields = { + states: null, + init: function() { + if ( typeof wc_users_params.countries !== 'undefined' ) { + /* State/Country select boxes */ + this.states = JSON.parse( wc_users_params.countries.replace( /"/g, '"' ) ); + } + + $( '.js_field-country' ).selectWoo().on( 'change', this.change_country ); + $( '.js_field-country' ).trigger( 'change', [ true ] ); + $( document.body ).on( 'change', 'select.js_field-state', this.change_state ); + + $( document.body ).on( 'click', 'button.js_copy-billing', this.copy_billing ); + }, + + change_country: function( e, stickValue ) { + // Check for stickValue before using it + if ( typeof stickValue === 'undefined' ) { + stickValue = false; + } + + // Prevent if we don't have the metabox data + if ( wc_users_fields.states === null ) { + return; + } + + var $this = $( this ), + country = $this.val(), + $state = $this.parents( '.form-table' ).find( ':input.js_field-state' ), + $parent = $state.parent(), + input_name = $state.attr( 'name' ), + input_id = $state.attr( 'id' ), + stickstatefield = 'woocommerce.stickState-' + country, + value = $this.data( stickstatefield ) ? $this.data( stickstatefield ) : $state.val(), + placeholder = $state.attr( 'placeholder' ), + $newstate; + + if ( stickValue ){ + $this.data( 'woocommerce.stickState-' + country, value ); + } + + // Remove the previous DOM element + $parent.show().find( '.select2-container' ).remove(); + + if ( ! $.isEmptyObject( wc_users_fields.states[ country ] ) ) { + var state = wc_users_fields.states[ country ], + $defaultOption = $( '' ) + .text( wc_users_fields.i18n_select_state_text ); + + $newstate = $( '' ) + .prop( 'id', input_id ) + .prop( 'name', input_name ) + .prop( 'placeholder', placeholder ) + .addClass( 'js_field-state' ) + .append( $defaultOption ); + + $.each( state, function( index ) { + var $option = $( '' ) + .prop( 'value', index ) + .text( state[ index ] ); + $newstate.append( $option ); + } ); + + $newstate.val( value ); + + $state.replaceWith( $newstate ); + + $newstate.show().selectWoo().hide().trigger( 'change' ); + } else { + $newstate = $( '' ) + .prop( 'id', input_id ) + .prop( 'name', input_name ) + .prop( 'placeholder', placeholder ) + .addClass( 'js_field-state regular-text' ) + .val( value ); + $state.replaceWith( $newstate ); + } + + // This event has a typo - deprecated in 2.5.0 + $( document.body ).trigger( 'contry-change.woocommerce', [country, $( this ).closest( 'div' )] ); + $( document.body ).trigger( 'country-change.woocommerce', [country, $( this ).closest( 'div' )] ); + }, + + change_state: function() { + // Here we will find if state value on a select has changed and stick it to the country data + var $this = $( this ), + state = $this.val(), + $country = $this.parents( '.form-table' ).find( ':input.js_field-country' ), + country = $country.val(); + + $country.data( 'woocommerce.stickState-' + country, state ); + }, + + copy_billing: function( event ) { + event.preventDefault(); + + $( '#fieldset-billing' ).find( 'input, select' ).each( function( i, el ) { + // The address keys match up, except for the prefix + var shipName = el.name.replace( /^billing_/, 'shipping_' ); + // Swap prefix, then check if there are any elements + var shipEl = $( '[name="' + shipName + '"]' ); + // No corresponding shipping field, skip this item + if ( ! shipEl.length ) { + return; + } + // Found a matching shipping element, update the value + shipEl.val( el.value ).trigger( 'change' ); + } ); + } + }; + + wc_users_fields.init(); + +}); diff --git a/plugins/woocommerce/legacy/js/admin/wc-clipboard.js b/plugins/woocommerce/legacy/js/admin/wc-clipboard.js new file mode 100644 index 00000000000..8a2391213ae --- /dev/null +++ b/plugins/woocommerce/legacy/js/admin/wc-clipboard.js @@ -0,0 +1,38 @@ +/* exported wcSetClipboard, wcClearClipboard */ + +/** + * Simple text copy functions using native browser clipboard capabilities. + * @since 3.2.0 + */ + +/** + * Set the user's clipboard contents. + * + * @param string data: Text to copy to clipboard. + * @param object $el: jQuery element to trigger copy events on. (Default: document) + */ +function wcSetClipboard( data, $el ) { + if ( 'undefined' === typeof $el ) { + $el = jQuery( document ); + } + var $temp_input = jQuery( '

    '; + + comment_form( apply_filters( 'woocommerce_product_review_comment_form_args', $comment_form ) ); + ?> + + + +

    + + +
    + diff --git a/templates/single-product.php b/plugins/woocommerce/templates/single-product.php similarity index 100% rename from templates/single-product.php rename to plugins/woocommerce/templates/single-product.php diff --git a/templates/single-product/add-to-cart/external.php b/plugins/woocommerce/templates/single-product/add-to-cart/external.php similarity index 100% rename from templates/single-product/add-to-cart/external.php rename to plugins/woocommerce/templates/single-product/add-to-cart/external.php diff --git a/plugins/woocommerce/templates/single-product/add-to-cart/grouped.php b/plugins/woocommerce/templates/single-product/add-to-cart/grouped.php new file mode 100644 index 00000000000..fb6fff7a6fc --- /dev/null +++ b/plugins/woocommerce/templates/single-product/add-to-cart/grouped.php @@ -0,0 +1,126 @@ + + +
    + + + get_id() ); + $quantites_required = $quantites_required || ( $grouped_product_child->is_purchasable() && ! $grouped_product_child->has_options() ); + $post = $post_object; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + setup_postdata( $post ); + + if ( $grouped_product_child->is_in_stock() ) { + $show_add_to_cart_button = true; + } + + echo ''; + + // Output columns for each product. + foreach ( $grouped_product_columns as $column_id ) { + do_action( 'woocommerce_grouped_product_list_before_' . $column_id, $grouped_product_child ); + + switch ( $column_id ) { + case 'quantity': + ob_start(); + + if ( ! $grouped_product_child->is_purchasable() || $grouped_product_child->has_options() || ! $grouped_product_child->is_in_stock() ) { + woocommerce_template_loop_add_to_cart(); + } elseif ( $grouped_product_child->is_sold_individually() ) { + echo ''; + } else { + do_action( 'woocommerce_before_add_to_cart_quantity' ); + + woocommerce_quantity_input( + array( + 'input_name' => 'quantity[' . $grouped_product_child->get_id() . ']', + 'input_value' => isset( $_POST['quantity'][ $grouped_product_child->get_id() ] ) ? wc_stock_amount( wc_clean( wp_unslash( $_POST['quantity'][ $grouped_product_child->get_id() ] ) ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Missing + 'min_value' => apply_filters( 'woocommerce_quantity_input_min', 0, $grouped_product_child ), + 'max_value' => apply_filters( 'woocommerce_quantity_input_max', $grouped_product_child->get_max_purchase_quantity(), $grouped_product_child ), + 'placeholder' => '0', + ) + ); + + do_action( 'woocommerce_after_add_to_cart_quantity' ); + } + + $value = ob_get_clean(); + break; + case 'label': + $value = ''; + break; + case 'price': + $value = $grouped_product_child->get_price_html() . wc_get_stock_html( $grouped_product_child ); + break; + default: + $value = ''; + break; + } + + echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + + do_action( 'woocommerce_grouped_product_list_after_' . $column_id, $grouped_product_child ); + } + + echo ''; + } + $post = $previous_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + setup_postdata( $post ); + + do_action( 'woocommerce_grouped_product_list_after', $grouped_product_columns, $quantites_required, $product ); + ?> + +
    ' . apply_filters( 'woocommerce_grouped_product_list_column_' . $column_id, $value, $grouped_product_child ) . '
    + + + + + + + + + + + + +
    + + diff --git a/templates/single-product/add-to-cart/simple.php b/plugins/woocommerce/templates/single-product/add-to-cart/simple.php similarity index 100% rename from templates/single-product/add-to-cart/simple.php rename to plugins/woocommerce/templates/single-product/add-to-cart/simple.php diff --git a/plugins/woocommerce/templates/single-product/add-to-cart/variable.php b/plugins/woocommerce/templates/single-product/add-to-cart/variable.php new file mode 100644 index 00000000000..53dfbfaa867 --- /dev/null +++ b/plugins/woocommerce/templates/single-product/add-to-cart/variable.php @@ -0,0 +1,85 @@ + + +
    + + + +

    + + + + $options ) : ?> + + + + + + +
    + $options, + 'attribute' => $attribute_name, + 'product' => $product, + ) + ); + echo end( $attribute_keys ) === $attribute_name ? wp_kses_post( apply_filters( 'woocommerce_reset_variations_link', '' . esc_html__( 'Clear', 'woocommerce' ) . '' ) ) : ''; + ?> +
    + + +
    + +
    + + + +
    + + [db-host] + ``` + +You may need to quote strings with backslashes to prevent them from being processed by the shell or other programs. + +Example: + + $ tests/bin/install.sh woocommerce_tests root root + + # woocommerce_tests is the database name and root is both the MySQL user and its password. + +**Important**: The `` database will be created if it doesn't exist and all data will be removed during testing. + +## Running Tests + +Change to the plugin root directory and type: + + $ vendor/bin/phpunit + +The tests will execute and you'll be presented with a summary. + +You can run specific tests by providing the path and filename to the test class: + + $ vendor/bin/phpunit tests/legacy/unit-tests/importer/product.php + +A text code coverage summary can be displayed using the `--coverage-text` option: + + $ vendor/bin/phpunit --coverage-text + +### Troubleshooting + +In case you're unable to run the unit tests, you might see an error message similar to: + +``` +Fatal error: require_once(): Failed opening required '/var/folders/qr/3cnz_5_j3j1cljph_246ty1h0000gn/T/wordpress-tests-lib/includes/functions.php' (include_path='.:/usr/local/Cellar/php@7.4/7.4.23/share/php@7.4/pear') in /Users/nielslange/Plugins/woocommerce/tests/legacy/bootstrap.php on line 59 +``` + +If you run into this problem, simply delete the WordPress test directory and run the installer again. In this particular case, you'd run the following command: + +``` +$ rm -rf /var/folders/qr/3cnz_5_j3j1cljph_246ty1h0000gn/T/wordpress-tests-lib +$ tests/bin/install.sh woocommerce_tests_1 root root +``` + +Or if you run into this error: + +``` +PHP Fatal error: require_once(): Failed opening required '/var/folders/n_/ksp7kpt9475byx0vs665j6gc0000gn/T/wordpress//wp-includes/PHPMailer/PHPMailer.php' (include_path='.:/usr/local/Cellar/php@7.4/7.4.26_1/share/php@7.4/pear') in /private/var/folders/n_/ksp7kpt9475byx0vs665j6gc0000gn/T/wordpress-tests-lib/includes/mock-mailer.php on line 2] +``` + +You will want to delete the wordpress folder + +``` +$ rm -rf /var/folders/qr/3cnz_5_j3j1cljph_246ty1h0000gn/T/wordpress +$ tests/bin/install.sh woocommerce_tests_1 root root +``` + +Note that `woocommerce_tests` changed to `woocommerce_tests_1` as the `woocommerce_tests` database already exists due to the prior command. + +### Running tests in PHP 8 + +WooCommerce currently supports PHP versions from 7.0 up to 8.0, and this poses an issue with PHPUnit: + +* The latest PHPUnit version that supports PHP 7.0 is 6.5.14 +* The latest PHPUnit version that WordPress (and thus WooCommerce) supports is 7.5.20, but that version doesn't work on PHP 8 + +To workaround this, the testing strategy used by WooCommerce is as follows: + +* We normally use PHPUnit 6.5.14 +* For PHP 8 we use [a custom fork of PHPUnit 7.5.20 with support for PHP 8](https://github.com/woocommerce/phpunit/pull/1). WooCommerce's GitHub Actions CI workflow is configured to use this fork instead of the old version 6 when running in PHP 8. + +If you want to run the tests locally under PHP 8 you'll need to temporarily modify `composer.json` to use the custom PHPUnit fork in the same way that the GitHub Actions CI workflow file does. These are the commands that you'll need (run them after a regular `composer install` from within the `plugins/woocommerce` directory): + +```shell +curl -L https://github.com/woocommerce/phpunit/archive/add-compatibility-with-php8-to-phpunit-7.zip -o /tmp/phpunit-7.5-fork.zip +unzip -d /tmp/phpunit-7.5-fork /tmp/phpunit-7.5-fork.zip +composer bin phpunit config --unset platform +composer bin phpunit config repositories.0 '{"type": "path", "url": "/tmp/phpunit-7.5-fork/phpunit-add-compatibility-with-php8-to-phpunit-7", "options": {"symlink": false}}' +composer bin phpunit require --dev -W phpunit/phpunit:@dev --ignore-platform-reqs +rm -rf ./vendor/phpunit/ +composer dump-autoload +``` + +Just remember that you can't include the modified `composer.json` in any commit! + +## Writing Tests + +There are three different unit test directories: + +- `tests/legacy/unit-tests` contains tests for code in the `includes` directory. No new tests should be added here, ever; existing test classes shouldn't get new tests either. Fixing faulty existing tests is allowed. +- `tests/php/includes` is where all the new tests for code in the `includes` directory should be written. +- `tests/php/src` is where all the tests for code in the `src` directory should be written. + +Each test file should correspond to an associated source file and be named accordingly: + * For `src` code: The base namespace for tests is `Automattic\WooCommerce\Tests`. A class named `Automattic\WooCommerce\TheNamespace\TheClass` should have a test named `Automattic\WooCommerce\Tests\TheNamespace\TheClassTest`. + * For `includes` code: + * When testing classes: use the same approach as for `src` except that namespaces are not used. So a `WC_Something` class in `includes/somefolder/class-wc-something.php` should have its tests in `tests/src/internal/somefolder/class-wc-something-test.php`. + * When testing functions: use one test file per functions group file, for example `wc-formatting-functions-test.php` for code in the `wc-formatting-functions.php` file. + + +See also [the guidelines for writing unit tests for `src` code](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/src/README.md#writing-unit-tests) and [the guidelines for `includes` code](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/includes/README.md#writing-unit-tests). + +General guidelines for all the unit tests: + +* Each test method should cover a single method or function with one or more assertions +* A single method or function can have multiple associated test methods if it's a large or complex method +* Use the test coverage HTML report (under `tmp/coverage/index.html`) to examine which lines your tests are covering and aim for 100% coverage +* For code that cannot be tested (e.g. they require a certain PHP version), you can exclude them from coverage using a comment: `// @codeCoverageIgnoreStart` and `// @codeCoverageIgnoreEnd`. For example, see [`wc_round_tax_total()`](https://github.com/woocommerce/woocommerce/blob/35f83867736713955fa2c4f463a024578bb88795/includes/wc-formatting-functions.php#L208-L219) +* In addition to covering each line of a method/function, make sure to test common input and edge cases. +* Prefer `assertSame()` where possible as it tests both type and value +* Remember that only methods prefixed with `test` will be run so use helper methods liberally to keep test methods small and reduce code duplication. If there is a common helper method used in multiple test files, consider adding it to the `WC_Unit_Test_Case` class so it can be shared by all test cases +* Filters persist between test cases so be sure to remove them in your test method or in the `tearDown()` method. +* Use data providers where possible. Be sure that their name is like `data_provider_function_to_test` (i.e. the data provider for `test_is_postcode` would be `data_provider_test_is_postcode`). Read more about data providers [here](https://phpunit.de/manual/current/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.data-providers). + +## Automated Tests + +Tests are automatically run with [GitHub Actions](https://github.com/woocommerce/woocommerce/actions/workflows/ci.yml) for each commit and pull request. + +## Code Coverage + +Code coverage is available on [Codecov](https://codecov.io/gh/woocommerce/woocommerce/) which receives updated data after each build. diff --git a/tests/Tools/CodeHacking/CodeHacker.php b/plugins/woocommerce/tests/Tools/CodeHacking/CodeHacker.php similarity index 100% rename from tests/Tools/CodeHacking/CodeHacker.php rename to plugins/woocommerce/tests/Tools/CodeHacking/CodeHacker.php diff --git a/tests/Tools/CodeHacking/Hacks/BypassFinalsHack.php b/plugins/woocommerce/tests/Tools/CodeHacking/Hacks/BypassFinalsHack.php similarity index 100% rename from tests/Tools/CodeHacking/Hacks/BypassFinalsHack.php rename to plugins/woocommerce/tests/Tools/CodeHacking/Hacks/BypassFinalsHack.php diff --git a/tests/Tools/CodeHacking/Hacks/CodeHack.php b/plugins/woocommerce/tests/Tools/CodeHacking/Hacks/CodeHack.php similarity index 100% rename from tests/Tools/CodeHacking/Hacks/CodeHack.php rename to plugins/woocommerce/tests/Tools/CodeHacking/Hacks/CodeHack.php diff --git a/plugins/woocommerce/tests/Tools/CodeHacking/Hacks/FunctionsMockerHack.php b/plugins/woocommerce/tests/Tools/CodeHacking/Hacks/FunctionsMockerHack.php new file mode 100644 index 00000000000..91f6b3e535a --- /dev/null +++ b/plugins/woocommerce/tests/Tools/CodeHacking/Hacks/FunctionsMockerHack.php @@ -0,0 +1,180 @@ + function($name, $default) { + * return 'foo' === $name ? 'bar' : get_option($name, $default); + * } + * ]); + * + * 1 and 2 must be done during the unit testing bootstrap process. + * + * Note that unless the tests directory is included in the hacking via 'CodeHacker::initialize' + * (and they shouldn't!), test code files aren't hacked, therefore the original functions are always + * executed inside tests (and thus the above example won't stack-overflow). + */ +final class FunctionsMockerHack extends CodeHack { + /** + * Tokens that precede a non-standalone-function identifier. + * + * @var array + */ + private static $non_global_function_tokens = array( + T_PAAMAYIM_NEKUDOTAYIM, + T_DOUBLE_COLON, + T_OBJECT_OPERATOR, + T_FUNCTION, + T_CLASS, + T_EXTENDS, + ); + + /** + * @var FunctionsMockerHack Holds the only existing instance of the class. + */ + private static $instance; + + /** + * Initializes the class. + * + * @param array $mockable_functions An array containing the names of the functions that will become mockable. + * + * @throws \Exception $mockable_functions is not an array or is empty. + */ + public static function initialize( $mockable_functions ) { + if ( ! is_array( $mockable_functions ) || empty( $mockable_functions ) ) { + throw new \Exception( 'FunctionsMockeHack::initialize: $mockable_functions must be a non-empty array of function names.' ); + } + + self::$instance = new FunctionsMockerHack( $mockable_functions ); + } + + /** + * FunctionsMockerHack constructor. + * + * @param array $mockable_functions An array containing the names of the functions that will become mockable. + */ + private function __construct( $mockable_functions ) { + $this->mockable_functions = $mockable_functions; + } + + /** + * Hacks code by replacing elegible function invocations with an invocation to this class' static method with the same name. + * + * @param string $code The code to hack. + * @param string $path The path of the file containing the code to hack. + * @return string The hacked code. + */ + public function hack( $code, $path ) { + $tokens = $this->tokenize( $code ); + $code = ''; + $previous_token_is_non_global_function_qualifier = false; + + foreach ( $tokens as $token ) { + $token_type = $this->token_type_of( $token ); + if ( T_WHITESPACE === $token_type ) { + $code .= $this->token_to_string( $token ); + } elseif ( T_STRING === $token_type && ! $previous_token_is_non_global_function_qualifier && in_array( $token[1], $this->mockable_functions, true ) ) { + $code .= __CLASS__ . "::{$token[1]}"; + $previous_token_is_non_global_function_qualifier = false; + } else { + $code .= $this->token_to_string( $token ); + $previous_token_is_non_global_function_qualifier = in_array( $token_type, self::$non_global_function_tokens, true ); + } + } + + return $code; + } + + /** + * @var array Functions that can be mocked, associative array of function name => callback. + */ + private $function_mocks = array(); + + /** + * Register function mocks. + * + * @param array $mocks Mocks as an associative array of function name => mock function with the same arguments as the original function. + * + * @throws \Exception Invalid input. + */ + public function register_function_mocks( $mocks ) { + if ( ! is_array( $mocks ) ) { + throw new \Exception( 'FunctionsMockerHack::add_function_mocks: $mocks must be an associative array of function name => callable.' ); + } + + foreach ( $mocks as $function_name => $mock ) { + if ( ! in_array( $function_name, $this->mockable_functions, true ) ) { + throw new \Exception( "FunctionsMockerHack::add_function_mocks: Can't mock '$function_name' since it isn't in the list of mockable functions supplied to 'initialize'." ); + } + if ( ! is_callable( $mock ) ) { + throw new \Exception( "FunctionsMockerHack::add_function_mocks: The mock supplied for '$function_name' isn't callable." ); + } + + $this->function_mocks[ $function_name ] = $mock; + } + } + + /** + * Register function mocks. + * + * @param array $mocks Mocks as an associative array of function name => mock function with the same arguments as the original function. + * + * @throws \Exception Invalid input. + */ + public static function add_function_mocks( $mocks ) { + self::$instance->register_function_mocks( $mocks ); + } + + /** + * Unregister all the registered function mocks. + */ + public function reset() { + $this->function_mocks = array(); + } + + /** + * Handler for undefined static methods on this class, it invokes the mock for the function if registered or the original function if not. + * + * @param string $name Name of the function. + * @param array $arguments Arguments for the function. + * + * @return mixed The return value from the invoked callback or function. + */ + public static function __callStatic( $name, $arguments ) { + if ( array_key_exists( $name, self::$instance->function_mocks ) ) { + return call_user_func_array( self::$instance->function_mocks[ $name ], $arguments ); + } else { + return call_user_func_array( $name, $arguments ); + } + } + + /** + * Get the only existing instance of this class. 'get_instance' is not used to avoid conflicts since that's a widely used method name. + * + * @return FunctionsMockerHack The only existing instance of this class. + */ + public static function get_hack_instance() { + return self::$instance; + } +} diff --git a/plugins/woocommerce/tests/Tools/CodeHacking/Hacks/StaticMockerHack.php b/plugins/woocommerce/tests/Tools/CodeHacking/Hacks/StaticMockerHack.php new file mode 100644 index 00000000000..61fd4a68d90 --- /dev/null +++ b/plugins/woocommerce/tests/Tools/CodeHacking/Hacks/StaticMockerHack.php @@ -0,0 +1,193 @@ + [ + * 'some_method' => function($some_arg) { + * return 'foo' === $some_arg ? 'bar' : SomeClass::some_method($some_arg); + * } + * ] + * ]); + * + * 1 and 2 must be done during the unit testing bootstrap process. + * + * Note that unless the tests directory is included in the hacking via 'CodeHacker::initialize' + * (and they shouldn't!), test code files aren't hacked, therefore the original functions are always + * executed inside tests (and thus the above example won't stack-overflow). + */ +final class StaticMockerHack extends CodeHack { + + /** + * @var StaticMockerHack Holds the only existing instance of the class. + */ + private static $instance; + + /** + * Initializes the class. + * + * @param array $mockable_classes An associative array of class name => array of class methods. + * + * @throws \Exception $mockable_functions is not an array or is empty. + */ + public static function initialize( $mockable_classes ) { + if ( ! is_array( $mockable_classes ) || empty( $mockable_classes ) ) { + throw new \Exception( 'StaticMockerHack::initialize:: $mockable_classes must be a non-empty associative array of class name => array of class methods.' ); + } + + self::$instance = new StaticMockerHack( $mockable_classes ); + } + + /** + * StaticMockerHack constructor. + * + * @param array $mockable_classes An associative array of class name => array of class methods. + */ + private function __construct( $mockable_classes ) { + $this->mockable_classes = $mockable_classes; + } + + /** + * Hacks code by replacing elegible method invocations with an invocation a static method on this class composed from the class and the method names. + * + * @param string $code The code to hack. + * @param string $path The path of the file containing the code to hack. + * @return string The hacked code. + * + */ + public function hack( $code, $path ) { + $last_item = null; + + $tokens = $this->tokenize( $code ); + $code = ''; + $current_token = null; + + // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition + while ( $current_token = current( $tokens ) ) { + if ( $this->is_token_of_type( $current_token, T_STRING ) && in_array( $current_token[1], $this->mockable_classes, true ) ) { + $class_name = $current_token[1]; + $next_token = next( $tokens ); + if ( $this->is_token_of_type( $next_token, T_DOUBLE_COLON ) ) { + $called_member = next( $tokens )[1]; + $code .= __CLASS__ . "::invoke__{$called_member}__for__{$class_name}"; + } else { + // Reference to source class, but not followed by '::'. + $code .= $this->token_to_string( $current_token ) . $this->token_to_string( $next_token ); + } + } else { + // Not a reference to source class. + $code .= $this->token_to_string( $current_token ); + } + next( $tokens ); + } + + return $code; + } + + /** + * @var array Associative array of class name => associative array of method name => callback. + */ + private $method_mocks = array(); + + /** + * Register method mocks. + * + * @param array $mocks Mocks as an associative array of class name => associative array of method name => mock method with the same arguments as the original method. + * + * @throws \Exception Invalid input. + */ + public function register_method_mocks( $mocks ) { + $exception_text = 'StaticMockerHack::register_method_mocks: $mocks must be an associative array of class name => associative array of method name => callable.'; + + if ( ! is_array( $mocks ) ) { + throw new \Exception( $exception_text ); + } + + foreach ( $mocks as $class_name => $class_mocks ) { + if ( ! is_string( $class_name ) || ! is_array( $class_mocks ) ) { + throw new \Exception( $exception_text ); + } + foreach ( $class_mocks as $method_name => $method_mock ) { + if ( ! is_string( $method_name ) || ! is_callable( $method_mock ) ) { + throw new \Exception( $exception_text ); + } + if ( ! in_array( $class_name, $this->mockable_classes, true ) ) { + throw new \Exception( "FunctionsMockerHack::add_function_mocks: Can't mock methods of the '$class_name' class since it isn't in the list of mockable classes supplied to 'initialize'." ); + } + + $this->method_mocks[ $class_name ][ $method_name ] = $method_mock; + } + } + } + + /** + * Register method mocks. + * + * @param array $mocks Mocks as an associative array of class name => associative array of method name => mock method with the same arguments as the original method. + * + * @throws \Exception Invalid input. + */ + public static function add_method_mocks( $mocks ) { + self::$instance->register_method_mocks( $mocks ); + } + + /** + * Unregister all the registered method mocks. + */ + public function reset() { + $this->method_mocks = array(); + } + + /** + * Handler for undefined static methods on this class, it invokes the mock for the method if both the class and the method are registered, or the original method in the original class if not. + * + * @param string $name Name of the method. + * @param array $arguments Arguments for the function. + * + * @return mixed The return value from the invoked callback or method. + * + * @throws \Exception Invalid method name. + */ + public static function __callStatic( $name, $arguments ) { + preg_match( '/invoke__(.+)__for__(.+)/', $name, $matches ); + if ( empty( $matches ) ) { + throw new \Exception( 'Invalid method ' . __CLASS__ . "::{$name}" ); + } + + $class_name = $matches[2]; + $method_name = $matches[1]; + + if ( array_key_exists( $class_name, self::$instance->method_mocks ) && array_key_exists( $method_name, self::$instance->method_mocks[ $class_name ] ) ) { + return call_user_func_array( self::$instance->method_mocks[ $class_name ][ $method_name ], $arguments ); + } else { + return call_user_func_array( "{$class_name}::{$method_name}", $arguments ); + } + } + + /** + * Get the only existing instance of this class. 'get_instance' is not used to avoid conflicts since that's a widely used method name. + * + * @return StaticMockerHack The only existing instance of this class. + */ + public static function get_hack_instance() { + return self::$instance; + } +} diff --git a/plugins/woocommerce/tests/Tools/CodeHacking/README.md b/plugins/woocommerce/tests/Tools/CodeHacking/README.md new file mode 100644 index 00000000000..f4beef28b68 --- /dev/null +++ b/plugins/woocommerce/tests/Tools/CodeHacking/README.md @@ -0,0 +1,156 @@ +# Code Hacking + +Code hacking is a mechanism that modifies PHP code files while they are loaded. It's intended to ease unit testing code that would otherwise be very difficult or impossible to test (and **only** for this - see [An important note](#an-important-note) about that). + +Currently, the code hacker allows to do the following inside unit tests: + +* Replace standalone functions with custom callbacks. +* Replace invocations to public static methods with custom callbacks. +* Create subclasses of `final` classes. + +## How to use + +Let's go through an example. + +First, create a file named `class-wc-admin-foobar.php` in `includes/admin` with the following code: + +```php + function( $name, $default = false ) { + return "Mocked get_option invoked for '$name'"; + } + ]); + + $expected = "The option returns: Mocked get_option invoked for 'some_option'"; + $actual = $tested->do_something_that_depends_on_an_option(); + $this->assertEquals( $expected, $actual ); + } + + public function test_static_method_mocking() { + $tested = new WC_Admin_Foobar(); + + StaticMockerHack::add_method_mocks([ + 'WC_Some_Legacy_Service' => [ + 'do_something' => function( $what ) { + return "MOCKED do_something invoked for '$what'"; + } + ] + ]); + + $expected = "The legacy service returns: MOCKED do_something invoked for 'foobar'"; + $actual = $tested->do_something_that_depends_on_the_legacy_service( 'foobar' ); + $this->assertEquals( $expected, $actual ); + } +} +``` + +Then run `vendor/bin/phpunit tests/legacy/unit-tests/admin/class-wc-tests-admin-foobar.php` and see the magic happen. + +### Mocking functions + +For a function to be mockable its name needs to be included in the array returned by `tests/legacy/mockable-functions.php`, so if you need to mock a function that is not included in the array, just go and add it. + +Function mocks can be defined by using `FunctionsMockerHack::add_function_mocks`. This method accepts an associative array where keys are function names and values are callbacks with the same signature as the functions they are replacing. + +If you ever need to remove the configured function mocks from inside a test, you can do so by executing `FunctionsMockerHack::get_hack_instance()->reset()`. This is done automatically before each test via PHPUnit's `BeforeTestHook`, so normally you won't need to do that. + +Note that the code hacker is configured so that only the production code files are modified, the tests code itself is **not** modified. This means that you can use the original functions within your tests even if you have mocked them, for example the following would work: + +```php +//Mock get_option but only if the requested option name is 'foo' +FunctionsMockerHack::add_function_mocks([ + 'get_option' => function($name, $default = false) { + return 'foo' === $name ? 'mocked value for option foo' : get_option( $name, $default ); + } +]); +``` + +### Mocking public static methods + +For a public static method to be mockable the name of the class that defines it needs to be included in the array returned by `tests/legacy/classes-with-mockable-static-methods.php`, so if you need to mock a static method for a class that is not included in the array, just go and add it. + +Static method mocks can be defined by using `StaticMockerHack::add_method_mocks`. This method accepts an associative array where keys are class names and values are in turn associative arrays, those having method names as keys and callbacks with the same signature as the methods they are replacing as values. + +If you ever need to remove the configured static method mocks from inside a test, you can do so by executing `StaticMockerHack::get_hack_instance()->reset()`. This is done automatically before each test via PHPUnit's `BeforeTestHook`, so normally you won't need to do that. + +Note that the code hacker is configured so that only the production code files are modified, the tests code itself is **not** modified. This means that you can use the original static methods within your tests even if you have mocked them, for example the following would work: + +```php +StaticMockerHack::add_method_mocks([ + 'WC_Some_Legacy_Service' => [ + //Mock WC_Some_Legacy_Service::do_something but only if the supplied parameter is 'foo' + 'do_something' => function( $what ) { + return 'foo' === $what ? "MOCKED do_something invoked for '$what'" : WC_Some_Legacy_Service::do_something( $what ); + } + ] +]); +``` + +### Subclassing `final` classes + +Inside your test files you can create classes that extend classes marked as `final` thanks to the `BypassFinalsHack` that is registered at bootstrap time. No extra configuration is needed. + +If you want to try it out, mark the `WC_Admin_Foobar` in the previos example as `final`, then add the following to the tests file: `class WC_Admin_Foobar_Subclass extends WC_Admin_Foobar {}`. Without the hack you would get a `Class WC_Admin_Foobar_Subclass may not inherit from final class (WC_Admin_Foobar)` error when trying to run the tests. + +## How it works under the hood + +The core of the code hacker is the `CodeHacker` class, which is based on [the Bypass Finals project](https://github.com/dg/bypass-finals) by David Grudl. This class is actually [a streamWrapper class](https://www.php.net/manual/en/class.streamwrapper.php) for the regular filesystem, most of its methods are just short-circuited to the regular PHP filesystem handling functions but the `stream_open` method contains some code that allows the magic to happen. What it does (for PHP files only) is to read the file contents and apply all the necessary modifications to it, then if the code has been modified it is stored in a temporary file which is then the one that receives any further filesystem operations instead of the original file. That way, for all practical purposes the content of the file is the "hacked" content. + +The files inside `tests/Tools/CodeHacking/Hacks` implement the "hacks" (code file modifications) that are registered via `CodeHacker::add_hack` within `tests/legacy/bootstrap.php`. + +A `BeforeTestHook` is used to reset all hacks to its initial state to ensure that no functions or methods are being mocked when the test starts. + +The functions mocker works by replacing all instances of `the_function(...)` with `FunctionsMockerHack::the_function(...)`, then `FunctionsMockerHack::__call_static` is implemented in a way that invokes the appropriate callback if defined for the invoked function, or reverts to executing the original function if not. The static methods mocker works similarly, but replacing instances of `TheClass::the_method(...)` with `StaticMockerHack::invoke__the_method__for__TheClass(...)`. + +## Creating new hacks + +If you ever need to define a new hack to cover a different kind of code that's difficult to test, that's what you need to do. + +First, implement the hack as a class that contains a `public function hack($code, $path)` method and a `public function reset()` method. The former takes in `$code` a string with the contents of the file pointed by `$path` and returns the modified code, the later reverts the hack to its original state (e.g. for `FunctionsMockerHack` it unregisters all the previously registered function mocks). For convenience you can make your hack a subclass of `CodeHack` but that's not mandatory. + +Second, configure the hack as required inside the `initialize_code_hacker` method in `tests/legacy/bootstrap.php`, and register it using `CodeHacker::add_hack`. + +## Temporarily disabling the code hacker + +In a few rare cases the code hacker will cause problems with tests that do write operations on the local filesystem. In these cases it is possible to temporarily disable the code hacker using `self::disable_code_hacker()` and `self::reenable_code_hacker()` in the test (these methods are defined in `WC_Unit_Test_Case`). These methods are carefully written so that they won't enable the code hacker if it wasn't enabled when the test started, and there's a disabling requests count in place to ensure that the code hacker isn't enabled before it should. + +One of these cases is the usage of the `copy` command to copy files. Since this function is used in a few tests, a convenience `file_copy` method is defined in `WC_Unit_Test_Case`; it just temporarily disables the hacker, does the copy, and reenables the hacker. + +## An important note + +The code hacker is intended to be a **last resort** mechanism to test stuff that it's **really** difficult or impossible to test otherwise - the mechanisms already in place to help testing (e.g. the PHPUnit's mocks or the Woo helpers) should still be used whenever possible. And of course, the code hacker should not be an excuse to write code that's difficult to test. diff --git a/tests/Tools/DependencyManagement/MockableLegacyProxy.php b/plugins/woocommerce/tests/Tools/DependencyManagement/MockableLegacyProxy.php similarity index 100% rename from tests/Tools/DependencyManagement/MockableLegacyProxy.php rename to plugins/woocommerce/tests/Tools/DependencyManagement/MockableLegacyProxy.php diff --git a/plugins/woocommerce/tests/Tools/FakeQueue.php b/plugins/woocommerce/tests/Tools/FakeQueue.php new file mode 100644 index 00000000000..cf60f524bf9 --- /dev/null +++ b/plugins/woocommerce/tests/Tools/FakeQueue.php @@ -0,0 +1,120 @@ +get_instance_of(\WC_Queue::class) + * + * 2. Add the following in the setUp() method of the unit tests class: + * + * $this->register_legacy_proxy_class_mocks([\WC_Queue::class => new FakeQueue()]); + * + * 3. Get the instance of the fake queue with $this->get_legacy_instance_of(\WC_Queue::class) + * and check its methods_called field as appropriate. + */ +class FakeQueue implements \WC_Queue_Interface { + + /** + * Records all the method calls to this instance. + * + * @var array + */ + private $methods_called = array(); + + // phpcs:disable Squiz.Commenting.FunctionComment.Missing + + public function add( $hook, $args = array(), $group = '' ) { + // TODO: Implement add() method. + } + + public function schedule_single( $timestamp, $hook, $args = array(), $group = '' ) { + $this->add_to_methods_called( + 'schedule_single', + $args, + $group, + array( + 'timestamp' => $timestamp, + 'hook' => $hook, + ) + ); + } + + public function schedule_recurring( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '' ) { + // TODO: Implement schedule_recurring() method. + } + + public function schedule_cron( $timestamp, $cron_schedule, $hook, $args = array(), $group = '' ) { + // TODO: Implement schedule_cron() method. + } + + public function cancel( $hook, $args = array(), $group = '' ) { + // TODO: Implement cancel() method. + } + + public function cancel_all( $hook, $args = array(), $group = '' ) { + // TODO: Implement cancel_all() method. + } + + public function get_next( $hook, $args = null, $group = '' ) { + // TODO: Implement get_next() method. + } + + public function search( $args = array(), $return_format = OBJECT ) { + $result = array(); + foreach ( $this->methods_called as $method_called ) { + if ( $method_called['args'] === $args['args'] && $method_called['hook'] === $args['hook'] ) { + $result[] = $method_called; + } + } + return $result; + } + + // phpcs:enable Squiz.Commenting.FunctionComment.Missing + + /** + * Registers a method call for this instance. + * + * @param string $method Name of the invoked method. + * @param array $args Arguments passed in '$args' to the method call. + * @param string $group Group name passed in '$group' to the method call. + * @param array $extra_args Any extra information to store about the method call. + */ + private function add_to_methods_called( $method, $args, $group, $extra_args = array() ) { + $value = array( + 'method' => $method, + 'args' => $args, + 'group' => $group, + ); + + $this->methods_called[] = array_merge( $value, $extra_args ); + } + + /** + * Get the data about the methods called so far. + * + * @return array The current value of $methods_called. + */ + public function get_methods_called() { + return $this->methods_called; + } + + /** + * Clears the collection of the methods called so far. + */ + public function clear_methods_called() { + $this->methods_called = array(); + } + +} diff --git a/plugins/woocommerce/tests/bin/install.sh b/plugins/woocommerce/tests/bin/install.sh new file mode 100755 index 00000000000..7bcc72190ea --- /dev/null +++ b/plugins/woocommerce/tests/bin/install.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +# See https://raw.githubusercontent.com/wp-cli/scaffold-command/master/templates/install-wp-tests.sh + +if [ $# -lt 3 ]; then + echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" + exit 1 +fi + +function check_command_exists { + if ! which "$1" > /dev/null; then + echo "It looks as if $1 is not installed. Please ensure it is installed and on the path." + exit 1 + fi +} + +# Basic pre-requisite checks +check_command_exists "mysqladmin" +check_command_exists "php" +check_command_exists "svn" + +DB_NAME=$1 +DB_USER=$2 +DB_PASS=$3 +DB_HOST=${4-localhost} +WP_VERSION=${5-latest} +SKIP_DB_CREATE=${6-false} + +TMPDIR=${TMPDIR-/tmp} +TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") +WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} +WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/} + +download() { + if [ `which curl` ]; then + curl -s "$1" > "$2"; + elif [ `which wget` ]; then + wget -nv -O "$2" "$1" + fi +} + +if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then + WP_TESTS_TAG="branches/$WP_VERSION" +elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + WP_TESTS_TAG="tags/${WP_VERSION%??}" + else + WP_TESTS_TAG="tags/$WP_VERSION" + fi +elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + WP_TESTS_TAG="trunk" +else + # http serves a single offer, whereas https serves multiple. we only want one + download http://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json + grep '[0-9]+\.[0-9]+(\.[0-9]+)?' $TMPDIR/wp-latest.json + LATEST_VERSION=$(grep -o '"version":"[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//') + if [[ -z "$LATEST_VERSION" ]]; then + echo "Latest WordPress version could not be found" + exit 1 + fi + WP_TESTS_TAG="tags/$LATEST_VERSION" +fi + +set -ex + +install_wp() { + + if [ -d $WP_CORE_DIR ]; then + return; + fi + + mkdir -p $WP_CORE_DIR + + if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + mkdir -p $TMPDIR/wordpress-nightly + download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip + unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/ + mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR + else + if [ $WP_VERSION == 'latest' ]; then + local ARCHIVE_NAME='latest' + elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then + # https serves multiple offers, whereas http serves single. + download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + LATEST_VERSION=${WP_VERSION%??} + else + # otherwise, scan the releases and get the most up to date minor version of the major release + local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` + LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) + fi + if [[ -z "$LATEST_VERSION" ]]; then + local ARCHIVE_NAME="wordpress-$WP_VERSION" + else + local ARCHIVE_NAME="wordpress-$LATEST_VERSION" + fi + else + local ARCHIVE_NAME="wordpress-$WP_VERSION" + fi + download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz + tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR + fi + + download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php +} + +install_test_suite() { + # portable in-place argument for both GNU sed and Mac OSX sed + if [[ $(uname -s) == 'Darwin' ]]; then + local ioption='-i .bak' + else + local ioption='-i' + fi + + # set up testing suite if it doesn't yet exist + if [ ! -d $WP_TESTS_DIR ]; then + # set up testing suite + mkdir -p $WP_TESTS_DIR + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data + else + echo -e "\nTest suite already installed at:\n\n\t$WP_TESTS_DIR\n\n(If you experience difficulties running the tests, consider removing it then re-running this script.)\n" + fi + + if [ ! -f wp-tests-config.php ]; then + download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php + # remove all forward slashes in the end + WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") + sed $ioption -E "s:(__DIR__ . '/src/'|dirname\( __FILE__ \) . '/src/'):'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + fi + +} + +install_db() { + + if [ ${SKIP_DB_CREATE} = "true" ]; then + return 0 + fi + + # If we're trying to connect to a socket we want to handle it differently. + if [[ "$DB_HOST" == *.sock ]]; then + # create database using the socket + mysqladmin create $DB_NAME --socket="$DB_HOST" + else + # Decide whether or not there is a port. + local PARTS=(${DB_HOST//\:/ }) + if [[ ${PARTS[1]} =~ ^[0-9]+$ ]]; then + EXTRA=" --host=${PARTS[0]} --port=${PARTS[1]} --protocol=tcp" + else + EXTRA=" --host=$DB_HOST --protocol=tcp" + fi + + # create database + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA + fi +} + +install_wp +install_test_suite +install_db diff --git a/plugins/woocommerce/tests/bin/phpcs.sh b/plugins/woocommerce/tests/bin/phpcs.sh new file mode 100755 index 00000000000..1a240a0fc69 --- /dev/null +++ b/plugins/woocommerce/tests/bin/phpcs.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +COMMIT_RANGE="${1} ${2}" +CHANGED_FILES=`git diff --name-only --diff-filter=ACMR $COMMIT_RANGE | grep '\.php' | grep 'plugins/woocommerce/' | sed -e 's/^plugins\/woocommerce\///' | awk '{print}' ORS=' '` +IGNORE="tests/cli/,includes/libraries/,includes/api/legacy/" + +if [ "$CHANGED_FILES" != "" ]; then + echo "Changed files: $CHANGED_FILES" + echo "Running Code Sniffer." + + ./vendor/bin/phpcs --ignore=$IGNORE --encoding=utf-8 -s -n -p --report-full --report-checkstyle=./phpcs-report.xml ${CHANGED_FILES} +else + echo "No changes found. Skipping PHPCS run." +fi diff --git a/plugins/woocommerce/tests/bin/phpunit.sh b/plugins/woocommerce/tests/bin/phpunit.sh new file mode 100755 index 00000000000..b9e69bc88c5 --- /dev/null +++ b/plugins/woocommerce/tests/bin/phpunit.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +if [[ ${RUN_PHPCS} == 1 ]] || [[ ${RUN_E2E} == 1 ]]; then + exit +fi + +if [[ ${RUN_CODE_COVERAGE} == 1 ]]; then + phpdbg -qrr ./vendor/bin/phpunit -d memory_limit=-1 -c phpunit.xml --coverage-clover=coverage.clover --exclude-group=timeout --exclude-group=run-in-separate-process $@ +else + vendor/bin/phpunit -c phpunit.xml $@ +fi diff --git a/tests/cli/bin/install-package-tests.sh b/plugins/woocommerce/tests/cli/bin/install-package-tests.sh similarity index 100% rename from tests/cli/bin/install-package-tests.sh rename to plugins/woocommerce/tests/cli/bin/install-package-tests.sh diff --git a/tests/cli/credits.txt b/plugins/woocommerce/tests/cli/credits.txt similarity index 100% rename from tests/cli/credits.txt rename to plugins/woocommerce/tests/cli/credits.txt diff --git a/tests/cli/features/bootstrap/FeatureContext.php b/plugins/woocommerce/tests/cli/features/bootstrap/FeatureContext.php similarity index 100% rename from tests/cli/features/bootstrap/FeatureContext.php rename to plugins/woocommerce/tests/cli/features/bootstrap/FeatureContext.php diff --git a/tests/cli/features/bootstrap/Process.php b/plugins/woocommerce/tests/cli/features/bootstrap/Process.php similarity index 100% rename from tests/cli/features/bootstrap/Process.php rename to plugins/woocommerce/tests/cli/features/bootstrap/Process.php diff --git a/tests/cli/features/bootstrap/support.php b/plugins/woocommerce/tests/cli/features/bootstrap/support.php similarity index 100% rename from tests/cli/features/bootstrap/support.php rename to plugins/woocommerce/tests/cli/features/bootstrap/support.php diff --git a/tests/cli/features/bootstrap/utils.php b/plugins/woocommerce/tests/cli/features/bootstrap/utils.php similarity index 100% rename from tests/cli/features/bootstrap/utils.php rename to plugins/woocommerce/tests/cli/features/bootstrap/utils.php diff --git a/tests/cli/features/customer.feature b/plugins/woocommerce/tests/cli/features/customer.feature similarity index 100% rename from tests/cli/features/customer.feature rename to plugins/woocommerce/tests/cli/features/customer.feature diff --git a/tests/cli/features/customer_download.feature b/plugins/woocommerce/tests/cli/features/customer_download.feature similarity index 100% rename from tests/cli/features/customer_download.feature rename to plugins/woocommerce/tests/cli/features/customer_download.feature diff --git a/tests/cli/features/extra/no-mail.php b/plugins/woocommerce/tests/cli/features/extra/no-mail.php similarity index 100% rename from tests/cli/features/extra/no-mail.php rename to plugins/woocommerce/tests/cli/features/extra/no-mail.php diff --git a/tests/cli/features/payment_gateway.feature b/plugins/woocommerce/tests/cli/features/payment_gateway.feature similarity index 100% rename from tests/cli/features/payment_gateway.feature rename to plugins/woocommerce/tests/cli/features/payment_gateway.feature diff --git a/tests/cli/features/product.feature b/plugins/woocommerce/tests/cli/features/product.feature similarity index 100% rename from tests/cli/features/product.feature rename to plugins/woocommerce/tests/cli/features/product.feature diff --git a/tests/cli/features/product_review.feature b/plugins/woocommerce/tests/cli/features/product_review.feature similarity index 100% rename from tests/cli/features/product_review.feature rename to plugins/woocommerce/tests/cli/features/product_review.feature diff --git a/tests/cli/features/shop_coupon.feature b/plugins/woocommerce/tests/cli/features/shop_coupon.feature similarity index 100% rename from tests/cli/features/shop_coupon.feature rename to plugins/woocommerce/tests/cli/features/shop_coupon.feature diff --git a/tests/cli/features/steps/given.php b/plugins/woocommerce/tests/cli/features/steps/given.php similarity index 100% rename from tests/cli/features/steps/given.php rename to plugins/woocommerce/tests/cli/features/steps/given.php diff --git a/tests/cli/features/steps/then.php b/plugins/woocommerce/tests/cli/features/steps/then.php similarity index 100% rename from tests/cli/features/steps/then.php rename to plugins/woocommerce/tests/cli/features/steps/then.php diff --git a/tests/cli/features/steps/when.php b/plugins/woocommerce/tests/cli/features/steps/when.php similarity index 100% rename from tests/cli/features/steps/when.php rename to plugins/woocommerce/tests/cli/features/steps/when.php diff --git a/tests/cli/features/tool.feature b/plugins/woocommerce/tests/cli/features/tool.feature similarity index 100% rename from tests/cli/features/tool.feature rename to plugins/woocommerce/tests/cli/features/tool.feature diff --git a/tests/cli/utils/behat-tags.php b/plugins/woocommerce/tests/cli/utils/behat-tags.php similarity index 100% rename from tests/cli/utils/behat-tags.php rename to plugins/woocommerce/tests/cli/utils/behat-tags.php diff --git a/plugins/woocommerce/tests/e2e/README.md b/plugins/woocommerce/tests/e2e/README.md new file mode 100644 index 00000000000..f9161e3df56 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/README.md @@ -0,0 +1,385 @@ +# WooCommerce End to End Tests + +Automated end-to-end tests for WooCommerce. + +## Table of contents + +- [Pre-requisites](#pre-requisites) + - [Install Node.js](#install-node.js) + - [Install NVM](#install-nvm) + - [Install Docker](#install-docker) +- [Configuration](#configuration) + - [Test Environment](#test-environment) + - [Test Variables](#test-variables) + - [Jest test sequencer](#jest-test-sequencer) + - [Chromium Download](#chromium-download) +- [Running tests](#running-tests) + - [Prep work for running tests](#prep-work-for-running-tests) + - [How to run tests in headless mode](#how-to-run-tests-in-headless-mode) + - [How to run tests in non-headless mode](#how-to-run-tests-in-non-headless-mode) + - [How to run tests in debug mode](#how-to-run-tests-in-debug-mode) + - [How to run an individual test](#how-to-run-an-individual-test) + - [How to skip tests](#how-to-skip-tests) + - [How to run tests using custom WordPress, PHP and MariaDB versions](#how-to-run-tests-using-custom-wordpress,-php-and-mariadb-versions) +- [Guide for writing e2e tests](#guide-for-writing-e2e-tests) + - [Tools for writing tests](#tools-for-writing-tests) + - [Creating test structure](#creating-test-structure) + - [Writing the test](#writing-the-test) + - [Best practices](#best-practices) + - [Writing tests for WooCommerce extensions](#Writing-tests-for-WooCommerce-extensions) +- [Debugging tests](#debugging-tests) + +## Pre-requisites + +### Install Nx + +Follow [instructions on nx.dev site](https://nx.dev/l/r/getting-started/nx-setup) to install Nx. + +### Install pnpm + +Follow [instructions on pnpm.io site](https://pnpm.io/installation) to install pnpm. + +### Install Node.js + +Follow [instructions on the node.js site](https://nodejs.org/en/download/) to install Node.js. + +### Install NVM + +Follow instructions in the [NVM repository](https://github.com/nvm-sh/nvm) to install NVM. + +### Install Docker + +Install Docker Desktop if you don't have it installed: + +- [Docker Desktop for Mac](https://docs.docker.com/docker-for-mac/install/) +- [Docker Desktop for Windows](https://docs.docker.com/docker-for-windows/install/) + +Once installed, you should see `Docker Desktop is running` message with the green light next to it indicating that everything is working as expected. + +Note, that if you install docker through other methods such as homebrew, for example, your steps to set it up will be different. The commands listed in steps below may also vary. + +## Configuration + +This section explains how e2e tests are working behind the scenes. These are not instructions on how to build environment for running e2e tests and run them. If you are looking for instructions on how to run e2e tests, jump to [Running tests](#running-tests). + +### Test Environment + +We recommend using Docker for running tests locally in order for the test environment to match the setup on GitHub CI (where Docker is also used for running tests). [An official WordPress Docker image](https://github.com/docker-library/docs/blob/master/wordpress/README.md) is used to build the site. Once the site using the WP Docker image is built, the current WooCommerce dev branch is mapped into the `plugins` folder of that newly built test site. + +### Test Variables + +The jest test sequencer uses the following test variables: + +``` +{ + "url": "http://localhost:8084/", + "users": { + "admin": { + "username": "admin", + "password": "password" + }, + "customer": { + "username": "customer", + "password": "password" + } + } +} +``` + +If you need to modify the port for your local test environment (eg. port is already in use), edit `tests/e2e/config/default.json`. Only edit this file while your test container is `down`. + +### Jest test sequencer + +[Jest](https://jestjs.io/) is being used to run e2e tests. Jest sequencer introduces tools that can be used to specify the order in which the tests are being run. In our case, they are being run in alphabetical order of the directories where tests are located. This way, tests in the directory `activate-and-setup` will run first. By default, jest runs tests ordered by the time it takes to run the test (the test that takes longer to run will be run first, the test that takes less time to run will run last). + +The Setup Wizard e2e test runs first to ensure that WooCommerce is active and that the setup wizard has been completed. This is necessary because `docker-up` creates a brand new install of WordPress and WooCommerce. + +### Chromium Download + +By default, `Puppeteer` downloads the `Chromium` package every time you run `pnpm install` or `pnpm update`. To disable that download add the following to your `.bash_profile` or `.zshrc` (whichever you use): + +```shell script +export PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true +``` + +Puppeteer will still automatically download Chromium when needed. + +## Running tests + +If you are using Windows, we recommend using [Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/) for End-to-end testing. Follow the [WSL Setup Instructions](./WSL_SETUP_INSTRUCTIONS.md) first before proceeding with the steps below. + +### Prep work for running tests + +Run the following in a terminal/command line window + +- `cd` to the WooCommerce monorepo folder + +- `git checkout trunk` (or the branch where you need to run tests) + +- `nvm use` + +- `pnpm install` + +- `pnpm nx composer-install woocommerce` + +- `pnpm nx build-assets woocommerce` + +- `npm install jest --global` (this only needs to be done once) + +- `pnpm nx docker-up woocommerce` (this will build the test site using Docker) + +- Use `docker ps` to confirm that the Docker containers are running. You should see a log similar to one below indicating that everything had been built as expected: + +``` +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +c380e1964506 env_wordpress-cli "entrypoint.sh" 7 seconds ago Up 5 seconds woocommerce_e2e_wordpress-cli +2ab8e8439e9f wordpress:5.5.1 "docker-entrypoint.s…" 8 seconds ago Up 7 seconds 0.0.0.0:8084->80/tcp woocommerce_e2e_wordpress-www +4c1e3f2a49db mariadb:10.5.5 "docker-entrypoint.s…" 10 seconds ago Up 8 seconds 3306/tcp woocommerce_e2e_db +``` + +Note that by default, Docker will download the latest images available for WordPress, PHP and MariaDB. In the example above, you can see that WordPress 5.5.1 and MariaDB 10.5.5 were used. + +See [How to run tests using custom WordPress, PHP and MariaDB versions](#how-to-run-tests-using-custom-wordpress,-php-and-mariadb-versions) if you'd like to use different versions. + +- Navigate to `http://localhost:8084/` + +If everything went well, you should be able to access the site. If you changed the port to something other than `8084` as per [Test Variables](#test-variables) section, use the appropriate port to access your site. + +As noted in [Test Variables](#test-variables) section, use the following Admin user details to login: + +``` +Username: admin +PW: password +``` + +- Run `pnpm nx docker-down woocommerce` when you are done with running e2e tests and before making any changes to test site configuration. + +Note that running `pnpm nx docker-down woocommerce` and then `pnpm nx docker-up woocommerce` re-initializes the test container. + +### How to run tests in headless mode + +To run e2e tests in headless mode use the following command: + +```bash +pnpm nx test-e2e woocommerce +``` + +### How to run tests in non-headless mode + +Tests run in headless mode by default. However, sometimes it's useful to observe the browser while running or developing tests. To do so, you can run tests in a non-headless (dev) mode: + +```bash +pnpm nx test-e2e-dev woocommerce +``` + +The dev mode also enables SlowMo mode. SlowMo slows down Puppeteer’s operations. This makes it easier to see what is happening in the browser. + +By default, SlowMo mode adds a 50 millisecond delay between test steps. If you'd like to override the length of the delay and have the tests run faster or slower in the `-dev` mode, pass `PUPPETEER_SLOWMO` variable when running tests as shown below: + +``` +PUPPETEER_SLOWMO=10 pnpm nx test-e2e-dev woocommerce +``` + +The faster you want the tests to run, the lower the value should be of `PUPPETEER_SLOWMO` should be. + +For example: + +- `PUPPETEER_SLOWMO=10` - will run tests faster +- `PUPPETEER_SLOWMO=70` - will run tests slower + +### How to retry failed tests + +Sometimes tests may fail for different reasons such as network issues, or lost connection. To mitigate against test flakiess, failed tests are rerun up to 3 times before being marked as failed. The amount of retry attempts can be adjusted by passing the `E2E_RETRY_TIMES` variable when running tests. For example: + +``` +cd plugins/woocommerce +E2E_RETRY_TIMES=2 pnpm exec wc-e2e test:e2e +``` + +### How to run tests in debug mode + +Tests run in headless mode by default. While writing tests it may be useful to have the debugger loaded while running a test in non-headless mode. To run tests in debug mode: + +```bash +pnpm nx test-e2e-debug woocommerce +``` + +When all tests have been completed the debugger remains active. Control doesn't return to the command line until the debugger is closed. Otherwise, debug mode functions the same as non-headless mode. + +### How to run an individual test + +To run an individual test, use the direct path to the spec. For example: + +```bash +cd plugins/woocommerce +pnpm exec wc-e2e test:e2e ./tests/e2e/specs/wp-admin/create-order.test.js +``` + +### How to skip tests + +To skip the tests, use `.only` in the relevant test entry to specify the tests that you do want to run. + +For example, in order to skip Setup Wizard tests, add `.only` to the login and activation tests as follows in the `setup-wizard.test.js`: + +``` +it.only( 'Can login', async () => {} +``` + +``` +it.only( 'Can make sure WooCommerce is activated. If not, activate it', async () => {} +``` + +As a result, when you run `setup-wizard.test.js`, only the login and activate tests will run. The rest will be skipped. You should see the following in the terminal: + +``` + PASS tests/e2e/specs/activate-and-setup/setup-wizard.test.js (11.927s) + Store owner can login and make sure WooCommerce is activated + ✓ Can login (7189ms) + ✓ Can make sure WooCommerce is activated. If not, activate it (1187ms) + Store owner can go through store Setup Wizard + ○ skipped Can start Setup Wizard + ○ skipped Can fill out Store setup details + ○ skipped Can fill out Payment details + ○ skipped Can fill out Shipping details + ○ skipped Can fill out Recommended details + ○ skipped Can skip Activate Jetpack section + ○ skipped Can finish Setup Wizard - Ready! section + Store owner can finish initial store setup + ○ skipped Can enable tax rates and calculations + ○ skipped Can configure permalink settings +``` + +You can also use `.skip` in the same fashion. For example: + +``` +it.skip( 'Can start Setup Wizard', async () => {} +``` + +Finally, you can apply both `.only` and `.skip` to `describe` part of the test: + +``` +describe.skip( 'Store owner can go through store Setup Wizard', () => {} +``` + +### How to run tests using custom WordPress, PHP and MariaDB versions + +The following variables can be used to specify the versions of WordPress, PHP and MariaDB that you'd like to use to build your test site with Docker: + +- `WP_VERSION` +- `TRAVIS_PHP_VERSION` +- `TRAVIS_MARIADB_VERSION` + +The full command to build the site will look as follows: + +``` +TRAVIS_MARIADB_VERSION=10.5.3 TRAVIS_PHP_VERSION=7.4.5 WP_VERSION=5.4.1 pnpm nx docker-up woocommerce +``` + +## Guide for writing e2e tests + +### Tools for writing tests + +We use the following tools to write e2e tests: + +- [Puppeteer](https://github.com/GoogleChrome/puppeteer) – a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol +- [jest-puppeteer](https://github.com/smooth-code/jest-puppeteer) – provides all required configuration to run tests using Puppeteer +- [expect-puppeteer](https://github.com/smooth-code/jest-puppeteer/tree/master/packages/expect-puppeteer) – assertion library for Puppeteer + +In the WooCommerce Core repository the tests are in `tests/e2e/core-tests/specs/` folder. However, if you are writing tests in your own project using WooCommerce Core e2e packages, the tests should be located in `tests/e2e/specs/` folder. + +The following packages are used in write tests: + +- `@automattic/puppeteer-utils` - utilities and configuration for running puppeteer against WordPress. See details in the [package's repository](https://github.com/Automattic/puppeteer-utils). +- `@woocommerce/e2e-utils` - this package contains utilities to simplify writing e2e tests specific to WooCommmerce. See details in the [package's repository](https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/e2e-utils). + +### Creating test structure + +It is a good practice to start working on the test by identifying what needs to be tested on the higher and lower levels. For example, if you are writing a test to verify that merchant can create a virtual product, the overview of the test will be as follows: + +- Merchant can create virtual product + - Merchant can log in + - Merchant can create virtual product + - Merchant can verify that virtual product was created + +Once you identify the structure of the test, you can move on to writing it. + +### Writing the test + +The structure of the test serves as a skeleton for the test itself. You can turn it into a test by using `describe()` and `it()` methods of Jest: + +- [`describe()`](https://jestjs.io/docs/en/api#describename-fn) - creates a block that groups together several related tests; +- [`it()`](https://jestjs.io/docs/en/api#testname-fn-timeout) - actual method that runs the test. + +Based on our example, the test skeleton would look as follows: + +``` +describe( 'Merchant can create virtual product', () => { + it( 'merchant can log in', async () => { + + } ); + + it( 'merchant can create virtual product', async () => { + + } ); + + it( 'merchant can verify that virtual product was created', async () => { + + } ); +} ); +``` + +Next, you can start filling up each section with relevant functions (test building blocks). Note, that we have the `@woocommerce/e2e-utils` package where many reusable helper functions can be found for writing tests. For example, `merchant.js` of `@woocommerce/e2e-utils` package contains `merchant` object that has `login` method. As a result, in the test it can be used as `await merchant.login();` so the first `it()` section of the test will become: + +``` +it( 'merchant can log in', async () => { + await merchant.login(); + } ); +``` + +Moving to the next section where we need to actually create a product. You will find that we have a reusable function such as `createSimpleProduct()` in the `components.js` of `@woocommerce/e2e-utils` package. However, note that this function should not be used for testing creating a product because the simple product is created using WooCommerce REST API. This is not how the merchant would typically create a virtual product, we would need to test it by writing actual steps for creating a product in the test. + +`createSimpleProduct()` should be used in tests where you need to test something else than creating a simple product. In other words, this function exists in order to quickly fill the site with test data required for running tests. For example, if you want to write a test that will verify that shopper can place a product to the cart on the site, you can use `createSimpleProduct()` to create a product to test the cart. + +Because `createSimpleProduct()` can't be used in the case of our example test, we'd need to navigate to the page where the user would usually create a product. To do that, there is `openNewProduct()` function of the `merchant` object that we already used above. As a result, that part of the test will look as follows: + +``` +it( 'merchant can create virtual product', async () => { + await merchant.openNewProduct(); + } ); +``` + +You would then continue writing the test using utilities where possible. + +Make sure to utilize the functions of the `@automattic/puppeteer-utils` package where possible. For example, if you need to wait for a certain element to be ready to be clicked on and then click on it, you can use `waitAndClick()` function: + +``` +await waitAndClick( page, '#selector' ); +``` + +### Best practices + +- It is best to keep the tests inside `describe()` block granular as it helps to debug the test if it fails. When the test has finished, you will see the result along with the breakdown of how each of the test sections performed. If one of the tests within `describe()` block fails, it will be shown as follows: + +``` +FAIL ../specs/front-end/front-end-my-account.test.js (9.219s) + My account page + ✓ allows customer to login (2924ms) + ✓ allows customer to see orders (1083ms) + x allows customer to see downloads (887ms) + ✓ allows customer to see addresses (1161ms) + ✓ allows customer to see account details (1066ms) +``` + +In the example above, you can see that `allows customer to see downloads` part of the test failed and can start looking at it right away. Without steps the test goes through being detailed, it is more difficult to debug it. + +### Writing tests for WooCommerce extensions + +If you want to set up E2E tests for your WooCommerce extension you can make use of the default WooCommerce E2E package. + +The [WooCommerce E2E Tests Boilerplate repo](https://github.com/woocommerce/woocommerce-e2e-boilerplate) aims to provide a stripped down version of the default WooCommerce E2E test suite along with basic set up instructions to get started. + +## Debugging tests + +The test sequencer (`pnpm nx test-e2e woocommerce`) includes support for saving [screenshots on test errors](https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/e2e-environment#test-screenshots) which can be sent to a Slack channel via a [Slackbot](https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/e2e-environment#slackbot-setup). + +For Puppeteer debugging, follow [Google's documentation](https://developers.google.com/web/tools/puppeteer/debugging). diff --git a/plugins/woocommerce/tests/e2e/WSL_SETUP_INSTRUCTIONS.md b/plugins/woocommerce/tests/e2e/WSL_SETUP_INSTRUCTIONS.md new file mode 100644 index 00000000000..7e6b7e88dfe --- /dev/null +++ b/plugins/woocommerce/tests/e2e/WSL_SETUP_INSTRUCTIONS.md @@ -0,0 +1,58 @@ +# Setup Instructions for Windows Subsystem for Linux (WSL) + +You can set up a local development environment on Windows with [Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/). The following instructions are for Ubuntu 20.04. + +## Pre-requisites + +You should have the following already set up on your Windows computer: +- **Docker Desktop for Windows** - https://docs.docker.com/docker-for-windows/install/ +- **WSL 2** - https://docs.microsoft.com/en-us/windows/wsl/install +- **Ubuntu 20.04 set as default Linux distribution** - https://docs.microsoft.com/en-us/windows/wsl/wsl-config#list-installed-distributions + +## Setup Steps + +Update and upgrade packages. +```bash +sudo apt update -y && sudo apt upgrade -y +``` + +In order for Composer commands to work later on, you have to install the following: +- PHP +- Composer +- `php-xml` +- `php-mbstring` +```bash +sudo apt install php-cli unzip -y + +cd ~ + +curl -sS https://getcomposer.org/installer -o composer-setup.php + +HASH=`curl -sS https://composer.github.io/installer.sig` + +echo $HASH + +php -r "if (hash_file('SHA384', 'composer-setup.php') === '$HASH') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" + +sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer + +composer --version --no-interaction # Verify that Composer installation was successful + +sudo apt install php-xml -y + +sudo apt install php-mbstring -y +``` + +For Puppeteer to run in headless mode you'll need to install additional packages: +```bash +sudo apt install -y ca-certificates fonts-liberation gconf-service libappindicator1 libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release wget xdg-utils +``` + +Add your username to the `docker` group to avoid having to type `sudo` when you run Docker commands. +```bash +sudo usermod -aG docker ${YOUR_USERNAME} + +su - ${YOUR_USERNAME} +``` + +At this point, you're now ready to proceed with the steps in [Prep work for running tests](./README.md#prep-work-for-running-tests). diff --git a/plugins/woocommerce/tests/e2e/config/default.json b/plugins/woocommerce/tests/e2e/config/default.json new file mode 100644 index 00000000000..5803d199412 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/config/default.json @@ -0,0 +1,211 @@ +{ + "url": "http://localhost:8084/", + "users": { + "admin": { + "username": "admin", + "password": "password" + }, + "customer": { + "username": "customer", + "password": "password" + } + }, + "products": { + "simple": { + "name": "Simple product" + }, + "variable": { + "name": "Variable Product with Three Attributes", + "defaultAttributes": [ + { + "id": 0, + "name": "Size", + "option": "Medium" + }, + { + "id": 0, + "name": "Colour", + "option": "Blue" + } + ], + "attributes": [ + { + "id": 0, + "name": "Colour", + "isVisibleOnProductPage": true, + "isForVariations": true, + "options": [ + "Red", + "Green", + "Blue" + ], + "sortOrder": 0 + }, + { + "id": 0, + "name": "Size", + "isVisibleOnProductPage": true, + "isForVariations": true, + "options": [ + "Small", + "Medium", + "Large" + ], + "sortOrder": 0 + }, + { + "id": 0, + "name": "Logo", + "isVisibleOnProductPage": true, + "isForVariations": true, + "options": [ + "Woo", + "WordPress" + ], + "sortOrder": 0 + } + ] + }, + "variations": [ + { + "regularPrice": "19.99", + "attributes": [ + { + "name": "Size", + "option": "Large" + }, + { + "name": "Colour", + "option": "Red" + } + ] + }, + { + "regularPrice": "18.99", + "attributes": [ + { + "name": "Size", + "option": "Medium" + }, + { + "name": "Colour", + "option": "Green" + } + ] + }, + { + "regularPrice": "17.99", + "attributes": [ + { + "name": "Size", + "option": "Small" + }, + { + "name": "Colour", + "option": "Blue" + } + ] + } + ], + "grouped": { + "name": "Grouped Product with Three Children", + "groupedProducts": [ + { + "name": "Base Unit", + "regularPrice": "29.99" + }, + { + "name": "Add-on A", + "regularPrice": "11.95" + }, + { + "name": "Add-on B", + "regularPrice": "18.97" + } + ] + }, + "external": { + "name": "External product", + "regularPrice": "24.99", + "buttonText": "Buy now", + "externalUrl": "https://wordpress.org/plugins/woocommerce" + } + }, + "coupons": { + "percentage": { + "code": "20percent", + "discountType": "percent", + "amount": "20.00" + } + }, + "addresses": { + "admin": { + "store": { + "email": "admin@woocommercecoree2etestsuite.com", + "firstname": "John", + "lastname": "Doe", + "company": "Automattic", + "country": "United States (US)", + "addressfirstline": "addr 1", + "addresssecondline": "addr 2", + "countryandstate": "United States (US) — California", + "city": "San Francisco", + "state": "CA", + "postcode": "94107" + } + }, + "customer": { + "billing": { + "firstname": "John", + "lastname": "Doe", + "company": "Automattic", + "country": "United States (US)", + "addressfirstline": "addr 1", + "addresssecondline": "addr 2", + "city": "San Francisco", + "state": "CA", + "postcode": "94107", + "phone": "123456789", + "email": "john.doe@example.com" + }, + "shipping": { + "firstname": "John", + "lastname": "Doe", + "company": "Automattic", + "country": "United States (US)", + "addressfirstline": "addr 1", + "addresssecondline": "addr 2", + "city": "San Francisco", + "state": "CA", + "postcode": "94107" + } + } + }, + "orders": { + "basicPaidOrder": { + "paymentMethod": "cod", + "status": "processing", + "billing": { + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com" + } + } + }, + "onboardingwizard": { + "industry": "Test industry", + "numberofproducts": "1 - 10", + "sellingelsewhere": "No", + "sellingOnAnotherPlatform": "Yes, on another platform", + "number_employees": "< 10", + "revenue": "Up to $2,500.00", + "other_platform_name": "Etsy" + }, + "settings": { + "shipping": { + "zonename": "United States", + "zoneregions": "United States (US)", + "shippingmethod": "Free shipping" + } + } +} diff --git a/plugins/woocommerce/tests/e2e/config/jest.config.js b/plugins/woocommerce/tests/e2e/config/jest.config.js new file mode 100644 index 00000000000..4e3b87bcce7 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/config/jest.config.js @@ -0,0 +1,8 @@ +const path = require( 'path' ); +const { useE2EJestConfig, getAppRoot } = require( '@woocommerce/e2e-environment' ); + +const jestConfig = useE2EJestConfig( { + roots: [ path.resolve( __dirname, '../specs' ) ], +} ); + +module.exports = jestConfig; diff --git a/plugins/woocommerce/tests/e2e/config/jest.setup.js b/plugins/woocommerce/tests/e2e/config/jest.setup.js new file mode 100644 index 00000000000..f413c18ce85 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/config/jest.setup.js @@ -0,0 +1,84 @@ +import { + clearLocalStorage, + setBrowserViewport, + withRestApi, + WP_ADMIN_LOGIN +} from '@woocommerce/e2e-utils'; + +const config = require( 'config' ); +const { HTTPClientFactory } = require( '@woocommerce/api' ); +const { addConsoleSuppression, updateReadyPageStatus, setupJestRetries } = require( '@woocommerce/e2e-environment' ); +const { DEFAULT_TIMEOUT_OVERRIDE } = process.env; + +// @todo: remove this once https://github.com/woocommerce/woocommerce-admin/issues/6992 has been addressed +addConsoleSuppression('woocommerce_shared_settings', false); +// @todo: remove this once https://github.com/woocommerce/woocommerce/issues/31867 has been addressed +addConsoleSuppression('wp.compose.withState', false); + +/** + * Uses the WordPress API to delete all existing posts + */ +async function trashExistingPosts() { + const apiUrl = config.get('url'); + const wpPostsEndpoint = '/wp/v2/posts'; + const adminUsername = config.get('users.admin.username'); + const adminPassword = config.get('users.admin.password'); + const client = HTTPClientFactory.build(apiUrl) + .withBasicAuth(adminUsername, adminPassword) + .create(); + + // List all existing posts + const response = await client.get(wpPostsEndpoint); + const posts = response.data; + + // Delete each post + for (const post of posts) { + await client.delete(`${wpPostsEndpoint}/${post.id}`); + } +} + +// Before every test suite run, delete all content created by the test. This ensures +// other posts/comments/etc. aren't dirtying tests and tests don't depend on +// each other's side-effects. +beforeAll(async () => { + + setupJestRetries( 2 ); + + if ( DEFAULT_TIMEOUT_OVERRIDE ) { + page.setDefaultNavigationTimeout( DEFAULT_TIMEOUT_OVERRIDE ); + page.setDefaultTimeout( DEFAULT_TIMEOUT_OVERRIDE ); + } + + try { + // Update the ready page to prevent concurrent test runs + await updateReadyPageStatus('draft'); + await trashExistingPosts(); + await withRestApi.deleteAllProducts(); + await withRestApi.deleteAllCoupons(); + await withRestApi.deleteAllOrders(); + } catch ( error ) { + // Prevent an error here causing tests to fail. + } + + await page.goto(WP_ADMIN_LOGIN); + await clearLocalStorage(); + await setBrowserViewport( { + width: 1280, + height: 800, + }); +}); + +// Clear browser cookies and cache using DevTools. +// This is to ensure that each test ends with no user logged in. +afterAll(async () => { + // Reset the ready page to published to allow future test runs + try { + await updateReadyPageStatus('publish'); + } catch ( error ) { + // Prevent an error here causing tests to fail. + } + + const client = await page.target().createCDPSession(); + await client.send('Network.clearBrowserCookies'); + await client.send('Network.clearBrowserCache'); +}); diff --git a/plugins/woocommerce/tests/e2e/docker/init-sample-products.sh b/plugins/woocommerce/tests/e2e/docker/init-sample-products.sh new file mode 100755 index 00000000000..17b5001f721 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/docker/init-sample-products.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +echo "Initializing WooCommerce E2E" + +wp plugin activate woocommerce + +wp user create customer customer@woocommercecoree2etestsuite.com --user_pass=password --role=subscriber --path=/var/www/html + +# we cannot create API keys for the API, so we using basic auth, this plugin allows that. +wp plugin install https://github.com/WP-API/Basic-Auth/archive/master.zip --activate + +# update permalinks to `pretty` to make it easier for testing APIs with k6 +wp option update permalink_structure '/%postname%' + +# install the WP Mail Logging plugin to test emails +wp plugin install wp-mail-logging --activate + +# Installing and activating the WordPress Importer plugin to import sample products" +wp plugin install wordpress-importer --activate + +# Adding basic WooCommerce settings" +wp option set woocommerce_store_address "Example Address Line 1" +wp option set woocommerce_store_address_2 "Example Address Line 2" +wp option set woocommerce_store_city "Example City" +wp option set woocommerce_default_country "US:CA" +wp option set woocommerce_store_postcode "94110" +wp option set woocommerce_currency "USD" +wp option set woocommerce_product_type "both" +wp option set woocommerce_allow_tracking "no" +wp option set woocommerce_enable_checkout_login_reminder "yes" +wp option set --format=json woocommerce_cod_settings '{"enabled":"yes"}' + +# WooCommerce shop pages +wp wc --user=admin tool run install_pages + +# Importing WooCommerce sample products" +wp import wp-content/plugins/woocommerce/sample-data/sample_products.xml --authors=skip + +# install Storefront +wp theme install storefront --activate + +echo "Success! Your E2E Test Environment is now ready." diff --git a/plugins/woocommerce/tests/e2e/docker/init-wp-beta.sh b/plugins/woocommerce/tests/e2e/docker/init-wp-beta.sh new file mode 100755 index 00000000000..c84029ab7af --- /dev/null +++ b/plugins/woocommerce/tests/e2e/docker/init-wp-beta.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +echo "Initializing WooCommerce E2E" + +wp plugin activate woocommerce +wp theme install twentynineteen --activate +wp user create customer customer@woocommercecoree2etestsuite.com --user_pass=password --role=customer --path=/var/www/html + +# we cannot create API keys for the API, so we using basic auth, this plugin allows that. +wp plugin install https://github.com/WP-API/Basic-Auth/archive/master.zip --activate + +# install the WP Mail Logging plugin to test emails +wp plugin install wp-mail-logging --activate + +# initialize pretty permalinks +wp rewrite structure /%postname%/ + +echo "Updating to WordPress Nightly Point Release" +wp core update https://wordpress.org/nightly-builds/wordpress-latest.zip + +echo "Updating the database" +wp core update-db diff --git a/plugins/woocommerce/tests/e2e/docker/initialize.sh b/plugins/woocommerce/tests/e2e/docker/initialize.sh new file mode 100755 index 00000000000..f86c50f218e --- /dev/null +++ b/plugins/woocommerce/tests/e2e/docker/initialize.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +echo "Initializing WooCommerce E2E" + +# This is a workaround to accommodate different directory names. +wp plugin activate --all +wp plugin deactivate akismet +wp plugin deactivate hello + +wp theme install twentynineteen --activate +wp user create customer customer@woocommercecoree2etestsuite.com \ + --user_pass=password \ + --role=subscriber \ + --first_name='Jane' \ + --last_name='Smith' \ + --path=/var/www/html + +# we cannot create API keys for the API, so we using basic auth, this plugin allows that. +wp plugin install https://github.com/WP-API/Basic-Auth/archive/master.zip --activate + +# Reset plugin that allows us to reset WooCommerce state between tests. +wp plugin install https://github.com/woocommerce/woocommerce-reset/zipball/trunk/ --activate + +# install the WP Mail Logging plugin to test emails +wp plugin install wp-mail-logging --activate + +# initialize pretty permalinks +wp rewrite structure /%postname%/ diff --git a/plugins/woocommerce/tests/e2e/specs/activate-and-setup/basic-setup.test.js b/plugins/woocommerce/tests/e2e/specs/activate-and-setup/basic-setup.test.js new file mode 100644 index 00000000000..4b26f2133a4 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/activate-and-setup/basic-setup.test.js @@ -0,0 +1,3 @@ +const { testAdminBasicSetup } = require( '@woocommerce/admin-e2e-tests' ); + +testAdminBasicSetup(); diff --git a/plugins/woocommerce/tests/e2e/specs/activate-and-setup/complete-onboarding-wizard.test.js b/plugins/woocommerce/tests/e2e/specs/activate-and-setup/complete-onboarding-wizard.test.js new file mode 100644 index 00000000000..28ca08ce731 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/activate-and-setup/complete-onboarding-wizard.test.js @@ -0,0 +1,21 @@ +const { + testAdminOnboardingWizard, + testSelectiveBundleWCPay, + testDifferentStoreCurrenciesWCPay, + testSubscriptionsInclusion, + testBusinessDetailsForm, + testAdminHomescreen, +} = require( '@woocommerce/admin-e2e-tests' ); +const { withRestApi, IS_RETEST_MODE } = require( '@woocommerce/e2e-utils' ); + +// Reset onboarding profile when re-running tests on a site +if ( IS_RETEST_MODE ) { + withRestApi.resetOnboarding(); +} + +testAdminOnboardingWizard(); +testSelectiveBundleWCPay(); +testDifferentStoreCurrenciesWCPay(); +testSubscriptionsInclusion(); +testBusinessDetailsForm(); +testAdminHomescreen(); diff --git a/plugins/woocommerce/tests/e2e/specs/activate-and-setup/setup-onboarding.js b/plugins/woocommerce/tests/e2e/specs/activate-and-setup/setup-onboarding.js new file mode 100644 index 00000000000..f3de8c5841b --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/activate-and-setup/setup-onboarding.js @@ -0,0 +1,8 @@ +/* + * Internal dependencies + */ +const { runActivationTest, runInitialStoreSettingsTest, runSetupOnboardingTests } = require( '@woocommerce/e2e-core-tests' ); + +runActivationTest(); +runInitialStoreSettingsTest(); +runSetupOnboardingTests(); diff --git a/plugins/woocommerce/tests/e2e/specs/admin-analytics/analytics-overview.test.js b/plugins/woocommerce/tests/e2e/specs/admin-analytics/analytics-overview.test.js new file mode 100644 index 00000000000..9bc1b4e7467 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/admin-analytics/analytics-overview.test.js @@ -0,0 +1,3 @@ +const { testAdminAnalyticsOverview } = require( '@woocommerce/admin-e2e-tests' ); + +testAdminAnalyticsOverview(); diff --git a/plugins/woocommerce/tests/e2e/specs/admin-analytics/analytics.test.js b/plugins/woocommerce/tests/e2e/specs/admin-analytics/analytics.test.js new file mode 100644 index 00000000000..5a38d6f3234 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/admin-analytics/analytics.test.js @@ -0,0 +1,3 @@ +const { testAdminAnalyticsPages } = require( '@woocommerce/admin-e2e-tests' ); + +testAdminAnalyticsPages(); diff --git a/plugins/woocommerce/tests/e2e/specs/admin-homescreen/activity-panel.test.js b/plugins/woocommerce/tests/e2e/specs/admin-homescreen/activity-panel.test.js new file mode 100644 index 00000000000..b8865709e32 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/admin-homescreen/activity-panel.test.js @@ -0,0 +1,3 @@ +const { testAdminHomescreenActivityPanel } = require( '@woocommerce/admin-e2e-tests' ); + +testAdminHomescreenActivityPanel(); diff --git a/plugins/woocommerce/tests/e2e/specs/admin-homescreen/task-list.test.js b/plugins/woocommerce/tests/e2e/specs/admin-homescreen/task-list.test.js new file mode 100644 index 00000000000..ae76561b357 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/admin-homescreen/task-list.test.js @@ -0,0 +1,3 @@ +const { testAdminHomescreenTasklist } = require( '@woocommerce/admin-e2e-tests' ); + +testAdminHomescreenTasklist(); diff --git a/plugins/woocommerce/tests/e2e/specs/admin-marketing/coupons.test.js b/plugins/woocommerce/tests/e2e/specs/admin-marketing/coupons.test.js new file mode 100644 index 00000000000..28629da035c --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/admin-marketing/coupons.test.js @@ -0,0 +1,3 @@ +const { testAdminCouponsPage } = require( '@woocommerce/admin-e2e-tests' ); + +testAdminCouponsPage(); diff --git a/plugins/woocommerce/tests/e2e/specs/admin-tasks/payment.test.js b/plugins/woocommerce/tests/e2e/specs/admin-tasks/payment.test.js new file mode 100644 index 00000000000..a0600e5d656 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/admin-tasks/payment.test.js @@ -0,0 +1,3 @@ +const { testAdminPaymentSetupTask } = require( '@woocommerce/admin-e2e-tests' ); + +testAdminPaymentSetupTask(); diff --git a/plugins/woocommerce/tests/e2e/specs/admin-tasks/purchase.test.js b/plugins/woocommerce/tests/e2e/specs/admin-tasks/purchase.test.js new file mode 100644 index 00000000000..8a7c6572c72 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/admin-tasks/purchase.test.js @@ -0,0 +1,3 @@ +const { testAdminPurchaseSetupTask } = require( '@woocommerce/admin-e2e-tests' ); + +testAdminPurchaseSetupTask(); diff --git a/tests/e2e/specs/front-end/test-cart.js b/plugins/woocommerce/tests/e2e/specs/front-end/cart-begin.test.js similarity index 100% rename from tests/e2e/specs/front-end/test-cart.js rename to plugins/woocommerce/tests/e2e/specs/front-end/cart-begin.test.js diff --git a/plugins/woocommerce/tests/e2e/specs/front-end/cart-calculate-shipping.test.js b/plugins/woocommerce/tests/e2e/specs/front-end/cart-calculate-shipping.test.js new file mode 100644 index 00000000000..6ee84f7b8f2 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/front-end/cart-calculate-shipping.test.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runCartCalculateShippingTest } = require( '@woocommerce/e2e-core-tests' ); + +runCartCalculateShippingTest(); diff --git a/tests/e2e/specs/front-end/test-cart-coupons.js b/plugins/woocommerce/tests/e2e/specs/front-end/cart-coupons.test.js similarity index 100% rename from tests/e2e/specs/front-end/test-cart-coupons.js rename to plugins/woocommerce/tests/e2e/specs/front-end/cart-coupons.test.js diff --git a/plugins/woocommerce/tests/e2e/specs/front-end/cart-redirection.cart.js b/plugins/woocommerce/tests/e2e/specs/front-end/cart-redirection.cart.js new file mode 100644 index 00000000000..c269788a2bb --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/front-end/cart-redirection.cart.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runCartRedirectionTest } = require( '@woocommerce/e2e-core-tests' ); + +runCartRedirectionTest(); diff --git a/tests/e2e/specs/front-end/test-checkout.js b/plugins/woocommerce/tests/e2e/specs/front-end/checkout-begin.test.js similarity index 100% rename from tests/e2e/specs/front-end/test-checkout.js rename to plugins/woocommerce/tests/e2e/specs/front-end/checkout-begin.test.js diff --git a/tests/e2e/specs/front-end/test-checkout-coupons.js b/plugins/woocommerce/tests/e2e/specs/front-end/checkout-coupons.test.js similarity index 100% rename from tests/e2e/specs/front-end/test-checkout-coupons.js rename to plugins/woocommerce/tests/e2e/specs/front-end/checkout-coupons.test.js diff --git a/plugins/woocommerce/tests/e2e/specs/front-end/checkout-create-account.test.js b/plugins/woocommerce/tests/e2e/specs/front-end/checkout-create-account.test.js new file mode 100644 index 00000000000..58cdf32b230 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/front-end/checkout-create-account.test.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runCheckoutCreateAccountTest } = require( '@woocommerce/e2e-core-tests' ); + +runCheckoutCreateAccountTest(); diff --git a/plugins/woocommerce/tests/e2e/specs/front-end/checkout-login-account.test.js b/plugins/woocommerce/tests/e2e/specs/front-end/checkout-login-account.test.js new file mode 100644 index 00000000000..1bf304e04ad --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/front-end/checkout-login-account.test.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runCheckoutLoginAccountTest } = require( '@woocommerce/e2e-core-tests' ); + +runCheckoutLoginAccountTest(); diff --git a/plugins/woocommerce/tests/e2e/specs/front-end/my-account-create-account.test.js b/plugins/woocommerce/tests/e2e/specs/front-end/my-account-create-account.test.js new file mode 100644 index 00000000000..cfcbfdc780b --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/front-end/my-account-create-account.test.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runMyAccountCreateAccountTest } = require( '@woocommerce/e2e-core-tests' ); + +runMyAccountCreateAccountTest(); diff --git a/plugins/woocommerce/tests/e2e/specs/front-end/my-account-pay-order.test.js b/plugins/woocommerce/tests/e2e/specs/front-end/my-account-pay-order.test.js new file mode 100644 index 00000000000..a0722df86c0 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/front-end/my-account-pay-order.test.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runMyAccountPayOrderTest } = require( '@woocommerce/e2e-core-tests' ); + +runMyAccountPayOrderTest(); diff --git a/tests/e2e/specs/front-end/test-my-account.js b/plugins/woocommerce/tests/e2e/specs/front-end/my-account.test.js similarity index 100% rename from tests/e2e/specs/front-end/test-my-account.js rename to plugins/woocommerce/tests/e2e/specs/front-end/my-account.test.js diff --git a/plugins/woocommerce/tests/e2e/specs/front-end/order-email-receiving.test.js b/plugins/woocommerce/tests/e2e/specs/front-end/order-email-receiving.test.js new file mode 100644 index 00000000000..a85e21d0c22 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/front-end/order-email-receiving.test.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runOrderEmailReceivingTest } = require( '@woocommerce/e2e-core-tests' ); + +runOrderEmailReceivingTest(); diff --git a/plugins/woocommerce/tests/e2e/specs/front-end/product-browse-search-sort.test.js b/plugins/woocommerce/tests/e2e/specs/front-end/product-browse-search-sort.test.js new file mode 100644 index 00000000000..ed12d1cb341 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/front-end/product-browse-search-sort.test.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runProductBrowseSearchSortTest } = require( '@woocommerce/e2e-core-tests' ); + +runProductBrowseSearchSortTest(); diff --git a/tests/e2e/specs/front-end/shopper.test.js b/plugins/woocommerce/tests/e2e/specs/front-end/shopper.js similarity index 100% rename from tests/e2e/specs/front-end/shopper.test.js rename to plugins/woocommerce/tests/e2e/specs/front-end/shopper.js diff --git a/tests/e2e/specs/front-end/test-single-product-page.js b/plugins/woocommerce/tests/e2e/specs/front-end/single-product-page.test.js similarity index 100% rename from tests/e2e/specs/front-end/test-single-product-page.js rename to plugins/woocommerce/tests/e2e/specs/front-end/single-product-page.test.js diff --git a/tests/e2e/specs/front-end/test-variable-product-updates.js b/plugins/woocommerce/tests/e2e/specs/front-end/variable-product-updates.test.js similarity index 100% rename from tests/e2e/specs/front-end/test-variable-product-updates.js rename to plugins/woocommerce/tests/e2e/specs/front-end/variable-product-updates.test.js diff --git a/plugins/woocommerce/tests/e2e/specs/rest-api/api.test.js b/plugins/woocommerce/tests/e2e/specs/rest-api/api.test.js new file mode 100644 index 00000000000..03719aa55a7 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/rest-api/api.test.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runApiTests } = require( '@woocommerce/e2e-core-tests' ); + +runApiTests(); diff --git a/plugins/woocommerce/tests/e2e/specs/rest-api/coupon.js b/plugins/woocommerce/tests/e2e/specs/rest-api/coupon.js new file mode 100644 index 00000000000..180b9835836 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/rest-api/coupon.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runCouponApiTest } = require( '@woocommerce/e2e-core-tests' ); + +runCouponApiTest(); diff --git a/plugins/woocommerce/tests/e2e/specs/rest-api/external-product.js b/plugins/woocommerce/tests/e2e/specs/rest-api/external-product.js new file mode 100644 index 00000000000..02a153243ed --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/rest-api/external-product.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runExternalProductAPITest } = require( '@woocommerce/e2e-core-tests' ); + +runExternalProductAPITest(); diff --git a/plugins/woocommerce/tests/e2e/specs/rest-api/grouped-product.js b/plugins/woocommerce/tests/e2e/specs/rest-api/grouped-product.js new file mode 100644 index 00000000000..9cdc71f4b94 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/rest-api/grouped-product.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runGroupedProductAPITest } = require( '@woocommerce/e2e-core-tests' ); + +runGroupedProductAPITest(); diff --git a/plugins/woocommerce/tests/e2e/specs/rest-api/order.js b/plugins/woocommerce/tests/e2e/specs/rest-api/order.js new file mode 100644 index 00000000000..d73a8be5f28 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/rest-api/order.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runOrderApiTest } = require( '@woocommerce/e2e-core-tests' ); + +runOrderApiTest(); diff --git a/plugins/woocommerce/tests/e2e/specs/rest-api/telemetry.js b/plugins/woocommerce/tests/e2e/specs/rest-api/telemetry.js new file mode 100644 index 00000000000..8e7a86b1870 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/rest-api/telemetry.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runTelemetryAPITest } = require( '@woocommerce/e2e-core-tests' ); + +runTelemetryAPITest(); diff --git a/plugins/woocommerce/tests/e2e/specs/rest-api/variable-product.js b/plugins/woocommerce/tests/e2e/specs/rest-api/variable-product.js new file mode 100644 index 00000000000..9561f630a76 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/rest-api/variable-product.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runVariableProductAPITest } = require( '@woocommerce/e2e-core-tests' ); + +runVariableProductAPITest(); diff --git a/plugins/woocommerce/tests/e2e/specs/smoke-tests/update-woocommerce.js b/plugins/woocommerce/tests/e2e/specs/smoke-tests/update-woocommerce.js new file mode 100644 index 00000000000..446a80cea38 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/smoke-tests/update-woocommerce.js @@ -0,0 +1,54 @@ +/** + * Internal dependencies + */ +const { merchant, utils } = require( '@woocommerce/e2e-utils' ); + +const { getRemotePluginZip, getLatestReleaseZipUrl, deleteDownloadedPluginFiles } = require( '@woocommerce/e2e-environment' ); + +/** + * External dependencies + */ +const { + it, + beforeAll, +} = require( '@jest/globals' ); + +const { UPDATE_WC, TEST_RELEASE } = process.env; + +let zipUrl; +const pluginName = 'WooCommerce'; + +let pluginPath; + +utils.describeIf( UPDATE_WC )( 'WooCommerce plugin can be uploaded and activated', () => { + beforeAll( async () => { + + if ( TEST_RELEASE ) { + zipUrl = await getLatestReleaseZipUrl( 'woocommerce/woocommerce' ); + } else { + zipUrl = 'https://github.com/woocommerce/woocommerce/releases/download/nightly/woocommerce-trunk-nightly.zip'; + } + + pluginPath = await getRemotePluginZip( zipUrl ); + await merchant.login(); + }); + + afterAll( async () => { + await merchant.logout(); + await deleteDownloadedPluginFiles(); + }); + + it( 'can upload and activate the WooCommerce plugin', async () => { + await merchant.uploadAndActivatePlugin( pluginPath, pluginName ); + }); + + it( 'can run the database update', async () => { + // Check for, and run, the database upgrade if needed + await merchant.runDatabaseUpdate(); + }); + + it( 'can remove downloaded plugin zip', async () => { + await deleteDownloadedPluginFiles(); + } ); + +}); diff --git a/plugins/woocommerce/tests/e2e/specs/smoke-tests/upload-plugin.js b/plugins/woocommerce/tests/e2e/specs/smoke-tests/upload-plugin.js new file mode 100644 index 00000000000..7526b336f00 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/smoke-tests/upload-plugin.js @@ -0,0 +1,48 @@ +/** + * Internal dependencies + */ +const { merchant, utils } = require( '@woocommerce/e2e-utils' ); + +const { getRemotePluginZip, getLatestReleaseZipUrl, deleteDownloadedPluginFiles } = require( '@woocommerce/e2e-environment' ); + +/** + * External dependencies + */ +const { + it, + beforeAll, +} = require( '@jest/globals' ); + +const { GITHUB_REPOSITORY, PLUGIN_NAME, GITHUB_TOKEN, PLUGIN_REPOSITORY } = process.env; + +// allows us to upload plugins from different repositories. +const pluginName = PLUGIN_NAME ? PLUGIN_NAME : 'WooCommerce'; +const repository = PLUGIN_REPOSITORY ? PLUGIN_REPOSITORY : GITHUB_REPOSITORY; + +let zipUrl; +let pluginPath; + +utils.describeIf( repository )( + `Upload and activate ${ pluginName } from ${ repository }`, + () => { + beforeAll( async () => { + zipUrl = await getLatestReleaseZipUrl( repository, GITHUB_TOKEN ); + + pluginPath = await getRemotePluginZip( zipUrl, GITHUB_TOKEN ); + + await merchant.login(); + } ); + + afterAll( async () => { + await merchant.logout(); + } ); + + it( 'can upload and activate the provided plugin', async () => { + await merchant.uploadAndActivatePlugin( pluginPath, PLUGIN_NAME ); + } ); + + it( 'can remove downloaded plugin zip', async () => { + await deleteDownloadedPluginFiles(); + } ); + } +); diff --git a/plugins/woocommerce/tests/e2e/specs/wp-admin/analytics-page-loads.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/analytics-page-loads.js new file mode 100644 index 00000000000..6113889de83 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/wp-admin/analytics-page-loads.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runAnalyticsPageLoadsTest } = require( '@woocommerce/e2e-core-tests' ); + +runAnalyticsPageLoadsTest(); diff --git a/tests/e2e/specs/wp-admin/test-create-coupon.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/create-coupon.test.js similarity index 100% rename from tests/e2e/specs/wp-admin/test-create-coupon.js rename to plugins/woocommerce/tests/e2e/specs/wp-admin/create-coupon.test.js diff --git a/tests/e2e/specs/wp-admin/test-create-order.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/create-order.test.js similarity index 100% rename from tests/e2e/specs/wp-admin/test-create-order.js rename to plugins/woocommerce/tests/e2e/specs/wp-admin/create-order.test.js diff --git a/plugins/woocommerce/tests/e2e/specs/wp-admin/create-shipping-classes.test.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/create-shipping-classes.test.js new file mode 100644 index 00000000000..5b51054cb7d --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/wp-admin/create-shipping-classes.test.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runAddShippingClassesTest } = require( '@woocommerce/e2e-core-tests' ); + +runAddShippingClassesTest(); diff --git a/plugins/woocommerce/tests/e2e/specs/wp-admin/create-shipping-zones.test.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/create-shipping-zones.test.js new file mode 100644 index 00000000000..bf084d01847 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/wp-admin/create-shipping-zones.test.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runAddNewShippingZoneTest } = require( '@woocommerce/e2e-core-tests' ); + +runAddNewShippingZoneTest(); diff --git a/tests/e2e/specs/wp-admin/test-add-simple-product.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/create-simple-product.test.js similarity index 100% rename from tests/e2e/specs/wp-admin/test-add-simple-product.js rename to plugins/woocommerce/tests/e2e/specs/wp-admin/create-simple-product.test.js diff --git a/tests/e2e/specs/wp-admin/test-add-variable-product.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/create-variable-product.test.js similarity index 100% rename from tests/e2e/specs/wp-admin/test-add-variable-product.js rename to plugins/woocommerce/tests/e2e/specs/wp-admin/create-variable-product.test.js diff --git a/tests/e2e/specs/wp-admin/merchant.test.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/merchant.js similarity index 100% rename from tests/e2e/specs/wp-admin/merchant.test.js rename to plugins/woocommerce/tests/e2e/specs/wp-admin/merchant.js diff --git a/tests/e2e/specs/wp-admin/test-order-apply-coupon.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/order-coupon.test.js similarity index 100% rename from tests/e2e/specs/wp-admin/test-order-apply-coupon.js rename to plugins/woocommerce/tests/e2e/specs/wp-admin/order-coupon.test.js diff --git a/tests/e2e/specs/wp-admin/test-order-customer-payment-page.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/order-customer-payment-page.test.js similarity index 100% rename from tests/e2e/specs/wp-admin/test-order-customer-payment-page.js rename to plugins/woocommerce/tests/e2e/specs/wp-admin/order-customer-payment-page.test.js diff --git a/tests/e2e/specs/wp-admin/test-edit-order.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/order-edit.test.js similarity index 100% rename from tests/e2e/specs/wp-admin/test-edit-order.js rename to plugins/woocommerce/tests/e2e/specs/wp-admin/order-edit.test.js diff --git a/plugins/woocommerce/tests/e2e/specs/wp-admin/order-emails.test.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/order-emails.test.js new file mode 100644 index 00000000000..e0597074bda --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/wp-admin/order-emails.test.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runMerchantOrderEmailsTest } = require( '@woocommerce/e2e-core-tests' ); + +runMerchantOrderEmailsTest(); diff --git a/plugins/woocommerce/tests/e2e/specs/wp-admin/order-refund-restock.test.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/order-refund-restock.test.js new file mode 100644 index 00000000000..d036c409845 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/wp-admin/order-refund-restock.test.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runOrderRefundRestockTest } = require( '@woocommerce/e2e-core-tests' ); + +runOrderRefundRestockTest(); diff --git a/tests/e2e/specs/wp-admin/test-order-refund.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/order-refund.test.js similarity index 100% rename from tests/e2e/specs/wp-admin/test-order-refund.js rename to plugins/woocommerce/tests/e2e/specs/wp-admin/order-refund.test.js diff --git a/plugins/woocommerce/tests/e2e/specs/wp-admin/order-searching.test.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/order-searching.test.js new file mode 100644 index 00000000000..1dd73167d24 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/wp-admin/order-searching.test.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runOrderSearchingTest } = require( '@woocommerce/e2e-core-tests' ); + +runOrderSearchingTest(); diff --git a/tests/e2e/specs/wp-admin/test-order-status-filters.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/order-status-filters.test.js similarity index 100% rename from tests/e2e/specs/wp-admin/test-order-status-filters.js rename to plugins/woocommerce/tests/e2e/specs/wp-admin/order-status-filters.test.js diff --git a/plugins/woocommerce/tests/e2e/specs/wp-admin/page-loads.test.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/page-loads.test.js new file mode 100644 index 00000000000..2fc1c01aa00 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/wp-admin/page-loads.test.js @@ -0,0 +1,6 @@ +/* + Internal dependencies + */ +const { runAdminPageLoadTests } = require( '@woocommerce/e2e-core-tests' ); + +runAdminPageLoadTests(); diff --git a/tests/e2e/specs/wp-admin/test-product-edit.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/product-edit.test.js similarity index 100% rename from tests/e2e/specs/wp-admin/test-product-edit.js rename to plugins/woocommerce/tests/e2e/specs/wp-admin/product-edit.test.js diff --git a/plugins/woocommerce/tests/e2e/specs/wp-admin/product-import-csv.test.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/product-import-csv.test.js new file mode 100644 index 00000000000..1eb5d972074 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/wp-admin/product-import-csv.test.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runImportProductsTest } = require( '@woocommerce/e2e-core-tests' ); + +runImportProductsTest(); diff --git a/tests/e2e/specs/wp-admin/test-product-search.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/product-search.test.js similarity index 100% rename from tests/e2e/specs/wp-admin/test-product-search.js rename to plugins/woocommerce/tests/e2e/specs/wp-admin/product-search.test.js diff --git a/tests/e2e/specs/wp-admin/test-update-general-settings.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/update-general-settings.test.js similarity index 100% rename from tests/e2e/specs/wp-admin/test-update-general-settings.js rename to plugins/woocommerce/tests/e2e/specs/wp-admin/update-general-settings.test.js diff --git a/tests/e2e/specs/wp-admin/test-update-product-settings.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/update-product-settings.test.js similarity index 100% rename from tests/e2e/specs/wp-admin/test-update-product-settings.js rename to plugins/woocommerce/tests/e2e/specs/wp-admin/update-product-settings.test.js diff --git a/tests/e2e/specs/wp-admin/test-update-tax-settings.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/update-tax-settings.test.js similarity index 100% rename from tests/e2e/specs/wp-admin/test-update-tax-settings.js rename to plugins/woocommerce/tests/e2e/specs/wp-admin/update-tax-settings.test.js diff --git a/plugins/woocommerce/tests/e2e/specs/wp-admin/wccom-connect.test.js b/plugins/woocommerce/tests/e2e/specs/wp-admin/wccom-connect.test.js new file mode 100644 index 00000000000..a7b4cd8deed --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/wp-admin/wccom-connect.test.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runInitiateWccomConnectionTest } = require( '@woocommerce/e2e-core-tests' ); + +runInitiateWccomConnectionTest(); diff --git a/plugins/woocommerce/tests/legacy/bootstrap.php b/plugins/woocommerce/tests/legacy/bootstrap.php new file mode 100644 index 00000000000..be42bad5755 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/bootstrap.php @@ -0,0 +1,280 @@ +tests_dir = dirname( __FILE__ ); + $this->plugin_dir = dirname( dirname( $this->tests_dir ) ); + + $this->register_autoloader_for_testing_tools(); + + $this->initialize_code_hacker(); + + ini_set( 'display_errors', 'on' ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Blacklisted + error_reporting( E_ALL ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.prevent_path_disclosure_error_reporting, WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_error_reporting + + // Ensure theme install tests use direct filesystem method. + define( 'FS_METHOD', 'direct' ); + + // Ensure server variable is set for WP email functions. + // phpcs:disable WordPress.VIP.SuperGlobalInputUsage.AccessDetected + if ( ! isset( $_SERVER['SERVER_NAME'] ) ) { + $_SERVER['SERVER_NAME'] = 'localhost'; + } + // phpcs:enable WordPress.VIP.SuperGlobalInputUsage.AccessDetected + + $this->wp_tests_dir = getenv( 'WP_TESTS_DIR' ) ? getenv( 'WP_TESTS_DIR' ) : sys_get_temp_dir() . '/wordpress-tests-lib'; + + // load test function so tests_add_filter() is available. + require_once $this->wp_tests_dir . '/includes/functions.php'; + + // Always load PayPal Standard for unit tests. + tests_add_filter( 'woocommerce_should_load_paypal_standard', '__return_true' ); + + // load WC. + tests_add_filter( 'muplugins_loaded', array( $this, 'load_wc' ) ); + + // install WC. + tests_add_filter( 'setup_theme', array( $this, 'install_wc' ) ); + + // Set up WC-Admin config. + tests_add_filter( 'woocommerce_admin_get_feature_config', array( $this, 'add_development_features' ) ); + + /* + * Load PHPUnit Polyfills for the WP testing suite. + * @see https://github.com/WordPress/wordpress-develop/pull/1563/ + */ + define( 'WP_TESTS_PHPUNIT_POLYFILLS_PATH', __DIR__ . '/../../vendor/yoast/phpunit-polyfills/phpunitpolyfills-autoload.php' ); + + // load the WP testing environment. + require_once $this->wp_tests_dir . '/includes/bootstrap.php'; + + // load WC testing framework. + $this->includes(); + + // re-initialize dependency injection, this needs to be the last operation after everything else is in place. + $this->initialize_dependency_injection(); + } + + /** + * Register autoloader for the files in the 'tests/tools' directory, for the root namespace 'Automattic\WooCommerce\Testing\Tools'. + */ + protected static function register_autoloader_for_testing_tools() { + return spl_autoload_register( + function ( $class ) { + $prefix = 'Automattic\\WooCommerce\\Testing\\Tools\\'; + $base_dir = dirname( dirname( __FILE__ ) ) . '/Tools/'; + $len = strlen( $prefix ); + if ( strncmp( $prefix, $class, $len ) !== 0 ) { + // no, move to the next registered autoloader. + return; + } + $relative_class = substr( $class, $len ); + $file = $base_dir . str_replace( '\\', '/', $relative_class ) . '.php'; + if ( ! file_exists( $file ) ) { + throw new \Exception( 'Autoloader for unit tests: file not found: ' . $file ); + } + require $file; + } + ); + } + + /** + * Initialize the code hacker. + * + * @throws Exception Error when initializing one of the hacks. + */ + private function initialize_code_hacker() { + CodeHacker::initialize( array( __DIR__ . '/../../includes/' ) ); + + $replaceable_functions = include_once __DIR__ . '/mockable-functions.php'; + if ( ! empty( $replaceable_functions ) ) { + FunctionsMockerHack::initialize( $replaceable_functions ); + CodeHacker::add_hack( FunctionsMockerHack::get_hack_instance() ); + } + + $mockable_static_classes = include_once __DIR__ . '/classes-with-mockable-static-methods.php'; + if ( ! empty( $mockable_static_classes ) ) { + StaticMockerHack::initialize( $mockable_static_classes ); + CodeHacker::add_hack( StaticMockerHack::get_hack_instance() ); + } + + CodeHacker::add_hack( new BypassFinalsHack() ); + + CodeHacker::enable(); + } + + /** + * Re-initialize the dependency injection engine. + * + * The dependency injection engine has been already initialized as part of the Woo initialization, but we need + * to replace the registered read-only container with a fully configurable one for testing. + * To this end we hack a bit and use reflection to grab the underlying container that the read-only one stores + * in a private property. + * + * Additionally, we replace the legacy/function proxies with mockable versions to easily replace anything + * in tests as appropriate. + * + * @throws \Exception The Container class doesn't have a 'container' property. + */ + private function initialize_dependency_injection() { + try { + $inner_container_property = new \ReflectionProperty( \Automattic\WooCommerce\Container::class, 'container' ); + } catch ( ReflectionException $ex ) { + throw new \Exception( "Error when trying to get the private 'container' property from the " . \Automattic\WooCommerce\Container::class . ' class using reflection during unit testing bootstrap, has the property been removed or renamed?' ); + } + + $inner_container_property->setAccessible( true ); + $inner_container = $inner_container_property->getValue( wc_get_container() ); + + $inner_container->replace( LegacyProxy::class, MockableLegacyProxy::class ); + $inner_container->reset_all_resolved(); + + $GLOBALS['wc_container'] = $inner_container; + } + + /** + * Load WooCommerce. + * + * @since 2.2 + */ + public function load_wc() { + define( 'WC_TAX_ROUNDING_MODE', 'auto' ); + define( 'WC_USE_TRANSACTIONS', false ); + update_option( 'woocommerce_enable_coupons', 'yes' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + update_option( 'woocommerce_onboarding_opt_in', 'yes' ); + + require_once $this->plugin_dir . '/woocommerce.php'; + FeaturePlugin::instance()->init(); + } + + /** + * Install WooCommerce after the test environment and WC have been loaded. + * + * @since 2.2 + */ + public function install_wc() { + // Clean existing install first. + define( 'WP_UNINSTALL_PLUGIN', true ); + define( 'WC_REMOVE_ALL_DATA', true ); + include $this->plugin_dir . '/uninstall.php'; + + WC_Install::install(); + + // Reload capabilities after install, see https://core.trac.wordpress.org/ticket/28374. + if ( version_compare( $GLOBALS['wp_version'], '4.7', '<' ) ) { + $GLOBALS['wp_roles']->reinit(); + } else { + $GLOBALS['wp_roles'] = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + wp_roles(); + } + + echo esc_html( 'Installing WooCommerce...' . PHP_EOL ); + } + + /** + * Load WC-specific test cases and factories. + * + * @since 2.2 + */ + public function includes() { + // framework. + require_once $this->tests_dir . '/framework/class-wc-unit-test-factory.php'; + require_once $this->tests_dir . '/framework/class-wc-mock-session-handler.php'; + require_once $this->tests_dir . '/framework/class-wc-mock-wc-data.php'; + require_once $this->tests_dir . '/framework/class-wc-mock-wc-object-query.php'; + require_once $this->tests_dir . '/framework/class-wc-mock-payment-gateway.php'; + require_once $this->tests_dir . '/framework/class-wc-mock-enhanced-payment-gateway.php'; + require_once $this->tests_dir . '/framework/class-wc-payment-token-stub.php'; + require_once $this->tests_dir . '/framework/vendor/class-wp-test-spy-rest-server.php'; + + // test cases. + require_once $this->tests_dir . '/includes/wp-http-testcase.php'; + require_once $this->tests_dir . '/framework/class-wc-unit-test-case.php'; + require_once $this->tests_dir . '/framework/class-wc-api-unit-test-case.php'; + require_once $this->tests_dir . '/framework/class-wc-rest-unit-test-case.php'; + + // Helpers. + require_once $this->tests_dir . '/framework/helpers/class-wc-helper-product.php'; + require_once $this->tests_dir . '/framework/helpers/class-wc-helper-coupon.php'; + require_once $this->tests_dir . '/framework/helpers/class-wc-helper-fee.php'; + require_once $this->tests_dir . '/framework/helpers/class-wc-helper-shipping.php'; + require_once $this->tests_dir . '/framework/helpers/class-wc-helper-customer.php'; + require_once $this->tests_dir . '/framework/helpers/class-wc-helper-order.php'; + require_once $this->tests_dir . '/framework/helpers/class-wc-helper-shipping-zones.php'; + require_once $this->tests_dir . '/framework/helpers/class-wc-helper-payment-token.php'; + require_once $this->tests_dir . '/framework/helpers/class-wc-helper-settings.php'; + require_once $this->tests_dir . '/framework/helpers/class-wc-helper-reports.php'; + require_once $this->tests_dir . '/framework/helpers/class-wc-helper-admin-notes.php'; + require_once $this->tests_dir . '/framework/helpers/class-wc-test-action-queue.php'; + require_once $this->tests_dir . '/framework/helpers/class-wc-helper-queue.php'; + + // Traits. + require_once $this->tests_dir . '/framework/traits/trait-wc-rest-api-complex-meta.php'; + } + + /** + * Use the `development` features for testing. + * + * @param array $flags Existing feature flags. + * @return array Filtered feature flags. + */ + public function add_development_features( $flags ) { + $config = json_decode( file_get_contents( $this->plugin_dir . '/client/admin/config/development.json' ) ); // @codingStandardsIgnoreLine. + foreach ( $config->features as $feature => $bool ) { + $flags[ $feature ] = $bool; + } + return $flags; + } + + /** + * Get the single class instance. + * + * @since 2.2 + * @return WC_Unit_Tests_Bootstrap + */ + public static function instance() { + if ( is_null( self::$instance ) ) { + self::$instance = new self(); + } + + return self::$instance; + } +} + +WC_Unit_Tests_Bootstrap::instance(); diff --git a/plugins/woocommerce/tests/legacy/classes-with-mockable-static-methods.php b/plugins/woocommerce/tests/legacy/classes-with-mockable-static-methods.php new file mode 100644 index 00000000000..ad0669fdb7d --- /dev/null +++ b/plugins/woocommerce/tests/legacy/classes-with-mockable-static-methods.php @@ -0,0 +1,17 @@ + + + +

    Hello World!

    + diff --git a/tests/legacy/data/sample-woo-plugin.php b/plugins/woocommerce/tests/legacy/data/sample-woo-plugin.php similarity index 100% rename from tests/legacy/data/sample-woo-plugin.php rename to plugins/woocommerce/tests/legacy/data/sample-woo-plugin.php diff --git a/plugins/woocommerce/tests/legacy/framework/class-wc-api-unit-test-case.php b/plugins/woocommerce/tests/legacy/framework/class-wc-api-unit-test-case.php new file mode 100644 index 00000000000..587c663a72a --- /dev/null +++ b/plugins/woocommerce/tests/legacy/framework/class-wc-api-unit-test-case.php @@ -0,0 +1,83 @@ +api->includes(); + + // set user. + $this->user_id = $this->login_as_role( 'shop_manager' ); + + // this isn't used, but it causes a warning unless set. + $_SERVER['REQUEST_METHOD'] = null; + + // mock the API server to prevent headers from being sent. + $this->mock_server = $this->getMockBuilder( 'WC_API_Server' )->setMethods( array( 'header' ) )->disableOriginalConstructor()->getMock(); + + WC()->api->register_resources( $this->mock_server ); + } + + /** + * Assert the given response is an API error with a specific code and status. + * + * @since 2.2 + * @param string $code error code, e.g. `woocommerce_api_user_cannot_read_orders_count`. + * @param int|null $status HTTP status code associated with error, e.g. 400. + * @param WP_Error $response Response to assert. + * @param string $message optional message to render when assertion fails. + */ + public function assertHasAPIError( $code, $status, $response, $message = '' ) { + + $this->assertWPError( $response, $message ); + + // code. + $this->assertEquals( $code, $response->get_error_code(), $message ); + + // status. + $data = $response->get_error_data(); + + $this->assertArrayHasKey( 'status', $data, $message ); + $this->assertEquals( $status, $data['status'], $message ); + } + + + /** + * Disable the given capability for the current user, used for testing. + * permission checking. + * + * @since 2.2 + * @param string $capability e.g. `read_private_shop_orders`. + */ + protected function disable_capability( $capability ) { + + $user = wp_get_current_user(); + $user->add_cap( $capability, false ); + + // flush capabilities, see https://core.trac.wordpress.org/ticket/28374. + $user->get_role_caps(); + $user->update_user_level_from_caps(); + } +} diff --git a/tests/legacy/framework/class-wc-dummy-data-store.php b/plugins/woocommerce/tests/legacy/framework/class-wc-dummy-data-store.php similarity index 100% rename from tests/legacy/framework/class-wc-dummy-data-store.php rename to plugins/woocommerce/tests/legacy/framework/class-wc-dummy-data-store.php diff --git a/plugins/woocommerce/tests/legacy/framework/class-wc-mock-enhanced-payment-gateway.php b/plugins/woocommerce/tests/legacy/framework/class-wc-mock-enhanced-payment-gateway.php new file mode 100644 index 00000000000..f1a56497c95 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/framework/class-wc-mock-enhanced-payment-gateway.php @@ -0,0 +1,102 @@ +enabled = 'yes'; + $this->id = 'mock-enhanced'; + $this->has_fields = false; + $this->method_title = 'Mock Enhanced Gateway'; + $this->method_description = 'Mock Enhanced Gateway for unit tests'; + + // Load the settings. + $this->init_form_fields(); + $this->init_settings(); + } + + /** + * Initialise Gateway Settings Form Fields. + */ + public function init_form_fields() { + $this->form_fields = array( + 'enabled' => array( + 'title' => '', + 'type' => 'checkbox', + 'label' => '', + 'default' => 'yes', + ), + 'api_key' => array( + 'title' => __( 'API Key', 'woocommerce-admin' ), + 'type' => 'text', + 'default' => '', + ), + ); + } + + /** + * Determine if the gateway requires further setup. + */ + public function needs_setup() { + $settings = get_option( 'woocommerce_mock-enhanced_settings', array() ); + return ! empty( $settings['api_key'] ); + } + + /** + * Get post install script handles. + * + * @return array + */ + public function get_post_install_script_handles() { + wp_register_script( + 'post-install-script', + 'post-install-script.js', + array(), + '1.0.0', + true + ); + + return array( + 'post-install-script', + 'unregistered-handle', + ); + } + + /** + * Get connection URL. + * + * @param string $return_url Return URL. + * @return string + */ + public function get_connection_url( $return_url ) { + return 'http://testconnection.com?return=' . $return_url; + } + + /** + * Get setup help text. + * + * @return string + */ + public function get_setup_help_text() { + return 'Test help text.'; + } + + /** + * Get required settings keys. + * + * @return array + */ + public function get_required_settings_keys() { + return array( 'api_key' ); + } +} + diff --git a/tests/legacy/framework/class-wc-mock-payment-gateway.php b/plugins/woocommerce/tests/legacy/framework/class-wc-mock-payment-gateway.php similarity index 100% rename from tests/legacy/framework/class-wc-mock-payment-gateway.php rename to plugins/woocommerce/tests/legacy/framework/class-wc-mock-payment-gateway.php diff --git a/tests/legacy/framework/class-wc-mock-session-handler.php b/plugins/woocommerce/tests/legacy/framework/class-wc-mock-session-handler.php similarity index 100% rename from tests/legacy/framework/class-wc-mock-session-handler.php rename to plugins/woocommerce/tests/legacy/framework/class-wc-mock-session-handler.php diff --git a/tests/legacy/framework/class-wc-mock-wc-data.php b/plugins/woocommerce/tests/legacy/framework/class-wc-mock-wc-data.php similarity index 100% rename from tests/legacy/framework/class-wc-mock-wc-data.php rename to plugins/woocommerce/tests/legacy/framework/class-wc-mock-wc-data.php diff --git a/tests/legacy/framework/class-wc-mock-wc-object-query.php b/plugins/woocommerce/tests/legacy/framework/class-wc-mock-wc-object-query.php similarity index 100% rename from tests/legacy/framework/class-wc-mock-wc-object-query.php rename to plugins/woocommerce/tests/legacy/framework/class-wc-mock-wc-object-query.php diff --git a/tests/legacy/framework/class-wc-payment-token-stub.php b/plugins/woocommerce/tests/legacy/framework/class-wc-payment-token-stub.php similarity index 100% rename from tests/legacy/framework/class-wc-payment-token-stub.php rename to plugins/woocommerce/tests/legacy/framework/class-wc-payment-token-stub.php diff --git a/plugins/woocommerce/tests/legacy/framework/class-wc-rest-unit-test-case.php b/plugins/woocommerce/tests/legacy/framework/class-wc-rest-unit-test-case.php new file mode 100644 index 00000000000..df7eec8f406 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/framework/class-wc-rest-unit-test-case.php @@ -0,0 +1,106 @@ +server = $wp_rest_server; + do_action( 'rest_api_init' ); + + // Reset payment gateways. + $gateways = WC_Payment_Gateways::instance(); + $gateways->payment_gateways = array(); + $gateways->init(); + } + + /** + * Unset the server. + */ + public function tearDown(): void { + parent::tearDown(); + global $wp_rest_server; + unset( $this->server ); + $wp_rest_server = null; + } + + /** + * Perform a REST request. + * + * @param string $url The endpopint url, if it doesn't start with '/' it'll be prepended with '/wc/v3/'. + * @param string $verb HTTP verb for the request, default is GET. + * @param array|null $body_params Body parameters for the request, null if none are required. + * @param array|null $query_params Query string parameters for the request, null if none are required. + * @return array Result from the request. + */ + public function do_rest_request( $url, $verb = 'GET', $body_params = null, $query_params = null ) { + if ( '/' !== $url[0] ) { + $url = '/wc/v3/' . $url; + } + + $request = new WP_REST_Request( $verb, $url ); + if ( ! is_null( $query_params ) ) { + $request->set_query_params( $query_params ); + } + if ( ! is_null( $body_params ) ) { + $request->set_body_params( $body_params ); + } + + return $this->server->dispatch( $request ); + } + + /** + * Perform a GET REST request. + * + * @param string $url The endpopint url, if it doesn't start with '/' it'll be prepended with '/wc/v3/'. + * @param array|null $query_params Query string parameters for the request, null if none are required. + * @return WP_REST_Response The response for the request. + */ + public function do_rest_get_request( $url, $query_params = null ) { + return $this->do_rest_request( $url, 'GET', null, $query_params ); + } + + /** + * Perform a POST REST request. + * + * @param string $url The endpopint url, if it doesn't start with '/' it'll be prepended with '/wc/v3/'. + * @param array|null $body_params Body parameters for the request, null if none are required. + * @param array|null $query_params Query string parameters for the request, null if none are required. + * @return array Result from the request. + */ + public function do_rest_post_request( $url, $body_params = null, $query_params = null ) { + return $this->do_rest_request( $url, 'POST', $body_params, $query_params ); + } + + /** + * Perform a PUT REST request. + * + * @param string $url The endpopint url, if it doesn't start with '/' it'll be prepended with '/wc/v3/'. + * @param array|null $body_params Body parameters for the request, null if none are required. + * @param array|null $query_params Query string parameters for the request, null if none are required. + * @return array Result from the request. + */ + public function do_rest_put_request( $url, $body_params = null, $query_params = null ) { + return $this->do_rest_request( $url, 'PUT', $body_params, $query_params ); + } +} diff --git a/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php b/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php new file mode 100644 index 00000000000..debd7d242a0 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php @@ -0,0 +1,333 @@ + 0 ) { + self::$code_hacker_temporary_disables_requested++; + } + } + + /** + * Decrease the count of Code Hacker disable requests, and effectively re-enable it if the count reaches zero. + * Does nothing if the count is already zero. + */ + protected static function reenable_code_hacker() { + if ( self::$code_hacker_temporary_disables_requested > 0 ) { + self::$code_hacker_temporary_disables_requested--; + if ( 0 === self::$code_hacker_temporary_disables_requested ) { + CodeHacker::enable(); + } + } + } + + /** + * Setup test case. + * + * @since 2.2 + */ + public function setUp(): void { + + parent::setUp(); + + // Add custom factories. + $this->factory = new WC_Unit_Test_Factory(); + + // Setup mock WC session handler. + add_filter( 'woocommerce_session_handler', array( $this, 'set_mock_session_handler' ) ); + + $this->setOutputCallback( array( $this, 'filter_output' ) ); + + // Register post types before each test. + WC_Post_types::register_post_types(); + WC_Post_types::register_taxonomies(); + + CodeHacker::reset_hacks(); + + // Reset the instance of MockableLegacyProxy that was registered during bootstrap, + // in order to start the test in a clean state (without anything mocked). + wc_get_container()->get( LegacyProxy::class )->reset(); + } + + /** + * Set up class unit test. + * + * @since 3.5.0 + */ + public static function setUpBeforeClass(): void { + parent::setUpBeforeClass(); + + // Terms are deleted in WP_UnitTestCase::tearDownAfterClass, then e.g. Uncategorized product_cat is missing. + WC_Install::create_terms(); + } + + /** + * Mock the WC session using the abstract class as cookies are not available. + * during tests. + * + * @since 2.2 + * @return string The $output string, sans newlines and tabs. + */ + public function set_mock_session_handler() { + return 'WC_Mock_Session_Handler'; + } + + /** + * Strip newlines and tabs when using expectedOutputString() as otherwise. + * the most template-related tests will fail due to indentation/alignment in. + * the template not matching the sample strings set in the tests. + * + * @since 2.2 + * + * @param string $output The captured output. + * @return string The $output string, sans newlines and tabs. + */ + public function filter_output( $output ) { + + $output = preg_replace( '/[\n]+/S', '', $output ); + $output = preg_replace( '/[\t]+/S', '', $output ); + + return $output; + } + + /** + * Throws an exception with an optional message and code. + * + * Note: can't use `throwException` as that's reserved. + * + * @since 3.3-dev + * @param string $message Optional. The exception message. Default is empty. + * @param int $code Optional. The exception code. Default is empty. + * @throws Exception Containing the given message and code. + */ + public function throwAnException( $message = null, $code = null ) { + $message = $message ? $message : "We're all doomed!"; + throw new Exception( $message, $code ); + } + + /** + * Copies a file, temporarily disabling the code hacker. + * Use this instead of "copy" in tests for compatibility with the code hacker. + * + * TODO: Investigate why invoking "copy" within a test with the code hacker active causes the test to fail. + * + * @param string $source Path to the source file. + * @param string $dest The destination path. + * @return bool true on success or false on failure. + */ + public static function file_copy( $source, $dest ) { + self::disable_code_hacker(); + $result = copy( $source, $dest ); + self::reenable_code_hacker(); + + return $result; + } + + /** + * Create a new user in a given role and set it as the current user. + * + * @param string $role The role for the user to be created. + * @return int The id of the user created. + */ + public function login_as_role( $role ) { + $user_id = $this->factory->user->create( array( 'role' => $role ) ); + wp_set_current_user( $user_id ); + return $user_id; + } + + /** + * Create a new administrator user and set it as the current user. + * + * @return int The id of the user created. + */ + public function login_as_administrator() { + return $this->login_as_role( 'administrator' ); + } + + /** + * Get an instance of a class that has been registered in the dependency injection container. + * To get an instance of a legacy class (such as the ones in the 'íncludes' directory) use + * 'get_legacy_instance_of' instead. + * + * @param string $class_name The class name to get an instance of. + * + * @return mixed The instance. + */ + public function get_instance_of( string $class_name ) { + return wc_get_container()->get( $class_name ); + } + + /** + * Get an instance of legacy class (such as the ones in the 'íncludes' directory). + * To get an instance of a class registered in the dependency injection container use 'get_instance_of' instead. + * + * @param string $class_name The class name to get an instance of. + * + * @return mixed The instance. + */ + public function get_legacy_instance_of( string $class_name ) { + return wc_get_container()->get( LegacyProxy::class )->get_instance_of( $class_name ); + } + + /** + * Reset all the cached resolutions in the dependency injection container, so any further "get" + * for shared definitions will generate the instance again. + * This may be needed when registering mocks for already resolved shared classes. + */ + public function reset_container_resolutions() { + wc_get_container()->reset_all_resolved(); + } + + /** + * Reset the mock legacy proxy class so that all the registered mocks are unregistered. + */ + public function reset_legacy_proxy_mocks() { + wc_get_container()->get( LegacyProxy::class )->reset(); + } + + /** + * Register the function mocks to use in the mockable LegacyProxy. + * + * @param array $mocks An associative array where keys are function names and values are function replacement callbacks. + * + * @throws \Exception Invalid parameter. + */ + public function register_legacy_proxy_function_mocks( array $mocks ) { + wc_get_container()->get( LegacyProxy::class )->register_function_mocks( $mocks ); + } + + /** + * Register the static method mocks to use in the mockable LegacyProxy. + * + * @param array $mocks An associative array where keys are class names and values are associative arrays, in which keys are method names and values are method replacement callbacks. + * + * @throws \Exception Invalid parameter. + */ + public function register_legacy_proxy_static_mocks( array $mocks ) { + wc_get_container()->get( LegacyProxy::class )->register_static_mocks( $mocks ); + } + + /** + * Register the class mocks to use in the mockable LegacyProxy. + * + * @param array $mocks An associative array where keys are class names and values are either factory callbacks (optionally with a $class_name argument) or objects. + * + * @throws \Exception Invalid parameter. + */ + public function register_legacy_proxy_class_mocks( array $mocks ) { + wc_get_container()->get( LegacyProxy::class )->register_class_mocks( $mocks ); + } + + /** + * Asserts that a certain callable output is equivalent to a given piece of HTML. + * + * "Equivalent" means that the string representations of the HTML pieces are equal + * except for line breaks, tabs and redundant whitespace. + * + * @param string $expected The expected HTML. + * @param callable $callable The callable that is supposed to output the expected HTML. + * @param string $message Optional error message to display if the assertion fails. + */ + protected function assertOutputsHTML( $expected, $callable, $message = '' ) { + $actual = $this->capture_output_from( $callable ); + $this->assertEqualsHTML( $expected, $actual, $message ); + } + + /** + * Asserts that two pieces of HTML are equivalent. + * + * "Equivalent" means that the string representations of the HTML pieces are equal + * except for line breaks, tabs and redundant whitespace. + * + * @param string $expected The expected HTML. + * @param string $actual The HTML that is supposed to be equivalent to the expected one. + * @param string $message Optional error message to display if the assertion fails. + */ + protected function assertEqualsHTML( $expected, $actual, $message = '' ) { + $this->assertEquals( $this->normalize_html( $expected ), $this->normalize_html( $actual ), $message ); + } + + /** + * Normalizes a block of HTML. + * Line breaks, tabs and redundand whitespaces are removed. + * + * @param string $html The block of HTML to normalize. + * + * @return string The normalized block. + */ + protected function normalize_html( $html ) { + $html = $this->filter_output( $html ); + $html = str_replace( '&', '&', $html ); + $html = preg_replace( '/> +<', $html ); + + $doc = new DomDocument(); + $doc->preserveWhiteSpace = false; //phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $doc->loadHTML( $html ); + + return $doc->saveHTML(); + } + + /** + * Executes a callable, captures its output and returns it. + * + * @param callable $callable Callable to execute. + * @param mixed ...$params Parameters to pass to the callable as arguments. + * + * @return false|string The output generated by the callable, or false if there is an error. + */ + protected function capture_output_from( $callable, ...$params ) { + ob_start(); + call_user_func( $callable, ...$params ); + $output = ob_get_contents(); + ob_end_clean(); + return $output; + } + + /** + * Asserts that a variable is of type int. + * TODO: After upgrading to PHPUnit 8 or newer, remove this method and replace calls with PHPUnit's built-in 'assertIsInt'. + * + * @param mixed $actual The value to check. + * @param mixed $message Error message to use if the assertion fails. + * @return bool mixed True if the value is of integer type, false otherwise. + */ + public static function assertIsInteger( $actual, $message = '' ) { + return self::assertIsInt( $actual, $message ); + } +} diff --git a/tests/legacy/framework/class-wc-unit-test-factory.php b/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-factory.php similarity index 100% rename from tests/legacy/framework/class-wc-unit-test-factory.php rename to plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-factory.php diff --git a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-admin-notes.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-admin-notes.php new file mode 100644 index 00000000000..66f12f2a352 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-admin-notes.php @@ -0,0 +1,156 @@ +query( "TRUNCATE TABLE {$wpdb->prefix}wc_admin_notes" ); // @codingStandardsIgnoreLine. + $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_admin_note_actions" ); // @codingStandardsIgnoreLine. + } + + /** + * Create four notes that we can use for notes REST API tests + */ + public static function add_notes_for_tests() { + $data_store = WC_Data_Store::load( 'admin-note' ); + + $note_1 = new Note(); + $note_1->set_title( 'PHPUNIT_TEST_NOTE_1_TITLE' ); + $note_1->set_content( 'PHPUNIT_TEST_NOTE_1_CONTENT' ); + $note_1->set_content_data( (object) array( 'amount' => 1.23 ) ); + $note_1->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); + $note_1->set_name( 'PHPUNIT_TEST_NOTE_NAME' ); + $note_1->set_source( 'PHPUNIT_TEST' ); + $note_1->set_is_snoozable( false ); + $note_1->set_layout( 'plain' ); + $note_1->set_image( '' ); + $note_1->add_action( + 'PHPUNIT_TEST_NOTE_1_ACTION_1_SLUG', + 'PHPUNIT_TEST_NOTE_1_ACTION_1_LABEL', + '?s=PHPUNIT_TEST_NOTE_1_ACTION_1_URL' + ); + $note_1->add_action( + 'PHPUNIT_TEST_NOTE_1_ACTION_2_SLUG', + 'PHPUNIT_TEST_NOTE_1_ACTION_2_LABEL', + '?s=PHPUNIT_TEST_NOTE_1_ACTION_2_URL' + ); + $note_1->save(); + + $note_2 = new Note(); + $note_2->set_title( 'PHPUNIT_TEST_NOTE_2_TITLE' ); + $note_2->set_content( 'PHPUNIT_TEST_NOTE_2_CONTENT' ); + $note_2->set_content_data( (object) array( 'amount' => 4.56 ) ); + $note_2->set_type( Note::E_WC_ADMIN_NOTE_WARNING ); + $note_2->set_name( 'PHPUNIT_TEST_NOTE_NAME' ); + $note_2->set_source( 'PHPUNIT_TEST' ); + $note_2->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED ); + $note_2->set_is_snoozable( true ); + $note_2->set_layout( 'banner' ); + $note_2->set_image( 'https://an-image.jpg' ); + // This note has no actions. + $note_2->save(); + + $note_3 = new Note(); + $note_3->set_title( 'PHPUNIT_TEST_NOTE_3_TITLE' ); + $note_3->set_content( 'PHPUNIT_TEST_NOTE_3_CONTENT' ); + $note_3->set_content_data( (object) array( 'amount' => 7.89 ) ); + $note_3->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); + $note_3->set_name( 'PHPUNIT_TEST_NOTE_NAME' ); + $note_3->set_source( 'PHPUNIT_TEST' ); + $note_3->set_status( Note::E_WC_ADMIN_NOTE_SNOOZED ); + $note_3->set_date_reminder( time() - HOUR_IN_SECONDS ); + $note_3->set_layout( 'thumbnail' ); + $note_3->set_image( 'https://an-image.jpg' ); + // This note has no actions. + $note_3->save(); + + $note_4 = new Note(); + $note_4->set_title( 'PHPUNIT_TEST_NOTE_4_TITLE' ); + $note_4->set_content( 'PHPUNIT_TEST_NOTE_4_CONTENT' ); + $note_4->set_content_data( (object) array( 'amount' => 1.23 ) ); + $note_4->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); + $note_4->set_name( 'PHPUNIT_TEST_NOTE_NAME' ); + $note_4->set_source( 'PHPUNIT_TEST' ); + $note_4->set_is_snoozable( false ); + $note_4->set_layout( 'plain' ); + $note_4->set_image( '' ); + $note_4->add_action( + 'PHPUNIT_TEST_NOTE_4_ACTION_1_SLUG', + 'PHPUNIT_TEST_NOTE_4_ACTION_1_LABEL', + '?s=PHPUNIT_TEST_NOTE_4_ACTION_1_URL' + ); + $note_4->add_action( + 'PHPUNIT_TEST_NOTE_4_ACTION_2_SLUG', + 'PHPUNIT_TEST_NOTE_4_ACTION_2_LABEL', + '?s=PHPUNIT_TEST_NOTE_4_ACTION_2_URL' + ); + $note_4->save(); + + } + + /** + * Create a note that we can use for email notes tests + */ + public static function add_email_notes_for_test() { + $data_store = WC_Data_Store::load( 'admin-note' ); + + $note_5 = new Note(); + $note_5->set_title( 'PHPUNIT_TEST_NOTE_5_TITLE' ); + $note_5->set_content( 'PHPUNIT_TEST_NOTE_5_CONTENT' ); + $additional_data = array( + 'heading' => 'PHPUNIT_TEST_EMAIL_HEADING', + 'role' => 'administrator', + 'template_html' => 'PHPUNIT_TEST_EMAIL_HTML_TEMPLATE', + 'template_plain' => 'PHPUNIT_TEST_EMAIL_HTML_PLAIN', + ); + $note_5->set_content_data( (object) $additional_data ); + $note_5->set_type( Note::E_WC_ADMIN_NOTE_EMAIL ); + $note_5->set_name( 'PHPUNIT_TEST_NOTE_NAME' ); + $note_5->set_source( 'PHPUNIT_TEST' ); + $note_5->set_is_snoozable( false ); + $note_5->set_layout( 'plain' ); + $note_5->set_image( '' ); + $note_5->add_action( + 'PHPUNIT_TEST_NOTE_5_ACTION_SLUG', + 'PHPUNIT_TEST_NOTE_5_ACTION_LABEL', + '?s=PHPUNIT_TEST_NOTE_5_ACTION_URL' + ); + $note_5->save(); + } + + /** + * Create a note that we can use for tests on `name` related filters + * + * @param string $name The name of the new note. + */ + public static function add_note_for_test( string $name = 'default_name' ) { + $data_store = WC_Data_Store::load( 'admin-note' ); + + $note = new Note(); + $note->set_title( 'PHPUNIT_TEST_NOTE_TITLE' ); + $note->set_content( 'PHPUNIT_TEST_NOTE_CONTENT' ); + + $note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING ); + $note->set_name( $name ); + $note->set_source( 'PHPUNIT_TEST' ); + $note->set_is_snoozable( false ); + $note->set_layout( 'plain' ); + $note->set_image( '' ); + $note->save(); + } +} diff --git a/tests/legacy/framework/helpers/class-wc-helper-coupon.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-coupon.php similarity index 100% rename from tests/legacy/framework/helpers/class-wc-helper-coupon.php rename to plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-coupon.php diff --git a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-customer.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-customer.php new file mode 100644 index 00000000000..c814ac104d7 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-customer.php @@ -0,0 +1,129 @@ + 0, + 'date_modified' => null, + 'country' => 'US', + 'state' => 'CA', + 'postcode' => '94110', + 'city' => 'San Francisco', + 'address' => '123 South Street', + 'address_2' => 'Apt 1', + 'shipping_country' => 'US', + 'shipping_state' => 'CA', + 'shipping_postcode' => '94110', + 'shipping_city' => 'San Francisco', + 'shipping_address' => '123 South Street', + 'shipping_address_2' => 'Apt 1', + 'is_vat_exempt' => false, + 'calculated_shipping' => false, + ); + + self::set_customer_details( $customer_data ); + + $customer = new WC_Customer( 0, true ); + + return $customer; + } + + /** + * Creates a customer in the tests DB. + */ + public static function create_customer( $username = 'testcustomer', $password = 'hunter2', $email = 'test@woo.local' ) { + $customer = new WC_Customer(); + $customer->set_billing_country( 'US' ); + $customer->set_first_name( 'Justin' ); + $customer->set_billing_state( 'CA' ); + $customer->set_billing_postcode( '94110' ); + $customer->set_billing_city( 'San Francisco' ); + $customer->set_billing_address( '123 South Street' ); + $customer->set_billing_address_2( 'Apt 1' ); + $customer->set_shipping_country( 'US' ); + $customer->set_shipping_state( 'CA' ); + $customer->set_shipping_postcode( '94110' ); + $customer->set_shipping_city( 'San Francisco' ); + $customer->set_shipping_address( '123 South Street' ); + $customer->set_shipping_address_2( 'Apt 1' ); + $customer->set_username( $username ); + $customer->set_password( $password ); + $customer->set_email( $email ); + $customer->save(); + return $customer; + } + + /** + * Get the expected output for the store's base location settings. + * + * @return array + */ + public static function get_expected_store_location() { + return array( 'US', 'CA', '', '' ); + } + + /** + * Get the customer's shipping and billing info from the session. + * + * @return array + */ + public static function get_customer_details() { + return WC()->session->get( 'customer' ); + } + + /** + * Get the user's chosen shipping method. + * + * @return array + */ + public static function get_chosen_shipping_methods() { + return WC()->session->get( 'chosen_shipping_methods' ); + } + + /** + * Get the "Tax Based On" WooCommerce option. + * + * @return string base or billing + */ + public static function get_tax_based_on() { + return get_option( 'woocommerce_tax_based_on' ); + } + + /** + * Set the the current customer's billing details in the session. + * + * @param string $default_shipping_method Shipping Method slug + */ + public static function set_customer_details( $customer_details ) { + WC()->session->set( 'customer', array_map( 'strval', $customer_details ) ); + } + + /** + * Set the user's chosen shipping method. + * + * @param string $chosen_shipping_method Shipping Method slug + */ + public static function set_chosen_shipping_methods( $chosen_shipping_methods ) { + WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); + } + + /** + * Set the "Tax Based On" WooCommerce option. + * + * @param string $default_shipping_method Shipping Method slug + */ + public static function set_tax_based_on( $default_shipping_method ) { + update_option( 'woocommerce_tax_based_on', $default_shipping_method ); + } +} diff --git a/tests/legacy/framework/helpers/class-wc-helper-fee.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-fee.php similarity index 100% rename from tests/legacy/framework/helpers/class-wc-helper-fee.php rename to plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-fee.php diff --git a/tests/legacy/framework/helpers/class-wc-helper-order.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-order.php similarity index 100% rename from tests/legacy/framework/helpers/class-wc-helper-order.php rename to plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-order.php diff --git a/tests/legacy/framework/helpers/class-wc-helper-payment-token.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-payment-token.php similarity index 100% rename from tests/legacy/framework/helpers/class-wc-helper-payment-token.php rename to plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-payment-token.php diff --git a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-product.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-product.php new file mode 100644 index 00000000000..b8326bff71c --- /dev/null +++ b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-product.php @@ -0,0 +1,429 @@ +delete( true ); + } + } + + /** + * Create simple product. + * + * @since 2.3 + * @param bool $save Save or return object. + * @param array $props Properties to be set in the new product, as an associative array. + * @return WC_Product_Simple + */ + public static function create_simple_product( $save = true, $props = array() ) { + $product = new WC_Product_Simple(); + $default_props = + array( + 'name' => 'Dummy Product', + 'regular_price' => 10, + 'price' => 10, + 'sku' => 'DUMMY SKU', + 'manage_stock' => false, + 'tax_status' => 'taxable', + 'downloadable' => false, + 'virtual' => false, + 'stock_status' => 'instock', + 'weight' => '1.1', + ); + + $product->set_props( array_merge( $default_props, $props ) ); + + if ( $save ) { + $product->save(); + return wc_get_product( $product->get_id() ); + } else { + return $product; + } + } + + /** + * Create a downloadable product. + * + * @since 6.4.0 + * + * @param array $downloads An array of arrays (each containing a 'name' and 'file' key) or WC_Product_Download objects. + * @param bool $save Save or return object. + * + * @return WC_Product_Simple|false + */ + public static function create_downloadable_product( array $downloads = array(), $save = true ) { + $product = new WC_Product_Simple(); + $product->set_props( + array( + 'name' => 'Downloadable Product', + 'regular_price' => 10, + 'price' => 10, + 'manage_stock' => false, + 'tax_status' => 'taxable', + 'downloadable' => true, + 'virtual' => false, + 'stock_status' => 'instock', + ) + ); + + $product->set_downloads( $downloads ); + + if ( $save ) { + $product->save(); + return \wc_get_product( $product->get_id() ); + } else { + return $product; + } + } + + /** + * Create external product. + * + * @since 3.0.0 + * @return WC_Product_External + */ + public static function create_external_product() { + $product = new WC_Product_External(); + $product->set_props( + array( + 'name' => 'Dummy External Product', + 'regular_price' => 10, + 'sku' => 'DUMMY EXTERNAL SKU', + 'product_url' => 'http://woocommerce.com', + 'button_text' => 'Buy external product', + ) + ); + $product->save(); + + return wc_get_product( $product->get_id() ); + } + + /** + * Create grouped product. + * + * @since 3.0.0 + * @return WC_Product_Grouped + */ + public static function create_grouped_product() { + $simple_product_1 = self::create_simple_product(); + $simple_product_2 = self::create_simple_product(); + $product = new WC_Product_Grouped(); + $product->set_props( + array( + 'name' => 'Dummy Grouped Product', + 'sku' => 'DUMMY GROUPED SKU', + ) + ); + $product->set_children( array( $simple_product_1->get_id(), $simple_product_2->get_id() ) ); + $product->save(); + + return wc_get_product( $product->get_id() ); + } + + /** + * Create a dummy variation product or configure an existing product object with dummy data. + * + * + * @since 2.3 + * @param WC_Product_Variable|null $product Product object to configure, or null to create a new one. + * @return WC_Product_Variable + */ + public static function create_variation_product( $product = null ) { + $is_new_product = is_null( $product ); + if ( $is_new_product ) { + $product = new WC_Product_Variable(); + } + + $product->set_props( + array( + 'name' => 'Dummy Variable Product', + 'sku' => 'DUMMY VARIABLE SKU', + ) + ); + + $attributes = array(); + + $attributes[] = self::create_product_attribute_object( 'size', array( 'small', 'large', 'huge' ) ); + $attributes[] = self::create_product_attribute_object( 'colour', array( 'red', 'blue' ) ); + $attributes[] = self::create_product_attribute_object( 'number', array( '0', '1', '2' ) ); + + $product->set_attributes( $attributes ); + $product->save(); + + $variations = array(); + + $variations[] = self::create_product_variation_object( + $product->get_id(), + 'DUMMY SKU VARIABLE SMALL', + 10, + array( 'pa_size' => 'small' ) + ); + + $variations[] = self::create_product_variation_object( + $product->get_id(), + 'DUMMY SKU VARIABLE LARGE', + 15, + array( 'pa_size' => 'large' ) + ); + + $variations[] = self::create_product_variation_object( + $product->get_id(), + 'DUMMY SKU VARIABLE HUGE RED 0', + 16, + array( + 'pa_size' => 'huge', + 'pa_colour' => 'red', + 'pa_number' => '0', + ) + ); + + $variations[] = self::create_product_variation_object( + $product->get_id(), + 'DUMMY SKU VARIABLE HUGE RED 2', + 17, + array( + 'pa_size' => 'huge', + 'pa_colour' => 'red', + 'pa_number' => '2', + ) + ); + + $variations[] = self::create_product_variation_object( + $product->get_id(), + 'DUMMY SKU VARIABLE HUGE BLUE 2', + 18, + array( + 'pa_size' => 'huge', + 'pa_colour' => 'blue', + 'pa_number' => '2', + ) + ); + + $variations[] = self::create_product_variation_object( + $product->get_id(), + 'DUMMY SKU VARIABLE HUGE BLUE ANY NUMBER', + 19, + array( + 'pa_size' => 'huge', + 'pa_colour' => 'blue', + 'pa_number' => '', + ) + ); + + if ( $is_new_product ) { + return wc_get_product( $product->get_id() ); + } + + $variation_ids = array_map( + function( $variation ) { + return $variation->get_id(); + }, + $variations + ); + $product->set_children( $variation_ids ); + return $product; + } + + /** + * Creates an instance of WC_Product_Variation with the supplied parameters, optionally persisting it to the database. + * + * @param string $parent_id Parent product id. + * @param string $sku SKU for the variation. + * @param int $price Price of the variation. + * @param array $attributes Attributes that define the variation, e.g. ['pa_color'=>'red']. + * @param bool $save If true, the object will be saved to the database after being created and configured. + * + * @return WC_Product_Variation The created object. + */ + public static function create_product_variation_object( $parent_id, $sku, $price, $attributes, $save = true ) { + $variation = new WC_Product_Variation(); + $variation->set_props( + array( + 'parent_id' => $parent_id, + 'sku' => $sku, + 'regular_price' => $price, + ) + ); + $variation->set_attributes( $attributes ); + if ( $save ) { + $variation->save(); + } + return $variation; + } + + /** + * Creates an instance of WC_Product_Attribute with the supplied parameters. + * + * @param string $raw_name Attribute raw name (without 'pa_' prefix). + * @param array $terms Possible values for the attribute. + * + * @return WC_Product_Attribute The created attribute object. + */ + public static function create_product_attribute_object( $raw_name = 'size', $terms = array( 'small' ) ) { + $attribute = new WC_Product_Attribute(); + $attribute_data = self::create_attribute( $raw_name, $terms ); + $attribute->set_id( $attribute_data['attribute_id'] ); + $attribute->set_name( $attribute_data['attribute_taxonomy'] ); + $attribute->set_options( $attribute_data['term_ids'] ); + $attribute->set_position( 1 ); + $attribute->set_visible( true ); + $attribute->set_variation( true ); + return $attribute; + } + + /** + * Create a dummy attribute. + * + * @since 2.3 + * + * @param string $raw_name Name of attribute to create. + * @param array(string) $terms Terms to create for the attribute. + * @return array + */ + public static function create_attribute( $raw_name = 'size', $terms = array( 'small' ) ) { + global $wpdb, $wc_product_attributes; + + // Make sure caches are clean. + delete_transient( 'wc_attribute_taxonomies' ); + WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); + + // These are exported as labels, so convert the label to a name if possible first. + $attribute_labels = wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_label', 'attribute_name' ); + $attribute_name = array_search( $raw_name, $attribute_labels, true ); + + if ( ! $attribute_name ) { + $attribute_name = wc_sanitize_taxonomy_name( $raw_name ); + } + + $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute_name ); + + if ( ! $attribute_id ) { + $taxonomy_name = wc_attribute_taxonomy_name( $attribute_name ); + + // Degister taxonomy which other tests may have created... + unregister_taxonomy( $taxonomy_name ); + + $attribute_id = wc_create_attribute( + array( + 'name' => $raw_name, + 'slug' => $attribute_name, + 'type' => 'select', + 'order_by' => 'menu_order', + 'has_archives' => 0, + ) + ); + + // Register as taxonomy. + register_taxonomy( + $taxonomy_name, + apply_filters( 'woocommerce_taxonomy_objects_' . $taxonomy_name, array( 'product' ) ), + apply_filters( + 'woocommerce_taxonomy_args_' . $taxonomy_name, + array( + 'labels' => array( + 'name' => $raw_name, + ), + 'hierarchical' => false, + 'show_ui' => false, + 'query_var' => true, + 'rewrite' => false, + ) + ) + ); + + // Set product attributes global. + $wc_product_attributes = array(); + + foreach ( wc_get_attribute_taxonomies() as $taxonomy ) { + $wc_product_attributes[ wc_attribute_taxonomy_name( $taxonomy->attribute_name ) ] = $taxonomy; + } + } + + $attribute = wc_get_attribute( $attribute_id ); + $return = array( + 'attribute_name' => $attribute->name, + 'attribute_taxonomy' => $attribute->slug, + 'attribute_id' => $attribute_id, + 'term_ids' => array(), + ); + + foreach ( $terms as $term ) { + $result = term_exists( $term, $attribute->slug ); + + if ( ! $result ) { + $result = wp_insert_term( $term, $attribute->slug ); + $return['term_ids'][] = $result['term_id']; + } else { + $return['term_ids'][] = $result['term_id']; + } + } + + return $return; + } + + /** + * Delete an attribute. + * + * @param int $attribute_id ID to delete. + * + * @since 2.3 + */ + public static function delete_attribute( $attribute_id ) { + global $wpdb; + + $attribute_id = absint( $attribute_id ); + + $wpdb->query( + $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_id = %d", $attribute_id ) + ); + } + + /** + * Creates a new product review on a specific product. + * + * @since 3.0 + * @param int $product_id integer Product ID that the review is for. + * @param string $review_content string Content to use for the product review. + * @return integer Product Review ID. + */ + public static function create_product_review( $product_id, $review_content = 'Review content here' ) { + $data = array( + 'comment_post_ID' => $product_id, + 'comment_author' => 'admin', + 'comment_author_email' => 'woo@woo.local', + 'comment_author_url' => '', + 'comment_date' => '2016-01-01T11:11:11', + 'comment_content' => $review_content, + 'comment_approved' => 1, + 'comment_type' => 'review', + ); + return wp_insert_comment( $data ); + } + + /** + * A helper function for hooking into save_post during the test_product_meta_save_post test. + * @since 3.0.1 + * + * @param int $id ID to update. + */ + public static function save_post_test_update_meta_data_direct( $id ) { + update_post_meta( $id, '_test2', 'world' ); + } +} diff --git a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-queue.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-queue.php new file mode 100644 index 00000000000..a2f4e0acfb8 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-queue.php @@ -0,0 +1,63 @@ +queue()->search( + array( + 'per_page' => -1, + 'status' => 'pending', + 'claimed' => false, + ) + ); + + return $jobs; + } + + /** + * Run all pending queued actions. + * + * @return void + */ + public static function run_all_pending() { + $queue_runner = new ActionScheduler_QueueRunner(); + + while ( $jobs = self::get_all_pending() ) { + foreach ( $jobs as $job_id => $job ) { + $queue_runner->process_action( $job_id ); + } + } + } + + /** + * Cancel all pending actions. + * + * @return void + */ + public static function cancel_all_pending() { + // Force immediate hard delete for Action Scheduler < 3.0. + global $wpdb; + $wpdb->query( "DELETE FROM {$wpdb->posts} WHERE post_type = 'scheduled-action'" ); + + // Delete actions for Action Scheduler >= 3.0. + $store = ActionScheduler_Store::instance(); + + if ( is_callable( array( $store, 'cancel_actions_by_group' ) ) ) { + $store->cancel_actions_by_group( 'wc-admin-data' ); + } + } +} diff --git a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-reports.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-reports.php new file mode 100644 index 00000000000..cfb31cdd8a1 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-reports.php @@ -0,0 +1,26 @@ +query( 'DELETE FROM ' . \Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore::get_db_table_name() ); // @codingStandardsIgnoreLine. + $wpdb->query( 'DELETE FROM ' . \Automattic\WooCommerce\Admin\API\Reports\Products\DataStore::get_db_table_name() ); // @codingStandardsIgnoreLine. + $wpdb->query( 'DELETE FROM ' . \Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore::get_db_table_name() ); // @codingStandardsIgnoreLine. + $wpdb->query( 'DELETE FROM ' . \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore::get_db_table_name() ); // @codingStandardsIgnoreLine. + \Automattic\WooCommerce\Internal\Admin\CategoryLookup::instance()->regenerate(); + } +} diff --git a/tests/legacy/framework/helpers/class-wc-helper-settings.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-settings.php similarity index 100% rename from tests/legacy/framework/helpers/class-wc-helper-settings.php rename to plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-settings.php diff --git a/tests/legacy/framework/helpers/class-wc-helper-shipping-zones.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-shipping-zones.php similarity index 100% rename from tests/legacy/framework/helpers/class-wc-helper-shipping-zones.php rename to plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-shipping-zones.php diff --git a/tests/legacy/framework/helpers/class-wc-helper-shipping.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-shipping.php similarity index 100% rename from tests/legacy/framework/helpers/class-wc-helper-shipping.php rename to plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-shipping.php diff --git a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-test-action-queue.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-test-action-queue.php new file mode 100644 index 00000000000..a53c67f495c --- /dev/null +++ b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-test-action-queue.php @@ -0,0 +1,33 @@ +actions[] = compact( 'timestamp', 'hook', 'args', 'group' ); + return true; + } +} diff --git a/tests/legacy/framework/traits/trait-wc-rest-api-complex-meta.php b/plugins/woocommerce/tests/legacy/framework/traits/trait-wc-rest-api-complex-meta.php similarity index 100% rename from tests/legacy/framework/traits/trait-wc-rest-api-complex-meta.php rename to plugins/woocommerce/tests/legacy/framework/traits/trait-wc-rest-api-complex-meta.php diff --git a/tests/legacy/framework/vendor/class-wp-test-spy-rest-server.php b/plugins/woocommerce/tests/legacy/framework/vendor/class-wp-test-spy-rest-server.php similarity index 100% rename from tests/legacy/framework/vendor/class-wp-test-spy-rest-server.php rename to plugins/woocommerce/tests/legacy/framework/vendor/class-wp-test-spy-rest-server.php diff --git a/plugins/woocommerce/tests/legacy/includes/wp-http-testcase.php b/plugins/woocommerce/tests/legacy/includes/wp-http-testcase.php new file mode 100644 index 00000000000..d03277392e0 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/includes/wp-http-testcase.php @@ -0,0 +1,443 @@ + 1, + 'headers' => 1, + 'cookies' => 1, + 'body' => 1, + ); + + /** + * The directory the cache files are in. + * + * @since 1.1.0 + * + * @var string + */ + protected static $cache_dir; + + /** + * The cache group to use. + * + * @since 1.1.0 + * + * @var string + */ + protected static $cache_group = 'default'; + + /** + * The currently loaded cache. + * + * @since 1.1.0 + * + * @var array + */ + protected static $cache; + + /** + * Whether the cache has changed. + * + * @since 1.1.0 + * + * @var bool + */ + protected static $cache_changed; + + /** + * Whether to skip just the next cache hit and put the request through. + * + * When true, the cache won't be checked for the next request, but the response + * will still overwrite the existing cache. + * + * @since 1.2.0 + * + * @var bool + */ + protected $skip_cache_next = false; + + /** + * @since 1.3.0 + */ + public static function setUpBeforeClass(): void { + + if ( ! self::$did_init ) { + self::init(); + } + + parent::setUpBeforeClass(); + } + + /** + * @since 1.3.1 + */ + public static function tearDownAfterClass(): void { + + self::save_cache(); + + parent::tearDownAfterClass(); + } + + /** + * Set up for each test. + * + * @since 1.0.0 + */ + public function setUp(): void { + + parent::setUp(); + + $this->http_requests = array(); + + if ( ! empty( self::$host ) ) { + $this->http_responder = array( $this, 'route_request' ); + } + + add_filter( 'pre_http_request', array( $this, 'http_request_listner' ), 10, 3 ); + } + + /** + * Clean up the filters after each test. + * + * @since 1.0.0 + */ + public function tearDown(): void { + + parent::tearDown(); + + remove_filter( 'pre_http_request', array( $this, 'http_request_listner' ) ); + + $this->skip_cache_next = false; + } + + // + // Helpers. + // + + /** + * Mock responses to HTTP requests coming from WordPress. + * + * @since 1.0.0 + * + * @WordPress\filter pre_http_request Added by self::setUp(). + * + * @param mixed $preempt Response to the request, or false to not preempt it. + * @param array $request The request arguments. + * @param string $url The URL the request is being made to. + * + * @return mixed A response, or false. + */ + public function http_request_listner( $preempt, $request, $url ) { + + $this->http_requests[] = array( + 'url' => $url, + 'request' => $request, + ); + + if ( $this->http_responder ) { + $preempt = call_user_func( $this->http_responder, $request, $url ); + } + + return $preempt; + } + + /** + * Route a request through to a predefined host, with optional caching. + * + * @since 1.1.0 + * + * @param array $request The request to route. + * @param string $url The URL the request is for. + * + * @return array|bool|false|WP_Error The response. + */ + protected function route_request( $request, $url ) { + + // Check the cache. + $cache_key = $this->get_cache_key( $request, $url ); + $cached = $this->get_cached_response( $cache_key ); + + if ( $cached ) { + return $cached; + } + + // Get the URL host. + $host = parse_url( $url, PHP_URL_HOST ); + + // If the host is already correct, return false so the request continues. + if ( $host === self::$host ) { + return false; + } + + $url = str_replace( $host, self::$host, $url ); + + $response = wp_remote_request( $url, $request ); + + $this->cache_response( $cache_key, $response ); + + return $response; + } + + /** + * Get the cache key for a request. + * + * @since 1.1.0 + * + * @param array $request The request. + * @param string $url The URL the request is for. + * + * @return string|false The cache key for the request. False if not caching. + */ + protected function get_cache_key( $request, $url ) { + + if ( ! self::$use_caching ) { + return false; + } + + $request = array_intersect_key( $request, self::$cache_request_fields ); + + return md5( serialize( $request ) . $url ); + } + + /** + * Get the cached response to a request. + * + * @since 1.1.0 + * + * @param string $cache_key The cache key for the request. + * + * @return array|false The cached response, or false if none. + */ + protected function get_cached_response( $cache_key ) { + + if ( ! self::$use_caching ) { + return false; + } + + // If we're to skip the cache this time, return false. + if ( $this->skip_cache_next ) { + $this->skip_cache_next = false; + return false; + } + + if ( ! isset( self::$cache[ $cache_key ] ) ) { + return false; + } + + return self::$cache[ $cache_key ]; + } + + /** + * Save a response to the cache. + * + * @since 1.1.0 + * + * @param string $cache_key The cache key for the request. + * @param array $response The response. + */ + protected function cache_response( $cache_key, $response ) { + + if ( ! self::$use_caching ) { + return; + } + + self::$cache[ $cache_key ] = $response; + self::$cache_changed = true; + } + + // + // Static Functions. + // + + /** + * Initialize the class. + * + * @since 1.1.0 + */ + public static function init() { + + self::load_env( 'HOST' ); + self::load_env( 'USE_CACHING', true ); + + self::load_cache(); + + self::$did_init = true; + } + + /** + * Get an environment setting. + * + * @since 1.1.0 + * + * @param string $var The name of the setting to get. + * @param mixed $default The default value for this setting. + * + * @return mixed|null|string + */ + protected static function get_env( $var, $default = null ) { + + $value = getenv( 'WP_HTTP_TC_' . $var ); + + if ( false !== $value ) { + return $value; + } + + if ( ! Constants::is_defined( 'WP_HTTP_TC_' . $var ) ) { + return $default; + } + + return Constants::get_constant( 'WP_HTTP_TC_' . $var ); + } + + /** + * Get an environment setting and assign it to the corresponding property. + * + * @since 1.2.0 + * + * @param string $var The var name. + * @param bool $is_bool Whether this is a boolean property. + */ + protected static function load_env( $var, $is_bool = false ) { + + $property = strtolower( $var ); + + self::$$property = self::get_env( $var, self::$$property ); + + if ( $is_bool ) { + self::$$property = (bool) self::$$property; + } + } + + /** + * Load the cache if caching is in use. + * + * @since 1.1.0 + */ + protected static function load_cache() { + + if ( ! self::$use_caching ) { + return; + } + + $request_fields = self::get_env( 'CACHE_REQUEST_FIELDS' ); + + if ( null !== $request_fields ) { + self::$cache_request_fields = array_flip( + array_map( 'trim', explode( ',', $request_fields ) ) + ); + } + + self::load_env( 'CACHE_GROUP' ); + + self::$cache_dir = self::get_env( 'CACHE_DIR', dirname( __FILE__ ) ); + + $cache_file = self::$cache_dir . '/' . self::$cache_group; + + if ( ! file_exists( $cache_file ) ) { + return; + } + + $cache = file_get_contents( $cache_file ); + + self::$cache = unserialize( $cache ); + } + + /** + * Save the cache. + * + * @since 1.1.0 + */ + public static function save_cache() { + + if ( ! self::$cache_changed ) { + return; + } + + // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_file_put_contents + file_put_contents( + self::$cache_dir . '/' . self::$cache_group, + serialize( self::$cache ) + ); + } +} + +if ( ! Constants::is_defined( 'WP_HTTP_TC_NO_BACKPAT' ) ) { + abstract class WP_HTTP_UnitTestCase extends WP_HTTP_TestCase {} +} + +// EOF diff --git a/plugins/woocommerce/tests/legacy/mockable-functions.php b/plugins/woocommerce/tests/legacy/mockable-functions.php new file mode 100644 index 00000000000..974619882e0 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/mockable-functions.php @@ -0,0 +1,19 @@ +assertEquals( 'http://example.org?lost-password', wc_lostpassword_url() ); + } + + /** + * Test wc_customer_edit_account_url(). + * + * @since 3.3.0 + */ + public function test_wc_customer_edit_account_url() { + $this->assertEquals( 'http://example.org?edit-account', wc_customer_edit_account_url() ); + } + + /** + * Test wc_edit_address_i18n(). + * + * @since 3.3.0 + */ + public function test_wc_edit_address_i18n() { + // Should return the same result, since it's using en_US in Unit Tests. + $this->assertEquals( 'billing', wc_edit_address_i18n( 'billing' ) ); + $this->assertEquals( 'billing', wc_edit_address_i18n( 'billing', true ) ); + } + + /** + * Test wc_get_account_menu_items(). + * + * @since 2.6.0 + */ + public function test_wc_get_account_menu_items() { + $this->assertEquals( + array( + 'dashboard' => 'Dashboard', + 'orders' => 'Orders', + 'downloads' => 'Downloads', + 'edit-address' => 'Addresses', + 'edit-account' => 'Account details', + 'customer-logout' => 'Logout', + ), + wc_get_account_menu_items() + ); + } + + /** + * Test wc_get_account_menu_item_classes(). + * + * @since 3.3.0 + */ + public function test_wc_get_account_menu_item_classes() { + $this->assertEquals( 'woocommerce-MyAccount-navigation-link woocommerce-MyAccount-navigation-link--test', wc_get_account_menu_item_classes( 'test' ) ); + } + + /** + * Test wc_get_account_endpoint_url(). + * + * @since 3.3.0 + */ + public function test_wc_get_account_endpoint_url() { + $this->assertEquals( 'http://example.org?test', wc_get_account_endpoint_url( 'test' ) ); + } + + /** + * Test wc_get_account_orders_columns(). + * + * @since 2.6.0 + */ + public function test_wc_get_account_orders_columns() { + $this->assertEquals( + array( + 'order-number' => 'Order', + 'order-date' => 'Date', + 'order-status' => 'Status', + 'order-total' => 'Total', + 'order-actions' => 'Actions', + ), + wc_get_account_orders_columns() + ); + } + + /** + * Test wc_get_account_downloads_columns(). + * + * @since 2.6.0 + */ + public function test_wc_get_account_downloads_columns() { + $this->assertEquals( + array( + 'download-file' => 'Download', + 'download-remaining' => 'Downloads remaining', + 'download-expires' => 'Expires', + 'download-product' => 'Product', + ), + wc_get_account_downloads_columns() + ); + } + + /** + * Test wc_get_account_payment_methods_columns(). + * + * @since 3.3.0 + */ + public function test_wc_get_account_payment_methods_columns() { + $this->assertEquals( + array( + 'method' => 'Method', + 'expires' => 'Expires', + 'actions' => ' ', + ), + wc_get_account_payment_methods_columns() + ); + } + + /** + * Test wc_get_account_payment_methods_types(). + * + * @since 3.3.0 + */ + public function test_wc_get_account_payment_methods_types() { + $this->assertEquals( + array( + 'cc' => 'Credit card', + 'echeck' => 'eCheck', + ), + wc_get_account_payment_methods_types() + ); + } + + /** + * Test wc_get_account_orders_actions(). + * + * @since 3.3.0 + */ + public function test_wc_get_account_orders_actions() { + $order = WC_Helper_Order::create_order(); + + $this->assertEquals( + array( + 'view' => array( + 'url' => $order->get_view_order_url(), + 'name' => 'View', + ), + 'pay' => array( + 'url' => $order->get_checkout_payment_url(), + 'name' => 'Pay', + ), + 'cancel' => array( + 'url' => $order->get_cancel_order_url( wc_get_page_permalink( 'myaccount' ) ), + 'name' => 'Cancel', + ), + ), + wc_get_account_orders_actions( $order->get_id() ) + ); + + $order->delete( true ); + } + + /** + * Test wc_get_account_formatted_address(). + * + * @since 3.3.0 + */ + public function test_wc_get_account_formatted_address() { + $customer = WC_Helper_Customer::create_customer(); + + $this->assertEquals( '123 South Street
    Apt 1
    San Francisco, CA 94110', wc_get_account_formatted_address( 'billing', $customer->get_id() ) ); + + $customer->delete( true ); + } + + /** + * Test wc_get_account_saved_payment_methods_list(). + * + * @since 3.3.0 + */ + public function test_wc_get_account_saved_payment_methods_list() { + $customer = WC_Helper_Customer::create_customer(); + + $token = new WC_Payment_Token_CC(); + $token->set_token( '1234' ); + $token->set_gateway_id( 'bacs' ); + $token->set_card_type( 'mastercard' ); + $token->set_last4( '1234' ); + $token->set_expiry_month( '12' ); + $token->set_expiry_year( '2020' ); + $token->set_user_id( $customer->get_id() ); + $token->save(); + + $delete_url = wc_get_endpoint_url( 'delete-payment-method', $token->get_id() ); + $delete_url = wp_nonce_url( $delete_url, 'delete-payment-method-' . $token->get_id() ); + + $this->assertEquals( + array( + 'cc' => array( + array( + 'method' => array( + 'gateway' => 'bacs', + 'last4' => '1234', + 'brand' => 'Mastercard', + ), + 'expires' => '12/20', + 'is_default' => true, + 'actions' => array( + 'delete' => array( + 'url' => $delete_url, + 'name' => 'Delete', + ), + ), + ), + ), + ), + wc_get_account_saved_payment_methods_list( array(), $customer->get_id() ) + ); + + $customer->delete( true ); + $token->delete( true ); + } + + /** + * Test wc_get_account_saved_payment_methods_list_item_cc(). + * + * @since 3.3.0 + */ + public function test_wc_get_account_saved_payment_methods_list_item_cc() { + $token = new WC_Payment_Token_CC(); + $token->set_token( '1234' ); + $token->set_gateway_id( 'bacs' ); + $token->set_card_type( 'mastercard' ); + $token->set_last4( '1234' ); + $token->set_expiry_month( '12' ); + $token->set_expiry_year( '2020' ); + $token->save(); + + $this->assertEquals( + array( + 'method' => array( + 'last4' => '1234', + 'brand' => 'Mastercard', + ), + 'expires' => '12/20', + ), + wc_get_account_saved_payment_methods_list_item_cc( array(), $token ) + ); + + $token->delete( true ); + } + + /** + * Test wc_get_account_saved_payment_methods_list_item_echeck(). + * + * @since 3.3.0 + */ + public function test_wc_get_account_saved_payment_methods_list_item_echeck() { + $token = new WC_Payment_Token_ECheck(); + $token->set_token( '1234' ); + $token->set_gateway_id( 'bacs' ); + $token->set_last4( '1234' ); + $token->save(); + + $this->assertEquals( + array( + 'method' => array( + 'last4' => '1234', + 'brand' => 'eCheck', + ), + ), + wc_get_account_saved_payment_methods_list_item_echeck( array(), $token ) + ); + + $token->delete( true ); + } +} diff --git a/plugins/woocommerce/tests/legacy/unit-tests/account/permissions.php b/plugins/woocommerce/tests/legacy/unit-tests/account/permissions.php new file mode 100644 index 00000000000..3a2ec0355f6 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/account/permissions.php @@ -0,0 +1,69 @@ +get_id() ); + $this->assertEquals( true, current_user_can( 'pay_for_order', $order->get_id() ) ); + } + + /** + * Test that guest orders can be paid when not logged in. + */ + public function test_wc_guest_pay_guest_order() { + $order = WC_Helper_Order::create_order( 0 ); + $this->assertEquals( true, current_user_can( 'pay_for_order', $order->get_id() ) ); + } + + /** + * Test that a customer cannot pay another customer's order. + */ + public function test_wc_customer_cannot_pay_another_customer_order() { + $customer1 = WC_Helper_Customer::create_customer(); + $order = WC_Helper_Order::create_order( $customer1->get_id() ); + $customer2 = WC_Helper_Customer::create_customer( 'testcustomer2', 'woo', 'test2@local.woo' ); + wp_set_current_user( $customer2->get_id() ); + $this->assertEquals( false, current_user_can( 'pay_for_order', $order->get_id() ) ); + } + + /** + * Test that customer can pay their own order. + */ + public function test_wc_customer_can_pay_their_order() { + $customer = WC_Helper_Customer::create_customer(); + wp_set_current_user( $customer->get_id() ); + $order = WC_Helper_Order::create_order( $customer->get_id() ); + $this->assertEquals( true, current_user_can( 'pay_for_order', $order->get_id() ) ); + } + +} diff --git a/plugins/woocommerce/tests/legacy/unit-tests/admin/class-wc-tests-admin-dashboard.php b/plugins/woocommerce/tests/legacy/unit-tests/admin/class-wc-tests-admin-dashboard.php new file mode 100644 index 00000000000..56b0e4cd6ca --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/admin/class-wc-tests-admin-dashboard.php @@ -0,0 +1,120 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + + // Mock http request to performance endpoint. + add_filter( 'rest_pre_dispatch', array( $this, 'mock_rest_responses' ), 10, 3 ); + } + + /** + * Tear down. + */ + public function tearDown(): void { + parent::tearDown(); + remove_filter( 'rest_pre_dispatch', array( $this, 'mock_rest_responses' ), 10 ); + } + + /** + * Test: get_status_widget + */ + public function test_status_widget() { + wp_set_current_user( $this->user ); + $order = WC_Helper_Order::create_order(); + $order->set_status( 'completed' ); + $order->save(); + + $this->expectOutputRegex( '/98,765\.00/' ); + + ( new WC_Admin_Dashboard() )->status_widget(); + + $widget_output = $this->getActualOutput(); + + $this->assertRegExp( '/page\=wc-admin\&\#038\;path\=\%2Fanalytics\%2Frevenue/', $widget_output ); + $this->assertRegExp( '/page\=wc-admin\&\#038\;filter\=single_product/', $widget_output ); + $this->assertRegExp( '/page\=wc-admin\&\#038\;type\=lowstock/', $widget_output ); + $this->assertRegExp( '/page\=wc-admin\&\#038\;type\=outofstock/', $widget_output ); + } + + /** + * Test: get_status_widget with woo admin disabled. + */ + public function test_status_widget_with_woo_admin_disabled() { + wp_set_current_user( $this->user ); + $order = WC_Helper_Order::create_order(); + $order->set_status( 'completed' ); + $order->save(); + + add_filter( 'woocommerce_admin_disabled', '__return_true' ); + + $this->expectOutputRegex( '/50\.00 worth in the/' ); + + ( new WC_Admin_Dashboard() )->status_widget(); + + $widget_output = $this->getActualOutput(); + $this->assertRegExp( '/page\=wc-reports\&\#038\;tab\=orders\&\#038\;range\=month/', $widget_output ); + $this->assertRegExp( '/page\=wc-reports\&\#038\;tab\=orders\&\#038\;report\=sales_by_product/', $widget_output ); + $this->assertRegExp( '/page\=wc-reports\&\#038\;tab\=stock\&\#038\;report\=low_in_stock/', $widget_output ); + $this->assertRegExp( '/page\=wc-reports\&\#038\;tab\=stock\&\#038\;report\=out_of_stock/', $widget_output ); + + remove_filter( 'woocommerce_admin_disabled', '__return_true' ); + } + + /** + * Helper method to mock rest_do_request method. + * + * @param false $response Request arguments. + * @param WP_REST_Server $rest_server rest server class. + * @param WP_REST_Request $request incoming request. + * + * @return WP_REST_Response|false mocked response or false to let WP perform a regular request. + */ + public function mock_rest_responses( $response, $rest_server, $request ) { + if ( '/wc-analytics/reports/performance-indicators' === $request->get_route() ) { + $response = new WP_REST_Response( + array( + 'status' => 200, + ) + ); + $response->set_data( + array( + array( + 'chart' => 'net_revenue', + 'value' => 98765.0, + ), + ) + ); + } elseif ( '/wc-analytics/reports/revenue/stats' === $request->get_route() ) { + $response = new WP_REST_Response( + array( + 'status' => 200, + ) + ); + $response->set_data( + array( + 'intervals' => array(), + ) + ); + } + + return $response; + } +} diff --git a/tests/legacy/unit-tests/admin/class-wc-tests-admin-duplicate-product.php b/plugins/woocommerce/tests/legacy/unit-tests/admin/class-wc-tests-admin-duplicate-product.php similarity index 100% rename from tests/legacy/unit-tests/admin/class-wc-tests-admin-duplicate-product.php rename to plugins/woocommerce/tests/legacy/unit-tests/admin/class-wc-tests-admin-duplicate-product.php diff --git a/plugins/woocommerce/tests/legacy/unit-tests/admin/notes/class-wc-tests-notes-run-db-update.php b/plugins/woocommerce/tests/legacy/unit-tests/admin/notes/class-wc-tests-notes-run-db-update.php new file mode 100644 index 00000000000..df8411e96ac --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/admin/notes/class-wc-tests-notes-run-db-update.php @@ -0,0 +1,195 @@ +plugin_dir . '/includes/admin/wc-admin-functions.php'; + include_once WC_Unit_Tests_Bootstrap::instance()->plugin_dir . '/includes/admin/notes/class-wc-notes-run-db-update.php'; + + } + + /** + * Clean up before each test. + */ + public function setUp(): void { + if ( ! WC()->is_wc_admin_active() ) { + $this->markTestSkipped( 'WC Admin is not active on WP versions < 5.3' ); + return; + } + self::remove_db_update_notes(); + } + + /** + * Returns a list of note ids with name 'wc-update-db-reminder' from the database. + * + * @return array( int ) List of note ids with name 'wc-update-db-reminder'. + */ + private static function get_db_update_notes() { + $data_store = \WC_Data_Store::load( 'admin-note' ); + $note_ids = $data_store->get_notes_with_name( WC_Notes_Run_Db_Update::NOTE_NAME ); + return $note_ids; + } + + /** + * Removes all the notes with name 'wc-update-db-reminder' from the database. + */ + private static function remove_db_update_notes() { + $data_store = \WC_Data_Store::load( 'admin-note' ); + $note_ids = $data_store->get_notes_with_name( WC_Notes_Run_Db_Update::NOTE_NAME ); + foreach ( $note_ids as $note_id ) { + $note = new Note( $note_id ); + $data_store->delete( $note ); + } + } + + /** + * Creates a sample note with name 'wc-update-db-reminder'. + * + * @return int Newly create note's id. + */ + private static function create_db_update_note() { + $update_url = html_entity_decode( + wp_nonce_url( + add_query_arg( 'do_update_woocommerce', 'true', admin_url( 'admin.php?page=wc-settings' ) ), + 'wc_db_update', + 'wc_db_update_nonce' + ) + ); + + $note_actions = array( + array( + 'name' => 'update-db_run', + 'label' => __( 'Update WooCommerce Database', 'woocommerce' ), + 'url' => $update_url, + 'status' => 'unactioned', + 'primary' => true, + ), + array( + 'name' => 'update-db_learn-more', + 'label' => __( 'Learn more about updates', 'woocommerce' ), + 'url' => 'https://docs.woocommerce.com/document/how-to-update-woocommerce/', + 'status' => 'unactioned', + 'primary' => false, + ), + ); + + $note = new Note(); + + $note->set_title( 'WooCommerce database update required' ); + $note->set_content( 'To keep things running smoothly, we have to update your database to the newest version.' ); + $note->set_type( Note::E_WC_ADMIN_NOTE_UPDATE ); + $note->set_name( WC_Notes_Run_Db_Update::NOTE_NAME ); + $note->set_content_data( (object) array() ); + $note->set_source( 'woocommerce-core' ); + $note->set_status( Note::E_WC_ADMIN_NOTE_UNACTIONED ); + + // Set new actions. + $note->clear_actions(); + foreach ( $note_actions as $note_action ) { + $note->add_action( ...array_values( $note_action ) ); + } + + return $note->save(); + } + + /** + * No note should be created/exist if db version is equal to WC code version. + */ + public function test_noop_db_update_note() { + update_option( 'woocommerce_db_version', WC()->version ); + + // No notes initially. + $this->assertEquals( 0, count( self::get_db_update_notes() ), 'There should be no db update notes initially.' ); + + WC_Notes_Run_Db_Update::show_reminder(); + + // No notice should be created. + $this->assertEquals( 0, count( self::get_db_update_notes() ), 'There should be no db update notes created if db is up to date.' ); + } + + + /** + * Note should be created if there is none and WC is updated. + */ + public function test_create_db_update_note() { + // No notes initially. + $this->assertEquals( 0, count( self::get_db_update_notes() ), 'There should be no db update notes initially.' ); + + // Make it appear as if db version is lower than WC version, i.e. db update is required. + update_option( 'woocommerce_db_version', '3.9.0' ); + + WC_Notes_Run_Db_Update::show_reminder(); + + // A notice should be created. + $this->assertEquals( 1, count( self::get_db_update_notes() ), 'A db update note should be created if db is NOT up to date.' ); + + // Update the db option back. + update_option( 'woocommerce_db_version', WC()->version ); + } + + /** + * Note should be created if there is none and WC is updated. + */ + public function test_clean_up_multiple_db_update_notes() { + // No notes initially. + $this->assertEquals( 0, count( self::get_db_update_notes() ), 'There should be no db update notes initially.' ); + + $note_1 = self::create_db_update_note(); + $note_2 = self::create_db_update_note(); + + $this->assertEquals( 2, count( self::get_db_update_notes() ), 'There should be 2 db update notes after I created 2.' ); + + WC_Notes_Run_Db_Update::show_reminder(); + + // Only one notice should remain, in case 2 were created under some weird circumstances. + $this->assertEquals( 1, count( self::get_db_update_notes() ), 'A db update note should be created if db is NOT up to date.' ); + + } + + /** + * Test switch from db update needed to thanks note. + */ + public function test_db_update_note_to_thanks_note() { + // No notes initially. + $this->assertEquals( 0, count( self::get_db_update_notes() ), 'There should be no db update notes initially.' ); + + // Make it appear as if db version is lower than WC version, i.e. db update is required. + update_option( 'woocommerce_db_version', '3.9.0' ); + + // Magic 1: nothing to update-db note. + WC_Notes_Run_Db_Update::show_reminder(); + + $note_ids = self::get_db_update_notes(); + // An 'update required' notice should be created. + $this->assertEquals( 1, count( $note_ids ), 'A db update note should be created if db is NOT up to date.' ); + + $note = new Note( $note_ids[0] ); + $actions = $note->get_actions(); + $this->assertEquals( 'update-db_run', $actions[0]->name, 'A db update note to update the database should be displayed now.' ); + + // Simulate database update has been performed. + update_option( 'woocommerce_db_version', WC()->version ); + + // Magic 2: update-db note to thank you note. + WC_Notes_Run_Db_Update::show_reminder(); + + $note = new Note( $note_ids[0] ); + $actions = $note->get_actions(); + $this->assertEquals( 'update-db_done', $actions[0]->name, 'A db update note--Thanks for the update--should be displayed now.' ); + } + +} diff --git a/plugins/woocommerce/tests/legacy/unit-tests/admin/reports/class-wc-tests-admin-report.php b/plugins/woocommerce/tests/legacy/unit-tests/admin/reports/class-wc-tests-admin-report.php new file mode 100644 index 00000000000..480af399bdc --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/admin/reports/class-wc-tests-admin-report.php @@ -0,0 +1,166 @@ +plugin_dir . '/includes/admin/reports/class-wc-admin-report.php'; + } + + /** + * Clear cached report data. + * + * @before + */ + public function clear_transients() { + delete_transient( 'wc_admin_report' ); + } + + /** + * Test: get_order_report_data + */ + public function test_get_order_report_data() { + $order = WC_Helper_Order::create_order(); + $order->set_status( 'completed' ); + $order->save(); + + $report = new WC_Admin_Report(); + $data = $report->get_order_report_data( + array( + 'data' => array( + 'ID' => array( + 'type' => 'post_data', + 'function' => 'COUNT', + 'name' => 'total_orders', + ), + ), + ) + ); + + $this->assertEquals( 1, $data->total_orders, 'Expected to see one completed order in the report.' ); + WC_Admin_Report::maybe_update_transients(); + $this->assertNotEmpty( get_transient( 'wc_admin_report' ), 'Results should be cached in a transient.' ); + } + + /** + * Test: get_order_report_data + */ + public function test_get_order_report_data_returns_empty_string_if_data_is_empty() { + $report = new WC_Admin_Report(); + + add_filter( 'woocommerce_reports_get_order_report_data_args', '__return_empty_string' ); + + $this->assertEmpty( $report->get_order_report_data() ); + } + + /** + * Test: get_order_report_data + */ + public function test_get_order_report_data_for_post_meta() { + $order = WC_Helper_Order::create_order(); + $order->set_status( 'completed' ); + $order->save(); + + $report = new WC_Admin_Report(); + $data = $report->get_order_report_data( + array( + 'data' => array( + '_billing_first_name' => array( + 'type' => 'meta', + 'function' => null, + 'name' => 'customer_name', + ), + ), + ) + ); + + $this->assertEquals( $order->get_billing_first_name(), $data->customer_name ); + } + + /** + * Test: get_order_report_data + */ + public function test_get_order_report_data_for_parent_meta() { + $order = WC_Helper_Order::create_order(); + $refund = wc_create_refund( + array( + 'order_id' => $order->get_id(), + ) + ); + + $report = new WC_Admin_Report(); + $data = $report->get_order_report_data( + array( + 'data' => array( + '_order_total' => array( + 'type' => 'parent_meta', + 'function' => '', + 'name' => 'total_refund', + ), + ), + ) + ); + + $this->assertEquals( $order->get_total(), $data->total_refund ); + } + + /** + * Test: get_order_report_data + */ + public function test_get_order_report_data_for_post_data() { + $order = WC_Helper_Order::create_order(); + $order->set_status( 'completed' ); + $order->save(); + + $report = new WC_Admin_Report(); + $data = $report->get_order_report_data( + array( + 'data' => array( + 'post_status' => array( + 'type' => 'post_data', + 'function' => null, + 'name' => 'post_status', + ), + ), + ) + ); + + $this->assertEquals( 'wc-completed', $data->post_status ); + } + + /** + * Test: get_order_report_data + */ + public function test_get_order_report_data_for_order_items() { + $product = WC_Helper_Product::create_simple_product(); + $order = WC_Helper_Order::create_order( 0, $product->get_id() ); + $order->set_status( 'completed' ); + $order->save(); + + $report = new WC_Admin_Report(); + $data = $report->get_order_report_data( + array( + 'data' => array( + 'order_item_name' => array( + 'type' => 'order_item', + 'function' => null, + 'name' => 'name', + ), + ), + ) + ); + + $this->assertEquals( $product->get_name(), $data->name ); + } +} diff --git a/plugins/woocommerce/tests/legacy/unit-tests/admin/reports/class-wc-tests-report-sales-by-date.php b/plugins/woocommerce/tests/legacy/unit-tests/admin/reports/class-wc-tests-report-sales-by-date.php new file mode 100644 index 00000000000..8f3364deb11 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/admin/reports/class-wc-tests-report-sales-by-date.php @@ -0,0 +1,148 @@ +plugin_dir . '/includes/admin/reports/class-wc-admin-report.php'; + include_once WC_Unit_Tests_Bootstrap::instance()->plugin_dir . '/includes/admin/reports/class-wc-report-sales-by-date.php'; + } + + /** + * Clear cached report data. + * + * @before + */ + public function clear_transients() { + delete_transient( 'wc_report_sales_by_date' ); + } + + /** + * Test: get_report_data + */ + public function test_get_report_data() { + update_option( 'woocommerce_default_customer_address', 'base' ); + update_option( 'woocommerce_tax_based_on', 'base' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + + $product = WC_Helper_Product::create_simple_product(); + $coupon = WC_Helper_Coupon::create_coupon(); + $tax = WC_Tax::_insert_tax_rate( + array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '10.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ) + ); + + // A standard order. + $order1 = WC_Helper_Order::create_order( 0, $product->get_id() ); + $order1->set_status( 'completed' ); + $order1->save(); + + // An order using a coupon. + $order2 = WC_Helper_Order::create_order(); + $order2->apply_coupon( $coupon ); + $order2->set_status( 'completed' ); + $order2->save(); + + // An order that was refunded, save for shipping. + $order3 = WC_Helper_Order::create_order(); + $order3->set_status( 'completed' ); + $order3->save(); + wc_create_refund( + array( + 'amount' => 7, + 'order_id' => $order3->get_id(), + ) + ); + + // Parameters borrowed from WC_Admin_Dashboard::get_sales_report_data(). + $report = new WC_Report_Sales_By_Date(); + $report->start_date = strtotime( date( 'Y-m-01', current_time( 'timestamp' ) ) ); + $report->end_date = current_time( 'timestamp' ); + $report->chart_groupby = 'day'; + $report->group_by_query = 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)'; + $data = $report->get_report_data(); + + $this->assertEquals( 3, $data->order_counts[0]->count, 'Expected to see three orders in total.' ); + $this->assertEquals( $data->order_counts[0]->count, $data->total_orders ); + + $this->assertEquals( + $order1->get_item_count() + $order2->get_item_count() + $order3->get_item_count(), + $data->order_items[0]->order_item_count, + 'Order item count.' + ); + $this->assertEquals( $data->order_items[0]->order_item_count, $data->total_items, 'Total items.' ); + + $this->assertEquals( + $coupon->get_code(), + $data->coupons[0]->order_item_name, + 'There should be a single coupon applied.' + ); + $this->assertEquals( wc_format_decimal( $data->coupons[0]->discount_amount, '' ), $data->total_coupons, 'Total discount amount.' ); + + $this->assertCount( 1, $data->refund_lines, 'There was one refund granted.' ); + $this->assertEquals( 7, $data->partial_refunds[0]->total_refund, 'Total refunds.' ); + $this->assertEquals( $data->partial_refunds[0]->total_refund, $data->total_refunds, 'Day refunds, total refunds.' ); + + $this->assertEquals( + $order1->get_shipping_total() + $order2->get_shipping_total() + $order3->get_shipping_total(), + $data->orders[0]->total_shipping, + 'Orders, total shipping.' + ); + $this->assertEquals( wc_format_decimal( $data->orders[0]->total_shipping, '' ), $data->total_shipping, 'Day shipping, total shipping.' ); + + $this->assertEquals( + $order1->get_shipping_tax() + $order2->get_shipping_tax() + $order3->get_shipping_tax(), + $data->orders[0]->total_shipping_tax, + 'Orders, total shipping tax.' + ); + $this->assertEquals( wc_format_decimal( $data->orders[0]->total_shipping_tax, '' ), $data->total_shipping_tax, 'Day shipping tax, total shipping tax.' ); + + $this->assertEquals( wc_format_decimal( $data->orders[0]->total_tax, '' ), $data->total_tax, 'Day tax, total tax.' ); + + $this->assertEquals( + $order1->get_total() + $order2->get_total() + $order3->get_total(), + $data->orders[0]->total_sales, + 'Orders, total sales.' + ); + $this->assertEquals( + wc_format_decimal( $data->orders[0]->total_sales - $data->total_refunds, '' ), + $data->total_sales, + 'Day sales, total sales.' + ); + $this->assertEquals( $data->total_sales, $data->average_total_sales, 'Average total sales.' ); + + $this->assertEquals( + $order1->get_subtotal() + $order2->get_subtotal() + $order3->get_subtotal() - $data->total_refunds - $data->total_coupons, + $data->net_sales, + 'Net sales.' + ); + $this->assertEquals( $data->net_sales, $data->average_sales, 'For this window, Average sales should match net sales.' ); + + // Report columns that won't have data, but we want to ensure they exist. + $this->assertEmpty( $data->full_refunds ); + $this->assertEquals( 0, $data->refunded_order_items ); + $this->assertEquals( 0, $data->total_tax_refunded ); + $this->assertEquals( 0, $data->total_shipping_refunded ); + $this->assertEquals( 0, $data->total_shipping_tax_refunded ); + $this->assertEquals( 0, $data->total_refunded_orders ); + } +} diff --git a/tests/legacy/unit-tests/admin/settings.php b/plugins/woocommerce/tests/legacy/unit-tests/admin/settings.php similarity index 100% rename from tests/legacy/unit-tests/admin/settings.php rename to plugins/woocommerce/tests/legacy/unit-tests/admin/settings.php diff --git a/plugins/woocommerce/tests/legacy/unit-tests/attributes/functions.php b/plugins/woocommerce/tests/legacy/unit-tests/attributes/functions.php new file mode 100644 index 00000000000..f25eea532b6 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/attributes/functions.php @@ -0,0 +1,224 @@ + 'Brand', + 'type' => 'select', + 'order_by' => 'name', + 'has_archives' => true, + ); + + $id = wc_create_attribute( $args ); + + $attribute = (array) wc_get_attribute( $id ); + $expected = array( + 'id' => $id, + 'name' => 'Brand', + 'slug' => 'pa_brand', + 'type' => 'select', + 'order_by' => 'name', + 'has_archives' => true, + ); + + wc_delete_attribute( $id ); + + $this->assertEquals( $expected, $attribute ); + } + + /** + * Tests wc_create_attribute(). + * + * @since 3.2.0 + */ + public function test_wc_create_attribute() { + // Test success. + $id = wc_create_attribute( array( 'name' => 'Brand' ) ); + $this->assertIsInt( $id ); + + // Test failures. + $err = wc_create_attribute( array() ); + $this->assertEquals( 'missing_attribute_name', $err->get_error_code() ); + + $err = wc_create_attribute( array( 'name' => 'This is a big name for a product attribute!' ) ); + $this->assertEquals( 'invalid_product_attribute_slug_too_long', $err->get_error_code() ); + + $err = wc_create_attribute( array( 'name' => 'Cat' ) ); + $this->assertEquals( 'invalid_product_attribute_slug_reserved_name', $err->get_error_code() ); + + register_taxonomy( 'pa_brand', array( 'product' ), array( 'labels' => array( 'name' => 'Brand' ) ) ); + $err = wc_create_attribute( array( 'name' => 'Brand' ) ); + $this->assertEquals( 'invalid_product_attribute_slug_already_exists', $err->get_error_code() ); + unregister_taxonomy( 'pa_brand' ); + + wc_delete_attribute( $id ); + } + + /** + * Test that updating a global attribute will not modify local attribute data. + * + * @since 3.4.6 + */ + public function test_wc_create_attribute_serialized_data() { + global $wpdb; + + $global_attribute_data = WC_Helper_Product::create_attribute( 'test', array( 'Chicken', 'Nuggets' ) ); + + $local_attribute = new WC_Product_Attribute(); + $local_attribute->set_id( 0 ); + $local_attribute->set_name( 'Test Local Attribute' ); + $local_attribute->set_options( array( 'Fish', 'Fingers', 's:7:"pa_test' ) ); + $local_attribute->set_position( 0 ); + $local_attribute->set_visible( true ); + $local_attribute->set_variation( false ); + + $global_attribute = new WC_Product_Attribute(); + $global_attribute->set_id( $global_attribute_data['attribute_id'] ); + $global_attribute->set_name( $global_attribute_data['attribute_taxonomy'] ); + $global_attribute->set_options( $global_attribute_data['term_ids'] ); + $global_attribute->set_position( 1 ); + $global_attribute->set_visible( true ); + $global_attribute->set_variation( false ); + + $product = new WC_Product_Simple(); + $product->set_attributes( + array( + 'test-local' => $local_attribute, + 'test-global' => $global_attribute, + ) + ); + $product->save(); + + // Check everything looks good before updating the attribute. + $meta_before_update = $wpdb->get_results( $wpdb->prepare( "SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_product_attributes' AND post_id = %d", $product->get_id() ), ARRAY_A ); + $product_meta_before_update = @unserialize( $meta_before_update[0]['meta_value'] ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize + $this->assertNotFalse( $product_meta_before_update, 'Meta should be an unserializable string' ); + + $expected_local_attribute_data = array( + 'name' => 'Test Local Attribute', + 'value' => 'Fish | Fingers | s:7:"pa_test', + 'position' => 0, + 'is_visible' => 1, + 'is_variation' => 0, + 'is_taxonomy' => 0, + ); + $expected_global_attribute_data = array( + 'name' => 'pa_test', + 'value' => '', + 'position' => 1, + 'is_visible' => 1, + 'is_variation' => 0, + 'is_taxonomy' => 1, + ); + $local_before = isset( $product_meta_before_update['Test Local Attribute'] ) ? $product_meta_before_update['Test Local Attribute'] : $product_meta_before_update['test-local-attribute']; + $this->assertEquals( $expected_local_attribute_data, $local_before ); + $this->assertEquals( $expected_global_attribute_data, $product_meta_before_update['pa_test'] ); + + // Update the global attribute. + $updated_global_attribute_id = wc_create_attribute( + array( + 'id' => $global_attribute_data['attribute_id'], + 'name' => 'Test Update', + 'old_slug' => 'test', + 'slug' => 'testupdate', + ) + ); + $this->assertEquals( $updated_global_attribute_id, $global_attribute_data['attribute_id'] ); + + // Changes to the global attribute should update in the product without causing side-effects. + $meta_after_update = $wpdb->get_results( $wpdb->prepare( "SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_product_attributes' AND post_id = %d", $product->get_id() ), ARRAY_A ); + $product_meta_after_update = @unserialize( $meta_after_update[0]['meta_value'] ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize + $this->assertNotFalse( $product_meta_after_update, 'Meta should be an unserializable string' ); + + $expected_global_attribute_data = array( + 'name' => 'pa_testupdate', + 'value' => '', + 'position' => 1, + 'is_visible' => 1, + 'is_variation' => 0, + 'is_taxonomy' => 1, + ); + $this->assertEquals( $local_before, isset( $product_meta_after_update['Test Local Attribute'] ) ? $product_meta_after_update['Test Local Attribute'] : $product_meta_after_update['test-local-attribute'] ); + $this->assertEquals( $expected_global_attribute_data, $product_meta_after_update['pa_testupdate'] ); + $this->assertArrayNotHasKey( 'pa_test', $product_meta_after_update ); + } + + /** + * Tests wc_update_attribute(). + * + * @since 3.2.0 + */ + public function test_wc_update_attribute() { + $args = array( + 'name' => 'Brand', + 'type' => 'select', + 'order_by' => 'name', + 'has_archives' => true, + ); + + $id = wc_create_attribute( $args ); + + $updated = array( + 'id' => $id, + 'name' => 'Brand', + 'slug' => 'pa_brand', + 'type' => 'select', + 'order_by' => 'menu_order', + 'has_archives' => true, + ); + + wc_update_attribute( $id, $updated ); + + $attribute = (array) wc_get_attribute( $id ); + + wc_delete_attribute( $id ); + + $this->assertEquals( $updated, $attribute ); + } + + /** + * Tests wc_delete_attribute(). + * + * @since 3.2.0 + */ + public function test_wc_delete_attribute() { + // Success. + $id = wc_create_attribute( array( 'name' => 'Brand' ) ); + $result = wc_delete_attribute( $id ); + $this->assertTrue( $result ); + + // Failure. + $result = wc_delete_attribute( 9999999 ); + $this->assertFalse( $result ); + } + + /** + * Test counts of attributes. + */ + public function test_count_attribute_terms() { + $global_attribute_data = WC_Helper_Product::create_attribute( 'test', array( 'Chicken', 'Nuggets' ) ); + $count = wp_count_terms( + $global_attribute_data['attribute_taxonomy'], + array( + 'hide_empty' => false, + ) + ); + + $this->assertEquals( 2, $count ); + } +} diff --git a/tests/legacy/unit-tests/blocks/class-wc-tests-blocks-utils.php b/plugins/woocommerce/tests/legacy/unit-tests/blocks/class-wc-tests-blocks-utils.php similarity index 100% rename from tests/legacy/unit-tests/blocks/class-wc-tests-blocks-utils.php rename to plugins/woocommerce/tests/legacy/unit-tests/blocks/class-wc-tests-blocks-utils.php diff --git a/tests/legacy/unit-tests/cart/cart-fees.php b/plugins/woocommerce/tests/legacy/unit-tests/cart/cart-fees.php similarity index 100% rename from tests/legacy/unit-tests/cart/cart-fees.php rename to plugins/woocommerce/tests/legacy/unit-tests/cart/cart-fees.php diff --git a/plugins/woocommerce/tests/legacy/unit-tests/cart/cart.php b/plugins/woocommerce/tests/legacy/unit-tests/cart/cart.php new file mode 100644 index 00000000000..f0e27b38d9f --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/cart/cart.php @@ -0,0 +1,2284 @@ +cart->empty_cart(); + WC()->customer->set_is_vat_exempt( false ); + } + + /** + * Test whether totals are correct when discount is applied. + */ + public function test_cart_total_with_discount_and_taxes() { + update_option( 'woocommerce_prices_include_tax', 'yes' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + update_option( 'woocommerce_tax_round_at_subtotal', 'yes' ); + + WC()->cart->empty_cart(); + + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'TAX20', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '0', + 'tax_rate_order' => '1', + 'tax_rate_class' => '20percent', + ); + $tax_rate_20 = WC_Tax::_insert_tax_rate( $tax_rate ); + + // Create product with price 19. + $product = WC_Helper_Product::create_simple_product(); + $product->set_price( 8.99 ); + $product->set_regular_price( 8.99 ); + $product->set_tax_class( '20percent' ); + $product->save(); + + $coupon = WC_Helper_Coupon::create_coupon( 'off5', array( 'coupon_amount' => 5 ) ); + + // Create a flat rate method. + $flat_rate_settings = array( + 'enabled' => 'yes', + 'title' => 'Flat rate', + 'availability' => 'all', + 'countries' => '', + 'tax_status' => 'taxable', + 'cost' => '9.59', + ); + update_option( 'woocommerce_flat_rate_settings', $flat_rate_settings ); + + WC_Helper_Shipping::force_customer_us_address(); + + WC()->cart->add_to_cart( $product->get_id(), 1 ); + WC()->cart->add_discount( $coupon->get_code() ); + WC()->session->set( 'chosen_shipping_methods', array( 'flat_rate' ) ); + + WC()->cart->calculate_totals(); + + $this->assertEquals( '13.58', WC()->cart->get_total( 'edit' ) ); + $this->assertEquals( 0.66, WC()->cart->get_total_tax() ); + $this->assertEquals( 0.83, wc_format_decimal( WC()->cart->get_discount_tax(), 2 ) ); + $this->assertEquals( 4.17, wc_format_decimal( WC()->cart->get_discount_total(), 2 ) ); + } + + /** + * Test for subtotals and multiple tax rounding. + * Ticket: + * https://github.com/woocommerce/woocommerce/issues/21871 + */ + public function test_cart_subtotal_issue_21871() { + update_option( 'woocommerce_prices_include_tax', 'no' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + update_option( 'woocommerce_tax_round_at_subtotal', 'no' ); + update_option( 'woocommerce_tax_display_cart', 'incl' ); + + // Create dummy product - price will be 10. + $product = WC_Helper_Product::create_simple_product(); + $product->set_regular_price( 79 ); + $product->save(); + + // Add taxes. + $tax_rate_025 = WC_Tax::_insert_tax_rate( + array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '0.2500', + 'tax_rate_name' => 'TAX025', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ) + ); + + $tax_rate_06 = WC_Tax::_insert_tax_rate( + array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '6.0000', + 'tax_rate_name' => 'TAX06', + 'tax_rate_priority' => '2', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ) + ); + + $tax_rate_015 = WC_Tax::_insert_tax_rate( + array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '1.5000', + 'tax_rate_name' => 'TAX015', + 'tax_rate_priority' => '3', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ) + ); + + $tax_rate_01 = WC_Tax::_insert_tax_rate( + array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '1.000', + 'tax_rate_name' => 'TAX01', + 'tax_rate_priority' => '4', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ) + ); + + // Add product to cart x1, calc and test. + WC()->cart->add_to_cart( $product->get_id(), 1 ); + WC()->cart->calculate_totals(); + $this->assertEquals( '85.92', number_format( WC()->cart->total, 2, '.', '' ) ); + $this->assertEquals( '85.92', number_format( WC()->cart->subtotal, 2, '.', '' ) ); + $this->assertEquals( '85.92', wc_get_price_including_tax( $product ) ); + + WC_Helper_Product::delete_product( $product->get_id() ); + WC_Tax::_delete_tax_rate( $tax_rate_025 ); + WC_Tax::_delete_tax_rate( $tax_rate_06 ); + WC_Tax::_delete_tax_rate( $tax_rate_015 ); + WC_Tax::_delete_tax_rate( $tax_rate_01 ); + } + + /** + * Test tax rounding. + * Ticket: https://github.com/woocommerce/woocommerce/issues/21021. + */ + public function test_cart_get_total_issue_21021() { + update_option( 'woocommerce_prices_include_tax', 'yes' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + update_option( 'woocommerce_tax_round_at_subtotal', 'yes' ); + + // Set an address so that shipping can be calculated. + WC_Helper_Shipping::force_customer_us_address(); + + // Create tax classes first. + WC_Tax::create_tax_class( '23percent' ); + WC_Tax::create_tax_class( '5percent' ); + + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '23.0000', + 'tax_rate_name' => 'TAX23', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '23percent', + ); + $tax_rate_23 = WC_Tax::_insert_tax_rate( $tax_rate ); + + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '5.0000', + 'tax_rate_name' => 'TAX5', + 'tax_rate_priority' => '2', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '0', + 'tax_rate_order' => '1', + 'tax_rate_class' => '5percent', + ); + $tax_rate_5 = WC_Tax::_insert_tax_rate( $tax_rate ); + + // Create product with price 19. + $product = WC_Helper_Product::create_simple_product(); + $product->set_price( 19 ); + $product->set_regular_price( 19 ); + $product->set_tax_class( '5percent' ); + $product->save(); + + // Create product with price 59. + $product2 = WC_Helper_Product::create_simple_product(); + $product2->set_price( 59 ); + $product2->set_regular_price( 59 ); + $product2->set_tax_class( '23percent' ); + $product2->save(); + + // Create a flat rate method. + $flat_rate_settings = array( + 'enabled' => 'yes', + 'title' => 'Flat rate', + 'availability' => 'all', + 'countries' => '', + 'tax_status' => 'taxable', + 'cost' => '8.05', + ); + update_option( 'woocommerce_flat_rate_settings', $flat_rate_settings ); + update_option( 'woocommerce_flat_rate', array() ); + WC_Cache_Helper::get_transient_version( 'shipping', true ); + WC()->shipping->load_shipping_methods(); + + WC()->cart->empty_cart(); + + // Set the flat_rate shipping method. + WC()->session->set( 'chosen_shipping_methods', array( 'flat_rate' ) ); + + // Add product to cart x1, calc and test. + WC()->cart->add_to_cart( $product->get_id(), 1 ); + WC()->session->set( 'chosen_shipping_methods', array( 'flat_rate' ) ); + WC()->cart->calculate_totals(); + $this->assertEquals( 27.05, WC()->cart->total ); + + // Add product2 to cart. + WC()->cart->add_to_cart( $product2->get_id(), 1 ); + WC()->session->set( 'chosen_shipping_methods', array( 'flat_rate' ) ); + WC()->cart->calculate_totals(); + $this->assertEquals( 87.9, WC()->cart->total ); + + WC_Helper_Product::delete_product( $product->get_id() ); + WC_Helper_Product::delete_product( $product2->get_id() ); + + WC_Tax::_delete_tax_rate( $tax_rate_23 ); + WC_Tax::_delete_tax_rate( $tax_rate_5 ); + } + + /** + * Test for subtotal when multiple tax slabs are present and round at subtotal is enabled. + * + * Ticket: @link https://github.com/woocommerce/woocommerce/issues/23917 + */ + public function test_cart_calculate_total_rounding_23917() { + update_option( 'woocommerce_prices_include_tax', 'yes' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + update_option( 'woocommerce_tax_round_at_subtotal', 'yes' ); + + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '24.0000', + 'tax_rate_name' => 'CGST', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '0', + 'tax_rate_order' => '1', + 'tax_rate_class' => 'tax_1', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '24.0000', + 'tax_rate_name' => 'SGST', + 'tax_rate_priority' => '2', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '0', + 'tax_rate_order' => '1', + 'tax_rate_class' => 'tax_2', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + // Create a product with price 599. + $product = WC_Helper_Product::create_simple_product(); + $product->set_regular_price( 599 ); + $product->save(); + + WC()->cart->add_to_cart( $product->get_id(), 1 ); + WC()->cart->calculate_totals(); + $this->assertEquals( 599, wc_format_decimal( WC()->cart->subtotal, wc_get_price_decimals() ) ); + $this->assertEquals( 599, WC()->cart->total ); + } + + /** + * Test some discount logic which has caused issues in the past. + * Ticket: https://github.com/woocommerce/woocommerce/issues/10963. + * + * Due to discounts being split amongst products in cart. + */ + public function test_cart_get_discounted_price_issue_10963() { + // Create dummy coupon - fixed cart, 1 value. + $coupon = WC_Helper_Coupon::create_coupon(); + + // Add coupon. + WC()->cart->add_discount( $coupon->get_code() ); + + // Create dummy product - price will be 10. + $product = WC_Helper_Product::create_simple_product(); + + // Add product to cart x1, calc and test. + WC()->cart->add_to_cart( $product->get_id(), 1 ); + WC()->cart->calculate_totals(); + $this->assertEquals( '9.00', number_format( WC()->cart->total, 2, '.', '' ) ); + $this->assertEquals( '1.00', number_format( WC()->cart->discount_cart, 2, '.', '' ) ); + + // Add product to cart x2, calc and test. + WC()->cart->add_to_cart( $product->get_id(), 1 ); + WC()->cart->calculate_totals(); + $this->assertEquals( '19.00', number_format( WC()->cart->total, 2, '.', '' ) ); + $this->assertEquals( '1.00', number_format( WC()->cart->discount_cart, 2, '.', '' ) ); + + // Add product to cart x3, calc and test. + WC()->cart->add_to_cart( $product->get_id(), 1 ); + WC()->cart->calculate_totals(); + $this->assertEquals( '29.00', number_format( WC()->cart->total, 2, '.', '' ) ); + $this->assertEquals( '1.00', number_format( WC()->cart->discount_cart, 2, '.', '' ) ); + } + + /** + * Ticket: https://github.com/woocommerce/woocommerce/issues/11626 + */ + public function test_cart_get_discounted_price_issue_11626() { + $expected_values = array( + 1 => '13.90', // Tax exempt customer: all the prices without tax, then halved, i.e. sum = 33.09, sum_w/o_tax = 27.807, halved = 13.9035. + 0 => '16.55', // Normal customer: all the prices halved, i.e. sum = 33.09, halved = 16.545. + ); + + // Create dummy coupon - fixed cart, 1 value. + $coupon = WC_Helper_Coupon::create_coupon(); + + // Test case 3 #11626. + update_post_meta( $coupon->get_id(), 'discount_type', 'percent' ); + update_post_meta( $coupon->get_id(), 'coupon_amount', '50' ); + update_option( 'woocommerce_prices_include_tax', 'yes' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '19.0000', + 'tax_rate_name' => 'TAX', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + $product_ids = array(); + $products_data = array( + '5.17', + '3.32', + '1.25', + '3.50', + '5.01', + '3.34', + '5.99', + '5.51', + ); + + foreach ( $expected_values as $customer_tax_exempt => $value ) { + WC()->customer->set_is_vat_exempt( $customer_tax_exempt ); + + foreach ( $products_data as $price ) { + $loop_product = WC_Helper_Product::create_simple_product(); + $product_ids[] = $loop_product->get_id(); + $loop_product->set_regular_price( $price ); + $loop_product->save(); + WC()->cart->add_to_cart( $loop_product->get_id(), 1 ); + } + + WC()->cart->add_discount( $coupon->get_code() ); + WC()->cart->calculate_totals(); + $this->assertEquals( $value, WC()->cart->total ); + + WC()->cart->empty_cart(); + } + } + + /** + * Ticket: https://github.com/woocommerce/woocommerce/issues/10573 + */ + public function test_cart_get_discounted_price_issue_10573() { + // Create dummy coupon - fixed cart, 1 value. + $coupon = WC_Helper_Coupon::create_coupon(); + + // Create dummy product - price will be 10. + $product = WC_Helper_Product::create_simple_product(); + + $product->set_regular_price( '29.95' ); + $product->save(); + update_post_meta( $coupon->get_id(), 'discount_type', 'percent' ); + update_post_meta( $coupon->get_id(), 'coupon_amount', '10' ); + update_option( 'woocommerce_prices_include_tax', 'yes' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '10.0000', + 'tax_rate_name' => 'TAX', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + $product = wc_get_product( $product->get_id() ); + + WC()->cart->add_to_cart( $product->get_id(), 1 ); + WC()->cart->add_discount( $coupon->get_code() ); + + WC()->cart->calculate_totals(); + $cart_item = current( WC()->cart->get_cart() ); + $this->assertEquals( '24.51', number_format( $cart_item['line_total'], 2, '.', '' ) ); + } + + /** + * Test that calculation rounding is done correctly with and without taxes. + * + * @see https://github.com/woocommerce/woocommerce/issues/16305. + * @since 3.2 + */ + public function test_discount_cart_rounding() { + $expected_values = array( + 1 => array( '31.12', '31.12' ), // Tax exempt customer: Total and Total + tax are the same. + 0 => array( '31.12', '33.69' ), // Normal customer: Total, then Total + tax. + ); + + // Test with no taxes. + $product = new WC_Product_Simple(); + $product->set_regular_price( 51.86 ); + $product->save(); + + $coupon = new WC_Coupon(); + $coupon->set_code( 'testpercent' ); + $coupon->set_discount_type( 'percent' ); + $coupon->set_amount( 40 ); + $coupon->save(); + + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '8.2500', + 'tax_rate_name' => 'TAX', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + foreach ( $expected_values as $customer_tax_exempt => $values ) { + WC()->customer->set_is_vat_exempt( $customer_tax_exempt ); + + // Test without taxes. + update_option( 'woocommerce_prices_include_tax', 'yes' ); + update_option( 'woocommerce_calc_taxes', 'no' ); + + WC()->cart->add_to_cart( $product->get_id(), 1 ); + WC()->cart->add_discount( $coupon->get_code() ); + + WC()->cart->calculate_totals(); + $cart_item = current( WC()->cart->get_cart() ); + $this->assertEquals( $values[0], number_format( $cart_item['line_total'], 2, '.', '' ) ); + + // Clean up. + WC()->cart->empty_cart(); + WC()->cart->remove_coupons(); + + // Test with taxes. + update_option( 'woocommerce_prices_include_tax', 'no' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + + WC()->cart->add_to_cart( $product->get_id(), 1 ); + WC()->cart->add_discount( $coupon->get_code() ); + + WC()->cart->calculate_totals(); + $cart_item = current( WC()->cart->get_cart() ); + $this->assertEquals( $values[1], number_format( $cart_item['line_total'] + $cart_item['line_tax'], 2, '.', '' ) ); + + WC()->cart->empty_cart(); + } + } + + /** + * Test cart calculations when out of base location and using inclusive taxes and discounts. + * + * @see GitHub issues #17517 and #17536. + * @since 3.3 + */ + public function test_out_of_base_discounts_inclusive_tax() { + // Set up tax options. + update_option( 'woocommerce_prices_include_tax', 'yes' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + update_option( 'woocommerce_default_country', 'GB' ); + update_option( 'woocommerce_default_customer_address', 'base' ); + update_option( 'woocommerce_tax_based_on', 'shipping' ); + + // 20% tax for GB. + $tax_rate = array( + 'tax_rate_country' => 'GB', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '0', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + // 20% tax everywhere else. + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'TAX', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '0', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + // Create product. + $product = new WC_Product_Simple(); + $product->set_regular_price( '9.99' ); + $product->save(); + + // Create coupons. + $ten_coupon = new WC_Coupon(); + $ten_coupon->set_code( '10off' ); + $ten_coupon->set_discount_type( 'percent' ); + $ten_coupon->set_amount( 10 ); + $ten_coupon->save(); + + $half_coupon = new WC_Coupon(); + $half_coupon->set_code( '50off' ); + $half_coupon->set_discount_type( 'percent' ); + $half_coupon->set_amount( 50 ); + $half_coupon->save(); + + $full_coupon = new WC_Coupon(); + $full_coupon->set_code( '100off' ); + $full_coupon->set_discount_type( 'percent' ); + $full_coupon->set_amount( 100 ); + $full_coupon->save(); + + add_filter( 'woocommerce_customer_get_shipping_country', array( $this, 'force_customer_gb_country' ) ); + add_filter( 'woocommerce_customer_get_shipping_postcode', array( $this, 'force_customer_gb_postcode' ) ); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Test in store location with no coupon. + WC()->cart->calculate_totals(); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_subtotal(), 2 ) ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_discount_total(), 2 ) ); + $this->assertEquals( '1.66', wc_format_decimal( WC()->cart->get_total_tax(), 2 ) ); + $this->assertEquals( '9.99', wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + + // Test in store location with 10% coupon. + WC()->cart->add_discount( $ten_coupon->get_code() ); + WC()->cart->calculate_totals(); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_subtotal(), 2 ) ); + $this->assertEquals( '0.83', wc_format_decimal( WC()->cart->get_discount_total(), 2 ) ); + $this->assertEquals( '1.50', wc_format_decimal( WC()->cart->get_total_tax(), 2 ), WC()->cart->get_total_tax() ); + $this->assertEquals( '8.99', wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + WC()->cart->remove_coupons(); + + // Test in store location with 50% coupon. + WC()->cart->add_discount( $half_coupon->get_code() ); + WC()->cart->calculate_totals(); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_subtotal(), 2 ) ); + $this->assertEquals( '4.16', wc_format_decimal( WC()->cart->get_discount_total(), 2 ) ); + $this->assertEquals( '0.83', wc_format_decimal( WC()->cart->get_total_tax(), 2 ) ); + $this->assertEquals( '5.00', wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + WC()->cart->remove_coupons(); + + // Test in store location with 100% coupon. + WC()->cart->add_discount( $full_coupon->get_code() ); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_subtotal(), 2 ) ); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_discount_total(), 2 ), 'Discount total in base' ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_total_tax(), 2 ) ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + WC()->cart->remove_coupons(); + + WC()->cart->empty_cart(); + remove_filter( 'woocommerce_customer_get_shipping_country', array( $this, 'force_customer_gb_country' ) ); + remove_filter( 'woocommerce_customer_get_shipping_postcode', array( $this, 'force_customer_gb_postcode' ) ); + WC_Helper_Shipping::force_customer_us_address(); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Test out of store location with no coupon. + WC()->cart->calculate_totals(); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_subtotal(), 2 ) ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_discount_total(), 2 ) ); + $this->assertEquals( '1.66', wc_format_decimal( WC()->cart->get_total_tax(), 2 ) ); + $this->assertEquals( '9.99', wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + + // Test out of store location with 10% coupon. + WC()->cart->add_discount( $ten_coupon->get_code() ); + WC()->cart->calculate_totals(); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_subtotal(), 2 ) ); + $this->assertEquals( '0.83', wc_format_decimal( WC()->cart->get_discount_total(), 2 ) ); + $this->assertEquals( '1.50', wc_format_decimal( WC()->cart->get_total_tax(), 2 ) ); + $this->assertEquals( '8.99', wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + WC()->cart->remove_coupons(); + + // Test out of store location with 50% coupon. + WC()->cart->add_discount( $half_coupon->get_code() ); + WC()->cart->calculate_totals(); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_subtotal(), 2 ) ); + $this->assertEquals( '4.16', wc_format_decimal( WC()->cart->get_discount_total(), 2 ) ); + $this->assertEquals( '0.83', wc_format_decimal( WC()->cart->get_total_tax(), 2 ) ); + $this->assertEquals( '5.00', wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + WC()->cart->remove_coupons(); + + // Test out of store location with 100% coupon. + WC()->cart->add_discount( $full_coupon->get_code() ); + WC()->cart->calculate_totals(); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_subtotal(), 2 ) ); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_discount_total(), 2 ), 'Discount total out of base' ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_total_tax(), 2 ) ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + } + + /** + * Test cart calculations when out of base location and using inclusive taxes and discounts, for tax exempt customer. + * + * @see GitHub issues #17517 and #17536. + * @since 3.5 + */ + public function test_out_of_base_discounts_inclusive_tax_for_tax_exempt_customer() { + WC()->customer->set_is_vat_exempt( true ); + + // Set up tax options. + update_option( 'woocommerce_prices_include_tax', 'yes' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + update_option( 'woocommerce_default_country', 'GB' ); + update_option( 'woocommerce_default_customer_address', 'base' ); + update_option( 'woocommerce_tax_based_on', 'shipping' ); + + // 20% tax for GB. + $tax_rate = array( + 'tax_rate_country' => 'GB', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '0', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + // 20% tax everywhere else. + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'TAX', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '0', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + // Create product. + $product = new WC_Product_Simple(); + $product->set_regular_price( '9.99' ); + $product->save(); + + // Create coupons. + $ten_coupon = new WC_Coupon(); + $ten_coupon->set_code( '10off' ); + $ten_coupon->set_discount_type( 'percent' ); + $ten_coupon->set_amount( 10 ); + $ten_coupon->save(); + + $half_coupon = new WC_Coupon(); + $half_coupon->set_code( '50off' ); + $half_coupon->set_discount_type( 'percent' ); + $half_coupon->set_amount( 50 ); + $half_coupon->save(); + + $full_coupon = new WC_Coupon(); + $full_coupon->set_code( '100off' ); + $full_coupon->set_discount_type( 'percent' ); + $full_coupon->set_amount( 100 ); + $full_coupon->save(); + + add_filter( 'woocommerce_customer_get_shipping_country', array( $this, 'force_customer_gb_country' ) ); + add_filter( 'woocommerce_customer_get_shipping_postcode', array( $this, 'force_customer_gb_postcode' ) ); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Test in store location with no coupon. + WC()->cart->calculate_totals(); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_subtotal(), 2 ) ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_discount_total(), 2 ) ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_total_tax(), 2 ) ); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + + // Test in store location with 10% coupon. + WC()->cart->add_discount( $ten_coupon->get_code() ); + WC()->cart->calculate_totals(); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_subtotal(), 2 ) ); + $this->assertEquals( '0.83', wc_format_decimal( WC()->cart->get_discount_total(), 2 ) ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_total_tax(), 2 ), WC()->cart->get_total_tax() ); + $this->assertEquals( '7.50', wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + WC()->cart->remove_coupons(); + + // Test in store location with 50% coupon. + WC()->cart->add_discount( $half_coupon->get_code() ); + WC()->cart->calculate_totals(); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_subtotal(), 2 ) ); + $this->assertEquals( '4.16', wc_format_decimal( WC()->cart->get_discount_total(), 2 ) ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_total_tax(), 2 ) ); + $this->assertEquals( '4.17', wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + WC()->cart->remove_coupons(); + + // Test in store location with 100% coupon. + WC()->cart->add_discount( $full_coupon->get_code() ); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_subtotal(), 2 ) ); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_discount_total(), 2 ), 'Discount total in base' ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_total_tax(), 2 ) ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + WC()->cart->remove_coupons(); + + WC()->cart->empty_cart(); + remove_filter( 'woocommerce_customer_get_shipping_country', array( $this, 'force_customer_gb_country' ) ); + remove_filter( 'woocommerce_customer_get_shipping_postcode', array( $this, 'force_customer_gb_postcode' ) ); + WC_Helper_Shipping::force_customer_us_address(); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Test out of store location with no coupon. + WC()->cart->calculate_totals(); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_subtotal(), 2 ) ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_discount_total(), 2 ) ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_total_tax(), 2 ) ); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + + // Test out of store location with 10% coupon. + WC()->cart->add_discount( $ten_coupon->get_code() ); + WC()->cart->calculate_totals(); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_subtotal(), 2 ) ); + $this->assertEquals( '0.83', wc_format_decimal( WC()->cart->get_discount_total(), 2 ) ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_total_tax(), 2 ) ); + $this->assertEquals( '7.50', wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + WC()->cart->remove_coupons(); + + // Test out of store location with 50% coupon. + WC()->cart->add_discount( $half_coupon->get_code() ); + WC()->cart->calculate_totals(); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_subtotal(), 2 ) ); + $this->assertEquals( '4.16', wc_format_decimal( WC()->cart->get_discount_total(), 2 ) ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_total_tax(), 2 ) ); + $this->assertEquals( '4.17', wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + WC()->cart->remove_coupons(); + + // Test out of store location with 100% coupon. + WC()->cart->add_discount( $full_coupon->get_code() ); + WC()->cart->calculate_totals(); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_subtotal(), 2 ) ); + $this->assertEquals( '8.33', wc_format_decimal( WC()->cart->get_discount_total(), 2 ), 'Discount total out of base' ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_total_tax(), 2 ) ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + } + + /** + * Test cart calculations when out of base location with no matching taxes and using inclusive taxes and discounts. + * + * @see GitHub issue #19390. + * @since 3.3 + */ + public function test_out_of_base_discounts_inclusive_tax_no_oob_tax() { + // Set up tax options. + update_option( 'woocommerce_prices_include_tax', 'yes' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + update_option( 'woocommerce_default_country', 'GB' ); + update_option( 'woocommerce_default_customer_address', 'base' ); + update_option( 'woocommerce_tax_based_on', 'shipping' ); + + // 20% tax for GB. + $tax_rate = array( + 'tax_rate_country' => 'GB', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '0', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + // 0% tax everywhere else. + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '0.0000', + 'tax_rate_name' => 'TAX', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '0', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + // Create product. + $product = new WC_Product_Simple(); + $product->set_regular_price( '24.99' ); + $product->save(); + + // Create coupon. + $ten_coupon = new WC_Coupon(); + $ten_coupon->set_code( '10off' ); + $ten_coupon->set_discount_type( 'percent' ); + $ten_coupon->set_amount( 10 ); + $ten_coupon->save(); + + $half_coupon = new WC_Coupon(); + $half_coupon->set_code( '50off' ); + $half_coupon->set_discount_type( 'percent' ); + $half_coupon->set_amount( 50 ); + $half_coupon->save(); + + $full_coupon = new WC_Coupon(); + $full_coupon->set_code( '100off' ); + $full_coupon->set_discount_type( 'percent' ); + $full_coupon->set_amount( 100 ); + $full_coupon->save(); + + WC_Helper_Shipping::force_customer_us_address(); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Test out of store location with no coupon. + WC()->cart->calculate_totals(); + $this->assertEquals( '20.83', wc_format_decimal( WC()->cart->get_subtotal(), 2 ) ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_discount_total(), 2 ) ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_total_tax(), 2 ) ); + $this->assertEquals( '20.83', wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + + // Test out of store location with 10% coupon. + WC()->cart->add_discount( $ten_coupon->get_code() ); + WC()->cart->calculate_totals(); + $this->assertEquals( '20.83', wc_format_decimal( WC()->cart->get_subtotal(), 2 ) ); + $this->assertEquals( '2.08', wc_format_decimal( WC()->cart->get_discount_total(), 2 ) ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_total_tax(), 2 ) ); + $this->assertEquals( '18.75', wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + WC()->cart->remove_coupons(); + + // Test out of store location with 50% coupon. + WC()->cart->add_discount( $half_coupon->get_code() ); + WC()->cart->calculate_totals(); + $this->assertEquals( '20.83', wc_format_decimal( WC()->cart->get_subtotal(), 2 ) ); + $this->assertEquals( '10.41', wc_format_decimal( WC()->cart->get_discount_total(), 2 ) ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_total_tax(), 2 ) ); + $this->assertEquals( '10.42', wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + WC()->cart->remove_coupons(); + + // Test out of store location with 100% coupon. + WC()->cart->add_discount( $full_coupon->get_code() ); + WC()->cart->calculate_totals(); + $this->assertEquals( '20.83', wc_format_decimal( WC()->cart->get_subtotal(), 2 ) ); + $this->assertEquals( '20.83', wc_format_decimal( WC()->cart->get_discount_total(), 2 ), 'Discount total out of base' ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_total_tax(), 2 ) ); + $this->assertEquals( '0.00', wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + } + + /** + * Helper that can be hooked to a filter to force the customer's shipping country to be GB. + * + * @since 3.3 + * @param string $country Country code. + * @return string + */ + public function force_customer_gb_country( $country ) { + return 'GB'; + } + + /** + * Helper that can be hooked to a filter to force the customer's shipping postal code to be ANN NAA. + * + * @since 4.0.0 + * @param string $postcode Postal code.. + * @return string + */ + public function force_customer_gb_postcode( $postcode ) { + return 'ANN NAA'; + } + + /** + * Helper that can be hooked to a filter to force the customer's shipping country to be US. + * + * @since 3.3 + * @param string $country Country code. + * @return string + */ + public function force_customer_us_country( $country ) { + return WC_Helper_Shipping::force_customer_us_country( $country ); + } + + /** + * Helper that can be hooked to a filter to force the customer's shipping state to be NY. + * + * @since 4.0.0 + * @param string $state State code. + * @return string + */ + public function force_customer_us_state( $state ) { + return WC_Helper_Shipping::force_customer_us_state( $state ); + } + + /** + * Helper that can be hooked to a filter to force the customer's shipping postal code to be 12345. + * + * @since 4.0.0 + * @param string $postcode Postal code. + * @return string + */ + public function force_customer_us_postcode( $postcode ) { + return WC_Helper_Shipping::force_customer_us_postcode( $postcode ); + } + + /** + * Test a rounding issue on prices that are entered inclusive tax and shipping is used. + * See: #17970. + * + * @since 3.2.6 + */ + public function test_inclusive_tax_rounding() { + $expected_values = array( + 1 => array( // Tax exempt customer. + 'total' => '129.45', // Price -> price w/o tax, then + shipping, i.e. 149.14 -> 125.3277 + 4.12 = 129.45. + 'total_tax' => '0.00', // No tax. + ), + 0 => array( // Normal customer. + 'total' => '154.04', // Price + shipping + shipping tax, i.e. 149.14 + 4.12 * 1.19 = 154.04. + 'total_tax' => '24.59', // Tax = price tax + shipping tax. + ), + ); + + // Store is set to enter product prices inclusive tax. + update_option( 'woocommerce_prices_include_tax', 'yes' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + + // Set an address so that shipping can be calculated. + WC_Helper_Shipping::force_customer_us_address(); + + // 19% tax. + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '19.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + // Create a flat rate method. + $flat_rate_settings = array( + 'enabled' => 'yes', + 'title' => 'Flat rate', + 'availability' => 'all', + 'countries' => '', + 'tax_status' => 'taxable', + 'cost' => '4.12', + ); + update_option( 'woocommerce_flat_rate_settings', $flat_rate_settings ); + update_option( 'woocommerce_flat_rate', array() ); + WC_Cache_Helper::get_transient_version( 'shipping', true ); + WC()->shipping()->load_shipping_methods(); + + // Create the product and add it to the cart. + $product = new WC_Product_Simple(); + $product->set_regular_price( '149.14' ); + $product->save(); + + foreach ( $expected_values as $customer_tax_exempt => $values ) { + WC()->customer->set_is_vat_exempt( $customer_tax_exempt ); + + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Set the flat_rate shipping method. + WC()->session->set( 'chosen_shipping_methods', array( 'flat_rate' ) ); + + WC()->cart->calculate_totals(); + $this->assertEquals( $values['total'], wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + $this->assertEquals( $values['total_tax'], wc_format_decimal( WC()->cart->get_total_tax(), 2 ) ); + + WC()->cart->empty_cart(); + } + } + + /** + * Test a rounding issue on prices that are entered inclusive tax. + * See #20997 + * + * @since 3.5.0 + */ + public function test_inclusive_tax_rounding_issue_20997() { + // Store is set to enter product prices inclusive tax. + update_option( 'woocommerce_prices_include_tax', 'yes' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + update_option( 'woocommerce_currency', 'EUR' ); + update_option( 'woocommerce_price_decimal_sep', ',' ); + + // 22% tax. + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '22.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + // Create products and add them to cart. + $product1 = new WC_Product_Simple(); + $product1->set_regular_price( '46,00' ); + $product1->save(); + + $product2 = new WC_Product_Simple(); + $product2->set_regular_price( '22,50' ); + $product2->save(); + + $product3 = new WC_Product_Simple(); + $product3->set_regular_price( '69,00' ); + $product3->save(); + + $product4 = new WC_Product_Simple(); + $product4->set_regular_price( '43,00' ); + $product4->save(); + + $product5 = new WC_Product_Simple(); + $product5->set_regular_price( '27,00' ); + $product5->save(); + + $product6 = new WC_Product_Simple(); + $product6->set_regular_price( '100.00' ); + $product6->save(); + + WC()->cart->add_to_cart( $product1->get_id(), 1 ); + WC()->cart->add_to_cart( $product2->get_id(), 1 ); + WC()->cart->calculate_totals(); + + $expected_price = '68,50'; + $this->assertEquals( $expected_price, WC()->cart->get_total() ); + $this->assertEquals( '12.36', wc_round_tax_total( WC()->cart->get_total_tax( 'edit' ) ) ); + + WC()->cart->empty_cart(); + WC()->cart->add_to_cart( $product3->get_id(), 1 ); + WC()->cart->add_to_cart( $product4->get_id(), 1 ); + WC()->cart->calculate_totals(); + + $expected_price = '112,00'; + $this->assertEquals( $expected_price, WC()->cart->get_total() ); + $this->assertEquals( '20.19', wc_round_tax_total( WC()->cart->get_total_tax( 'edit' ) ) ); + + WC()->cart->empty_cart(); + WC()->cart->add_to_cart( $product3->get_id(), 1 ); + WC()->cart->add_to_cart( $product4->get_id(), 1 ); + WC()->cart->add_to_cart( $product5->get_id(), 1 ); + WC()->cart->add_to_cart( $product6->get_id(), 1 ); + WC()->cart->calculate_totals(); + + $expected_price = '239,00'; + $this->assertEquals( $expected_price, WC()->cart->get_total() ); + $this->assertEquals( '43.09', wc_round_tax_total( WC()->cart->get_total_tax( 'edit' ) ) ); + } + + /** + * Test a rounding issue on prices that are entered exclusive tax. + * See: #17970. + * + * @since 3.2.6 + */ + public function test_exclusive_tax_rounding() { + $expected_values = array( + 1 => '95.84', // Tax exempt customer: Total = simple sum of prices w/o tax. + 0 => '115.00', // Normal customer: Total = sum of prices with tax added. + ); + + // Store is set to enter product prices excluding tax. + update_option( 'woocommerce_prices_include_tax', 'no' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + + // 20% tax. + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '0', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + // Add 2 products whose retail prices (inc tax) are: £65, £50. + // Their net prices are therefore: £54.1666667 and £41.6666667. + $product1 = new WC_Product_Simple(); + $product1->set_regular_price( '54.1666667' ); + $product1->save(); + + $product2 = new WC_Product_Simple(); + $product2->set_regular_price( '41.6666667' ); + $product2->save(); + + foreach ( $expected_values as $customer_tax_exempt => $value ) { + WC()->customer->set_is_vat_exempt( $customer_tax_exempt ); + + WC()->cart->add_to_cart( $product1->get_id(), 1 ); + WC()->cart->add_to_cart( $product2->get_id(), 1 ); + + WC()->cart->calculate_totals(); + $this->assertEquals( $value, wc_format_decimal( WC()->cart->get_total( 'edit' ), 2 ) ); + + WC()->cart->empty_cart(); + } + } + + /** + * Test a rounding issue on prices and totals that are entered exclusive tax. + * See: #17647. + * + * @since 3.2.6 + */ + public function test_exclusive_tax_rounding_and_totals() { + $expected_values = array( + 1 => array( // Tax exempt customer. + 'total_tax' => '0.00', + 'cart_contents_tax' => '0.00', + 'cart_contents_total' => '44.17', + 'total' => '44.17', + ), + 0 => array( // Normal customer. + 'total_tax' => '2.44', + 'cart_contents_tax' => '2.44', + 'cart_contents_total' => '44.17', + 'total' => '46.61', + ), + ); + + $product_data = array( + // price, quantity. + array( 2.13, 1 ), + array( 2.55, 0.5 ), + array( 39, 1 ), + array( 1.76, 1 ), + ); + + foreach ( $product_data as $data ) { + $product = new WC_Product_Simple(); + $product->set_regular_price( $data[0] ); + $product->save(); + $products[] = array( $product, $data[1] ); + } + + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '5.5', + 'tax_rate_name' => 'TAX', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + + WC_Tax::_insert_tax_rate( $tax_rate ); + + update_option( 'woocommerce_prices_include_tax', 'no' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + update_option( 'woocommerce_tax_round_at_subtotal', 'no' ); + + foreach ( $expected_values as $customer_tax_exempt => $values ) { + WC()->customer->set_is_vat_exempt( $customer_tax_exempt ); + + foreach ( $products as $data ) { + WC()->cart->add_to_cart( $data[0]->get_id(), $data[1] ); + } + + WC()->cart->calculate_totals(); + + $cart_totals = WC()->cart->get_totals(); + + $this->assertEquals( $values['total_tax'], wc_format_decimal( $cart_totals['total_tax'], 2 ) ); + $this->assertEquals( $values['cart_contents_tax'], wc_format_decimal( $cart_totals['cart_contents_tax'], 2 ) ); + $this->assertEquals( $values['cart_contents_total'], wc_format_decimal( $cart_totals['cart_contents_total'], 2 ) ); + $this->assertEquals( $values['total'], wc_format_decimal( $cart_totals['total'], 2 ) ); + + WC()->cart->empty_cart(); + } + } + + /** + * Test get_remove_url. + * + * @since 2.3 + */ + public function test_get_remove_url() { + // Get the cart page id. + $cart_page_url = wc_get_page_permalink( 'cart' ); + + // Test cart item key. + $cart_item_key = 'test'; + + // Do the check. + $this->assertEquals( apply_filters( 'woocommerce_get_remove_url', $cart_page_url ? wp_nonce_url( add_query_arg( 'remove_item', $cart_item_key, $cart_page_url ), 'woocommerce-cart' ) : '' ), wc_get_cart_remove_url( $cart_item_key ) ); + } + + /** + * Test add to cart simple product. + * + * @since 2.3 + */ + public function test_add_to_cart_simple() { + // Create dummy product. + $product = WC_Helper_Product::create_simple_product(); + + // Add the product to the cart. Methods returns boolean on failure, string on success. + $this->assertNotFalse( WC()->cart->add_to_cart( $product->get_id(), 1 ) ); + + // Check if the item is in the cart. + $this->assertEquals( 1, WC()->cart->get_cart_contents_count() ); + } + + /** + * Check if we can add a trashed product to the cart. + */ + public function test_add_to_cart_trashed() { + // Create dummy product. + $product = WC_Helper_Product::create_simple_product(); + + // Trash product. + wp_trash_post( $product->get_id() ); + + // Refetch product, to be sure. + $product = wc_get_product( $product->get_id() ); + + // Add product to cart. + $this->assertFalse( WC()->cart->add_to_cart( $product->get_id(), 1 ) ); + } + + /** + * Test add to cart variable product. + * + * @since 2.3 + */ + public function test_add_to_cart_variable() { + $product = WC_Helper_Product::create_variation_product(); + $variations = $product->get_available_variations(); + $variation = array_shift( $variations ); + + // Add the product to the cart. Methods returns boolean on failure, string on success. + $result = WC()->cart->add_to_cart( + $product->get_id(), + 1, + $variation['variation_id'], + array( + 'attribute_pa_colour' => 'red', // Set a value since this is an 'any' attribute. + 'attribute_pa_number' => '2', // Set a value since this is an 'any' attribute. + ) + ); + $this->assertNotFalse( $result ); + + // Check if the item is in the cart. + $this->assertEquals( 1, WC()->cart->get_cart_contents_count() ); + } + + /** + * Check if adding a product that is sold individually is corrected when adding multiple times. + * + * @since 2.3 + */ + public function test_add_to_cart_sold_individually() { + // Create dummy product. + $product = WC_Helper_Product::create_simple_product(); + + $product->set_sold_individually( true ); + $product->save(); + + // Add the product twice to cart, should be corrected to 1. Methods returns boolean on failure, string on success. + $this->assertNotFalse( WC()->cart->add_to_cart( $product->get_id(), 2 ) ); + + // Check if the item is in the cart. + $this->assertEquals( 1, WC()->cart->get_cart_contents_count() ); + } + + /** + * Test the find_product_in_cart method. + * + * @since 2.3 + */ + public function test_find_product_in_cart() { + // Create dummy product. + $product = WC_Helper_Product::create_simple_product(); + + // Add product to cart. + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Generate cart id. + $cart_id = WC()->cart->generate_cart_id( $product->get_id() ); + + // Get the product from the cart. + $this->assertNotEquals( '', WC()->cart->find_product_in_cart( $cart_id ) ); + } + + /** + * Test the generate_cart_id method. + * + * @since 2.3 + */ + public function test_generate_cart_id() { + // Setup data. + $product_id = 1; + $variation_id = 2; + $variation = array( 'Testing' => 'yup' ); + $cart_item_data = array( + 'string_val' => 'The string I was talking about', + 'array_val' => array( + 'this', + 'is', + 'an', + 'array', + ), + ); + + // Manually generate ID. + $id_parts = array( $product_id ); + + if ( $variation_id && 0 != $variation_id ) { + $id_parts[] = $variation_id; + } + + if ( is_array( $variation ) && ! empty( $variation ) ) { + $variation_key = ''; + foreach ( $variation as $key => $value ) { + $variation_key .= trim( $key ) . trim( $value ); + } + $id_parts[] = $variation_key; + } + + if ( is_array( $cart_item_data ) && ! empty( $cart_item_data ) ) { + $cart_item_data_key = ''; + foreach ( $cart_item_data as $key => $value ) { + + if ( is_array( $value ) ) { + $value = http_build_query( $value ); + } + $cart_item_data_key .= trim( $key ) . trim( $value ); + + } + $id_parts[] = $cart_item_data_key; + } + + $manual_cart_id = md5( implode( '_', $id_parts ) ); + + $this->assertEquals( $manual_cart_id, WC()->cart->generate_cart_id( $product_id, $variation_id, array( 'Testing' => 'yup' ), $cart_item_data ) ); + + } + + /** + * Test the set_quantity method. + * + * @since 2.3 + */ + public function test_set_quantity() { + // Create dummy product. + $product = WC_Helper_Product::create_simple_product(); + + // Add 1 product to cart. + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Get cart id. + $cart_id = WC()->cart->generate_cart_id( $product->get_id() ); + + // Set quantity of product in cart to 2. + $this->assertTrue( WC()->cart->set_quantity( $cart_id, 2 ), $cart_id ); + + // Check if there are 2 items in cart now. + $this->assertEquals( 2, WC()->cart->get_cart_contents_count() ); + + // Set quantity of product in cart to 0. + $this->assertTrue( WC()->cart->set_quantity( $cart_id, 0 ) ); + + // Check if there are 0 items in cart now. + $this->assertEquals( 0, WC()->cart->get_cart_contents_count() ); + } + + /** + * Test check_cart_item_validity method. + * + * @since 2.3 + */ + public function test_check_cart_item_validity() { + + // Create dummy product. + $product = WC_Helper_Product::create_simple_product(); + + // Add product to cart. + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Check cart validity, should pass. + $this->assertTrue( WC()->cart->check_cart_item_validity() ); + } + + /** + * Test get_total. + * + * @since 2.3 + */ + public function test_get_total() { + $product = WC_Helper_Product::create_simple_product(); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + $this->assertEquals( apply_filters( 'woocommerce_cart_total', wc_price( WC()->cart->total ) ), WC()->cart->get_total() ); + } + + /** + * Test get_total_ex_tax. + * + * @since 2.3 + */ + public function test_get_total_ex_tax() { + $expected_values = array( + 1 => array( // Tax exempt customer. + 'total' => 20, + 'total_ex_tax' => 20, + ), + 0 => array( // Normal customer. + 'total' => 22, + 'total_ex_tax' => 20, + ), + ); + + // Set calc taxes option. + update_option( 'woocommerce_calc_taxes', 'yes' ); + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '10.0000', + 'tax_rate_name' => 'TAX', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + // Create dummy product. + $product = WC_Helper_Product::create_simple_product(); + + foreach ( $expected_values as $customer_tax_exempt => $values ) { + WC()->customer->set_is_vat_exempt( $customer_tax_exempt ); + + // Add 10 fee. + WC_Helper_Fee::add_cart_fee( 'taxed' ); + + // Add product to cart (10). + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Check. + $this->assertEquals( wc_price( $values['total'] ), WC()->cart->get_total() ); + $this->assertEquals( wc_price( $values['total_ex_tax'] ), WC()->cart->get_total_ex_tax() ); + + WC()->cart->empty_cart(); + } + } + + /** + * Test needs_shipping_address method. + */ + public function test_needs_shipping_address() { + $needs_shipping_address = false; + + if ( WC()->cart->needs_shipping() === true && ! wc_ship_to_billing_address_only() ) { + $needs_shipping_address = true; + } + + $this->assertEquals( apply_filters( 'woocommerce_cart_needs_shipping_address', $needs_shipping_address ), WC()->cart->needs_shipping_address() ); + } + + /** + * Test shipping total. + * + * @since 2.3 + */ + public function test_shipping_total() { + // Create product. + $product = WC_Helper_Product::create_simple_product(); + $product->set_regular_price( 10 ); + $product->save(); + + // Create a flat rate method. + WC_Helper_Shipping::create_simple_flat_rate(); + + // Add product to cart. + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Set an address so that shipping can be calculated. + WC_Helper_Shipping::force_customer_us_address(); + + // Set the flat_rate shipping method. + WC()->session->set( 'chosen_shipping_methods', array( 'flat_rate' ) ); + WC()->cart->calculate_totals(); + + // Test if the shipping total amount is equal 20. + $this->assertEquals( 10, WC()->cart->shipping_total ); + + // Test if the cart total amount is equal 20. + $this->assertEquals( 20, WC()->cart->total ); + } + + /** + * Test that shipping tax rounding does not round down when price are inclusive of taxes. + */ + public function test_calculate_totals_shipping_tax_rounded_26654() { + update_option( 'woocommerce_prices_include_tax', 'yes' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + update_option( 'woocommerce_tax_round_at_subtotal', 'yes' ); + + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '25.0000', + 'tax_rate_name' => 'Tax @ 25%', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => 'standard', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + $product = WC_Helper_Product::create_simple_product(); + $product->set_regular_price( 242 ); + $product->set_tax_class( 'product' ); + $product->save(); + + WC_Helper_Shipping::create_simple_flat_rate( 75.10 ); + WC_Helper_Shipping::force_customer_us_address(); + + WC()->cart->empty_cart(); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + WC()->session->set( 'chosen_shipping_methods', array( 'flat_rate' ) ); + WC()->cart->calculate_totals(); + + $this->assertEquals( 18.775, WC()->cart->get_shipping_tax() ); + $this->assertEquals( 335.88, WC()->cart->get_total( 'edit' ) ); + $this->assertEquals( 67.18, WC()->cart->get_taxes_total() ); + + $checkout = WC_Checkout::instance(); + $order = new WC_Order(); + $checkout->set_data_from_cart( $order ); + $this->assertEquals( 67.18, $order->get_total_tax() ); + $this->assertEquals( 335.88, $order->get_total() ); + $this->assertEquals( 18.775, $order->get_shipping_tax() ); + + update_option( 'woocommerce_tax_round_at_subtotal', 'no' ); + WC()->cart->calculate_totals(); + + $this->assertEquals( 18.78, WC()->cart->get_shipping_tax() ); + $this->assertEquals( 335.88, WC()->cart->get_total( 'edit' ) ); + $this->assertEquals( 67.18, WC()->cart->get_taxes_total() ); + + $order = new WC_Order(); + $checkout->set_data_from_cart( $order ); + $this->assertEquals( 67.18, $order->get_total_tax() ); + $this->assertEquals( 335.88, $order->get_total() ); + $this->assertEquals( 18.78, $order->get_shipping_tax() ); + } + + /** + * Test cart fee. + * + * @since 2.3 + */ + public function test_cart_fee() { + // Create product. + $product = WC_Helper_Product::create_simple_product(); + $product->set_regular_price( 10 ); + $product->save(); + + // Add fee. + WC_Helper_Fee::add_cart_fee(); + + // Add product to cart. + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Test if the cart total amount is equal 20. + $this->assertEquals( 20, WC()->cart->total ); + } + + /** + * Test cart fee with taxes. + * + * @since 3.2 + */ + public function test_cart_fee_taxes() { + $expected_values = array( + 1 => 20, // Tax exempt customer. + 0 => 22, // Normal customer. + ); + + // Set up taxes. + update_option( 'woocommerce_calc_taxes', 'yes' ); + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '10.0000', + 'tax_rate_name' => 'TAX', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + // Create product. + $product = WC_Helper_Product::create_simple_product(); + $product->set_regular_price( 10 ); + $product->save(); + + foreach ( $expected_values as $customer_tax_exempt => $value ) { + WC()->customer->set_is_vat_exempt( $customer_tax_exempt ); + + // Add fee. + WC_Helper_Fee::add_cart_fee( 'taxed' ); + + // Add product to cart. + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Test if the cart total amount is equal 22 ($10 item + $10 fee + 10% taxes). + $this->assertEquals( $value, WC()->cart->total ); + + WC()->cart->empty_cart(); + } + } + + /** + * Test negative cart fee. + * + * @since 3.2 + */ + public function test_cart_negative_fee() { + // Create product. + $product = WC_Helper_Product::create_simple_product(); + $product->set_regular_price( 15 ); + $product->save(); + + // Add fee. + WC_Helper_Fee::add_cart_fee( 'negative' ); + + // Add product to cart. + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Test if the cart total amount is equal 5. + $this->assertEquals( 5, WC()->cart->total ); + } + + /** + * Test negative cart fee with taxes. + * + * @since 3.2 + */ + public function test_cart_negative_fee_taxes() { + $expected_values = array( + 1 => 5.00, // Tax exempt customer. + 0 => 5.50, // Normal customer: cart total amount is equal 5.50 ($15 item - $10 negative fee + 10% tax). + ); + + // Set up taxes. + update_option( 'woocommerce_calc_taxes', 'yes' ); + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '10.0000', + 'tax_rate_name' => 'TAX', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + // Create product. + $product = WC_Helper_Product::create_simple_product(); + $product->set_regular_price( 15 ); + $product->save(); + + foreach ( $expected_values as $customer_tax_exempt => $value ) { + WC()->customer->set_is_vat_exempt( $customer_tax_exempt ); + + // Add fee. + WC_Helper_Fee::add_cart_fee( 'negative-taxed' ); + + // Add product to cart. + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Test if the cart total amount is correct. + $this->assertEquals( $value, WC()->cart->total ); + + WC()->cart->empty_cart(); + } + } + + /** + * Test cart coupons. + */ + public function test_get_coupons() { + // Create coupon. + $coupon = WC_Helper_Coupon::create_coupon(); + + // Add coupon. + WC()->cart->add_discount( $coupon->get_code() ); + + $this->assertEquals( count( WC()->cart->get_coupons() ), 1 ); + } + + /** + * Test add_discount allows coupons by code but not by ID. + * + * @since 3.2 + */ + public function test_add_discount_code_id() { + + $coupon = new WC_Coupon(); + $coupon->set_code( 'test' ); + $coupon->set_amount( 100 ); + $coupon->save(); + + $success = WC()->cart->add_discount( $coupon->get_code() ); + $this->assertTrue( $success ); + + $success = WC()->cart->add_discount( (string) $coupon->get_id() ); + $this->assertFalse( $success ); + } + + /** + * test_add_invidual_use_coupon. + */ + public function test_add_invidual_use_coupon() { + $iu_coupon = WC_Helper_Coupon::create_coupon( 'code1' ); + $iu_coupon->set_individual_use( true ); + $iu_coupon->save(); + $coupon = WC_Helper_Coupon::create_coupon(); + + WC()->cart->add_discount( $iu_coupon->get_code() ); + WC()->cart->add_discount( $coupon->get_code() ); + + $coupons = WC()->cart->get_coupons(); + + $this->assertEquals( count( $coupons ), 1 ); + $this->assertEquals( 'code1', reset( $coupons )->get_code() ); + } + + /** + * test_add_individual_use_coupon_removal. + */ + public function test_add_individual_use_coupon_removal() { + $coupon = WC_Helper_Coupon::create_coupon(); + $iu_coupon = WC_Helper_Coupon::create_coupon( 'code1' ); + $iu_coupon->set_individual_use( true ); + $iu_coupon->save(); + + WC()->cart->add_discount( $coupon->get_code() ); + WC()->cart->add_discount( $iu_coupon->get_code() ); + + $coupons = WC()->cart->get_coupons(); + + $this->assertEquals( count( $coupons ), 1 ); + $this->assertEquals( 'code1', reset( $coupons )->get_code() ); + $this->assertEquals( 1, did_action( 'woocommerce_removed_coupon' ) ); + } + + /** + * test_add_individual_use_coupon_double_individual. + */ + public function test_add_individual_use_coupon_double_individual() { + $iu_coupon1 = WC_Helper_Coupon::create_coupon( 'code1' ); + $iu_coupon1->set_individual_use( true ); + $iu_coupon1->save(); + + $iu_coupon2 = WC_Helper_Coupon::create_coupon( 'code2' ); + $iu_coupon2->set_individual_use( true ); + $iu_coupon2->save(); + + WC()->cart->add_discount( $iu_coupon1->get_code() ); + WC()->cart->add_discount( $iu_coupon2->get_code() ); + + $coupons = WC()->cart->get_coupons(); + + $this->assertEquals( count( $coupons ), 1 ); + $this->assertEquals( 'code2', reset( $coupons )->get_code() ); + } + + /** + * test_clone_cart. + */ + public function test_clone_cart() { + $cart = wc()->cart; + $new_cart = clone $cart; + $is_identical_cart = $cart === $new_cart; + + // Cloned carts should not be identical. + $this->assertFalse( $is_identical_cart, 'Cloned cart not identical to original cart' ); + } + + /** + * test_cloned_cart_session. + */ + public function test_cloned_cart_session() { + $cart = wc()->cart; + $new_cart = clone $cart; + + // Allow accessing protected properties. + $reflected_cart = new ReflectionClass( $cart ); + $cart_session = $reflected_cart->getProperty( 'session' ); + $cart_session->setAccessible( true ); + $reflected_new_cart = new ReflectionClass( $new_cart ); + $new_cart_session = $reflected_new_cart->getProperty( 'session' ); + $new_cart_session->setAccessible( true ); + + // Ensure that cloned properties are not identical. + $identical_sessions = $cart_session->getValue( $cart ) === $new_cart_session->getValue( $new_cart ); + $this->assertFalse( $identical_sessions, 'Cloned cart sessions should not be identical to original cart' ); + } + + /** + * test_cloned_cart_fees. + */ + public function test_cloned_cart_fees() { + $cart = wc()->cart; + $new_cart = clone $cart; + + // Get the properties from each object. + $cart_fees = $cart->fees_api(); + $new_cart_fees = $new_cart->fees_api(); + + // Ensure that cloned properties are not identical. + $identical_fees = $cart_fees === $new_cart_fees; + $this->assertFalse( $identical_fees, 'Cloned cart fees should not be identical to original cart.' ); + } + + /** + * test_cart_object_istantiation. + */ + public function test_cart_object_istantiation() { + $cart = new WC_Cart(); + $this->assertInstanceOf( 'WC_Cart', $cart ); + } + + /** + * test_get_cart_item_quantities. + */ + public function test_get_cart_item_quantities() { + // Create dummy product. + $product = WC_Helper_Product::create_simple_product(); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + $this->assertEquals( 1, array_sum( WC()->cart->get_cart_item_quantities() ) ); + } + + /** + * test_get_cart_contents_weight. + */ + public function test_get_cart_contents_weight() { + // Create dummy product. + $product = WC_Helper_Product::create_simple_product(); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + $this->assertEquals( 1.1, WC()->cart->get_cart_contents_weight() ); + } + + /** + * test_check_cart_items. + */ + public function test_check_cart_items() { + // Create dummy product. + $product = WC_Helper_Product::create_simple_product(); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + $this->assertEquals( true, WC()->cart->check_cart_items() ); + } + + /** + * test_check_cart_item_stock. + */ + public function test_check_cart_item_stock() { + // Create dummy product. + $product = WC_Helper_Product::create_simple_product(); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + $this->assertEquals( true, WC()->cart->check_cart_item_stock() ); + } + + /** + * test_get_cross_sells. + */ + public function test_get_cross_sells() { + // Create dummy product. + $product = WC_Helper_Product::create_simple_product(); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + $this->assertEquals( array(), WC()->cart->get_cross_sells() ); + } + + /** + * test_get_tax_totals. + */ + public function test_get_tax_totals() { + // Set calc taxes option. + update_option( 'woocommerce_calc_taxes', 'yes' ); + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '10.0000', + 'tax_rate_name' => 'TAX', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + // Create dummy product. + $product = WC_Helper_Product::create_simple_product(); + + WC()->customer->set_is_vat_exempt( false ); + + // Add product to cart (10). + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Check. + $tax_totals = WC()->cart->get_tax_totals(); + $this->assertArrayHasKey( 'TAX-1', $tax_totals ); + $this->assertEquals( 1, $tax_totals['TAX-1']->amount ); + $this->assertEquals( false, $tax_totals['TAX-1']->is_compound ); + $this->assertEquals( 'TAX', $tax_totals['TAX-1']->label ); + + WC()->cart->empty_cart(); + + // Test the same for tax exempt customer. + WC()->customer->set_is_vat_exempt( true ); + + // Add product to cart (10). + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Check. + $tax_totals = WC()->cart->get_tax_totals(); + $this->assertEquals( array(), $tax_totals ); + } + + /** + * Test is_coupon_emails_allowed function on the cart, specifically test wildcard emails. + * + * @return void + */ + public function test_is_coupon_emails_allowed() { + $this->assertEquals( true, WC()->cart->is_coupon_emails_allowed( array( 'customer@wc.local' ), array( '*.local' ) ) ); + $this->assertEquals( false, WC()->cart->is_coupon_emails_allowed( array( 'customer@wc.local' ), array( '*.test' ) ) ); + $this->assertEquals( true, WC()->cart->is_coupon_emails_allowed( array( 'customer@wc.local' ), array( 'customer@wc.local' ) ) ); + $this->assertEquals( false, WC()->cart->is_coupon_emails_allowed( array( 'customer@wc.local' ), array( 'customer2@wc.local' ) ) ); + } + + /** + * Check subtotals align when using filters. Ref: 23340 + */ + public function test_changing_tax_class_via_filter_issue_23340() { + // Store is set to enter product prices inclusive tax. + update_option( 'woocommerce_prices_include_tax', 'yes' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + + // 5% tax. + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '5.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + // 20% tax. + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => 'reduced-rate', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + // Create products and add them to cart. + $product1 = new WC_Product_Simple(); + $product1->set_regular_price( '6' ); + $product1->save(); + + WC()->cart->add_to_cart( $product1->get_id(), 1 ); + WC()->cart->calculate_totals(); + + $this->assertEquals( '5.71', WC()->cart->get_subtotal() ); + $this->assertEquals( '6.00', WC()->cart->get_total( 'edit' ) ); + + add_filter( 'woocommerce_product_get_tax_class', array( $this, 'change_tax_class_filter' ) ); + add_filter( 'woocommerce_product_variation_get_tax_class', array( $this, 'change_tax_class_filter' ) ); + + WC()->cart->calculate_totals(); + $this->assertEquals( '5.71', WC()->cart->get_subtotal() ); + $this->assertEquals( '6.85', WC()->cart->get_total( 'edit' ) ); + + remove_filter( 'woocommerce_product_get_tax_class', array( $this, 'change_tax_class_filter' ) ); + remove_filter( 'woocommerce_product_variation_get_tax_class', array( $this, 'change_tax_class_filter' ) ); + } + + /** + * Test rounding with fees as described in Github issue 25629. + */ + public function test_rounding_with_fees_25629() { + update_option( 'woocommerce_prices_include_tax', 'yes' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + update_option( 'woocommerce_tax_round_at_subtotal', 'yes' ); + + WC()->cart->empty_cart(); + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '10.0000', + 'tax_rate_name' => 'TAX10', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + $product = WC_Helper_Product::create_simple_product(); + $product->set_regular_price( 71.50 ); + $product->save(); + + WC_Helper_Coupon::create_coupon( + '3percent', + array( + 'discount_type' => 'percent', + 'coupon_amount' => 3, + ) + ); + + add_action( 'woocommerce_cart_calculate_fees', array( $this, 'add_fee_1_5_to_cart' ) ); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + WC()->cart->apply_coupon( '3percent' ); + WC()->cart->calculate_totals(); + remove_action( 'woocommerce_cart_calculate_fees', array( $this, 'add_fee_1_5_to_cart' ) ); + + $this->assertEquals( 70.86, WC()->cart->get_total( 'edit' ) ); + } + + /** + * Test that adding a variation with URL parameter increases the quantity appropriately + * as described in issue 24000. + */ + public function test_add_variation_by_url() { + add_filter( 'woocommerce_add_to_cart_redirect', '__return_false' ); + update_option( 'woocommerce_cart_redirect_after_add', 'no' ); + WC()->cart->empty_cart(); + WC()->session->set( 'wc_notices', null ); + + $product = WC_Helper_Product::create_variation_product(); + $variations = $product->get_available_variations(); + $variation = current( + array_filter( + $variations, + function( $variation ) { + return 'DUMMY SKU VARIABLE HUGE RED 2' === $variation['sku']; + } + ) + ); + + // Add variation with add_to_cart_action. + $_REQUEST['add-to-cart'] = $variation['variation_id']; + WC_Form_Handler::add_to_cart_action( false ); + $notices = WC()->session->get( 'wc_notices', array() ); + + // Reset filter / REQUEST variables. + unset( $_REQUEST['add-to-cart'] ); + remove_filter( 'woocommerce_add_to_cart_redirect', '__return_false' ); + + // Check if the item is in the cart. + $this->assertCount( 1, WC()->cart->get_cart_contents() ); + $this->assertEquals( 1, WC()->cart->get_cart_contents_count() ); + + // Check that there are no error notices. + $this->assertArrayNotHasKey( 'error', $notices ); + + // Add variation using parent id. + WC()->cart->add_to_cart( + $product->get_id(), + 1, + $variation['variation_id'], + array( + 'attribute_pa_size' => 'huge', + 'attribute_pa_colour' => 'red', + 'attribute_pa_number' => '2', + ) + ); + $notices = WC()->session->get( 'wc_notices', array() ); + + // Check that the second add to cart call increases the quantity of the existing cart-item. + $this->assertCount( 1, WC()->cart->get_cart_contents() ); + $this->assertEquals( 2, WC()->cart->get_cart_contents_count() ); + + // Check that there are no error notices. + $this->assertArrayNotHasKey( 'error', $notices ); + } + + /** + * Test that adding a variation via URL parameter fails when specifying a value for the attribute + * that differs from a value belonging to that variant. + */ + public function test_add_variation_by_url_with_invalid_attribute() { + add_filter( 'woocommerce_add_to_cart_redirect', '__return_false' ); + update_option( 'woocommerce_cart_redirect_after_add', 'no' ); + WC()->cart->empty_cart(); + WC()->session->set( 'wc_notices', null ); + + $product = WC_Helper_Product::create_variation_product(); + $variations = $product->get_available_variations(); + $variation = array_pop( $variations ); + + // Attempt adding variation with add_to_cart_action, specifying a different colour. + $_REQUEST['add-to-cart'] = $variation['variation_id']; + $_REQUEST['attribute_pa_colour'] = 'green'; + WC_Form_Handler::add_to_cart_action( false ); + $notices = WC()->session->get( 'wc_notices', array() ); + + // Reset filter / REQUEST variables. + unset( $_REQUEST['add-to-cart'] ); + unset( $_REQUEST['attribute_pa_colour'] ); + remove_filter( 'woocommerce_add_to_cart_redirect', '__return_false' ); + + // Check that the notices contain an error message about an invalid colour. + $this->assertArrayHasKey( 'error', $notices ); + $this->assertCount( 1, $notices['error'] ); + $this->assertEquals( 'Invalid value posted for colour', $notices['error'][0]['notice'] ); + } + + /** + * Test that adding a variation via URL parameter succeeds when some attributes belong to the + * variation and others are specificed via URL parameter. + */ + public function test_add_variation_by_url_with_valid_attribute() { + add_filter( 'woocommerce_add_to_cart_redirect', '__return_false' ); + update_option( 'woocommerce_cart_redirect_after_add', 'no' ); + WC()->cart->empty_cart(); + WC()->session->set( 'wc_notices', null ); + + $product = WC_Helper_Product::create_variation_product(); + $variations = $product->get_available_variations(); + $variation = array_shift( $variations ); + + // Attempt adding variation with add_to_cart_action, specifying attributes not defined in the variation. + $_REQUEST['add-to-cart'] = $variation['variation_id']; + $_REQUEST['attribute_pa_colour'] = 'red'; + $_REQUEST['attribute_pa_number'] = '1'; + WC_Form_Handler::add_to_cart_action( false ); + $notices = WC()->session->get( 'wc_notices', array() ); + + // Reset filter / REQUEST variables. + unset( $_REQUEST['add-to-cart'] ); + unset( $_REQUEST['attribute_pa_colour'] ); + unset( $_REQUEST['attribute_pa_number'] ); + remove_filter( 'woocommerce_add_to_cart_redirect', '__return_false' ); + + // Check if the item is in the cart. + $this->assertCount( 1, WC()->cart->get_cart_contents() ); + $this->assertEquals( 1, WC()->cart->get_cart_contents_count() ); + + // Check that there are no error notices. + $this->assertArrayNotHasKey( 'error', $notices ); + } + + /** + * Test that adding a varition via URL parameter fails when an 'any' attribute is missing. + */ + public function test_add_variation_by_url_fails_with_missing_any_attribute() { + add_filter( 'woocommerce_add_to_cart_redirect', '__return_false' ); + update_option( 'woocommerce_cart_redirect_after_add', 'no' ); + WC()->cart->empty_cart(); + WC()->session->set( 'wc_notices', null ); + + $product = WC_Helper_Product::create_variation_product(); + $variations = $product->get_available_variations(); + $variation = array_shift( $variations ); + + // Attempt adding variation with add_to_cart_action, without specifying attribute_pa_colour. + $_REQUEST['add-to-cart'] = $variation['variation_id']; + $_REQUEST['attribute_pa_number'] = ''; + WC_Form_Handler::add_to_cart_action( false ); + $notices = WC()->session->get( 'wc_notices', array() ); + + // Reset filter / REQUEST variables. + unset( $_REQUEST['add-to-cart'] ); + unset( $_REQUEST['attribute_pa_number'] ); + remove_filter( 'woocommerce_add_to_cart_redirect', '__return_false' ); + + // Verify that there is nothing in the cart. + $this->assertCount( 0, WC()->cart->get_cart_contents() ); + $this->assertEquals( 0, WC()->cart->get_cart_contents_count() ); + + // Check that the notices contain an error message about invalid colour and number. + $this->assertArrayHasKey( 'error', $notices ); + $this->assertCount( 1, $notices['error'] ); + $this->assertEquals( 'colour and number are required fields', $notices['error'][0]['notice'] ); + } + + /** + * Helper function. Adds 1.5 taxable fees to cart. + */ + public function add_fee_1_5_to_cart() { + WC()->cart->add_fee( 'Dummy fee', 1.5 / 1.1, true ); + } + + /** + * Change tax class. + * + * @return string + */ + public function change_tax_class_filter() { + return 'reduced-rate'; + } +} diff --git a/tests/legacy/unit-tests/cart/functions.php b/plugins/woocommerce/tests/legacy/unit-tests/cart/functions.php similarity index 100% rename from tests/legacy/unit-tests/cart/functions.php rename to plugins/woocommerce/tests/legacy/unit-tests/cart/functions.php diff --git a/plugins/woocommerce/tests/legacy/unit-tests/checkout/checkout.php b/plugins/woocommerce/tests/legacy/unit-tests/checkout/checkout.php new file mode 100644 index 00000000000..0d273e91519 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/checkout/checkout.php @@ -0,0 +1,314 @@ +cart->empty_cart(); + } + + /** + * Setup. + */ + public function setUp(): void { + parent::setUp(); + WC()->cart->empty_cart(); + } + + /** + * Test if order can be created when a coupon with usage limit is applied. + * + * @throws Exception When unable to create order. + */ + public function test_create_order_with_limited_coupon() { + $coupon_code = 'coupon4one'; + $coupon_data_store = WC_Data_Store::load( 'coupon' ); + $coupon = WC_Helper_Coupon::create_coupon( + $coupon_code, + array( 'usage_limit' => 1 ) + ); + $product = WC_Helper_Product::create_simple_product( true ); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + WC()->cart->add_discount( $coupon->get_code() ); + $checkout = WC_Checkout::instance(); + $order_id = $checkout->create_order( + array( + 'billing_email' => 'a@b.com', + 'payment_method' => 'dummy_payment_gateway', + ) + ); + $this->assertNotWPError( $order_id ); + $order = new WC_Order( $order_id ); + $coupon_held_key = $order->get_data_store()->get_coupon_held_keys( $order ); + $this->assertEquals( count( $coupon_held_key ), 1 ); + $this->assertEquals( array_keys( $coupon_held_key )[0], $coupon->get_id() ); + $this->assertEquals( strpos( $coupon_held_key[ $coupon->get_id() ], '_coupon_held_' ), 0 ); + $this->assertEquals( $coupon_data_store->get_tentative_usage_count( $coupon->get_id() ), 1 ); + + $order2_id = $checkout->create_order( + array( + 'billing_email' => 'a@c.com', + 'payment_method' => 'dummy_payment_gateway', + ) + ); + $this->assertWPError( $order2_id ); + $this->assertEquals( $coupon_data_store->get_tentative_usage_count( $coupon->get_id() ), 1 ); + } + + /** + * Test when order is created with multiple coupon when usage limit for one is exhausted. + * + * @throws Exception When unable to create an order. + */ + public function test_create_order_with_multiple_limited_coupons() { + $coupon_code1 = 'coupon1'; + $coupon_code2 = 'coupon2'; + $coupon_data_store = WC_Data_Store::load( 'coupon' ); + + $coupon1 = WC_Helper_Coupon::create_coupon( + $coupon_code1, + array( 'usage_limit' => 2 ) + ); + $coupon2 = WC_Helper_Coupon::create_coupon( + $coupon_code2, + array( 'usage_limit' => 1 ) + ); + $product = WC_Helper_Product::create_simple_product( true ); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + WC()->cart->add_discount( $coupon_code1 ); + WC()->cart->add_discount( $coupon_code2 ); + $checkout = WC_Checkout::instance(); + $order_id1 = $checkout->create_order( + array( + 'billing_email' => 'a@b.com', + 'payment_method' => 'dummy_payment_gateway', + ) + ); + + $this->assertNotWPError( $order_id1 ); + $this->assertEquals( $coupon_data_store->get_tentative_usage_count( $coupon1->get_id() ), 1 ); + $this->assertEquals( $coupon_data_store->get_tentative_usage_count( $coupon2->get_id() ), 1 ); + + $order2_id = $checkout->create_order( + array( + 'billing_email' => 'a@b.com', + 'payment_method' => 'dummy_payment_gateway', + ) + ); + + $this->assertWPError( $order2_id ); + $this->assertEquals( $coupon_data_store->get_tentative_usage_count( $coupon1->get_id() ), 1 ); + $this->assertEquals( $coupon_data_store->get_tentative_usage_count( $coupon2->get_id() ), 1 ); + } + + /** + * Test when `usage_count` meta is deleted for some reason. + * + * @throws Exception When unable to create order. + */ + public function test_create_order_with_usage_limit_deleted() { + $coupon_code = 'coupon4one'; + $coupon_data_store = WC_Data_Store::load( 'coupon' ); + $coupon = WC_Helper_Coupon::create_coupon( + $coupon_code, + array( 'usage_limit' => 1 ) + ); + + delete_post_meta( $coupon->get_id(), 'usage_count' ); + + $product = WC_Helper_Product::create_simple_product( true ); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + WC()->cart->add_discount( $coupon->get_code() ); + $checkout = WC_Checkout::instance(); + $order_id = $checkout->create_order( + array( + 'billing_email' => 'a@b.com', + 'payment_method' => 'dummy_payment_gateway', + ) + ); + $this->assertNotWPError( $order_id ); + $this->assertEquals( $coupon_data_store->get_tentative_usage_count( $coupon->get_id() ), 1 ); + } + + /** + * Test usage limit for guest users usage limit per user is set. + * + * @throws Exception When unable to create order. + */ + public function test_usage_limit_per_user_for_guest() { + wp_set_current_user( 0 ); + wc_clear_notices(); + $coupon_code = 'coupon4one'; + $coupon = WC_Helper_Coupon::create_coupon( + $coupon_code, + array( 'usage_limit_per_user' => 1 ) + ); + $product = WC_Helper_Product::create_simple_product( true ); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + WC()->cart->add_discount( $coupon->get_code() ); + $checkout = WC_Checkout::instance(); + $posted_data = array( + 'billing_email' => 'a@b.com', + 'payment_method' => 'dummy_payment_gateway', + ); + $order_id = $checkout->create_order( $posted_data ); + $this->assertNotWPError( $order_id ); + + WC()->cart->add_to_cart( $product->get_id(), 1 ); + WC()->cart->add_discount( $coupon->get_code() ); + WC()->cart->check_customer_coupons( $posted_data ); + $this->assertTrue( wc_has_notice( $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST ), 'error' ) ); + } + + /** + * Helper function to return 0.01. + * + * @return float + */ + public function __return_0_01() { + return 0.01; + } + + /** + * Helper method to create a managed product and a order for that product. + * + * @return array + * @throws Exception When unable to create an order . + */ + protected function create_order_for_managed_inventory_product() { + $product = WC_Helper_Product::create_simple_product(); + $product->set_props( array( 'manage_stock' => true ) ); + $product->set_stock_quantity( 10 ); + $product->save(); + + WC()->cart->add_to_cart( $product->get_id(), 9 ); + $this->assertEquals( true, WC()->cart->check_cart_items() ); + + $checkout = WC_Checkout::instance(); + $order_id = $checkout->create_order( + array( + 'payment_method' => 'cod', + 'billing_email' => 'a@b.com', + ) + ); + + // Assertions whether the order was created successfully. + $this->assertNotWPError( $order_id ); + $order = wc_get_order( $order_id ); + + return array( $product, $order ); + } + + /** + * Test when order is out stock because it is held by an order in pending status. + * + * @throws Exception When unable to create order. + */ + public function test_create_order_when_out_of_stock() { + list( $product, $order ) = $this->create_order_for_managed_inventory_product(); + + $this->assertEquals( 9, $order->get_item_count() ); + $this->assertEquals( 'pending', $order->get_status() ); + $this->assertEquals( 9, wc_get_held_stock_quantity( $product ) ); + + WC()->cart->empty_cart(); + WC()->cart->add_to_cart( $product->get_stock_managed_by_id(), 2 ); + + $this->assertEquals( false, WC()->cart->check_cart_items() ); + } + + /** + * Test if pending stock is cleared when order is cancelled. + * + * @throws Exception When unable to create order. + */ + public function test_pending_is_cleared_when_order_is_cancelled() { + list( $product, $order ) = $this->create_order_for_managed_inventory_product(); + + $this->assertEquals( 9, wc_get_held_stock_quantity( $product ) ); + $order->set_status( 'cancelled' ); + $order->save(); + + $this->assertEquals( 0, wc_get_held_stock_quantity( $product ) ); + $this->assertEquals( 10, $product->get_stock_quantity() ); + + } + + /** + * Test if pending stock is cleared when order is processing. + * + * @throws Exception When unable to create order. + */ + public function test_pending_is_cleared_when_order_processed() { + list( $product, $order ) = $this->create_order_for_managed_inventory_product(); + + $this->assertEquals( 9, wc_get_held_stock_quantity( $product ) ); + $order->set_status( 'processing' ); + $order->save(); + + $this->assertEquals( 0, wc_get_held_stock_quantity( $product ) ); + } + + /** + * Test creating order from managed stock for variable product. + * + * @throws Exception When unable to create an order. + */ + public function test_create_order_for_variation_product() { + $parent_product = WC_Helper_Product::create_variation_product(); + $variation = $parent_product->get_available_variations()[0]; + $variation = wc_get_product( $variation['variation_id'] ); + $variation->set_manage_stock( true ); + $variation->set_stock_quantity( 10 ); + $variation->save(); + WC()->cart->add_to_cart( + $variation->get_id(), + 9, + 0, + array( + 'attribute_pa_colour' => 'red', // Set a value since this is an 'any' attribute. + 'attribute_pa_number' => '2', // Set a value since this is an 'any' attribute. + ) + ); + $this->assertEquals( true, WC()->cart->check_cart_items() ); + + $checkout = WC_Checkout::instance(); + $order_id = $checkout->create_order( + array( + 'payment_method' => 'cod', + 'billing_email' => 'a@b.com', + ) + ); + + // Assertions whether the first order was created successfully. + $this->assertNotWPError( $order_id ); + $order = wc_get_order( $order_id ); + + $this->assertEquals( 9, $order->get_item_count() ); + $this->assertEquals( 'pending', $order->get_status() ); + $this->assertEquals( 9, wc_get_held_stock_quantity( $variation ) ); + + WC()->cart->empty_cart(); + WC()->cart->add_to_cart( + $variation->get_stock_managed_by_id(), + 2, + 0, + array( + 'attribute_pa_colour' => 'red', + 'attribute_pa_number' => '2', + ) + ); + + $this->assertEquals( false, WC()->cart->check_cart_items() ); + } +} diff --git a/plugins/woocommerce/tests/legacy/unit-tests/core/main-class.php b/plugins/woocommerce/tests/legacy/unit-tests/core/main-class.php new file mode 100644 index 00000000000..bc90cbf5fce --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/core/main-class.php @@ -0,0 +1,84 @@ +wc = WC(); + } + + /** + * Test WC has static instance. + * + * @since 2.2 + */ + public function test_wc_instance() { + $this->assertClassHasStaticAttribute( '_instance', 'WooCommerce' ); + } + + /** + * Test that all WC constants are set. + * + * @since 2.2 + */ + public function test_constants() { + $this->assertEquals( str_replace( 'tests/legacy/unit-tests/core/', '', plugin_dir_path( __FILE__ ) ) . 'woocommerce.php', WC_PLUGIN_FILE ); + $this->assertEquals( $this->wc->version, Constants::get_constant( 'WC_VERSION' ) ); + $this->assertEquals( WC_VERSION, WOOCOMMERCE_VERSION ); + $this->assertEquals( 6, WC_ROUNDING_PRECISION ); + $this->assertEquals( 2, WC_DISCOUNT_ROUNDING_MODE ); + $this->assertEquals( 'wc_session_id', WC_SESSION_CACHE_GROUP ); + $this->assertContains( WC_TAX_ROUNDING_MODE, array( 2, 1, 'auto' ) ); + $this->assertEquals( '|', WC_DELIMITER ); + $this->assertNotEquals( WC_LOG_DIR, '' ); + $this->assertEquals( false, WC_TEMPLATE_DEBUG_MODE ); + $this->assertEquals( $this->wc->template_path(), WC_TEMPLATE_PATH ); + } + + /** + * Test class instance. + * + * @since 2.2 + */ + public function test_wc_class_instances() { + $this->assertInstanceOf( 'WooCommerce', $this->wc ); + $this->assertInstanceOf( 'WC_Product_Factory', $this->wc->product_factory ); + $this->assertInstanceOf( 'WC_Order_Factory', $this->wc->order_factory ); + $this->assertInstanceOf( 'WC_Countries', $this->wc->countries ); + $this->assertInstanceOf( 'WC_Integrations', $this->wc->integrations ); + $this->assertInstanceOf( 'WC_Cart', $this->wc->cart ); + $this->assertInstanceOf( 'WC_Customer', $this->wc->customer ); + $this->assertInstanceOf( 'WC_Session', $this->wc->session ); + $this->assertInstanceOf( 'WC_Query', $this->wc->query ); + $this->assertInstanceOf( 'WC_Structured_Data', $this->wc->structured_data ); + $this->assertInstanceOf( 'WC_Deprecated_Action_Hooks', $this->wc->deprecated_hook_handlers['actions'] ); + $this->assertInstanceOf( 'WC_Deprecated_Filter_Hooks', $this->wc->deprecated_hook_handlers['filters'] ); + $this->assertInstanceOf( 'WC_Emails', $this->wc->mailer() ); + $this->assertInstanceOf( 'WC_Payment_Gateways', $this->wc->payment_gateways() ); + $this->assertInstanceOf( 'WC_Checkout', $this->wc->checkout() ); + } +} diff --git a/plugins/woocommerce/tests/legacy/unit-tests/core/post-types-admin.php b/plugins/woocommerce/tests/legacy/unit-tests/core/post-types-admin.php new file mode 100644 index 00000000000..55c122e9f1d --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/core/post-types-admin.php @@ -0,0 +1,50 @@ +wc_cpt = new WC_Admin_Post_Types(); + } + + /** + * Check if filename is extended and extension is preserved. + */ + public function test_unique_filename() { + $full_filename = 'dummy_filename.csv'; + $ext = '.csv'; + + $unique_filename = $this->wc_cpt->unique_filename( $full_filename, $ext ); + $this->assertEquals( strlen( $full_filename ) + 6 + 1, strlen( $unique_filename ) ); + $this->assertEquals( $ext, substr( $unique_filename, -4 ) ); + } + + /** + * Check if filename is extended properly when its very long. + */ + public function test_unique_filename_for_large_name() { + $full_filename = str_repeat( 'w', 250 ) . '.csv'; + $ext = '.csv'; + $unique_filename = $this->wc_cpt->unique_filename( $full_filename, $ext ); + $this->assertEquals( 254, strlen( $unique_filename ) ); + $this->assertEquals( $ext, substr( $unique_filename, - 4 ) ); + } +} diff --git a/tests/legacy/unit-tests/core/taxonomies.php b/plugins/woocommerce/tests/legacy/unit-tests/core/taxonomies.php similarity index 100% rename from tests/legacy/unit-tests/core/taxonomies.php rename to plugins/woocommerce/tests/legacy/unit-tests/core/taxonomies.php diff --git a/tests/legacy/unit-tests/core/template-cache.php b/plugins/woocommerce/tests/legacy/unit-tests/core/template-cache.php similarity index 100% rename from tests/legacy/unit-tests/core/template-cache.php rename to plugins/woocommerce/tests/legacy/unit-tests/core/template-cache.php diff --git a/plugins/woocommerce/tests/legacy/unit-tests/countries/countries.php b/plugins/woocommerce/tests/legacy/unit-tests/countries/countries.php new file mode 100644 index 00000000000..d2e6c0e76d4 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/countries/countries.php @@ -0,0 +1,224 @@ +assertEquals( $countries->get_countries(), $countries->countries ); + $this->assertEquals( $countries->get_states(), $countries->states ); + } + + /** + * Test get_shipping_continents. + * + * @since 3.6.0 + */ + public function test_get_shipping_continents() { + $countries = new WC_Countries(); + $all_continents = $countries->get_continents(); + + update_option( 'woocommerce_ship_to_countries', 'all' ); + $this->assertSame( $all_continents, $countries->get_shipping_continents() ); + + update_option( 'woocommerce_ship_to_countries', 'specific' ); + update_option( 'woocommerce_specific_ship_to_countries', array( 'CA', 'JP' ) ); + $expected = array( + 'AS' => $all_continents['AS'], + 'NA' => $all_continents['NA'], + ); + $this->assertSame( $expected, $countries->get_shipping_continents() ); + } + + /** + * Test get_allowed_countries. + * + * @since 3.1 + */ + public function test_get_allowed_countries() { + $countries = new WC_Countries(); + + update_option( 'woocommerce_allowed_countries', 'specific' ); + update_option( 'woocommerce_specific_allowed_countries', array( 'RO', 'SI' ) ); + $expected = array( + 'RO' => 'Romania', + 'SI' => 'Slovenia', + ); + $this->assertEquals( $expected, $countries->get_allowed_countries() ); + + update_option( 'woocommerce_allowed_countries', 'all' ); + $this->assertEquals( $countries->get_countries(), $countries->get_allowed_countries() ); + + update_option( 'woocommerce_allowed_countries', 'all_except' ); + update_option( 'woocommerce_all_except_countries', array( 'RO', 'SI' ) ); + $allowed_countries = $countries->get_allowed_countries(); + $this->assertEquals( count( $countries->get_countries() ) - 2, count( $allowed_countries ) ); + $this->assertFalse( isset( $allowed_countries['RO'], $allowed_countries['SI'] ) ); + } + + /** + * Test get_shipping_countries. + * + * @since 3.1 + */ + public function test_get_shipping_countries() { + $countries = new WC_Countries(); + + update_option( 'woocommerce_ship_to_countries', '' ); + $this->assertEquals( $countries->get_allowed_countries(), $countries->get_shipping_countries() ); + + update_option( 'woocommerce_allowed_countries', 'specific' ); + update_option( 'woocommerce_specific_allowed_countries', array( 'RO', 'SI' ) ); + $this->assertEquals( $countries->get_allowed_countries(), $countries->get_shipping_countries() ); + + update_option( 'woocommerce_ship_to_countries', 'all' ); + $this->assertEquals( $countries->get_countries(), $countries->get_shipping_countries() ); + + update_option( 'woocommerce_ship_to_countries', 'specific' ); + update_option( 'woocommerce_specific_ship_to_countries', array( 'RO', 'SI' ) ); + $expected = array( + 'RO' => 'Romania', + 'SI' => 'Slovenia', + ); + $this->assertEquals( $expected, $countries->get_shipping_countries() ); + } + + /** + * Test get_allowed_country_states. + * + * @since 3.1 + */ + public function test_get_allowed_country_states() { + $countries = new WC_Countries(); + + update_option( 'woocommerce_allowed_countries', 'all' ); + $this->assertEquals( $countries->get_states(), $countries->get_allowed_country_states() ); + + update_option( 'woocommerce_allowed_countries', 'specific' ); + update_option( 'woocommerce_specific_allowed_countries', array( 'US' ) ); + + $all_states = $countries->get_allowed_country_states(); + $us_states = $all_states['US']; + + $this->assertEquals( 'Oregon', $us_states['OR'] ); + $this->assertGreaterThanOrEqual( 50, count( $us_states ) ); + } + + /** + * Test get_shipping_country_states. + * + * @since 3.1 + */ + public function test_get_shipping_country_states() { + $countries = new WC_Countries(); + + update_option( 'woocommerce_ship_to_countries', '' ); + $this->assertEquals( $countries->get_allowed_country_states(), $countries->get_shipping_country_states() ); + + update_option( 'woocommerce_ship_to_countries', 'all' ); + $this->assertEquals( $countries->get_states(), $countries->get_shipping_country_states() ); + + update_option( 'woocommerce_ship_to_countries', 'specific' ); + update_option( 'woocommerce_specific_ship_to_countries', array( 'US' ) ); + + $all_states = $countries->get_shipping_country_states(); + $us_states = $all_states['US']; + + $this->assertEquals( 'Oregon', $us_states['OR'] ); + $this->assertGreaterThanOrEqual( 50, count( $us_states ) ); + } + + /** + * Test shipping_to_prefix. + * + * @since 3.1 + */ + public function test_shipping_to_prefix() { + $countries = new WC_Countries(); + + $this->assertEquals( 'to', $countries->shipping_to_prefix( 'RO' ) ); + $this->assertEquals( 'to the', $countries->shipping_to_prefix( 'US' ) ); + } + + /** + * Test estimated_for_prefix. + * + * @since 3.1 + */ + public function test_estimated_for_prefix() { + $countries = new WC_Countries(); + + $this->assertEquals( 'the ', $countries->estimated_for_prefix( 'GB' ) ); + $this->assertEquals( '', $countries->estimated_for_prefix( 'RO' ) ); + } + + /** + * Test tax_or_vat. + * + * @since 3.1 + */ + public function test_tax_or_vat() { + $countries = new WC_Countries(); + + update_option( 'woocommerce_default_country', 'CZ' ); + $this->assertEquals( 'VAT', $countries->tax_or_vat() ); + + update_option( 'woocommerce_default_country', 'NO' ); + $this->assertEquals( 'VAT', $countries->tax_or_vat() ); + + update_option( 'woocommerce_default_country', 'US:CA' ); + $this->assertEquals( 'Tax', $countries->tax_or_vat() ); + } + + /** + * Test get_country_locale. + * + * @since 3.1 + */ + public function test_get_country_locale() { + $countries = new WC_Countries(); + update_option( 'woocommerce_allowed_countries', 'specific' ); + update_option( 'woocommerce_specific_allowed_countries', array( 'RO', 'SI' ) ); + + $locales = $countries->get_country_locale(); + $this->assertArrayHasKey( 'RO', $locales ); + $this->assertArrayHasKey( 'SI', $locales ); + $this->assertArrayNotHasKey( 'AU', $locales ); + $this->assertArrayHasKey( 'default', $locales ); + } + + /** + * Test get_european_union_countries. + * + * @return void + */ + public function test_get_european_union_countries() { + // After Brexit there should be 27 countries in the EU. + $countries = new WC_Countries(); + $this->assertCount( 27, $countries->get_european_union_countries() ); + } + + /** + * Test get_vat_countries. + * + * @return void + */ + public function test_get_vat_countries() { + $countries = new WC_Countries(); + $this->assertCount( 80, $countries->get_vat_countries() ); + } +} diff --git a/plugins/woocommerce/tests/legacy/unit-tests/coupon/coupon.php b/plugins/woocommerce/tests/legacy/unit-tests/coupon/coupon.php new file mode 100644 index 00000000000..836ca80b011 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/coupon/coupon.php @@ -0,0 +1,349 @@ +customer->set_shipping_country( 'US' ); + WC()->customer->set_shipping_state( 'NY' ); + WC()->customer->set_shipping_postcode( '12345' ); + } + + /** + * Cleans up after the test class. + */ + public function tearDown(): void { + WC()->cart->empty_cart(); + WC()->cart->remove_coupons(); + + parent::tearDown(); + } + + /** + * Test the code/id differentiation of the coupon constructor. + * + * @since 3.2 + */ + public function test_constructor_code_id() { + $string_code_1 = 'test'; + + // Coupon with a standard string code. + $coupon_1 = new WC_Coupon(); + $coupon_1->set_code( $string_code_1 ); + $coupon_1->save(); + + // Coupon with a string code that is the same as coupon 1's ID. + $coupon_2 = new WC_Coupon(); + $coupon_2->set_code( (string) $coupon_1->get_id() ); + $coupon_2->save(); + + $int_id_1 = $coupon_1->get_id(); + $int_id_2 = $coupon_2->get_id(); + $string_code_2 = $coupon_2->get_code(); + + // Test getting a coupon by integer ID. + $test_coupon = new WC_Coupon( $int_id_1 ); + $this->assertEquals( $int_id_1, $test_coupon->get_id() ); + $test_coupon = new WC_Coupon( $int_id_2 ); + $this->assertEquals( $int_id_2, $test_coupon->get_id() ); + + // Test getting a coupon by string code. + $test_coupon = new WC_Coupon( $string_code_1 ); + $this->assertEquals( $string_code_1, $test_coupon->get_code() ); + $test_coupon = new WC_Coupon( $string_code_2 ); + $this->assertEquals( $string_code_2, $test_coupon->get_code() ); + + // Test getting a coupon by string id. + // Required for backwards compatibility, but will try and initialize coupon by code if possible first. + $test_coupon = new WC_Coupon( (string) $coupon_2->get_id() ); + $this->assertEquals( $coupon_2->get_id(), $test_coupon->get_id() ); + + // Test getting a coupon by coupon object. + $test_coupon = new WC_Coupon( $coupon_1 ); + $this->assertEquals( $test_coupon->get_id(), $coupon_1->get_id() ); + } + + /** + * Test add_discount method. + * + * @since 2.3 + */ + public function test_add_discount() { + + // Create coupon. + $coupon = WC_Helper_Coupon::create_coupon(); + + // Add coupon, test return statement. + $this->assertTrue( WC()->cart->add_discount( $coupon->get_code() ) ); + + // Test if total amount of coupons is 1. + $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) ); + } + + /** + * Test add_discount method. + * + * @since 2.3 + */ + public function test_add_discount_duplicate() { + + // Create coupon. + $coupon = WC_Helper_Coupon::create_coupon(); + + // Add coupon. + $this->assertTrue( WC()->cart->add_discount( $coupon->get_code() ) ); + + // Add coupon again, test return statement. + $this->assertFalse( WC()->cart->add_discount( $coupon->get_code() ) ); + + // Test if total amount of coupons is 1. + $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) ); + } + + /** + * Test fixed cart discount method. + * + * @since 2.3 + */ + public function test_fixed_cart_discount() { + + // Create product. + $product = WC_Helper_Product::create_simple_product(); + $product->set_regular_price( 10 ); + $product->save(); + + // Create coupon. + $coupon = WC_Helper_Coupon::create_coupon(); + update_post_meta( $coupon->get_id(), 'discount_type', 'fixed_cart' ); + update_post_meta( $coupon->get_id(), 'coupon_amount', '5' ); + + // Create a flat rate method. + WC_Helper_Shipping::create_simple_flat_rate(); + + // Add product to cart. + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Add coupon. + WC()->cart->add_discount( $coupon->get_code() ); + + // Set the flat_rate shipping method. + WC()->session->set( 'chosen_shipping_methods', array( 'flat_rate' ) ); + WC()->cart->calculate_totals(); + + // Test if the cart total amount is equal 15. + $this->assertEquals( 15, WC()->cart->total ); + } + + /** + * Test fixed product discount method. + * + * @since 2.3 + */ + public function test_fixed_product_discount() { + + // Create product. + $product = WC_Helper_Product::create_simple_product(); + $product->set_regular_price( 10 ); + $product->save(); + + // Create coupon. + $coupon = WC_Helper_Coupon::create_coupon(); + update_post_meta( $coupon->get_id(), 'discount_type', 'fixed_product' ); + update_post_meta( $coupon->get_id(), 'coupon_amount', '5' ); + + // Create a flat rate method - $10. + WC_Helper_Shipping::create_simple_flat_rate(); + + // Add fee - $10. + WC_Helper_Fee::add_cart_fee(); + + // Add product to cart. + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Add coupon. + WC()->cart->add_discount( $coupon->get_code() ); + + // Set the flat_rate shipping method. + WC()->session->set( 'chosen_shipping_methods', array( 'flat_rate' ) ); + WC()->cart->calculate_totals(); + + // Test if the cart total amount is equal 25. + $this->assertEquals( 25, WC()->cart->total ); + } + + /** + * Test percent product discount method. + * + * @since 2.3 + */ + public function test_percent_discount() { + + // Create product. + $product = WC_Helper_Product::create_simple_product(); + $product->set_regular_price( 10 ); + $product->save(); + + // Create coupon. + $coupon = WC_Helper_Coupon::create_coupon(); + update_post_meta( $coupon->get_id(), 'discount_type', 'percent' ); + update_post_meta( $coupon->get_id(), 'coupon_amount', '5' ); + + // Create a flat rate method. + WC_Helper_Shipping::create_simple_flat_rate(); + + // Add fee. + WC_Helper_Fee::add_cart_fee(); + + // Add product to cart. + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Add coupon. + WC()->cart->add_discount( $coupon->get_code() ); + + // Set the flat_rate shipping method. + WC()->session->set( 'chosen_shipping_methods', array( 'flat_rate' ) ); + WC()->cart->calculate_totals(); + + // Test if the cart total amount is equal 29.5. + $this->assertEquals( 29.5, WC()->cart->total ); + } + + /** + * Tests that the remainder is handled correctly and does not turn the total negative. + * + * @since 4.1 + */ + public function test_percent_discount_handles_remainder_correctly() { + + // Create product. + $product = WC_Helper_Product::create_simple_product(); + $product->set_regular_price( 18 ); + $product->save(); + + // Create coupon. + $coupon = WC_Helper_Coupon::create_coupon( '20off' ); + update_post_meta( $coupon->get_id(), 'discount_type', 'percent' ); + update_post_meta( $coupon->get_id(), 'coupon_amount', '20' ); + $coupon_2 = WC_Helper_Coupon::create_coupon( '100off' ); + update_post_meta( $coupon_2->get_id(), 'discount_type', 'percent' ); + update_post_meta( $coupon_2->get_id(), 'coupon_amount', '100' ); + + // Create a flat rate method. + WC_Helper_Shipping::create_simple_flat_rate(); + + // Add product to cart. + WC()->cart->add_to_cart( $product->get_id(), 4 ); + + // Add coupon. + WC()->cart->add_discount( $coupon->get_code() ); + WC()->cart->add_discount( $coupon_2->get_code() ); + + // Set the flat_rate shipping method. + WC()->session->set( 'chosen_shipping_methods', array( 'flat_rate' ) ); + WC()->cart->calculate_totals(); + + // Test if the cart total amount is equal 29.5. + $this->assertEquals( 10, WC()->cart->total ); + } + + /** + * Test date setters/getters. + * + * @since 3.0.0 + */ + public function test_dates() { + $valid_coupon = WC_Helper_Coupon::create_coupon(); + $valid_coupon->set_date_expires( time() + 1000 ); + $valid_coupon->set_date_created( time() ); + $valid_coupon->set_date_modified( time() ); + + $expired_coupon = WC_Helper_Coupon::create_coupon(); + $expired_coupon->set_date_expires( time() - 10 ); + $expired_coupon->set_date_created( time() - 20 ); + $expired_coupon->set_date_modified( time() - 20 ); + + $this->assertInstanceOf( 'WC_DateTime', $valid_coupon->get_date_created() ); + $this->assertTrue( $valid_coupon->is_valid() ); + $this->assertFalse( $expired_coupon->is_valid() ); + $this->assertEquals( $expired_coupon->get_error_message(), $expired_coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_EXPIRED ) ); + } + + /** + * Test an item limit for percent discounts. + */ + public function test_percent_discount_item_limit() { + // Create product. + $product = WC_Helper_Product::create_simple_product(); + update_post_meta( $product->get_id(), '_price', '10' ); + update_post_meta( $product->get_id(), '_regular_price', '10' ); + + // Create coupon. + $coupon = WC_Helper_Coupon::create_coupon( + 'dummycoupon', + array( + 'discount_type' => 'percent', + 'coupon_amount' => '5', + 'limit_usage_to_x_items' => 1, + ) + ); + + // We need this to have the calculate_totals() method calculate totals. + if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) { + define( 'WOOCOMMERCE_CHECKOUT', true ); + } + + // Add 2 products and coupon to cart. + WC()->cart->add_to_cart( $product->get_id(), 2 ); + WC()->cart->add_discount( $coupon->get_code() ); + WC()->cart->calculate_totals(); + + // Test if the cart total amount is equal 19.5 (coupon only applying to one item). + $this->assertEquals( 19.5, WC()->cart->total ); + } + + /** + * Test the coupon's item limit. + */ + public function test_custom_discount_item_limit() { + // Register custom discount type. + WC_Helper_Coupon::register_custom_type( __FUNCTION__ ); + + // Create product. + $product = WC_Helper_Product::create_simple_product(); + update_post_meta( $product->get_id(), '_price', '10' ); + update_post_meta( $product->get_id(), '_regular_price', '10' ); + + // Create coupon. + $coupon = WC_Helper_Coupon::create_coupon( + 'dummycoupon', + array( + 'discount_type' => __FUNCTION__, + 'coupon_amount' => '5', + 'limit_usage_to_x_items' => 1, + ) + ); + + // Add 4 products and coupon to cart. + WC()->cart->add_to_cart( $product->get_id(), 4 ); + WC()->cart->add_discount( $coupon->get_code() ); + WC()->cart->calculate_totals(); + + // Test if the cart total amount is equal 39.5 (coupon only applying to one item). + $this->assertEquals( 39.5, WC()->cart->total ); + } +} diff --git a/plugins/woocommerce/tests/legacy/unit-tests/coupon/data-store.php b/plugins/woocommerce/tests/legacy/unit-tests/coupon/data-store.php new file mode 100644 index 00000000000..fb94ee0d3d0 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/coupon/data-store.php @@ -0,0 +1,169 @@ +assertTrue( is_callable( array( $store, 'read' ) ) ); + $this->assertEquals( 'WC_Coupon_Data_Store_CPT', $store->get_current_class_name() ); + } + + /** + * Test coupon create. + * @since 3.0.0 + */ + public function test_coupon_create() { + $code = 'coupon-' . time(); + $coupon = new WC_Coupon(); + $coupon->set_code( $code ); + $coupon->set_description( 'This is a test comment.' ); + $coupon->save(); + + $this->assertEquals( $code, $coupon->get_code() ); + $this->assertNotEquals( 0, $coupon->get_id() ); + $this->assertNotEquals( 'publish', $coupon->get_status() ); + } + + /** + * Test coupon deletion. + * @since 3.0.0 + */ + public function test_coupon_delete() { + $coupon = WC_Helper_Coupon::create_coupon(); + $coupon_id = $coupon->get_id(); + $this->assertNotEquals( 0, $coupon_id ); + $coupon->delete( true ); + $this->assertEquals( 0, $coupon->get_id() ); + // Test loading a deleted coupon exception. + try { + $coupon = new WC_Coupon( $coupon_id ); + } catch ( Exception $e ) { + $this->assertEquals( 'Invalid coupon.', $e->getMessage() ); + } + + } + + /** + * Test coupon accurately cleans up object cache upon deletion. + */ + public function test_coupon_cache_deletion() { + $coupon = WC_Helper_Coupon::create_coupon( 'test' ); + $coupon->delete( true ); + + $cache_name = WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $coupon->get_code(); + $ids = wp_cache_get( $cache_name, 'coupons' ); + + $this->assertEquals( false, $ids, sprintf( 'Object cache for %s was not removed upon deletion of coupon.', $cache_name ) ); + } + + /** + * Test coupon update. + * @since 3.0.0 + */ + public function test_coupon_update() { + $coupon = WC_Helper_Coupon::create_coupon(); + $coupon_id = $coupon->get_id(); + $this->assertEquals( 'dummycoupon', $coupon->get_code() ); + $coupon->set_code( 'dummycoupon2' ); + $coupon->save(); + $coupon = new WC_Coupon( $coupon->get_id() ); + $this->assertEquals( 'dummycoupon2', $coupon->get_code() ); + } + + /** + * Test coupon reading from the DB. + * @since 3.0.0 + */ + public function test_coupon_read() { + $code = 'coupon-' . time(); + $coupon = new WC_Coupon(); + $coupon->set_code( $code ); + $coupon->set_description( 'This is a test coupon.' ); + $coupon->set_amount( '' ); + $coupon->set_usage_count( 5 ); + $coupon->save(); + $coupon_id = $coupon->get_id(); + + $coupon_read = new WC_Coupon( $coupon_id ); + + $this->assertEquals( 5, $coupon_read->get_usage_count() ); + $this->assertEquals( $code, $coupon_read->get_code() ); + $this->assertEquals( 'publish', $coupon_read->get_status() ); + $this->assertEquals( 0, $coupon->get_amount() ); + $this->assertEquals( 'This is a test coupon.', $coupon_read->get_description() ); + } + + /** + * Test coupon saving. + * @since 3.0.0 + */ + public function test_coupon_save() { + $coupon = WC_Helper_Coupon::create_coupon(); + $coupon_id = $coupon->get_id(); + $coupon->set_code( 'dummycoupon2' ); + $coupon->save(); + $coupon = new WC_Coupon( $coupon_id ); // Read from DB to retest + $this->assertEquals( 'dummycoupon2', $coupon->get_code() ); + $this->assertEquals( $coupon_id, $coupon->get_id() ); + + $new_coupon = new WC_Coupon(); + $new_coupon->set_code( 'dummycoupon3' ); + $new_coupon->save(); + $new_coupon_id = $new_coupon->get_id(); + $this->assertEquals( 'dummycoupon3', $new_coupon->get_code() ); + $this->assertNotEquals( 0, $new_coupon_id ); + } + + /** + * Test coupon date saving/loading. + * @since 3.0.0 + */ + public function test_coupon_date_saving() { + $expiry_date = time() - 10; + + $coupon = WC_Helper_Coupon::create_coupon( 'coupon-' . time() ); + $coupon->set_date_expires( $expiry_date ); + $coupon->save(); + + $coupon_read = new WC_Coupon( $coupon->get_id() ); + + $this->assertEquals( date( 'Y-m-d', $expiry_date ), date( 'Y-m-d', $coupon_read->get_date_expires()->getTimestamp() ) ); + } + + /** + * Test coupon increase, decrease, user usage count methods. + * @since 3.0.0 + */ + public function test_coupon_usage_magic_methods() { + $coupon = WC_Helper_Coupon::create_coupon(); + $user_id = 1; + + $this->assertEquals( 0, $coupon->get_usage_count() ); + $this->assertEmpty( $coupon->get_used_by() ); + + $coupon->increase_usage_count( 'woo@woo.local' ); + + $this->assertEquals( 1, $coupon->get_usage_count() ); + $this->assertEquals( array( 'woo@woo.local' ), $coupon->get_used_by() ); + + $coupon->increase_usage_count( $user_id ); + $coupon->increase_usage_count( $user_id ); + + $data_store = WC_Data_Store::load( 'coupon' ); + $this->assertEquals( 2, $data_store->get_usage_by_user_id( $coupon, $user_id ) ); + + $coupon->decrease_usage_count( 'woo@woo.local' ); + $coupon->decrease_usage_count( $user_id ); + $this->assertEquals( 1, $coupon->get_usage_count() ); + $this->assertEquals( array( 1 ), $coupon->get_used_by() ); + } + +} diff --git a/plugins/woocommerce/tests/legacy/unit-tests/coupon/data.php b/plugins/woocommerce/tests/legacy/unit-tests/coupon/data.php new file mode 100644 index 00000000000..13f229b97cd --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/coupon/data.php @@ -0,0 +1,233 @@ +expected_doing_it_wrong = array_merge( $this->expected_doing_it_wrong, $legacy_keys ); + + $coupon = WC_Helper_Coupon::create_coupon(); + $coupon->add_meta_data( 'test_coupon_field', 'testing', true ); + $coupon->save_meta_data(); + $coupon = new WC_Coupon( $coupon->get_id() ); + + $this->assertEquals( $coupon->get_id(), $coupon->id ); + $this->assertEquals( ( ( $coupon->get_id() > 0 ) ? true : false ), $coupon->exists ); + $coupon_cf = $coupon->get_meta( 'test_coupon_field' ); + $this->assertEquals( $coupon_cf, $coupon->coupon_custom_fields['test_coupon_field'][0] ); + $this->assertEquals( $coupon->get_discount_type(), $coupon->type ); + $this->assertEquals( $coupon->get_discount_type(), $coupon->discount_type ); + $this->assertEquals( $coupon->get_amount(), $coupon->amount ); + $this->assertEquals( $coupon->get_amount(), $coupon->coupon_amount ); + $this->assertEquals( $coupon->get_code(), $coupon->code ); + $this->assertEquals( $coupon->get_individual_use(), ( 'yes' === $coupon->individual_use ? true : false ) ); + $this->assertEquals( $coupon->get_product_ids(), $coupon->product_ids ); + $this->assertEquals( $coupon->get_excluded_product_ids(), $coupon->exclude_product_ids ); + $this->assertEquals( $coupon->get_usage_limit(), $coupon->usage_limit ); + $this->assertEquals( $coupon->get_usage_limit_per_user(), $coupon->usage_limit_per_user ); + $this->assertEquals( $coupon->get_limit_usage_to_x_items(), $coupon->limit_usage_to_x_items ); + $this->assertEquals( $coupon->get_usage_count(), $coupon->usage_count ); + $this->assertEquals( $coupon->get_date_expires(), $coupon->expiry_date ); + $this->assertEquals( $coupon->get_product_categories(), $coupon->product_categories ); + $this->assertEquals( $coupon->get_excluded_product_categories(), $coupon->exclude_product_categories ); + $this->assertEquals( $coupon->get_minimum_amount(), $coupon->minimum_amount ); + $this->assertEquals( $coupon->get_maximum_amount(), $coupon->maximum_amount ); + $this->assertEquals( $coupon->get_email_restrictions(), $coupon->customer_email ); + } + + /** + * Developers can create manual coupons (code only). This test will make sure this works correctly + * and some of our backwards compat handling works correctly as well. + * @since 3.0.0 + */ + public function test_read_manual_coupon() { + $code = 'manual_coupon_' . time(); + $coupon = new WC_Coupon( $code ); + $coupon->read_manual_coupon( + $code, + array( + 'id' => true, + 'type' => 'fixed_cart', + 'amount' => 0, + 'individual_use' => true, + 'product_ids' => array(), + 'exclude_product_ids' => array(), + 'usage_limit' => '', + 'usage_count' => '', + 'expiry_date' => '', + 'free_shipping' => false, + 'product_categories' => array(), + 'exclude_product_categories' => array(), + 'exclude_sale_items' => false, + 'minimum_amount' => '', + 'maximum_amount' => 100, + 'customer_email' => '', + ) + ); + $this->assertEquals( $code, $coupon->get_code() ); + $this->assertTrue( $coupon->get_individual_use() ); + $this->assertEquals( 100, $coupon->get_maximum_amount() ); + + /** + * test our back compat logic: passing in product_ids/exclude_product_ids in as strings + * and passing free_shipping, exclude_sale_items, and individual_use in as yes|no strings. + * setting these values this way will also throw a deprecated notice so we will let + * PHPUnit know that its okay to continue. + */ + $legacy_keys = array( + 'product_ids', + 'exclude_product_ids', + 'individual_use', + 'free_shipping', + 'exclude_sale_items', + ); + $this->expected_doing_it_wrong = array_merge( $this->expected_doing_it_wrong, $legacy_keys ); + $code = 'bc_manual_coupon_' . time(); + $coupon = new WC_Coupon( $code ); + $coupon->read_manual_coupon( + $code, + array( + 'id' => true, + 'type' => 'fixed_cart', + 'amount' => 0, + 'individual_use' => 'yes', + 'product_ids' => '', + 'exclude_product_ids' => '5,6', + 'usage_limit' => '', + 'usage_count' => '', + 'expiry_date' => '', + 'free_shipping' => 'no', + 'product_categories' => array(), + 'exclude_product_categories' => array(), + 'exclude_sale_items' => 'no', + 'minimum_amount' => '', + 'maximum_amount' => 100, + 'customer_email' => '', + ) + ); + $this->assertEquals( $code, $coupon->get_code() ); + $this->assertTrue( $coupon->get_individual_use() ); + $this->assertFalse( $coupon->get_free_shipping() ); + $this->assertFalse( $coupon->get_exclude_sale_items() ); + $this->assertEquals( array( 5, 6 ), $coupon->get_excluded_product_ids() ); + $this->assertEquals( array(), $coupon->get_product_ids() ); + } + + /** + * Test standard coupon getters & setters. + * @since 3.0.0 + */ + public function test_coupon_getters_and_setters() { + $standard_getters_and_setters = array( + 'code' => 'test', + 'description' => 'hello world', + 'discount_type' => 'percent', + 'amount' => 10.50, + 'status' => 'publish', + 'usage_count' => 5, + 'individual_use' => true, + 'product_ids' => array( 5, 10 ), + 'exclude_product_ids' => array( 2, 1 ), + 'usage_limit' => 2, + 'usage_limit_per_user' => 10, + 'limit_usage_to_x_items' => 2, + 'free_shipping' => true, + 'product_categories' => array( 6 ), + 'exclude_product_categories' => array( 8 ), + 'exclude_sale_items' => true, + 'minimum_amount' => 2, + 'maximum_amount' => 1000, + 'customer_email' => array( 'test@woo.local' ), + 'used_by' => array( 1 ), + ); + + $coupon = new WC_Coupon(); + foreach ( $standard_getters_and_setters as $function => $value ) { + $function = $this->get_function_name( $function ); + $coupon->{"set_{$function}"}( $value ); + $this->assertEquals( $value, $coupon->{"get_{$function}"}(), $function ); + } + } + + /** + * Test getting custom fields. + * @since 3.0.0 + */ + public function test_get_custom_fields() { + $coupon = WC_Helper_Coupon::create_coupon(); + $coupon_id = $coupon->get_id(); + $meta_value = time() . '-custom-value'; + $coupon->add_meta_data( 'test_coupon_field', $meta_value, true ); + $coupon->save_meta_data(); + + $coupon = new WC_Coupon( $coupon_id ); + $custom_fields = $coupon->get_meta_data(); + $this->assertCount( 1, $custom_fields ); + $this->assertEquals( $meta_value, $coupon->get_meta( 'test_coupon_field' ) ); + } + + /** + * Test setting custom fields. + * @since 3.0.0 + */ + public function test_set_custom_fields() { + $coupon = WC_Helper_Coupon::create_coupon(); + $coupon_id = $coupon->get_id(); + $meta_value = time() . '-custom-value'; + $coupon->add_meta_data( 'my-custom-field', $meta_value, true ); + $coupon->save(); + $coupon = new WC_Coupon( $coupon_id ); + $this->assertEquals( $meta_value, $coupon->get_meta( 'my-custom-field' ) ); + } +} diff --git a/tests/legacy/unit-tests/coupon/functions.php b/plugins/woocommerce/tests/legacy/unit-tests/coupon/functions.php similarity index 100% rename from tests/legacy/unit-tests/coupon/functions.php rename to plugins/woocommerce/tests/legacy/unit-tests/coupon/functions.php diff --git a/tests/legacy/unit-tests/crud/data-store.php b/plugins/woocommerce/tests/legacy/unit-tests/crud/data-store.php similarity index 100% rename from tests/legacy/unit-tests/crud/data-store.php rename to plugins/woocommerce/tests/legacy/unit-tests/crud/data-store.php diff --git a/plugins/woocommerce/tests/legacy/unit-tests/crud/data.php b/plugins/woocommerce/tests/legacy/unit-tests/crud/data.php new file mode 100644 index 00000000000..a9d56645f95 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/crud/data.php @@ -0,0 +1,569 @@ +data_store->set_meta_type( 'post' ); + $object->data_store->set_object_id_field( '' ); + $object->set_content( 'testing' ); + $object->save(); + return $object; + } + + /** + * Create a test user we can add/test meta against. + */ + public function create_test_user() { + $object = new WC_Mock_WC_Data(); + $object->data_store->set_meta_type( 'user' ); + $object->data_store->set_object_id_field( 'user_id' ); + $object->set_content( 'testing@woo.dev' ); + $object->save(); + return $object; + } + + /** + * Test: get_data. + */ + public function test_get_data() { + $object = new WC_Mock_WC_Data(); + $this->assertIsArray( $object->get_data() ); + } + + /** + * Test: delete_meta_data_by_mid. + */ + public function test_delete_meta_data_by_mid() { + $object = $this->create_test_post(); + $object_id = $object->get_id(); + $meta_id = add_metadata( 'post', $object_id, 'test_meta_key', 'val1', true ); + $object->delete_meta_data_by_mid( $meta_id ); + $this->assertEmpty( $object->get_meta( 'test_meta_key' ) ); + } + + /** + * Test: set_props. + */ + public function test_set_props() { + $object = new WC_Mock_WC_Data(); + $data_to_set = array( + 'content' => 'I am a fish', + 'bool_value' => true, + ); + $result = $object->set_props( $data_to_set ); + $this->assertFalse( is_wp_error( $result ) ); + $this->assertEquals( 'I am a fish', $object->get_content() ); + $this->assertTrue( $object->get_bool_value() ); + + $data_to_set = array( + 'content' => 'I am also a fish', + 'bool_value' => 'thisisinvalid', + ); + $result = $object->set_props( $data_to_set ); + $this->assertTrue( is_wp_error( $result ) ); + $this->assertEquals( 'I am also a fish', $object->get_content() ); + $this->assertNotEquals( 'thisisinvalid', $object->get_bool_value() ); + } + + /** + * Tests reading and getting set metadata. + */ + public function test_get_meta_data() { + $object = $this->create_test_post(); + $object_id = $object->get_id(); + $object->add_meta_data( 'test_meta_key', 'val1', true ); + $object->add_meta_data( 'test_multi_meta_key', 'val2' ); + $object->add_meta_data( 'test_multi_meta_key', 'val3' ); + $object->save_meta_data(); + + $meta_data = $object->get_meta_data(); + $i = 1; + + $this->assertNotEmpty( $meta_data ); + foreach ( $meta_data as $mid => $data ) { + $this->assertEquals( "val{$i}", $data->value ); + $i++; + } + } + + /** + * Tests that the meta data cache is not shared among instances. + */ + public function test_get_meta_data_shared_bug() { + $object = new WC_Order(); + $object->add_meta_data( 'test_meta_key', 'val1', true ); + $object->add_meta_data( 'test_multi_meta_key', 'val2' ); + $object->add_meta_data( 'test_multi_meta_key', 'val3' ); + $object->save(); + + $order = new WC_Order( $object->get_id() ); + $metas = $order->get_meta_data(); + $metas[0]->value = 'wrong value'; + + $order = new WC_Order( $object->get_id() ); + $metas = $order->get_meta_data(); + $this->assertNotEquals( 'wrong value', $metas[0]->value ); + } + + /** + * Tests the cache invalidation after an order is saved. + */ + public function test_get_meta_data_cache_invalidation() { + $object = new WC_Order(); + $object->add_meta_data( 'test_meta_key', 'val1', true ); + $object->add_meta_data( 'test_multi_meta_key', 'val2' ); + $object->add_meta_data( 'test_multi_meta_key', 'val3' ); + $object->save(); + + $order = new WC_Order( $object->get_id() ); + $metas = $order->get_meta_data(); + $metas[0]->value = 'updated value'; + $order->save(); + + $order = new WC_Order( $object->get_id() ); + $metas = $order->get_meta_data(); + $this->assertEquals( 'updated value', $metas[0]->value ); + } + + /** + * Ensure get_meta_data() can overwrite array meta values with scalar values. + */ + public function test_get_meta_data_cache_invalidation_array_to_scalar() { + $object = new WC_Order(); + $object->add_meta_data( 'test_meta_key', array( 'val1' ), true ); + $object->add_meta_data( 'test_multi_meta_key', 'val2' ); + $object->add_meta_data( 'test_multi_meta_key', 'val3' ); + $object->save(); + + $order = new WC_Order( $object->get_id() ); + $metas = $order->get_meta_data(); + $metas[0]->value = 'updated value'; + $order->save(); + + $order = new WC_Order( $object->get_id() ); + $metas = $order->get_meta_data(); + $this->assertEquals( 'updated value', $metas[0]->value ); + } + + /** + * Test getting meta by ID. + */ + public function test_get_meta() { + $object = $this->create_test_post(); + $object_id = $object->get_id(); + $object->add_meta_data( 'test_meta_key', 'val1', true ); + $object->add_meta_data( 'test_multi_meta_key', 'val2' ); + $object->add_meta_data( 'test_multi_meta_key', 'val3' ); + $object->save_meta_data(); + $object = new WC_Mock_WC_Data( $object_id ); + + // test single meta key. + $single_meta = $object->get_meta( 'test_meta_key' ); + $this->assertEquals( 'val1', $single_meta ); + + // test getting multiple. + $meta = $object->get_meta( 'test_multi_meta_key', false ); + $i = 2; + foreach ( $meta as $data ) { + $this->assertEquals( 'test_multi_meta_key', $data->key ); + $this->assertEquals( "val{$i}", $data->value ); + $i++; + } + } + + /** + * Test seeing if meta exists. + */ + public function test_has_meta() { + $object = $this->create_test_post(); + $object_id = $object->get_id(); + $object->add_meta_data( 'test_meta_key', 'val1', true ); + $object->add_meta_data( 'test_multi_meta_key', 'val2' ); + $object->add_meta_data( 'test_multi_meta_key', 'val3' ); + $object->save_meta_data(); + $object = new WC_Mock_WC_Data( $object_id ); + + $this->assertTrue( $object->meta_exists( 'test_meta_key' ) ); + $this->assertTrue( $object->meta_exists( 'test_multi_meta_key' ) ); + $this->assertFalse( $object->meta_exists( 'thiskeyisnothere' ) ); + } + + /** + * Test getting meta that hasn't been set. + */ + public function test_get_meta_no_meta() { + $object = $this->create_test_post(); + $object_id = $object->get_id(); + $object = new WC_Mock_WC_Data( $object_id ); + + $single_on = $object->get_meta( 'doesnt-exist', true ); + $single_off = $object->get_meta( 'also-doesnt-exist', false ); + + $this->assertEquals( '', $single_on ); + $this->assertEquals( array(), $single_off ); + } + + /** + * Test setting meta. + */ + public function test_set_meta_data() { + global $wpdb; + $object = $this->create_test_post(); + $object_id = $object->get_id(); + add_metadata( 'post', $object_id, 'test_meta_key', 'val1', true ); + add_metadata( 'post', $object_id, 'test_meta_key_2', 'val2', true ); + $object = new WC_Mock_WC_Data( $object_id ); + + $metadata = array(); + $raw_metadata = $wpdb->get_results( + $wpdb->prepare( + " + SELECT meta_id, meta_key, meta_value + FROM {$wpdb->prefix}postmeta + WHERE post_id = %d ORDER BY meta_id + ", + $object_id + ) + ); + + foreach ( $raw_metadata as $meta ) { + $metadata[] = (object) array( + 'id' => $meta->meta_id, + 'key' => $meta->meta_key, + 'value' => $meta->meta_value, + ); + } + + $object = new WC_Mock_WC_Data(); + $object->set_meta_data( $metadata ); + + foreach ( $object->get_meta_data() as $id => $meta ) { + $this->assertEquals( $metadata[ $id ]->id, $meta->id ); + $this->assertEquals( $metadata[ $id ]->key, $meta->key ); + $this->assertEquals( $metadata[ $id ]->value, $meta->value ); + } + + } + + /** + * Test adding meta data. + */ + public function test_add_meta_data() { + $object = $this->create_test_post(); + $object_id = $object->get_id(); + $data = 'add_meta_data_' . time(); + $object->add_meta_data( 'test_new_field', $data ); + $meta = $object->get_meta( 'test_new_field' ); + $this->assertEquals( $data, $meta ); + } + + /** + * Test updating meta data. + */ + public function test_update_meta_data() { + global $wpdb; + $object = $this->create_test_post(); + $object_id = $object->get_id(); + add_metadata( 'post', $object_id, 'test_meta_key', 'val1', true ); + $object = new WC_Mock_WC_Data( $object_id ); + + $this->assertEquals( 'val1', $object->get_meta( 'test_meta_key' ) ); + + $metadata = array(); + $meta_id = $wpdb->get_var( + $wpdb->prepare( + " + SELECT meta_id + FROM {$wpdb->prefix}postmeta + WHERE post_id = %d LIMIT 1 + ", + $object_id + ) + ); + + $object->update_meta_data( 'test_meta_key', 'updated_value', $meta_id ); + $this->assertEquals( 'updated_value', $object->get_meta( 'test_meta_key' ) ); + } + + /** + * Test deleting meta. + */ + public function test_delete_meta_data() { + $object = $this->create_test_post(); + $object_id = $object->get_id(); + add_metadata( 'post', $object_id, 'test_meta_key', 'val1', true ); + $object = new WC_Mock_WC_Data( $object_id ); + + $this->assertEquals( 'val1', $object->get_meta( 'test_meta_key' ) ); + + $object->delete_meta_data( 'test_meta_key' ); + + $this->assertEmpty( $object->get_meta( 'test_meta_key' ) ); + } + + + /** + * Test saving metadata (Actually making sure changes are written to DB). + */ + public function test_save_meta_data() { + global $wpdb; + $object = $this->create_test_post(); + $object_id = $object->get_id(); + $object->add_meta_data( 'test_meta_key', 'val1', true ); + $object->add_meta_data( 'test_meta_key_2', 'val2', true ); + $object->save_meta_data(); + $object = new WC_Mock_WC_Data( $object_id ); + + $raw_metadata = $wpdb->get_results( + $wpdb->prepare( + " + SELECT meta_id, meta_key, meta_value + FROM {$wpdb->prefix}postmeta + WHERE post_id = %d ORDER BY meta_id + ", + $object_id + ) + ); + + $object->delete_meta_data( 'test_meta_key' ); + $object->update_meta_data( 'test_meta_key_2', 'updated_value', $raw_metadata[1]->meta_id ); + + $object->save(); + $object = new WC_Mock_WC_Data( $object_id ); // rereads from the DB. + + $this->assertEmpty( $object->get_meta( 'test_meta_key' ) ); + $this->assertEquals( 'updated_value', $object->get_meta( 'test_meta_key_2' ) ); + } + + /** + * Test reading/getting user meta data too. + */ + public function test_usermeta() { + $object = $this->create_test_user(); + $object_id = $object->get_id(); + $object->add_meta_data( 'test_meta_key', 'val1', true ); + $object->add_meta_data( 'test_meta_key_2', 'val2', true ); + $object->save_meta_data(); + + $this->assertEquals( 'val1', $object->get_meta( 'test_meta_key' ) ); + $this->assertEquals( 'val2', $object->get_meta( 'test_meta_key_2' ) ); + } + + /** + * Test adding meta data/updating meta data just added without keys colliding when changing + * data before a save. + */ + public function test_add_meta_data_overwrite_before_save() { + $object = new WC_Mock_WC_Data(); + $object->add_meta_data( 'test_field_0', 'another field', true ); + $object->add_meta_data( 'test_field_1', 'another field', true ); + $object->add_meta_data( 'test_field_2', 'val1', true ); + $object->update_meta_data( 'test_field_0', 'another field 2' ); + $this->assertEquals( 'val1', $object->get_meta( 'test_field_2' ) ); + } + + /** + * Test protected method set_date_prop by testing a order date setter. + * + * @param string $timezone The default timezone to operate under. + */ + public function set_date_prop_gmt_offset( $timezone = 'UTC' ) { + // @codingStandardsIgnoreStart + date_default_timezone_set( $timezone ); + + $object = new WC_Order(); + + // Change timezone in WP. + update_option( 'gmt_offset', -4 ); + + // Set date to a UTC timestamp and expect a valid UTC timestamp back. + $object->set_date_created( 1488979186 ); + $this->assertEquals( 1488979186, $object->get_date_created()->getTimestamp() ); + + // Set date to a string without timezone info. This will be assumed in local timezone and thus should match the offset timestamp. + $object->set_date_created( '2017-01-02' ); + $this->assertEquals( -14400, $object->get_date_created()->getOffset() ); + $this->assertEquals( '2017-01-02 00:00:00', $object->get_date_created()->date( 'Y-m-d H:i:s' ) ); + $this->assertEquals( 1483315200 - $object->get_date_created()->getOffset(), $object->get_date_created()->getTimestamp() ); + $this->assertEquals( 1483315200, $object->get_date_created()->getOffsetTimestamp() ); + + // Date time with no timezone. + $object->set_date_created( '2017-01-02T00:00' ); + $this->assertEquals( 1483315200 - $object->get_date_created()->getOffset(), $object->get_date_created()->getTimestamp() ); + $this->assertEquals( 1483315200, $object->get_date_created()->getOffsetTimestamp() ); + $this->assertEquals( '2017-01-02 00:00:00', $object->get_date_created()->date( 'Y-m-d H:i:s' ) ); + + // ISO 8601 date time with offset. + $object->set_date_created( '2017-01-01T20:00:00-04:00' ); + $this->assertEquals( 1483315200, $object->get_date_created()->getTimestamp() ); + $this->assertEquals( '2017-01-01 20:00:00', $object->get_date_created()->date( 'Y-m-d H:i:s' ) ); + + // ISO 8601 date time different offset to site timezone. + $object->set_date_created( '2017-01-01T16:00:00-08:00' ); + $this->assertEquals( 1483315200, $object->get_date_created()->getTimestamp() ); + $this->assertEquals( '2017-01-01 20:00:00', $object->get_date_created()->date( 'Y-m-d H:i:s' ) ); + + // ISO 8601 date time in UTC. + $object->set_date_created( '2017-01-02T00:00:00+00:00' ); + $this->assertEquals( 1483315200, $object->get_date_created()->getTimestamp() ); + $this->assertEquals( '2017-01-01 20:00:00', $object->get_date_created()->date( 'Y-m-d H:i:s' ) ); + + // Restore default. + update_option( 'gmt_offset', 0 ); + + date_default_timezone_set( 'UTC' ); + // @codingStandardsIgnoreEnd + } + + /** + * Test protected method set_date_prop by testing a order date setter. + * + * @param string $timezone The default timezone to operate under. + */ + public function set_date_prop_timezone_string( $timezone = 'UTC' ) { + // @codingStandardsIgnoreStart + date_default_timezone_set( $timezone ); + + $object = new WC_Order(); + + // Repeat tests with timezone_string. America/New_York is -5 in the winter and -4 in summer. + update_option( 'timezone_string', 'America/New_York' ); + + // Set date to a UTC timestamp and expect a valid UTC timestamp back. + $object->set_date_created( 1488979186 ); + $this->assertEquals( 1488979186, $object->get_date_created()->getTimestamp() ); + + // Set date to a string without timezone info. This will be assumed in local timezone and thus should match the offset timestamp. + $object->set_date_created( '2017-01-02' ); + $this->assertEquals( 1483315200 - $object->get_date_created()->getOffset(), $object->get_date_created()->getTimestamp() ); + $this->assertEquals( 1483315200, $object->get_date_created()->getOffsetTimestamp() ); + $this->assertEquals( '2017-01-02 00:00:00', $object->get_date_created()->date( 'Y-m-d H:i:s' ) ); + + // Date time with no timezone. + $object->set_date_created( '2017-01-02T00:00' ); + $this->assertEquals( 1483315200 - $object->get_date_created()->getOffset(), $object->get_date_created()->getTimestamp() ); + $this->assertEquals( 1483315200, $object->get_date_created()->getOffsetTimestamp() ); + $this->assertEquals( '2017-01-02 00:00:00', $object->get_date_created()->date( 'Y-m-d H:i:s' ) ); + + // ISO 8601 date time with offset. + $object->set_date_created( '2017-01-01T19:00:00-05:00' ); + $this->assertEquals( 1483315200, $object->get_date_created()->getTimestamp() ); + $this->assertEquals( '2017-01-01 19:00:00', $object->get_date_created()->date( 'Y-m-d H:i:s' ) ); + + // ISO 8601 date time different offset to site timezone. + $object->set_date_created( '2017-01-01T16:00:00-08:00' ); + $this->assertEquals( 1483315200, $object->get_date_created()->getTimestamp() ); + $this->assertEquals( '2017-01-01 19:00:00', $object->get_date_created()->date( 'Y-m-d H:i:s' ) ); + + // ISO 8601 date time in UTC. + $object->set_date_created( '2017-01-02T00:00:00+00:00' ); + $this->assertEquals( 1483315200, $object->get_date_created()->getTimestamp() ); + $this->assertEquals( '2017-01-01 19:00:00', $object->get_date_created()->date( 'Y-m-d H:i:s' ) ); + + // Restore default. + update_option( 'timezone_string', '' ); + + date_default_timezone_set( 'UTC' ); + // @codingStandardsIgnoreEnd + } + + /** + * Test protected method set_date_prop by testing a order date setter. + */ + public function test_set_date_prop_server_timezone() { + $this->set_date_prop_gmt_offset(); + $this->set_date_prop_timezone_string(); + + // Repeat all tests with different server timezone. + $this->set_date_prop_gmt_offset( 'Pacific/Fiji' ); + $this->set_date_prop_timezone_string( 'Pacific/Fiji' ); + + // Repeat all tests with different server timezone. + $this->set_date_prop_gmt_offset( 'Pacific/Tahiti' ); + $this->set_date_prop_timezone_string( 'Pacific/Tahiti' ); + } + + /** + * Test applying changes. + */ + public function test_apply_changes() { + $data = array( + 'prop1' => 'value1', + 'prop2' => 'value2', + ); + + $changes = array( + 'prop1' => 'new_value1', + 'prop3' => 'value3', + ); + + $object = new WC_Mock_WC_Data(); + $object->set_data( $data ); + $object->set_changes( $changes ); + $object->apply_changes(); + + $new_data = $object->get_data(); + $new_changes = $object->get_changes(); + + $this->assertEquals( 'new_value1', $new_data['prop1'] ); + $this->assertEquals( 'value2', $new_data['prop2'] ); + $this->assertEquals( 'value3', $new_data['prop3'] ); + $this->assertEmpty( $new_changes ); + } + + /** + * Test applying changes with a nested array. + */ + public function test_apply_changes_nested() { + $data = array( + 'prop1' => 'value1', + 'prop2' => array( + 'subprop1' => 1, + 'subprop2' => 2, + ), + ); + + $changes = array( + 'prop2' => array( + 'subprop1' => 1000, + 'subprop3' => 3, + ), + ); + + $object = new WC_Mock_WC_Data(); + $object->set_data( $data ); + $object->set_changes( $changes ); + $object->apply_changes(); + + $new_data = $object->get_data(); + + $this->assertEquals( 'value1', $new_data['prop1'] ); + $this->assertEquals( 1000, $new_data['prop2']['subprop1'] ); + $this->assertEquals( 2, $new_data['prop2']['subprop2'] ); + $this->assertEquals( 3, $new_data['prop2']['subprop3'] ); + } +} diff --git a/tests/legacy/unit-tests/crud/meta.php b/plugins/woocommerce/tests/legacy/unit-tests/crud/meta.php similarity index 100% rename from tests/legacy/unit-tests/crud/meta.php rename to plugins/woocommerce/tests/legacy/unit-tests/crud/meta.php diff --git a/tests/legacy/unit-tests/crud/query.php b/plugins/woocommerce/tests/legacy/unit-tests/crud/query.php similarity index 100% rename from tests/legacy/unit-tests/crud/query.php rename to plugins/woocommerce/tests/legacy/unit-tests/crud/query.php diff --git a/tests/legacy/unit-tests/crud/refunds.php b/plugins/woocommerce/tests/legacy/unit-tests/crud/refunds.php similarity index 100% rename from tests/legacy/unit-tests/crud/refunds.php rename to plugins/woocommerce/tests/legacy/unit-tests/crud/refunds.php diff --git a/tests/legacy/unit-tests/customer/class-wc-customer-download-log-data-store.php b/plugins/woocommerce/tests/legacy/unit-tests/customer/class-wc-customer-download-log-data-store.php similarity index 100% rename from tests/legacy/unit-tests/customer/class-wc-customer-download-log-data-store.php rename to plugins/woocommerce/tests/legacy/unit-tests/customer/class-wc-customer-download-log-data-store.php diff --git a/plugins/woocommerce/tests/legacy/unit-tests/customer/class-wc-tests-customer-download.php b/plugins/woocommerce/tests/legacy/unit-tests/customer/class-wc-tests-customer-download.php new file mode 100644 index 00000000000..b2768c516c8 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/customer/class-wc-tests-customer-download.php @@ -0,0 +1,134 @@ +customer_id = 1; + $this->customer_email = 'test@example.com'; + + $this->download = new WC_Customer_Download(); + $this->download->set_user_id( $this->customer_id ); + $this->download->set_user_email( $this->customer_email ); + $this->download->set_order_id( 1 ); + $this->download->set_access_granted( '2018-01-22 00:00:00' ); + $this->download->save(); + } + + /** + * Test WC_Customer_Download_Data_Store::delete() + */ + public function test_delete() { + $data_store = WC_Data_Store::load( 'customer-download' ); + $data_store->delete( $this->download ); + $this->assertEquals( 0, $this->download->get_id() ); + } + + /** + * Test WC_Customer_Download_Data_Store::delete_by_id() + */ + public function test_delete_by_id() { + $data_store = WC_Data_Store::load( 'customer-download' ); + $data_store->delete_by_id( $this->download->get_id() ); + $this->assertEquals( 0, $data_store->get_id() ); + } + + /** + * Test WC_Customer_Download_Data_Store::delete_by_download_id() + */ + public function test_delete_by_download_id() { + $download_id = $this->download->get_download_id(); + $data_store = WC_Data_Store::load( 'customer-download' ); + $downloads = $data_store->get_downloads_for_customer( $this->customer_id ); + $this->assertInstanceOf( 'StdClass', $downloads[0] ); + $data_store->delete_by_download_id( $download_id ); + $downloads = $data_store->get_downloads_for_customer( $this->customer_id ); + $this->assertEquals( array(), $downloads ); + } + + /** + * Test WC_Customer_Download_Data_Store::get_downloads() + */ + public function test_get_downloads() { + $download_2 = new WC_Customer_Download(); + $download_2->set_user_id( $this->customer_id ); + $download_2->set_user_email( $this->customer_email ); + $download_2->set_order_id( 2 ); + $download_2->set_access_granted( '2018-01-22 00:00:00' ); + $download_2->save(); + + $data_store = WC_Data_Store::load( 'customer-download' ); + $downloads = $data_store->get_downloads( + array( + 'user_email' => $this->customer_email, + 'orderby' => 'order_id', + 'order' => 'DESC', + ) + ); + $this->assertEquals( array( $download_2, $this->download ), $downloads ); + + $downloads = $data_store->get_downloads( array( 'user_email' => 'test2@example.com' ) ); + $this->assertEquals( array(), $downloads ); + + $expected_result = array( $this->download->get_id(), $download_2->get_id() ); + $downloads = $data_store->get_downloads( + array( + 'user_email' => $this->customer_email, + 'return' => 'ids', + ) + ); + $this->assertEquals( $expected_result, $downloads ); + + $expected_result = array( + array( + 'user_email' => $this->customer_email, + 'permission_id' => $this->download->get_id(), + ), + array( + 'user_email' => $this->customer_email, + 'permission_id' => $download_2->get_id(), + ), + ); + $downloads = $data_store->get_downloads( + array( + 'user_email' => $this->customer_email, + 'return' => 'permission_id,user_email', + ) + ); + $this->assertEquals( $expected_result, $downloads ); + } +} diff --git a/plugins/woocommerce/tests/legacy/unit-tests/customer/crud.php b/plugins/woocommerce/tests/legacy/unit-tests/customer/crud.php new file mode 100644 index 00000000000..6b09a159d7d --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/customer/crud.php @@ -0,0 +1,493 @@ +set_username( $username ); + $customer->set_password( 'test123' ); + $customer->set_email( 'test@woo.local' ); + $customer->save(); + $wp_user = new WP_User( $customer->get_id() ); + + $this->assertEquals( $username, $customer->get_username() ); + $this->assertNotEquals( 0, $customer->get_id() ); + $this->assertEquals( strtotime( $wp_user->user_registered ), $customer->get_date_created()->getOffsetTimestamp() ); + } + + /** + * Test updating a customer. + * @since 3.0.0 + */ + public function test_update_customer() { + $customer = WC_Helper_Customer::create_customer(); + $customer_id = $customer->get_id(); + $this->assertEquals( 'test@woo.local', $customer->get_email() ); + $this->assertEquals( 'Apt 1', $customer->get_billing_address_2() ); + $customer->set_email( 'test@wc.local' ); + $customer->set_first_name( 'Justin' ); + $customer->set_billing_address_2( 'Apt 5' ); + $customer->save(); + + $customer = new WC_Customer( $customer_id ); // so we can read fresh copies from the DB + $this->assertEquals( 'test@wc.local', $customer->get_email() ); + $this->assertEquals( 'Justin', $customer->get_first_name() ); + $this->assertEquals( 'Apt 5', $customer->get_billing_address_2() ); + } + + + /** + * Test saving a customer. + * @since 3.0.0 + */ + public function test_save_customer() { + // test save() -> Create + $customer = new WC_Customer(); + $customer->set_username( 'testusername-' . time() ); + $customer->set_password( 'test123' ); + $customer->set_email( 'test@woo.local' ); + $this->assertEquals( 0, $customer->get_id() ); + $customer->save(); + $customer_id = $customer->get_id(); + $wp_user = new WP_User( $customer->get_id() ); + + $this->assertNotEquals( 0, $customer->get_id() ); + + // test save() -> Update + $this->assertEquals( 'test@woo.local', $customer->get_email() ); + $customer->set_email( 'test@wc.local' ); + $customer->save(); + + $customer = new WC_Customer( $customer_id ); + $this->assertEquals( 'test@wc.local', $customer->get_email() ); + } + + /** + * Test deleting a customer. + * @since 3.0.0 + */ + public function test_delete_customer() { + $customer = WC_Helper_Customer::create_customer(); + $customer_id = $customer->get_id(); + $this->assertNotEquals( 0, $customer->get_id() ); + $customer->delete(); + $this->assertEquals( 0, $customer->get_id() ); + } + + /** + * Test reading a customer. + * @since 3.0.0 + */ + public function test_read_customer() { + $username = 'user-' . time(); + $customer = new WC_Customer(); + $customer->set_username( $username ); + $customer->set_email( 'test@woo.local' ); + $customer->set_password( 'hunter2' ); + $customer->set_first_name( 'Billy' ); + $customer->set_last_name( 'Bob' ); + $customer->set_display_name( 'Billy Bob' ); + $customer->save(); + $customer_id = $customer->get_id(); + $customer_read = new WC_Customer( $customer_id ); + + $this->assertEquals( $customer_id, $customer_read->get_id() ); + $this->assertEquals( 'test@woo.local', $customer_read->get_email() ); + $this->assertEquals( 'Billy', $customer_read->get_first_name() ); + $this->assertEquals( 'Bob', $customer_read->get_last_name() ); + $this->assertEquals( 'Billy Bob', $customer_read->get_display_name() ); + $this->assertEquals( $username, $customer_read->get_username() ); + } + + /** + * Tests backwards compat / legacy handling. + * @expectedDeprecated WC_Customer::get_default_country + * @expectedDeprecated WC_Customer::get_default_state + * @expectedDeprecated WC_Customer::is_paying_customer + * @expectedDeprecated WC_Customer::calculated_shipping + * @since 3.0.0 + */ + public function test_customer_backwards_compat() { + // Properties. + // Accessing properties directly will throw some wanted deprected notices + // So we need to let PHPUnit know we are expecting them and it's fine to continue + $legacy_keys = array( + 'id', + 'country', + 'state', + 'postcode', + 'city', + 'address', + 'address_1', + 'address_2', + 'shipping_country', + 'shipping_state', + 'shipping_postcode', + 'shipping_city', + 'shipping_address', + 'shipping_address_1', + 'shipping_address_2', + 'is_vat_exempt', + 'calculated_shipping', + ); + + $this->expected_doing_it_wrong = array_merge( $this->expected_doing_it_wrong, $legacy_keys ); + + $customer = WC_Helper_Customer::create_customer(); + + $this->assertEquals( $customer->get_id(), $customer->id ); + $this->assertEquals( $customer->get_billing_country(), $customer->country ); + $this->assertEquals( $customer->get_billing_state(), $customer->state ); + $this->assertEquals( $customer->get_billing_postcode(), $customer->postcode ); + $this->assertEquals( $customer->get_billing_city(), $customer->city ); + $this->assertEquals( $customer->get_billing_address(), $customer->address ); + $this->assertEquals( $customer->get_billing_address(), $customer->address_1 ); + $this->assertEquals( $customer->get_billing_address_2(), $customer->address_2 ); + $this->assertEquals( $customer->get_shipping_country(), $customer->shipping_country ); + $this->assertEquals( $customer->get_shipping_state(), $customer->shipping_state ); + $this->assertEquals( $customer->get_shipping_postcode(), $customer->shipping_postcode ); + $this->assertEquals( $customer->get_shipping_city(), $customer->shipping_city ); + $this->assertEquals( $customer->get_shipping_address(), $customer->shipping_address ); + $this->assertEquals( $customer->get_shipping_address(), $customer->shipping_address_1 ); + $this->assertEquals( $customer->get_shipping_address_2(), $customer->shipping_address_2 ); + $this->assertEquals( $customer->get_is_vat_exempt(), $customer->is_vat_exempt ); + $this->assertEquals( $customer->has_calculated_shipping(), $customer->calculated_shipping ); + + // Functions + $this->assertEquals( $customer->get_is_vat_exempt(), $customer->is_vat_exempt() ); + $this->assertEquals( $customer->has_calculated_shipping(), $customer->has_calculated_shipping() ); + $default = wc_get_customer_default_location(); + $this->assertEquals( $default['country'], $customer->get_default_country() ); + $this->assertEquals( $default['state'], $customer->get_default_state() ); + $this->assertFalse( $customer->has_calculated_shipping() ); + $customer->calculated_shipping( true ); + $this->assertTrue( $customer->has_calculated_shipping() ); + $this->assertEquals( $customer->get_is_paying_customer(), $customer->is_paying_customer() ); + } + + /** + * Test generic getters & setters + * @since 3.0.0 + */ + public function test_customer_setters_and_getters() { + $time = time(); + $setters = array( + 'username' => 'test', + 'email' => 'test@woo.local', + 'first_name' => 'Bob', + 'last_name' => 'tester', + 'display_name' => 'Bob Tester', + 'role' => 'customer', + 'date_created' => $time, + 'date_modified' => $time, + 'billing_postcode' => 11010, + 'billing_city' => 'New York', + 'billing_address' => '123 Main St.', + 'billing_address_1' => '123 Main St.', + 'billing_address_2' => 'Apt 2', + 'billing_state' => 'NY', + 'billing_country' => 'US', + 'shipping_state' => 'NY', + 'shipping_postcode' => 11011, + 'shipping_city' => 'New York', + 'shipping_address' => '123 Main St.', + 'shipping_address_1' => '123 Main St.', + 'shipping_address_2' => 'Apt 2', + 'is_vat_exempt' => true, + 'calculated_shipping' => true, + 'is_paying_customer' => true, + ); + + $customer = new WC_Customer(); + + foreach ( $setters as $method => $value ) { + $customer->{"set_{$method}"}( $value ); + } + + $getters = array(); + + foreach ( $setters as $method => $value ) { + $getters[ $method ] = $customer->{"get_{$method}"}(); + } + + // Get timestamps from date_created and date_modified. + $getters['date_created'] = $getters['date_created']->getOffsetTimestamp(); + $getters['date_modified'] = $getters['date_modified']->getOffsetTimestamp(); + + $this->assertEquals( $setters, $getters ); + } + + /** + * Test getting a customer's last order ID and date + * @since 3.0.0 + */ + public function test_customer_get_last_order_info() { + $customer = WC_Helper_Customer::create_customer(); + $customer_id = $customer->get_id(); + $order = WC_Helper_Order::create_order( $customer_id ); + $customer = new WC_Customer( $customer_id ); + $last_order = $customer->get_last_order(); + $this->assertEquals( $order->get_id(), $last_order ? $last_order->get_id() : 0 ); + $this->assertEquals( $order->get_date_created(), $last_order ? $last_order->get_date_created() : 0 ); + $order->delete(); + } + + /** + * Test getting a customer's order count from DB. + * @since 3.0.0 + */ + public function test_customer_get_order_count_read() { + $customer = WC_Helper_Customer::create_customer(); + $customer_id = $customer->get_id(); + WC_Helper_Order::create_order( $customer_id ); + WC_Helper_Order::create_order( $customer_id ); + WC_Helper_Order::create_order( $customer_id ); + $customer = new WC_Customer( $customer_id ); + $this->assertEquals( 3, $customer->get_order_count() ); + } + + /** + * Test getting a customer's total amount spent from DB. + * @since 3.0.0 + */ + public function test_customer_get_total_spent_read() { + $customer = WC_Helper_Customer::create_customer(); + $customer_id = $customer->get_id(); + $order = WC_Helper_Order::create_order( $customer_id ); + $customer = new WC_Customer( $customer_id ); + $this->assertEquals( 0, $customer->get_total_spent() ); + $order->update_status( 'wc-completed' ); + $customer = new WC_Customer( $customer_id ); + $this->assertEquals( 50, $customer->get_total_spent() ); + $order->delete(); + } + + /** + * Test getting a customer's avatar URL. + * @since 3.0.0 + */ + public function test_customer_get_avatar_url() { + $customer = WC_Helper_Customer::create_customer(); + $this->assertStringContainsString( 'gravatar.com/avatar', $customer->get_avatar_url() ); + $this->assertStringContainsString( md5( 'test@woo.local' ), $customer->get_avatar_url() ); + } + + /** + * Test getting a customer's creation date from DB. + * @since 3.0.0 + */ + public function test_customer_get_date_created_read() { + $customer = WC_Helper_Customer::create_customer(); + $customer_id = $customer->get_id(); + $user = new WP_User( $customer_id ); + $this->assertEquals( strtotime( $user->data->user_registered ), $customer->get_date_created()->getOffsetTimestamp() ); + } + + /** + * Test getting a customer's modification date from DB. + * @since 3.0.0 + */ + public function test_customer_get_date_modified_read() { + $customer = WC_Helper_Customer::create_customer(); + $customer_id = $customer->get_id(); + $last = get_user_meta( $customer_id, 'last_update', true ); + sleep( 1 ); + $this->assertEquals( $last, $customer->get_date_modified()->getOffsetTimestamp() ); + $customer->set_billing_address( '1234 Some St.' ); + $customer->save(); + $update = get_user_meta( $customer_id, 'last_update', true ); + $this->assertEquals( $update, $customer->get_date_modified()->getOffsetTimestamp() ); + $this->assertNotEquals( $update, $last ); + } + + /** + * Test getting a customer's taxable address. + * @since 3.0.0 + */ + public function test_customer_get_taxable_address() { + $customer = WC_Helper_Customer::create_customer(); + $customer_id = $customer->get_id(); + $customer->set_shipping_postcode( '11111' ); + $customer->set_shipping_city( 'Test' ); + $customer->save(); + $customer = new WC_Customer( $customer_id ); + + update_option( 'woocommerce_tax_based_on', 'shipping' ); + $taxable = $customer->get_taxable_address(); + $this->assertEquals( 'US', $taxable[0] ); + $this->assertEquals( 'CA', $taxable[1] ); + $this->assertEquals( '11111', $taxable[2] ); + $this->assertEquals( 'Test', $taxable[3] ); + + update_option( 'woocommerce_tax_based_on', 'billing' ); + $taxable = $customer->get_taxable_address(); + $this->assertEquals( 'US', $taxable[0] ); + $this->assertEquals( 'CA', $taxable[1] ); + $this->assertEquals( '94110', $taxable[2] ); + $this->assertEquals( 'San Francisco', $taxable[3] ); + + update_option( 'woocommerce_tax_based_on', 'base' ); + $taxable = $customer->get_taxable_address(); + $this->assertEquals( WC()->countries->get_base_country(), $taxable[0] ); + $this->assertEquals( WC()->countries->get_base_state(), $taxable[1] ); + $this->assertEquals( WC()->countries->get_base_postcode(), $taxable[2] ); + $this->assertEquals( WC()->countries->get_base_city(), $taxable[3] ); + } + + /** + * Test getting a customer's downloadable products. + * @since 3.0.0 + */ + public function test_customer_get_downloadable_products() { + $customer = WC_Helper_Customer::create_customer(); + $customer_id = $customer->get_id(); + $this->assertEquals( wc_get_customer_available_downloads( $customer_id ), $customer->get_downloadable_products() ); + } + + /** + * Test setting a password on update - making sure it actually changes the users password. + * @since 3.0.0 + */ + public function test_customer_password() { + $customer = WC_Helper_Customer::create_customer(); + $customer_id = $customer->get_id(); + + $user = get_user_by( 'id', $customer_id ); + $this->assertTrue( wp_check_password( 'hunter2', $user->data->user_pass, $user->ID ) ); + + $customer->set_password( 'hunter3' ); + $customer->save(); + + $user = get_user_by( 'id', $customer_id ); + $this->assertTrue( wp_check_password( 'hunter3', $user->data->user_pass, $user->ID ) ); + } + + /** + * Test setting a customer's address to the base address. + * @since 3.0.0 + */ + public function test_customer_set_address_to_base() { + $customer = WC_Helper_Customer::create_customer(); + $customer->set_billing_address_to_base(); + $base = wc_get_customer_default_location(); + $this->assertEquals( $base['country'], $customer->get_billing_country() ); + $this->assertEquals( $base['state'], $customer->get_billing_state() ); + $this->assertEmpty( $customer->get_billing_postcode() ); + $this->assertEmpty( $customer->get_billing_city() ); + } + + /** + * Test setting a customer's shipping address to the base address. + * @since 3.0.0 + */ + public function test_customer_set_shipping_address_to_base() { + $customer = WC_Helper_Customer::create_customer(); + $customer->set_shipping_address_to_base(); + $base = wc_get_customer_default_location(); + $this->assertEquals( $base['country'], $customer->get_shipping_country() ); + $this->assertEquals( $base['state'], $customer->get_shipping_state() ); + $this->assertEmpty( $customer->get_shipping_postcode() ); + $this->assertEmpty( $customer->get_shipping_city() ); + } + + /** + * Test setting a customer's location (multiple address fields at once) + * @since 3.0.0 + */ + public function test_customer_set_location() { + $customer = WC_Helper_Customer::create_customer(); + $customer->set_billing_location( 'US', 'OH', '12345', 'Cleveland' ); + $this->assertEquals( 'US', $customer->get_billing_country() ); + $this->assertEquals( 'OH', $customer->get_billing_state() ); + $this->assertEquals( '12345', $customer->get_billing_postcode() ); + $this->assertEquals( 'Cleveland', $customer->get_billing_city() ); + } + + /** + * Test setting a customer's shipping location (multiple address fields at once) + * @since 3.0.0 + */ + public function test_customer_set_shipping_location() { + $customer = WC_Helper_Customer::create_customer(); + $customer->set_shipping_location( 'US', 'OH', '12345', 'Cleveland' ); + $this->assertEquals( 'US', $customer->get_shipping_country() ); + $this->assertEquals( 'OH', $customer->get_shipping_state() ); + $this->assertEquals( '12345', $customer->get_shipping_postcode() ); + $this->assertEquals( 'Cleveland', $customer->get_shipping_city() ); + } + + /** + * Test is_customer_outside_base. + * @since 3.0.0 + */ + public function test_customer_is_customer_outside_base() { + $customer = WC_Helper_Customer::create_customer(); + $this->assertFalse( $customer->is_customer_outside_base() ); + update_option( 'woocommerce_tax_based_on', 'base' ); + $customer->set_billing_address_to_base(); + $this->assertFalse( $customer->is_customer_outside_base() ); + } + + /** + * Test WC_Customer's session handling code. + * @since 3.0.0 + */ + public function test_customer_sessions() { + $session = WC_Helper_Customer::create_mock_customer(); // set into session.... + + $this->assertEquals( '94110', $session->get_billing_postcode() ); + $this->assertEquals( '123 South Street', $session->get_billing_address() ); + $this->assertEquals( 'San Francisco', $session->get_billing_city() ); + + $session->set_billing_address( '124 South Street' ); + $session->save(); + + $session = new WC_Customer( 0, true ); + $this->assertEquals( '124 South Street', $session->get_billing_address() ); + + $session = new WC_Customer( 0, true ); + $session->set_billing_postcode( '32191' ); + $session->save(); + + // should still be session ID, not a created row, since we are working with guests/sessions + $this->assertFalse( $session->get_id() > 0 ); + $this->assertEquals( '32191', $session->get_billing_postcode() ); + } + + /** + * Test getting meta. + * @since 3.0.0 + */ + public function test_get_meta() { + $customer = WC_Helper_Customer::create_customer(); + $customer_id = $customer->get_id(); + $meta_value = time() . '-custom-value'; + add_user_meta( $customer_id, 'test_field', $meta_value, true ); + $customer = new WC_Customer( $customer_id ); + $fields = $customer->get_meta_data(); + $this->assertEquals( $meta_value, $customer->get_meta( 'test_field' ) ); + } + + /** + * Test setting meta. + * @since 3.0.0 + */ + public function test_set_meta() { + $customer = WC_Helper_Customer::create_customer(); + $customer_id = $customer->get_id(); + $meta_value = time() . '-custom-value'; + $customer->add_meta_data( 'my-field', $meta_value, true ); + $customer->save(); + $customer = new WC_Customer( $customer_id ); + $this->assertEquals( $meta_value, $customer->get_meta( 'my-field' ) ); + } +} diff --git a/tests/legacy/unit-tests/customer/customer-download-log.php b/plugins/woocommerce/tests/legacy/unit-tests/customer/customer-download-log.php similarity index 100% rename from tests/legacy/unit-tests/customer/customer-download-log.php rename to plugins/woocommerce/tests/legacy/unit-tests/customer/customer-download-log.php diff --git a/plugins/woocommerce/tests/legacy/unit-tests/customer/customer.php b/plugins/woocommerce/tests/legacy/unit-tests/customer/customer.php new file mode 100644 index 00000000000..de73d7ccdaa --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/customer/customer.php @@ -0,0 +1,101 @@ +get_taxable_address(); // Default is geolocation! + + // Get the original settings for the session and the WooCommerce options + $original_chosen_shipping_methods = WC_Helper_Customer::get_chosen_shipping_methods(); + $original_tax_based_on = WC_Helper_Customer::get_tax_based_on(); + $original_customer_details = WC_Helper_Customer::get_customer_details(); + + // Create dummy product, and add the product to the cart. + $product = WC_Helper_Product::create_simple_product(); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Customer is going with the Local Pickup option, and the store calculates tax based on the store location. + WC_Helper_Customer::set_chosen_shipping_methods( array( 'local_pickup' ) ); + WC_Helper_Customer::set_tax_based_on( 'base' ); + $this->assertEquals( $customer->get_taxable_address(), $base_store_address ); + + // Customer is going with the Local Pickup option, and the store calculates tax based on the customer's billing address. + WC_Helper_Customer::set_chosen_shipping_methods( array( 'local_pickup' ) ); + WC_Helper_Customer::set_tax_based_on( 'billing' ); + $this->assertEquals( $customer->get_taxable_address(), $base_store_address ); + + // Customer is going with the Free Shipping option, and the store calculates tax based on the customer's billing address. + WC_Helper_Customer::set_chosen_shipping_methods( array( 'free_shipping' ) ); + WC_Helper_Customer::set_tax_based_on( 'billing' ); + $this->assertEquals( $customer->get_taxable_address(), $customer_address ); + + // Customer is going with the Free Shipping option, and the store calculates tax based on the store base location. + WC_Helper_Customer::set_chosen_shipping_methods( array( 'free_shipping' ) ); + WC_Helper_Customer::set_tax_based_on( 'base' ); + $this->assertEquals( $customer->get_taxable_address(), $base_store_address ); + + // Now reset the settings back to the way they were before this test + WC_Helper_Customer::set_chosen_shipping_methods( $original_chosen_shipping_methods ); + WC_Helper_Customer::set_tax_based_on( $original_tax_based_on ); + WC_Helper_Customer::set_customer_details( $original_customer_details ); + + // Clean up the cart + WC()->cart->empty_cart(); + } + + /** + * Test the is_customer_outside_base method. + */ + public function test_is_customer_outside_base() { + + // Get the original settings for the session and the WooCommerce options + $original_chosen_shipping_methods = WC_Helper_Customer::get_chosen_shipping_methods(); + $original_tax_based_on = WC_Helper_Customer::get_tax_based_on(); + $original_customer_details = WC_Helper_Customer::get_customer_details(); + + $customer = WC_Helper_Customer::create_mock_customer(); + + // Create dummy product, and add the product to the cart. + $product = WC_Helper_Product::create_simple_product(); + + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Customer is going with the Local Pickup option, and the store calculates tax based on the store location. + WC_Helper_Customer::set_chosen_shipping_methods( array( 'local_pickup' ) ); + WC_Helper_Customer::set_tax_based_on( 'base' ); + $this->assertEquals( $customer->is_customer_outside_base(), false ); + + // Customer is going with the Local Pickup option, and the store calculates tax based on the customer's billing address. + WC_Helper_Customer::set_chosen_shipping_methods( array( 'local_pickup' ) ); + WC_Helper_Customer::set_tax_based_on( 'billing' ); + $this->assertEquals( $customer->is_customer_outside_base(), false ); + + // Customer is going with the Free Shipping option, and the store calculates tax based on the customer's billing address. + WC_Helper_Customer::set_chosen_shipping_methods( array( 'free_shipping' ) ); + WC_Helper_Customer::set_tax_based_on( 'billing' ); + $this->assertEquals( $customer->is_customer_outside_base(), false ); + + // Customer is going with the Free Shipping option, and the store calculates tax based on the store base location. + WC_Helper_Customer::set_chosen_shipping_methods( array( 'free_shipping' ) ); + WC_Helper_Customer::set_tax_based_on( 'base' ); + $this->assertEquals( $customer->is_customer_outside_base(), false ); + + // Now reset the settings back to the way they were before this test + WC_Helper_Customer::set_chosen_shipping_methods( $original_chosen_shipping_methods ); + WC_Helper_Customer::set_tax_based_on( $original_tax_based_on ); + WC_Helper_Customer::set_customer_details( $original_customer_details ); + + // Clean up the cart + WC()->cart->empty_cart(); + } +} diff --git a/plugins/woocommerce/tests/legacy/unit-tests/customer/functions.php b/plugins/woocommerce/tests/legacy/unit-tests/customer/functions.php new file mode 100644 index 00000000000..c3c33acd987 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/customer/functions.php @@ -0,0 +1,521 @@ +get( Download_Directories::class )->set_mode( Download_Directories::MODE_DISABLED ); + } + + /** + * Set illegal login + * + * @param array $logins Array of blocked logins. + * @return array + */ + public function setup_illegal_user_logins( $logins ) { + return array( 'test' ); + } + + /** + * Test wc_create_new_customer. + * + * @since 3.1 + */ + public function test_wc_create_new_customer() { + + // Basic. + $id = wc_create_new_customer( 'test@example.com', 'testuser', 'testpassword' ); + $this->assertTrue( is_numeric( $id ) && $id > 0 ); + + // Existing email. + $id = wc_create_new_customer( 'test@example.com', 'testuser', 'testpassword' ); + $this->assertInstanceOf( 'WP_Error', $id ); + + // Empty username. + $id = wc_create_new_customer( '', 'testuser', 'testpassword' ); + $this->assertInstanceOf( 'WP_Error', $id ); + + // Bad email. + $id = wc_create_new_customer( 'bademail', 'testuser', 'testpassword' ); + $this->assertInstanceOf( 'WP_Error', $id ); + + // Existing username. + $id = wc_create_new_customer( 'test2@example.com', 'testuser', 'testpassword' ); + $this->assertInstanceOf( 'WP_Error', $id ); + + // Username with auto-generation. + update_option( 'woocommerce_registration_generate_username', 'yes' ); + $id = wc_create_new_customer( 'fred@example.com', '', 'testpassword' ); + $userdata = get_userdata( $id ); + $this->assertEquals( 'fred', $userdata->user_login ); + $id = wc_create_new_customer( 'fred@mail.com', '', 'testpassword' ); + $userdata = get_userdata( $id ); + $this->assertNotEquals( 'fred', $userdata->user_login ); + $this->assertStringContainsString( 'fred', $userdata->user_login ); + $id = wc_create_new_customer( 'fred@test.com', '', 'testpassword' ); + $userdata = get_userdata( $id ); + $this->assertNotEquals( 'fred', $userdata->user_login ); + $this->assertStringContainsString( 'fred', $userdata->user_login ); + + // Test extra arguments to generate display_name. + $id = wc_create_new_customer( + 'john.doe@example.com', + '', + 'testpassword', + array( + 'first_name' => 'John', + 'last_name' => 'Doe', + ) + ); + $userdata = get_userdata( $id ); + $this->assertEquals( 'John Doe', $userdata->display_name ); + + // No password. + update_option( 'woocommerce_registration_generate_password', 'no' ); + $id = wc_create_new_customer( 'joe@example.com', 'joecustomer', '' ); + $this->assertInstanceOf( 'WP_Error', $id ); + + // Auto-generated password. + update_option( 'woocommerce_registration_generate_password', 'yes' ); + $id = wc_create_new_customer( 'joe@example.com', 'joecustomer', '' ); + $this->assertTrue( is_numeric( $id ) && $id > 0 ); + } + + /** + * Test username generation. + */ + public function test_wc_create_new_customer_username() { + // Test getting name from email. + $this->assertEquals( 'mike', wc_create_new_customer_username( 'mike@fakemail.com', array() ) ); + + // Test getting name if username exists. + wc_create_new_customer( 'mike@fakemail.com', '', 'testpassword' ); + $username = wc_create_new_customer_username( 'mike@fakemail.com', array() ); + $this->assertNotEquals( 'mike', $username, $username ); + $this->assertStringContainsString( 'mike', $username, $username ); + + // Test common email prefix avoidance. + $this->assertEquals( 'somecompany.com', wc_create_new_customer_username( 'info@somecompany.com', array() ) ); + + // Test first/last name generation. + $this->assertEquals( + 'bob.bobson', + wc_create_new_customer_username( + 'bob@bobbobson.com', + array( + 'first_name' => 'Bob', + 'last_name' => 'Bobson', + ) + ) + ); + + // Test unicode fallbacks. + $this->assertEquals( + 'unicode', + wc_create_new_customer_username( + 'unicode@unicode.com', + array( + 'first_name' => 'こんにちは', + 'last_name' => 'こんにちは', + ) + ) + ); + + // Test username generation triggered by illegal_user_logins filter. + add_filter( 'illegal_user_logins', array( $this, 'setup_illegal_user_logins' ) ); + + $this->assertStringStartsWith( + 'woo_user_', + wc_create_new_customer_username( 'test@test.com' ) + ); + + remove_filter( 'illegal_user_logins', array( $this, 'setup_illegal_user_logins' ) ); + } + + /** + * Test wc_update_new_customer_past_orders. + * + * @since 3.1 + */ + public function test_wc_update_new_customer_past_orders() { + $customer_id = wc_create_new_customer( 'test@example.com', 'testuser', 'testpassword' ); + $order1 = new WC_Order(); + $order1->set_billing_email( 'test@example.com' ); + $order1->set_status( 'completed' ); + $order1->save(); + $order2 = new WC_Order(); + $order2->save(); + + // Test download permissions. + $prod_download = new WC_Product_Download(); + $prod_download->set_file( plugin_dir_url( __FILE__ ) . '/assets/images/help.png' ); + $prod_download->set_id( 'download' ); + + $product = new WC_Product_Simple(); + $product->set_downloadable( 'yes' ); + $product->set_downloads( array( $prod_download ) ); + $product->save(); + + $order3 = new WC_Order(); + $item = new WC_Order_Item_Product(); + $item->set_props( + array( + 'product' => $product, + 'quantity' => 1, + ) + ); + $order3->set_billing_email( 'test@example.com' ); + $order3->set_status( 'completed' ); + $order3->add_item( $item ); + $order3->save(); + + $downloads = wc_get_customer_available_downloads( $customer_id ); + $this->assertEquals( 0, count( $downloads ) ); + + // Link orders that haven't been linked. + $linked = wc_update_new_customer_past_orders( $customer_id ); + $this->assertEquals( 2, $linked ); + $order1 = wc_get_order( $order1->get_id() ); + $this->assertEquals( $customer_id, $order1->get_customer_id() ); + $order3 = wc_get_order( $order3->get_id() ); + $this->assertEquals( $customer_id, $order3->get_customer_id() ); + + // Test download permissions. + $downloads = wc_get_customer_available_downloads( $customer_id ); + $this->assertEquals( 1, count( $downloads ) ); + + // Don't link linked orders again. + $linked = wc_update_new_customer_past_orders( $customer_id ); + $this->assertEquals( 0, $linked ); + } + + /** + * Test wc_update_new_customer_past_orders with invalid or changed email. + * + * @since 3.1 + */ + public function test_wc_update_new_customer_past_orders_invalid_changed_email() { + $customer_id = wc_create_new_customer( 'test@example.com', 'testuser', 'testpassword' ); + $order1 = new WC_Order(); + $order1->set_billing_email( 'test@example.com' ); + $order1->set_status( 'completed' ); + $order1->save(); + + wp_update_user( + array( + 'ID' => $customer_id, + 'user_email' => 'invalid', + ) + ); + $linked = wc_update_new_customer_past_orders( $customer_id ); + $this->assertEquals( 0, $linked ); + + wp_update_user( + array( + 'ID' => $customer_id, + 'user_email' => 'new@example.com', + ) + ); + $linked = wc_update_new_customer_past_orders( $customer_id ); + $this->assertEquals( 0, $linked ); + } + + /** + * Test wc_paying_customer. + * + * @since 3.1 + */ + public function test_wc_paying_customer() { + $customer_id = wc_create_new_customer( 'test@example.com', 'testuser', 'testpassword' ); + + $customer = new WC_Customer( $customer_id ); + $this->assertFalse( $customer->get_is_paying_customer() ); + + // Test after new order created. + $order1 = new WC_Order(); + $order1->set_customer_id( $customer_id ); + $order1->set_status( 'completed' ); + $order1->save(); + + $customer = new WC_Customer( $customer_id ); + $this->assertTrue( $customer->get_is_paying_customer() ); + + // Test after the order is deleted! + $order1->delete( true ); + $customer = new WC_Customer( $customer_id ); + $this->assertFalse( $customer->get_is_paying_customer() ); + } + + /** + * Test wc_customer_bought_product. + * + * @since 3.1 + */ + public function test_wc_customer_bought_product() { + $customer_id_1 = wc_create_new_customer( 'test@example.com', 'testuser', 'testpassword' ); + $customer_id_2 = wc_create_new_customer( 'test2@example.com', 'testuser2', 'testpassword2' ); + $product_1 = new WC_Product_Simple(); + $product_1->save(); + $product_id_1 = $product_1->get_id(); + $product_2 = new WC_Product_Simple(); + $product_2->save(); + $product_id_2 = $product_2->get_id(); + + $order_1 = WC_Helper_Order::create_order( $customer_id_1, $product_1 ); + $order_1->set_billing_email( 'test@example.com' ); + $order_1->set_status( 'completed' ); + $order_1->save(); + $order_2 = WC_Helper_Order::create_order( $customer_id_2, $product_2 ); + $order_2->set_billing_email( 'test2@example.com' ); + $order_2->set_status( 'completed' ); + $order_2->save(); + $order_3 = WC_Helper_Order::create_order( $customer_id_1, $product_2 ); + $order_3->set_billing_email( 'test@example.com' ); + $order_3->set_status( 'pending' ); + $order_3->save(); + + $this->assertTrue( wc_customer_bought_product( 'test@example.com', $customer_id_1, $product_id_1 ) ); + $this->assertTrue( wc_customer_bought_product( '', $customer_id_1, $product_id_1 ) ); + $this->assertTrue( wc_customer_bought_product( 'test@example.com', 0, $product_id_1 ) ); + $this->assertFalse( wc_customer_bought_product( 'test@example.com', $customer_id_1, $product_id_2 ) ); + $this->assertFalse( wc_customer_bought_product( 'test2@example.com', $customer_id_2, $product_id_1 ) ); + } + + /** + * Test wc_customer_has_capability. + * + * @since 3.1 + */ + public function test_wc_customer_has_capability() { + $customer_id = wc_create_new_customer( 'test@example.com', 'testuser', 'testpassword' ); + + $order = new WC_Order(); + $order->set_customer_id( $customer_id ); + $order->save(); + + // Can view same user's order. + $allcaps = wc_customer_has_capability( array(), array( 'view_order' ), array( 'view_order', $customer_id, $order->get_id() ) ); + $this->assertTrue( $allcaps['view_order'] ); + + // Can't view other user's order. + $allcaps = wc_customer_has_capability( array(), array( 'view_order' ), array( 'view_order', 99, $order->get_id() ) ); + $this->assertTrue( empty( $allcaps['view_order'] ) ); + + // Can pay same user's order. + $allcaps = wc_customer_has_capability( array(), array( 'pay_for_order' ), array( 'pay_for_order', $customer_id, $order->get_id() ) ); + $this->assertTrue( $allcaps['pay_for_order'] ); + + // Can't pay other user's order. + $allcaps = wc_customer_has_capability( array(), array( 'pay_for_order' ), array( 'pay_for_order', 99, $order->get_id() ) ); + $this->assertTrue( empty( $allcaps['pay_for_order'] ) ); + + // Can pay new order. + $allcaps = wc_customer_has_capability( array(), array( 'pay_for_order' ), array( 'pay_for_order', $customer_id, null ) ); + $this->assertTrue( $allcaps['pay_for_order'] ); + + // Can order user's order again. + $allcaps = wc_customer_has_capability( array(), array( 'order_again' ), array( 'order_again', $customer_id, $order->get_id() ) ); + $this->assertTrue( $allcaps['order_again'] ); + + // Can't order other user's order again. + $allcaps = wc_customer_has_capability( array(), array( 'order_again' ), array( 'order_again', 99, $order->get_id() ) ); + $this->assertTrue( empty( $allcaps['order_again'] ) ); + + // Can cancel order. + $allcaps = wc_customer_has_capability( array(), array( 'cancel_order' ), array( 'cancel_order', $customer_id, $order->get_id() ) ); + $this->assertTrue( $allcaps['cancel_order'] ); + + // Can't cancel other user's order. + $allcaps = wc_customer_has_capability( array(), array( 'cancel_order' ), array( 'cancel_order', 99, $order->get_id() ) ); + $this->assertTrue( empty( $allcaps['cancel_order'] ) ); + + $download = new WC_Customer_Download(); + $download->set_user_id( $customer_id ); + $download->save(); + + // Can download. + $allcaps = wc_customer_has_capability( array(), array( 'download_file' ), array( 'download_file', $customer_id, $download ) ); + $this->assertTrue( $allcaps['download_file'] ); + + // Can't download other user's download. + $allcaps = wc_customer_has_capability( array(), array( 'download_file' ), array( 'download_file', 99, $download ) ); + $this->assertTrue( empty( $allcaps['download_file'] ) ); + } + + /** + * Test wc_get_customer_download_permissions. + * + * @since 3.1 + */ + public function test_wc_get_customer_download_permissions() { + $customer_id_1 = wc_create_new_customer( 'test@example.com', 'testuser', 'testpassword' ); + $customer_id_2 = wc_create_new_customer( 'test2@example.com', 'testuser2', 'testpassword2' ); + + $download_1 = new WC_Customer_Download(); + $download_1->set_user_id( $customer_id_1 ); + $download_1->set_order_id( 1 ); + $download_1->save(); + $download_2 = new WC_Customer_Download(); + $download_2->set_user_id( $customer_id_2 ); + $download_2->set_order_id( 2 ); + $download_2->save(); + + $permissions = wc_get_customer_download_permissions( $customer_id_1 ); + $this->assertEquals( 1, count( $permissions ) ); + $this->assertEquals( $download_1->get_id(), $permissions[0]->permission_id ); + + $permissions = wc_get_customer_download_permissions( $customer_id_2 ); + $this->assertEquals( 1, count( $permissions ) ); + $this->assertEquals( $download_2->get_id(), $permissions[0]->permission_id ); + } + + /** + * Test wc_get_customer_available_downloads. + * + * @since 3.1 + */ + public function test_wc_get_customer_available_downloads() { + $customer_id = wc_create_new_customer( 'test@example.com', 'testuser', 'testpassword' ); + + $prod_download = new WC_Product_Download(); + $prod_download->set_file( plugin_dir_url( __FILE__ ) . '/assets/images/help.png' ); + $prod_download->set_id( 1 ); + + $product = new WC_Product_Simple(); + $product->set_downloadable( 'yes' ); + $product->set_downloads( array( $prod_download ) ); + $product->save(); + + $cust_download = new WC_Customer_Download(); + $cust_download->set_user_id( $customer_id ); + $cust_download->set_product_id( $product->get_id() ); + $cust_download->set_download_id( $prod_download->get_id() ); + $cust_download->save(); + + $order = new WC_Order(); + $order->set_customer_id( $customer_id ); + $order->set_status( 'completed' ); + $order->save(); + + $cust_download->set_order_id( $order->get_id() ); + $cust_download->save(); + + $downloads = wc_get_customer_available_downloads( $customer_id ); + $this->assertEquals( 1, count( $downloads ) ); + + $download = current( $downloads ); + $this->assertEquals( $prod_download->get_id(), $download['download_id'] ); + $this->assertEquals( $order->get_id(), $download['order_id'] ); + $this->assertEquals( $product->get_id(), $download['product_id'] ); + } + + /** + * Test wc_get_customer_total_spent. + * + * @since 3.1 + */ + public function test_wc_get_customer_total_spent() { + $customer_id_1 = wc_create_new_customer( 'test@example.com', 'testuser', 'testpassword' ); + $customer_id_2 = wc_create_new_customer( 'test2@example.com', 'testuser2', 'testpassword2' ); + + $order_1 = new WC_Order(); + $order_1->set_status( 'completed' ); + $order_1->set_total( '100.00' ); + $order_1->set_customer_id( $customer_id_1 ); + $order_1->save(); + $order_2 = new WC_Order(); + $order_2->set_status( 'completed' ); + $order_2->set_total( '15.50' ); + $order_2->set_customer_id( $customer_id_1 ); + $order_2->save(); + $order_3 = new WC_Order(); + $order_3->set_status( 'completed' ); + $order_3->set_total( '50.01' ); + $order_3->set_customer_id( $customer_id_2 ); + $order_3->save(); + $order_4 = new WC_Order(); + $order_4->set_status( 'pending' ); + $order_4->set_total( '1.00' ); + $order_4->set_customer_id( $customer_id_2 ); + $order_4->save(); + + $this->assertEquals( 115.5, wc_get_customer_total_spent( $customer_id_1 ) ); + $this->assertEquals( 50.01, wc_get_customer_total_spent( $customer_id_2 ) ); + } + + /** + * Test wc_get_customer_order_count. + * + * @since 3.1 + */ + public function test_wc_get_customer_order_count() { + $customer_id_1 = wc_create_new_customer( 'test@example.com', 'testuser', 'testpassword' ); + $customer_id_2 = wc_create_new_customer( 'test2@example.com', 'testuser2', 'testpassword2' ); + + $order_1 = new WC_Order(); + $order_1->set_customer_id( $customer_id_1 ); + $order_1->save(); + $order_2 = new WC_Order(); + $order_2->set_customer_id( $customer_id_1 ); + $order_2->save(); + $order_3 = new WC_Order(); + $order_3->set_customer_id( $customer_id_2 ); + $order_3->save(); + + $this->assertEquals( 2, wc_get_customer_order_count( $customer_id_1 ) ); + $this->assertEquals( 1, wc_get_customer_order_count( $customer_id_2 ) ); + } + + /** + * Test wc_reset_order_customer_id_on_deleted_user. + * + * @since 3.1 + */ + public function test_wc_reset_order_customer_id_on_deleted_user() { + $customer_id = wc_create_new_customer( 'test@example.com', 'testuser', 'testpassword' ); + $customer = new WC_Customer( $customer_id ); + + $order = new WC_Order(); + $order->set_customer_id( $customer_id ); + $order->save(); + + $customer->delete(); + + $order = new WC_Order( $order->get_id() ); + $this->assertEquals( 0, $order->get_customer_id() ); + } + + /** + * Test wc_get_customer_last_order. + * + * @since 3.1 + */ + public function test_wc_get_customer_last_order() { + $customer_id = wc_create_new_customer( 'test@example.com', 'testuser', 'testpassword' ); + + $order_1 = new WC_Order(); + $order_1->set_customer_id( $customer_id ); + $order_1->save(); + $order_2 = new WC_Order(); + $order_2->set_customer_id( $customer_id ); + $order_2->save(); + + $last_order = wc_get_customer_last_order( $customer_id ); + $this->assertEquals( $order_2->get_id(), $last_order->get_id() ); + } +} diff --git a/plugins/woocommerce/tests/legacy/unit-tests/discounts/discounts.php b/plugins/woocommerce/tests/legacy/unit-tests/discounts/discounts.php new file mode 100644 index 00000000000..070fa56d1b3 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/discounts/discounts.php @@ -0,0 +1,1592 @@ +coupons[ $coupon->get_code() ] = $coupon; + } + + /** + * Helper function to hold a reference to created product objects so they + * can be cleaned up properly at the end of each test. + * + * @param WC_Product $product The product object to store. + */ + protected function store_product( $product ) { + $this->products[] = $product; + } + + /** + * Helper function to hold a reference to created order objects so they + * can be cleaned up properly at the end of each test. + * + * @param WC_Order $order The order object to store. + */ + protected function store_order( $order ) { + $this->orders[] = $order; + } + + /** + * Setup tests. + */ + public function setUp(): void { + parent::setUp(); + + $this->products = array(); + $this->coupons = array(); + $this->orders = array(); + $this->last_test_data = null; + } + + /** + * Clean up after each test. DB changes are reverted in parent::tearDown(). + */ + public function tearDown(): void { + WC()->cart->empty_cart(); + WC()->cart->remove_coupons(); + + parent::tearDown(); + } + + /** + * Test get and set items. + */ + public function test_get_set_items_from_cart() { + // Create dummy product - price will be 10. + $product = WC_Helper_Product::create_simple_product(); + $this->store_product( $product ); + + // Add product to the cart. + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Add product to a dummy order. + $order = new WC_Order(); + $order->add_product( $product, 1 ); + $order->calculate_totals(); + $order->save(); + $this->store_order( $order ); + + // Test setting items to the cart. + $discounts = new WC_Discounts(); + $discounts->set_items_from_cart( WC()->cart ); + $this->assertEquals( 1, count( $discounts->get_items() ) ); + + // Test setting items to an order. + $discounts = new WC_Discounts(); + $discounts->set_items_from_cart( WC()->cart ); + $this->assertEquals( 1, count( $discounts->get_items() ) ); + + // Empty array of items. + $discounts = new WC_Discounts(); + $discounts->set_items_from_cart( array() ); + $this->assertEquals( array(), $discounts->get_items() ); + + // Invalid items. + $discounts = new WC_Discounts(); + $discounts->set_items_from_cart( false ); + $this->assertEquals( array(), $discounts->get_items() ); + } + + /** + * Test applying a coupon (make sure it changes prices). + */ + public function test_apply_coupon() { + $discounts = new WC_Discounts(); + + // Create dummy content. + $product = WC_Helper_Product::create_simple_product(); + $product->set_tax_status( 'taxable' ); + $product->save(); + $this->store_product( $product ); + WC()->cart->empty_cart(); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + $coupon = WC_Helper_Coupon::create_coupon( 'test' ); + $coupon->set_amount( 10 ); + $this->store_coupon( $coupon ); + + // Apply a percent discount. + $coupon->set_discount_type( 'percent' ); + $discounts->set_items_from_cart( WC()->cart ); + $discounts->apply_coupon( $coupon ); + $this->assertEquals( 9, $discounts->get_discounted_price( current( $discounts->get_items() ) ), print_r( $discounts->get_discounts(), true ) ); + + // Apply a fixed cart coupon. + $coupon->set_discount_type( 'fixed_cart' ); + $discounts->set_items_from_cart( WC()->cart ); + $discounts->apply_coupon( $coupon ); + $this->assertEquals( 0, $discounts->get_discounted_price( current( $discounts->get_items() ) ), print_r( $discounts->get_discounts(), true ) ); + + // Apply a fixed product coupon. + $coupon->set_discount_type( 'fixed_product' ); + $discounts->set_items_from_cart( WC()->cart ); + $discounts->apply_coupon( $coupon ); + $this->assertEquals( 0, $discounts->get_discounted_price( current( $discounts->get_items() ) ), print_r( $discounts->get_discounts(), true ) ); + } + + /** + * Test various discount calculations are working correctly and producing expected results. + * + * @dataProvider calculations_test_provider + * + * @param array $test_data All of the settings to use for testing. + */ + public function test_calculations( $test_data ) { + $this->last_test_data = $test_data; + $discounts = new WC_Discounts(); + $products = array(); + + if ( isset( $test_data['tax_rate'] ) ) { + WC_Tax::_insert_tax_rate( $test_data['tax_rate'] ); + } + + if ( isset( $test_data['wc_options'] ) ) { + foreach ( $test_data['wc_options'] as $_option_name => $_option_value ) { + update_option( $_option_name, $_option_value['set'] ); + } + } + + foreach ( $test_data['cart'] as $key => $item ) { + $products[ $key ] = WC_Helper_Product::create_simple_product(); + $products[ $key ]->set_regular_price( $item['price'] ); + $products[ $key ]->set_tax_status( 'taxable' ); + $products[ $key ]->save(); + $this->store_product( $products[ $key ] ); + WC()->cart->add_to_cart( $products[ $key ]->get_id(), $item['qty'] ); + } + + $discounts->set_items_from_cart( WC()->cart ); + + foreach ( $test_data['coupons'] as $coupon_props ) { + $coupon = WC_Helper_Coupon::create_coupon( $coupon_props['code'] ); + $coupon->set_props( $coupon_props ); + $discounts->apply_coupon( $coupon ); + $this->store_coupon( $coupon ); + } + + $all_discounts = $discounts->get_discounts(); + + $discount_total = 0; + foreach ( $all_discounts as $code_name => $discounts_by_coupon ) { + $discount_total += array_sum( $discounts_by_coupon ); + } + + $this->assertEquals( $test_data['expected_total_discount'], $discount_total, 'Failed (' . print_r( $test_data, true ) . ' - ' . print_r( $discounts->get_discounts(), true ) . ')' ); + } + + /** + * This is a dataProvider for test_calculations(). + * + * @return array An array of discount tests to be run. + */ + public function calculations_test_provider() { + return array( + array( + array( + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'wc_options' => array( + 'woocommerce_calc_taxes' => array( + 'set' => 'yes', + 'revert' => 'no', + ), + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 10, + 'qty' => 1, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'percent', + 'amount' => '20', + ), + ), + 'expected_total_discount' => 2, + ), + ), + array( + array( + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 10, + 'qty' => 2, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'fixed_cart', + 'amount' => '10', + ), + ), + 'expected_total_discount' => 10, + ), + ), + array( + array( + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 10, + 'qty' => 1, + ), + array( + 'price' => 10, + 'qty' => 1, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'fixed_cart', + 'amount' => '10', + ), + ), + 'expected_total_discount' => 10, + ), + ), + array( + array( + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 10, + 'qty' => 1, + ), + array( + 'price' => 10, + 'qty' => 1, + ), + array( + 'price' => 10, + 'qty' => 1, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'fixed_cart', + 'amount' => '10', + ), + ), + 'expected_total_discount' => 10, + ), + ), + array( + array( + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 10, + 'qty' => 2, + ), + array( + 'price' => 10, + 'qty' => 3, + ), + array( + 'price' => 10, + 'qty' => 2, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'fixed_cart', + 'amount' => '10', + ), + ), + 'expected_total_discount' => 10, + ), + ), + array( + array( + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 10, + 'qty' => 1, + ), + array( + 'price' => 10, + 'qty' => 1, + ), + array( + 'price' => 10, + 'qty' => 1, + ), + array( + 'price' => 10, + 'qty' => 1, + ), + array( + 'price' => 10, + 'qty' => 1, + ), + array( + 'price' => 10, + 'qty' => 1, + ), + array( + 'price' => 10, + 'qty' => 1, + ), + array( + 'price' => 10, + 'qty' => 1, + ), + array( + 'price' => 10, + 'qty' => 1, + ), + array( + 'price' => 10, + 'qty' => 1, + ), + array( + 'price' => 10, + 'qty' => 1, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'fixed_cart', + 'amount' => '10', + ), + ), + 'expected_total_discount' => 10, + ), + ), + array( + array( + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 1, + 'qty' => 1, + ), + array( + 'price' => 1, + 'qty' => 1, + ), + array( + 'price' => 1, + 'qty' => 1, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'fixed_cart', + 'amount' => '1', + ), + ), + 'expected_total_discount' => 1, + ), + ), + array( + array( + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 10, + 'qty' => 2, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'percent', + 'amount' => '10', + 'limit_usage_to_x_items' => 1, + ), + ), + 'expected_total_discount' => 1, + ), + ), + array( + array( + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 10, + 'qty' => 2, + ), + array( + 'price' => 10, + 'qty' => 2, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'percent', + 'amount' => '10', + 'limit_usage_to_x_items' => 1, + ), + ), + 'expected_total_discount' => 1, + ), + ), + array( + array( + 'desc' => 'Test multiple coupons. No limits. Not discounting sequentially.', + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 10, + 'qty' => 2, + ), + array( + 'price' => 5, + 'qty' => 1, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'percent', + 'amount' => '10', + ), + array( + 'code' => 'test1', + 'discount_type' => 'percent', + 'amount' => '20', + ), + ), + 'expected_total_discount' => 7.5, + ), + ), + array( + array( + 'desc' => 'Test multiple coupons. One coupon has limit up to one item. Not discounting sequentially.', + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 10, + 'qty' => 2, + ), + array( + 'price' => 5, + 'qty' => 1, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'percent', + 'amount' => '10', + 'limit_usage_to_x_items' => 1, + ), + array( + 'code' => 'test1', + 'discount_type' => 'percent', + 'amount' => '20', + ), + ), + 'expected_total_discount' => 6, + ), + ), + array( + array( + 'desc' => 'Test multiple coupons. No limits. Discounting sequentially.', + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'wc_options' => array( + 'woocommerce_calc_discounts_sequentially' => array( + 'set' => 'yes', + 'revert' => 'no', + ), + ), + 'cart' => array( + array( + 'price' => 10, + 'qty' => 2, + ), + array( + 'price' => 5, + 'qty' => 1, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'percent', + 'amount' => '10', + ), + array( + 'code' => 'test1', + 'discount_type' => 'percent', + 'amount' => '20', + ), + ), + 'expected_total_discount' => 7, + ), + ), + array( + array( + 'desc' => 'Test multiple coupons. No limits. Discounting sequentially.', + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'wc_options' => array( + 'woocommerce_calc_discounts_sequentially' => array( + 'set' => 'yes', + 'revert' => 'no', + ), + ), + 'cart' => array( + array( + 'price' => 1.80, + 'qty' => 10, + ), + array( + 'price' => 13.95, + 'qty' => 3, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'percent', + 'amount' => '10', + ), + array( + 'code' => 'test1', + 'discount_type' => 'percent', + 'amount' => '20', + ), + ), + 'expected_total_discount' => 16.75, + ), + ), + array( + array( + 'desc' => 'Test multiple coupons. One coupon has limit up to 5 item. Discounting non-sequentially.', + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 10, + 'qty' => 3, + ), + array( + 'price' => 5, + 'qty' => 3, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'percent', + 'amount' => '30', + 'limit_usage_to_x_items' => 5, + ), + array( + 'code' => 'test1', + 'discount_type' => 'percent', + 'amount' => '20', + ), + ), + 'expected_total_discount' => 21, + ), + ), + array( + array( + 'desc' => 'Test multiple coupons. One coupon has limit up to 5 item. Discounting sequentially.', + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'wc_options' => array( + 'woocommerce_calc_discounts_sequentially' => array( + 'set' => 'yes', + 'revert' => 'no', + ), + ), + 'cart' => array( + array( + 'price' => 10, + 'qty' => 3, + ), + array( + 'price' => 5, + 'qty' => 3, + ), + ), + 'coupons' => array( + array( + 'code' => 'test1', + 'discount_type' => 'percent', + 'amount' => '20', + ), + array( + 'code' => 'test', + 'discount_type' => 'percent', + 'amount' => '30', + 'limit_usage_to_x_items' => 5, + ), + ), + 'expected_total_discount' => 18.60, + ), + ), + array( + array( + 'desc' => 'Test multiple coupons. One coupon has limit up to 5 item. Discounting sequentially. Multiple zero-dollar items.', + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'wc_options' => array( + 'woocommerce_calc_discounts_sequentially' => array( + 'set' => 'yes', + 'revert' => 'no', + ), + ), + 'cart' => array( + array( + 'price' => 1.80, + 'qty' => 3, + ), + array( + 'price' => 13.95, + 'qty' => 3, + ), + array( + 'price' => 0, + 'qty' => 1, + ), + array( + 'price' => 0, + 'qty' => 1, + ), + array( + 'price' => 0, + 'qty' => 1, + ), + array( + 'price' => 0, + 'qty' => 1, + ), + array( + 'price' => 0, + 'qty' => 1, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'percent', + 'amount' => '30', + 'limit_usage_to_x_items' => 5, + ), + array( + 'code' => 'test1', + 'discount_type' => 'percent', + 'amount' => '20', + ), + ), + 'expected_total_discount' => 20.35, + ), + ), + array( + array( + 'desc' => 'Test single fixed product coupon on one item.', + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 13.95, + 'qty' => 3, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'fixed_product', + 'amount' => '10', + ), + ), + 'expected_total_discount' => 30, + ), + ), + array( + array( + 'desc' => 'Test single fixed product coupon on one item. Coupon greater than item cost.', + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 13.95, + 'qty' => 3, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'fixed_product', + 'amount' => '20', + ), + ), + 'expected_total_discount' => 41.85, + ), + ), + array( + array( + 'desc' => 'Test single fixed product coupon on one item. Limit to one item.', + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 13.95, + 'qty' => 3, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'fixed_product', + 'amount' => '10', + 'limit_usage_to_x_items' => 1, + ), + ), + 'expected_total_discount' => 10, + ), + ), + array( + array( + 'desc' => 'Test single fixed product coupon on one item. Limit to one item. Price greater than product.', + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 13.95, + 'qty' => 3, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'fixed_product', + 'amount' => '15', + 'limit_usage_to_x_items' => 1, + ), + ), + 'expected_total_discount' => 13.95, + ), + ), + array( + array( + 'desc' => 'Test single fixed product coupon on one item. Limit to same number of items as product. Price same as product.', + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 13.95, + 'qty' => 3, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'fixed_product', + 'amount' => '13.95', + 'limit_usage_to_x_items' => 3, + ), + ), + 'expected_total_discount' => 41.85, + ), + ), + array( + array( + 'desc' => 'Test single fixed product coupon on two items. No limit.', + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 13.95, + 'qty' => 3, + ), + array( + 'price' => 1.80, + 'qty' => 5, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'fixed_product', + 'amount' => '10', + ), + ), + 'expected_total_discount' => 39, + ), + ), + array( + array( + 'desc' => 'Test single fixed product coupon on two items. Limit to same number of items as first product.', + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 13.95, + 'qty' => 3, + ), + array( + 'price' => 1.80, + 'qty' => 5, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'fixed_product', + 'amount' => '10', + 'limit_usage_to_x_items' => 3, + ), + ), + 'expected_total_discount' => 30, + ), + ), + array( + array( + 'desc' => 'Test single fixed product coupon on two items. Limit to number greater than first product but less than total quantities.', + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 13.95, + 'qty' => 3, + ), + array( + 'price' => 1.80, + 'qty' => 5, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'fixed_product', + 'amount' => '10', + 'limit_usage_to_x_items' => 5, + ), + ), + 'expected_total_discount' => 33.60, + ), + ), + array( + array( + 'desc' => 'Test single fixed product coupon on two items. Limit to number greater than first product but less than total quantities. Amount less than price of either product.', + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 13.95, + 'qty' => 3, + ), + array( + 'price' => 1.80, + 'qty' => 5, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'fixed_product', + 'amount' => '1', + 'limit_usage_to_x_items' => 5, + ), + ), + 'expected_total_discount' => 5, + ), + ), + array( + array( + 'desc' => 'Test single fixed product coupon on two items. Limit to number greater than both product quantities combined. Amount less than price of either product.', + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 13.95, + 'qty' => 3, + ), + array( + 'price' => 1.80, + 'qty' => 5, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'fixed_product', + 'amount' => '1', + 'limit_usage_to_x_items' => 10, + ), + ), + 'expected_total_discount' => 8, + ), + ), + array( + array( + 'desc' => 'Test single fixed product coupon on two items. Limit to two items. Testing the products are sorted according to legacy method where first one to apply is the one with greatest price * quantity.', + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 1.80, + 'qty' => 5, + ), + array( + 'price' => 13.95, + 'qty' => 3, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'fixed_product', + 'amount' => '10', + 'limit_usage_to_x_items' => 2, + ), + ), + 'expected_total_discount' => 20, + ), + ), + array( + array( + 'desc' => 'Test single fixed product coupon on one item to illustrate type conversion precision bug.', + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 8.95, + 'qty' => 1, + ), + ), + 'coupons' => array( + array( + 'code' => 'test', + 'discount_type' => 'fixed_product', + 'amount' => '10', + ), + ), + 'expected_total_discount' => 8.95, + ), + ), + array( + array( + 'desc' => 'Test multiple coupons with limits of 1.', + 'tax_rate' => array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ), + 'prices_include_tax' => false, + 'cart' => array( + array( + 'price' => 10, + 'qty' => 4, + ), + ), + 'coupons' => array( + array( + 'code' => 'one', + 'discount_type' => 'fixed_product', + 'amount' => '10', + 'limit_usage_to_x_items' => 1, + ), + array( + 'code' => 'two', + 'discount_type' => 'fixed_product', + 'amount' => '10', + 'limit_usage_to_x_items' => 1, + ), + array( + 'code' => 'three', + 'discount_type' => 'percent', + 'amount' => '100', + 'limit_usage_to_x_items' => 1, + ), + ), + 'expected_total_discount' => 30, + ), + ), + ); + } + + /** + * test_free_shipping_coupon_no_products. + */ + public function test_free_shipping_coupon_no_products() { + $discounts = new WC_Discounts(); + $coupon = WC_Helper_Coupon::create_coupon( 'freeshipping' ); + $coupon->set_props( + array( + 'discount_type' => 'percent', + 'amount' => '', + 'free_shipping' => 'yes', + ) + ); + + $discounts->apply_coupon( $coupon ); + + $all_discounts = $discounts->get_discounts(); + $this->assertEquals( 0, count( $all_discounts['freeshipping'] ), 'Free shipping coupon should not have any discounts.' ); + } + + /** + * filter_woocommerce_coupon_get_discount_amount. + * + * @param float $discount Discount amount. + */ + public function filter_woocommerce_coupon_get_discount_amount( $discount ) { + return $discount / 2; + } + + /** + * test_coupon_discount_amount_filter. + */ + public function test_coupon_discount_amount_filter() { + $discounts = new WC_Discounts(); + + add_filter( 'woocommerce_coupon_get_discount_amount', array( $this, 'filter_woocommerce_coupon_get_discount_amount' ) ); + + $product = WC_Helper_Product::create_simple_product(); + $product->set_regular_price( 100 ); + $product->set_tax_status( 'taxable' ); + $product->save(); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + $product2 = WC_Helper_Product::create_simple_product(); + $product2->set_regular_price( 100 ); + $product2->set_tax_status( 'taxable' ); + $product2->save(); + WC()->cart->add_to_cart( $product2->get_id(), 1 ); + + $coupon = WC_Helper_Coupon::create_coupon( 'test' ); + $coupon->set_props( + array( + 'code' => 'test', + 'discount_type' => 'percent', + 'amount' => '20', + ) + ); + + $discounts->set_items_from_cart( WC()->cart ); + $discounts->apply_coupon( $coupon ); + + $all_discounts = $discounts->get_discounts(); + + $discount_total = 0; + foreach ( $all_discounts as $code_name => $discounts_by_coupon ) { + $discount_total += array_sum( $discounts_by_coupon ); + } + + $this->assertEquals( 20, $discount_total ); + + remove_filter( 'woocommerce_coupon_get_discount_amount', array( $this, 'filter_woocommerce_coupon_get_discount_amount' ) ); + } + + /** + * Test the percent coupon logic with and without sale items. + * + * @since 3.4.6 + */ + public function test_is_coupon_valid_percent_sale_items() { + $product_no_sale = new WC_Product_Simple(); + $product_no_sale->set_regular_price( 20 ); + $product_no_sale->save(); + + $product_sale = new WC_Product_Simple(); + $product_sale->set_regular_price( 20 ); + $product_sale->set_sale_price( 10 ); + $product_sale->save(); + + $coupon_percent = new WC_Coupon(); + $coupon_percent->set_props( + array( + 'amount' => 10, + 'discount_type' => 'percent', + 'exclude_sale_items' => false, + ) + ); + $coupon_percent->save(); + + $coupon_percent_no_sale = new WC_Coupon(); + $coupon_percent_no_sale->set_props( + array( + 'amount' => 10, + 'discount_type' => 'percent', + 'exclude_sale_items' => true, + ) + ); + $coupon_percent_no_sale->save(); + + WC()->cart->empty_cart(); + WC()->cart->add_to_cart( $product_no_sale->get_id(), 1 ); + $discounts = new WC_Discounts( WC()->cart ); + + // Percent coupons should be valid when no sale items are in the cart. + $this->assertTrue( $discounts->is_coupon_valid( $coupon_percent ) ); + $this->assertTrue( $discounts->is_coupon_valid( $coupon_percent_no_sale ) ); + + // Percent coupons should be valid when sale items are in the cart. + WC()->cart->add_to_cart( $product_sale->get_id(), 1 ); + $discounts = new WC_Discounts( WC()->cart ); + $this->assertTrue( $discounts->is_coupon_valid( $coupon_percent ) ); + $this->assertTrue( $discounts->is_coupon_valid( $coupon_percent_no_sale ) ); + + // Sale-allowed coupons should apply discount to both cart items. + $discounts->apply_coupon( $coupon_percent ); + $coupon_discounts = array_sum( $discounts->get_discounts_by_coupon() ); + $this->assertEquals( 3.0, $coupon_discounts ); // 10% off $20 + 10% off $10. + + // No-sale coupons should only apply discount to non-sale items. + $discounts = new WC_Discounts( WC()->cart ); + $discounts->apply_coupon( $coupon_percent_no_sale ); + $coupon_discounts = array_sum( $discounts->get_discounts_by_coupon() ); + $this->assertEquals( 2.0, $coupon_discounts ); // 10% off $20. + } + + /** + * Test the fixed cart coupon logic with and without sale items. + * + * @since 3.4.6 + */ + public function test_is_coupon_valid_fixed_cart_sale_items() { + $product_no_sale = new WC_Product_Simple(); + $product_no_sale->set_regular_price( 20 ); + $product_no_sale->save(); + + $product_sale = new WC_Product_Simple(); + $product_sale->set_regular_price( 20 ); + $product_sale->set_sale_price( 10 ); + $product_sale->save(); + + $coupon_cart = new WC_Coupon(); + $coupon_cart->set_props( + array( + 'amount' => 5, + 'discount_type' => 'fixed_cart', + 'exclude_sale_items' => false, + ) + ); + $coupon_cart->save(); + + $coupon_cart_no_sale = new WC_Coupon(); + $coupon_cart_no_sale->set_props( + array( + 'amount' => 5, + 'discount_type' => 'fixed_cart', + 'exclude_sale_items' => true, + ) + ); + $coupon_cart_no_sale->save(); + + WC()->cart->empty_cart(); + WC()->cart->add_to_cart( $product_no_sale->get_id(), 1 ); + $discounts = new WC_Discounts( WC()->cart ); + + // Fixed cart coupons should be valid when no sale items are in the cart. + $this->assertTrue( $discounts->is_coupon_valid( $coupon_cart ) ); + $this->assertTrue( $discounts->is_coupon_valid( $coupon_cart_no_sale ) ); + + // No-sale fixed cart coupons should not be valid when sale items are in the cart. + WC()->cart->add_to_cart( $product_sale->get_id(), 1 ); + $discounts = new WC_Discounts( WC()->cart ); + $this->assertTrue( $discounts->is_coupon_valid( $coupon_cart ) ); + $this->assertTrue( is_wp_error( $discounts->is_coupon_valid( $coupon_cart_no_sale ) ) ); + + // Sale-allowed coupons should apply discount to total cart. + $discounts->apply_coupon( $coupon_cart ); + $coupon_discounts = array_sum( $discounts->get_discounts_by_coupon() ); + $this->assertEquals( 5.0, $coupon_discounts ); // $5 fixed cart discount. + } + + /** + * Test the per product coupon logic with and without sale items. + */ + public function test_is_coupon_valid_fixed_product_sale_items() { + $product_no_sale = new WC_Product_Simple(); + $product_no_sale->set_regular_price( 20 ); + $product_no_sale->save(); + + $product_sale = new WC_Product_Simple(); + $product_sale->set_regular_price( 20 ); + $product_sale->set_sale_price( 10 ); + $product_sale->save(); + + $coupon_product = new WC_Coupon(); + $coupon_product->set_props( + array( + 'amount' => 5, + 'discount_type' => 'fixed_product', + 'exclude_sale_items' => false, + ) + ); + $coupon_product->save(); + + $coupon_product_no_sale = new WC_Coupon(); + $coupon_product_no_sale->set_props( + array( + 'amount' => 5, + 'discount_type' => 'fixed_product', + 'exclude_sale_items' => true, + ) + ); + $coupon_product_no_sale->save(); + + WC()->cart->empty_cart(); + WC()->cart->add_to_cart( $product_no_sale->get_id(), 1 ); + $discounts = new WC_Discounts( WC()->cart ); + + // Per product coupons should be valid when no sale items are in the cart. + $this->assertTrue( $discounts->is_coupon_valid( $coupon_product ) ); + $this->assertTrue( $discounts->is_coupon_valid( $coupon_product_no_sale ) ); + + // Per product coupons should be valid when sale items are in the cart. + WC()->cart->add_to_cart( $product_sale->get_id(), 1 ); + $discounts = new WC_Discounts( WC()->cart ); + $this->assertTrue( $discounts->is_coupon_valid( $coupon_product ) ); + $this->assertTrue( $discounts->is_coupon_valid( $coupon_product_no_sale ) ); + + // Sale-allowed coupons should apply discount to each item. + $discounts->apply_coupon( $coupon_product ); + $coupon_discounts = array_sum( $discounts->get_discounts_by_coupon() ); + $this->assertEquals( 10.0, $coupon_discounts ); // $5 discount for 2 products. + + // No-sale coupons should only apply discount to non-sale items. + $discounts = new WC_Discounts( WC()->cart ); + $discounts->apply_coupon( $coupon_product_no_sale ); + $coupon_discounts = array_sum( $discounts->get_discounts_by_coupon() ); + $this->assertEquals( 5.0, $coupon_discounts ); // $5 discount for 1 product. + } +} diff --git a/plugins/woocommerce/tests/legacy/unit-tests/email/emails.php b/plugins/woocommerce/tests/legacy/unit-tests/email/emails.php new file mode 100644 index 00000000000..40a6b3a6333 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/email/emails.php @@ -0,0 +1,66 @@ +init(); + } + + /** + * Test get and set items. + */ + public function test_style_inline() { + $email = new WC_Email(); + + // Test HTML email with inline styles. + $email->email_type = 'html'; + + // Set some content to get converted. + $result = $email->style_inline( '

    Hello World!

    ' ); + + ob_start(); + include WC_Unit_Tests_Bootstrap::instance()->tests_dir . '/data/sample-email.html'; + $expected = ob_get_clean(); + + $this->assertEquals( $expected, $result ); + + // Test plain text email. + $email->email_type = 'plain'; + + // Set some content to get converted. + $result = $email->style_inline( '

    Hello World!

    ' ); + $expected = '

    Hello World!

    '; + + $this->assertEquals( $expected, $result ); + } + + /** + * Test that we remove elemets with style display none from html mails. + */ + public function test_remove_display_none_elements() { + $email = new WC_Email(); + $email->email_type = 'html'; + $str_present = 'Should be present!'; + $str_removed = 'Should be removed!'; + $result = $email->style_inline( "
    $str_present
    $str_removed
    " ); + $this->assertTrue( false !== strpos( $result, $str_present ) ); + $this->assertTrue( false === strpos( $result, $str_removed ) ); + } + +} diff --git a/plugins/woocommerce/tests/legacy/unit-tests/exporter/product.php b/plugins/woocommerce/tests/legacy/unit-tests/exporter/product.php new file mode 100644 index 00000000000..0e3b7036567 --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/exporter/product.php @@ -0,0 +1,163 @@ +plugin_dir . '/includes/export/class-wc-product-csv-exporter.php'; + } + + /** + * Test escape_data to prevent regressions that could open security holes. + * @since 3.1.0 + */ + public function test_escape_data() { + $exporter = new WC_Product_CSV_Exporter(); + + $data = "=cmd|' /C calc'!A0"; + $this->assertEquals( "'=cmd|' /C calc'!A0", $exporter->escape_data( $data ) ); + + $data = "+cmd|' /C calc'!A0"; + $this->assertEquals( "'+cmd|' /C calc'!A0", $exporter->escape_data( $data ) ); + + $data = "-cmd|' /C calc'!A0"; + $this->assertEquals( "'-cmd|' /C calc'!A0", $exporter->escape_data( $data ) ); + + $data = "@cmd|' /C calc'!A0"; + $this->assertEquals( "'@cmd|' /C calc'!A0", $exporter->escape_data( $data ) ); + } + + /** + * Test format_data. + * @since 3.1.0 + */ + public function test_format_data() { + $exporter = new WC_Product_CSV_Exporter(); + + $data = 'test'; + $this->assertEquals( 'test', $exporter->format_data( $data ) ); + + $time = time(); + $data = new WC_DateTime( "@{$time}", new DateTimeZone( 'UTC' ) ); + $this->assertEquals( date( 'Y-m-d G:i:s', $time ), $exporter->format_data( $data ) ); + + $data = true; + $this->assertEquals( 1, $exporter->format_data( $data ) ); + + $data = false; + $this->assertEquals( 0, $exporter->format_data( $data ) ); + } + + /** + * Test escape_data to prevent regressions that could open security holes. + * @since 3.1.0 + */ + public function test_format_term_ids() { + $exporter = new WC_Product_CSV_Exporter(); + + $term1 = wp_insert_category( array( 'cat_name' => 'cat1' ) ); + $term2 = wp_insert_category( array( 'cat_name' => 'cat2' ) ); + $term3 = wp_insert_category( array( 'cat_name' => 'cat3' ) ); + + $expected = 'cat1, cat2, cat3'; + $this->assertEquals( $expected, $exporter->format_term_ids( array( $term1, $term2, $term3 ), 'category' ) ); + + wp_insert_category( + array( + 'cat_ID' => $term2, + 'cat_name' => 'cat2', + 'category_parent' => $term1, + ) + ); + + $expected = 'cat1, cat1 > cat2, cat3'; + $this->assertEquals( $expected, $exporter->format_term_ids( array( $term1, $term2, $term3 ), 'category' ) ); + + wp_insert_category( + array( + 'cat_ID' => $term3, + 'cat_name' => 'cat3', + 'category_parent' => $term2, + ) + ); + $expected = 'cat1, cat1 > cat2, cat1 > cat2 > cat3'; + $this->assertEquals( $expected, $exporter->format_term_ids( array( $term1, $term2, $term3 ), 'category' ) ); + } + + /** + * Test prepare_data_to_export. + * @since 3.1.0 + */ + public function test_prepare_data_to_export() { + add_filter( 'woocommerce_product_export_row_data', array( $this, 'verify_exported_data' ), 10, 2 ); + $exporter = new WC_Product_CSV_Exporter(); + + $product = WC_Helper_Product::create_simple_product(); + $product->set_description( 'Test description' ); + $product->set_short_description( 'Test short description' ); + $product->set_weight( 12.5 ); + $product->set_height( 10 ); + $product->set_length( 20 ); + $product->set_width( 1 ); + + $sale_start = time(); + $sale_end = $sale_start + DAY_IN_SECONDS; + $product->set_date_on_sale_from( $sale_start ); + $product->set_date_on_sale_to( $sale_end ); + + $product->save(); + WC_Helper_Product::create_external_product(); + WC_Helper_Product::create_grouped_product(); + WC_Helper_Product::create_variation_product(); + + $exporter->set_product_category_to_export( array() ); + $exporter->prepare_data_to_export(); + } + + /** + * Verify one product for test_perpare_data_to_export. + * @since 3.1.0 + */ + public function verify_exported_data( $row, $product ) { + $this->assertEquals( $product->get_id(), $row['id'] ); + $this->assertEquals( $product->get_type(), $row['type'] ); + $this->assertEquals( $product->get_sku(), $row['sku'] ); + $this->assertEquals( $product->get_name(), $row['name'] ); + $this->assertEquals( $product->get_short_description(), $row['short_description'] ); + $this->assertEquals( $product->get_description(), $row['description'] ); + $this->assertEquals( $product->get_tax_status(), $row['tax_status'] ); + $this->assertEquals( $product->get_width(), $row['width'] ); + $this->assertEquals( $product->get_height(), $row['height'] ); + $this->assertEquals( $product->get_length(), $row['length'] ); + $this->assertEquals( $product->get_weight(), $row['weight'] ); + $this->assertEquals( $product->get_featured(), $row['featured'] ); + $this->assertEquals( $product->get_sold_individually(), $row['sold_individually'] ); + $this->assertEquals( $product->get_date_on_sale_from(), $row['date_on_sale_from'] ); + $this->assertEquals( $product->get_date_on_sale_to(), $row['date_on_sale_to'] ); + $this->assertEquals( 'publish' === $product->get_status(), $row['published'] ); + $this->assertEquals( 'instock' === $product->get_stock_status(), $row['stock_status'] ); + $this->assertEquals( $product->get_menu_order(), $row['menu_order'] ); + + $this->assertContains( $row['catalog_visibility'], array( 'visible', 'catalog', 'search', 'hidden' ) ); + $this->assertContains( $row['backorders'], array( 1, 0, 'notify' ) ); + + $expected_parent = ''; + $parent_id = $product->get_parent_id(); + if ( $parent_id ) { + $parent = wc_get_product( $parent_id ); + $expected_parent = $parent->get_sku() ? $parent->get_sku() : 'id:' . $parent->get_id(); + } + $this->assertEquals( $expected_parent, $row['parent_id'] ); + + return $row; + } +} diff --git a/plugins/woocommerce/tests/legacy/unit-tests/formatting/functions.php b/plugins/woocommerce/tests/legacy/unit-tests/formatting/functions.php new file mode 100644 index 00000000000..e613d9341ed --- /dev/null +++ b/plugins/woocommerce/tests/legacy/unit-tests/formatting/functions.php @@ -0,0 +1,1078 @@ +http_responder = array( $this, 'mock_http_responses' ); + } + + /** + * Test wc_string_to_bool(). + * + * @since 3.3.0 + */ + public function test_wc_string_to_bool() { + $this->assertTrue( wc_string_to_bool( 1 ) ); + $this->assertTrue( wc_string_to_bool( 'yes' ) ); + $this->assertTrue( wc_string_to_bool( 'Yes' ) ); + $this->assertTrue( wc_string_to_bool( 'YES' ) ); + $this->assertTrue( wc_string_to_bool( 'true' ) ); + $this->assertTrue( wc_string_to_bool( 'True' ) ); + $this->assertTrue( wc_string_to_bool( 'TRUE' ) ); + $this->assertFalse( wc_string_to_bool( 0 ) ); + $this->assertFalse( wc_string_to_bool( 'no' ) ); + $this->assertFalse( wc_string_to_bool( 'No' ) ); + $this->assertFalse( wc_string_to_bool( 'NO' ) ); + $this->assertFalse( wc_string_to_bool( 'false' ) ); + $this->assertFalse( wc_string_to_bool( 'False' ) ); + $this->assertFalse( wc_string_to_bool( 'FALSE' ) ); + } + + /** + * Test wc_bool_to_string(). + * + * @since 3.3.0 + */ + public function test_wc_bool_to_string() { + $this->assertEquals( array( 'yes', 'no' ), array( wc_bool_to_string( true ), wc_bool_to_string( false ) ) ); + } + + /** + * Test wc_string_to_array(). + * + * @since 3.3.0 + */ + public function test_wc_string_to_array() { + $this->assertEquals( + array( + 'foo', + 'bar', + ), + wc_string_to_array( 'foo|bar', '|' ) + ); + } + + /** + * Test wc_sanitize_taxonomy_name(). + * + * @since 2.2 + */ + public function test_wc_sanitize_taxonomy_name() { + $this->assertEquals( 'name-with-spaces', wc_sanitize_taxonomy_name( 'Name With Spaces' ) ); + $this->assertEquals( 'namewithtabs', wc_sanitize_taxonomy_name( 'Name With Tabs' ) ); + $this->assertEquals( 'specialchars', wc_sanitize_taxonomy_name( 'special!@#$%^&*()chars' ) ); + $this->assertEquals( 'look-of-ಠ_ಠ', wc_sanitize_taxonomy_name( 'Look Of ಠ_ಠ' ) ); + } + + /** + * Test wc_sanitize_permalink(). + * + * @since 3.3.0 + */ + public function test_wc_sanitize_permalink() { + $this->assertEquals( 'foo.com/bar', wc_sanitize_permalink( 'http://foo.com/bar' ) ); + } + + /** + * Test wc_get_filename_from_url(). + * + * @since 2.2 + */ + public function test_wc_get_filename_from_url() { + $this->assertEquals( 'woocommerce.pdf', wc_get_filename_from_url( 'https://woocommerce.com/woocommerce.pdf' ) ); + $this->assertEmpty( wc_get_filename_from_url( 'ftp://wc' ) ); + $this->assertEmpty( wc_get_filename_from_url( 'http://www.skyverge.com' ) ); + $this->assertEquals( 'woocommerce', wc_get_filename_from_url( 'https://woocommerce.com/woocommerce' ) ); + } + + /** + * Test wc_get_dimension(). + * + * @since 2.2 + */ + public function test_wc_get_dimension() { + // Save default. + $default_unit = get_option( 'woocommerce_dimension_unit' ); + + // cm (default unit). + $this->assertEquals( + array( 10, 3.937, 0.10936133, 100, 0.1 ), + array( + wc_get_dimension( 10, 'cm' ), + wc_get_dimension( 10, 'in' ), + wc_get_dimension( 10, 'yd' ), + wc_get_dimension( 10, 'mm' ), + wc_get_dimension( 10, 'm' ), + ) + ); + + // in. + update_option( 'woocommerce_dimension_unit', 'in' ); + $this->assertEquals( + array( 25.4, 10, 0.2777777782, 254, 0.254 ), + array( + wc_get_dimension( 10, 'cm' ), + wc_get_dimension( 10, 'in' ), + wc_get_dimension( 10, 'yd' ), + wc_get_dimension( 10, 'mm' ), + wc_get_dimension( 10, 'm' ), + ) + ); + + // m. + update_option( 'woocommerce_dimension_unit', 'm' ); + $this->assertEquals( + array( 1000, 393.7, 10.936133, 10000, 10 ), + array( + wc_get_dimension( 10, 'cm' ), + wc_get_dimension( 10, 'in' ), + wc_get_dimension( 10, 'yd' ), + wc_get_dimension( 10, 'mm' ), + wc_get_dimension( 10, 'm' ), + ) + ); + + // mm. + update_option( 'woocommerce_dimension_unit', 'mm' ); + $this->assertEquals( + array( 1, 0.3937, 0.010936133, 10, 0.01 ), + array( + wc_get_dimension( 10, 'cm' ), + wc_get_dimension( 10, 'in' ), + wc_get_dimension( 10, 'yd' ), + wc_get_dimension( 10, 'mm' ), + wc_get_dimension( 10, 'm' ), + ) + ); + + // yd. + update_option( 'woocommerce_dimension_unit', 'yd' ); + $this->assertEquals( + array( 914.4, 359.99928, 10, 9144, 9.144 ), + array( + wc_get_dimension( 10, 'cm' ), + wc_get_dimension( 10, 'in' ), + wc_get_dimension( 10, 'yd' ), + wc_get_dimension( 10, 'mm' ), + wc_get_dimension( 10, 'm' ), + ) + ); + + // Negative. + $this->assertEquals( 0, wc_get_dimension( -10, 'mm' ) ); + + // Custom. + $this->assertEquals( + array( 25.4, 914.4, 393.7, 0.010936133 ), + array( + wc_get_dimension( 10, 'cm', 'in' ), + wc_get_dimension( 10, 'cm', 'yd' ), + wc_get_dimension( 10, 'in', 'm' ), + wc_get_dimension( 10, 'yd', 'mm' ), + ) + ); + + // Restore default. + update_option( 'woocommerce_dimension_unit', $default_unit ); + } + + /** + * Test wc_get_weight(). + * + * @since 2.2 + */ + public function test_wc_get_weight() { + // Save default. + $default_unit = get_option( 'woocommerce_weight_unit' ); + + // kg (default unit). + $this->assertEquals( 10, wc_get_weight( 10, 'kg' ) ); + $this->assertEquals( 10000, wc_get_weight( 10, 'g' ) ); + $this->assertEquals( 22.0462, wc_get_weight( 10, 'lbs' ) ); + $this->assertEquals( 352.74, wc_get_weight( 10, 'oz' ) ); + + // g. + update_option( 'woocommerce_weight_unit', 'g' ); + $this->assertEquals( 0.01, wc_get_weight( 10, 'kg' ) ); + $this->assertEquals( 10, wc_get_weight( 10, 'g' ) ); + $this->assertEquals( 0.0220462, wc_get_weight( 10, 'lbs' ) ); + $this->assertEquals( 0.35274, wc_get_weight( 10, 'oz' ) ); + + // lbs. + update_option( 'woocommerce_weight_unit', 'lbs' ); + $this->assertEquals( 4.53592, wc_get_weight( 10, 'kg' ) ); + $this->assertEquals( 4535.92, wc_get_weight( 10, 'g' ) ); + $this->assertEquals( 10, wc_get_weight( 10, 'lbs' ) ); + $this->assertEquals( 160.00004208, wc_get_weight( 10, 'oz' ) ); + + // oz. + update_option( 'woocommerce_weight_unit', 'oz' ); + $this->assertEquals( 0.283495, wc_get_weight( 10, 'kg' ) ); + $this->assertEquals( 283.495, wc_get_weight( 10, 'g' ) ); + $this->assertEquals( 0.6249987469, wc_get_weight( 10, 'lbs' ) ); + $this->assertEquals( 10, wc_get_weight( 10, 'oz' ) ); + + // Custom from unit. + $this->assertEquals( 0.283495, wc_get_weight( 10, 'kg', 'oz' ) ); + $this->assertEquals( 0.01, wc_get_weight( 10, 'kg', 'g' ) ); + $this->assertEquals( 4.53592, wc_get_weight( 10, 'kg', 'lbs' ) ); + $this->assertEquals( 10, wc_get_weight( 10, 'kg', 'kg' ) ); + + // Negative. + $this->assertEquals( 0, wc_get_weight( -10, 'g' ) ); + + // Restore default. + update_option( 'woocommerce_weight_unit', $default_unit ); + } + + /** + * Test wc_trim_zeros(). + * + * @since 2.2 + */ + public function test_wc_trim_zeros() { + $this->assertEquals( '$1', wc_trim_zeros( '$1.00' ) ); + $this->assertEquals( '$1.10', wc_trim_zeros( '$1.10' ) ); + } + + /** + * Test wc_round_tax_total(). + * + * @since 2.2 + */ + public function test_wc_round_tax_total() { + update_option( 'woocommerce_prices_include_tax', 'no' ); + $this->assertEquals( 1.25, wc_round_tax_total( 1.246 ) ); + $this->assertEquals( 20, wc_round_tax_total( 19.9997 ) ); + $this->assertEquals( 19.99, wc_round_tax_total( 19.99 ) ); + $this->assertEquals( 19.99, wc_round_tax_total( 19.99 ) ); + $this->assertEquals( 83, wc_round_tax_total( 82.5, 0 ) ); + $this->assertEquals( 83, wc_round_tax_total( 82.54, 0 ) ); + $this->assertEquals( 83, wc_round_tax_total( 82.546, 0 ) ); + + update_option( 'woocommerce_prices_include_tax', 'yes' ); + $this->assertEquals( 1.25, wc_round_tax_total( 1.246 ) ); + $this->assertEquals( 20, wc_round_tax_total( 19.9997 ) ); + $this->assertEquals( 19.99, wc_round_tax_total( 19.99 ) ); + $this->assertEquals( 19.99, wc_round_tax_total( 19.99 ) ); + $this->assertEquals( 82, wc_round_tax_total( 82.5, 0 ) ); + $this->assertEquals( 83, wc_round_tax_total( 82.54, 0 ) ); + $this->assertEquals( 83, wc_round_tax_total( 82.546, 0 ) ); + + // Default. + update_option( 'woocommerce_prices_include_tax', 'no' ); + } + + /** + * Test wc_format_refund_total(). + * + * @since 2.2 + */ + public function test_wc_format_refund_total() { + $this->assertEquals( -10, wc_format_refund_total( 10 ) ); + $this->assertEquals( 10, wc_format_refund_total( -10 ) ); + } + + /** + * Test wc_format_decimal(). + * + * @since 2.2 + */ + public function test_wc_format_decimal() { + // Given string. + $this->assertEquals( '9.99', wc_format_decimal( '9.99' ) ); + + // Given string with multiple decimals points. + $this->assertEquals( '9.99', wc_format_decimal( '9...99' ) ); + + // Given string with multiple decimals points. + $this->assertEquals( '99.9', wc_format_decimal( '9...9....9' ) ); + + // Negative string. + $this->assertEquals( '-9.99', wc_format_decimal( '-9.99' ) ); + + // Float. + $this->assertEquals( '9.99', wc_format_decimal( 9.99 ) ); + + // DP false, no rounding. + $this->assertEquals( '9.9999', wc_format_decimal( 9.9999 ) ); + + // DP when empty string uses default as 2. + $this->assertEquals( '9.99', wc_format_decimal( 9.9911, '' ) ); + + // DP use default as 2 and round. + $this->assertEquals( '10.00', wc_format_decimal( 9.9999, '' ) ); + + // DP use custom. + $this->assertEquals( '9.991', wc_format_decimal( 9.9912, 3 ) ); + + // Trim zeros. + $this->assertEquals( '9', wc_format_decimal( 9.00, false, true ) ); + + // Trim zeros and round. + $this->assertEquals( '10', wc_format_decimal( 9.9999, '', true ) ); + + // Given string with thousands in german format. + update_option( 'woocommerce_price_decimal_sep', ',' ); + update_option( 'woocommerce_price_thousand_sep', '.' ); + + // Given string. + $this->assertEquals( '9.99', wc_format_decimal( '9,99' ) ); + + // Given string with multiple decimals points. + $this->assertEquals( '9.99', wc_format_decimal( '9,,,99' ) ); + + // Given string with multiple decimals points. + $this->assertEquals( '99.9', wc_format_decimal( '9,,,9,,,,9' ) ); + + // Negative string. + $this->assertEquals( '-9.99', wc_format_decimal( '-9,99' ) ); + + // Float. + $this->assertEquals( '9.99', wc_format_decimal( 9.99 ) ); + + // DP false, no rounding. + $this->assertEquals( '9.9999', wc_format_decimal( 9.9999 ) ); + + // DP when empty string uses default as 2. + $this->assertEquals( '9.99', wc_format_decimal( 9.9911, '' ) ); + + // DP use default as 2 and round. + $this->assertEquals( '10.00', wc_format_decimal( 9.9999, '' ) ); + + // DP use custom. + $this->assertEquals( '9.991', wc_format_decimal( 9.9912, 3 ) ); + + // Trim zeros. + $this->assertEquals( '9', wc_format_decimal( 9.00, false, true ) ); + + // Trim zeros and round. + $this->assertEquals( '10', wc_format_decimal( 9.9999, '', true ) ); + + update_option( 'woocommerce_price_num_decimals', '8' ); + + // Floats. + $this->assertEquals( '0.00001', wc_format_decimal( 0.00001 ) ); + $this->assertEquals( '0.22222222', wc_format_decimal( 0.22222222 ) ); + + update_option( 'woocommerce_price_num_decimals', '2' ); + update_option( 'woocommerce_price_decimal_sep', '.' ); + update_option( 'woocommerce_price_thousand_sep', ',' ); + } + + /** + * Test wc_float_to_string(). + * + * @since 2.2 + */ + public function test_wc_float_to_string() { + // Given string, return string. + $this->assertEquals( '1.99', wc_float_to_string( '1.99' ) ); + $this->assertEquals( '1.17', wc_float_to_string( 1.17 ) ); + } + + /** + * Test wc_format_localized_price(). + * + * @since 2.2 + */ + public function test_wc_format_localized_price() { + // Save default. + $decimal_sep = get_option( 'woocommerce_price_decimal_sep' ); + update_option( 'woocommerce_price_decimal_sep', ',' ); + + $this->assertEquals( '1,17', wc_format_localized_price( '1.17' ) ); + + // Restore default. + update_option( 'woocommerce_price_decimal_sep', $decimal_sep ); + } + + /** + * Test wc_format_localized_decimal(). + * + * @since 2.2 + */ + public function test_wc_format_localized_decimal() { + $this->assertEquals( '1.17', wc_format_localized_decimal( '1.17' ) ); + } + + /** + * Test wc_format_coupon_code(). + * + * @since 3.3.0 + */ + public function test_wc_format_coupon_code() { + $this->assertEquals( 'foo#baralert();', wc_format_coupon_code( 'FOO#bar' ) ); + } + + /** + * Test wc_clean(). + * + * @since 2.2 + */ + public function test_wc_clean() { + $this->assertEquals( 'cleaned', wc_clean( 'cleaned' ) ); + $this->assertEquals( array( 'cleaned', 'foo' ), wc_clean( array( 'cleaned', 'foo' ) ) ); + } + + /** + * Test wc_sanitize_textarea(). + * + * @since 3.3.0 + */ + public function test_wc_sanitize_textarea() { + $this->assertEquals( "foo\ncleaned\nbar", wc_sanitize_textarea( "foo\ncleaned\nbar" ) ); + } + + /** + * Test wc_sanitize_tooltip(). + * + * Note this is a basic type test as WP core already has coverage for wp_kses(). + * + * @since 2.4 + */ + public function test_wc_sanitize_tooltip() { + $this->assertEquals( 'alert();cleaned<p>foo</p><span>bar</span>', wc_sanitize_tooltip( 'cleaned

    foo

    bar' ) ); + } + + /** + * Test wc_array_overlay(). + * + * @since 2.2 + */ + public function test_wc_array_overlay() { + $a1 = array( + 'apple' => 'banana', + 'pear' => 'grape', + 'vegetables' => array( + 'cucumber' => 'asparagus', + ), + ); + + $a2 = array( + 'strawberry' => 'orange', + 'apple' => 'kiwi', + 'vegetables' => array( + 'cucumber' => 'peas', + ), + ); + + $overlayed = array( + 'apple' => 'kiwi', + 'pear' => 'grape', + 'vegetables' => array( + 'cucumber' => 'peas', + ), + ); + + $this->assertEquals( $overlayed, wc_array_overlay( $a1, $a2 ) ); + } + + /** + * Test wc_stock_amount(). + * + * @since 2.2 + */ + public function test_wc_stock_amount() { + $this->assertEquals( 10, wc_stock_amount( 10 ) ); + $this->assertEquals( 10, wc_stock_amount( '10' ) ); + $this->assertEquals( 3, wc_stock_amount( 3.43 ) ); + } + + /** + * Test wc_get_woocommerce_price_format(). + * + * @since 2.2 + */ + public function test_get_woocommerce_price_format() { + // Save default. + $currency_pos = get_option( 'woocommerce_currency_pos' ); + + // Default format (left). + $this->assertEquals( '%1$s%2$s', get_woocommerce_price_format() ); + + // Right. + update_option( 'woocommerce_currency_pos', 'right' ); + $this->assertEquals( '%2$s%1$s', get_woocommerce_price_format() ); + + // Left space. + update_option( 'woocommerce_currency_pos', 'left_space' ); + $this->assertEquals( '%1$s %2$s', get_woocommerce_price_format() ); + + // Right space. + update_option( 'woocommerce_currency_pos', 'right_space' ); + $this->assertEquals( '%2$s %1$s', get_woocommerce_price_format() ); + + // Restore default. + update_option( 'woocommerce_currency_pos', $currency_pos ); + } + + /** + * Test wc_get_price_thousand_separator(). + * + * @since 2.4 + */ + public function test_wc_get_price_thousand_separator() { + $separator = get_option( 'woocommerce_price_thousand_sep' ); + + // Default value. + $this->assertEquals( ',', wc_get_price_thousand_separator() ); + + update_option( 'woocommerce_price_thousand_sep', '.' ); + $this->assertEquals( '.', wc_get_price_thousand_separator() ); + + update_option( 'woocommerce_price_thousand_sep', '<.>' ); + $this->assertEquals( '<.>', wc_get_price_thousand_separator() ); + + update_option( 'woocommerce_price_thousand_sep', $separator ); + } + + /** + * Test wc_get_price_decimal_separator(). + * + * @since 2.4 + */ + public function test_wc_get_price_decimal_separator() { + $separator = get_option( 'woocommerce_price_decimal_sep' ); + + // Default value. + $this->assertEquals( '.', wc_get_price_decimal_separator() ); + + update_option( 'woocommerce_price_decimal_sep', ',' ); + $this->assertEquals( ',', wc_get_price_decimal_separator() ); + + update_option( 'woocommerce_price_decimal_sep', '<.>' ); + $this->assertEquals( '<.>', wc_get_price_decimal_separator() ); + + update_option( 'woocommerce_price_decimal_sep', $separator ); + } + + /** + * Test wc_get_price_decimals(). + * + * @since 2.4 + */ + public function test_wc_get_price_decimals() { + $decimals = get_option( 'woocommerce_price_num_decimals' ); + + // Default value. + $this->assertEquals( 2, wc_get_price_decimals() ); + + update_option( 'woocommerce_price_num_decimals', '1' ); + $this->assertEquals( 1, wc_get_price_decimals() ); + + update_option( 'woocommerce_price_num_decimals', '-2' ); + $this->assertEquals( 2, wc_get_price_decimals() ); + + update_option( 'woocommerce_price_num_decimals', '2.50' ); + $this->assertEquals( 2, wc_get_price_decimals() ); + + update_option( 'woocommerce_price_num_decimals', $decimals ); + } + + /** + * Test wc_price(). + * + * @since 2.2 + */ + public function test_wc_price() { + // Common prices. + $this->assertEquals( '$1.00', wc_price( 1 ) ); + $this->assertEquals( '$1.10', wc_price( 1.1 ) ); + $this->assertEquals( '$1.17', wc_price( 1.17 ) ); + $this->assertEquals( '$1,111.17', wc_price( 1111.17 ) ); + $this->assertEquals( '$0.00', wc_price( 0 ) ); + + // Different currency. + $this->assertEquals( '£1,111.17', wc_price( 1111.17, array( 'currency' => 'GBP' ) ) ); + + // Negative price. + $this->assertEquals( '-$1.17', wc_price( -1.17 ) ); + + // Bogus prices. + $this->assertEquals( '$0.00', wc_price( null ) ); + $this->assertEquals( '$0.00', wc_price( 'Q' ) ); + $this->assertEquals( '$0.00', wc_price( 'ಠ_ಠ' ) ); + + // Trim zeros. + add_filter( 'woocommerce_price_trim_zeros', '__return_true' ); + $this->assertEquals( '$1', wc_price( 1.00 ) ); + remove_filter( 'woocommerce_price_trim_zeros', '__return_true' ); + + // Ex tax label. + $calc_taxes = get_option( 'woocommerce_calc_taxes' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + $this->assertEquals( '$1,111.17 (ex. tax)', wc_price( '1111.17', array( 'ex_tax_label' => true ) ) ); + update_option( 'woocommerce_calc_taxes', $calc_taxes ); + } + + /** + * Test wc_let_to_num(). + * + * @since 2.2 + */ + public function test_wc_let_to_num() { + $this->assertSame( + array( 10240, 10485760, 10737418240, 10995116277760, 11258999068426240, 0, 0, 0, 0 ), + array( + wc_let_to_num( '10K' ), + wc_let_to_num( '10M' ), + wc_let_to_num( '10G' ), + wc_let_to_num( '10T' ), + wc_let_to_num( '10P' ), + wc_let_to_num( false ), + wc_let_to_num( true ), + wc_let_to_num( '' ), + wc_let_to_num( 'ABC' ), + ) + ); + } + + /** + * Test wc_date_format(). + * + * @since 2.2 + */ + public function test_wc_date_format() { + $this->assertEquals( get_option( 'date_format' ), wc_date_format() ); + } + + /** + * Test wc_time_format(). + * + * @since 2.2 + */ + public function test_wc_time_format() { + $this->assertEquals( get_option( 'time_format' ), wc_time_format() ); + } + + /** + * Test wc_string_to_timestamp(). + * + * @since 3.3.0 + */ + public function test_wc_string_to_timestamp() { + $this->assertEquals( 1507075200, wc_string_to_timestamp( '2017-10-04' ) ); + $this->assertEquals( 1507075200, wc_string_to_timestamp( '2017-10-04', strtotime( '3000-10-04' ) ) ); + } + + /** + * Test wc_string_to_datetime(). + * + * @since 3.3.0 + */ + public function test_wc_string_to_datetime() { + $data = wc_string_to_datetime( '2014-10-04' ); + + $this->assertInstanceOf( 'WC_DateTime', $data ); + $this->assertEquals( 1412380800, $data->getTimestamp() ); + } + + /** + * Test wc_timezone_string(). + * + * @since 2.2 + */ + public function test_wc_timezone_string() { + // Test when timezone string exists. + update_option( 'timezone_string', 'America/New_York' ); + $this->assertEquals( 'America/New_York', wc_timezone_string() ); + + // Restore default. + update_option( 'timezone_string', '' ); + + // Test with missing UTC offset. + delete_option( 'gmt_offset' ); + $this->assertContains( wc_timezone_string(), array( '+00:00', 'UTC' ) ); + + // Test with manually set UTC offset. + update_option( 'gmt_offset', -4 ); + $this->assertNotContains( wc_timezone_string(), array( '+00:00', 'UTC' ) ); + + // Test with invalid offset. + update_option( 'gmt_offset', 'invalid' ); + $this->assertContains( wc_timezone_string(), array( '+00:00', 'UTC' ) ); + + // Restore default. + update_option( 'gmt_offset', '0' ); + } + + /** + * Test wc_timezone_offset(). + * + * @since 3.3.0 + */ + public function test_wc_timezone_offset() { + $this->assertEquals( 0.0, wc_timezone_offset() ); + } + + /** + * Test wc_rgb_from_hex(). + * + * @since 2.2 + */ + public function test_wc_rgb_from_hex() { + $rgb = array( + 'R' => 0, + 'G' => 93, + 'B' => 171, + ); + + $this->assertEquals( $rgb, wc_rgb_from_hex( '005dab' ) ); + $this->assertEquals( $rgb, wc_rgb_from_hex( '#005dab' ) ); + } + + /** + * Test wc_hex_darker(). + * + * @since 2.2 + */ + public function test_wc_hex_darker() { + $this->assertEquals( '#004178', wc_hex_darker( '005dab' ) ); + $this->assertEquals( '#004178', wc_hex_darker( '#005dab' ) ); + } + + /** + * Test wc_hex_lighter(). + * + * @since 2.2 + */ + public function test_wc_hex_lighter() { + $this->assertEquals( '#4d8ec4', wc_hex_lighter( '005dab' ) ); + $this->assertEquals( '#4d8ec4', wc_hex_lighter( '#005dab' ) ); + $this->assertEquals( '#0c3a3b', wc_hex_lighter( '0a3839', 1 ) ); + } + + /** + * Test wc_light_or_dark(). + * + * @since 2.2 + */ + public function test_wc_light_or_dark() { + $this->assertEquals( '#FFFFFF', wc_light_or_dark( '005dab' ) ); + $this->assertEquals( '#FFFFFF', wc_light_or_dark( '#005dab' ) ); + } + + /** + * Test wc_format_hex(). + * + * @since 2.2 + */ + public function test_wc_format_hex() { + $this->assertEquals( '#CCCCCC', wc_format_hex( 'CCC' ) ); + $this->assertEquals( '#CCCCCC', wc_format_hex( '#CCC' ) ); + $this->assertEquals( null, wc_format_hex( null ) ); + } + + /** + * Test wc_format_postcode(). + * + * @since 2.2 + */ + public function test_wc_format_postcode() { + // Generic postcode. + $this->assertEquals( '02111', wc_format_postcode( ' 02111 ', 'US' ) ); + + // US 9-digit postcode. + $this->assertEquals( '02111-9999', wc_format_postcode( ' 021119999 ', 'US' ) ); + + // UK postcode. + $this->assertEquals( 'PCRN 1ZZ', wc_format_postcode( 'pcrn1zz', 'GB' ) ); + + // BR/PL postcode. + $this->assertEquals( '99999-999', wc_format_postcode( '99999999', 'BR' ) ); + + // JP postcode. + $this->assertEquals( '999-9999', wc_format_postcode( '9999999', 'JP' ) ); + + // Test empty NL postcode. + $this->assertEquals( '', wc_format_postcode( '', 'NL' ) ); + } + + /** + * Test wc_normalize_postcode(). + * + * @since 3.3.0 + */ + public function test_wc_normalize_postcode() { + $this->assertEquals( '99999999', wc_normalize_postcode( '99999-999' ) ); + } + + /** + * Test wc_format_phone_number(). + * + * @since 2.2 + */ + public function test_wc_format_phone_number() { + $this->assertEquals( '1-610-385-0000', wc_format_phone_number( '1.610.385.0000' ) ); + $this->assertEquals( '(32) 3212-2345', wc_format_phone_number( '(32) 3212-2345' ) ); + // This number contains non-visible unicode chars at the beginning and end of string, which makes it invalid phone number. + $this->assertEquals( '', wc_format_phone_number( '‭+47 0000 00003‬' ) ); + $this->assertEquals( '27 00 00 0000', wc_format_phone_number( '27 00 00 0000' ) ); + $this->assertEquals( '', wc_format_phone_number( '1-800-not a phone number' ) ); + } + + /** + * Test wc_sanitize_phone_number(). + * + * @since 3.6.0 + */ + public function test_wc_sanitize_phone_number() { + $this->assertEquals( '+16103850000', wc_sanitize_phone_number( '+1.610.385.0000' ) ); + // This number contains non-visible unicode chars at the beginning and end of string. + $this->assertEquals( '+47000000003', wc_sanitize_phone_number( '‭+47 0000 00003‬' ) ); + $this->assertEquals( '2700000000', wc_sanitize_phone_number( '27 00 00 0000' ) ); + // Check with an invalid number too. + $this->assertEquals( '1800', wc_sanitize_phone_number( '1-800-not a phone number' ) ); + } + + /** + * Test wc_trim_string(). + * + * @since 2.2 + */ + public function test_wc_trim_string() { + $this->assertEquals( 'string', wc_trim_string( 'string' ) ); + $this->assertEquals( 's...', wc_trim_string( 'string', 4 ) ); + $this->assertEquals( 'st.', wc_trim_string( 'string', 3, '.' ) ); + $this->assertEquals( 'string¥', wc_trim_string( 'string¥', 7, '' ) ); + } + + /** + * Test wc_format_content(). + * + * @since 3.3.0 + */ + public function test_wc_format_content() { + $this->assertEquals( "

    foo

    \n", wc_format_content( 'foo' ) ); + } + + /** + * Test wc_sanitize_term_text_based(). + * + * @since 3.3.0 + */ + public function test_wc_sanitize_term_text_based() { + $this->assertEquals( 'foo', wc_sanitize_term_text_based( "

    foo

    \n" ) ); + } + + /** + * Test wc_make_numeric_postcode(). + * + * @since 3.3.0 + */ + public function test_wc_make_numeric_postcode() { + $this->assertEquals( '16050300', wc_make_numeric_postcode( 'PE30' ) ); + } + + /** + * Test wc_format_stock_for_display(). + * + * @since 3.3.0 + */ + public function test_wc_format_stock_for_display() { + $product = WC_Helper_Product::create_simple_product(); + + $product->set_stock_quantity( '10' ); + $this->assertEquals( '10 in stock', wc_format_stock_for_display( $product ) ); + + $product->set_stock_quantity( '1' ); + $default = get_option( 'woocommerce_stock_format' ); + update_option( 'woocommerce_stock_format', 'low_amount' ); + $this->assertEquals( 'Only 1 left in stock', wc_format_stock_for_display( $product ) ); + update_option( 'woocommerce_stock_format', $default ); + + $product->set_stock_quantity( '-1' ); + $product->set_manage_stock( true ); + $product->set_backorders( 'notify' ); + $this->assertEquals( '-1 in stock (can be backordered)', wc_format_stock_for_display( $product ) ); + + $product->delete( true ); + } + + /** + * Test wc_format_stock_quantity_for_display(). + * + * @since 3.3.0 + */ + public function test_wc_format_stock_quantity_for_display() { + $product = WC_Helper_Product::create_simple_product(); + + $product->set_stock_quantity( '10' ); + $this->assertEquals( '10', wc_format_stock_quantity_for_display( $product->get_stock_quantity(), $product ) ); + + $product->delete( true ); + } + + /** + * Test wc_format_sale_price(). + * + * @since 3.3.0 + */ + public function test_wc_format_sale_price() { + $this->assertEquals( ' $5.00', wc_format_sale_price( '10', '5' ) ); + } + + /** + * Test wc_format_price_range(). + * + * @since 3.3.0 + */ + public function test_wc_format_price_range() { + $this->assertEquals( '$10.00$5.00', wc_format_price_range( '10', '5' ) ); + } + + /** + * Test wc_format_weight(). + * + * @since 3.3.0 + */ + public function test_wc_format_weight() { + $this->assertEquals( '10 kg', wc_format_weight( '10' ) ); + } + + /** + * Test wc_format_dimensions(). + * + * @since 3.3.0 + */ + public function test_wc_format_dimensions() { + $this->assertEquals( '10 × 10 × 10 cm', wc_format_dimensions( array( 10, 10, 10 ) ) ); + } + + /** + * Test wc_format_datetime(). + * + * @since 3.3.0 + */ + public function test_wc_format_datetime() { + $date = new WC_DateTime( '2017-10-05', new DateTimeZone( 'UTC' ) ); + $this->assertEquals( 'October 5, 2017', wc_format_datetime( $date ) ); + $this->assertEquals( '', wc_format_datetime( 'foo' ) ); + } + + /** + * Test wc_do_oembeds(). + * + * @since 3.3.0 + */ + public function test_wc_do_oembeds() { + // In this case should only return the URL back, since oEmbed will run other actions on frontend. + $this->assertEquals( + "", // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript + wc_do_oembeds( 'https://wordpress.tv/2015/10/19/mike-jolley-user-onboarding-for-wordpress-plugins/' ) + ); + } + + /** + * Provides a mocked response for the oembed test. This way it is not necessary to perform + * a regular request to an external server which would significantly slow down the tests. + * + * This function is called by WP_HTTP_TestCase::http_request_listner(). + * + * @param array $request Request arguments. + * @param string $url URL of the request. + * + * @return array|false mocked response or false to let WP perform a regular request. + */ + protected function mock_http_responses( $request, $url ) { + $mocked_response = false; + + if ( false !== strpos( $url, 'https://wordpress.tv/oembed/' ) ) { + $mocked_response = array( + // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript + 'body' => '{"type":"video","version":"1.0","title":null,"width":500,"height":281,"html":"", // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript - wc_do_oembeds( 'https://wordpress.tv/2015/10/19/mike-jolley-user-onboarding-for-wordpress-plugins/' ) - ); - } - - /** - * Provides a mocked response for the oembed test. This way it is not necessary to perform - * a regular request to an external server which would significantly slow down the tests. - * - * This function is called by WP_HTTP_TestCase::http_request_listner(). - * - * @param array $request Request arguments. - * @param string $url URL of the request. - * - * @return array|false mocked response or false to let WP perform a regular request. - */ - protected function mock_http_responses( $request, $url ) { - $mocked_response = false; - - if ( false !== strpos( $url, 'https://wordpress.tv/oembed/' ) ) { - $mocked_response = array( - // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript - 'body' => '{"type":"video","version":"1.0","title":null,"width":500,"height":281,"html":"