Merge remote-tracking branch 'upstream/master' into patch-1
This commit is contained in:
commit
f08b3c2d3a
|
@ -0,0 +1,26 @@
|
|||
.*
|
||||
.*/
|
||||
*.lock
|
||||
*.md
|
||||
*.zip
|
||||
/bin/
|
||||
/build/
|
||||
/node_modules/
|
||||
/tests/
|
||||
babel.config.js
|
||||
CHANGELOG.txt
|
||||
composer.*
|
||||
contributors.html
|
||||
docker-compose.yaml
|
||||
Dockerfile
|
||||
Gruntfile.js
|
||||
none
|
||||
package-lock.json
|
||||
package.json
|
||||
packages/woocommerce-admin/docs
|
||||
phpcs.xml
|
||||
phpunit.xml
|
||||
phpunit.xml.dist
|
||||
README.md
|
||||
renovate.json
|
||||
webpack.config.js
|
|
@ -1,48 +1,7 @@
|
|||
<!-- This form is for reporting bugs and issues specific to the WooCommerce plugin. This is not a support portal. If you need technical support from a human being, please submit a ticket via the helpdesk instead: https://woocommerce.com/contact-us/ -->
|
||||
<!-- This form is for other issue types specific to the WooCommerce plugin. This is not a support portal. -->
|
||||
|
||||
<!-- Usage questions can also be directed to the public support forum here: https://wordpress.org/support/plugin/woocommerce, unless this is a question about a premium extension in which case you should use the helpdesk. -->
|
||||
**Prerequisites (mark completed items with an [x]):**
|
||||
- [ ] I have checked that my issue type is not listed here https://github.com/woocommerce/woocommerce/issues/new/choose
|
||||
- [ ] My issue is not a security issue, support request, bug report, enhancement or feature request (Please use the link above if it is).
|
||||
|
||||
<!-- If you have a feature request, submit it to: http://ideas.woocommerce.com/forums/133476-woocommerce -->
|
||||
|
||||
<!-- If you are a developer who needs a new filter/hook raise a PR instead :) -->
|
||||
|
||||
<!-- Please be as descriptive as possible; issues lacking the below details, or for any other reason than to report a bug, may be closed without action. -->
|
||||
|
||||
## Prerequisites
|
||||
|
||||
<!-- MARK COMPLETED ITEMS WITH AN [x] -->
|
||||
|
||||
- [ ] I have searched for similar issues in both open and closed tickets and cannot find a duplicate
|
||||
- [ ] The issue still exists against the latest `master` branch of WooCommerce on Github (this is **not** the same version as on WordPress.org!)
|
||||
- [ ] I have attempted to find the simplest possible steps to reproduce the issue
|
||||
- [ ] I have included a failing test as a pull request (Optional)
|
||||
|
||||
## Steps to reproduce the issue
|
||||
|
||||
<!-- We need to be able to reproduce the bug in order to fix it so please be descriptive! -->
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## Expected/actual behavior
|
||||
|
||||
When I follow those steps, I see...
|
||||
|
||||
I was expecting to see...
|
||||
|
||||
## Isolating the problem
|
||||
|
||||
<!-- MARK COMPLETED ITEMS WITH AN [x] -->
|
||||
|
||||
- [ ] This bug happens with only WooCommerce plugin 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
|
||||
|
||||
<details>
|
||||
```
|
||||
Copy and paste the system status report from **WooCommerce > System Status** in WordPress admin here.
|
||||
```
|
||||
</details>
|
||||
**Issue Description:**
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
name: "\U0001F46E♂️ Security issue"
|
||||
name: "\U0001F512 Security issue"
|
||||
about: Please report security issues *only* via https://www.hackerone.com
|
||||
title: ''
|
||||
labels: ''
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
---
|
||||
name: "\U0001F47D External issues"
|
||||
about: Please report WooCommerce REST API or WooCommerce Gutenberg Products Blocks issues directly to their respective repositories.
|
||||
about: Please report WooCommerce REST API, WooCommerce Admin or WooCommerce Gutenberg Products Blocks issues directly to their respective repositories.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Please report WooCommerce REST API (https://github.com/woocommerce/woocommerce-rest-api) or WooCommerce Gutenberg Products Blocks (https://github.com/woocommerce/woocommerce-gutenberg-products-block) issues directly to their respective repositories.
|
||||
Please report issues for the following features directly to their respective repositories.
|
||||
|
||||
WooCommerce REST API: https://github.com/woocommerce/woocommerce-rest-api
|
||||
|
||||
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
|
||||
|
|
|
@ -16,10 +16,13 @@ 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**
|
||||
from a human being - submit a ticket via the helpdesk
|
||||
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 WooCommerce Help and Share Facebook group
|
||||
- The Official WooCommerce Facebook Group https://www.facebook.com/groups/advanced.woocommerce/
|
||||
|
|
|
@ -8,11 +8,30 @@ assignees: ''
|
|||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
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.
|
||||
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.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
<!-- This template is for confirmed bugs only. If you have a support request or custom code related question please see our docs or use our forums, helpdesk, or Slack Community! https://github.com/woocommerce/woocommerce/issues/new?assignees=&labels=&template=3-Support.md&title= -->
|
||||
|
||||
<!-- Make sure to look through the existing issues 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 component labels https://github.com/woocommerce/woocommerce/labels?q=component -->
|
||||
<!-- 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 -->
|
||||
|
||||
**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 '....'
|
||||
|
@ -21,8 +40,7 @@ Steps to reproduce the behavior:
|
|||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
<!-- 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. -->
|
||||
|
||||
**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.
|
||||
|
@ -30,8 +48,11 @@ A clear and concise description of what you expected to happen.
|
|||
- [ ] 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.
|
||||
<details>
|
||||
```
|
||||
Copy and paste the system status report from **WooCommerce > System Status** in WordPress admin.
|
||||
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.
|
||||
```
|
||||
</details>
|
||||
|
|
|
@ -8,6 +8,10 @@ assignees: ''
|
|||
|
||||
---
|
||||
|
||||
<!-- Make sure to look through existing issues to see whether your idea is already being discussed. Feel free to contribute to any existing issues. -->
|
||||
|
||||
<!-- Search tip: You can filter issues using our enhancement label https://github.com/woocommerce/woocommerce/issues?q=is%3Aissue+label%3Aenhancement -->
|
||||
|
||||
**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 [...]
|
||||
|
||||
|
|
|
@ -8,6 +8,10 @@ assignees: ''
|
|||
|
||||
---
|
||||
|
||||
<!-- Make sure to look through existing issues to see whether your idea is already being discussed. Feel free to contribute to any existing issues. -->
|
||||
|
||||
<!-- Search tip: You can filter issues using our enhancement label https://github.com/woocommerce/woocommerce/issues?q=is%3Aissue+label%3Aenhancement -->
|
||||
|
||||
**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 [...]
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
name: Build release asset
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
jobs:
|
||||
build:
|
||||
name: Build release asset
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Build
|
||||
id: build
|
||||
uses: woocommerce/action-build@master
|
||||
with:
|
||||
generate-zip: true
|
||||
- name: Upload release asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: ${{ steps.build.outputs.zip_path }}
|
||||
asset_name: woocommerce.zip
|
||||
asset_content_type: application/zip
|
|
@ -13,7 +13,7 @@ project.properties
|
|||
*.swp
|
||||
|
||||
# Grunt
|
||||
/node_modules/
|
||||
node_modules/
|
||||
none
|
||||
|
||||
# Sass
|
||||
|
@ -47,6 +47,8 @@ tests/cli/vendor
|
|||
/tests/e2e/config/local.json
|
||||
/tests/e2e/docker
|
||||
/tests/e2e/env/docker/wp-cli/initialize.sh
|
||||
/tests/e2e/env/build/
|
||||
/tests/e2e/env/build-module/
|
||||
|
||||
# Logs
|
||||
/logs
|
||||
|
@ -54,6 +56,7 @@ tests/cli/vendor
|
|||
# Composer
|
||||
/vendor/
|
||||
contributors.md
|
||||
contributors.html
|
||||
|
||||
# Packages
|
||||
/packages/*
|
||||
|
@ -64,3 +67,6 @@ contributors.md
|
|||
|
||||
# Language files
|
||||
i18n/languages/woocommerce.pot
|
||||
|
||||
# Build
|
||||
build/
|
||||
|
|
28
.travis.yml
28
.travis.yml
|
@ -29,38 +29,36 @@ env:
|
|||
jobs:
|
||||
fast_finish: true
|
||||
include:
|
||||
- name: "Coding standard check"
|
||||
php: 7.4
|
||||
env: WP_VERSION=latest WP_MULTISITE=0 RUN_PHPCS=1
|
||||
- name: "Core E2E Tests"
|
||||
php: 7.4
|
||||
env: WP_VERSION=latest WP_MULTISITE=0 RUN_E2E=1
|
||||
script:
|
||||
- composer require wp-cli/i18n-command
|
||||
- npm run build
|
||||
- npm run build:packages
|
||||
- npm install jest --global
|
||||
- npm run docker:up
|
||||
- npm run test:e2e
|
||||
after_script:
|
||||
- npm run docker:down
|
||||
- name: "Unit tests code coverage"
|
||||
php: 7.4
|
||||
env: WP_VERSION=latest WP_MULTISITE=0 RUN_CODE_COVERAGE=1
|
||||
- name: "WooCommerce unit tests using WordPress nightly"
|
||||
- name: "WP Nightly"
|
||||
php: 7.4
|
||||
env: WP_VERSION=nightly WP_MULTISITE=0
|
||||
- name: "WP latest - 1"
|
||||
- name: "WP Latest"
|
||||
php: 7.2
|
||||
env: WP_VERSION=5.4 WP_MULTISITE=0
|
||||
- name: "WP Latest - 1"
|
||||
php: 7.2
|
||||
env: WP_VERSION=5.3 WP_MULTISITE=0
|
||||
- name: "WP latest - 2"
|
||||
- name: "WP Latest - 2"
|
||||
php: 7.2
|
||||
env: WP_VERSION=5.2 WP_MULTISITE=0
|
||||
- name: "WP 5.1"
|
||||
php: 7.2
|
||||
env: WP_VERSION=5.1 WP_MULTISITE=0
|
||||
- name: "WP 5.0"
|
||||
php: 7.0
|
||||
env: WP_VERSION=5.0 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
|
||||
|
|
|
@ -1,5 +1,84 @@
|
|||
== Changelog ==
|
||||
|
||||
= 4.2.0 - 2020-06-02 =
|
||||
|
||||
**WooCommerce**
|
||||
* Enhancement - Added Ghanaian regions to the state dropdown. #26273
|
||||
* Enhancement - Added Mozambique provinces to the state dropdown. #26162
|
||||
* Enhancement - Added support for the new group descriptions available on WordPress privacy exporters as of WP 5.3. #25575
|
||||
* Fix - Fixed false positives when checking if uploads directory is public. #26600
|
||||
* Fix - Introduced a new admin body class for supporting styling issues in WP 5.3+. #26251
|
||||
* Fix - Removed case conversion of meta keys from CSV imports. #25517
|
||||
* Fix - Allow schedule coupons via CRUD. #26387
|
||||
* Fix - Password visibility toggle when password strength check fails. #26132
|
||||
* Fix - Cross-sell placement when product has no description. #26334
|
||||
* Fix - Display of the rate limit warning during payment method creation. #26411
|
||||
* Fix - Made the shipping zone matching query's `zone_id` field more specific. #26308
|
||||
* Fix - Corrected the display of RTL languages on the WooCommerce.com addons page. #26080
|
||||
* Fix - Removed the postcode field for Ghana. #26272
|
||||
* Fix - Made the hiding of state fields more explicit for Germany, Denmark, and Sweden. #25598
|
||||
* Fix - Ensured that global attribute prefixes are passed to the `woocommerce_attribute_label` filter. #26022
|
||||
* Dev - Increased WordPress minimum version to 5.2 according to policy. #26550
|
||||
* Dev - Added the customer as a third argument to the `woocommerce_matched_rates` filter. #26361
|
||||
* Dev - Introduced `woocommerce_menu_order_count` filter. #26044
|
||||
* Dev - Introduced `should_send_ajax_request.adding_to_cart `cart event to allow short-circuiting cart addition. #25760
|
||||
* Dev - Made the jQuery selector for checkout form rows less specific. #25654
|
||||
* Dev - Changed the `{site_address}` placeholder to `{site_url}` for clarity. #25630
|
||||
* Dev - Deprecated `.wp-policy-help` and replaced with with the `.privacy-policy-tutorial` and `.wp-suggested-text` classes added in WP 5.1. #26072
|
||||
* Dev - Updated `automattic/jetpack-autoloader` to 1.7.0. #26559
|
||||
* Dev - Add a way to fetch basic object data. #26025
|
||||
|
||||
**REST API 1.0.8**
|
||||
* Enhancement - Add support for trash status for products in V2 and V3 API. #184
|
||||
* Dev - Updated minimum PHP requirement to 7.0 to keep up with WooCommerce Core.
|
||||
* Dev - Fixed failing unit tests. #105
|
||||
|
||||
**WooCommerce Admin 1.2.3**
|
||||
* Enhancement - Add onboarding payments note #4157
|
||||
* Enhancement - Marketing Inbox Note #4030
|
||||
* Performance - Use Route based code splitting to reduce bundle size #4094
|
||||
* Performance - trim down inbox note API request. #3977
|
||||
* Fix - Proper display of elements in wc-admin pages when in a RTL environment. #4051
|
||||
* Fix - Update UX when knowledge base articles fail to retrieve #4133
|
||||
* Fix - Updated messaging after last step in OBW. #4148
|
||||
* Fix - Reset profiler when visiting old OBW URL #4166.
|
||||
* Fix - Dashboard flash before OBW chunk loads #4259
|
||||
* Tweak - Enable the default homepage template to be filtered #4072
|
||||
* Tweak - Create admin note if Jetpack or WooCommerce Services plugin doesn't get installed due to an error during OBW #3888
|
||||
* Tweak - Update Email Marketing note. #4167
|
||||
* Tweak - Adjust "demo products" verbiage to "Sample Products" #4184
|
||||
* Tweak - Don't reschedule imports on failed imports #4263
|
||||
* Tweak - Remove obsolete inbox messages #4182
|
||||
* Tweak - Updates to WooCommerce Payments in Setup Checklist #4293
|
||||
* Dev - Make query selector for admin alerts more specific #4289
|
||||
* Dev - Guard against null themes in OBW #4244
|
||||
* Dev - Update wcadmin db version after db callback #4323
|
||||
* Dev - Only migrate options on version change #4324
|
||||
* Dev - Use `PAGE_ROOT` constant to reduce redundant strings #4238
|
||||
* Dev - Decouple Plugins DataStore from onboarding feature #4048
|
||||
* Dev - Move API out of Onboarding #4093
|
||||
* Dev - Add Profiler Step View Tracks #4141
|
||||
* Dev - Add React Testing Library #4221
|
||||
* Dev - Add List and Link components to Storybook #4219
|
||||
* Dev - Cast Shipping Total to float #4042
|
||||
* Dev - Dynamic Currency with Context API #4027
|
||||
* Dev - Remove Duplicate array entry #4049
|
||||
|
||||
= 4.1.1 - 2020-05-19 =
|
||||
|
||||
* Enhancement - Added notice about public uploads directory. #26207
|
||||
* Tweak - Disallow directory listing in woocommerce_uploads when "Redirect only" it's the selected download method. #26399
|
||||
* Fix - Added correct handling of nonces to database update notice dismissal. #26500
|
||||
* Dev - Updated WooCommerce admin version to 1.1.3 and Action Scheduler to 3.1.6.
|
||||
* Dev - Add prop `isEnabled` and a function to dynamically enable tracks. #26493
|
||||
|
||||
**WooCommerce Admin**
|
||||
* Tweak - Onboarding: Add Jetpack flow back to onboarding profiler. #4382
|
||||
* Fix - Respect tracking opt-in before new page load. #4368
|
||||
|
||||
**ActionScheduler**
|
||||
* Fix - Shutdown deprecated notice changed to a warning when as_* functions called without data store initialization. #546
|
||||
|
||||
= 4.1.0 - 2020-05-05 =
|
||||
|
||||
**WooCommerce**
|
||||
|
@ -8,6 +87,7 @@
|
|||
* Enhancement - Added option to ignore discounts from cart's total amount to enable free shipping. #24776
|
||||
* Enhancement - Changed show password icon color to a darker grey hue. #25625
|
||||
* Enhancement - Use new Setup Wizard for all users. #26016
|
||||
* Security - Fixed unescaped meta data while duplicating products. Reported by Slavco.
|
||||
* Tweak - Show notice for WP min version to WP 5.2. #26094
|
||||
* Tweak - Improve the string for untested WooCommerce extensions in the system status page to avoid confusion. #25904
|
||||
* Tweak - Updated KZT (₸) symbol. #25609
|
||||
|
|
|
@ -1 +1 @@
|
|||
FROM wordpress:5.4.1
|
||||
FROM wordpress:5.4.2
|
||||
|
|
69
Gruntfile.js
69
Gruntfile.js
|
@ -198,56 +198,6 @@ module.exports = function( grunt ) {
|
|||
}
|
||||
},
|
||||
|
||||
// Exec shell commands.
|
||||
shell: {
|
||||
options: {
|
||||
stdout: true,
|
||||
stderr: true
|
||||
},
|
||||
e2e_test: {
|
||||
command: 'npm run --silent test:single tests/e2e-tests/' + grunt.option( 'file' )
|
||||
},
|
||||
e2e_tests: {
|
||||
command: 'npm run --silent test'
|
||||
},
|
||||
e2e_tests_grep: {
|
||||
command: 'npm run --silent test:grep "' + grunt.option( 'grep' ) + '"'
|
||||
},
|
||||
contributors: {
|
||||
command: [
|
||||
'echo "Generating contributor list since <%= fromDate %>"',
|
||||
'./node_modules/.bin/githubcontrib --owner woocommerce --repo woocommerce --fromDate <%= fromDate %>' +
|
||||
' --authToken <%= authToken %> --cols 6 --sortBy contributions --format md --sortOrder desc' +
|
||||
' --showlogin true --sha <%= sha %> --filter renovate-bot > contributors.md'
|
||||
].join( '&&' )
|
||||
}
|
||||
},
|
||||
|
||||
prompt: {
|
||||
contributors: {
|
||||
options: {
|
||||
questions: [
|
||||
{
|
||||
config: 'fromDate',
|
||||
type: 'input',
|
||||
message: 'What date (YYYY-MM-DD) should we get contributions since?'
|
||||
},
|
||||
{
|
||||
config: 'sha',
|
||||
type: 'input',
|
||||
message: 'What branch should we get contributors from?'
|
||||
},
|
||||
{
|
||||
config: 'authToken',
|
||||
type: 'input',
|
||||
message: '(optional) Provide a personal access token.' +
|
||||
' This will allow 5000 requests per hour rather than 60 - use if nothing is generated.'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// PHP Code Sniffer.
|
||||
phpcs: {
|
||||
options: {
|
||||
|
@ -283,7 +233,6 @@ module.exports = function( grunt ) {
|
|||
|
||||
// Load NPM tasks to be used here.
|
||||
grunt.loadNpmTasks( 'grunt-sass' );
|
||||
grunt.loadNpmTasks( 'grunt-shell' );
|
||||
grunt.loadNpmTasks( 'grunt-phpcs' );
|
||||
grunt.loadNpmTasks( 'grunt-rtlcss' );
|
||||
grunt.loadNpmTasks( 'grunt-postcss' );
|
||||
|
@ -295,7 +244,6 @@ module.exports = function( grunt ) {
|
|||
grunt.loadNpmTasks( 'grunt-contrib-copy' );
|
||||
grunt.loadNpmTasks( 'grunt-contrib-watch' );
|
||||
grunt.loadNpmTasks( 'grunt-contrib-clean' );
|
||||
grunt.loadNpmTasks( 'grunt-prompt' );
|
||||
|
||||
// Register tasks.
|
||||
grunt.registerTask( 'default', [
|
||||
|
@ -329,25 +277,8 @@ module.exports = function( grunt ) {
|
|||
'css'
|
||||
]);
|
||||
|
||||
grunt.registerTask( 'contributors', [
|
||||
'prompt:contributors',
|
||||
'shell:contributors'
|
||||
]);
|
||||
|
||||
// Only an alias to 'default' task.
|
||||
grunt.registerTask( 'dev', [
|
||||
'default'
|
||||
]);
|
||||
|
||||
grunt.registerTask( 'e2e-tests', [
|
||||
'shell:e2e_tests'
|
||||
]);
|
||||
|
||||
grunt.registerTask( 'e2e-tests-grep', [
|
||||
'shell:e2e_tests_grep'
|
||||
]);
|
||||
|
||||
grunt.registerTask( 'e2e-test', [
|
||||
'shell:e2e_test'
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -9,7 +9,10 @@
|
|||
div.woocommerce-message {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border-left-color: #cc99c2 !important;
|
||||
|
||||
&.updated {
|
||||
border-left-color: #cc99c2 !important;
|
||||
}
|
||||
}
|
||||
|
||||
p.woocommerce-actions,
|
||||
|
|
|
@ -541,9 +541,12 @@
|
|||
|
||||
.woocommerce-message {
|
||||
position: relative;
|
||||
border-left-color: #cc99c2 !important;
|
||||
overflow: hidden;
|
||||
|
||||
&.updated {
|
||||
border-left-color: #cc99c2 !important;
|
||||
}
|
||||
|
||||
a.skip,
|
||||
a.docs {
|
||||
text-decoration: none !important;
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
// Init after gallery.
|
||||
setTimeout( function() {
|
||||
$form.trigger( 'check_variations' );
|
||||
$form.trigger( 'wc_variation_form' );
|
||||
$form.trigger( 'wc_variation_form', self );
|
||||
self.loading = false;
|
||||
}, 100 );
|
||||
};
|
||||
|
@ -160,12 +160,12 @@
|
|||
/**
|
||||
* Looks for matching variations for current selected attributes.
|
||||
*/
|
||||
VariationForm.prototype.onFindVariation = function( event ) {
|
||||
VariationForm.prototype.onFindVariation = function( event, chosenAttributes ) {
|
||||
var form = event.data.variationForm,
|
||||
attributes = form.getChosenAttributes(),
|
||||
attributes = 'undefined' !== typeof chosenAttributes ? chosenAttributes : form.getChosenAttributes(),
|
||||
currentAttributes = attributes.data;
|
||||
|
||||
if ( attributes.count === attributes.chosenCount ) {
|
||||
if ( attributes.count && attributes.count === attributes.chosenCount ) {
|
||||
if ( form.useAjax ) {
|
||||
if ( form.xhr ) {
|
||||
form.xhr.abort();
|
||||
|
|
|
@ -105,7 +105,7 @@ jQuery( function( $ ) {
|
|||
}
|
||||
|
||||
$( '.woocommerce-cart-form' ).replaceWith( $new_form );
|
||||
$( '.woocommerce-cart-form' ).find( ':input[name="update_cart"]' ).prop( 'disabled', true );
|
||||
$( '.woocommerce-cart-form' ).find( ':input[name="update_cart"]' ).prop( 'disabled', true ).attr( 'aria-disabled', true );
|
||||
|
||||
if ( $notices.length > 0 ) {
|
||||
show_notice( $notices );
|
||||
|
@ -304,14 +304,14 @@ jQuery( function( $ ) {
|
|||
'.woocommerce-cart-form .cart_item :input',
|
||||
this.input_changed );
|
||||
|
||||
$( '.woocommerce-cart-form :input[name="update_cart"]' ).prop( 'disabled', true );
|
||||
$( '.woocommerce-cart-form :input[name="update_cart"]' ).prop( 'disabled', true ).attr( 'aria-disabled', true );
|
||||
},
|
||||
|
||||
/**
|
||||
* After an input is changed, enable the update cart button.
|
||||
*/
|
||||
input_changed: function() {
|
||||
$( '.woocommerce-cart-form :input[name="update_cart"]' ).prop( 'disabled', false );
|
||||
$( '.woocommerce-cart-form :input[name="update_cart"]' ).prop( 'disabled', false ).attr( 'aria-disabled', false );
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -9,7 +9,7 @@ jQuery( function( $ ) {
|
|||
$( 'body' )
|
||||
// Tabs
|
||||
.on( 'init', '.wc-tabs-wrapper, .woocommerce-tabs', function() {
|
||||
$( '.wc-tab, .woocommerce-tabs .panel:not(.panel .panel)' ).hide();
|
||||
$( this ).find( '.wc-tab, .woocommerce-tabs .panel:not(.panel .panel)' ).hide();
|
||||
|
||||
var hash = window.location.hash;
|
||||
var url = window.location.href;
|
||||
|
|
|
@ -81,15 +81,17 @@ jQuery( function( $ ) {
|
|||
|
||||
// Show password visiblity hover icon on woocommerce forms
|
||||
$( '.woocommerce form .woocommerce-Input[type="password"]' ).wrap( '<span class="password-input"></span>' );
|
||||
$( '.password-input' ).prepend( '<span class="show-password-input"></span>' );
|
||||
// Add 'password-input' class to the password wrapper in checkout page.
|
||||
$( '.woocommerce form input' ).filter(':password').parent('span').addClass('password-input');
|
||||
$( '.password-input' ).append( '<span class="show-password-input"></span>' );
|
||||
|
||||
$( '.show-password-input' ).click(
|
||||
function() {
|
||||
$( this ).toggleClass( 'display-password' );
|
||||
if ( $( this ).hasClass( 'display-password' ) ) {
|
||||
$( this ).siblings( ['input[name^="password"]', 'input[type="password"]'] ).prop( 'type', 'text' );
|
||||
$( this ).siblings( ['input[type="password"]'] ).prop( 'type', 'text' );
|
||||
} else {
|
||||
$( this ).siblings( 'input[name^="password"]' ).prop( 'type', 'password' );
|
||||
$( this ).siblings( 'input[type="text"]' ).prop( 'type', 'password' );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -755,6 +755,12 @@ S2.define('select2/utils',[
|
|||
});
|
||||
};
|
||||
|
||||
Utils.entityDecode = function(html) {
|
||||
var txt = document.createElement("textarea");
|
||||
txt.innerHTML = html;
|
||||
return txt.value;
|
||||
}
|
||||
|
||||
// Append an array of jQuery nodes to a given element.
|
||||
Utils.appendMany = function ($element, $nodes) {
|
||||
// jQuery 1.7.x does not support $.fn.append() with an array
|
||||
|
@ -1611,9 +1617,9 @@ S2.define('select2/selection/single',[
|
|||
var selection = data[0];
|
||||
|
||||
var $rendered = this.$selection.find('.select2-selection__rendered');
|
||||
var formatted = this.display(selection, $rendered);
|
||||
var formatted = Utils.entityDecode(this.display(selection, $rendered));
|
||||
|
||||
$rendered.empty().append(formatted);
|
||||
$rendered.empty().text(formatted);
|
||||
$rendered.prop('title', selection.title || selection.text);
|
||||
};
|
||||
|
||||
|
@ -1742,12 +1748,14 @@ S2.define('select2/selection/multiple',[
|
|||
var selection = data[d];
|
||||
|
||||
var $selection = this.selectionContainer();
|
||||
var removeItemTag = $selection.html();
|
||||
var formatted = this.display(selection, $selection);
|
||||
if ('string' === typeof formatted) {
|
||||
formatted = formatted.trim();
|
||||
formatted = Utils.entityDecode(formatted.trim());
|
||||
}
|
||||
|
||||
$selection.append(formatted);
|
||||
$selection.text(formatted);
|
||||
$selection.prepend(removeItemTag);
|
||||
$selection.prop('title', selection.title || selection.text);
|
||||
|
||||
$selection.data('data', selection);
|
||||
|
@ -1786,7 +1794,7 @@ S2.define('select2/selection/placeholder',[
|
|||
Placeholder.prototype.createPlaceholder = function (decorated, placeholder) {
|
||||
var $placeholder = this.selectionContainer();
|
||||
|
||||
$placeholder.html(this.display(placeholder));
|
||||
$placeholder.text(Utils.entityDecode(this.display(placeholder)));
|
||||
$placeholder.addClass('select2-selection__placeholder')
|
||||
.removeClass('select2-selection__choice');
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -755,6 +755,12 @@ S2.define('select2/utils',[
|
|||
});
|
||||
};
|
||||
|
||||
Utils.entityDecode = function(html) {
|
||||
var txt = document.createElement("textarea");
|
||||
txt.innerHTML = html;
|
||||
return txt.value;
|
||||
}
|
||||
|
||||
// Append an array of jQuery nodes to a given element.
|
||||
Utils.appendMany = function ($element, $nodes) {
|
||||
// jQuery 1.7.x does not support $.fn.append() with an array
|
||||
|
@ -1611,9 +1617,9 @@ S2.define('select2/selection/single',[
|
|||
var selection = data[0];
|
||||
|
||||
var $rendered = this.$selection.find('.select2-selection__rendered');
|
||||
var formatted = this.display(selection, $rendered);
|
||||
var formatted = Utils.entityDecode(this.display(selection, $rendered));
|
||||
|
||||
$rendered.empty().append(formatted);
|
||||
$rendered.empty().text(formatted);
|
||||
$rendered.prop('title', selection.title || selection.text);
|
||||
};
|
||||
|
||||
|
@ -1742,12 +1748,14 @@ S2.define('select2/selection/multiple',[
|
|||
var selection = data[d];
|
||||
|
||||
var $selection = this.selectionContainer();
|
||||
var removeItemTag = $selection.html();
|
||||
var formatted = this.display(selection, $selection);
|
||||
if ('string' === typeof formatted) {
|
||||
formatted = formatted.trim();
|
||||
formatted = Utils.entityDecode(formatted.trim());
|
||||
}
|
||||
|
||||
$selection.append(formatted);
|
||||
$selection.text(formatted);
|
||||
$selection.prepend(removeItemTag);
|
||||
$selection.prop('title', selection.title || selection.text);
|
||||
|
||||
$selection.data('data', selection);
|
||||
|
@ -1786,7 +1794,7 @@ S2.define('select2/selection/placeholder',[
|
|||
Placeholder.prototype.createPlaceholder = function (decorated, placeholder) {
|
||||
var $placeholder = this.selectionContainer();
|
||||
|
||||
$placeholder.html(this.display(placeholder));
|
||||
$placeholder.text(Utils.entityDecode(this.display(placeholder)));
|
||||
$placeholder.addClass('select2-selection__placeholder')
|
||||
.removeClass('select2-selection__choice');
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,32 @@
|
|||
#!/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..."
|
||||
npm install
|
||||
composer install || exit "$?"
|
||||
echo "Running JS Build..."
|
||||
npm 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 "Restoring PHP dependencies..."
|
||||
composer install || exit "$?"
|
||||
npm run build || exit "$?"
|
||||
|
||||
echo "Generating zip file..."
|
||||
cd "$BUILD_PATH" || exit
|
||||
zip -q -r "${PLUGIN_SLUG}.zip" "$PLUGIN_SLUG/"
|
||||
echo "$BUILD_PATH/${PLUGIN_SLUG}.zip file generated!"
|
||||
|
||||
echo "Build done!"
|
|
@ -0,0 +1,32 @@
|
|||
#!/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]"
|
||||
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 "<h2>WooCommerce core</h2>" > $output_file
|
||||
echo "Generating contributor list for WC core since $from_date"
|
||||
./node_modules/.bin/githubcontrib --repo woocommerce $common_arguments >> $output_file
|
||||
|
||||
echo "<h2>WooCommerce Admin</h2>" >> $output_file
|
||||
echo "Generating contributor list for WC Admin since $from_date"
|
||||
./node_modules/.bin/githubcontrib --repo woocommerce-admin $common_arguments >> $output_file
|
||||
|
||||
echo "<h2>WooCommerce Blocks</h2>" >> $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 "<h2>Action Scheduler</h2>" >> $output_file
|
||||
echo "Generating contributor list for Action Scheduler since $from_date"
|
||||
./node_modules/.bin/githubcontrib --repo action-scheduler $common_arguments >> $output_file
|
||||
|
||||
echo "<h2>REST API</h2>" >> $output_file
|
||||
echo "Generating contributor list for REST API since $from_date"
|
||||
./node_modules/.bin/githubcontrib --repo woocommerce-rest-api $common_arguments >> $output_file
|
||||
|
||||
echo "Output generated to $output_file."
|
|
@ -34,6 +34,7 @@ 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" {} \;
|
||||
find ./packages/woocommerce-admin -iname '*.js' -exec sed -i.bak -e "s/'woocommerce-admin'/'woocommerce'/g" -e "s/\"woocommerce-admin\"/'woocommerce'/g" {} \;
|
||||
|
||||
# Cleanup backup files
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
#!/bin/sh
|
||||
|
||||
PROTECTED_BRANCH="master"
|
||||
REMOTE_REF=$(echo "$HUSKY_GIT_STDIN" | cut -d " " -f 3)
|
||||
|
||||
if [ -n "$REMOTE_REF" ]; then
|
||||
if [ -z "${REMOTE_REF##*$PROTECTED_BRANCH*}" ]; then
|
||||
printf "%sYou're about to push to master, is that what you intended? [y/N]: %s" "$(tput setaf 3)" "$(tput sgr0)"
|
||||
read -r PROCEED < /dev/tty
|
||||
echo
|
||||
|
||||
if [ "$(echo "${PROCEED:-n}" | tr "[:upper:]" "[:lower:]")" = "y" ]; then
|
||||
echo "$(tput setaf 2)Brace yourself! Pushing to the master branch...$(tput sgr0)"
|
||||
echo
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "$(tput setaf 2)Push to master cancelled!$(tput sgr0)"
|
||||
echo
|
||||
exit 1
|
||||
fi
|
||||
fi
|
|
@ -8,15 +8,15 @@
|
|||
"minimum-stability": "dev",
|
||||
"require": {
|
||||
"php": ">=7.0",
|
||||
"automattic/jetpack-autoloader": "^1.6.0",
|
||||
"automattic/jetpack-autoloader": "^1.7.0",
|
||||
"automattic/jetpack-constants": "^1.1",
|
||||
"composer/installers": "1.7.0",
|
||||
"maxmind-db/reader": "1.6.0",
|
||||
"pelago/emogrifier": "^3.1",
|
||||
"woocommerce/action-scheduler": "3.1.6",
|
||||
"woocommerce/woocommerce-admin": "1.2.0",
|
||||
"woocommerce/woocommerce-blocks": "2.5.16",
|
||||
"woocommerce/woocommerce-rest-api": "1.0.8"
|
||||
"woocommerce/woocommerce-admin": "1.3.0-rc.1",
|
||||
"woocommerce/woocommerce-blocks": "2.7.1",
|
||||
"woocommerce/woocommerce-rest-api": "1.0.10"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "7.5.20",
|
||||
|
@ -45,7 +45,8 @@
|
|||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Automattic\\WooCommerce\\Tests\\": "tests/php/"
|
||||
"Automattic\\WooCommerce\\Tests\\": "tests/php/src",
|
||||
"Automattic\\WooCommerce\\Testing\\Tools\\": "tests/Tools"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
@ -68,7 +69,7 @@
|
|||
"phpcbf -p"
|
||||
],
|
||||
"makepot-audit": [
|
||||
"wp i18n make-pot . --exclude=\".github,.wordpress-org,bin,sample-data,node_modules,tests\" --slug=woocommerce"
|
||||
"wp --allow-root i18n make-pot . --exclude=\".github,.wordpress-org,bin,sample-data,node_modules,tests\" --slug=woocommerce"
|
||||
],
|
||||
"makepot": [
|
||||
"@makepot-audit --skip-audit"
|
||||
|
@ -85,8 +86,8 @@
|
|||
"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/langauges/woocommerce.pot file and run audit",
|
||||
"makepot": "Generate i18n/langauges/woocommerce.pot file"
|
||||
"makepot-audit": "Generate i18n/languages/woocommerce.pot file and run audit",
|
||||
"makepot": "Generate i18n/languages/woocommerce.pot file"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "9548fdb91087ffaa8f14374698bf402e",
|
||||
"content-hash": "c35209de8f965f88aa5121e3ba76fc65",
|
||||
"packages": [
|
||||
{
|
||||
"name": "automattic/jetpack-autoloader",
|
||||
|
@ -331,7 +331,7 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/css-selector",
|
||||
"version": "v3.4.40",
|
||||
"version": "v3.4.42",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/css-selector.git",
|
||||
|
@ -419,16 +419,16 @@
|
|||
},
|
||||
{
|
||||
"name": "woocommerce/woocommerce-admin",
|
||||
"version": "v1.2.0",
|
||||
"version": "v1.3.0-rc.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/woocommerce/woocommerce-admin.git",
|
||||
"reference": "aa5062218a399843e68160e1b4b884320a7530ae"
|
||||
"reference": "12bc8bf522298a099bb725990cd50bae944e667f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/aa5062218a399843e68160e1b4b884320a7530ae",
|
||||
"reference": "aa5062218a399843e68160e1b4b884320a7530ae",
|
||||
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/12bc8bf522298a099bb725990cd50bae944e667f",
|
||||
"reference": "12bc8bf522298a099bb725990cd50bae944e667f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -462,20 +462,20 @@
|
|||
],
|
||||
"description": "A modern, javascript-driven WooCommerce Admin experience.",
|
||||
"homepage": "https://github.com/woocommerce/woocommerce-admin",
|
||||
"time": "2020-05-08T22:39:16+00:00"
|
||||
"time": "2020-06-23T02:57:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "woocommerce/woocommerce-blocks",
|
||||
"version": "v2.5.16",
|
||||
"version": "v2.7.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/woocommerce/woocommerce-gutenberg-products-block.git",
|
||||
"reference": "3bd91b669247000fd3f5277954701d0b148d3f1a"
|
||||
"reference": "0025c5cda83892c6f566fffd05197006f230d16c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/woocommerce/woocommerce-gutenberg-products-block/zipball/3bd91b669247000fd3f5277954701d0b148d3f1a",
|
||||
"reference": "3bd91b669247000fd3f5277954701d0b148d3f1a",
|
||||
"url": "https://api.github.com/repos/woocommerce/woocommerce-gutenberg-products-block/zipball/0025c5cda83892c6f566fffd05197006f230d16c",
|
||||
"reference": "0025c5cda83892c6f566fffd05197006f230d16c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -509,20 +509,20 @@
|
|||
"gutenberg",
|
||||
"woocommerce"
|
||||
],
|
||||
"time": "2020-04-07T11:47:19+00:00"
|
||||
"time": "2020-06-16T13:34:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "woocommerce/woocommerce-rest-api",
|
||||
"version": "1.0.8",
|
||||
"version": "1.0.10",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/woocommerce/woocommerce-rest-api.git",
|
||||
"reference": "0756027c669bb5749554ee58b9416cbdcceaa752"
|
||||
"reference": "fdcb116b4f5b699b942c01b46fd863c7da8b4b7c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/woocommerce/woocommerce-rest-api/zipball/0756027c669bb5749554ee58b9416cbdcceaa752",
|
||||
"reference": "0756027c669bb5749554ee58b9416cbdcceaa752",
|
||||
"url": "https://api.github.com/repos/woocommerce/woocommerce-rest-api/zipball/fdcb116b4f5b699b942c01b46fd863c7da8b4b7c",
|
||||
"reference": "fdcb116b4f5b699b942c01b46fd863c7da8b4b7c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -549,7 +549,7 @@
|
|||
],
|
||||
"description": "The WooCommerce core REST API.",
|
||||
"homepage": "https://github.com/woocommerce/woocommerce-rest-api",
|
||||
"time": "2020-05-11T14:54:30+00:00"
|
||||
"time": "2020-06-16T09:51:51+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
|
@ -621,20 +621,20 @@
|
|||
},
|
||||
{
|
||||
"name": "doctrine/instantiator",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/instantiator.git",
|
||||
"reference": "ae466f726242e637cebdd526a7d991b9433bacf1"
|
||||
"reference": "f350df0268e904597e3bd9c4685c53e0e333feea"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/ae466f726242e637cebdd526a7d991b9433bacf1",
|
||||
"reference": "ae466f726242e637cebdd526a7d991b9433bacf1",
|
||||
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea",
|
||||
"reference": "f350df0268e904597e3bd9c4685c53e0e333feea",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1"
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^6.0",
|
||||
|
@ -673,7 +673,7 @@
|
|||
"constructor",
|
||||
"instantiate"
|
||||
],
|
||||
"time": "2019-10-21T16:45:58+00:00"
|
||||
"time": "2020-05-29T17:27:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "gettext/gettext",
|
||||
|
@ -800,16 +800,16 @@
|
|||
},
|
||||
{
|
||||
"name": "mck89/peast",
|
||||
"version": "v1.10.3",
|
||||
"version": "v1.10.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mck89/peast.git",
|
||||
"reference": "6d1100f39f684c9e004f808b27f6c824b083d8d8"
|
||||
"reference": "e11664ef53ba2a4ca1d16d8bc73fcc317cd65d3d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/mck89/peast/zipball/6d1100f39f684c9e004f808b27f6c824b083d8d8",
|
||||
"reference": "6d1100f39f684c9e004f808b27f6c824b083d8d8",
|
||||
"url": "https://api.github.com/repos/mck89/peast/zipball/e11664ef53ba2a4ca1d16d8bc73fcc317cd65d3d",
|
||||
"reference": "e11664ef53ba2a4ca1d16d8bc73fcc317cd65d3d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -821,7 +821,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.10.3-dev"
|
||||
"dev-master": "1.10.4-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -841,7 +841,7 @@
|
|||
}
|
||||
],
|
||||
"description": "Peast is PHP library that generates AST for JavaScript code",
|
||||
"time": "2020-04-03T09:06:20+00:00"
|
||||
"time": "2020-06-21T17:16:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mustache/mustache",
|
||||
|
@ -2414,7 +2414,7 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v3.4.40",
|
||||
"version": "v3.4.42",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
|
@ -2463,16 +2463,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.16.0",
|
||||
"version": "v1.17.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "1aab00e39cebaef4d8652497f46c15c1b7e45294"
|
||||
"reference": "2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1aab00e39cebaef4d8652497f46c15c1b7e45294",
|
||||
"reference": "1aab00e39cebaef4d8652497f46c15c1b7e45294",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d",
|
||||
"reference": "2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2484,7 +2484,11 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.16-dev"
|
||||
"dev-master": "1.17-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -2517,7 +2521,7 @@
|
|||
"polyfill",
|
||||
"portable"
|
||||
],
|
||||
"time": "2020-05-08T16:50:20+00:00"
|
||||
"time": "2020-06-06T08:46:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "theseer/tokenizer",
|
||||
|
@ -2561,16 +2565,16 @@
|
|||
},
|
||||
{
|
||||
"name": "webmozart/assert",
|
||||
"version": "1.8.0",
|
||||
"version": "1.9.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/webmozart/assert.git",
|
||||
"reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6"
|
||||
"reference": "9dc4f203e36f2b486149058bade43c851dd97451"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/webmozart/assert/zipball/ab2cb0b3b559010b75981b1bdce728da3ee90ad6",
|
||||
"reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6",
|
||||
"url": "https://api.github.com/repos/webmozart/assert/zipball/9dc4f203e36f2b486149058bade43c851dd97451",
|
||||
"reference": "9dc4f203e36f2b486149058bade43c851dd97451",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2578,6 +2582,7 @@
|
|||
"symfony/polyfill-ctype": "^1.8"
|
||||
},
|
||||
"conflict": {
|
||||
"phpstan/phpstan": "<0.12.20",
|
||||
"vimeo/psalm": "<3.9.1"
|
||||
},
|
||||
"require-dev": {
|
||||
|
@ -2605,7 +2610,7 @@
|
|||
"check",
|
||||
"validate"
|
||||
],
|
||||
"time": "2020-04-18T12:12:48+00:00"
|
||||
"time": "2020-06-16T10:16:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "woocommerce/woocommerce-sniffs",
|
||||
|
@ -2649,16 +2654,16 @@
|
|||
},
|
||||
{
|
||||
"name": "wp-cli/i18n-command",
|
||||
"version": "v2.2.2",
|
||||
"version": "v2.2.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/wp-cli/i18n-command.git",
|
||||
"reference": "2804c5246d9338da59951737b03c54d257be8e47"
|
||||
"reference": "7a5d483d872dfec1b89d88d348666ecd59454d52"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/wp-cli/i18n-command/zipball/2804c5246d9338da59951737b03c54d257be8e47",
|
||||
"reference": "2804c5246d9338da59951737b03c54d257be8e47",
|
||||
"url": "https://api.github.com/repos/wp-cli/i18n-command/zipball/7a5d483d872dfec1b89d88d348666ecd59454d52",
|
||||
"reference": "7a5d483d872dfec1b89d88d348666ecd59454d52",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2702,7 +2707,7 @@
|
|||
],
|
||||
"description": "Provides internationalization tools for WordPress projects.",
|
||||
"homepage": "https://github.com/wp-cli/i18n-command",
|
||||
"time": "2019-12-13T09:00:43+00:00"
|
||||
"time": "2020-06-04T07:07:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "wp-cli/mustangostang-spyc",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
version: '3.7'
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
|
||||
|
|
|
@ -352,7 +352,6 @@ return array(
|
|||
'GH' => array( // Ghanaian Regions.
|
||||
'AF' => __( 'Ahafo', 'woocommerce' ),
|
||||
'AH' => __( 'Ashanti', 'woocommerce' ),
|
||||
'AV' => __( 'Avannaata Kommunia', 'woocommerce' ),
|
||||
'BA' => __( 'Brong-Ahafo', 'woocommerce' ),
|
||||
'BO' => __( 'Bono', 'woocommerce' ),
|
||||
'BE' => __( 'Bono East', 'woocommerce' ),
|
||||
|
@ -716,6 +715,55 @@ return array(
|
|||
'JP46' => __( 'Kagoshima', 'woocommerce' ),
|
||||
'JP47' => __( 'Okinawa', 'woocommerce' ),
|
||||
),
|
||||
'KE' => array( // Kenya 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(
|
||||
|
|
|
@ -437,12 +437,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
|
|||
* @return float
|
||||
*/
|
||||
public function get_subtotal() {
|
||||
$subtotal = 0;
|
||||
|
||||
foreach ( $this->get_items() as $item ) {
|
||||
$subtotal += wc_remove_number_precision( self::round_item_subtotal( wc_add_number_precision( $item->get_subtotal() ) ) );
|
||||
}
|
||||
|
||||
$subtotal = round( $this->get_cart_subtotal_for_order(), wc_get_price_decimals() );
|
||||
return apply_filters( 'woocommerce_order_get_subtotal', (float) $subtotal, $this );
|
||||
}
|
||||
|
||||
|
@ -1672,18 +1667,13 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
|
|||
public function calculate_totals( $and_taxes = true ) {
|
||||
do_action( 'woocommerce_order_before_calculate_totals', $and_taxes, $this );
|
||||
|
||||
$cart_subtotal = 0;
|
||||
$cart_total = 0;
|
||||
$fees_total = 0;
|
||||
$shipping_total = 0;
|
||||
$cart_subtotal_tax = 0;
|
||||
$cart_total_tax = 0;
|
||||
|
||||
// Sum line item costs without rounding.
|
||||
foreach ( $this->get_items() as $item ) {
|
||||
$cart_subtotal += $item->get_subtotal();
|
||||
$cart_total += $item->get_total();
|
||||
}
|
||||
$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 ) {
|
||||
|
|
|
@ -708,7 +708,7 @@ abstract class WC_Settings_API {
|
|||
<legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend>
|
||||
<select class="select <?php echo esc_attr( $data['class'] ); ?>" name="<?php echo esc_attr( $field_key ); ?>" id="<?php echo esc_attr( $field_key ); ?>" style="<?php echo esc_attr( $data['css'] ); ?>" <?php disabled( $data['disabled'], true ); ?> <?php echo $this->get_custom_attribute_html( $data ); // WPCS: XSS ok. ?>>
|
||||
<?php foreach ( (array) $data['options'] as $option_key => $option_value ) : ?>
|
||||
<option value="<?php echo esc_attr( $option_key ); ?>" <?php selected( (string) $option_key, esc_attr( $this->get_option( $key ) ) ); ?>><?php echo esc_attr( $option_value ); ?></option>
|
||||
<option value="<?php echo esc_attr( $option_key ); ?>" <?php selected( (string) $option_key, esc_attr( $this->get_option( $key ) ) ); ?>><?php echo esc_html( $option_value ); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php echo $this->get_description_html( $data ); // WPCS: XSS ok. ?>
|
||||
|
@ -761,11 +761,11 @@ abstract class WC_Settings_API {
|
|||
<?php if ( is_array( $option_value ) ) : ?>
|
||||
<optgroup label="<?php echo esc_attr( $option_key ); ?>">
|
||||
<?php foreach ( $option_value as $option_key_inner => $option_value_inner ) : ?>
|
||||
<option value="<?php echo esc_attr( $option_key_inner ); ?>" <?php selected( in_array( (string) $option_key_inner, $value, true ), true ); ?>><?php echo esc_attr( $option_value_inner ); ?></option>
|
||||
<option value="<?php echo esc_attr( $option_key_inner ); ?>" <?php selected( in_array( (string) $option_key_inner, $value, true ), true ); ?>><?php echo esc_html( $option_value_inner ); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</optgroup>
|
||||
<?php else : ?>
|
||||
<option value="<?php echo esc_attr( $option_key ); ?>" <?php selected( in_array( (string) $option_key, $value, true ), true ); ?>><?php echo esc_attr( $option_value ); ?></option>
|
||||
<option value="<?php echo esc_attr( $option_key ); ?>" <?php selected( in_array( (string) $option_key, $value, true ), true ); ?>><?php echo esc_html( $option_value ); ?></option>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
|
|
@ -216,36 +216,61 @@ if ( ! class_exists( 'WC_Admin_Dashboard', false ) ) :
|
|||
$lowinstock_count = get_transient( $transient_name );
|
||||
|
||||
if ( false === $lowinstock_count ) {
|
||||
$lowinstock_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 stock_quantity > %d
|
||||
AND posts.post_status = 'publish'",
|
||||
$stock,
|
||||
$nostock
|
||||
)
|
||||
);
|
||||
set_transient( $transient_name, $lowinstock_count, DAY_IN_SECONDS * 30 );
|
||||
/**
|
||||
* Status widget low in stock count pre query.
|
||||
*
|
||||
* @since 4.3.0
|
||||
* @param null|string $low_in_stock_count Low in stock count, by default null.
|
||||
* @param int $stock Low stock amount.
|
||||
* @param int $nostock No stock amount
|
||||
*/
|
||||
$lowinstock_count = apply_filters( 'woocommerce_status_widget_low_in_stock_count_pre_query', null, $stock, $nostock );
|
||||
|
||||
if ( is_null( $lowinstock_count ) ) {
|
||||
$lowinstock_count = $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 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 );
|
||||
|
||||
if ( false === $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, $outofstock_count, DAY_IN_SECONDS * 30 );
|
||||
/**
|
||||
* 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 );
|
||||
}
|
||||
?>
|
||||
<li class="low-in-stock">
|
||||
|
|
|
@ -40,6 +40,7 @@ class WC_Admin_Notices {
|
|||
'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',
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -506,6 +507,21 @@ class WC_Admin_Notices {
|
|||
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.
|
||||
*
|
||||
|
@ -568,7 +584,12 @@ class WC_Admin_Notices {
|
|||
$uploads = wp_get_upload_dir();
|
||||
|
||||
// Check for the "uploads/woocommerce_uploads" directory.
|
||||
$response = wp_safe_remote_get( esc_url_raw( $uploads['baseurl'] . '/woocommerce_uploads' ) );
|
||||
$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 );
|
||||
|
||||
|
|
|
@ -869,25 +869,29 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
|
|||
* If using force or x-sendfile, this ensures the .htaccess is in place.
|
||||
*/
|
||||
public static function check_download_folder_protection() {
|
||||
$upload_dir = wp_upload_dir();
|
||||
$downloads_url = $upload_dir['basedir'] . '/woocommerce_uploads';
|
||||
$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 ( 'redirect' === $download_method ) {
|
||||
|
||||
// Redirect method - don't protect.
|
||||
if ( file_exists( $downloads_url . '/.htaccess' ) ) {
|
||||
unlink( $downloads_url . '/.htaccess' ); // @codingStandardsIgnoreLine
|
||||
}
|
||||
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
|
||||
|
||||
// Force method - protect, add rules to the htaccess file.
|
||||
if ( ! file_exists( $downloads_url . '/.htaccess' ) ) {
|
||||
$file_handle = @fopen( $downloads_url . '/.htaccess', 'w' ); // @codingStandardsIgnoreLine
|
||||
if ( $file_handle ) {
|
||||
fwrite( $file_handle, 'deny from all' ); // @codingStandardsIgnoreLine
|
||||
fclose( $file_handle ); // @codingStandardsIgnoreLine
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -339,6 +339,33 @@ class WC_Admin_Status {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
?>
|
||||
|
||||
<br>
|
||||
<strong style="color:#a00;">
|
||||
<span class="dashicons dashicons-warning"></span>
|
||||
<?php
|
||||
echo esc_html(
|
||||
sprintf(
|
||||
// translators: Comma seperated list of missing tables.
|
||||
__( 'Missing base tables: %s. Some WooCommerce functionality may not work as expected.', 'woocommerce' ),
|
||||
implode( ', ', $missing_tables )
|
||||
)
|
||||
);
|
||||
?>
|
||||
</strong>
|
||||
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the information about plugins for the system status report.
|
||||
* Used for both active and inactive plugins sections.
|
||||
|
|
|
@ -319,6 +319,7 @@ class WC_Product_CSV_Importer_Controller {
|
|||
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' ) );
|
||||
}
|
||||
|
@ -327,7 +328,7 @@ class WC_Product_CSV_Importer_Controller {
|
|||
'test_form' => false,
|
||||
'mimes' => self::get_valid_csv_filetypes(),
|
||||
);
|
||||
$import = $_FILES['import']; // WPCS: sanitization ok, input var ok.
|
||||
$import = $_FILES['import']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$upload = wp_handle_upload( $import, $overrides );
|
||||
|
||||
if ( isset( $upload['error'] ) ) {
|
||||
|
@ -577,13 +578,15 @@ class WC_Product_CSV_Importer_Controller {
|
|||
|
||||
$headers = array();
|
||||
foreach ( $raw_headers as $key => $field ) {
|
||||
$normalized_field = strtolower( $field );
|
||||
$index = $num_indexes ? $key : $field;
|
||||
$headers[ $index ] = $field;
|
||||
$headers[ $index ] = $normalized_field;
|
||||
|
||||
if ( isset( $default_columns[ $field ] ) ) {
|
||||
$headers[ $index ] = $default_columns[ $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;
|
||||
|
@ -618,7 +621,7 @@ class WC_Product_CSV_Importer_Controller {
|
|||
* @return string
|
||||
*/
|
||||
protected function sanitize_special_column_name_regex( $value ) {
|
||||
return '/' . str_replace( array( '%d', '%s' ), '(.*)', trim( quotemeta( $value ) ) ) . '/';
|
||||
return '/' . str_replace( array( '%d', '%s' ), '(.*)', trim( quotemeta( $value ) ) ) . '/i';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -12,7 +12,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
<form class="wc-progress-form-content woocommerce-importer" enctype="multipart/form-data" method="post">
|
||||
<header>
|
||||
<h2><?php esc_html_e( 'Import products from a CSV file', 'woocommerce' ); ?></h2>
|
||||
<p><?php esc_html_e( 'This tool allows you to import (or merge) product data to your store from a CSV file.', 'woocommerce' ); ?></p>
|
||||
<p><?php esc_html_e( 'This tool allows you to import (or merge) product data to your store from a CSV or TXT file.', 'woocommerce' ); ?></p>
|
||||
</header>
|
||||
<section>
|
||||
<table class="form-table woocommerce-importer-options">
|
||||
|
|
|
@ -351,7 +351,7 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
|
|||
*/
|
||||
protected function render_products_type_filter() {
|
||||
$current_product_type = isset( $_REQUEST['product_type'] ) ? wc_clean( wp_unslash( $_REQUEST['product_type'] ) ) : false; // WPCS: input var ok, sanitization ok.
|
||||
$output = '<select name="product_type" id="dropdown_product_type"><option value="">' . __( 'Filter by product type', 'woocommerce' ) . '</option>';
|
||||
$output = '<select name="product_type" id="dropdown_product_type"><option value="">' . esc_html__( 'Filter by product type', 'woocommerce' ) . '</option>';
|
||||
|
||||
foreach ( wc_get_product_types() as $value => $label ) {
|
||||
$output .= '<option value="' . esc_attr( $value ) . '" ';
|
||||
|
@ -362,11 +362,11 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
|
|||
|
||||
$output .= '<option value="downloadable" ';
|
||||
$output .= selected( 'downloadable', $current_product_type, false );
|
||||
$output .= '> ' . ( is_rtl() ? '←' : '→' ) . ' ' . __( 'Downloadable', 'woocommerce' ) . '</option>';
|
||||
$output .= '> ' . ( is_rtl() ? '←' : '→' ) . ' ' . esc_html__( 'Downloadable', 'woocommerce' ) . '</option>';
|
||||
|
||||
$output .= '<option value="virtual" ';
|
||||
$output .= selected( 'virtual', $current_product_type, false );
|
||||
$output .= '> ' . ( is_rtl() ? '←' : '→' ) . ' ' . __( 'Virtual', 'woocommerce' ) . '</option>';
|
||||
$output .= '> ' . ( is_rtl() ? '←' : '→' ) . ' ' . esc_html__( 'Virtual', 'woocommerce' ) . '</option>';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -194,7 +194,7 @@ class WC_Meta_Box_Coupon_Data {
|
|||
foreach ( $product_ids as $product_id ) {
|
||||
$product = wc_get_product( $product_id );
|
||||
if ( is_object( $product ) ) {
|
||||
echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . wp_kses_post( $product->get_formatted_name() ) . '</option>';
|
||||
echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . htmlspecialchars( wp_kses_post( $product->get_formatted_name() ) ) . '</option>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
@ -212,7 +212,7 @@ class WC_Meta_Box_Coupon_Data {
|
|||
foreach ( $product_ids as $product_id ) {
|
||||
$product = wc_get_product( $product_id );
|
||||
if ( is_object( $product ) ) {
|
||||
echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . wp_kses_post( $product->get_formatted_name() ) . '</option>';
|
||||
echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . htmlspecialchars( wp_kses_post( $product->get_formatted_name() ) ) . '</option>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
|
|
@ -404,9 +404,9 @@ class WC_Meta_Box_Order_Data {
|
|||
}
|
||||
|
||||
if ( ! $found_method && ! empty( $payment_method ) ) {
|
||||
echo '<option value="' . esc_attr( $payment_method ) . '" selected="selected">' . __( 'Other', 'woocommerce' ) . '</option>';
|
||||
echo '<option value="' . esc_attr( $payment_method ) . '" selected="selected">' . esc_html__( 'Other', 'woocommerce' ) . '</option>';
|
||||
} else {
|
||||
echo '<option value="other">' . __( 'Other', 'woocommerce' ) . '</option>';
|
||||
echo '<option value="other">' . esc_html__( 'Other', 'woocommerce' ) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
|
|
@ -244,7 +244,7 @@ class WC_Meta_Box_Product_Data {
|
|||
continue;
|
||||
}
|
||||
$attribute_id = 0;
|
||||
$attribute_name = wc_clean( $attribute_names[ $i ] );
|
||||
$attribute_name = wc_clean( esc_html( $attribute_names[ $i ] ) );
|
||||
|
||||
if ( 'pa_' === substr( $attribute_name, 0, 3 ) ) {
|
||||
$attribute_id = wc_attribute_taxonomy_id_by_name( $attribute_name );
|
||||
|
@ -257,7 +257,7 @@ class WC_Meta_Box_Product_Data {
|
|||
$options = wp_parse_id_list( $options );
|
||||
} else {
|
||||
// Terms or text sent in textarea.
|
||||
$options = 0 < $attribute_id ? wc_sanitize_textarea( wc_sanitize_term_text_based( $options ) ) : wc_sanitize_textarea( $options );
|
||||
$options = 0 < $attribute_id ? wc_sanitize_textarea( esc_html( wc_sanitize_term_text_based( $options ) ) ) : wc_sanitize_textarea( esc_html( $options ) );
|
||||
$options = wc_get_text_attributes( $options );
|
||||
}
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
foreach ( $all_terms as $term ) {
|
||||
$options = $attribute->get_options();
|
||||
$options = ! empty( $options ) ? $options : array();
|
||||
echo '<option value="' . esc_attr( $term->term_id ) . '"' . wc_selected( $term->term_id, $options ) . '>' . esc_attr( apply_filters( 'woocommerce_product_attribute_term_name', $term->name, $term ) ) . '</option>';
|
||||
echo '<option value="' . esc_attr( $term->term_id ) . '"' . wc_selected( $term->term_id, $options ) . '>' . esc_html( apply_filters( 'woocommerce_product_attribute_term_name', $term->name, $term ) ) . '</option>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
|
|
@ -19,7 +19,7 @@ defined( 'ABSPATH' ) || exit;
|
|||
foreach ( $product_ids as $product_id ) {
|
||||
$product = wc_get_product( $product_id );
|
||||
if ( is_object( $product ) ) {
|
||||
echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . wp_kses_post( $product->get_formatted_name() ) . '</option>';
|
||||
echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . htmlspecialchars( wp_kses_post( $product->get_formatted_name() ) ) . '</option>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
@ -37,7 +37,7 @@ defined( 'ABSPATH' ) || exit;
|
|||
foreach ( $product_ids as $product_id ) {
|
||||
$product = wc_get_product( $product_id );
|
||||
if ( is_object( $product ) ) {
|
||||
echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . wp_kses_post( $product->get_formatted_name() ) . '</option>';
|
||||
echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . htmlspecialchars( wp_kses_post( $product->get_formatted_name() ) ) . '</option>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
@ -53,7 +53,7 @@ defined( 'ABSPATH' ) || exit;
|
|||
foreach ( $product_ids as $product_id ) {
|
||||
$product = wc_get_product( $product_id );
|
||||
if ( is_object( $product ) ) {
|
||||
echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . wp_kses_post( $product->get_formatted_name() ) . '</option>';
|
||||
echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . htmlspecialchars( wp_kses_post( $product->get_formatted_name() ) ) . '</option>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
|
|
@ -48,12 +48,22 @@ class WC_Notes_Run_Db_Update {
|
|||
} catch ( Exception $e ) {
|
||||
return;
|
||||
}
|
||||
$note_ids = $data_store->get_notes_with_name( self::NOTE_NAME );
|
||||
$note_ids = $data_store->get_notes_with_name( self::NOTE_NAME );
|
||||
|
||||
if ( empty( $note_ids ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( count( $note_ids ) > 1 ) {
|
||||
// Remove weird duplicates. Leave the first one.
|
||||
$current_notice = array_shift( $note_ids );
|
||||
foreach ( $note_ids as $note_id ) {
|
||||
$note = new WC_Admin_Note( $note_id );
|
||||
$data_store->delete( $note );
|
||||
}
|
||||
return $current_notice;
|
||||
}
|
||||
|
||||
return current( $note_ids );
|
||||
}
|
||||
|
||||
|
@ -79,14 +89,15 @@ class WC_Notes_Run_Db_Update {
|
|||
* - actions are set up for the first 'Update database' notice, and
|
||||
* - URL for note's action is equal to the given URL (to check for potential nonce update).
|
||||
*
|
||||
* @param WC_Admin_Note $note Note to check.
|
||||
* @param string $update_url URL to check the note against.
|
||||
* @param WC_Admin_Note $note Note to check.
|
||||
* @param string $update_url URL to check the note against.
|
||||
* @param array( string ) $current_actions List of actions to check for.
|
||||
* @return bool
|
||||
*/
|
||||
private static function note_up_to_date( $note, $update_url ) {
|
||||
private static function note_up_to_date( $note, $update_url, $current_actions ) {
|
||||
$actions = $note->get_actions();
|
||||
if ( 2 === count( array_intersect( wp_list_pluck( $actions, 'name' ), array( 'update-db_run', 'update-db_learn-more' ) ) )
|
||||
&& in_array( $update_url, wp_list_pluck( $actions, 'query' ) ) ) {
|
||||
if ( count( $current_actions ) === count( array_intersect( wp_list_pluck( $actions, 'name' ), $current_actions ) )
|
||||
&& in_array( $update_url, wp_list_pluck( $actions, 'query' ), true ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -110,6 +121,23 @@ class WC_Notes_Run_Db_Update {
|
|||
)
|
||||
);
|
||||
|
||||
$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,
|
||||
),
|
||||
);
|
||||
|
||||
if ( $note_id ) {
|
||||
$note = new WC_Admin_Note( $note_id );
|
||||
} else {
|
||||
|
@ -117,7 +145,7 @@ class WC_Notes_Run_Db_Update {
|
|||
}
|
||||
|
||||
// Check if the note needs to be updated (e.g. expired nonce or different note type stored in the previous run).
|
||||
if ( self::note_up_to_date( $note, $update_url ) ) {
|
||||
if ( self::note_up_to_date( $note, $update_url, wp_list_pluck( $note_actions, 'name' ) ) ) {
|
||||
return $note_id;
|
||||
}
|
||||
|
||||
|
@ -128,7 +156,6 @@ class WC_Notes_Run_Db_Update {
|
|||
. sprintf( ' ' . esc_html__( 'The database update process runs in the background and may take a little while, so please be patient. Advanced users can alternatively update via %1$sWP CLI%2$s.', 'woocommerce' ), '<a href="https://github.com/woocommerce/woocommerce/wiki/Upgrading-the-database-using-WP-CLI">', '</a>' )
|
||||
);
|
||||
$note->set_type( WC_Admin_Note::E_WC_ADMIN_NOTE_UPDATE );
|
||||
$note->set_icon( 'info' );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_source( 'woocommerce-core' );
|
||||
|
@ -138,20 +165,9 @@ class WC_Notes_Run_Db_Update {
|
|||
|
||||
// Set new actions.
|
||||
$note->clear_actions();
|
||||
$note->add_action(
|
||||
'update-db_run',
|
||||
__( 'Update WooCommerce Database', 'woocommerce' ),
|
||||
$update_url,
|
||||
'unactioned',
|
||||
true
|
||||
);
|
||||
$note->add_action(
|
||||
'update-db_learn-more',
|
||||
__( 'Learn more about updates', 'woocommerce' ),
|
||||
'https://docs.woocommerce.com/document/how-to-update-woocommerce/',
|
||||
'unactioned',
|
||||
false
|
||||
);
|
||||
foreach ( $note_actions as $note_action ) {
|
||||
$note->add_action( ...array_values( $note_action ) );
|
||||
}
|
||||
|
||||
return $note->save();
|
||||
}
|
||||
|
@ -164,7 +180,7 @@ class WC_Notes_Run_Db_Update {
|
|||
* @param int $note_id Note id to update.
|
||||
*/
|
||||
private static function update_in_progress_notice( $note_id ) {
|
||||
// Same actions as in includes/admin/views/html-notice-updating.php.
|
||||
// Same actions as in includes/admin/views/html-notice-updating.php. This just redirects, performs no action, so without nonce.
|
||||
$pending_actions_url = admin_url( 'admin.php?page=wc-status&tab=action-scheduler&s=woocommerce_run_update&status=pending' );
|
||||
$cron_disabled = Constants::is_true( 'DISABLE_WP_CRON' );
|
||||
$cron_cta = $cron_disabled ? __( 'You can manually run queued updates here.', 'woocommerce' ) : __( 'View progress →', 'woocommerce' );
|
||||
|
@ -205,89 +221,81 @@ class WC_Notes_Run_Db_Update {
|
|||
)
|
||||
);
|
||||
|
||||
$note_actions = array(
|
||||
array(
|
||||
'name' => 'update-db_done',
|
||||
'label' => __( 'Thanks!', 'woocommerce' ),
|
||||
'url' => $hide_notices_url,
|
||||
'status' => 'actioned',
|
||||
'primary' => true,
|
||||
),
|
||||
);
|
||||
|
||||
$note = new WC_Admin_Note( $note_id );
|
||||
|
||||
// Check if the note needs to be updated (e.g. expired nonce or different note type stored in the previous run).
|
||||
if ( self::note_up_to_date( $note, $hide_notices_url, wp_list_pluck( $note_actions, 'name' ) ) ) {
|
||||
return $note_id;
|
||||
}
|
||||
|
||||
$note->set_title( __( 'WooCommerce database update done', 'woocommerce' ) );
|
||||
$note->set_content( __( 'WooCommerce database update complete. Thank you for updating to the latest version!', 'woocommerce' ) );
|
||||
|
||||
$actions = $note->get_actions();
|
||||
if ( ! in_array( 'update-db_done', wp_list_pluck( $actions, 'name' ) ) ) {
|
||||
$note->clear_actions();
|
||||
$note->add_action(
|
||||
'update-db_done',
|
||||
__( 'Thanks!', 'woocommerce' ),
|
||||
$hide_notices_url,
|
||||
'actioned',
|
||||
true
|
||||
);
|
||||
|
||||
$note->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if db update notice should be shown, false otherwise.
|
||||
*
|
||||
* If the db needs an update, the notice should be always shown.
|
||||
* If the db does not need an update, but the notice has *not* been actioned (i.e. after the db update, when
|
||||
* store owner hasn't acknowledged the successful db update), still show the notice.
|
||||
* If the db does not need an update, and the notice has been actioned, then notice should *not* be shown.
|
||||
* The same is true if the db does not need an update and the notice does not exist.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function should_show_notice() {
|
||||
if ( ! \WC_Install::needs_db_update() ) {
|
||||
try {
|
||||
$data_store = \WC_Data_Store::load( 'admin-note' );
|
||||
} catch ( Exception $e ) {
|
||||
// Bail out in case of incorrect use.
|
||||
return false;
|
||||
}
|
||||
$note_ids = $data_store->get_notes_with_name( self::NOTE_NAME );
|
||||
|
||||
if ( ! empty( $note_ids ) ) {
|
||||
// Db update not needed && note actioned -> don't show it.
|
||||
$note = new WC_Admin_Note( $note_ids[0] );
|
||||
if ( $note::E_WC_ADMIN_NOTE_ACTIONED === $note->get_status() ) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Db update not needed && note does not exist -> don't show it.
|
||||
return false;
|
||||
}
|
||||
$note->clear_actions();
|
||||
foreach ( $note_actions as $note_action ) {
|
||||
$note->add_action( ...array_values( $note_action ) );
|
||||
}
|
||||
|
||||
return true;
|
||||
$note->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the correct content of the db update note to be displayed by WC Admin.
|
||||
*
|
||||
* This one gets called on each page load, so try to bail quickly.
|
||||
*
|
||||
* If the db needs an update, the notice should be always shown.
|
||||
* If the db does not need an update, but the notice has *not* been actioned (i.e. after the db update, when
|
||||
* store owner hasn't acknowledged the successful db update), still show the Thanks notice.
|
||||
* If the db does not need an update, and the notice has been actioned, then notice should *not* be shown.
|
||||
* The notice should also be hidden if the db does not need an update and the notice does not exist.
|
||||
*/
|
||||
public static function show_reminder() {
|
||||
if ( ! self::should_show_notice() ) {
|
||||
return;
|
||||
}
|
||||
$needs_db_update = \WC_Install::needs_db_update();
|
||||
|
||||
$note_id = self::get_current_notice();
|
||||
if ( ! $needs_db_update ) {
|
||||
// Db update not needed && note does not exist -> don't show it.
|
||||
if ( ! $note_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( \WC_Install::needs_db_update() && empty( $note_id ) ) {
|
||||
// Db needs update && no notice exists -> create one.
|
||||
$note_id = self::update_needed_notice();
|
||||
}
|
||||
|
||||
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.
|
||||
self::update_in_progress_notice( $note_id );
|
||||
$note = new WC_Admin_Note( $note_id );
|
||||
if ( $note::E_WC_ADMIN_NOTE_ACTIONED === $note->get_status() ) {
|
||||
// Db update not needed && note actioned -> don't show it.
|
||||
return;
|
||||
} else {
|
||||
self::update_needed_notice( $note_id );
|
||||
// Db update not needed && notice is unactioned -> Thank you note.
|
||||
\WC_Install::update_db_version();
|
||||
self::update_done_notice( $note_id );
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
\WC_Install::update_db_version();
|
||||
self::update_done_notice( $note_id );
|
||||
// Db needs update &&.
|
||||
if ( ! $note_id ) {
|
||||
// Db needs update && no notice exists -> create one that shows Nudge to update.
|
||||
$note_id = self::update_needed_notice();
|
||||
}
|
||||
|
||||
$next_scheduled_date = WC()->queue()->get_next( 'woocommerce_run_update_callback', null, 'woocommerce-db-updates' );
|
||||
|
||||
if ( $next_scheduled_date || ! empty( $_GET['do_update_woocommerce'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
// Db needs update && db update is scheduled -> update note to In progress.
|
||||
self::update_in_progress_notice( $note_id );
|
||||
} else {
|
||||
// Db needs update && db update is not scheduled -> Nudge to run the db update.
|
||||
self::update_needed_notice( $note_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -45,13 +45,6 @@ class WC_Plugin_Updates {
|
|||
*/
|
||||
protected $major_untested_plugins = array();
|
||||
|
||||
/**
|
||||
* Array of plugins lacking testing with the minor version.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $minor_untested_plugins = array();
|
||||
|
||||
/**
|
||||
* Common JS for initializing and managing thickbox-based modals.
|
||||
*/
|
||||
|
@ -103,29 +96,6 @@ class WC_Plugin_Updates {
|
|||
| Methods for getting messages.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the inline warning notice for minor version updates.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_extensions_inline_warning_minor() {
|
||||
$upgrade_type = 'minor';
|
||||
$plugins = ! empty( $this->major_untested_plugins ) ? array_diff_key( $this->minor_untested_plugins, $this->major_untested_plugins ) : $this->minor_untested_plugins;
|
||||
$version_parts = explode( '.', $this->new_version );
|
||||
$new_version = $version_parts[0] . '.' . $version_parts[1];
|
||||
|
||||
if ( empty( $plugins ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* translators: %s: version number */
|
||||
$message = sprintf( __( "<strong>Heads up!</strong> The versions of the following plugins you're running haven't been tested with the latest version of WooCommerce (%s).", 'woocommerce' ), $new_version );
|
||||
|
||||
ob_start();
|
||||
include 'views/html-notice-untested-extensions-inline.php';
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inline warning notice for major version updates.
|
||||
*
|
||||
|
|
|
@ -45,7 +45,6 @@ class WC_Plugins_Screen_Updates extends WC_Plugin_Updates {
|
|||
$this->new_version = $response->new_version;
|
||||
$this->upgrade_notice = $this->get_upgrade_notice( $response->new_version );
|
||||
$this->major_untested_plugins = $this->get_untested_plugins( $response->new_version, 'major' );
|
||||
$this->minor_untested_plugins = $this->get_untested_plugins( $response->new_version, 'minor' );
|
||||
|
||||
$current_version_parts = explode( '.', Constants::get_constant( 'WC_VERSION' ) );
|
||||
$new_version_parts = explode( '.', $this->new_version );
|
||||
|
@ -59,10 +58,6 @@ class WC_Plugins_Screen_Updates extends WC_Plugin_Updates {
|
|||
$this->upgrade_notice .= $this->get_extensions_inline_warning_major();
|
||||
}
|
||||
|
||||
if ( ! empty( $this->minor_untested_plugins ) ) {
|
||||
$this->upgrade_notice .= $this->get_extensions_inline_warning_minor();
|
||||
}
|
||||
|
||||
if ( ! empty( $this->major_untested_plugins ) ) {
|
||||
$this->upgrade_notice .= $this->get_extensions_modal_warning();
|
||||
add_action( 'admin_print_footer_scripts', array( $this, 'plugin_screen_modal_js' ) );
|
||||
|
|
|
@ -32,12 +32,14 @@ class WC_Settings_Accounts extends WC_Settings_Page {
|
|||
*/
|
||||
public function get_settings() {
|
||||
$erasure_text = esc_html__( 'account erasure request', 'woocommerce' );
|
||||
$privacy_text = esc_html__( 'privacy page', 'woocommerce' );
|
||||
if ( current_user_can( 'manage_privacy_options' ) ) {
|
||||
if ( version_compare( get_bloginfo( 'version' ), '5.3', '<' ) ) {
|
||||
$erasure_text = sprintf( '<a href="%s">%s</a>', esc_url( admin_url( 'tools.php?page=remove_personal_data' ) ), $erasure_text );
|
||||
} else {
|
||||
$erasure_text = sprintf( '<a href="%s">%s</a>', esc_url( admin_url( 'erase-personal-data.php' ) ), $erasure_text );
|
||||
}
|
||||
$privacy_text = sprintf( '<a href="%s">%s</a>', esc_url( admin_url( 'options-privacy.php' ) ), $privacy_text );
|
||||
}
|
||||
|
||||
$account_settings = array(
|
||||
|
@ -136,18 +138,8 @@ class WC_Settings_Accounts extends WC_Settings_Page {
|
|||
'title' => __( 'Privacy policy', 'woocommerce' ),
|
||||
'type' => 'title',
|
||||
'id' => 'privacy_policy_options',
|
||||
'desc' => __( 'This section controls the display of your website privacy policy. The privacy notices below will not show up unless a privacy page is first set.', 'woocommerce' ),
|
||||
),
|
||||
|
||||
array(
|
||||
'title' => __( 'Privacy page', 'woocommerce' ),
|
||||
'desc' => __( 'Choose a page to act as your privacy policy.', 'woocommerce' ),
|
||||
'id' => 'wp_page_for_privacy_policy',
|
||||
'type' => 'single_select_page',
|
||||
'default' => '',
|
||||
'class' => 'wc-enhanced-select-nostd',
|
||||
'css' => 'min-width:300px;',
|
||||
'desc_tip' => true,
|
||||
/* translators: %s: privacy page link. */
|
||||
'desc' => sprintf( esc_html__( 'This section controls the display of your website privacy policy. The privacy notices below will not show up unless a %s is set.', 'woocommerce' ), $privacy_text ),
|
||||
),
|
||||
|
||||
array(
|
||||
|
|
|
@ -42,6 +42,7 @@ class WC_Settings_Advanced extends WC_Settings_Page {
|
|||
'webhooks' => __( 'Webhooks', 'woocommerce' ),
|
||||
'legacy_api' => __( 'Legacy API', 'woocommerce' ),
|
||||
'woocommerce_com' => __( 'WooCommerce.com', 'woocommerce' ),
|
||||
'features' => __( 'Features', 'woocommerce' ),
|
||||
);
|
||||
|
||||
return apply_filters( 'woocommerce_get_sections_' . $this->id, $sections );
|
||||
|
@ -397,6 +398,28 @@ class WC_Settings_Advanced extends WC_Settings_Page {
|
|||
),
|
||||
)
|
||||
);
|
||||
} elseif ( 'features' === $current_section ) {
|
||||
$settings = apply_filters(
|
||||
'woocommerce_settings_features',
|
||||
array(
|
||||
array(
|
||||
'title' => __( 'Features', 'woocommerce' ),
|
||||
'type' => 'title',
|
||||
'desc' => __( 'Start using new features that are being progressively rolled out to improve the store management experience.', 'woocommerce' ),
|
||||
'id' => 'features_options',
|
||||
),
|
||||
array(
|
||||
'title' => __( 'Home Screen', 'woocommerce' ),
|
||||
'desc' => __( 'Displays analytical insights, inbox notifications, and handy shortcuts in a single screen', 'woocommerce' ),
|
||||
'id' => 'woocommerce_homescreen_enabled',
|
||||
'type' => 'checkbox',
|
||||
),
|
||||
array(
|
||||
'type' => 'sectionend',
|
||||
'id' => 'features_options',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return apply_filters( 'woocommerce_get_settings_' . $this->id, $settings, $current_section );
|
||||
|
|
|
@ -286,7 +286,7 @@ class WC_Settings_Shipping extends WC_Settings_Page {
|
|||
* Show zones
|
||||
*/
|
||||
protected function zones_screen() {
|
||||
$method_count = wc_get_shipping_method_count();
|
||||
$method_count = wc_get_shipping_method_count( false, true );
|
||||
|
||||
wp_localize_script(
|
||||
'wc-shipping-zones',
|
||||
|
|
|
@ -189,7 +189,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
if ( ! $method->supports( 'shipping-zones' ) ) {
|
||||
continue;
|
||||
}
|
||||
echo '<option data-description="' . esc_attr( wp_kses_post( wpautop( $method->get_method_description() ) ) ) . '" value="' . esc_attr( $method->id ) . '">' . esc_attr( $method->get_method_title() ) . '</li>';
|
||||
echo '<option data-description="' . esc_attr( wp_kses_post( wpautop( $method->get_method_description() ) ) ) . '" value="' . esc_attr( $method->id ) . '">' . esc_html( $method->get_method_title() ) . '</li>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
|
|
@ -108,7 +108,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
if ( ! $method->supports( 'shipping-zones' ) ) {
|
||||
continue;
|
||||
}
|
||||
echo '<option data-description="' . esc_attr( wp_kses_post( wpautop( $method->get_method_description() ) ) ) . '" value="' . esc_attr( $method->id ) . '">' . esc_attr( $method->get_method_title() ) . '</li>';
|
||||
echo '<option data-description="' . esc_attr( wp_kses_post( wpautop( $method->get_method_description() ) ) ) . '" value="' . esc_attr( $method->id ) . '">' . esc_html( $method->get_method_title() ) . '</li>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
|
|
@ -496,10 +496,17 @@ $untested_plugins = $plugin_updates->get_untested_plugins( WC()->version, 'min
|
|||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="wc_status_table widefat" cellspacing="0">
|
||||
<table id="status-database" class="wc_status_table widefat" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="3" data-export-label="Database"><h2><?php esc_html_e( 'Database', 'woocommerce' ); ?></h2></th>
|
||||
<th colspan="3" data-export-label="Database">
|
||||
<h2>
|
||||
<?php
|
||||
esc_html_e( 'Database', 'woocommerce' );
|
||||
self::output_tables_info();
|
||||
?>
|
||||
</h2>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -792,7 +799,7 @@ if ( 0 < count( $dropins_mu_plugins['mu_plugins'] ) ) :
|
|||
</tr>
|
||||
<tr>
|
||||
<td data-export-label="Connected to WooCommerce.com"><?php esc_html_e( 'Connected to WooCommerce.com', 'woocommerce' ); ?>:</td>
|
||||
<td class="help"><?php echo wc_help_tip( esc_html__( 'Are your site connected to WooCommerce.com?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
|
||||
<td class="help"><?php echo wc_help_tip( esc_html__( 'Is your site connected to WooCommerce.com?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
|
||||
<td><?php echo 'yes' === $settings['woocommerce_com_connected'] ? '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>' : '<mark class="no">–</mark>'; ?></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
@ -29,7 +29,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
'3' => __( 'Decrease existing price by (fixed amount or %):', 'woocommerce' ),
|
||||
);
|
||||
foreach ( $options as $key => $value ) {
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . $value . '</option>';
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
@ -54,7 +54,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
'4' => __( 'Set to regular price decreased by (fixed amount or %):', 'woocommerce' ),
|
||||
);
|
||||
foreach ( $options as $key => $value ) {
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . $value . '</option>';
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
@ -78,7 +78,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
'none' => _x( 'None', 'Tax status', 'woocommerce' ),
|
||||
);
|
||||
foreach ( $options as $key => $value ) {
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . $value . '</option>';
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
@ -104,7 +104,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
}
|
||||
|
||||
foreach ( $options as $key => $value ) {
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . $value . '</option>';
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
@ -124,7 +124,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
'1' => __( 'Change to:', 'woocommerce' ),
|
||||
);
|
||||
foreach ( $options as $key => $value ) {
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . $value . '</option>';
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
@ -148,7 +148,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
'1' => __( 'Change to:', 'woocommerce' ),
|
||||
);
|
||||
foreach ( $options as $key => $value ) {
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . $value . '</option>';
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
@ -170,7 +170,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
<option value="_no_shipping_class"><?php _e( 'No shipping class', 'woocommerce' ); ?></option>
|
||||
<?php
|
||||
foreach ( $shipping_class as $key => $value ) {
|
||||
echo '<option value="' . esc_attr( $value->slug ) . '">' . $value->name . '</option>';
|
||||
echo '<option value="' . esc_attr( $value->slug ) . '">' . esc_html( $value->name ) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
@ -190,7 +190,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
'hidden' => __( 'Hidden', 'woocommerce' ),
|
||||
);
|
||||
foreach ( $options as $key => $value ) {
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . $value . '</option>';
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
@ -207,7 +207,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
'no' => __( 'No', 'woocommerce' ),
|
||||
);
|
||||
foreach ( $options as $key => $value ) {
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . $value . '</option>';
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
@ -222,7 +222,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
echo '<option value="">' . esc_html__( '— No Change —', 'woocommerce' ) . '</option>';
|
||||
|
||||
foreach ( wc_get_product_stock_status_options() as $key => $value ) {
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . $value . '</option>';
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
@ -241,7 +241,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
'no' => __( 'No', 'woocommerce' ),
|
||||
);
|
||||
foreach ( $options as $key => $value ) {
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . $value . '</option>';
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
@ -261,7 +261,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
'3' => __( 'Decrease existing stock by:', 'woocommerce' ),
|
||||
);
|
||||
foreach ( $options as $key => $value ) {
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . $value . '</option>';
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
@ -280,7 +280,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
echo '<option value="">' . esc_html__( '— No Change —', 'woocommerce' ) . '</option>';
|
||||
|
||||
foreach ( wc_get_product_backorder_options() as $key => $value ) {
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . $value . '</option>';
|
||||
echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
/**
|
||||
* Admin View: Notice - Base table missing.
|
||||
*
|
||||
* @package WooCommerce\Admin
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
?>
|
||||
<div class="updated woocommerce-message">
|
||||
<a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'base_tables_missing' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>">
|
||||
<?php esc_html_e( 'Dismiss', 'woocommerce' ); ?>
|
||||
</a>
|
||||
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Database tables missing', 'woocommerce' ); ?></strong>
|
||||
</p>
|
||||
<p>
|
||||
<?php
|
||||
$verify_db_tool_available = array_key_exists( 'verify_db_tables', WC_Admin_Status::get_tools() );
|
||||
$missing_tables = get_option( 'woocommerce_schema_missing_tables' );
|
||||
if ( $verify_db_tool_available ) {
|
||||
echo wp_kses_post(
|
||||
sprintf(
|
||||
/* translators: %1%s: Missing tables (seperated by ",") %2$s: Link to check again */
|
||||
__( 'One or more tables required for WooCommerce to function are missing, some features may not work as expected. Missing tables: %1$s. <a href="%2$s">Check again.</a>', 'woocommerce' ),
|
||||
esc_html( implode( ', ', $missing_tables ) ),
|
||||
wp_nonce_url( admin_url( 'admin.php?page=wc-status&tab=tools&action=verify_db_tables' ), 'debug_action' )
|
||||
)
|
||||
);
|
||||
} else {
|
||||
echo wp_kses_post(
|
||||
sprintf(
|
||||
/* translators: %1%s: Missing tables (seperated by ",") */
|
||||
__( 'One or more tables required for WooCommerce to function are missing, some features may not work as expected. Missing tables: %1$s.', 'woocommerce' ),
|
||||
esc_html( implode( ', ', $missing_tables ) )
|
||||
)
|
||||
);
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
</div>
|
|
@ -40,8 +40,15 @@ class WC_Cache_Helper {
|
|||
* @since 3.6.0
|
||||
*/
|
||||
public static function additional_nocache_headers( $headers ) {
|
||||
// no-transform: Opt-out of Google weblight if page is dynamic e.g. cart/checkout. https://support.google.com/webmasters/answer/6211428?hl=en.
|
||||
$headers['Cache-Control'] = 'no-transform, no-cache, no-store, must-revalidate';
|
||||
/**
|
||||
* Allow CDN plugins to disable nocache headers.
|
||||
*
|
||||
* @param bool $enable_nocache_headers Flag indicating whether to add nocache headers. Default: true.
|
||||
*/
|
||||
if ( apply_filters( 'woocommerce_enable_nocache_headers', true ) ) {
|
||||
// no-transform: Opt-out of Google weblight. https://support.google.com/webmasters/answer/6211428?hl=en.
|
||||
$headers['Cache-Control'] = 'no-transform, no-cache, no-store, must-revalidate';
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
|
||||
|
|
|
@ -765,7 +765,6 @@ class WC_Cart extends WC_Legacy_Cart {
|
|||
public function check_cart_item_stock() {
|
||||
$error = new WP_Error();
|
||||
$product_qty_in_cart = $this->get_cart_item_quantities();
|
||||
$hold_stock_minutes = (int) get_option( 'woocommerce_hold_stock_minutes', 0 );
|
||||
$current_session_order_id = isset( WC()->session->order_awaiting_payment ) ? absint( WC()->session->order_awaiting_payment ) : 0;
|
||||
|
||||
foreach ( $this->get_cart() as $cart_item_key => $values ) {
|
||||
|
@ -784,7 +783,7 @@ class WC_Cart extends WC_Legacy_Cart {
|
|||
}
|
||||
|
||||
// Check stock based on all items in the cart and consider any held stock within pending orders.
|
||||
$held_stock = ( $hold_stock_minutes > 0 ) ? wc_get_held_stock_quantity( $product, $current_session_order_id ) : 0;
|
||||
$held_stock = wc_get_held_stock_quantity( $product, $current_session_order_id );
|
||||
$required_stock = $product_qty_in_cart[ $product->get_stock_managed_by_id() ];
|
||||
|
||||
if ( $product->get_stock_quantity() < ( $held_stock + $required_stock ) ) {
|
||||
|
|
|
@ -388,12 +388,30 @@ class WC_Checkout {
|
|||
// Save the order.
|
||||
$order_id = $order->save();
|
||||
|
||||
/**
|
||||
* Action hook fired after an order is created used to add custom meta to the order.
|
||||
*
|
||||
* @since 3.0.0
|
||||
*/
|
||||
do_action( 'woocommerce_checkout_update_order_meta', $order_id, $data );
|
||||
|
||||
/**
|
||||
* Action hook fired after an order is created.
|
||||
*
|
||||
* @since 4.3.0
|
||||
*/
|
||||
do_action( 'woocommerce_checkout_order_created', $order );
|
||||
|
||||
return $order_id;
|
||||
} catch ( Exception $e ) {
|
||||
if ( $order && $order instanceof WC_Order ) {
|
||||
$order->get_data_store()->release_held_coupons( $order );
|
||||
/**
|
||||
* Action hook fired when an order is discarded due to Exception.
|
||||
*
|
||||
* @since 4.3.0
|
||||
*/
|
||||
do_action( 'woocommerce_checkout_order_exception', $order );
|
||||
}
|
||||
return new WP_Error( 'checkout-error', $e->getMessage() );
|
||||
}
|
||||
|
@ -748,14 +766,14 @@ class WC_Checkout {
|
|||
/* translators: %s: field name */
|
||||
$postcode_validation_notice = sprintf( __( '%s is not a valid postcode / ZIP.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' );
|
||||
}
|
||||
$errors->add( 'validation', apply_filters( 'woocommerce_checkout_postcode_validation_notice', $postcode_validation_notice, $country, $data[ $key ] ), array( 'id' => $key ) );
|
||||
$errors->add( $key . '_validation', apply_filters( 'woocommerce_checkout_postcode_validation_notice', $postcode_validation_notice, $country, $data[ $key ] ), array( 'id' => $key ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( in_array( 'phone', $format, true ) ) {
|
||||
if ( $validate_fieldset && '' !== $data[ $key ] && ! WC_Validation::is_phone( $data[ $key ] ) ) {
|
||||
/* translators: %s: phone number */
|
||||
$errors->add( 'validation', sprintf( __( '%s is not a valid phone number.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' ), array( 'id' => $key ) );
|
||||
$errors->add( $key . '_validation', sprintf( __( '%s is not a valid phone number.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' ), array( 'id' => $key ) );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -765,7 +783,7 @@ class WC_Checkout {
|
|||
|
||||
if ( $validate_fieldset && ! $email_is_valid ) {
|
||||
/* translators: %s: email address */
|
||||
$errors->add( 'validation', sprintf( __( '%s is not a valid email address.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' ), array( 'id' => $key ) );
|
||||
$errors->add( $key . '_validation', sprintf( __( '%s is not a valid email address.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' ), array( 'id' => $key ) );
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
@ -785,14 +803,14 @@ class WC_Checkout {
|
|||
|
||||
if ( $validate_fieldset && ! in_array( $data[ $key ], $valid_state_values, true ) ) {
|
||||
/* translators: 1: state field 2: valid states */
|
||||
$errors->add( 'validation', sprintf( __( '%1$s is not valid. Please enter one of the following: %2$s', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>', implode( ', ', $valid_states ) ), array( 'id' => $key ) );
|
||||
$errors->add( $key . '_validation', sprintf( __( '%1$s is not valid. Please enter one of the following: %2$s', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>', implode( ', ', $valid_states ) ), array( 'id' => $key ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $validate_fieldset && $required && '' === $data[ $key ] ) {
|
||||
/* translators: %s: field name */
|
||||
$errors->add( 'required-field', apply_filters( 'woocommerce_checkout_required_field_notice', sprintf( __( '%s is a required field.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' ), $field_label ), array( 'id' => $key ) );
|
||||
$errors->add( $key . '_required', apply_filters( 'woocommerce_checkout_required_field_notice', sprintf( __( '%s is a required field.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' ), $field_label ), array( 'id' => $key ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -146,7 +146,7 @@ class WC_Comments {
|
|||
*/
|
||||
public static function check_comment_rating( $comment_data ) {
|
||||
// If posting a comment (not trackback etc) and not logged in.
|
||||
if ( ! is_admin() && isset( $_POST['comment_post_ID'], $_POST['rating'], $comment_data['comment_type'] ) && 'product' === get_post_type( absint( $_POST['comment_post_ID'] ) ) && empty( $_POST['rating'] ) && '' === $comment_data['comment_type'] && wc_review_ratings_enabled() && wc_review_ratings_required() ) { // WPCS: input var ok, CSRF ok.
|
||||
if ( ! is_admin() && isset( $_POST['comment_post_ID'], $_POST['rating'], $comment_data['comment_type'] ) && 'product' === get_post_type( absint( $_POST['comment_post_ID'] ) ) && empty( $_POST['rating'] ) && self::is_default_comment_type( $comment_data['comment_type'] ) && wc_review_ratings_enabled() && wc_review_ratings_required() ) { // WPCS: input var ok, CSRF ok.
|
||||
wp_die( esc_html__( 'Please rate the product.', 'woocommerce' ) );
|
||||
exit;
|
||||
}
|
||||
|
@ -406,12 +406,26 @@ class WC_Comments {
|
|||
* @return array
|
||||
*/
|
||||
public static function update_comment_type( $comment_data ) {
|
||||
if ( ! is_admin() && isset( $_POST['comment_post_ID'], $comment_data['comment_type'] ) && '' === $comment_data['comment_type'] && 'product' === get_post_type( absint( $_POST['comment_post_ID'] ) ) ) { // WPCS: input var ok, CSRF ok.
|
||||
if ( ! is_admin() && isset( $_POST['comment_post_ID'], $comment_data['comment_type'] ) && self::is_default_comment_type( $comment_data['comment_type'] ) && 'product' === get_post_type( absint( $_POST['comment_post_ID'] ) ) ) { // WPCS: input var ok, CSRF ok.
|
||||
$comment_data['comment_type'] = 'review';
|
||||
}
|
||||
|
||||
return $comment_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a comment is of the default type.
|
||||
*
|
||||
* Prior to WordPress 5.5, '' was the default comment type.
|
||||
* As of 5.5, the default type is 'comment'.
|
||||
*
|
||||
* @since 4.3.0
|
||||
* @param string $comment_type Comment type.
|
||||
* @return bool
|
||||
*/
|
||||
private static function is_default_comment_type( $comment_type ) {
|
||||
return ( '' === $comment_type || 'comment' === $comment_type );
|
||||
}
|
||||
}
|
||||
|
||||
WC_Comments::init();
|
||||
|
|
|
@ -474,7 +474,7 @@ class WC_Countries {
|
|||
echo ' selected="selected"';
|
||||
}
|
||||
|
||||
echo '>' . esc_html( $value ) . ' — ' . ( $escape ? esc_js( $state_value ) : $state_value ) . '</option>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo '>' . esc_html( $value ) . ' — ' . ( $escape ? esc_html( $state_value ) : $state_value ) . '</option>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
|
||||
}
|
||||
echo '</optgroup>';
|
||||
|
@ -483,7 +483,7 @@ class WC_Countries {
|
|||
if ( $selected_country === $key && '*' === $selected_state ) {
|
||||
echo ' selected="selected"';
|
||||
}
|
||||
echo ' value="' . esc_attr( $key ) . '">' . ( $escape ? esc_js( $value ) : $value ) . '</option>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo ' value="' . esc_attr( $key ) . '">' . ( $escape ? esc_html( $value ) : $value ) . '</option>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -459,6 +459,10 @@ class WC_Form_Handler {
|
|||
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;
|
||||
|
@ -865,11 +869,12 @@ class WC_Form_Handler {
|
|||
*/
|
||||
private static function add_to_cart_handler_variable( $product_id ) {
|
||||
try {
|
||||
$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
|
||||
$missing_attributes = array();
|
||||
$variations = array();
|
||||
$adding_to_cart = wc_get_product( $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
|
||||
$missing_attributes = array();
|
||||
$variations = array();
|
||||
$variation_attributes = array();
|
||||
$adding_to_cart = wc_get_product( $product_id );
|
||||
|
||||
if ( ! $adding_to_cart ) {
|
||||
return false;
|
||||
|
@ -877,6 +882,9 @@ class WC_Form_Handler {
|
|||
|
||||
// If the $product_id was in fact a variation ID, update the variables.
|
||||
if ( $adding_to_cart->is_type( 'variation' ) ) {
|
||||
$variation_attributes = $adding_to_cart->get_variation_attributes();
|
||||
// Filter out 'any' variations, which are empty, as they need to be explicitly specified while adding to cart.
|
||||
$variation_attributes = array_filter( $variation_attributes );
|
||||
$variation_id = $product_id;
|
||||
$product_id = $adding_to_cart->get_parent_id();
|
||||
$adding_to_cart = wc_get_product( $product_id );
|
||||
|
@ -907,6 +915,9 @@ class WC_Form_Handler {
|
|||
}
|
||||
}
|
||||
|
||||
// Merge variation attributes and posted attributes.
|
||||
$posted_and_variation_attributes = array_merge( $variation_attributes, $posted_attributes );
|
||||
|
||||
// If no variation ID is set, attempt to get a variation ID from posted attributes.
|
||||
if ( empty( $variation_id ) ) {
|
||||
$data_store = WC_Data_Store::load( 'product' );
|
||||
|
@ -935,8 +946,8 @@ class WC_Form_Handler {
|
|||
*
|
||||
* If no attribute was posted, only error if the variation has an 'any' attribute which requires a value.
|
||||
*/
|
||||
if ( isset( $posted_attributes[ $attribute_key ] ) ) {
|
||||
$value = $posted_attributes[ $attribute_key ];
|
||||
if ( isset( $posted_and_variation_attributes[ $attribute_key ] ) ) {
|
||||
$value = $posted_and_variation_attributes[ $attribute_key ];
|
||||
|
||||
// Allow if valid or show error.
|
||||
if ( $valid_value === $value ) {
|
||||
|
|
|
@ -287,6 +287,7 @@ class WC_Install {
|
|||
WC()->wpdb_table_fix();
|
||||
self::remove_admin_notices();
|
||||
self::create_tables();
|
||||
self::verify_base_tables();
|
||||
self::create_options();
|
||||
self::create_roles();
|
||||
self::setup_environment();
|
||||
|
@ -296,6 +297,7 @@ class WC_Install {
|
|||
self::maybe_enable_setup_wizard();
|
||||
self::update_wc_version();
|
||||
self::maybe_update_db_version();
|
||||
self::maybe_enable_homescreen();
|
||||
|
||||
delete_transient( 'wc_installing' );
|
||||
|
||||
|
@ -303,6 +305,54 @@ class WC_Install {
|
|||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the homepage should be enabled and set the appropriate option if thats the case.
|
||||
*
|
||||
* @since 4.3.0
|
||||
*/
|
||||
private static function maybe_enable_homescreen() {
|
||||
if ( self::is_new_install() && ! get_option( 'woocommerce_homescreen_enabled' ) ) {
|
||||
add_option( 'woocommerce_homescreen_enabled', 'yes' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset any notices added to admin.
|
||||
*
|
||||
|
@ -621,6 +671,11 @@ class WC_Install {
|
|||
|
||||
/**
|
||||
* 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
|
||||
|
@ -946,6 +1001,14 @@ CREATE TABLE {$wpdb->prefix}wc_tax_rate_classes (
|
|||
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;
|
||||
";
|
||||
|
||||
|
@ -980,6 +1043,7 @@ CREATE TABLE {$wpdb->prefix}wc_tax_rate_classes (
|
|||
"{$wpdb->prefix}woocommerce_shipping_zones",
|
||||
"{$wpdb->prefix}woocommerce_tax_rate_locations",
|
||||
"{$wpdb->prefix}woocommerce_tax_rates",
|
||||
"{$wpdb->prefix}wc_reserved_stock",
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -1187,7 +1251,7 @@ CREATE TABLE {$wpdb->prefix}wc_tax_rate_classes (
|
|||
}
|
||||
|
||||
// Install files and folders for uploading files and prevent hotlinking.
|
||||
$upload_dir = wp_upload_dir();
|
||||
$upload_dir = wp_get_upload_dir();
|
||||
$download_method = get_option( 'woocommerce_file_download_method', 'force' );
|
||||
|
||||
$files = array(
|
||||
|
@ -1206,19 +1270,16 @@ CREATE TABLE {$wpdb->prefix}wc_tax_rate_classes (
|
|||
'file' => 'index.html',
|
||||
'content' => '',
|
||||
),
|
||||
);
|
||||
|
||||
if ( 'redirect' !== $download_method ) {
|
||||
$files[] = array(
|
||||
array(
|
||||
'base' => $upload_dir['basedir'] . '/woocommerce_uploads',
|
||||
'file' => '.htaccess',
|
||||
'content' => 'deny from all',
|
||||
);
|
||||
}
|
||||
'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'], 'w' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen
|
||||
$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
|
||||
|
|
|
@ -59,6 +59,7 @@ class WC_Post_Types {
|
|||
'query_var' => is_admin(),
|
||||
'rewrite' => false,
|
||||
'public' => false,
|
||||
'label' => _x( 'Product type', 'Taxonomy name', 'woocommerce' ),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
@ -75,6 +76,7 @@ class WC_Post_Types {
|
|||
'query_var' => is_admin(),
|
||||
'rewrite' => false,
|
||||
'public' => false,
|
||||
'label' => _x( 'Product visibility', 'Taxonomy name', 'woocommerce' ),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
|
|
@ -332,7 +332,26 @@ class WC_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() ) {
|
||||
$package['rates'] = $package['rates'] + $shipping_method->get_rates_for_package( $package ); // + instead of array_merge maintains numeric keys
|
||||
/**
|
||||
* 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 );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,15 @@ final class WooCommerce {
|
|||
*/
|
||||
public $version = '4.3.0';
|
||||
|
||||
/**
|
||||
* WooCommerce Schema version.
|
||||
*
|
||||
* @since 4.3 started with version string 430.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $db_version = '430';
|
||||
|
||||
/**
|
||||
* The single instance of the class.
|
||||
*
|
||||
|
@ -229,7 +238,7 @@ final class WooCommerce {
|
|||
$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.0' );
|
||||
$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 );
|
||||
}
|
||||
|
@ -246,6 +255,7 @@ final class WooCommerce {
|
|||
'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 ) {
|
||||
|
|
|
@ -762,10 +762,16 @@ class WC_Shop_Customizer {
|
|||
)
|
||||
);
|
||||
|
||||
$choose_pages = array(
|
||||
'wp_page_for_privacy_policy' => __( 'Privacy policy', 'woocommerce' ),
|
||||
'woocommerce_terms_page_id' => __( 'Terms and conditions', 'woocommerce' ),
|
||||
);
|
||||
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',
|
||||
|
|
|
@ -881,8 +881,8 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
|
|||
$outofstock_where = ' AND exclude_join.object_id IS NULL';
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
return $wpdb->get_results(
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
"
|
||||
SELECT posts.ID as id, posts.post_parent as parent_id
|
||||
FROM {$wpdb->posts} AS posts
|
||||
|
@ -900,8 +900,8 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
|
|||
)
|
||||
GROUP BY posts.ID
|
||||
"
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1603,7 +1603,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
|
|||
|
||||
foreach ( $search_terms as $search_term ) {
|
||||
$like = '%' . $wpdb->esc_like( $search_term ) . '%';
|
||||
$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 ) )", $like, $like, $like, $like ); // @codingStandardsIgnoreLine.
|
||||
$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 ) )", $like, $like, $like, $like ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$searchand = ' AND ';
|
||||
}
|
||||
|
||||
|
@ -2062,4 +2062,23 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
|
|||
}
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -656,10 +656,10 @@ class WC_Product_CSV_Exporter extends WC_CSV_Batch_Exporter {
|
|||
|
||||
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 ) : $attribute;
|
||||
$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 ] = $attribute;
|
||||
$row[ 'attributes:value' . $i ] = str_replace( ',', '\\,', $attribute );
|
||||
$row[ 'attributes:taxonomy' . $i ] = 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -400,21 +400,22 @@ class WC_Product_CSV_Importer extends WC_Product_Importer {
|
|||
$total = count( $_terms );
|
||||
|
||||
foreach ( $_terms as $index => $_term ) {
|
||||
// Check if category exists. Parent must be empty string or null if doesn't exists.
|
||||
$term = term_exists( $_term, 'product_cat', $parent );
|
||||
|
||||
if ( is_array( $term ) ) {
|
||||
$term_id = $term['term_id'];
|
||||
// Don't allow users without capabilities to create new categories.
|
||||
} elseif ( ! current_user_can( 'manage_product_terms' ) ) {
|
||||
// Don't allow users without capabilities to create new categories.
|
||||
if ( ! current_user_can( 'manage_product_terms' ) ) {
|
||||
break;
|
||||
} else {
|
||||
$term = wp_insert_term( $_term, 'product_cat', array( 'parent' => intval( $parent ) ) );
|
||||
}
|
||||
|
||||
if ( is_wp_error( $term ) ) {
|
||||
break; // We cannot continue if the term cannot be inserted.
|
||||
$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'];
|
||||
}
|
||||
|
||||
|
|
|
@ -84,9 +84,8 @@ class WC_Shortcode_Checkout {
|
|||
// Pay for existing order.
|
||||
if ( isset( $_GET['pay_for_order'], $_GET['key'] ) && $order_id ) { // WPCS: input var ok, CSRF ok.
|
||||
try {
|
||||
$order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // WPCS: input var ok, CSRF ok.
|
||||
$order = wc_get_order( $order_id );
|
||||
$hold_stock_minutes = (int) get_option( 'woocommerce_hold_stock_minutes', 0 );
|
||||
$order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // WPCS: input var ok, CSRF ok.
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
// Order or payment link is invalid.
|
||||
if ( ! $order || $order->get_id() !== $order_id || ! hash_equals( $order->get_order_key(), $order_key ) ) {
|
||||
|
@ -158,7 +157,7 @@ class WC_Shortcode_Checkout {
|
|||
}
|
||||
|
||||
// Check stock based on all items in the cart and consider any held stock within pending orders.
|
||||
$held_stock = ( $hold_stock_minutes > 0 ) ? wc_get_held_stock_quantity( $product, $order->get_id() ) : 0;
|
||||
$held_stock = wc_get_held_stock_quantity( $product, $order->get_id() );
|
||||
$required_stock = $quantities[ $product->get_stock_managed_by_id() ];
|
||||
|
||||
if ( ! apply_filters( 'woocommerce_pay_order_product_has_enough_stock', ( $product->get_stock_quantity() >= ( $held_stock + $required_stock ) ), $product, $order ) ) {
|
||||
|
|
|
@ -507,7 +507,7 @@ class WC_Shortcode_Products {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set product as visible when quering for hidden products.
|
||||
* Set product as visible when querying for hidden products.
|
||||
*
|
||||
* @since 3.2.0
|
||||
* @param bool $visibility Product visibility.
|
||||
|
|
|
@ -44,17 +44,18 @@ class WC_Site_Tracking {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register scripts required to record events from javascript.
|
||||
*/
|
||||
public static function register_scripts() {
|
||||
wp_register_script( 'woo-tracks', 'https://stats.wp.com/w.js', array( 'wp-hooks' ), gmdate( 'YW' ), false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add scripts required to record events from javascript.
|
||||
*/
|
||||
public static function enqueue_scripts() {
|
||||
|
||||
// Add w.js to the page.
|
||||
wp_enqueue_script( 'woo-tracks', 'https://stats.wp.com/w.js', array( 'wp-hooks' ), gmdate( 'YW' ), false );
|
||||
|
||||
// Expose tracking via a function in the wcTracks global namespace directly before wc_print_js.
|
||||
add_filter( 'admin_footer', array( __CLASS__, 'add_tracking_function' ), 24 );
|
||||
|
||||
wp_enqueue_script( 'woo-tracks' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -65,7 +66,12 @@ class WC_Site_Tracking {
|
|||
<!-- WooCommerce Tracks -->
|
||||
<script type="text/javascript">
|
||||
window.wcTracks = window.wcTracks || {};
|
||||
window.wcTracks.isEnabled = <?php echo self::is_tracking_enabled() ? 'true' : 'false'; ?>;
|
||||
window.wcTracks.recordEvent = function( name, properties ) {
|
||||
if ( ! window.wcTracks.isEnabled ) {
|
||||
return;
|
||||
}
|
||||
|
||||
var eventName = '<?php echo esc_attr( WC_Tracks::PREFIX ); ?>' + name;
|
||||
var eventProperties = properties || {};
|
||||
eventProperties.url = '<?php echo esc_html( home_url() ); ?>'
|
||||
|
@ -83,14 +89,43 @@ class WC_Site_Tracking {
|
|||
}
|
||||
|
||||
/**
|
||||
* Add empty tracking function to admin footer when tracking is disabled in case
|
||||
* it's called without checking if it's defined beforehand.
|
||||
* Adds a function to load tracking scripts and enable them client-side on the fly.
|
||||
* Note that this function does not update `woocommerce_allow_tracking` in the database
|
||||
* and will not persist enabled tracking across page loads.
|
||||
*/
|
||||
public static function add_empty_tracking_function() {
|
||||
public static function add_enable_tracking_function() {
|
||||
global $wp_scripts;
|
||||
$woo_tracks_script = $wp_scripts->registered['woo-tracks']->src;
|
||||
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
window.wcTracks = window.wcTracks || {};
|
||||
window.wcTracks.recordEvent = function() {};
|
||||
window.wcTracks.enable = function( callback = null ) {
|
||||
window.wcTracks.isEnabled = true;
|
||||
|
||||
var scriptUrl = '<?php echo esc_url( $woo_tracks_script ); ?>';
|
||||
var existingScript = document.querySelector( `script[src="${ scriptUrl }"]` );
|
||||
if ( existingScript ) {
|
||||
return;
|
||||
}
|
||||
|
||||
var script = document.createElement('script');
|
||||
script.src = scriptUrl;
|
||||
document.body.append(script);
|
||||
|
||||
// Callback after scripts have loaded.
|
||||
script.onload = function() {
|
||||
if ( 'function' === typeof callback ) {
|
||||
callback( true );
|
||||
}
|
||||
}
|
||||
|
||||
// Callback triggered if the script fails to load.
|
||||
script.onerror = function() {
|
||||
if ( 'function' === typeof callback ) {
|
||||
callback( false );
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
@ -100,11 +135,12 @@ class WC_Site_Tracking {
|
|||
*/
|
||||
public static function init() {
|
||||
|
||||
// Define window.wcTracks.recordEvent in case it is enabled client-side.
|
||||
self::register_scripts();
|
||||
add_filter( 'admin_footer', array( __CLASS__, 'add_tracking_function' ), 24 );
|
||||
|
||||
if ( ! self::is_tracking_enabled() ) {
|
||||
|
||||
// Define window.wcTracks.recordEvent in case there is an attempt to use it when tracking is turned off.
|
||||
add_filter( 'admin_footer', array( __CLASS__, 'add_empty_tracking_function' ), 24 );
|
||||
|
||||
add_filter( 'admin_footer', array( __CLASS__, 'add_enable_tracking_function' ), 24 );
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -318,8 +318,13 @@ function wc_cart_totals_order_total_html() {
|
|||
$taxable_address = WC()->customer->get_taxable_address();
|
||||
/* translators: %s: country name */
|
||||
$estimated_text = WC()->customer->is_customer_outside_base() && ! WC()->customer->has_calculated_shipping() ? sprintf( ' ' . __( 'estimated for %s', 'woocommerce' ), WC()->countries->estimated_for_prefix( $taxable_address[0] ) . WC()->countries->countries[ $taxable_address[0] ] ) : '';
|
||||
/* translators: %s: tax information */
|
||||
$value .= '<small class="includes_tax">' . sprintf( __( '(includes %s)', 'woocommerce' ), implode( ', ', $tax_string_array ) . $estimated_text ) . '</small>';
|
||||
$value .= '<small class="includes_tax">('
|
||||
/* translators: includes tax information */
|
||||
. esc_html__( 'includes', 'woocommerce' )
|
||||
. ' '
|
||||
. wp_kses_post( implode( ', ', $tax_string_array ) )
|
||||
. esc_html( $estimated_text )
|
||||
. ')</small>';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -150,6 +150,86 @@ function wc_update_order( $args ) {
|
|||
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).
|
||||
*
|
||||
|
@ -187,7 +267,13 @@ function wc_get_template_part( $slug, $name = '' ) {
|
|||
);
|
||||
}
|
||||
|
||||
wp_cache_set( $cache_key, $template, 'woocommerce' );
|
||||
// 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.
|
||||
|
@ -212,7 +298,14 @@ function wc_get_template( $template_name, $args = array(), $template_path = '',
|
|||
|
||||
if ( ! $template ) {
|
||||
$template = wc_locate_template( $template_name, $template_path, $default_path );
|
||||
wp_cache_set( $cache_key, $template, 'woocommerce' );
|
||||
|
||||
// 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.
|
||||
|
@ -310,6 +403,42 @@ function wc_locate_template( $template_name, $template_path = '', $default_path
|
|||
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.
|
||||
*
|
||||
|
@ -1493,9 +1622,12 @@ function wc_postcode_location_matcher( $postcode, $objects, $object_id_key, $obj
|
|||
*
|
||||
* @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 ) {
|
||||
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';
|
||||
|
@ -1506,7 +1638,8 @@ function wc_get_shipping_method_count( $include_legacy = false ) {
|
|||
return absint( $transient_value['value'] );
|
||||
}
|
||||
|
||||
$method_count = absint( $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods" ) );
|
||||
$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.
|
||||
|
|
|
@ -713,6 +713,11 @@ function wc_string_to_datetime( $time_string ) {
|
|||
* @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 ) {
|
||||
|
@ -720,13 +725,13 @@ function wc_timezone_string() {
|
|||
}
|
||||
|
||||
// Get UTC offset, if it isn't set then return UTC.
|
||||
$utc_offset = intval( get_option( 'gmt_offset', 0 ) );
|
||||
if ( 0 === $utc_offset ) {
|
||||
$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 *= 3600;
|
||||
$utc_offset = (int) ( $utc_offset * 3600 );
|
||||
|
||||
// Attempt to guess the timezone string from the UTC offset.
|
||||
$timezone = timezone_name_from_abbr( '', $utc_offset );
|
||||
|
|
|
@ -299,33 +299,80 @@ function wc_increase_stock_levels( $order_id ) {
|
|||
* @param integer $exclude_order_id Order ID to exclude.
|
||||
* @return int
|
||||
*/
|
||||
function wc_get_held_stock_quantity( $product, $exclude_order_id = 0 ) {
|
||||
global $wpdb;
|
||||
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 $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"
|
||||
SELECT SUM( order_item_meta.meta_value ) AS held_qty
|
||||
FROM {$wpdb->posts} AS posts
|
||||
LEFT JOIN {$wpdb->postmeta} as postmeta ON posts.ID = postmeta.post_id
|
||||
LEFT JOIN {$wpdb->prefix}woocommerce_order_items as order_items ON posts.ID = order_items.order_id
|
||||
LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta as order_item_meta ON order_items.order_item_id = order_item_meta.order_item_id
|
||||
LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta as order_item_meta2 ON order_items.order_item_id = order_item_meta2.order_item_id
|
||||
WHERE order_item_meta.meta_key = '_qty'
|
||||
AND order_item_meta2.meta_key = %s
|
||||
AND order_item_meta2.meta_value = %d
|
||||
AND postmeta.meta_key = '_created_via'
|
||||
AND postmeta.meta_value = 'checkout'
|
||||
AND posts.post_type IN ( '" . implode( "','", wc_get_order_types() ) . "' )
|
||||
AND posts.post_status = 'wc-pending'
|
||||
AND posts.ID != %d;",
|
||||
'product_variation' === get_post_type( $product->get_stock_managed_by_id() ) ? '_variation_id' : '_product_id',
|
||||
$product->get_stock_managed_by_id(),
|
||||
$exclude_order_id
|
||||
)
|
||||
); // WPCS: unprepared SQL ok.
|
||||
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
|
||||
*
|
||||
|
|
|
@ -673,7 +673,7 @@ function wc_get_product_class( $class = '', $product = null ) {
|
|||
* WooCommerce Post Class filter.
|
||||
*
|
||||
* @since 3.6.2
|
||||
* @param array $class Array of CSS classes.
|
||||
* @param array $classes Array of CSS classes.
|
||||
* @param WC_Product $product Product object.
|
||||
*/
|
||||
$classes = apply_filters( 'woocommerce_post_class', $classes, $product );
|
||||
|
@ -2097,7 +2097,7 @@ if ( ! function_exists( 'woocommerce_widget_shopping_cart_subtotal' ) ) {
|
|||
* @since 3.7.0
|
||||
*/
|
||||
function woocommerce_widget_shopping_cart_subtotal() {
|
||||
echo '<strong>' . esc_html__( 'Subtotal', 'woocommerce' ) . ':</strong> ' . WC()->cart->get_cart_subtotal(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo '<strong>' . esc_html__( 'Subtotal:', 'woocommerce' ) . '</strong> ' . WC()->cart->get_cart_subtotal(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2706,7 +2706,7 @@ if ( ! function_exists( 'woocommerce_form_field' ) ) {
|
|||
$field = '<select name="' . esc_attr( $key ) . '" id="' . esc_attr( $args['id'] ) . '" class="country_to_state country_select ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" ' . implode( ' ', $custom_attributes ) . '><option value="">' . esc_html__( 'Select a country / region…', 'woocommerce' ) . '</option>';
|
||||
|
||||
foreach ( $countries as $ckey => $cvalue ) {
|
||||
$field .= '<option value="' . esc_attr( $ckey ) . '" ' . selected( $value, $ckey, false ) . '>' . $cvalue . '</option>';
|
||||
$field .= '<option value="' . esc_attr( $ckey ) . '" ' . selected( $value, $ckey, false ) . '>' . esc_html( $cvalue ) . '</option>';
|
||||
}
|
||||
|
||||
$field .= '</select>';
|
||||
|
@ -2733,7 +2733,7 @@ if ( ! function_exists( 'woocommerce_form_field' ) ) {
|
|||
<option value="">' . esc_html__( 'Select an option…', 'woocommerce' ) . '</option>';
|
||||
|
||||
foreach ( $states as $ckey => $cvalue ) {
|
||||
$field .= '<option value="' . esc_attr( $ckey ) . '" ' . selected( $value, $ckey, false ) . '>' . $cvalue . '</option>';
|
||||
$field .= '<option value="' . esc_attr( $ckey ) . '" ' . selected( $value, $ckey, false ) . '>' . esc_html( $cvalue ) . '</option>';
|
||||
}
|
||||
|
||||
$field .= '</select>';
|
||||
|
@ -2782,7 +2782,7 @@ if ( ! function_exists( 'woocommerce_form_field' ) ) {
|
|||
}
|
||||
$custom_attributes[] = 'data-allow_clear="true"';
|
||||
}
|
||||
$options .= '<option value="' . esc_attr( $option_key ) . '" ' . selected( $value, $option_key, false ) . '>' . esc_attr( $option_text ) . '</option>';
|
||||
$options .= '<option value="' . esc_attr( $option_key ) . '" ' . selected( $value, $option_key, false ) . '>' . esc_html( $option_text ) . '</option>';
|
||||
}
|
||||
|
||||
$field .= '<select name="' . esc_attr( $key ) . '" id="' . esc_attr( $args['id'] ) . '" class="select ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" ' . implode( ' ', $custom_attributes ) . ' data-placeholder="' . esc_attr( $args['placeholder'] ) . '">
|
||||
|
@ -2797,7 +2797,7 @@ if ( ! function_exists( 'woocommerce_form_field' ) ) {
|
|||
if ( ! empty( $args['options'] ) ) {
|
||||
foreach ( $args['options'] as $option_key => $option_text ) {
|
||||
$field .= '<input type="radio" class="input-radio ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" value="' . esc_attr( $option_key ) . '" name="' . esc_attr( $key ) . '" ' . implode( ' ', $custom_attributes ) . ' id="' . esc_attr( $args['id'] ) . '_' . esc_attr( $option_key ) . '"' . checked( $value, $option_key, false ) . ' />';
|
||||
$field .= '<label for="' . esc_attr( $args['id'] ) . '_' . esc_attr( $option_key ) . '" class="radio ' . implode( ' ', $args['label_class'] ) . '">' . $option_text . '</label>';
|
||||
$field .= '<label for="' . esc_attr( $args['id'] ) . '_' . esc_attr( $option_key ) . '" class="radio ' . implode( ' ', $args['label_class'] ) . '">' . esc_html( $option_text ) . '</label>';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"command": {
|
||||
"publish": {
|
||||
"message": "chore(release): publish"
|
||||
}
|
||||
},
|
||||
"ignoreChanges": [ "**/CHANGELOG.md", "**/test/**" ],
|
||||
"packages": [ "tests/e2e/*" ],
|
||||
"version": "independent"
|
||||
}
|
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
|
@ -12,6 +12,8 @@
|
|||
"scripts": {
|
||||
"build": "grunt && npm run makepot",
|
||||
"build-watch": "grunt watch",
|
||||
"build:packages": "node ./tests/e2e/bin/build.js",
|
||||
"build:zip": "./bin/build-zip.sh",
|
||||
"lint:js": "eslint assets/js --ext=js",
|
||||
"docker:up": "npm explore @woocommerce/e2e-environment -- npm run docker:up",
|
||||
"docker:down": "npm explore @woocommerce/e2e-environment -- npm run docker:down",
|
||||
|
@ -19,6 +21,7 @@
|
|||
"test:e2e-dev": "npm explore @woocommerce/e2e-environment -- npm run test:e2e-dev",
|
||||
"makepot": "composer run-script makepot",
|
||||
"packages:fix:textdomain": "node ./bin/package-update-textdomain.js",
|
||||
"publish-packages": "npm run build:packages && lerna publish from-package",
|
||||
"git:update-hooks": "rm -r .git/hooks && mkdir -p .git/hooks && node ./node_modules/husky/husky.js install"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -29,14 +32,17 @@
|
|||
"@babel/register": "7.9.0",
|
||||
"@jest/test-sequencer": "^25.0.0",
|
||||
"@woocommerce/e2e-environment": "file:tests/e2e/env",
|
||||
"@wordpress/e2e-test-utils": "4.3.1",
|
||||
"autoprefixer": "9.7.5",
|
||||
"@wordpress/babel-plugin-import-jsx-pragma": "1.1.3",
|
||||
"@wordpress/babel-preset-default": "3.0.2",
|
||||
"@wordpress/e2e-test-utils": "4.6.0",
|
||||
"autoprefixer": "9.8.4",
|
||||
"babel-eslint": "10.1.0",
|
||||
"chai": "4.2.0",
|
||||
"chai-as-promised": "7.1.1",
|
||||
"commander": "4.1.1",
|
||||
"config": "3.3.1",
|
||||
"cross-env": "6.0.3",
|
||||
"deasync": "0.1.19",
|
||||
"eslint": "6.8.0",
|
||||
"eslint-config-wpcalypso": "5.0.0",
|
||||
"eslint-plugin-jest": "23.8.2",
|
||||
|
@ -50,18 +56,17 @@
|
|||
"grunt-contrib-watch": "1.1.0",
|
||||
"grunt-phpcs": "0.4.0",
|
||||
"grunt-postcss": "0.9.0",
|
||||
"grunt-prompt": "1.3.3",
|
||||
"grunt-rtlcss": "2.0.2",
|
||||
"grunt-sass": "3.1.0",
|
||||
"grunt-shell": "3.0.1",
|
||||
"grunt-stylelint": "0.14.0",
|
||||
"gruntify-eslint": "5.0.0",
|
||||
"husky": "4.2.5",
|
||||
"istanbul": "1.0.0-alpha.2",
|
||||
"jest": "25.1.0",
|
||||
"jest-puppeteer": "4.4.0",
|
||||
"lerna": "3.20.2",
|
||||
"lint-staged": "9.5.0",
|
||||
"mocha": "7.0.1",
|
||||
"mocha": "7.2.0",
|
||||
"node-sass": "4.13.0",
|
||||
"prettier": "github:automattic/calypso-prettier#c56b4251",
|
||||
"puppeteer": "2.0.0",
|
||||
|
@ -78,8 +83,9 @@
|
|||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"post-merge": "./bin/post-merge.sh",
|
||||
"pre-commit": "lint-staged",
|
||||
"post-merge": "./bin/post-merge.sh"
|
||||
"pre-push": "./bin/pre-push.sh"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
|
|
|
@ -59,5 +59,6 @@
|
|||
<exclude-pattern>i18n/</exclude-pattern>
|
||||
<exclude-pattern>src/</exclude-pattern>
|
||||
<exclude-pattern>tests/php</exclude-pattern>
|
||||
<exclude-pattern>tests/Tools/</exclude-pattern>
|
||||
</rule>
|
||||
</ruleset>
|
||||
|
|
|
@ -49,4 +49,7 @@
|
|||
<listeners>
|
||||
<listener class="SpeedTrapListener" file="tests/legacy/includes/listener-loader.php" />
|
||||
</listeners>
|
||||
<extensions>
|
||||
<extension class="\Automattic\WooCommerce\Testing\Tools\CodeHacking\CodeHackerTestHook" />
|
||||
</extensions>
|
||||
</phpunit>
|
||||
|
|
47
readme.txt
47
readme.txt
|
@ -1,10 +1,10 @@
|
|||
=== WooCommerce ===
|
||||
Contributors: automattic, mikejolley, jameskoster, claudiosanches, kloon, rodrigosprimo, peterfabian1000, vedjain, jamosova, obliviousharmony
|
||||
Tags: e-commerce, store, sales, sell, woo, shop, cart, checkout, downloadable, downloads, payments, paypal, storefront, stripe, woo commerce
|
||||
Requires at least: 5.0
|
||||
Requires at least: 5.2
|
||||
Tested up to: 5.4
|
||||
Requires PHP: 7.0
|
||||
Stable tag: 4.0.1
|
||||
Stable tag: 4.1.1
|
||||
License: GPLv3
|
||||
License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
|
@ -179,48 +179,7 @@ INTERESTED IN DEVELOPMENT?
|
|||
|
||||
== Changelog ==
|
||||
|
||||
= 4.1.0 - 2020-05-05 =
|
||||
|
||||
**WooCommerce**
|
||||
* Enhancement - Update dependency woocommerce/woocommerce-admin to v1.1.0 #26057
|
||||
* Enhancement - Updated jetpack-autoloader to 1.6 and woocommerce-blocks to 2.5.16. #26099
|
||||
* Enhancement - Added option to ignore discounts from cart's total amount to enable free shipping. #24776
|
||||
* Enhancement - Changed show password icon color to a darker grey hue. #25625
|
||||
* Enhancement - Use new Setup Wizard for all users. #26016
|
||||
* Tweak - Show notice for WP min version to WP 5.2. #26094
|
||||
* Tweak - Improve the string for untested WooCommerce extensions in the system status page to avoid confusion. #25904
|
||||
* Tweak - Updated KZT (₸) symbol. #25609
|
||||
* Tweak - Trim whitespaces and strip slashes from MaxMind License Key. #25466
|
||||
* Tweak - Updated "Help" tabs documentation. #25826
|
||||
* Tweak - Update serbian currency symbol to рсд from дин. #25885
|
||||
* Fix - Password visibility toggle to hide password again from text. #25627
|
||||
* Fix - Undefined property error when attempting to modify the coupon post meta. #25755
|
||||
* Fix - Remove some of the individual rounding logic to make sure we round at certain places only. #25800
|
||||
* Fix - Order totals calculation if the order contains taxable and non-taxable products and percentage coupons. #25092
|
||||
* Fix - Wording for cancelled order email. #25316
|
||||
* Fix - Removed guided tour videos link on setup wizard (since current link only redirects to the docs). #25823
|
||||
* Fix - Add RTL style to the onboarding wizard. #25835
|
||||
* Fix - Trigger change and set val to qty on the frontend so that it properly updates event handlers. #25903
|
||||
* Fix - Corrected the way percent coupons apply remainders across the order. #25943
|
||||
* Fix - Clarified the error messaging for WooCommerce.com package update failures. #26034
|
||||
* Fix - Enforce per user usage limit check for a coupon on guest users based on email. #26066
|
||||
* Fix - Remove elements with style=display:none explicitly to address a regression causing broken email html. #26075
|
||||
* Dev - Added woocommerce_can_restock_refunded_items filter. #25728
|
||||
* Dev - Added woocommerce_order_get_tax_location filter. #25727
|
||||
* Dev - Updated stock handling to prevent race conditions when orders come in at the same time. #25708
|
||||
* Dev - Updated /myaccount/form-login.php to use consistent kebab-case class names for woocommerce-form-row. #25668
|
||||
* Dev - Add filter woocommerce_product_upsells_products_heading to allow heading modification without having to override the template file. #25628
|
||||
* Dev - Added woocommerce_order_get_tax_location filter. #25727
|
||||
* Dev - Added the get_woocommerce_currency_symbols function to allow develops to get an array of all the currency symbol registered with WooCommerce. #25733
|
||||
* Dev - Changed string typed label_class to array in checkout fields.
|
||||
* Dev - Added "woocommerce_emogrifier" action before the content of the emails is "emogrified". #25801
|
||||
* Dev - Add Ability to Filter Event Props. #25851
|
||||
* Dev - Updated the unit test install script to support paths to MySQL sockets that contain spaces. #25923
|
||||
* Dev - Made the default test source folders support the system tmp folder. #25923
|
||||
* Dev - Add cart & checkout block/shortcode info to tracker data. #25932
|
||||
* Dev - Make WC_Product_Data_Store_CPT::update_product_stock operations atomic. #26039
|
||||
* Dev - Adds usage data for the of cart & checkout blocks (currently in development in WooCommmerce Blocks plugin) to the WC Tracker snapshot. #26084
|
||||
* Dev - Implement some additional tracks for coupons, orders, and products. #26085
|
||||
= 4.3.0 - 2020-07-07 =
|
||||
|
||||
[See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce/master/CHANGELOG.txt).
|
||||
|
||||
|
|
|
@ -11,5 +11,11 @@
|
|||
"depTypeList": ["devDependencies"],
|
||||
"extends": ["schedule:monthly"]
|
||||
}
|
||||
],
|
||||
"ignoreDeps": [
|
||||
"woocommerce/action-scheduler",
|
||||
"woocommerce/woocommerce-admin",
|
||||
"woocommerce/woocommerce-blocks",
|
||||
"woocommerce/woocommerce-rest-api"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
<?php
|
||||
/**
|
||||
* Handle product stock reservation during checkout.
|
||||
*
|
||||
* @package Automattic/WooCommerce
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Checkout\Helpers;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Stock Reservation class.
|
||||
*/
|
||||
final class ReserveStock {
|
||||
|
||||
/**
|
||||
* Is stock reservation enabled?
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private $enabled = true;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
// Table needed for this feature are added in 4.3.
|
||||
$this->enabled = get_option( 'woocommerce_schema_version', 0 ) >= 430;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is stock reservation enabled?
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
protected function is_enabled() {
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query for any existing holds on stock for this item.
|
||||
*
|
||||
* @param \WC_Product $product Product to get reserved stock for.
|
||||
* @param integer $exclude_order_id Optional order to exclude from the results.
|
||||
*
|
||||
* @return integer Amount of stock already reserved.
|
||||
*/
|
||||
public function get_reserved_stock( \WC_Product $product, $exclude_order_id = 0 ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! $this->is_enabled() ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
|
||||
return (int) $wpdb->get_var( $this->get_query_for_reserved_stock( $product->get_stock_managed_by_id(), $exclude_order_id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Put a temporary hold on stock for an order if enough is available.
|
||||
*
|
||||
* @throws ReserveStockException If stock cannot be reserved.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
* @param int $minutes How long to reserve stock in minutes. Defaults to woocommerce_hold_stock_minutes.
|
||||
*/
|
||||
public function reserve_stock_for_order( \WC_Order $order, $minutes = 0 ) {
|
||||
$minutes = $minutes ? $minutes : (int) get_option( 'woocommerce_hold_stock_minutes', 60 );
|
||||
|
||||
if ( ! $minutes || ! $this->is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$items = array_filter(
|
||||
$order->get_items(),
|
||||
function( $item ) {
|
||||
return $item->is_type( 'line_item' ) && $item->get_product() instanceof \WC_Product && $item->get_quantity() > 0;
|
||||
}
|
||||
);
|
||||
$rows = array();
|
||||
|
||||
foreach ( $items as $item ) {
|
||||
$product = $item->get_product();
|
||||
|
||||
if ( ! $product->is_in_stock() ) {
|
||||
throw new ReserveStockException(
|
||||
'woocommerce_product_out_of_stock',
|
||||
sprintf(
|
||||
/* translators: %s: product name */
|
||||
__( '"%s" is out of stock and cannot be purchased.', 'woocommerce' ),
|
||||
$product->get_name()
|
||||
),
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
// If stock management is off, no need to reserve any stock here.
|
||||
if ( ! $product->managing_stock() || $product->backorders_allowed() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$managed_by_id = $product->get_stock_managed_by_id();
|
||||
$rows[ $managed_by_id ] = isset( $rows[ $managed_by_id ] ) ? $rows[ $managed_by_id ] + $item->get_quantity() : $item->get_quantity();
|
||||
}
|
||||
|
||||
if ( ! empty( $rows ) ) {
|
||||
foreach ( $rows as $product_id => $quantity ) {
|
||||
$this->reserve_stock_for_product( $product_id, $quantity, $order, $minutes );
|
||||
}
|
||||
}
|
||||
} catch ( ReserveStockException $e ) {
|
||||
$this->release_stock_for_order( $order );
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a temporary hold on stock for an order.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
*/
|
||||
public function release_stock_for_order( \WC_Order $order ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! $this->is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$wpdb->delete(
|
||||
$wpdb->wc_reserved_stock,
|
||||
array(
|
||||
'order_id' => $order->get_id(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reserve stock for a product by inserting rows into the DB.
|
||||
*
|
||||
* @throws ReserveStockException If a row cannot be inserted.
|
||||
*
|
||||
* @param int $product_id Product ID which is having stock reserved.
|
||||
* @param int $stock_quantity Stock amount to reserve.
|
||||
* @param \WC_Order $order Order object which contains the product.
|
||||
* @param int $minutes How long to reserve stock in minutes.
|
||||
*/
|
||||
private function reserve_stock_for_product( $product_id, $stock_quantity, \WC_Order $order, $minutes ) {
|
||||
global $wpdb;
|
||||
|
||||
$product_data_store = \WC_Data_Store::load( 'product' );
|
||||
$query_for_stock = $product_data_store->get_query_for_stock( $product_id );
|
||||
$query_for_reserved_stock = $this->get_query_for_reserved_stock( $product_id, $order->get_id() );
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
|
||||
$result = $wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"
|
||||
INSERT INTO {$wpdb->wc_reserved_stock} ( `order_id`, `product_id`, `stock_quantity`, `timestamp`, `expires` )
|
||||
SELECT %d, %d, %d, NOW(), ( NOW() + INTERVAL %d MINUTE ) FROM DUAL
|
||||
WHERE ( $query_for_stock FOR UPDATE ) - ( $query_for_reserved_stock FOR UPDATE ) >= %d
|
||||
ON DUPLICATE KEY UPDATE `expires` = VALUES( `expires` ), `stock_quantity` = VALUES( `stock_quantity` )
|
||||
",
|
||||
$order->get_id(),
|
||||
$product_id,
|
||||
$stock_quantity,
|
||||
$minutes,
|
||||
$stock_quantity
|
||||
)
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
|
||||
|
||||
if ( ! $result ) {
|
||||
$product = wc_get_product( $product_id );
|
||||
throw new ReserveStockException(
|
||||
'woocommerce_product_not_enough_stock',
|
||||
sprintf(
|
||||
/* translators: %s: product name */
|
||||
__( 'Not enough units of %s are available in stock to fulfil this order.', 'woocommerce' ),
|
||||
$product ? $product->get_name() : '#' . $product_id
|
||||
),
|
||||
403
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns query statement for getting reserved stock of a product.
|
||||
*
|
||||
* @param int $product_id Product ID.
|
||||
* @param integer $exclude_order_id Optional order to exclude from the results.
|
||||
* @return string|void Query statement.
|
||||
*/
|
||||
private function get_query_for_reserved_stock( $product_id, $exclude_order_id = 0 ) {
|
||||
global $wpdb;
|
||||
return $wpdb->prepare(
|
||||
"
|
||||
SELECT COALESCE( SUM( stock_table.`stock_quantity` ), 0 ) FROM $wpdb->wc_reserved_stock stock_table
|
||||
LEFT JOIN $wpdb->posts posts ON stock_table.`order_id` = posts.ID
|
||||
WHERE posts.post_status IN ( 'wc-checkout-draft', 'wc-pending' )
|
||||
AND stock_table.`expires` > NOW()
|
||||
AND stock_table.`product_id` = %d
|
||||
AND stock_table.`order_id` != %d
|
||||
",
|
||||
$product_id,
|
||||
$exclude_order_id
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
/**
|
||||
* Exceptions for stock reservation.
|
||||
*
|
||||
* @package Automattic/WooCommerce
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Checkout\Helpers;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* ReserveStockException class.
|
||||
*/
|
||||
class ReserveStockException extends \Exception {
|
||||
/**
|
||||
* Sanitized error code.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $error_code;
|
||||
|
||||
/**
|
||||
* Error extra data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $error_data;
|
||||
|
||||
/**
|
||||
* Setup exception.
|
||||
*
|
||||
* @param string $code Machine-readable error code, e.g `woocommerce_invalid_product_id`.
|
||||
* @param string $message User-friendly translated error message, e.g. 'Product ID is invalid'.
|
||||
* @param int $http_status_code Proper HTTP status code to respond with, e.g. 400.
|
||||
* @param array $data Extra error data.
|
||||
*/
|
||||
public function __construct( $code, $message, $http_status_code = 400, $data = array() ) {
|
||||
$this->error_code = $code;
|
||||
$this->error_data = $data;
|
||||
|
||||
parent::__construct( $message, $http_status_code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error code.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getErrorCode() {
|
||||
return $this->error_code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns error data.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getErrorData() {
|
||||
return $this->error_data;
|
||||
}
|
||||
}
|
|
@ -50,6 +50,7 @@ defined( 'ABSPATH' ) || exit;
|
|||
printf( esc_html__( 'Logged in as %s', 'woocommerce' ), esc_html( $user->display_name ) );
|
||||
?>
|
||||
<a href="<?php echo esc_url( $logout_url ); ?>" class="wc-auth-logout"><?php esc_html_e( 'Logout', 'woocommerce' ); ?></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="wc-auth-actions">
|
||||
|
|
|
@ -47,6 +47,8 @@ if ( $available_gateways ) : ?>
|
|||
?>
|
||||
</ul>
|
||||
|
||||
<?php do_action( 'woocommerce_add_payment_method_form_bottom' ); ?>
|
||||
|
||||
<div class="form-row">
|
||||
<?php wp_nonce_field( 'woocommerce-add-payment-method', 'woocommerce-add-payment-method-nonce' ); ?>
|
||||
<button type="submit" class="woocommerce-Button woocommerce-Button--alt button alt" id="place_order" value="<?php esc_attr_e( 'Add payment method', 'woocommerce' ); ?>"><?php esc_html_e( 'Add payment method', 'woocommerce' ); ?></button>
|
||||
|
|
|
@ -121,7 +121,7 @@ if ( ! comments_open() ) {
|
|||
}
|
||||
|
||||
if ( wc_review_ratings_enabled() ) {
|
||||
$comment_form['comment_field'] = '<div class="comment-form-rating"><label for="rating">' . esc_html__( 'Your rating', 'woocommerce' ) . '</label><select name="rating" id="rating" required>
|
||||
$comment_form['comment_field'] = '<div class="comment-form-rating"><label for="rating">' . esc_html__( 'Your rating', 'woocommerce' ) . ( wc_review_ratings_required() ? ' <span class="required">*</span>' : '' ) . '</label><select name="rating" id="rating" required>
|
||||
<option value="">' . esc_html__( 'Rate…', 'woocommerce' ) . '</option>
|
||||
<option value="5">' . esc_html__( 'Perfect', 'woocommerce' ) . '</option>
|
||||
<option value="4">' . esc_html__( 'Good', 'woocommerce' ) . '</option>
|
||||
|
|
|
@ -0,0 +1,511 @@
|
|||
<?php
|
||||
/**
|
||||
* CodeHacker class file.
|
||||
*
|
||||
* @package WooCommerce/Testing
|
||||
*/
|
||||
|
||||
//phpcs:disable WordPress.WP.AlternativeFunctions, WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
|
||||
namespace Automattic\WooCommerce\Testing\Tools\CodeHacking;
|
||||
|
||||
use \ReflectionObject;
|
||||
use \ReflectionException;
|
||||
|
||||
/**
|
||||
* CodeHacker - allows to hack (alter on the fly) the content of PHP code files.
|
||||
*
|
||||
* Based on BypassFinals: https://github.com/dg/bypass-finals
|
||||
*
|
||||
* How to use:
|
||||
*
|
||||
* 1. Register hacks using CodeHacker::add_hack(hack). A hack is either:
|
||||
* - A function with 'hack($code, $path)' signature, or
|
||||
* - An object having a public 'hack($code, $path)' method.
|
||||
*
|
||||
* Where $code is a string containing the code to hack, and $path is the full path of the file
|
||||
* containing the code. The function/method must return a string with the code already hacked.
|
||||
*
|
||||
* 2. Run CodeHacker::enable()
|
||||
*
|
||||
* For using with PHPUnit, see CodeHackerTestHook.
|
||||
*/
|
||||
final class CodeHacker {
|
||||
|
||||
const PROTOCOL = 'file';
|
||||
const HACK_CALLBACK_ARGUMENT_COUNT = 2;
|
||||
|
||||
/**
|
||||
* Value of "context" parameter to be passed to the native PHP filesystem related functions.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
public $context;
|
||||
|
||||
/**
|
||||
* File handle of the file that is open.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
private $handle;
|
||||
|
||||
/**
|
||||
* Optional white list of files to hack, if empty all the files will be hacked.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $paths_with_files_to_hack = array();
|
||||
|
||||
/**
|
||||
* Registered hacks.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $hacks = array();
|
||||
|
||||
/**
|
||||
* Is the code hacker enabled?.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private static $enabled = false;
|
||||
|
||||
/**
|
||||
* Enable the code hacker.
|
||||
*/
|
||||
public static function enable() {
|
||||
if ( ! self::$enabled ) {
|
||||
stream_wrapper_unregister( self::PROTOCOL );
|
||||
stream_wrapper_register( self::PROTOCOL, __CLASS__ );
|
||||
self::$enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the code hacker.
|
||||
*/
|
||||
public static function disable() {
|
||||
if ( self::$enabled ) {
|
||||
stream_wrapper_restore( self::PROTOCOL );
|
||||
self::$enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the code hacker is enabled.
|
||||
*
|
||||
* @return bool True if the code hacker is enabled.
|
||||
*/
|
||||
public static function is_enabled() {
|
||||
return self::$enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the 'reset()' method in all the registered hacks.
|
||||
*/
|
||||
public static function reset_hacks() {
|
||||
foreach ( self::$hacks as $hack ) {
|
||||
call_user_func( array( $hack, 'reset' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new hack.
|
||||
*
|
||||
* @param mixed $hack A function with signature "hack($code, $path)" or an object containing a method with that signature.
|
||||
* @throws \Exception Invalid input.
|
||||
*/
|
||||
public static function add_hack( $hack ) {
|
||||
if ( ! self::is_valid_hack_object( $hack ) ) {
|
||||
$class = get_class( $hack );
|
||||
throw new \Exception( "CodeHacker::addhack for instance of $class: Hacks must be objects having a 'process(\$text, \$path)' method and a 'reset()' method." );
|
||||
}
|
||||
|
||||
self::$hacks[] = $hack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the supplied argument is a valid hack object (has a public "hack" method with two mandatory arguments).
|
||||
*
|
||||
* @param mixed $callback Argument to check.
|
||||
*
|
||||
* @return bool rue if the argument is a valid hack object, false otherwise.
|
||||
*/
|
||||
private static function is_valid_hack_object( $callback ) {
|
||||
if ( ! is_object( $callback ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ro = new ReflectionObject( ( $callback ) );
|
||||
try {
|
||||
$rm = $ro->getMethod( 'hack' );
|
||||
$has_valid_hack_method = $rm->isPublic() && ! $rm->isStatic() && 2 === $rm->getNumberOfRequiredParameters();
|
||||
|
||||
$rm = $ro->getMethod( 'reset' );
|
||||
$has_valid_reset_method = $rm->isPublic() && ! $rm->isStatic() && 0 === $rm->getNumberOfRequiredParameters();
|
||||
|
||||
return $has_valid_hack_method && $has_valid_reset_method;
|
||||
} catch ( ReflectionException $exception ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the code hacker.
|
||||
*
|
||||
* @param array $paths Paths of the directories containing the files to hack.
|
||||
* @throws \Exception Invalid input.
|
||||
*/
|
||||
public static function initialize( array $paths ) {
|
||||
if ( ! is_array( $paths ) || empty( $paths ) ) {
|
||||
throw new \Exception( 'CodeHacker::initialize - $paths must be a non-empty array with the directories containing the files to be hacked.' );
|
||||
}
|
||||
self::$paths_with_files_to_hack = array_map(
|
||||
function( $path ) {
|
||||
return realpath( $path );
|
||||
},
|
||||
$paths
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close directory handle.
|
||||
*/
|
||||
public function dir_closedir() {
|
||||
closedir( $this->handle );
|
||||
}
|
||||
|
||||
/**
|
||||
* Open directory handle.
|
||||
*
|
||||
* @param string $path Specifies the URL that was passed to opendir().
|
||||
* @param int $options Whether or not to enforce safe_mode (0x04).
|
||||
*
|
||||
* @return bool TRUE on success or FALSE on failure.
|
||||
*/
|
||||
public function dir_opendir( $path, $options ) {
|
||||
$this->handle = $this->context
|
||||
? $this->native( 'opendir', $path, $this->context )
|
||||
: $this->native( 'opendir', $path );
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read entry from directory handle.
|
||||
*
|
||||
* @return false|string string representing the next filename, or FALSE if there is no next file.
|
||||
*/
|
||||
public function dir_readdir() {
|
||||
return readdir( $this->handle );
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind directory handle.
|
||||
*
|
||||
* @return TRUE on success or FALSE on failure.
|
||||
*/
|
||||
public function dir_rewinddir() {
|
||||
return rewinddir( $this->handle );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a directory.
|
||||
*
|
||||
* @param string $path Directory which should be created.
|
||||
* @param int $mode The value passed to mkdir().
|
||||
* @param int $options A bitwise mask of values, such as STREAM_MKDIR_RECURSIVE.
|
||||
*
|
||||
* @return bool TRUE on success or FALSE on failure.
|
||||
*/
|
||||
public function mkdir( $path, $mode, $options ) {
|
||||
$recursive = (bool) ( $options & STREAM_MKDIR_RECURSIVE );
|
||||
return $this->native( 'mkdir', $path, $mode, $recursive, $this->context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a file or directory.
|
||||
*
|
||||
* @param string $path_from The URL to the current file.
|
||||
* @param string $path_to The URL which the path_from should be renamed to.
|
||||
*
|
||||
* @return bool TRUE on success or FALSE on failure.
|
||||
*/
|
||||
public function rename( $path_from, $path_to ) {
|
||||
return $this->native( 'rename', $path_from, $path_to, $this->context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a directory.
|
||||
*
|
||||
* @param string $path The directory URL which should be removed.
|
||||
* @param int $options A bitwise mask of values, such as STREAM_MKDIR_RECURSIVE.
|
||||
*
|
||||
* @return bool TRUE on success or FALSE on failure.
|
||||
*/
|
||||
public function rmdir( $path, $options ) {
|
||||
return $this->native( 'rmdir', $path, $this->context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the underlying resource.
|
||||
*
|
||||
* @param mixed $cast_as Can be STREAM_CAST_FOR_SELECT when stream_select() is calling stream_cast() or STREAM_CAST_AS_STREAM when stream_cast() is called for other uses.
|
||||
*
|
||||
* @return mixed The underlying stream resource used by the wrapper, or FALSE.
|
||||
*/
|
||||
public function stream_cast( $cast_as ) {
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a resource.
|
||||
*/
|
||||
public function stream_close() {
|
||||
fclose( $this->handle );
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests for end-of-file on a file pointer.
|
||||
*
|
||||
* @return bool TRUE if the read/write position is at the end of the stream and if no more data is available to be read, or FALSE otherwise.
|
||||
*/
|
||||
public function stream_eof() {
|
||||
return feof( $this->handle );
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes the output.
|
||||
*
|
||||
* @return bool TRUE if the cached data was successfully stored (or if there was no data to store), or FALSE if the data could not be stored.
|
||||
*/
|
||||
public function stream_flush() {
|
||||
return fflush( $this->handle );
|
||||
}
|
||||
|
||||
/**
|
||||
* Advisory file locking.
|
||||
*
|
||||
* @param int $operation LOCK_SH, LOCK_EX, LOCK_UN, or LOCK_NB.
|
||||
*
|
||||
* @return bool TRUE on success or FALSE on failure.
|
||||
*/
|
||||
public function stream_lock( $operation ) {
|
||||
return $operation
|
||||
? flock( $this->handle, $operation )
|
||||
: true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change stream metadata.
|
||||
*
|
||||
* @param string $path The file path or URL to set metadata. Note that in the case of a URL, it must be a :// delimited URL. Other URL forms are not supported.
|
||||
* @param int $option STREAM_META_TOUCH, STREAM_META_OWNER_NAME, STREAM_META_OWNER, STREAM_META_GROUP_NAME, STREAM_META_GROUP, or STREAM_META_ACCESS.
|
||||
* @param mixed $value Depends on $option.
|
||||
*
|
||||
* @return bool TRUE on success or FALSE on failure. If option is not implemented, FALSE should be returned.
|
||||
*/
|
||||
public function stream_metadata( $path, $option, $value ) {
|
||||
switch ( $option ) {
|
||||
case STREAM_META_TOUCH:
|
||||
$value += array( null, null );
|
||||
return $this->native( 'touch', $path, $value[0], $value[1] );
|
||||
case STREAM_META_OWNER_NAME:
|
||||
case STREAM_META_OWNER:
|
||||
return $this->native( 'chown', $path, $value );
|
||||
case STREAM_META_GROUP_NAME:
|
||||
case STREAM_META_GROUP:
|
||||
return $this->native( 'chgrp', $path, $value );
|
||||
case STREAM_META_ACCESS:
|
||||
return $this->native( 'chmod', $path, $value );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens file or URL. Note that this is where the hacking actually happens.
|
||||
*
|
||||
* @param string $path Specifies the URL that was passed to the original function.
|
||||
* @param string $mode The mode used to open the file, as detailed for fopen().
|
||||
* @param int $options Holds additional flags set by the streams API: STREAM_USE_PATH, STREAM_REPORT_ERRORS.
|
||||
* @param string $opened_path If the path is opened successfully, and STREAM_USE_PATH is set in options, opened_path should be set to the full path of the file/resource that was actually opened.
|
||||
*
|
||||
* @return bool TRUE on success or FALSE on failure.
|
||||
*/
|
||||
public function stream_open( $path, $mode, $options, &$opened_path ) {
|
||||
$use_path = (bool) ( $options & STREAM_USE_PATH );
|
||||
if ( 'rb' === $mode && self::path_in_list_of_paths_to_hack( $path ) && 'php' === pathinfo( $path, PATHINFO_EXTENSION ) ) {
|
||||
$content = $this->native( 'file_get_contents', $path, $use_path, $this->context );
|
||||
if ( false === $content ) {
|
||||
return false;
|
||||
}
|
||||
$modified = self::hack( $content, $path );
|
||||
if ( $modified !== $content ) {
|
||||
$this->handle = tmpfile();
|
||||
$this->native( 'fwrite', $this->handle, $modified );
|
||||
$this->native( 'fseek', $this->handle, 0 );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
$this->handle = $this->context
|
||||
? $this->native( 'fopen', $path, $mode, $use_path, $this->context )
|
||||
: $this->native( 'fopen', $path, $mode, $use_path );
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read from stream.
|
||||
*
|
||||
* @param int $count How many bytes of data from the current position should be returned.
|
||||
*
|
||||
* @return false|string If there are less than count bytes available, return as many as are available. If no more data is available, return either FALSE or an empty string.
|
||||
*/
|
||||
public function stream_read( $count ) {
|
||||
return fread( $this->handle, $count );
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeks to specific location in a stream.
|
||||
*
|
||||
* @param int $offset The stream offset to seek to.
|
||||
* @param int $whence SEEK_SET, SEEK_CUR, or SEEK_END.
|
||||
*
|
||||
* @return bool TRUE if the position was updated, FALSE otherwise.
|
||||
*/
|
||||
public function stream_seek( $offset, $whence = SEEK_SET ) {
|
||||
return fseek( $this->handle, $offset, $whence ) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change stream options.
|
||||
*
|
||||
* @param int $option STREAM_OPTION_BLOCKING, STREAM_OPTION_READ_TIMEOUT, or STREAM_OPTION_WRITE_BUFFER.
|
||||
* @param int $arg1 Depends on $option.
|
||||
* @param int $arg2 Depends on $option.
|
||||
*/
|
||||
public function stream_set_option( $option, $arg1, $arg2 ) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve information about a file resource.
|
||||
*
|
||||
* @return array See stat().
|
||||
*/
|
||||
public function stream_stat() {
|
||||
return fstat( $this->handle );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current position of a stream.
|
||||
*
|
||||
* @return false|int The current position of the stream.
|
||||
*/
|
||||
public function stream_tell() {
|
||||
return ftell( $this->handle );
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate stream.
|
||||
*
|
||||
* @param int $new_size The new size.
|
||||
*
|
||||
* @return bool TRUE on success or FALSE on failure.
|
||||
*/
|
||||
public function stream_truncate( $new_size ) {
|
||||
return ftruncate( $this->handle, $new_size );
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to stream.
|
||||
*
|
||||
* @param string $data Should be stored into the underlying stream.
|
||||
*
|
||||
* @return false|int The number of bytes that were successfully stored, or 0 if none could be stored.
|
||||
*/
|
||||
public function stream_write( $data ) {
|
||||
return fwrite( $this->handle, $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file.
|
||||
*
|
||||
* @param string $path The file URL which should be deleted.
|
||||
*
|
||||
* @return bool TRUE on success or FALSE on failure.
|
||||
*/
|
||||
public function unlink( $path ) {
|
||||
return $this->native( 'unlink', $path );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve information about a file.
|
||||
*
|
||||
* @param string $path The file path or URL to stat. Note that in the case of a URL, it must be a :// delimited URL. Other URL forms are not supported.
|
||||
* @param int $flags Holds additional flags set by the streams API. It can hold one or more of the following values OR'd together.
|
||||
*
|
||||
* @return mixed Should return as many elements as stat() does. Unknown or unavailable values should be set to a rational value (usually 0). Pay special attention to mode as documented under stat().
|
||||
*/
|
||||
public function url_stat( $path, $flags ) {
|
||||
$func = $flags & STREAM_URL_STAT_LINK ? 'lstat' : 'stat';
|
||||
return $flags & STREAM_URL_STAT_QUIET
|
||||
? @$this->native( $func, $path )
|
||||
: $this->native( $func, $path );
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a native PHP function.
|
||||
*
|
||||
* @param string $func Name of the function to execute. Pass the arguments for the PHP function after this one.
|
||||
*
|
||||
* @return mixed Return value from the native PHP function.
|
||||
*/
|
||||
private function native( $func ) {
|
||||
stream_wrapper_restore( self::PROTOCOL );
|
||||
$res = call_user_func_array( $func, array_slice( func_get_args(), 1 ) );
|
||||
stream_wrapper_unregister( self::PROTOCOL );
|
||||
stream_wrapper_register( self::PROTOCOL, __CLASS__ );
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the reigstered hacks to the contents of a file.
|
||||
*
|
||||
* @param string $code Code content to hack.
|
||||
* @param string $path Path of the file being hacked.
|
||||
*
|
||||
* @return string The code after applying all the registered hacks.
|
||||
*/
|
||||
private static function hack( $code, $path ) {
|
||||
foreach ( self::$hacks as $hack ) {
|
||||
if ( is_callable( $hack ) ) {
|
||||
$code = call_user_func( $hack, $code, $path );
|
||||
} else {
|
||||
$code = $hack->hack( $code, $path );
|
||||
}
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file path is in the white list.
|
||||
*
|
||||
* @param string $path File path to check.
|
||||
*
|
||||
* @return bool TRUE if there's an entry in the white list that ends with $path, FALSE otherwise.
|
||||
*
|
||||
* @throws \Exception The class is not initialized.
|
||||
*/
|
||||
private static function path_in_list_of_paths_to_hack( $path ) {
|
||||
if ( empty( self::$paths_with_files_to_hack ) ) {
|
||||
throw new \Exception( "CodeHacker is not initialized, it must initialized by invoking 'initialize'" );
|
||||
}
|
||||
foreach ( self::$paths_with_files_to_hack as $white_list_item ) {
|
||||
if ( substr( $path, 0, strlen( $white_list_item ) ) === $white_list_item ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//phpcs:enable WordPress.WP.AlternativeFunctions, WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
/**
|
||||
* CodeHackerTestHook class file.
|
||||
*
|
||||
* @package WooCommerce/Testing
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Testing\Tools\CodeHacking;
|
||||
|
||||
use PHPUnit\Runner\BeforeTestHook;
|
||||
|
||||
/**
|
||||
* Helper to use the CodeHacker class in PHPUnit. To use, add this to phpunit.xml:
|
||||
*
|
||||
* <extensions>
|
||||
* <extension class="CodeHackerTestHook" />
|
||||
* </extensions>
|
||||
*/
|
||||
final class CodeHackerTestHook implements BeforeTestHook {
|
||||
|
||||
/**
|
||||
* Runs before each test.
|
||||
*
|
||||
* @param string $test "TestClass::TestMethod".
|
||||
*
|
||||
* @throws \ReflectionException Thrown by execute_before_methods.
|
||||
*/
|
||||
public function executeBeforeTest( string $test ): void {
|
||||
CodeHacker::reset_hacks();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
/**
|
||||
* BypassFinalsHack class file.
|
||||
*
|
||||
* @package WooCommerce/Testing
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Testing\Tools\CodeHacking\Hacks;
|
||||
|
||||
/**
|
||||
* Code hack to bypass finals.
|
||||
*
|
||||
* Removes all the "final" keywords from class definitions.
|
||||
*/
|
||||
final class BypassFinalsHack extends CodeHack {
|
||||
|
||||
/**
|
||||
* Hacks code by removing "final" keywords from class definitions.
|
||||
*
|
||||
* @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 ) {
|
||||
if ( stripos( $code, 'final' ) !== false ) {
|
||||
$tokens = $this->tokenize( $code );
|
||||
$code = '';
|
||||
foreach ( $tokens as $token ) {
|
||||
$code .= $this->is_token_of_type( $token, T_FINAL ) ? '' : $this->token_to_string( $token );
|
||||
}
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert the hack to its initial state - nothing to do since finals can't be reverted.
|
||||
*/
|
||||
public function reset() {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
/**
|
||||
* CodeHack class file.
|
||||
*
|
||||
* @package WooCommerce/Testing
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Testing\Tools\CodeHacking\Hacks;
|
||||
|
||||
/**
|
||||
* Base class to define Hacks for CodeHacker.
|
||||
*
|
||||
* This class is included for convenience only, any class having a 'public function hack($code, $path)'
|
||||
* can be used as a hack class for CodeHacker.
|
||||
*/
|
||||
abstract class CodeHack {
|
||||
|
||||
/**
|
||||
* The hack method to implement.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
abstract public function hack( $code, $path);
|
||||
|
||||
/**
|
||||
* Revert the hack to its initial state.
|
||||
*/
|
||||
abstract public function reset();
|
||||
|
||||
/**
|
||||
* Tokenize PHP source code.
|
||||
*
|
||||
* @param string $code PHP code to tokenize.
|
||||
* @return array Tokenized code.
|
||||
* @throws \Exception PHP version is less than 7.0.
|
||||
*/
|
||||
protected function tokenize( $code ) {
|
||||
if ( PHP_VERSION_ID < 70000 ) {
|
||||
throw new \Exception( 'The code hacker can be used in PHP 7.0+ only.' );
|
||||
}
|
||||
|
||||
return token_get_all( $code, TOKEN_PARSE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a token is of a given type.
|
||||
*
|
||||
* @param mixed $token Token to check.
|
||||
* @param int $type Type of token to check (see https://www.php.net/manual/en/tokens.php).
|
||||
* @return bool True if it's a token of the given type, false otherwise.
|
||||
*/
|
||||
protected function is_token_of_type( $token, $type ) {
|
||||
return is_array( $token ) && $type === $token[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the type of a given token.
|
||||
*
|
||||
* @param mixed $token Token to check.
|
||||
* @return mixed|null Type of token (see https://www.php.net/manual/en/tokens.php), or null if it's a character.
|
||||
*/
|
||||
protected function token_type_of( $token ) {
|
||||
return is_array( $token ) ? $token[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a token to its string representation.
|
||||
*
|
||||
* @param mixed $token Token to convert.
|
||||
* @return mixed String representation of the token.
|
||||
*/
|
||||
protected function token_to_string( $token ) {
|
||||
return is_array( $token ) ? $token[1] : $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string ends with a certain substring.
|
||||
* This method is added to help processing path names within 'hack' methods needing to do so.
|
||||
*
|
||||
* @param string $haystack The string to search in.
|
||||
* @param string $needle The substring to search for.
|
||||
* @return bool True if the $haystack ends with $needle, false otherwise.
|
||||
*/
|
||||
protected function string_ends_with( $haystack, $needle ) {
|
||||
$length = strlen( $needle );
|
||||
if ( 0 === $length ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return ( substr( $haystack, -$length ) === $needle );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
/**
|
||||
* FunctionsMockerHack class file.
|
||||
*
|
||||
* @package WooCommerce/Testing
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Testing\Tools\CodeHacking\Hacks;
|
||||
|
||||
use ReflectionMethod;
|
||||
use ReflectionClass;
|
||||
|
||||
/**
|
||||
* Hack to mock standalone functions.
|
||||
*
|
||||
* How to use:
|
||||
*
|
||||
* 1. Invoke 'FunctionsMockerHack::initialize' once, passing an array with the names of the functions
|
||||
* that can be mocked.
|
||||
*
|
||||
* 2. Invoke 'CodeHacker::add_hack(FunctionsMockerHack::get_hack_instance())' once.
|
||||
*
|
||||
* 3. Use 'add_function_mocks' in tests as needed to register callbacks to be executed instead of the functions, e.g.:
|
||||
*
|
||||
* FunctionsMockerHack::add_function_mocks([
|
||||
* 'get_option' => 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 = array_merge( $this->function_mocks, $mocks );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
<?php
|
||||
/**
|
||||
* StaticMockerHack class file.
|
||||
*
|
||||
* @package WooCommerce/Testing
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Testing\Tools\CodeHacking\Hacks;
|
||||
|
||||
/**
|
||||
* Hack to mock public static methods and properties.
|
||||
*
|
||||
* How to use:
|
||||
*
|
||||
* 1. Invoke 'StaticMockerHack::initialize' once, passing an array with the names of the classes
|
||||
* that can be mocked.
|
||||
*
|
||||
* 2. Invoke 'CodeHacker::add_hack(StaticMockerHack::get_hack_instance())' once.
|
||||
*
|
||||
* 3. Use 'add_method_mocks' in tests as needed to register callbacks to be executed instead of the functions, e.g.:
|
||||
*
|
||||
* StaticMockerHack::add_method_mocks(
|
||||
* [
|
||||
* 'SomeClass' => [
|
||||
* '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 = array_merge_recursive( $this->method_mocks, $mocks );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
class WC_Admin_Foobar {
|
||||
public function do_something_that_depends_on_an_option() {
|
||||
return 'The option returns: ' . get_option('some_option', 'default option value');
|
||||
}
|
||||
|
||||
public function do_something_that_depends_on_the_legacy_service( $what ) {
|
||||
return 'The legacy service returns: ' . WC_Some_Legacy_Service::do_something( $what );
|
||||
}
|
||||
}
|
||||
|
||||
class WC_Some_Legacy_Service {
|
||||
public static function do_something( $what ) {
|
||||
return "The legacy service does something with: " . $what;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This class has two bits that are difficult to unit test: the call to `WC_Some_Legacy_Service::do_something` (let's assume that we can't refactor that one) and the `get_option` invocation. Let's see how the code hacker can help us with that.
|
||||
|
||||
Now, modify `tests/legacy/mockable-functions.php` so that the returned array contains `'get_option'` (if it doesn't already), and modify `tests/legacy/classes-with-mockable-static-methods.php` so that the returned array contains `WC_Some_Legacy_Service`.
|
||||
|
||||
Create a file named `class-wc-tests-admin-foobar.php` in `tests/unit-tests/admin` with this code:
|
||||
|
||||
```
|
||||
<?php
|
||||
|
||||
use Automattic\WooCommerce\Testing\Tools\CodeHacking\Hacks\FunctionsMockerHack;
|
||||
use Automattic\WooCommerce\Testing\Tools\CodeHacking\Hacks\StaticMockerHack;
|
||||
|
||||
class WC_Tests_Admin_Foobar extends WC_Unit_Test_Case {
|
||||
public function test_functions_mocking() {
|
||||
$tested = new WC_Admin_Foobar();
|
||||
|
||||
FunctionsMockerHack::add_function_mocks([
|
||||
'get_option' => 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:
|
||||
|
||||
```
|
||||
//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:
|
||||
|
||||
```
|
||||
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.
|
|
@ -0,0 +1,163 @@
|
|||
/**
|
||||
* script to build packages into `build/` directory.
|
||||
*
|
||||
* Example:
|
||||
* node ./bin/packages/build.js
|
||||
*/
|
||||
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
const fs = require( 'fs' );
|
||||
const path = require( 'path' );
|
||||
const glob = require( 'glob' );
|
||||
const babel = require( '@babel/core' );
|
||||
const chalk = require( 'chalk' );
|
||||
const mkdirp = require( 'mkdirp' );
|
||||
const deasync = require( 'deasync' );
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
const getPackages = require( './get-packages' );
|
||||
const getBabelConfig = require( './get-babel-config' );
|
||||
|
||||
/**
|
||||
* Module Constants
|
||||
*/
|
||||
const PACKAGES_DIR = path.resolve( __dirname, '../' );
|
||||
const SRC_DIR = 'src';
|
||||
const BUILD_DIR = {
|
||||
main: 'build',
|
||||
module: 'build-module',
|
||||
};
|
||||
const DONE = chalk.reset.inverse.bold.green( ' DONE ' );
|
||||
|
||||
/**
|
||||
* Get the package name for a specified file
|
||||
*
|
||||
* @param {string} file File name
|
||||
* @return {string} Package name
|
||||
*/
|
||||
function getPackageName( file ) {
|
||||
return path.relative( PACKAGES_DIR, file ).split( path.sep )[ 0 ];
|
||||
}
|
||||
|
||||
const isJsFile = ( filepath ) => {
|
||||
return /.\.js$/.test( filepath );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Build Path for a specified file
|
||||
*
|
||||
* @param {string} file File to build
|
||||
* @param {string} buildFolder Output folder
|
||||
* @return {string} Build path
|
||||
*/
|
||||
function getBuildPath( file, buildFolder ) {
|
||||
const pkgName = getPackageName( file );
|
||||
const pkgSrcPath = path.resolve( PACKAGES_DIR, pkgName, SRC_DIR );
|
||||
const pkgBuildPath = path.resolve( PACKAGES_DIR, pkgName, buildFolder );
|
||||
const relativeToSrcPath = path.relative( pkgSrcPath, file );
|
||||
return path.resolve( pkgBuildPath, relativeToSrcPath );
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of scss and js filepaths, divide them into sets them and rebuild.
|
||||
*
|
||||
* @param {Array} files list of files to rebuild
|
||||
*/
|
||||
function buildFiles( files ) {
|
||||
// Reduce files into a unique sets of javaScript files and scss packages.
|
||||
const buildPaths = files.reduce(
|
||||
( accumulator, filePath ) => {
|
||||
if ( isJsFile( filePath ) ) {
|
||||
accumulator.jsFiles.add( filePath );
|
||||
}
|
||||
return accumulator;
|
||||
},
|
||||
{ jsFiles: new Set() }
|
||||
);
|
||||
|
||||
buildPaths.jsFiles.forEach( buildJsFile );
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a javaScript file for the required environments (node and ES5)
|
||||
*
|
||||
* @param {string} file File path to build
|
||||
* @param {boolean} silent Show logs
|
||||
*/
|
||||
function buildJsFile( file, silent ) {
|
||||
buildJsFileFor( file, silent, 'main' );
|
||||
buildJsFileFor( file, silent, 'module' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a file for a specific environment
|
||||
*
|
||||
* @param {string} file File path to build
|
||||
* @param {boolean} silent Show logs
|
||||
* @param {string} environment Dist environment (node or es5)
|
||||
*/
|
||||
function buildJsFileFor( file, silent, environment ) {
|
||||
const buildDir = BUILD_DIR[ environment ];
|
||||
const destPath = getBuildPath( file, buildDir );
|
||||
const babelOptions = getBabelConfig( environment );
|
||||
babelOptions.sourceMaps = true;
|
||||
babelOptions.sourceFileName = file;
|
||||
|
||||
mkdirp.sync( path.dirname( destPath ) );
|
||||
const transformed = babel.transformFileSync( file, babelOptions );
|
||||
fs.writeFileSync( destPath + '.map', JSON.stringify( transformed.map ) );
|
||||
fs.writeFileSync(
|
||||
destPath,
|
||||
transformed.code +
|
||||
'\n//# sourceMappingURL=' +
|
||||
path.basename( destPath ) +
|
||||
'.map'
|
||||
);
|
||||
|
||||
if ( ! silent ) {
|
||||
process.stdout.write(
|
||||
chalk.green( ' \u2022 ' ) +
|
||||
path.relative( PACKAGES_DIR, file ) +
|
||||
chalk.green( ' \u21D2 ' ) +
|
||||
path.relative( PACKAGES_DIR, destPath ) +
|
||||
'\n'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the provided package path
|
||||
*
|
||||
* @param {string} packagePath absolute package path
|
||||
*/
|
||||
function buildPackage( packagePath ) {
|
||||
const srcDir = path.resolve( packagePath, SRC_DIR );
|
||||
const jsFiles = glob.sync( `${ srcDir }/**/*.js`, {
|
||||
ignore: [
|
||||
`${ srcDir }/**/test/**/*.js`,
|
||||
`${ srcDir }/**/__mocks__/**/*.js`,
|
||||
],
|
||||
nodir: true,
|
||||
} );
|
||||
|
||||
process.stdout.write( `${ path.basename( packagePath ) }\n` );
|
||||
|
||||
// Build js files individually.
|
||||
jsFiles.forEach( ( file ) => buildJsFile( file, true ) );
|
||||
|
||||
process.stdout.write( `${ DONE }\n` );
|
||||
}
|
||||
|
||||
const files = process.argv.slice( 2 );
|
||||
|
||||
if ( files.length ) {
|
||||
buildFiles( files );
|
||||
} else {
|
||||
process.stdout.write( chalk.inverse( '>> Building packages \n' ) );
|
||||
getPackages().forEach( buildPackage );
|
||||
process.stdout.write( '\n' );
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
const { get, map } = require( 'lodash' );
|
||||
const babel = require( '@babel/core' );
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
const { options: babelDefaultConfig } = babel.loadPartialConfig( {
|
||||
configFile: '@wordpress/babel-preset-default',
|
||||
} );
|
||||
const plugins = babelDefaultConfig.plugins;
|
||||
if ( ! process.env.SKIP_JSX_PRAGMA_TRANSFORM ) {
|
||||
plugins.push( [ '@wordpress/babel-plugin-import-jsx-pragma', {
|
||||
scopeVariable: 'createElement',
|
||||
source: '@wordpress/element',
|
||||
isDefault: false,
|
||||
} ] );
|
||||
}
|
||||
|
||||
const overrideOptions = ( target, targetName, options ) => {
|
||||
if ( get( target, [ 'file', 'request' ] ) === targetName ) {
|
||||
return [ targetName, Object.assign(
|
||||
{},
|
||||
target.options,
|
||||
options
|
||||
) ];
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
||||
const babelConfigs = {
|
||||
main: Object.assign(
|
||||
{},
|
||||
babelDefaultConfig,
|
||||
{
|
||||
plugins,
|
||||
presets: map(
|
||||
babelDefaultConfig.presets,
|
||||
( preset ) => overrideOptions( preset, '@babel/preset-env', {
|
||||
modules: 'commonjs',
|
||||
} )
|
||||
),
|
||||
}
|
||||
),
|
||||
module: Object.assign(
|
||||
{},
|
||||
babelDefaultConfig,
|
||||
{
|
||||
plugins: map(
|
||||
plugins,
|
||||
( plugin ) => overrideOptions( plugin, '@babel/plugin-transform-runtime', {
|
||||
useESModules: true,
|
||||
} )
|
||||
),
|
||||
presets: map(
|
||||
babelDefaultConfig.presets,
|
||||
( preset ) => overrideOptions( preset, '@babel/preset-env', {
|
||||
modules: false,
|
||||
} )
|
||||
),
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
function getBabelConfig( environment ) {
|
||||
return babelConfigs[ environment ];
|
||||
}
|
||||
|
||||
module.exports = getBabelConfig;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue